diff --git a/docs/iseberg.md b/docs/iseberg.md index a651a6a..08ffdf0 100644 --- a/docs/iseberg.md +++ b/docs/iseberg.md @@ -92,11 +92,14 @@ PowerShell 7.4.0 through 7.4.5 cannot load the patched SDK's `System.Management.Automation` 7.4.6.500 assembly reference and are rejected up front. The child architecture does not need to match DT. Make `pwsh` available on PATH, or set `DT_ISEBERG_PSHOME` to its absolute installation directory before starting DT. -DT checks its version and child-runtime compatibility. A short noninteractive -`pwsh -NoProfile` probe discovers and validates the installation. DT then starts -an owned `pwsh -NoLogo -NoProfile` child and imports the absolute path to its -`Devolutions.Iseberg.PowerShell.dll` binary module. The module creates a custom-host execution -runspace; it does not replace PowerShell's bootstrap console host. +DT checks its version and child-runtime compatibility without a separate probe +process: DT starts an owned `pwsh -NoLogo -NoProfile` child whose bootstrap first +prints one line describing `$PSHOME`, its version, .NET runtime and architecture, +then imports the absolute path to its `Devolutions.Iseberg.PowerShell.dll` binary +module. DT validates that description while the module loads, so an installation +too old to load the module still fails with the version it found. The module +creates a custom-host execution runspace; it does not replace PowerShell's +bootstrap console host. Parser analysis, completion, execution, host input, debugging and portable `$psISE` proxies run in that child. The NativeAOT parent uses typed, source-generated diff --git a/scripts/Install-IsebergTestPowerShell.ps1 b/scripts/Install-IsebergTestPowerShell.ps1 index 5b6253c..42a7c8e 100644 --- a/scripts/Install-IsebergTestPowerShell.ps1 +++ b/scripts/Install-IsebergTestPowerShell.ps1 @@ -55,7 +55,9 @@ if (-not (Test-Path -LiteralPath $executable -PathType Leaf)) { New-Item -ItemType Directory -Path $engineDirectory -Force | Out-Null $asset = if ($IsWindows) { "PowerShell-$version-$rid.zip" } else { "powershell-$version-$rid.tar.gz" } $archive = Join-Path $root $asset - Invoke-WebRequest "https://github.com/PowerShell/PowerShell/releases/download/v$version/$asset" -OutFile $archive + # GitHub release downloads intermittently return 5xx pages; retry before failing the job. + Invoke-WebRequest "https://github.com/PowerShell/PowerShell/releases/download/v$version/$asset" -OutFile $archive ` + -MaximumRetryCount 4 -RetryIntervalSec 15 if ((Get-FileHash -LiteralPath $archive -Algorithm SHA256).Hash -ne $hashes[$rid]) { throw "PowerShell test runtime checksum mismatch: $archive" } diff --git a/src/Devolutions.Terminal.App/Connections/PowerShellInstallation.cs b/src/Devolutions.Terminal.App/Connections/PowerShellInstallation.cs deleted file mode 100644 index 50e3900..0000000 --- a/src/Devolutions.Terminal.App/Connections/PowerShellInstallation.cs +++ /dev/null @@ -1,92 +0,0 @@ -using System.ComponentModel; -using System.Diagnostics; -using System.Runtime.InteropServices; -using System.Text.Json; -using System.Text.Json.Serialization; -using Iseberg.Core; - -namespace Devolutions.Terminal.App.Connections; - -public sealed record PowerShellInstallation(string Home, string Version, string RuntimeVersion, string Architecture) -{ - public const string HomeVariable = "DT_ISEBERG_PSHOME"; - public static Version MinimumVersion => PowerShellCompatibility.MinimumVersion; - - public static PowerShellInstallation Find() - { - var home = Environment.GetEnvironmentVariable(HomeVariable); - var executable = OperatingSystem.IsWindows() ? "pwsh.exe" : "pwsh"; - var start = new ProcessStartInfo(string.IsNullOrWhiteSpace(home) ? executable : Path.Combine(home, executable)) - { - UseShellExecute = false, - CreateNoWindow = true, - RedirectStandardOutput = true, - RedirectStandardError = true - }; - foreach (var argument in new[] - { - "-NoLogo", "-NoProfile", "-NonInteractive", "-Command", - """ - [pscustomobject]@{ - Home = $PSHOME - Version = $PSVersionTable.PSVersion.ToString() - RuntimeVersion = [Environment]::Version.ToString() - Architecture = [System.Runtime.InteropServices.RuntimeInformation]::ProcessArchitecture.ToString() - } | ConvertTo-Json -Compress - """ - }) - start.ArgumentList.Add(argument); - - using var process = new Process { StartInfo = start }; - try { process.Start(); } - catch (Win32Exception exception) - { - throw new InvalidOperationException( - $"Install PowerShell {PowerShellCompatibility.SupportedVersions} on PATH, or set {HomeVariable} to its installation directory.", exception); - } - var output = process.StandardOutput.ReadToEndAsync(); - var error = process.StandardError.ReadToEndAsync(); - if (!process.WaitForExit(15000)) - { - process.Kill(entireProcessTree: true); - process.WaitForExit(); - throw new InvalidOperationException("PowerShell did not respond to the installation check within 15 seconds."); - } - if (process.ExitCode != 0) - throw new InvalidOperationException($"PowerShell installation check failed: {error.GetAwaiter().GetResult().Trim()}"); - PowerShellInstallation installation; - try - { - installation = JsonSerializer.Deserialize(output.GetAwaiter().GetResult(), - PowerShellInstallationJsonContext.Default.PowerShellInstallation) - ?? throw new InvalidOperationException("PowerShell returned an empty installation description."); - } - catch (JsonException exception) - { - throw new InvalidOperationException("PowerShell returned an invalid installation description.", exception); - } - installation.Validate(); - return installation; - } - - public void Validate() - { - if (!System.Version.TryParse(Version, out var version) || - !PowerShellCompatibility.IsSupportedVersion(version)) - throw new InvalidOperationException($"Iseberg requires PowerShell {PowerShellCompatibility.SupportedVersions}; found {Version}."); - if (!System.Version.TryParse(RuntimeVersion, out var runtime) || - !PowerShellCompatibility.IsSupportedRuntime(version, runtime)) - throw new InvalidOperationException($"PowerShell {version.Major}.{version.Minor} requires its bundled .NET {PowerShellCompatibility.RuntimeMajor(version)} runtime; found {RuntimeVersion}."); - if (!Enum.TryParse(Architecture, out var architecture) || - !Enum.IsDefined(architecture)) - throw new InvalidOperationException($"PowerShell returned an unknown process architecture: {Architecture}."); - if (string.IsNullOrWhiteSpace(Home) || !Path.IsPathFullyQualified(Home) || - !File.Exists(Path.Combine(Home, "System.Management.Automation.dll")) || - !File.Exists(Path.Combine(Home, "pwsh.dll"))) - throw new InvalidOperationException($"PowerShell installation is incomplete: {Home}."); - } - -} - -[JsonSerializable(typeof(PowerShellInstallation))] -internal sealed partial class PowerShellInstallationJsonContext : JsonSerializerContext; diff --git a/src/Devolutions.Terminal.App/Connections/PowerShellIseRuntime.cs b/src/Devolutions.Terminal.App/Connections/PowerShellIseRuntime.cs deleted file mode 100644 index 0ae80fc..0000000 --- a/src/Devolutions.Terminal.App/Connections/PowerShellIseRuntime.cs +++ /dev/null @@ -1,20 +0,0 @@ -#if POWERSHELL_ISE -namespace Devolutions.Terminal.App.Connections; - -public static class PowerShellIseRuntime -{ - private static readonly object Sync = new(); - private static Task? _initialization; - - public static Task InitializeAsync() - { - lock (Sync) - { - // A failed discovery can be retried after the user installs PowerShell or changes the override. - if (_initialization is null || _initialization.IsFaulted) - _initialization = Task.Run(() => PowerShellInstallation.Find()); - return _initialization; - } - } -} -#endif diff --git a/src/Devolutions.Terminal.App/Devolutions.Terminal.App.csproj b/src/Devolutions.Terminal.App/Devolutions.Terminal.App.csproj index 5c6bdb8..a58d617 100644 --- a/src/Devolutions.Terminal.App/Devolutions.Terminal.App.csproj +++ b/src/Devolutions.Terminal.App/Devolutions.Terminal.App.csproj @@ -8,9 +8,6 @@ $(DefineConstants);POWERSHELL_ISE - - - diff --git a/src/Devolutions.Terminal.App/Views/MainWindow.PowerShellIse.cs b/src/Devolutions.Terminal.App/Views/MainWindow.PowerShellIse.cs index ab794a4..3a36f9c 100644 --- a/src/Devolutions.Terminal.App/Views/MainWindow.PowerShellIse.cs +++ b/src/Devolutions.Terminal.App/Views/MainWindow.PowerShellIse.cs @@ -23,7 +23,7 @@ private async Task CreatePowerShellIseTabAsync( #if POWERSHELL_ISE if (_isClosed || _closeConfirmationPending || _applicationClosePending) throw new InvalidOperationException("Cannot open Iseberg while DT is closing."); - await PowerShellIseRuntime.InitializeAsync().ConfigureAwait(true); + // The session's own pwsh reports and validates its installation during startup. return await CreatePowerShellIseTabCoreAsync(profile, restored, regenerateIdentities).ConfigureAwait(true); #else await Task.CompletedTask; diff --git a/src/Devolutions.Terminal.App/Views/MainWindow.axaml.cs b/src/Devolutions.Terminal.App/Views/MainWindow.axaml.cs index 35cbec9..a158068 100644 --- a/src/Devolutions.Terminal.App/Views/MainWindow.axaml.cs +++ b/src/Devolutions.Terminal.App/Views/MainWindow.axaml.cs @@ -1801,6 +1801,16 @@ private void BeginTabDrag(TerminalTab tab, Control control, PointerPressedEventA return; } + // A press on a nested button (the close X) belongs to that button. This + // handler also sees handled events, so capturing here would steal the + // nested button's pointer capture and it would never raise Click. + if (e.Source is Visual source && + Avalonia.VisualTree.VisualExtensions.FindAncestorOfType