diff --git a/.gitignore b/.gitignore index 0e1fbb7..1f4ba34 100644 --- a/.gitignore +++ b/.gitignore @@ -43,6 +43,7 @@ bld/ [Oo]bj/ [Ll]og/ .vs/ +.vscode/ # NuGet Packages *.nupkg diff --git a/docs/ScreenShot-MenuItems.png b/docs/ScreenShot-MenuItems.png index 2c55de1..2910827 100644 Binary files a/docs/ScreenShot-MenuItems.png and b/docs/ScreenShot-MenuItems.png differ diff --git a/docs/ScreenShot-ToolsOptions.png b/docs/ScreenShot-ToolsOptions.png index dc24594..e53fd5f 100644 Binary files a/docs/ScreenShot-ToolsOptions.png and b/docs/ScreenShot-ToolsOptions.png differ diff --git a/nuget.config b/nuget.config new file mode 100644 index 0000000..128d95e --- /dev/null +++ b/nuget.config @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/readme.md b/readme.md index fe8ab48..2343c51 100644 --- a/readme.md +++ b/readme.md @@ -12,27 +12,41 @@ Suess Labs consulting is sponsored by _Xeno Innovations, Inc._ ## Overview -Now developers can build, deploy and debug projects on their remote Linux (Ubuntu, Raspberry PI, etc) devices! Customize your SSH connection to use either a _password_ or a _private key_. +Now developers can build, deploy and debug projects on their remote Linux (Ubuntu, Raspberry PI, etc) devices! Customize your SSH connection to use either a _password_, a _private key_, or an SSH CA-signed certificate. If you enjoy using the extension, please give it a ★★★★★ rating on the [Visual Studio Marketplace](https://marketplace.visualstudio.com/items?itemName=SuessLabs.VSLinuxDebugger). +### What's new + +* **SSH CA certificate authentication** — supports a private key with an accompanying `-cert.pub` certificate (auto-detected next to the key, or set explicitly), in addition to password and plain private-key auth. +* **Sudo-elevated debugger launch** — an opt-in setting launches `vsdbg` via a configurable `sudo` command, for debuggees running with elevated or ambient capabilities that the debugger must match to attach. +* **Deploys via `dotnet publish`** — always produces a real native executable (self-contained or framework-dependent, your choice), with the executable bit restored after transfer (lost by default over tar/scp from Windows). +* **Environment variables for the debuggee** — pass `KEY=VALUE` pairs through to the remote process, for programs that read required configuration from the environment. +* **Configurable pre/post-deploy commands and attach-to-running-process** — run arbitrary shell commands before/after each deploy (i.e. stopping/restarting a systemd service), and optionally attach the debugger to that already-running process (via a configurable PID-lookup command) instead of launching a new one. +* **Attach Only** — reattach to a running/deployed process without rebuilding or redeploying. +* **Stop button** — cancels an in-progress build/deploy/debug between steps. +* **Modernized Options UI** — settings are split across focused Tools > Options pages (Remote Host, Remote Credentials, Remote Debugger, Remote Launch, Local) built with a real WPF UI (checkboxes, dynamic show/hide for dependent fields) instead of a single page with a WinForms PropertyGrid. +* Support for Visual Studio 2026 and newer .NET target frameworks (net8.0, net10.0). + ### Supported Remote OS -The following Linux distrobutions have been validated and are supported. +The following Linux distributions have been validated and are supported. * Ubuntu (20.04 LTS, 22.04 LTS, 24.x LTS) -* Raspberry PI OS +* Raspberry Pi OS +* Debian-based embedded Linux images (i.e. Yocto/OpenEmbedded targets), where `curl` and an SSH server are available ### Usage ![VS Menu](docs/ScreenShot-MenuItems.png) -* Build and upload to remote devices -* Remote debugging* - * _This is still in the experimental stages. Please use VS' Attach to Process if you have issues_ +* **Build and Deploy** — build, upload to the remote device, and (re)start it if pre/post-deploy commands are configured. +* **Build, Deploy and Debug** — the above, then attach the debugger (or launch it, for a fresh non-supervised process). +* **Attach Only (no build/deploy)** — reattach to whatever's already running/deployed, without rebuilding or redeploying. +* **Stop** — cancels an in-progress build/deploy/debug between steps. * VS Linux Debugger will automatically detect and install `vsdbg` for you! -For GUI app debugging, you can use the _Build and Deploy_ feature, however, you must manually _Attach to Process_ via SSH using Visual Studio at this time. +For GUI app debugging, you must manually _Attach to Process_ via SSH using Visual Studio at this time (see below). ### Getting Started @@ -46,9 +60,9 @@ sudo apt install curl **Windows**: 1. Open Visual Studio (VS) > Tools > Options > **Linux Debugger** -2. **Input:** Remote Host IP address -3. **Input:** Remote's User Name and Password -4. VS > Extensions > Linux Debugger > **Build, Deploy, Debug** +2. Configure the **Remote Host** page (IP address) and **Remote Credentials** page (user name, and either a password, a private key, or an SSH CA certificate) +3. Optionally, configure **Remote Debugger** (deploy folder, self-contained/RID) and **Remote Launch** (env vars, pre/post-deploy commands, attach-to-running-process, sudo, X11) +4. VS > Extensions > Linux Debugger > **Build, Deploy and Debug** ![Tools Options](docs/ScreenShot-ToolsOptions.png) @@ -70,53 +84,7 @@ For GUI projects, you can use **Build and Deploy** and then manually attach to t This will save you 1.5 minutes on every build of manual uploading and updating rights via `chown -R`. -### Manually Attaching (for Command line apps) - -For CLI projects, you can use **Build and Deploy** and then manually attach to the process via SSH by using Visual Studio's built-in tool (similar to above). - -You may have to manually interrupt your app via `Console.ReadLine();` high-up in your entry-point (i.e. `main()`). - -1. Deploy to remote machine via - 1. Extensions > Linux Debugger > **"Build and Deploy"** -2. Run your CLI app on remote machine - 1. `dotnet MyCliApp.dll` -3. Debug > **"Attach to Process.."** - 1. Connection Type: **SSH** - 2. Connection Target: **(Remote machine's IP)** - 3. (Select process) - 4. Click, **Attach** - 5. Check, **"Managed (.NET Core for Unix)"** - 6. Click, **OK** -4. Continue your application, if using a manual interrupt (i.e. `Console.ReadLine();`) - -This will save you 1.5 minutes on every build of manual uploading and updating rights via `chown -R`. - -## How To Generate Private Key (optional) - -The following steps are options if you wish to use an SSH Private Key. These steps were written for Windows 10, however, on Linux the steps are similar. - -1. Open PowerShell: -2. **Generate key** (_with old PEM format_) - 1. `ssh-keygen -m PEM -t rsa -b 4096` - 2. In the future, we'll be able to use `ssh-keygen`.. just not yet. -3. Set output name (_default is okay for basic setups_) -4. Input a passphrase for the key _(OPTIONAL)_ -5. Windows will now generate your RSA public/private key pair. - 1. Default location: `%UserProfile%\.ssh` (WINOWS) - 2. The public key will be stored as `id_rsa.pub` in the directory -6. **Upload the public key** to your remote machine - 1. Navigate to folder, `~/.ssh/` on Linux device - 2. If `~/.ssh/authorized_keys` exists, append the contents of `id_rsa.pub` to the next line. - 3. If it does not exist, simply upload `id_rsa.pub` and rename it to, `authorized_keys` -7. DONE! - -## Used By - -* [SuessLabs](https://suesslabs.com) and [Xeno Innovations](https://xenoinc.com) -* [Wilderness Labs](https://github.com/WildernessLabs) -* [Omnicell, Inc.](https://omnicell.com) - -_Want your name added? Reach out to us_ +If your remote device uses **SSH CA-signed certificates** instead of `authorized_keys` (i.e. `TrustedUserCAKeys` configured in `sshd_config`), point "SSH Private Key File" at your CA-issued private key; the matching `-cert.pub` certificate is picked up automatically if it sits next to the key, or can be set explicitly via "SSH Certificate File" on the Remote Credentials options page. ## Future Features diff --git a/release-notes.md b/release-notes.md index 830e730..d637168 100644 --- a/release-notes.md +++ b/release-notes.md @@ -4,6 +4,17 @@ This document contains the release information for the project. +### 3.0.0 + +* Added: SSH CA certificate authentication (a private key with an accompanying `-cert.pub` certificate, auto-detected or set explicitly), in addition to password and plain private-key auth. +* Added: Sudo-elevated debugger launch, for debuggees running with elevated or ambient capabilities that the debugger must match to attach. +* Added: Configurable pre/post-deploy shell commands, and an "Attach to Already-Running Process" mode (with a configurable PID-lookup command) for debuggees managed by an external supervisor (i.e. a systemd service). +* Added: "Attach Only" menu item to reattach to a running/deployed process without rebuilding or redeploying. +* Added: A "Stop" button to cancel an in-progress build/deploy/debug between steps. +* Update: Deployment now always goes through `dotnet publish` (self-contained or framework-dependent), producing a real native executable either way, instead of uploading a plain build's output folder. +* Update: Options window split across focused Tools > Options pages (Remote Host, Remote Credentials, Remote Debugger, Remote Launch, Local) with a real WPF UI (checkboxes, dynamic show/hide for dependent fields) instead of a single page with a WinForms PropertyGrid. +* Update: Support for Visual Studio 2026 and newer .NET target frameworks (net8.0, net10.0). + ### 2.3.0 - 2024-12-02 Early patch release before Christmas break. Effort moving forward will be for the upcoming 3.0 release. diff --git a/sandbox/ConsoleNet6.sln b/sandbox/Console.sln similarity index 87% rename from sandbox/ConsoleNet6.sln rename to sandbox/Console.sln index f2928a1..6313930 100644 --- a/sandbox/ConsoleNet6.sln +++ b/sandbox/Console.sln @@ -3,7 +3,7 @@ Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 17 VisualStudioVersion = 17.2.32317.152 MinimumVisualStudioVersion = 10.0.40219.1 -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ConsoleNet6", "ConsoleNet6\ConsoleNet6.csproj", "{F181E39A-CCE1-45C1-B196-0CE759BB86AB}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Console", "Console\Console.csproj", "{F181E39A-CCE1-45C1-B196-0CE759BB86AB}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution diff --git a/sandbox/ConsoleNet6/ConsoleNet6.csproj b/sandbox/Console/Console.csproj similarity index 69% rename from sandbox/ConsoleNet6/ConsoleNet6.csproj rename to sandbox/Console/Console.csproj index 4d39f1d..58ef372 100644 --- a/sandbox/ConsoleNet6/ConsoleNet6.csproj +++ b/sandbox/Console/Console.csproj @@ -2,10 +2,9 @@ Exe - net6.0 + net8.0;net10.0 enable enable - false portable diff --git a/sandbox/ConsoleNet6/Program.cs b/sandbox/Console/Program.cs similarity index 100% rename from sandbox/ConsoleNet6/Program.cs rename to sandbox/Console/Program.cs diff --git a/sandbox/GuiNet6.sln b/sandbox/Gui.sln similarity index 88% rename from sandbox/GuiNet6.sln rename to sandbox/Gui.sln index e6d2b2f..56d2724 100644 --- a/sandbox/GuiNet6.sln +++ b/sandbox/Gui.sln @@ -3,7 +3,7 @@ Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 17 VisualStudioVersion = 17.3.32804.467 MinimumVisualStudioVersion = 10.0.40219.1 -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "GuiNet6", "GuiNet6\GuiNet6.csproj", "{98F164D1-51CF-4BC1-8F9D-8DFB541E57F5}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Gui", "Gui\Gui.csproj", "{98F164D1-51CF-4BC1-8F9D-8DFB541E57F5}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution diff --git a/sandbox/Gui/App.axaml b/sandbox/Gui/App.axaml new file mode 100644 index 0000000..0f926dc --- /dev/null +++ b/sandbox/Gui/App.axaml @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + diff --git a/sandbox/Gui/App.axaml.cs b/sandbox/Gui/App.axaml.cs new file mode 100644 index 0000000..0d69487 --- /dev/null +++ b/sandbox/Gui/App.axaml.cs @@ -0,0 +1,37 @@ +using Avalonia; +using Avalonia.Controls.ApplicationLifetimes; +using Avalonia.Markup.Xaml; +using Gui.ViewModels; +using Gui.Views; + +namespace Gui; + +public class App : Application +{ + /// App entry point. + public override void Initialize() + { + AvaloniaXamlLoader.Load(this); +#if DEBUG + this.AttachDeveloperTools(); +#endif + } + + /// Called once the Avalonia framework has finished initializing. + public override void OnFrameworkInitializationCompleted() + { + if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) + { + // Compose views/view-models directly - no DI container, no region manager. + var shellViewModel = new ShellWindowViewModel(); + var shellWindow = new ShellWindow + { + DataContext = shellViewModel, + }; + + desktop.MainWindow = shellWindow; + } + + base.OnFrameworkInitializationCompleted(); + } +} diff --git a/sandbox/GuiNet6/Assets/avalonia-logo.ico b/sandbox/Gui/Assets/avalonia-logo.ico similarity index 100% rename from sandbox/GuiNet6/Assets/avalonia-logo.ico rename to sandbox/Gui/Assets/avalonia-logo.ico diff --git a/sandbox/Gui/Gui.csproj b/sandbox/Gui/Gui.csproj new file mode 100644 index 0000000..80702a8 --- /dev/null +++ b/sandbox/Gui/Gui.csproj @@ -0,0 +1,22 @@ + + + WinExe + net8.0;net10.0 + enable + + + + + + + + + + + + + + ShellWindow.axaml + + + diff --git a/sandbox/GuiNet6/Program.cs b/sandbox/Gui/Program.cs similarity index 83% rename from sandbox/GuiNet6/Program.cs rename to sandbox/Gui/Program.cs index 490f7ad..5c194d5 100644 --- a/sandbox/GuiNet6/Program.cs +++ b/sandbox/Gui/Program.cs @@ -1,8 +1,8 @@ using System.Diagnostics.CodeAnalysis; using Avalonia; -using Avalonia.ReactiveUI; +using ReactiveUI.Avalonia; -namespace GuiNet6; +namespace Gui; internal class Program { @@ -17,11 +17,10 @@ public static AppBuilder BuildAvaloniaApp() => AppBuilder }) .With(new Win32PlatformOptions { - EnableMultitouch = true, - AllowEglInitialization = true, + RenderingMode = [Win32RenderingMode.AngleEgl, Win32RenderingMode.Software], }) .UseSkia() - .UseReactiveUI() + .UseReactiveUI(rxui => { }) .LogToTrace(); // Initialization code. Don't use any Avalonia, third-party APIs or any diff --git a/sandbox/GuiNet6/Properties/launchSettings.json b/sandbox/Gui/Properties/launchSettings.json similarity index 92% rename from sandbox/GuiNet6/Properties/launchSettings.json rename to sandbox/Gui/Properties/launchSettings.json index da8521b..efdff85 100644 --- a/sandbox/GuiNet6/Properties/launchSettings.json +++ b/sandbox/Gui/Properties/launchSettings.json @@ -1,6 +1,6 @@ { "profiles": { - "GuiNet6": { + "Gui": { "commandName": "Project" }, "WSL": { diff --git a/sandbox/Gui/ViewModels/DashboardViewModel.cs b/sandbox/Gui/ViewModels/DashboardViewModel.cs new file mode 100644 index 0000000..171f859 --- /dev/null +++ b/sandbox/Gui/ViewModels/DashboardViewModel.cs @@ -0,0 +1,21 @@ +using System.Windows.Input; +using ReactiveUI; + +namespace Gui.ViewModels; + +public class DashboardViewModel : ViewModelBase +{ + public DashboardViewModel() + { + Title = "Dashboard View!"; + + CmdBreakPoint = ReactiveCommand.Create(() => + { + // Force a breakpoint + System.Diagnostics.Debug.WriteLine("Breakpoint triggering"); + System.Diagnostics.Debugger.Break(); + }); + } + + public ICommand CmdBreakPoint { get; } +} diff --git a/sandbox/GuiNet6/ViewModels/SettingsViewModel.cs b/sandbox/Gui/ViewModels/SettingsViewModel.cs similarity index 78% rename from sandbox/GuiNet6/ViewModels/SettingsViewModel.cs rename to sandbox/Gui/ViewModels/SettingsViewModel.cs index 986ee0c..662872c 100644 --- a/sandbox/GuiNet6/ViewModels/SettingsViewModel.cs +++ b/sandbox/Gui/ViewModels/SettingsViewModel.cs @@ -1,4 +1,4 @@ -namespace GuiNet6.ViewModels; +namespace Gui.ViewModels; public class SettingsViewModel : ViewModelBase { diff --git a/sandbox/Gui/ViewModels/ShellWindowViewModel.cs b/sandbox/Gui/ViewModels/ShellWindowViewModel.cs new file mode 100644 index 0000000..28473d4 --- /dev/null +++ b/sandbox/Gui/ViewModels/ShellWindowViewModel.cs @@ -0,0 +1,35 @@ +using ReactiveUI; + +namespace Gui.ViewModels; + +public class ShellWindowViewModel : ViewModelBase +{ + private ViewModelBase _currentPage; + + public ShellWindowViewModel() + { + Title = "Sample Avalonia - Navigation"; + + Sidebar = new SidebarViewModel(NavigateTo); + + var dashboard = new DashboardViewModel(); + _currentPage = dashboard; + } + + /// Gets the sidebar view model hosted in this shell. + public SidebarViewModel Sidebar { get; } + + /// Gets or sets the view model currently displayed in the shell's content area. + public ViewModelBase CurrentPage + { + get => _currentPage; + set => this.RaiseAndSetIfChanged(ref _currentPage, value); + } + + /// Switches the content area to the given view model. + /// View model to display. + private void NavigateTo(ViewModelBase viewModel) + { + CurrentPage = viewModel; + } +} diff --git a/sandbox/Gui/ViewModels/SidebarViewModel.cs b/sandbox/Gui/ViewModels/SidebarViewModel.cs new file mode 100644 index 0000000..0ed6502 --- /dev/null +++ b/sandbox/Gui/ViewModels/SidebarViewModel.cs @@ -0,0 +1,24 @@ +using System; +using System.Windows.Input; +using ReactiveUI; + +namespace Gui.ViewModels; + +public class SidebarViewModel : ViewModelBase +{ + private readonly Action _navigate; + + public SidebarViewModel(Action navigate) + { + _navigate = navigate; + + Title = "Navigation"; + + CmdDashboard = ReactiveCommand.Create(() => _navigate(new DashboardViewModel())); + CmdSettings = ReactiveCommand.Create(() => _navigate(new SettingsViewModel())); + } + + public ICommand CmdDashboard { get; } + + public ICommand CmdSettings { get; } +} diff --git a/sandbox/Gui/ViewModels/ViewModelBase.cs b/sandbox/Gui/ViewModels/ViewModelBase.cs new file mode 100644 index 0000000..59888be --- /dev/null +++ b/sandbox/Gui/ViewModels/ViewModelBase.cs @@ -0,0 +1,15 @@ +using ReactiveUI; + +namespace Gui.ViewModels; + +public class ViewModelBase : ReactiveObject +{ + private string _title = string.Empty; + + /// Gets or sets the title of the View. + public string Title + { + get => _title; + set => this.RaiseAndSetIfChanged(ref _title, value); + } +} diff --git a/sandbox/GuiNet6/Views/DashboardView.axaml b/sandbox/Gui/Views/DashboardView.axaml similarity index 73% rename from sandbox/GuiNet6/Views/DashboardView.axaml rename to sandbox/Gui/Views/DashboardView.axaml index 91ec7a2..2e5a1e5 100644 --- a/sandbox/GuiNet6/Views/DashboardView.axaml +++ b/sandbox/Gui/Views/DashboardView.axaml @@ -2,13 +2,13 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" - xmlns:prism="http://prismlibrary.com/" - prism:ViewModelLocator.AutoWireViewModel="True" + xmlns:vm="using:Gui.ViewModels" mc:Ignorable="d" d:DesignWidth="400" d:DesignHeight="300" - x:Class="GuiNet6.Views.DashboardView"> + x:Class="Gui.Views.DashboardView" + x:DataType="vm:DashboardViewModel"> - + - + + - --> - - + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/VsLinuxDebugger/OptionsPages/LocalOptionsControl.xaml.cs b/src/VsLinuxDebugger/OptionsPages/LocalOptionsControl.xaml.cs new file mode 100644 index 0000000..bbb8cf9 --- /dev/null +++ b/src/VsLinuxDebugger/OptionsPages/LocalOptionsControl.xaml.cs @@ -0,0 +1,14 @@ +using System.Windows.Controls; + +namespace Xeno.VsLinuxDebug.OptionsPages +{ + /// WPF UI for . + public partial class LocalOptionsControl : UserControl + { + public LocalOptionsControl(LocalOptionsPage page) + { + InitializeComponent(); + DataContext = page; + } + } +} diff --git a/src/VsLinuxDebugger/OptionsPages/LocalOptionsPage.cs b/src/VsLinuxDebugger/OptionsPages/LocalOptionsPage.cs new file mode 100644 index 0000000..3b939b2 --- /dev/null +++ b/src/VsLinuxDebugger/OptionsPages/LocalOptionsPage.cs @@ -0,0 +1,77 @@ +using System.ComponentModel; +using System.Windows; +using Microsoft.VisualStudio.Shell; +using VsLinuxDebugger; + +namespace Xeno.VsLinuxDebug.OptionsPages +{ + /// Local-machine settings. + public class LocalOptionsPage : UIElementDialogPage, INotifyPropertyChanged + { + private bool _useSSHExeEnabled = true; + private string _plinkPath = ""; + private bool _deleteLaunchJsonAfterBuild = false; + private bool _autoSwitchLinuxDbgOutput = true; + private bool _forceKillOnStop = false; + + public event PropertyChangedEventHandler PropertyChanged; + + [Category("Local Settings")] + [DisplayName("Use SSH.exe instead of PLINK")] + [Description("Use SSH.exe (supports OpenSSH keys directly) instead of PLINK (needs a '.ppk' key).")] + public bool UseSSHExeEnabled + { + get => _useSSHExeEnabled; + set { _useSSHExeEnabled = value; OnPropertyChanged(nameof(UseSSHExeEnabled)); } + } + + [Category("Local Settings")] + [DisplayName("PLink Local Path (blank to use embedded)")] + [Description("Full path to local PLINK.EXE, used when 'Use SSH.exe' is unchecked.")] + public string PLinkPath + { + get => _plinkPath; + set { _plinkPath = value; OnPropertyChanged(nameof(PLinkPath)); } + } + + [Category("Local Settings")] + [DisplayName("Delete 'launch.json' after build.")] + [Description(@"The `launch.json` is generated in your build folder. You may keep this for debugging.")] + public bool DeleteLaunchJsonAfterBuild + { + get => _deleteLaunchJsonAfterBuild; + set { _deleteLaunchJsonAfterBuild = value; OnPropertyChanged(nameof(DeleteLaunchJsonAfterBuild)); } + } + + [Category("Local Settings")] + [DisplayName("Switch to LinuxDbg Output on Build")] + [Description("Automatically show output for Linux Debugger on build (default = true).")] + public bool SwitchLinuxDbgOutput + { + get => _autoSwitchLinuxDbgOutput; + set + { + Logger.AutoSwitchToLinuxDbgOutput = value; + _autoSwitchLinuxDbgOutput = value; + OnPropertyChanged(nameof(SwitchLinuxDbgOutput)); + } + } + + [Category("Local Settings")] + [DisplayName("Force kill on Stop (interrupt mid-command)")] + [Description("When Stop is used to cancel an in-progress build/deploy, also immediately " + + "close the SSH connection even if a remote command is currently running (default = false, " + + "which instead lets the current step finish before stopping, avoiding a half-uploaded or " + + "half-restarted remote state).")] + public bool ForceKillOnStop + { + get => _forceKillOnStop; + set { _forceKillOnStop = value; OnPropertyChanged(nameof(ForceKillOnStop)); } + } + + protected override UIElement Child => new LocalOptionsControl(this); + + private void OnPropertyChanged(string propertyName) + => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); + } +} diff --git a/src/VsLinuxDebugger/OptionsPages/MultilineEditDialog.xaml b/src/VsLinuxDebugger/OptionsPages/MultilineEditDialog.xaml new file mode 100644 index 0000000..c823ca4 --- /dev/null +++ b/src/VsLinuxDebugger/OptionsPages/MultilineEditDialog.xaml @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + +