Skip to content

Commit

Permalink
Rework cross-process coordination and support "Open With"
Browse files Browse the repository at this point in the history
  • Loading branch information
cyanfish committed Mar 30, 2024
1 parent 0c57cd1 commit 484941c
Show file tree
Hide file tree
Showing 13 changed files with 296 additions and 214 deletions.
5 changes: 2 additions & 3 deletions NAPS2.App.Tests/GuiAppTests.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
using System.Threading;
using NAPS2.App.Tests.Targets;
using NAPS2.Remoting;
using NAPS2.Sdk.Tests;
Expand All @@ -23,8 +22,8 @@ public void CreatesWindow(IAppTestTarget target)
}
else
{
Thread.Sleep(1000);
Assert.True(Pipes.SendMessage(process, Pipes.MSG_CLOSE_WINDOW));
var helper = ProcessCoordinator.CreateDefault();
Assert.True(helper.CloseWindow(process, 1000));
}
Assert.True(process.WaitForExit(5000));
AppTestHelper.AssertNoErrorLog(FolderPath);
Expand Down
30 changes: 29 additions & 1 deletion NAPS2.Lib.Tests/WinForms/DesktopControllerTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@
using NAPS2.EtoForms.Desktop;
using NAPS2.EtoForms.Notifications;
using NAPS2.ImportExport;
using NAPS2.ImportExport.Images;
using NAPS2.Platform.Windows;
using NAPS2.Recovery;
using NAPS2.Remoting;
using NAPS2.Remoting.Server;
using NAPS2.Remoting.Worker;
using NAPS2.Sdk.Tests;
Expand Down Expand Up @@ -39,6 +39,7 @@ public class DesktopControllerTests : ContextualTests
private readonly IScannedImagePrinter _scannedImagePrinter;
private readonly ThumbnailController _thumbnailController;
private readonly ISharedDeviceManager _sharedDeviceManager;
private readonly ProcessCoordinator _processCoordinator;

public DesktopControllerTests()
{
Expand All @@ -61,6 +62,8 @@ public DesktopControllerTests()
_scannedImagePrinter = Substitute.For<IScannedImagePrinter>();
_thumbnailController = new ThumbnailController(_thumbnailRenderQueue, _config);
_sharedDeviceManager = Substitute.For<ISharedDeviceManager>();
_processCoordinator =
new ProcessCoordinator(Path.Combine(FolderPath, "instance.lock"), Guid.NewGuid().ToString("D"));
ScanningContext.WorkerFactory = Substitute.For<IWorkerFactory>();
_desktopController = new DesktopController(
ScanningContext,
Expand All @@ -82,6 +85,7 @@ public DesktopControllerTests()
_desktopFormProvider,
_scannedImagePrinter,
_sharedDeviceManager,
_processCoordinator,
new RecoveryManager(ScanningContext)
);

Expand Down Expand Up @@ -262,4 +266,28 @@ public async Task Initialize_WithOldUpdateCheck_NotifiesOfUpdate()
_updateChecker.ReceivedCallsCount(1);
_notify.ReceivedCallsCount(1);
}

[Fact]
public async Task ProcessCoordinatorOpenFile()
{
var importOp = new ImportOperation(new FileImporter(ScanningContext));
_operationFactory.Create<ImportOperation>().Returns(importOp);
var path = CopyResourceToFile(ImageResources.dog, "test.jpg");

await _desktopController.Initialize();
Assert.True(_processCoordinator.OpenFile(Process.GetCurrentProcess(), 10000, path));
await Task.WhenAny(importOp.Success, Task.Delay(10000));

Assert.Single(_imageList.Images);
ImageAsserts.Similar(ImageResources.dog, _imageList.Images[0].GetClonedImage().Render());
}

