WIP: Share as service

This commit is contained in:
Ben Olden-Cooligan 2023-12-21 10:35:43 -08:00
parent 0d52098606
commit c0440c5ff3
12 changed files with 192 additions and 26 deletions

View File

@ -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)

View File

@ -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())
};
}
}

View File

@ -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>();

View 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);
}
}

View File

@ -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())
};
}
}

View 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();
}
}

View File

@ -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>();

View File

@ -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)
{

View File

@ -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);
}
}
}
}

View File

@ -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>

View 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();
}

View File

@ -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
{