mirror of
https://github.com/cyanfish/naps2.git
synced 2024-10-05 20:07:42 +03:00
Linux: Support SingleInstance
This commit is contained in:
parent
319c374656
commit
426330ea13
@ -23,7 +23,7 @@ public class GuiAppTests : ContextualTests
|
||||
else
|
||||
{
|
||||
var helper = ProcessCoordinator.CreateDefault();
|
||||
Assert.True(helper.CloseWindow(process, 1000));
|
||||
Assert.True(helper.CloseWindow(process, 5000));
|
||||
}
|
||||
Assert.True(process.WaitForExit(5000));
|
||||
AppTestHelper.AssertNoErrorLog(FolderPath);
|
||||
|
@ -12,6 +12,7 @@ public class GtkModule : GuiModule
|
||||
{
|
||||
base.Load(builder);
|
||||
|
||||
builder.RegisterType<LinuxApplicationLifecycle>().As<ApplicationLifecycle>();
|
||||
builder.RegisterType<GtkScannedImagePrinter>().As<IScannedImagePrinter>();
|
||||
builder.RegisterType<GtkDarkModeProvider>().As<IDarkModeProvider>().SingleInstance();
|
||||
builder.RegisterType<LinuxServiceManager>().As<IOsServiceManager>();
|
||||
|
8
NAPS2.Lib.Gtk/Platform/LinuxApplicationLifecycle.cs
Normal file
8
NAPS2.Lib.Gtk/Platform/LinuxApplicationLifecycle.cs
Normal file
@ -0,0 +1,8 @@
|
||||
using NAPS2.Remoting;
|
||||
|
||||
namespace NAPS2.Platform;
|
||||
|
||||
public class LinuxApplicationLifecycle(ProcessCoordinator processCoordinator, Naps2Config config)
|
||||
: ApplicationLifecycle(processCoordinator, config)
|
||||
{
|
||||
}
|
@ -13,6 +13,7 @@ public class MacModule : GuiModule
|
||||
{
|
||||
base.Load(builder);
|
||||
|
||||
builder.RegisterType<MacApplicationLifecycle>().As<ApplicationLifecycle>();
|
||||
builder.RegisterType<MacScannedImagePrinter>().As<IScannedImagePrinter>();
|
||||
builder.RegisterType<AppleMailEmailProvider>().As<IAppleMailEmailProvider>();
|
||||
builder.RegisterType<MacDarkModeProvider>().As<IDarkModeProvider>().SingleInstance();
|
||||
|
13
NAPS2.Lib.Mac/Platform/MacApplicationLifecycle.cs
Normal file
13
NAPS2.Lib.Mac/Platform/MacApplicationLifecycle.cs
Normal file
@ -0,0 +1,13 @@
|
||||
using NAPS2.Remoting;
|
||||
|
||||
namespace NAPS2.Platform;
|
||||
|
||||
public class MacApplicationLifecycle(ProcessCoordinator processCoordinator, Naps2Config config)
|
||||
: ApplicationLifecycle(processCoordinator, config)
|
||||
{
|
||||
protected override void HandleSingleInstance()
|
||||
{
|
||||
// Mac is single-instance by default and doesn't need any special handling.
|
||||
// "Open With" also uses NSApplicationDelegate so we don't need to communicate that cross-process.
|
||||
}
|
||||
}
|
@ -63,7 +63,7 @@ public class DesktopControllerTests : ContextualTests
|
||||
_thumbnailController = new ThumbnailController(_thumbnailRenderQueue, _config);
|
||||
_sharedDeviceManager = Substitute.For<ISharedDeviceManager>();
|
||||
_processCoordinator =
|
||||
new ProcessCoordinator(Path.Combine(FolderPath, "instance.lock"), Guid.NewGuid().ToString("D"));
|
||||
new ProcessCoordinator(FolderPath, Guid.NewGuid().ToString("D"));
|
||||
ScanningContext.WorkerFactory = Substitute.For<IWorkerFactory>();
|
||||
_desktopController = new DesktopController(
|
||||
ScanningContext,
|
||||
|
@ -5,26 +5,21 @@ using NAPS2.Remoting;
|
||||
namespace NAPS2.Platform.Windows;
|
||||
|
||||
// TODO: Can we add tests for this somehow?
|
||||
/// <summary>
|
||||
/// A class to help manage the lifecycle of the NAPS2 GUI.
|
||||
/// </summary>
|
||||
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,
|
||||
ProcessCoordinator processCoordinator, Naps2Config config)
|
||||
ProcessCoordinator processCoordinator, Naps2Config config) : base(processCoordinator, config)
|
||||
{
|
||||
_sti = sti;
|
||||
_windowsEventLogger = windowsEventLogger;
|
||||
_processCoordinator = processCoordinator;
|
||||
_config = config;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -178,42 +173,10 @@ public class WindowsApplicationLifecycle : ApplicationLifecycle
|
||||
}
|
||||
}
|
||||
|
||||
// Only start one instance if configured for SingleInstance
|
||||
if (_config.Get(c => c.SingleInstance))
|
||||
{
|
||||
if (!_processCoordinator.TryTakeInstanceLock())
|
||||
{
|
||||
Log.Debug("Failed to get SingleInstance lock");
|
||||
var process = _processCoordinator.GetProcessWithInstanceLock();
|
||||
if (process != null)
|
||||
{
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
base.ExitIfRedundant();
|
||||
}
|
||||
|
||||
private static void SetMainWindowToForeground(Process process)
|
||||
protected override void SetMainWindowToForeground(Process process)
|
||||
{
|
||||
if (process.MainWindowHandle != IntPtr.Zero)
|
||||
{
|
||||
|
@ -1,12 +1,70 @@
|
||||
using NAPS2.Remoting;
|
||||
|
||||
namespace NAPS2.Platform;
|
||||
|
||||
public class ApplicationLifecycle
|
||||
/// <summary>
|
||||
/// A class to help manage the lifecycle of the NAPS2 GUI.
|
||||
/// </summary>
|
||||
public abstract class ApplicationLifecycle
|
||||
{
|
||||
private readonly ProcessCoordinator _processCoordinator;
|
||||
private readonly Naps2Config _config;
|
||||
|
||||
protected ApplicationLifecycle(ProcessCoordinator processCoordinator, Naps2Config config)
|
||||
{
|
||||
_processCoordinator = processCoordinator;
|
||||
_config = config;
|
||||
}
|
||||
|
||||
public virtual void ParseArgs(string[] args)
|
||||
{
|
||||
}
|
||||
|
||||
public virtual void ExitIfRedundant()
|
||||
{
|
||||
HandleSingleInstance();
|
||||
}
|
||||
|
||||
protected virtual void HandleSingleInstance()
|
||||
{
|
||||
// Only start one instance if configured for SingleInstance
|
||||
Log.Debug("HandleSingleInstance");
|
||||
if (_config.Get(c => c.SingleInstance))
|
||||
{
|
||||
Log.Debug("SingleInstance enabled");
|
||||
if (!_processCoordinator.TryTakeInstanceLock())
|
||||
{
|
||||
Log.Debug("Failed to get SingleInstance lock");
|
||||
var process = _processCoordinator.GetProcessWithInstanceLock();
|
||||
if (process != null)
|
||||
{
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected virtual void SetMainWindowToForeground(Process process)
|
||||
{
|
||||
}
|
||||
}
|
@ -12,14 +12,20 @@ namespace NAPS2.Remoting;
|
||||
/// 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 class ProcessCoordinator(string basePath, string pipeNameFormat)
|
||||
{
|
||||
private const string LOCK_FILE_NAME = "instance.lock";
|
||||
private const string PROC_FILE_NAME = "instance.proc";
|
||||
|
||||
public static ProcessCoordinator CreateDefault() =>
|
||||
new(Path.Combine(Paths.AppData, "instance.lock"), "NAPS2_PIPE_v2_{0}");
|
||||
new(Paths.AppData, "NAPS2_PIPE_v2_{0}");
|
||||
|
||||
private NamedPipeServer? _server;
|
||||
private FileStream? _instanceLock;
|
||||
|
||||
private string LockFilePath => Path.Combine(basePath, LOCK_FILE_NAME);
|
||||
private string ProcFilePath => Path.Combine(basePath, PROC_FILE_NAME);
|
||||
|
||||
private string GetPipeName(Process process)
|
||||
{
|
||||
return string.Format(pipeNameFormat, process.Id);
|
||||
@ -80,9 +86,10 @@ public class ProcessCoordinator(string instanceLockPath, string pipeNameFormat)
|
||||
}
|
||||
try
|
||||
{
|
||||
_instanceLock = new FileStream(instanceLockPath, FileMode.Create, FileAccess.ReadWrite, FileShare.Read);
|
||||
_instanceLock.SetLength(0);
|
||||
using var writer = new StreamWriter(_instanceLock, Encoding.UTF8, 1024, true);
|
||||
_instanceLock = new FileStream(LockFilePath, FileMode.Create, FileAccess.ReadWrite, FileShare.None);
|
||||
using var procFile = new FileStream(ProcFilePath, FileMode.Create, FileAccess.ReadWrite, FileShare.Read);
|
||||
procFile.SetLength(0);
|
||||
using var writer = new StreamWriter(procFile, Encoding.UTF8, 1024);
|
||||
writer.WriteLine(Process.GetCurrentProcess().Id);
|
||||
}
|
||||
catch (Exception)
|
||||
@ -96,7 +103,7 @@ public class ProcessCoordinator(string instanceLockPath, string pipeNameFormat)
|
||||
{
|
||||
try
|
||||
{
|
||||
using var reader = new FileStream(instanceLockPath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
|
||||
using var reader = new FileStream(ProcFilePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
|
||||
var id = int.Parse(new StreamReader(reader).ReadLine()?.Trim() ?? "");
|
||||
return Process.GetProcessById(id);
|
||||
}
|
||||
|
@ -24,7 +24,7 @@ internal class LinuxSystemCompat : ISystemCompat
|
||||
|
||||
public bool SupportsButtonActions => true;
|
||||
|
||||
public bool SupportsSingleInstance => false;
|
||||
public bool SupportsSingleInstance => true;
|
||||
|
||||
public bool CanUseWin32 => false;
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user