mirror of
https://github.com/cyanfish/naps2.git
synced 2024-10-05 20:07:42 +03:00
WIP: Share as service
This commit is contained in:
parent
0d52098606
commit
c0440c5ff3
@ -14,16 +14,14 @@ public static class GtkEntryPoint
|
||||
GLib.ExceptionManager.UnhandledException += UnhandledGtkException;
|
||||
EtoPlatform.Current = new GtkEtoPlatform();
|
||||
|
||||
if (args.Length > 0 && args[0] is "cli" or "console")
|
||||
var subArgs = args.Skip(1).ToArray();
|
||||
return args switch
|
||||
{
|
||||
return ConsoleEntryPoint.Run(args.Skip(1).ToArray(), new GtkImagesModule());
|
||||
}
|
||||
if (args.Length > 0 && args[0] == "worker")
|
||||
{
|
||||
return WorkerEntryPoint.Run(args.Skip(1).ToArray(), new GtkImagesModule());
|
||||
}
|
||||
|
||||
return GuiEntryPoint.Run(args, new GtkImagesModule(), new GtkModule());
|
||||
["cli" or "console", ..] => ConsoleEntryPoint.Run(subArgs, new GtkImagesModule()),
|
||||
["worker", ..] => WorkerEntryPoint.Run(subArgs, new GtkImagesModule()),
|
||||
["server", ..] => ServerEntryPoint.Run(subArgs, new GtkImagesModule()),
|
||||
_ => GuiEntryPoint.Run(args, new GtkImagesModule(), new GtkModule())
|
||||
};
|
||||
}
|
||||
|
||||
private static void UnhandledGtkException(GLib.UnhandledExceptionArgs e)
|
||||
|
@ -23,15 +23,13 @@ public static class MacEntryPoint
|
||||
|
||||
EtoPlatform.Current = new MacEtoPlatform();
|
||||
|
||||
if (args.Length > 0 && args[0] is "cli" or "console")
|
||||
var subArgs = args.Skip(1).ToArray();
|
||||
return args switch
|
||||
{
|
||||
return ConsoleEntryPoint.Run(args.Skip(1).ToArray(), new MacImagesModule());
|
||||
}
|
||||
if (args.Length > 0 && args[0] == "worker")
|
||||
{
|
||||
return MacWorkerEntryPoint.Run(args.Skip(1).ToArray());
|
||||
}
|
||||
|
||||
return GuiEntryPoint.Run(args, new MacImagesModule(), new MacModule());
|
||||
["cli" or "console", ..] => ConsoleEntryPoint.Run(subArgs, new MacImagesModule()),
|
||||
["worker", ..] => MacWorkerEntryPoint.Run(subArgs),
|
||||
["server", ..] => ServerEntryPoint.Run(subArgs, new MacImagesModule()),
|
||||
_ => GuiEntryPoint.Run(args, new MacImagesModule(), new MacModule())
|
||||
};
|
||||
}
|
||||
}
|
@ -17,6 +17,7 @@ public class MacModule : GuiModule
|
||||
builder.RegisterType<AppleMailEmailProvider>().As<IAppleMailEmailProvider>();
|
||||
builder.RegisterType<MacDarkModeProvider>().As<IDarkModeProvider>().SingleInstance();
|
||||
builder.RegisterType<MacIconProvider>().As<IIconProvider>();
|
||||
builder.RegisterType<MacServiceManager>().As<IOsServiceManager>();
|
||||
|
||||
builder.RegisterType<MacDesktopForm>().As<DesktopForm>();
|
||||
builder.RegisterType<MacPreviewForm>().As<PreviewForm>();
|
||||
|
55
NAPS2.Lib.Mac/Platform/MacServiceManager.cs
Normal file
55
NAPS2.Lib.Mac/Platform/MacServiceManager.cs
Normal file
@ -0,0 +1,55 @@
|
||||
namespace NAPS2.Platform;
|
||||
|
||||
/// <summary>
|
||||
/// Manages a user-level launchd (https://www.launchd.info/) service on macOS.
|
||||
/// </summary>
|
||||
// TODO: Is it feasible to run a system-level service?
|
||||
public class MacServiceManager : IOsServiceManager
|
||||
{
|
||||
private const string SERVICE_NAME = "com.naps2.ScannerSharing";
|
||||
|
||||
private static string PlistPath =>
|
||||
Path.Combine(
|
||||
Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
|
||||
$"Library/LaunchAgents/{SERVICE_NAME}.plist");
|
||||
|
||||
public bool IsRegistered => File.Exists(PlistPath);
|
||||
|
||||
public void Register()
|
||||
{
|
||||
var serviceDef = $"""
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>Label</key>
|
||||
<string>{SERVICE_NAME}</string>
|
||||
<key>Program</key>
|
||||
<string>{Environment.ProcessPath}</string>
|
||||
<key>ProgramArguments</key>
|
||||
<array>
|
||||
<string>{Environment.ProcessPath}</string>
|
||||
<string>server</string>
|
||||
</array>
|
||||
<key>RunAtLoad</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
""";
|
||||
File.WriteAllText(PlistPath, serviceDef);
|
||||
if (!ProcessHelper.TryRun("launchctl", $"load \"{PlistPath}\"", 1000))
|
||||
{
|
||||
Log.Error($"Could not load service {SERVICE_NAME}");
|
||||
}
|
||||
}
|
||||
|
||||
public void Unregister()
|
||||
{
|
||||
// TODO: Longer timeout / run async?
|
||||
if (!ProcessHelper.TryRun("launchctl", $"unload \"{PlistPath}\"", 1000))
|
||||
{
|
||||
Log.Error($"Could not unload service {SERVICE_NAME}");
|
||||
}
|
||||
File.Delete(PlistPath);
|
||||
}
|
||||
}
|
@ -13,11 +13,12 @@ public static class WinFormsEntryPoint
|
||||
{
|
||||
EtoPlatform.Current = new WinFormsEtoPlatform();
|
||||
|
||||
if (args.Length > 0 && args[0] == "worker")
|
||||
var subArgs = args.Skip(1).ToArray();
|
||||
return args switch
|
||||
{
|
||||
return WindowsWorkerEntryPoint.Run(args.Skip(1).ToArray());
|
||||
}
|
||||
|
||||
return GuiEntryPoint.Run(args, new GdiModule(), new WinFormsModule());
|
||||
["worker", ..] => WindowsWorkerEntryPoint.Run(subArgs),
|
||||
["server", ..] => ServerEntryPoint.Run(subArgs, new GdiModule()),
|
||||
_ => GuiEntryPoint.Run(args, new GdiModule(), new WinFormsModule())
|
||||
};
|
||||
}
|
||||
}
|
49
NAPS2.Lib/EntryPoints/ServerEntryPoint.cs
Normal file
49
NAPS2.Lib/EntryPoints/ServerEntryPoint.cs
Normal file
@ -0,0 +1,49 @@
|
||||
using System.Threading;
|
||||
using Autofac;
|
||||
using NAPS2.Modules;
|
||||
using NAPS2.Remoting.Server;
|
||||
using NAPS2.Remoting.Worker;
|
||||
using NAPS2.Scan;
|
||||
|
||||
namespace NAPS2.EntryPoints;
|
||||
|
||||
/// <summary>
|
||||
/// The entry point for NAPS2 running as a server for scanner sharing.
|
||||
/// </summary>
|
||||
public static class ServerEntryPoint
|
||||
{
|
||||
public static int Run(string[] args, Module imageModule, Action<IContainer>? run = null)
|
||||
{
|
||||
// Initialize Autofac (the DI framework)
|
||||
var container = AutoFacHelper.FromModules(
|
||||
new CommonModule(), imageModule, new WorkerModule(), new ContextModule());
|
||||
|
||||
TaskScheduler.UnobservedTaskException += UnhandledTaskException;
|
||||
|
||||
// Start a pending worker process
|
||||
container.Resolve<IWorkerFactory>().Init(container.Resolve<ScanningContext>());
|
||||
|
||||
run ??= _ =>
|
||||
{
|
||||
var sharedDeviceManager = container.Resolve<ISharedDeviceManager>();
|
||||
sharedDeviceManager.StartSharing();
|
||||
AppDomain.CurrentDomain.ProcessExit += (_, _) =>
|
||||
{
|
||||
// TODO: Actually wait for sharing to stop
|
||||
sharedDeviceManager.StopSharing();
|
||||
};
|
||||
var reset = new ManualResetEvent(false);
|
||||
Console.CancelKeyPress += (_, _) => reset.Set();
|
||||
reset.WaitOne();
|
||||
};
|
||||
run(container);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
private static void UnhandledTaskException(object? sender, UnobservedTaskExceptionEventArgs e)
|
||||
{
|
||||
Log.FatalException("An error occurred that caused the server task to terminate.", e.Exception);
|
||||
e.SetObserved();
|
||||
}
|
||||
}
|
@ -9,17 +9,25 @@ namespace NAPS2.EtoForms.Ui;
|
||||
public class ScannerSharingForm : EtoDialogBase
|
||||
{
|
||||
private readonly ISharedDeviceManager _sharedDeviceManager;
|
||||
private readonly IOsServiceManager _osServiceManager;
|
||||
private readonly ErrorOutput _errorOutput;
|
||||
|
||||
private readonly CheckBox _shareAsService = C.CheckBox(UiStrings.ShareAsService);
|
||||
private readonly IListView<SharedDevice> _listView;
|
||||
|
||||
private readonly Command _addCommand;
|
||||
private readonly Command _editCommand;
|
||||
private readonly Command _deleteCommand;
|
||||
|
||||
public ScannerSharingForm(Naps2Config config, SharedDevicesListViewBehavior listViewBehavior, ISharedDeviceManager sharedDeviceManager)
|
||||
private bool _suppressChangeEvent;
|
||||
|
||||
public ScannerSharingForm(Naps2Config config, SharedDevicesListViewBehavior listViewBehavior,
|
||||
ISharedDeviceManager sharedDeviceManager, IOsServiceManager osServiceManager, ErrorOutput errorOutput)
|
||||
: base(config)
|
||||
{
|
||||
_sharedDeviceManager = sharedDeviceManager;
|
||||
_osServiceManager = osServiceManager;
|
||||
_errorOutput = errorOutput;
|
||||
|
||||
_listView = EtoPlatform.Current.CreateListView(listViewBehavior);
|
||||
_addCommand = new ActionCommand(DoAdd)
|
||||
@ -39,6 +47,8 @@ public class ScannerSharingForm : EtoDialogBase
|
||||
Shortcut = Keys.Delete
|
||||
};
|
||||
|
||||
_shareAsService.Checked = _osServiceManager.IsRegistered;
|
||||
_shareAsService.CheckedChanged += ShareAsServiceCheckedChanged;
|
||||
_listView.ImageSize = 48;
|
||||
_listView.SelectionChanged += SelectionChanged;
|
||||
|
||||
@ -64,6 +74,7 @@ public class ScannerSharingForm : EtoDialogBase
|
||||
|
||||
LayoutController.Content = L.Column(
|
||||
C.Label(UiStrings.ScannerSharingIntro).DynamicWrap(400),
|
||||
_shareAsService,
|
||||
C.Spacer(),
|
||||
_listView.Control.Scale().NaturalHeight(80),
|
||||
L.Row(
|
||||
@ -100,6 +111,34 @@ public class ScannerSharingForm : EtoDialogBase
|
||||
_deleteCommand.Enabled = SelectedDevice != null;
|
||||
}
|
||||
|
||||
private void ShareAsServiceCheckedChanged(object? sender, EventArgs e)
|
||||
{
|
||||
if (_suppressChangeEvent) return;
|
||||
_suppressChangeEvent = true;
|
||||
try
|
||||
{
|
||||
if (_shareAsService.IsChecked())
|
||||
{
|
||||
_osServiceManager.Register();
|
||||
}
|
||||
else
|
||||
{
|
||||
_osServiceManager.Unregister();
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// TODO: Maybe we display a generic string here?
|
||||
Log.ErrorException(ex.Message, ex);
|
||||
_errorOutput.DisplayError(ex.Message, ex);
|
||||
_shareAsService.Checked = _osServiceManager.IsRegistered;
|
||||
}
|
||||
finally
|
||||
{
|
||||
_suppressChangeEvent = false;
|
||||
}
|
||||
}
|
||||
|
||||
private void DoAdd()
|
||||
{
|
||||
var fedit = FormFactory.Create<SharedDeviceForm>();
|
||||
|
@ -13,7 +13,7 @@ public class ThunderbirdEmailProvider : IEmailProvider
|
||||
|
||||
// Note we can't really support the Flatpak version of Thunderbird as it won't have access to attachment files from
|
||||
// the sandbox.
|
||||
public bool IsAvailable => ProcessHelper.IsSuccessful("thunderbird", "-v", 1000);
|
||||
public bool IsAvailable => ProcessHelper.TryRun("thunderbird", "-v", 1000);
|
||||
|
||||
public Task<bool> SendEmail(EmailMessage message, ProgressHandler progress = default)
|
||||
{
|
||||
|
9
NAPS2.Lib/Lang/Resources/UiStrings.Designer.cs
generated
9
NAPS2.Lib/Lang/Resources/UiStrings.Designer.cs
generated
@ -2191,5 +2191,14 @@ namespace NAPS2.Lang.Resources {
|
||||
return ResourceManager.GetString("ZoomOut", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Share even when NAPS2 is closed.
|
||||
/// </summary>
|
||||
internal static string ShareAsService {
|
||||
get {
|
||||
return ResourceManager.GetString("ShareAsService", resourceCulture);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -828,4 +828,7 @@
|
||||
<data name="ConfirmDeleteSharedDevice" xml:space="preserve">
|
||||
<value>Are you sure you want to stop sharing {0}?</value>
|
||||
</data>
|
||||
<data name="ShareAsService" xml:space="preserve">
|
||||
<value>Share even when NAPS2 is closed</value>
|
||||
</data>
|
||||
</root>
|
13
NAPS2.Lib/Platform/IOsServiceManager.cs
Normal file
13
NAPS2.Lib/Platform/IOsServiceManager.cs
Normal file
@ -0,0 +1,13 @@
|
||||
namespace NAPS2.Platform;
|
||||
|
||||
/// <summary>
|
||||
/// Abstraction for OS-specific "run on startup" registration logic.
|
||||
/// </summary>
|
||||
public interface IOsServiceManager
|
||||
{
|
||||
bool IsRegistered { get; }
|
||||
|
||||
void Register();
|
||||
|
||||
void Unregister();
|
||||
}
|
@ -16,7 +16,7 @@ public static class ProcessHelper
|
||||
|
||||
public static void OpenFolder(string folder) => OpenFile(folder);
|
||||
|
||||
public static bool IsSuccessful(string command, string args, int timeoutMs)
|
||||
public static bool TryRun(string command, string args, int timeoutMs)
|
||||
{
|
||||
try
|
||||
{
|
||||
|
Loading…
Reference in New Issue
Block a user