Run the digital bridge on Windows
The bridge launched its helper only through Wine. It now starts any process, so on Windows it runs the helper directly. WineBridgeChannel is renamed ProcessBridgeChannel. WindowsControlSetup sets up the control side on Windows. The digital dialog, AppSession, README and docs/digital-bridge.md describe the Windows setup. publish-windows.sh builds a Windows release. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -699,10 +699,12 @@ public sealed class AppSession : IDisposable
|
||||
OnTop = Settings.DigitalEngineOnTop,
|
||||
PttPort = Settings.DigitalPttPort.Trim().Length > 0 ? Settings.DigitalPttPort : null,
|
||||
};
|
||||
WineBridgeChannel channel = new(
|
||||
ProcessBridgeChannel channel = new(
|
||||
Settings.DigitalBridgePath,
|
||||
Settings.DigitalWinePrefix.Trim().Length > 0 ? Settings.DigitalWinePrefix : null,
|
||||
Settings.DigitalWineCommand.Trim().Length > 0 ? Settings.DigitalWineCommand : "wine",
|
||||
OperatingSystem.IsWindows()
|
||||
? null
|
||||
: Settings.DigitalWineCommand.Trim().Length > 0 ? Settings.DigitalWineCommand : "wine",
|
||||
Paths.Diagnostics);
|
||||
MmttyEngine started = new(channel, options);
|
||||
await started.StartAsync();
|
||||
|
||||
@@ -124,7 +124,7 @@
|
||||
<TabItem Header="Engine">
|
||||
<ScrollViewer>
|
||||
<StackPanel Margin="4,8,10,4">
|
||||
<TextBlock TextWrapping="Wrap" FontSize="11" Opacity="0.75"
|
||||
<TextBlock Name="EngineNote" TextWrapping="Wrap" FontSize="11" Opacity="0.75"
|
||||
Text="MMTTY and 2Tone are Windows programs. They run under Wine, driven through the same XMMT.ocx control N1MM uses, with a small bridge program in between. See docs/digital-bridge.md for setting the Wine prefix up." />
|
||||
<TextBlock Classes="label" Text="Engine program (mmtty.exe or 2Tone.exe)" />
|
||||
<Grid ColumnDefinitions="*,6,Auto">
|
||||
@@ -136,13 +136,13 @@
|
||||
<TextBox Name="BridgeBox" />
|
||||
<Button Grid.Column="2" Content="Browse…" Click="OnBrowseBridge" />
|
||||
</Grid>
|
||||
<Grid ColumnDefinitions="*,8,160" RowDefinitions="Auto,Auto">
|
||||
<Grid Name="WineRow" ColumnDefinitions="*,8,160" RowDefinitions="Auto,Auto">
|
||||
<TextBlock Classes="label" Text="Wine prefix (empty for Wine's own)" />
|
||||
<TextBox Name="PrefixBox" Grid.Row="1" />
|
||||
<TextBlock Grid.Column="2" Classes="label" Text="Wine command" />
|
||||
<TextBox Name="WineBox" Grid.Row="1" Grid.Column="2" />
|
||||
</Grid>
|
||||
<TextBlock Classes="label" Text="Control (XMMT.ocx), registered in the prefix above" />
|
||||
<TextBlock Name="ControlLabel" Classes="label" Text="Control (XMMT.ocx), registered in the prefix above" />
|
||||
<Grid ColumnDefinitions="*,6,Auto,6,Auto">
|
||||
<TextBox Name="ControlBox" />
|
||||
<Button Grid.Column="2" Content="Browse…" Click="OnBrowseControl" />
|
||||
|
||||
@@ -106,6 +106,14 @@ public sealed partial class DigitalDialog : Window
|
||||
ControlBox.Text = settings.DigitalControlPath;
|
||||
PrefixBox.Text = settings.DigitalWinePrefix;
|
||||
WineBox.Text = settings.DigitalWineCommand;
|
||||
if (OperatingSystem.IsWindows())
|
||||
{
|
||||
WineRow.IsVisible = false;
|
||||
EngineNote.Text = "MMTTY and 2Tone are driven through the same XMMT.ocx control N1MM uses, "
|
||||
+ "with a small bridge program in between. Register the control once, before the first start. "
|
||||
+ "See docs/digital-bridge.md.";
|
||||
ControlLabel.Text = "Control (XMMT.ocx), registered where it is. Keep the file there";
|
||||
}
|
||||
PttBox.Text = settings.DigitalPttPort;
|
||||
}
|
||||
|
||||
@@ -251,12 +259,20 @@ public sealed partial class DigitalDialog : Window
|
||||
{
|
||||
RegisterButton.IsEnabled = false;
|
||||
Say("Registering…");
|
||||
WinePrefixSetup setup = new(
|
||||
PrefixBox.Text?.Trim() is { Length: > 0 } prefix ? prefix : null,
|
||||
WineBox.Text?.Trim() is { Length: > 0 } wine ? wine : "wine");
|
||||
string control = ControlBox.Text?.Trim() ?? "";
|
||||
try
|
||||
{
|
||||
Say(await setup.RegisterAsync(ControlBox.Text?.Trim() ?? ""));
|
||||
if (OperatingSystem.IsWindows())
|
||||
{
|
||||
Say(await new WindowsControlSetup().RegisterAsync(control));
|
||||
}
|
||||
else
|
||||
{
|
||||
WinePrefixSetup setup = new(
|
||||
PrefixBox.Text?.Trim() is { Length: > 0 } prefix ? prefix : null,
|
||||
WineBox.Text?.Trim() is { Length: > 0 } wine ? wine : "wine");
|
||||
Say(await setup.RegisterAsync(control));
|
||||
}
|
||||
}
|
||||
catch (Exception failure) when (failure is IOException or InvalidOperationException)
|
||||
{
|
||||
|
||||
+20
-10
@@ -2,14 +2,15 @@ using System.Diagnostics;
|
||||
|
||||
namespace Nonemm.Digital;
|
||||
|
||||
/// Runs the bridge under Wine and talks to it over its standard input and
|
||||
/// output. Wine writes its own diagnostics to standard error, so the protocol
|
||||
/// has standard output to itself.
|
||||
public sealed class WineBridgeChannel : BridgeChannel
|
||||
/// Runs the bridge and talks to it over its standard input and output. On
|
||||
/// Linux the bridge runs under Wine, which writes its own diagnostics to
|
||||
/// standard error, so the protocol has standard output to itself. On Windows
|
||||
/// `wine` is null and the bridge is started directly.
|
||||
public sealed class ProcessBridgeChannel : BridgeChannel
|
||||
{
|
||||
private const int KeptErrorLines = 20;
|
||||
|
||||
private readonly string wine;
|
||||
private readonly string? wine;
|
||||
private readonly string bridgePath;
|
||||
private readonly string? prefix;
|
||||
private readonly string? logFolder;
|
||||
@@ -22,12 +23,13 @@ public sealed class WineBridgeChannel : BridgeChannel
|
||||
private int lastCount = int.MinValue;
|
||||
|
||||
/// `prefix` is the WINEPREFIX to run in. Left null, Wine uses its default,
|
||||
/// which is what a station with one prefix wants. `logFolder`, when it is
|
||||
/// which is what a station with one prefix wants. `wine` is the Wine
|
||||
/// command, or null to run the bridge without Wine. `logFolder`, when it is
|
||||
/// given, is where the protocol log for this run is written.
|
||||
public WineBridgeChannel(
|
||||
public ProcessBridgeChannel(
|
||||
string bridgePath,
|
||||
string? prefix = null,
|
||||
string wine = "wine",
|
||||
string? wine = "wine",
|
||||
string? logFolder = null)
|
||||
{
|
||||
this.bridgePath = bridgePath;
|
||||
@@ -70,6 +72,10 @@ public sealed class WineBridgeChannel : BridgeChannel
|
||||
string path,
|
||||
CancellationToken cancellation = default)
|
||||
{
|
||||
if (wine is null)
|
||||
{
|
||||
return Path.GetFullPath(path);
|
||||
}
|
||||
using Process winepath = Start("winepath", "-w", path);
|
||||
string converted = await winepath.StandardOutput.ReadToEndAsync(cancellation)
|
||||
.ConfigureAwait(false);
|
||||
@@ -168,13 +174,17 @@ public sealed class WineBridgeChannel : BridgeChannel
|
||||
{
|
||||
ProcessStartInfo start = new()
|
||||
{
|
||||
FileName = wine,
|
||||
FileName = wine ?? program,
|
||||
RedirectStandardInput = true,
|
||||
RedirectStandardOutput = true,
|
||||
RedirectStandardError = true,
|
||||
UseShellExecute = false,
|
||||
CreateNoWindow = true,
|
||||
};
|
||||
start.ArgumentList.Add(program);
|
||||
if (wine is not null)
|
||||
{
|
||||
start.ArgumentList.Add(program);
|
||||
}
|
||||
foreach (string argument in arguments)
|
||||
{
|
||||
start.ArgumentList.Add(argument);
|
||||
@@ -0,0 +1,58 @@
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace Nonemm.Digital;
|
||||
|
||||
/// Registers XMMT.ocx on Windows, which is what the bridge needs before it can
|
||||
/// create the control. The control is registered where it is, so the file must
|
||||
/// stay in place afterwards.
|
||||
public sealed class WindowsControlSetup
|
||||
{
|
||||
/// The control is 32 bit. On 64-bit Windows the 32-bit regsvr32 is in
|
||||
/// SysWOW64, and the one in System32 cannot load it.
|
||||
public static string Regsvr32Path(string windowsFolder) =>
|
||||
Directory.Exists(Path.Combine(windowsFolder, "SysWOW64"))
|
||||
? Path.Combine(windowsFolder, "SysWOW64", "regsvr32.exe")
|
||||
: Path.Combine(windowsFolder, "System32", "regsvr32.exe");
|
||||
|
||||
/// Registers the control and returns what to show the operator. It writes
|
||||
/// to HKEY_CLASSES_ROOT, so it runs elevated: Windows asks the operator to
|
||||
/// allow that. It throws when the operator refuses or regsvr32 fails.
|
||||
public async Task<string> RegisterAsync(
|
||||
string controlPath,
|
||||
CancellationToken cancellation = default)
|
||||
{
|
||||
if (!File.Exists(controlPath))
|
||||
{
|
||||
throw new FileNotFoundException($"{controlPath} is not there", controlPath);
|
||||
}
|
||||
string regsvr32 = Regsvr32Path(Environment.GetFolderPath(Environment.SpecialFolder.Windows));
|
||||
ProcessStartInfo start = new()
|
||||
{
|
||||
FileName = regsvr32,
|
||||
ArgumentList = { "/s", Path.GetFullPath(controlPath) },
|
||||
UseShellExecute = true,
|
||||
Verb = "runas",
|
||||
};
|
||||
Process running;
|
||||
try
|
||||
{
|
||||
running = Process.Start(start)
|
||||
?? throw new InvalidOperationException($"{regsvr32} did not start");
|
||||
}
|
||||
catch (System.ComponentModel.Win32Exception refused)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"{regsvr32} did not start: {refused.Message}", refused);
|
||||
}
|
||||
using (running)
|
||||
{
|
||||
await running.WaitForExitAsync(cancellation).ConfigureAwait(false);
|
||||
if (running.ExitCode != 0)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"{regsvr32} /s {controlPath} failed with exit code {running.ExitCode}");
|
||||
}
|
||||
}
|
||||
return $"{Path.GetFileName(controlPath)} registered";
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user