mirror of
https://github.com/cyanfish/naps2.git
synced 2024-10-05 20:07:42 +03:00
parent
0c57cd167f
commit
484941c7ad
@ -1,4 +1,3 @@
|
||||
using System.Threading;
|
||||
using NAPS2.App.Tests.Targets;
|
||||
using NAPS2.Remoting;
|
||||
using NAPS2.Sdk.Tests;
|
||||
@ -23,8 +22,8 @@ public class GuiAppTests : ContextualTests
|
||||
}
|
||||
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);
|
||||
|
@ -2,9 +2,9 @@ using NAPS2.EtoForms;
|
||||
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;
|
||||
@ -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()
|
||||
{
|
||||
@ -61,6 +62,8 @@ public class DesktopControllerTests : ContextualTests
|
||||
_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,
|
||||
@ -82,6 +85,7 @@ public class DesktopControllerTests : ContextualTests
|
||||
_desktopFormProvider,
|
||||
_scannedImagePrinter,
|
||||
_sharedDeviceManager,
|
||||
_processCoordinator,
|
||||
new RecoveryManager(ScanningContext)
|
||||
);
|
||||
|
||||
@ -262,4 +266,28 @@ public class DesktopControllerTests : ContextualTests
|
||||
_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");
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
|
||||
@ -104,7 +107,8 @@ public class WindowsApplicationLifecycle : ApplicationLifecycle
|
||||
}
|
||||
}
|
||||
|
||||
_shouldCreateEventSource = args.Any(x => x.Equals("/CreateEventSource", StringComparison.InvariantCultureIgnoreCase));
|
||||
_shouldCreateEventSource =
|
||||
args.Any(x => x.Equals("/CreateEventSource", StringComparison.InvariantCultureIgnoreCase));
|
||||
if (_shouldCreateEventSource)
|
||||
{
|
||||
try
|
||||
@ -165,8 +169,8 @@ public class WindowsApplicationLifecycle : ApplicationLifecycle
|
||||
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);
|
||||
@ -177,21 +181,39 @@ public class WindowsApplicationLifecycle : ApplicationLifecycle
|
||||
// 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)
|
||||
{
|
||||
|
@ -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;
|
||||
@ -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();
|
||||
|
||||
@ -50,7 +53,7 @@ public class DesktopController
|
||||
DialogHelper dialogHelper,
|
||||
DesktopImagesController desktopImagesController, IDesktopScanController desktopScanController,
|
||||
DesktopFormProvider desktopFormProvider, IScannedImagePrinter scannedImagePrinter,
|
||||
ISharedDeviceManager sharedDeviceManager, RecoveryManager recoveryManager)
|
||||
ISharedDeviceManager sharedDeviceManager, ProcessCoordinator processCoordinator, RecoveryManager recoveryManager)
|
||||
{
|
||||
_scanningContext = scanningContext;
|
||||
_imageList = imageList;
|
||||
@ -70,6 +73,7 @@ public class DesktopController
|
||||
_desktopFormProvider = desktopFormProvider;
|
||||
_scannedImagePrinter = scannedImagePrinter;
|
||||
_sharedDeviceManager = sharedDeviceManager;
|
||||
_processCoordinator = processCoordinator;
|
||||
_recoveryManager = recoveryManager;
|
||||
}
|
||||
|
||||
@ -87,9 +91,10 @@ public class DesktopController
|
||||
if (_initialized) return;
|
||||
_initialized = true;
|
||||
_sharedDeviceManager.StartSharing();
|
||||
StartPipesServer();
|
||||
StartProcessCoordinator();
|
||||
ShowStartupMessages();
|
||||
ShowRecoveryPrompt();
|
||||
ImportFilesFromCommandLine();
|
||||
InitThumbnailRendering();
|
||||
await RunStillImageEvents();
|
||||
SetFirstRunDate();
|
||||
@ -168,7 +173,7 @@ public class DesktopController
|
||||
public void Cleanup()
|
||||
{
|
||||
if (_suspended) return;
|
||||
Pipes.KillServer();
|
||||
_processCoordinator.KillServer();
|
||||
_sharedDeviceManager.StopSharing();
|
||||
if (!SkipRecoveryCleanup && !_config.Get(c => c.KeepSession))
|
||||
{
|
||||
@ -256,44 +261,10 @@ public class DesktopController
|
||||
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()
|
||||
@ -345,18 +316,33 @@ public class DesktopController
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -537,4 +523,57 @@ public class DesktopController
|
||||
{
|
||||
_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());
|
||||
}
|
||||
}
|
||||
}
|
@ -49,10 +49,6 @@ public class EtoOperationProgress : OperationProgress
|
||||
{
|
||||
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(() =>
|
||||
@ -73,10 +69,6 @@ public class EtoOperationProgress : OperationProgress
|
||||
{
|
||||
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));
|
||||
|
@ -2,15 +2,18 @@ namespace NAPS2.EtoForms.Notifications;
|
||||
|
||||
public class NotificationManager
|
||||
{
|
||||
public NotificationManager(ColorScheme colorScheme)
|
||||
public NotificationManager(ColorScheme colorScheme, Naps2Config config)
|
||||
{
|
||||
ColorScheme = colorScheme;
|
||||
Config = config;
|
||||
}
|
||||
|
||||
public List<NotificationModel> Notifications { get; } = [];
|
||||
|
||||
public ColorScheme ColorScheme { get; }
|
||||
|
||||
public Naps2Config Config { get; }
|
||||
|
||||
public event EventHandler? Updated;
|
||||
|
||||
public event EventHandler? TimersStarting;
|
||||
|
@ -60,7 +60,11 @@ public class ProgressNotificationView : NotificationView
|
||||
|
||||
protected override void NotificationClicked()
|
||||
{
|
||||
Manager!.Hide(Model);
|
||||
var bgOps = Manager!.Config.Get(c => c.BackgroundOperations);
|
||||
bgOps = bgOps.Remove(_op.GetType().Name);
|
||||
Manager.Config.User.Set(c => c.BackgroundOperations, bgOps);
|
||||
|
||||
Manager.Hide(Model);
|
||||
_operationProgress.ShowModalProgress(_op);
|
||||
}
|
||||
|
||||
|
@ -28,6 +28,7 @@ public class ProgressForm : EtoDialogBase
|
||||
protected override void BuildLayout()
|
||||
{
|
||||
FormStateController.RestoreFormState = false;
|
||||
FormStateController.SaveFormState = false;
|
||||
|
||||
LayoutController.Content = L.Column(
|
||||
_status,
|
||||
@ -113,6 +114,10 @@ public class ProgressForm : EtoDialogBase
|
||||
|
||||
private void RunInBg_Click(object? sender, EventArgs e)
|
||||
{
|
||||
var bgOps = Config.Get(c => c.BackgroundOperations);
|
||||
bgOps = bgOps.Add(Operation.GetType().Name);
|
||||
Config.User.Set(c => c.BackgroundOperations, bgOps);
|
||||
|
||||
_background = true;
|
||||
Close();
|
||||
}
|
||||
|
@ -8,6 +8,7 @@ using NAPS2.Ocr;
|
||||
using NAPS2.Pdf;
|
||||
using NAPS2.Platform.Windows;
|
||||
using NAPS2.Recovery;
|
||||
using NAPS2.Remoting;
|
||||
using NAPS2.Remoting.Server;
|
||||
using NAPS2.Remoting.Worker;
|
||||
using NAPS2.Scan;
|
||||
@ -46,6 +47,7 @@ public class CommonModule : Module
|
||||
ctx.Resolve<ScanningContext>(),
|
||||
ctx.Resolve<Naps2Config>(),
|
||||
Path.Combine(Paths.AppData, "sharing.xml"))).SingleInstance();
|
||||
builder.RegisterInstance(ProcessCoordinator.CreateDefault());
|
||||
|
||||
// Logging
|
||||
builder.Register<ILogger>(ctx =>
|
||||
|
@ -34,6 +34,7 @@
|
||||
<PackageReference Include="Autofac" Version="8.0.0" />
|
||||
<PackageReference Include="Ben.Demystifier" Version="0.4.1" />
|
||||
<PackageReference Include="CommandLineParser" Version="2.9.1" />
|
||||
<PackageReference Include="Grpc.Tools" Version="2.62.0" PrivateAssets="all" />
|
||||
<PackageReference Include="MimeKitLite" Version="4.4.0" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
||||
<PackageReference Include="NLog" Version="5.2.8" />
|
||||
@ -55,6 +56,8 @@
|
||||
<AssemblyAttribute Include="System.Runtime.CompilerServices.InternalsVisibleTo">
|
||||
<_Parameter1>NAPS2.Lib.Gtk</_Parameter1>
|
||||
</AssemblyAttribute>
|
||||
|
||||
<Protobuf Include="**/*.proto" Access="Public" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
@ -1,146 +0,0 @@
|
||||
using System.IO.Pipes;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
|
||||
namespace NAPS2.Remoting;
|
||||
|
||||
/// <summary>
|
||||
/// A class for simple inter-process communication between NAPS2 instances via named pipes.
|
||||
/// </summary>
|
||||
public static class Pipes
|
||||
{
|
||||
public const string MSG_SCAN_WITH_DEVICE = "SCAN_WDEV_";
|
||||
public const string MSG_ACTIVATE = "ACTIVATE";
|
||||
public const string MSG_KILL_PIPE_SERVER = "KILL_PIPE_SERVER";
|
||||
public const string MSG_CLOSE_WINDOW = "CLOSE_WINDOW";
|
||||
|
||||
// An arbitrary non-secret unique name with a single format argument (for the process ID).
|
||||
// This could be edtion/version-specific, but I like the idea that if the user is running a portable version and
|
||||
// happens to have NAPS2 installed too, the scan button will propagate to the portable version.
|
||||
private const string PIPE_NAME_FORMAT = "NAPS2_PIPE_v1_{0}";
|
||||
// The timeout is small since pipe connections should be on the local machine only.
|
||||
private const int TIMEOUT = 1000;
|
||||
|
||||
private static bool _serverRunning;
|
||||
|
||||
private static string GetPipeName(Process process)
|
||||
{
|
||||
return string.Format(PIPE_NAME_FORMAT, process.Id);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Send a message to a NAPS2 instance running a pipe server.
|
||||
/// </summary>
|
||||
/// <param name="recipient">The process to send the message to.</param>
|
||||
/// <param name="msg">The message to send.</param>
|
||||
public static bool SendMessage(Process recipient, string msg)
|
||||
{
|
||||
try
|
||||
{
|
||||
using var pipeClient = new NamedPipeClientStream(".", GetPipeName(recipient), PipeDirection.Out);
|
||||
//MessageBox.Show("Sending msg:" + msg);
|
||||
pipeClient.Connect(TIMEOUT);
|
||||
var streamString = new StreamString(pipeClient);
|
||||
streamString.WriteString(msg);
|
||||
//MessageBox.Show("Sent");
|
||||
return true;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.ErrorException("Error sending message through pipe", e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Start a pipe server on a background thread, calling the callback each time a message is received. Only one pipe server can be running per process.
|
||||
/// </summary>
|
||||
/// <param name="msgCallback">The message callback.</param>
|
||||
public static void StartServer(Action<string> msgCallback)
|
||||
{
|
||||
if (_serverRunning)
|
||||
{
|
||||
return;
|
||||
}
|
||||
var thread = new Thread(() =>
|
||||
{
|
||||
try
|
||||
{
|
||||
using var pipeServer = new NamedPipeServerStream(GetPipeName(Process.GetCurrentProcess()), PipeDirection.In);
|
||||
while (true)
|
||||
{
|
||||
pipeServer.WaitForConnection();
|
||||
var streamString = new StreamString(pipeServer);
|
||||
var msg = streamString.ReadString();
|
||||
//MessageBox.Show("Received msg:" + msg);
|
||||
if (msg == MSG_KILL_PIPE_SERVER)
|
||||
{
|
||||
break;
|
||||
}
|
||||
msgCallback(msg);
|
||||
pipeServer.Disconnect();
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.ErrorException("Error in named pipe server", ex);
|
||||
}
|
||||
_serverRunning = false;
|
||||
});
|
||||
_serverRunning = true;
|
||||
thread.Start();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Kills the pipe server background thread if one is running.
|
||||
/// </summary>
|
||||
public static void KillServer()
|
||||
{
|
||||
if (_serverRunning)
|
||||
{
|
||||
SendMessage(Process.GetCurrentProcess(), MSG_KILL_PIPE_SERVER);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// From https://msdn.microsoft.com/en-us/library/bb546085%28v=vs.110%29.aspx
|
||||
/// </summary>
|
||||
private class StreamString
|
||||
{
|
||||
private Stream _ioStream;
|
||||
private UnicodeEncoding _streamEncoding;
|
||||
|
||||
public StreamString(Stream ioStream)
|
||||
{
|
||||
_ioStream = ioStream;
|
||||
_streamEncoding = new UnicodeEncoding();
|
||||
}
|
||||
|
||||
public string ReadString()
|
||||
{
|
||||
int len;
|
||||
len = _ioStream.ReadByte() * 256;
|
||||
len += _ioStream.ReadByte();
|
||||
byte[] inBuffer = new byte[len];
|
||||
_ioStream.Read(inBuffer, 0, len);
|
||||
|
||||
return _streamEncoding.GetString(inBuffer);
|
||||
}
|
||||
|
||||
public int WriteString(string outString)
|
||||
{
|
||||
byte[] outBuffer = _streamEncoding.GetBytes(outString);
|
||||
int len = outBuffer.Length;
|
||||
if (len > UInt16.MaxValue)
|
||||
{
|
||||
len = (int)UInt16.MaxValue;
|
||||
}
|
||||
_ioStream.WriteByte((byte)(len / 256));
|
||||
_ioStream.WriteByte((byte)(len & 255));
|
||||
_ioStream.Write(outBuffer, 0, len);
|
||||
_ioStream.Flush();
|
||||
|
||||
return outBuffer.Length + 2;
|
||||
}
|
||||
}
|
||||
}
|
105
NAPS2.Lib/Remoting/ProcessCoordinator.cs
Normal file
105
NAPS2.Lib/Remoting/ProcessCoordinator.cs
Normal file
@ -0,0 +1,105 @@
|
||||
using System.Text;
|
||||
using GrpcDotNetNamedPipes;
|
||||
using static NAPS2.Remoting.ProcessCoordinatorService;
|
||||
|
||||
namespace NAPS2.Remoting;
|
||||
|
||||
/// <summary>
|
||||
/// Manages communication and coordination between multiple NAPS2 GUI processes. Specifically:
|
||||
/// - Allows sending messages to other NAPS2 processes via named pipes
|
||||
/// - Allows taking the SingleInstance lock (or checking which process currently owns it)
|
||||
/// This is different than the worker service - workers are owned by the parent process and are considered part of the
|
||||
/// same unit. Instead, this class handles the case where the user (or a system feature like StillImage) opens NAPS2
|
||||
/// twice.
|
||||
/// </summary>
|
||||
public class ProcessCoordinator(string instanceLockPath, string pipeNameFormat)
|
||||
{
|
||||
public static ProcessCoordinator CreateDefault() =>
|
||||
new(Path.Combine(Paths.AppData, "instance.lock"), "NAPS2_PIPE_v2_{0}");
|
||||
|
||||
private NamedPipeServer? _server;
|
||||
private FileStream? _instanceLock;
|
||||
|
||||
private string GetPipeName(Process process)
|
||||
{
|
||||
return string.Format(pipeNameFormat, process.Id);
|
||||
}
|
||||
|
||||
public void StartServer(ProcessCoordinatorServiceBase service)
|
||||
{
|
||||
_server = new NamedPipeServer(GetPipeName(Process.GetCurrentProcess()));
|
||||
ProcessCoordinatorService.BindService(_server.ServiceBinder, service);
|
||||
_server.Start();
|
||||
}
|
||||
|
||||
public void KillServer()
|
||||
{
|
||||
_server?.Kill();
|
||||
}
|
||||
|
||||
private ProcessCoordinatorServiceClient GetClient(Process recipient, int timeout) =>
|
||||
new(new NamedPipeChannel(".", GetPipeName(recipient),
|
||||
new NamedPipeChannelOptions { ConnectionTimeout = timeout }));
|
||||
|
||||
private bool TrySendMessage(Process recipient, int timeout, Action<ProcessCoordinatorServiceClient> send)
|
||||
{
|
||||
var client = GetClient(recipient, timeout);
|
||||
try
|
||||
{
|
||||
send(client);
|
||||
return true;
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public bool Activate(Process recipient, int timeout) =>
|
||||
TrySendMessage(recipient, timeout, client => client.Activate(new ActivateRequest()));
|
||||
|
||||
public bool CloseWindow(Process recipient, int timeout) =>
|
||||
TrySendMessage(recipient, timeout, client => client.CloseWindow(new CloseWindowRequest()));
|
||||
|
||||
public bool ScanWithDevice(Process recipient, int timeout, string device) =>
|
||||
TrySendMessage(recipient, timeout,
|
||||
client => client.ScanWithDevice(new ScanWithDeviceRequest { Device = device }));
|
||||
|
||||
public bool OpenFile(Process recipient, int timeout, string path) =>
|
||||
TrySendMessage(recipient, timeout,
|
||||
client => client.OpenFile(new OpenFileRequest { Path = { path } }));
|
||||
|
||||
public bool TryTakeInstanceLock()
|
||||
{
|
||||
if (_instanceLock != null)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
try
|
||||
{
|
||||
_instanceLock = new FileStream(instanceLockPath, FileMode.Create, FileAccess.ReadWrite, FileShare.Read);
|
||||
_instanceLock.SetLength(0);
|
||||
using var writer = new StreamWriter(_instanceLock, Encoding.UTF8, 1024, true);
|
||||
writer.WriteLine(Process.GetCurrentProcess().Id);
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public Process? GetProcessWithInstanceLock()
|
||||
{
|
||||
try
|
||||
{
|
||||
using var reader = new FileStream(instanceLockPath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
|
||||
var id = int.Parse(new StreamReader(reader).ReadLine()?.Trim() ?? "");
|
||||
return Process.GetProcessById(id);
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
26
NAPS2.Lib/Remoting/ProcessCoordinatorService.proto
Normal file
26
NAPS2.Lib/Remoting/ProcessCoordinatorService.proto
Normal file
@ -0,0 +1,26 @@
|
||||
syntax = "proto3";
|
||||
|
||||
package NAPS2.Remoting;
|
||||
|
||||
import "google/protobuf/empty.proto";
|
||||
|
||||
service ProcessCoordinatorService {
|
||||
rpc Activate (ActivateRequest) returns (google.protobuf.Empty) {}
|
||||
rpc CloseWindow (CloseWindowRequest) returns (google.protobuf.Empty) {}
|
||||
rpc ScanWithDevice (ScanWithDeviceRequest) returns (google.protobuf.Empty) {}
|
||||
rpc OpenFile (OpenFileRequest) returns (google.protobuf.Empty) {}
|
||||
}
|
||||
|
||||
message ActivateRequest {
|
||||
}
|
||||
|
||||
message CloseWindowRequest {
|
||||
}
|
||||
|
||||
message ScanWithDeviceRequest {
|
||||
string device = 1;
|
||||
}
|
||||
|
||||
message OpenFileRequest {
|
||||
repeated string path = 1;
|
||||
}
|
Loading…
Reference in New Issue
Block a user