From 905487ce824ae3dbe500ebeba685458a86b34554 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 8 Sep 2026 17:16:38 +0000 Subject: [PATCH] Replace .NET WinForms handler with the Rust build in CI; drop SshHandler Remove the .NET/WinForms project (SshHandler/, SshHandler.sln) now that the Rust rewrite (SshHandlerRs/) covers the same behavior with a ~250KB binary and no CLR startup cost, and its Default-Apps-style registration is confirmed working. Rewire the release workflow to build ssh-handler.exe natively on windows-latest with the MSVC toolchain instead of dotnet publish, and package it alongside the .bat and .sh handlers as release assets (ssh-handler-rs-*.exe, openssh_protocol_handler-*.bat, ssh-handler-*.sh) when a v* tag is pushed. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01GYCT2RpUjFfBfp6CD4rREw --- .github/workflows/build.yml | 48 +++-------- .gitignore | 20 ----- SshHandler.sln | 21 ----- SshHandler/Form1.Designer.cs | 113 -------------------------- SshHandler/Form1.cs | 40 --------- SshHandler/Form1.resx | 67 --------------- SshHandler/Program.cs | 106 ------------------------ SshHandler/Properties/AssemblyInfo.cs | 0 SshHandler/SshHandler.csproj | 12 --- SshHandler/ssh_icon.ico | Bin 4710 -> 0 bytes SshHandlerRs/README.md | 40 +++++---- 11 files changed, 34 insertions(+), 433 deletions(-) delete mode 100644 SshHandler.sln delete mode 100644 SshHandler/Form1.Designer.cs delete mode 100644 SshHandler/Form1.cs delete mode 100644 SshHandler/Form1.resx delete mode 100644 SshHandler/Program.cs delete mode 100644 SshHandler/Properties/AssemblyInfo.cs delete mode 100644 SshHandler/SshHandler.csproj delete mode 100644 SshHandler/ssh_icon.ico diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index b76622a..5671077 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -16,38 +16,14 @@ jobs: - name: Checkout repository uses: actions/checkout@v4 - - name: Configure Git Line Endings - run: git config --global core.autocrlf input - - - name: Setup .NET - uses: actions/setup-dotnet@v4 + - name: Setup Rust + uses: dtolnay/rust-toolchain@stable with: - dotnet-version: 8.0.x + targets: x86_64-pc-windows-msvc - - name: Restore dependencies - run: dotnet restore - - # 1. Build Framework-Dependent FIRST (The tiny one) - - name: Build Framework-Dependent (Requires .NET 8) - run: > - dotnet publish SshHandler/SshHandler.csproj --configuration Release - --self-contained false - /p:PublishSingleFile=true - -o ./publish-dotnet8 - - # 2. Clean to prevent artifact leakage - - name: Clean intermediate files - run: dotnet clean SshHandler/SshHandler.csproj - - # 3. Build Standalone SECOND (The heavy one) - - name: Build Standalone (Includes Runtime) - run: > - dotnet publish SshHandler/SshHandler.csproj --configuration Release - --runtime win-x64 - --self-contained true - /p:PublishSingleFile=true - /p:IncludeNativeLibrariesForSelfExtract=true - -o ./publish-standalone + - name: Build ssh-handler (Rust) + working-directory: SshHandlerRs + run: cargo build --release --target x86_64-pc-windows-msvc - name: Ensure Unix Line Endings for Shell Script shell: pwsh @@ -64,11 +40,10 @@ jobs: run: | $tag = "${{ github.ref_name }}" if (-not $tag -or $tag -eq "main") { $tag = "manual" } - - # Rename Executables - Move-Item ./publish-standalone/SshHandler.exe "./SshHandler-$tag-Standalone.exe" - Move-Item ./publish-dotnet8/SshHandler.exe "./SshHandler-$tag-dotnet8.exe" - + + # Rename Rust executable + Move-Item SshHandlerRs/target/x86_64-pc-windows-msvc/release/ssh-handler.exe "./ssh-handler-rs-$tag.exe" + # Rename Scripts Copy-Item "ssh-handler.sh" "./ssh-handler-$tag.sh" Copy-Item "openssh_protocol_handler.bat" "./openssh_protocol_handler-$tag.bat" @@ -78,8 +53,7 @@ jobs: if: startsWith(github.ref, 'refs/tags/') with: files: | - SshHandler-*-Standalone.exe - SshHandler-*-dotnet8.exe + ssh-handler-rs-*.exe ssh-handler-*.sh openssh_protocol_handler-*.bat generate_release_notes: true diff --git a/.gitignore b/.gitignore index 9a49c78..6c8c89a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,24 +1,4 @@ # .gitignore -# User-specific files -*.rsuser -*.suo -*.user -*.userosscache -*.sln.docstates - -# Build results -[Dd]ebug/ -[Dd]ebugPublic/ -[Rr]elease/ -[Rr]eleases/ -x64/ -x86/ -[Aa][Rr][Mm]/ -[Aa][Rr][Mm]64/ -bld/ -[Bb]in/ -[Oo]bj/ - # Rust /SshHandlerRs/target/ diff --git a/SshHandler.sln b/SshHandler.sln deleted file mode 100644 index deffb31..0000000 --- a/SshHandler.sln +++ /dev/null @@ -1,21 +0,0 @@ -Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 17 -VisualStudioVersion = 17.0.31903.59 -MinimumVisualStudioVersion = 10.0.40219.1 -Project("{50924D75-2903-46D4-86DD-02269EFDBCA9}") = "SshHandler", "SshHandler\SshHandler.csproj", "{960BAB18-309B-42D0-B67E-B4EF8E7F7490}" -EndProject -Global - GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|Any CPU = Debug|Any CPU - Release|Any CPU = Release|Any CPU - EndGlobalSection - GlobalSection(ProjectConfigurationPlatforms) = postSolution - {960BAB18-309B-42D0-B67E-B4EF8E7F7490}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {960BAB18-309B-42D0-B67E-B4EF8E7F7490}.Debug|Any CPU.Build.0 = Debug|Any CPU - {960BAB18-309B-42D0-B67E-B4EF8E7F7490}.Release|Any CPU.ActiveCfg = Release|Any CPU - {960BAB18-309B-42D0-B67E-B4EF8E7F7490}.Release|Any CPU.Build.0 = Release|Any CPU - EndGlobalSection - GlobalSection(SolutionProperties) = preSolution - HideSolutionNode = FALSE - EndGlobalSection -EndGlobal diff --git a/SshHandler/Form1.Designer.cs b/SshHandler/Form1.Designer.cs deleted file mode 100644 index b1e5ad7..0000000 --- a/SshHandler/Form1.Designer.cs +++ /dev/null @@ -1,113 +0,0 @@ -partial class Form1 -{ - /// - /// Required designer variable. - /// - private System.ComponentModel.IContainer components = null; - - /// - /// Clean up any resources being used. - /// - /// true if managed resources should be disposed; otherwise, false. - protected override void Dispose(bool disposing) - { - if (disposing && (components != null)) - { - components.Dispose(); - } - base.Dispose(disposing); - } - - #region Windows Form Designer generated code - - /// - /// Required method for Designer support - do not modify - /// the contents of this method with the code editor. - /// - private void InitializeComponent() - { - this.label1 = new System.Windows.Forms.Label(); - this.targetTextBox = new System.Windows.Forms.TextBox(); - this.legacyCheckBox = new System.Windows.Forms.CheckBox(); - this.connectButton = new System.Windows.Forms.Button(); - this.cancelButton = new System.Windows.Forms.Button(); - this.SuspendLayout(); - // - // label1 - // - this.label1.AutoSize = true; - this.label1.Location = new System.Drawing.Point(12, 15); - this.label1.Name = "label1"; - this.label1.Size = new System.Drawing.Size(73, 15); - this.label1.TabIndex = 0; - this.label1.Text = "Connect to:"; - // - // targetTextBox - // - this.targetTextBox.Location = new System.Drawing.Point(15, 33); - this.targetTextBox.Name = "targetTextBox"; - this.targetTextBox.Size = new System.Drawing.Size(357, 23); - this.targetTextBox.TabIndex = 1; - // - // legacyCheckBox - // - this.legacyCheckBox.AutoSize = true; - this.legacyCheckBox.Location = new System.Drawing.Point(15, 71); - this.legacyCheckBox.Name = "legacyCheckBox"; - this.legacyCheckBox.Size = new System.Drawing.Size(206, 19); - this.legacyCheckBox.TabIndex = 2; - this.legacyCheckBox.Text = "Enable Legacy Mode (for old devices)"; - this.legacyCheckBox.UseVisualStyleBackColor = true; - // - // connectButton - // - this.connectButton.Location = new System.Drawing.Point(297, 106); - this.connectButton.Name = "connectButton"; - this.connectButton.Size = new System.Drawing.Size(75, 23); - this.connectButton.TabIndex = 3; - this.connectButton.Text = "Connect"; - this.connectButton.UseVisualStyleBackColor = true; - this.connectButton.Click += new System.EventHandler(this.connectButton_Click); - // - // cancelButton - // - this.cancelButton.DialogResult = System.Windows.Forms.DialogResult.Cancel; - this.cancelButton.Location = new System.Drawing.Point(216, 106); - this.cancelButton.Name = "cancelButton"; - this.cancelButton.Size = new System.Drawing.Size(75, 23); - this.cancelButton.TabIndex = 4; - this.cancelButton.Text = "Cancel"; - this.cancelButton.UseVisualStyleBackColor = true; - this.cancelButton.Click += new System.EventHandler(this.cancelButton_Click); - // - // Form1 - // - this.AcceptButton = this.connectButton; - this.AutoScaleDimensions = new System.Drawing.SizeF(7F, 15F); - this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; - this.CancelButton = this.cancelButton; - this.ClientSize = new System.Drawing.Size(384, 141); - this.Controls.Add(this.cancelButton); - this.Controls.Add(this.connectButton); - this.Controls.Add(this.legacyCheckBox); - this.Controls.Add(this.targetTextBox); - this.Controls.Add(this.label1); - this.FormBorderStyle = System.Windows.Forms.FormBorderStyle.FixedDialog; - this.MaximizeBox = false; - this.MinimizeBox = false; - this.Name = "Form1"; - this.ShowIcon = false; - this.StartPosition = System.Windows.Forms.FormStartPosition.CenterScreen; - this.Text = "SSH Connection"; - this.ResumeLayout(false); - this.PerformLayout(); - } - - #endregion - - private System.Windows.Forms.Label label1; - private System.Windows.Forms.TextBox targetTextBox; - private System.Windows.Forms.CheckBox legacyCheckBox; - private System.Windows.Forms.Button connectButton; - private System.Windows.Forms.Button cancelButton; -} diff --git a/SshHandler/Form1.cs b/SshHandler/Form1.cs deleted file mode 100644 index 90cc8c5..0000000 --- a/SshHandler/Form1.cs +++ /dev/null @@ -1,40 +0,0 @@ -using System; -using System.Windows.Forms; - -public partial class Form1 : Form -{ - // This property will store the result for the main program - public string FinalSshCommand { get; private set; } - - public Form1(string initialTarget) - { - InitializeComponent(); - targetTextBox.Text = initialTarget; - // Select the text for easy editing - targetTextBox.SelectAll(); - targetTextBox.Focus(); - } - - private void connectButton_Click(object sender, EventArgs e) - { - string target = targetTextBox.Text; - bool useLegacy = legacyCheckBox.Checked; - - string legacyArgs = "-o KexAlgorithms=+diffie-hellman-group1-sha1,diffie-hellman-group14-sha1 " + - "-o HostKeyAlgorithms=+ssh-rsa " + - "-o MACs=+hmac-sha1,hmac-sha1-96"; - - // Build the arguments for ssh.exe - string arguments = $"-A -C {(useLegacy ? legacyArgs : "")} {target}"; - - FinalSshCommand = arguments; - this.DialogResult = DialogResult.OK; // Set dialog result to OK - this.Close(); // Close the form - } - - private void cancelButton_Click(object sender, EventArgs e) - { - this.DialogResult = DialogResult.Cancel; // Set dialog result to Cancel - this.Close(); - } -} diff --git a/SshHandler/Form1.resx b/SshHandler/Form1.resx deleted file mode 100644 index 5aada26..0000000 --- a/SshHandler/Form1.resx +++ /dev/null @@ -1,67 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - text/microsoft-resx - - - 2.0 - - - System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - 384, 141 - - - SSH Connection - - diff --git a/SshHandler/Program.cs b/SshHandler/Program.cs deleted file mode 100644 index ff98f3c..0000000 --- a/SshHandler/Program.cs +++ /dev/null @@ -1,106 +0,0 @@ -using Microsoft.Win32; -using System; -using System.Diagnostics; -using System.Security.Principal; -using System.Windows.Forms; - -static class Program -{ - private const string ProtocolName = "ssh"; - - [STAThread] - static void Main(string[] args) - { - Application.EnableVisualStyles(); - Application.SetCompatibleTextRenderingDefault(false); - - // If no arguments, run setup mode. - if (args.Length == 0) - { - SetupHandler(); - } - // Otherwise, run handler mode. - else - { - HandleConnection(args[0]); - } - } - - static void HandleConnection(string url) - { - string target = url.Replace("ssh://", "").TrimEnd('/'); - if (!target.Contains("@")) - { - target = $"{Environment.UserName}@{target}"; - } - - // Show the form as a dialog - using (var form = new Form1(target)) - { - if (form.ShowDialog() == DialogResult.OK) - { - // If user clicked "Connect", run the command - ProcessStartInfo psi = new ProcessStartInfo - { - FileName = "ssh.exe", - Arguments = form.FinalSshCommand, - UseShellExecute = true, - CreateNoWindow = false - }; - Process.Start(psi); - } - } - } - - static void SetupHandler() - { - if (!IsAdmin()) - { - MessageBox.Show("Please run this application as an administrator to register the URL handler.", "Admin Privileges Required", MessageBoxButtons.OK, MessageBoxIcon.Error); - return; - } - - // The path is simple and correct because it's a native app - string exePath = Application.ExecutablePath; - string commandStr = $"\"{exePath}\" \"%1\""; - - var confirmResult = MessageBox.Show( - "This will register this application to handle all ssh:// links.\n\n" + - $"Command:\n{commandStr}\n\n" + - "Do you want to proceed?", - "Registry Setup", - MessageBoxButtons.YesNo, - MessageBoxIcon.Information); - - if (confirmResult == DialogResult.Yes) - { - try - { - // Write the keys to HKEY_CLASSES_ROOT - using (RegistryKey key = Registry.ClassesRoot.CreateSubKey(ProtocolName)) - { - key.SetValue("", $"URL:{ProtocolName} Protocol"); - key.SetValue("URL Protocol", ""); - } - using (RegistryKey key = Registry.ClassesRoot.CreateSubKey($"{ProtocolName}\\shell\\open\\command")) - { - key.SetValue("", commandStr); - } - MessageBox.Show("Successfully registered ssh:// protocol handler!", "Success", MessageBoxButtons.OK, MessageBoxIcon.Information); - } - catch (Exception ex) - { - MessageBox.Show($"Failed to write to registry:\n{ex.Message}", "Error", MessageBoxButtons.OK, MessageBoxIcon.Error); - } - } - } - - private static bool IsAdmin() - { - using (WindowsIdentity identity = WindowsIdentity.GetCurrent()) - { - WindowsPrincipal principal = new WindowsPrincipal(identity); - return principal.IsInRole(WindowsBuiltInRole.Administrator); - } - } -} diff --git a/SshHandler/Properties/AssemblyInfo.cs b/SshHandler/Properties/AssemblyInfo.cs deleted file mode 100644 index e69de29..0000000 diff --git a/SshHandler/SshHandler.csproj b/SshHandler/SshHandler.csproj deleted file mode 100644 index b695334..0000000 --- a/SshHandler/SshHandler.csproj +++ /dev/null @@ -1,12 +0,0 @@ - - - - WinExe - net8.0-windows - enable - true - enable - ssh_icon.ico - - - diff --git a/SshHandler/ssh_icon.ico b/SshHandler/ssh_icon.ico deleted file mode 100644 index 3334a3d1e3c670b8e52e0c00cd96e8b42847e6e0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4710 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 Apps > Default Apps > "Choose default apps by link + type". A bare `HKCR\ssh\shell\open\command` write (what the old .NET + version did) gets silently ignored once a `UserChoice` already exists for + the protocol, which is the common case. - `ssh://user@host` argument: handler mode. Parses the target (defaulting the username to the current Windows user if omitted), shows a small dialog to confirm/edit the target and optionally enable "legacy mode" (older