[Fact]
public async Task ProcessCoordinatorScanWithDevice()
{
await _desktopController.Initialize();
Assert.True(_processCoordinator.ScanWithDevice(Process.GetCurrentProcess(), 10000, "abc"));

_ = _desktopScanController.Received().ScanWithDevice("abc");
}
}
46 changes: 34 additions & 12 deletions NAPS2.Lib.WinForms/Platform/Windows/WindowsApplicationLifecycle.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,18 @@ public class WindowsApplicationLifecycle : ApplicationLifecycle
{
private readonly StillImage _sti;
private readonly WindowsEventLogger _windowsEventLogger;
private readonly ProcessCoordinator _processCoordinator;
private readonly Naps2Config _config;

private bool _shouldCreateEventSource;
private int _returnCode;

public WindowsApplicationLifecycle(StillImage sti, WindowsEventLogger windowsEventLogger, Naps2Config config)
public WindowsApplicationLifecycle(StillImage sti, WindowsEventLogger windowsEventLogger,
ProcessCoordinator processCoordinator, Naps2Config config)
{
_sti = sti;
_windowsEventLogger = windowsEventLogger;
_processCoordinator = processCoordinator;
_config = config;
}

Expand Down Expand Up @@ -104,7 +107,8 @@ bool ElevationRequired(Action action)
}
}

_shouldCreateEventSource = args.Any(x => x.Equals("/CreateEventSource", StringComparison.InvariantCultureIgnoreCase));
_shouldCreateEventSource =
args.Any(x => x.Equals("/CreateEventSource", StringComparison.InvariantCultureIgnoreCase));
if (_shouldCreateEventSource)
{
try
Expand Down Expand Up @@ -165,8 +169,8 @@ public override void ExitIfRedundant()
foreach (var process in GetOtherNaps2Processes())
{
// Another instance of NAPS2 is running, so send it the "Scan" signal
ActivateProcess(process);
if (Pipes.SendMessage(process, Pipes.MSG_SCAN_WITH_DEVICE + _sti.DeviceID!))
SetMainWindowToForeground(process);
if (_processCoordinator.ScanWithDevice(process, 100, _sti.DeviceID!))
{
// Successful, so this instance can be closed before showing any UI
Environment.Exit(0);
Expand All @@ -177,21 +181,39 @@ public override void ExitIfRedundant()
// Only start one instance if configured for SingleInstance
if (_config.Get(c => c.SingleInstance))
{
// See if there's another NAPS2 process running
foreach (var process in GetOtherNaps2Processes())
if (!_processCoordinator.TryTakeInstanceLock())
{
// Another instance of NAPS2 is running, so send it the "Activate" signal
ActivateProcess(process);
if (Pipes.SendMessage(process, Pipes.MSG_ACTIVATE))
Log.Debug("Failed to get SingleInstance lock");
var process = _processCoordinator.GetProcessWithInstanceLock();
if (process != null)
{
// Successful, so this instance should be closed
Environment.Exit(0);
// Another instance of NAPS2 is running, so send it the "Activate" signal
Log.Debug($"Activating process {process.Id}");

// For new processes, wait until the process is at least 5 seconds old.
// This might be useful in cases where multiple NAPS2 processes are started at once, e.g. clicking
// to open a group of files associated with NAPS2.
int processAge = (DateTime.Now - process.StartTime).Milliseconds;
int timeout = (5000 - processAge).Clamp(100, 5000);

SetMainWindowToForeground(process);
bool ok = true;
if (Environment.GetCommandLineArgs() is [_, var arg] && File.Exists(arg))
{
Log.Debug($"Sending OpenFileRequest for {arg}");
ok = _processCoordinator.OpenFile(process, timeout, arg);
}
if (ok && _processCoordinator.Activate(process, timeout))
{
// Successful, so this instance should be closed
Environment.Exit(0);
}
}
}
}
}

private static void ActivateProcess(Process process)
private static void SetMainWindowToForeground(Process process)
{
if (process.MainWindowHandle != IntPtr.Zero)
{
Expand Down
123 changes: 81 additions & 42 deletions NAPS2.Lib/EtoForms/Desktop/DesktopController.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
using System.Threading;
using Eto.Drawing;
using Eto.Forms;
using Google.Protobuf.WellKnownTypes;
using Grpc.Core;
using NAPS2.EtoForms.Notifications;
using NAPS2.ImportExport;
using NAPS2.ImportExport.Images;
Expand Down Expand Up @@ -33,6 +35,7 @@ public class DesktopController
private readonly DesktopFormProvider _desktopFormProvider;
private readonly IScannedImagePrinter _scannedImagePrinter;
private readonly ISharedDeviceManager _sharedDeviceManager;
private readonly ProcessCoordinator _processCoordinator;
private readonly RecoveryManager _recoveryManager;
private readonly ImageTransfer _imageTransfer = new();

Expand All @@ -50,7 +53,7 @@ public DesktopController(ScanningContext scanningContext, UiImageList imageList,
DialogHelper dialogHelper,
DesktopImagesController desktopImagesController, IDesktopScanController desktopScanController,
DesktopFormProvider desktopFormProvider, IScannedImagePrinter scannedImagePrinter,
ISharedDeviceManager sharedDeviceManager, RecoveryManager recoveryManager)
ISharedDeviceManager sharedDeviceManager, ProcessCoordinator processCoordinator, RecoveryManager recoveryManager)
{
_scanningContext = scanningContext;
_imageList = imageList;
Expand All @@ -70,6 +73,7 @@ public DesktopController(ScanningContext scanningContext, UiImageList imageList,
_desktopFormProvider = desktopFormProvider;
_scannedImagePrinter = scannedImagePrinter;
_sharedDeviceManager = sharedDeviceManager;
_processCoordinator = processCoordinator;
_recoveryManager = recoveryManager;
}

Expand All @@ -87,9 +91,10 @@ public async Task Initialize()
if (_initialized) return;
_initialized = true;
_sharedDeviceManager.StartSharing();
StartPipesServer();
StartProcessCoordinator();
ShowStartupMessages();
ShowRecoveryPrompt();
ImportFilesFromCommandLine();
InitThumbnailRendering();
await RunStillImageEvents();
SetFirstRunDate();
Expand Down Expand Up @@ -168,7 +173,7 @@ private async Task RunStillImageEvents()
public void Cleanup()
{
if (_suspended) return;
Pipes.KillServer();
_processCoordinator.KillServer();
_sharedDeviceManager.StopSharing();
if (!SkipRecoveryCleanup && !_config.Get(c => c.KeepSession))
{
Expand Down Expand Up @@ -256,44 +261,10 @@ public bool PrepareForClosing(bool userClosing)
return true;
}

private void StartPipesServer()
private void StartProcessCoordinator()
{
// Receive messages from other processes
Pipes.StartServer(msg =>
{
if (msg.StartsWith(Pipes.MSG_SCAN_WITH_DEVICE, StringComparison.InvariantCulture))
{
Invoker.Current.Invoke(async () =>
await _desktopScanController.ScanWithDevice(msg.Substring(Pipes.MSG_SCAN_WITH_DEVICE.Length)));
}
if (msg.Equals(Pipes.MSG_ACTIVATE))
{
Invoker.Current.Invoke(() =>
{
// TODO: xplat
var formOnTop = Application.Instance.Windows.Last();
if (formOnTop.WindowState == WindowState.Minimized && PlatformCompat.System.CanUseWin32)
{
Win32.ShowWindow(formOnTop.NativeHandle, Win32.ShowWindowCommands.Restore);
}
formOnTop.BringToFront();
});
}
if (msg.Equals(Pipes.MSG_CLOSE_WINDOW))
{
Invoker.Current.Invoke(() =>
{
_desktopFormProvider.DesktopForm.Close();
#if NET6_0_OR_GREATER
if (OperatingSystem.IsMacOS())
{
// Closing the main window isn't enough to quit the app on Mac
Application.Instance.Quit();
}
#endif
});
}
});
// Receive messages from other NAPS2 processes
_processCoordinator.StartServer(new ProcessCoordinatorServiceImpl(this));
}

private void ShowStartupMessages()
Expand Down Expand Up @@ -345,18 +316,33 @@ private void ShowRecoveryPrompt()
}
}

private void ImportFilesFromCommandLine()
{
if (Environment.GetCommandLineArgs() is [_, var arg] && File.Exists(arg))
{
ImportFiles([arg]);
}
}

private void InitThumbnailRendering()
{
_thumbnailController.Init(_imageList);
}

public void ImportFiles(IEnumerable<string> files)
public void ImportFiles(ICollection<string> files, bool background = false)
{
var op = _operationFactory.Create<ImportOperation>();
if (op.Start(OrderFiles(files), _desktopImagesController.ReceiveScannedImage(),
new ImportParams { ThumbnailSize = _thumbnailController.RenderSize }))
{
_operationProgress.ShowProgress(op);
if (background)
{
_operationProgress.ShowBackgroundProgress(op);
}
else
{
_operationProgress.ShowProgress(op);
}
}
}

Expand Down Expand Up @@ -537,4 +523,57 @@ public void Resume()
{
_suspended = false;
}

private class ProcessCoordinatorServiceImpl(DesktopController controller) : ProcessCoordinatorService.ProcessCoordinatorServiceBase
{
public override Task<Empty> Activate(ActivateRequest request, ServerCallContext context)
{
Invoker.Current.Invoke(() =>
{
var formOnTop = Application.Instance.Windows.Last();
if (PlatformCompat.System.CanUseWin32)
{
if (formOnTop.WindowState == WindowState.Minimized)
{
Win32.ShowWindow(formOnTop.NativeHandle, Win32.ShowWindowCommands.Restore);
}
Win32.SetForegroundWindow(formOnTop.NativeHandle);
}
else
{
formOnTop.BringToFront();
}
});
return Task.FromResult(new Empty());
}

public override Task<Empty> CloseWindow(CloseWindowRequest request, ServerCallContext context)
{
Invoker.Current.Invoke(() =>
{
controller._desktopFormProvider.DesktopForm.Close();
#if NET6_0_OR_GREATER
if (OperatingSystem.IsMacOS())
{
// Closing the main window isn't enough to quit the app on Mac
Application.Instance.Quit();
}
#endif
});
return Task.FromResult(new Empty());
}

public override Task<Empty> OpenFile(OpenFileRequest request, ServerCallContext context)
{
controller.ImportFiles(request.Path, true);
return Task.FromResult(new Empty());
}

public override Task<Empty> ScanWithDevice(ScanWithDeviceRequest request, ServerCallContext context)
{
Invoker.Current.Invoke(async () =>
await controller._desktopScanController.ScanWithDevice(request.Device));
return Task.FromResult(new Empty());
}
}
}
8 changes: 0 additions & 8 deletions NAPS2.Lib/EtoForms/EtoOperationProgress.cs
Original file line number Diff line number Diff line change
Expand Up @@ -49,10 +49,6 @@ public override void ShowModalProgress(IOperation op)
{
Attach(op);

var bgOps = _config.Get(c => c.BackgroundOperations);
bgOps = bgOps.Remove(op.GetType().Name);
_config.User.Set(c => c.BackgroundOperations, bgOps);

if (!op.IsFinished)
{
Invoker.Current.Invoke(() =>
Expand All @@ -73,10 +69,6 @@ public override void ShowBackgroundProgress(IOperation op)
{
Attach(op);

var bgOps = _config.Get(c => c.BackgroundOperations);
bgOps = bgOps.Add(op.GetType().Name);
_config.User.Set(c => c.BackgroundOperations, bgOps);

if (!op.IsFinished)
{
Invoker.Current.Invoke(() => _notify.OperationProgress(this, op));
Expand Down
Loading

0 comments on commit 484941c

Please sign in to comment.