aboutsummaryrefslogtreecommitdiff
path: root/scripts
diff options
context:
space:
mode:
authorOgulcan Celik <ogulcancelik@gmail.com>2026-06-23 21:43:23 +0300
committerOgulcan Celik <ogulcancelik@gmail.com>2026-06-23 21:43:23 +0300
commita66f4c64e9d1591f042a94d6f3bef3b4298a4d1c (patch)
tree747de08b6ada4300afb4124da9feff0cc66b96a7 /scripts
parent5981ba426e8ed8f6785195a03cc0d8a190173e61 (diff)
fix: force system conpty on windows
refs #761
Diffstat (limited to 'scripts')
-rw-r--r--scripts/test_vendor_portable_pty.py103
-rw-r--r--scripts/windows_smoke_conpty_path.ps1105
2 files changed, 208 insertions, 0 deletions
diff --git a/scripts/test_vendor_portable_pty.py b/scripts/test_vendor_portable_pty.py
new file mode 100644
index 00000000..8a6d4ca1
--- /dev/null
+++ b/scripts/test_vendor_portable_pty.py
@@ -0,0 +1,103 @@
+from __future__ import annotations
+
+import json
+import subprocess
+import unittest
+from pathlib import Path
+
+
+class VendorPortablePtyTests(unittest.TestCase):
+ def test_vendored_tree_contains_required_upstream_files(self) -> None:
+ root = Path(__file__).resolve().parent.parent / "vendor" / "portable-pty"
+ required = [
+ root / "Cargo.toml",
+ root / "LICENSE.md",
+ root / "src" / "lib.rs",
+ root / "src" / "win" / "psuedocon.rs",
+ ]
+
+ missing = [str(path.relative_to(root)) for path in required if not path.exists()]
+ self.assertEqual(missing, [])
+
+ def test_cargo_patch_points_at_vendored_tree(self) -> None:
+ project_root = Path(__file__).resolve().parent.parent
+ cargo_toml = (project_root / "Cargo.toml").read_text()
+
+ self.assertIn('portable-pty = "=0.9.0"', cargo_toml)
+ self.assertIn("[patch.crates-io]", cargo_toml)
+ self.assertIn('portable-pty = { path = "vendor/portable-pty" }', cargo_toml)
+
+ def test_cargo_metadata_resolves_portable_pty_to_vendored_tree(self) -> None:
+ project_root = Path(__file__).resolve().parent.parent
+ result = subprocess.run(
+ ["cargo", "metadata", "--locked", "--format-version", "1"],
+ cwd=project_root,
+ text=True,
+ capture_output=True,
+ )
+ self.assertEqual(
+ result.returncode,
+ 0,
+ f"cargo metadata failed:\nstdout:\n{result.stdout}\nstderr:\n{result.stderr}",
+ )
+
+ metadata = json.loads(result.stdout)
+ packages = [
+ package
+ for package in metadata["packages"]
+ if package["name"] == "portable-pty" and package["version"] == "0.9.0"
+ ]
+ self.assertEqual(len(packages), 1)
+
+ manifest_path = Path(packages[0]["manifest_path"]).resolve()
+ expected = (project_root / "vendor" / "portable-pty" / "Cargo.toml").resolve()
+ self.assertEqual(manifest_path, expected)
+
+ def test_local_vendor_patches_are_listed_in_patch_index(self) -> None:
+ project_root = Path(__file__).resolve().parent.parent
+ index = project_root / "vendor" / "portable-pty.patches.md"
+ patch_dir = project_root / "vendor" / "patches" / "portable-pty"
+ patches = sorted(patch_dir.glob("*.patch"))
+
+ if not patches:
+ return
+
+ self.assertTrue(index.exists())
+ text = index.read_text()
+ missing = [
+ str(path.relative_to(project_root))
+ for path in patches
+ if str(path.relative_to(project_root)) not in text
+ ]
+ self.assertEqual(missing, [])
+
+ def test_local_vendor_patches_are_applied_to_vendored_tree(self) -> None:
+ project_root = Path(__file__).resolve().parent.parent
+ patch_dir = project_root / "vendor" / "patches" / "portable-pty"
+
+ for patch in sorted(patch_dir.glob("*.patch")):
+ result = subprocess.run(
+ ["git", "apply", "--check", "--reverse", str(patch.relative_to(project_root))],
+ cwd=project_root,
+ text=True,
+ capture_output=True,
+ )
+ self.assertEqual(
+ result.returncode,
+ 0,
+ f"{patch.relative_to(project_root)} is not applied cleanly:\n"
+ f"stdout:\n{result.stdout}\n"
+ f"stderr:\n{result.stderr}",
+ )
+
+ def test_windows_conpty_loader_does_not_probe_path_conpty_dll(self) -> None:
+ project_root = Path(__file__).resolve().parent.parent
+ source = project_root / "vendor" / "portable-pty" / "src" / "win" / "psuedocon.rs"
+ text = source.read_text()
+
+ self.assertIn('ConPtyFuncs::open(Path::new("kernel32.dll"))', text)
+ self.assertNotIn('Path::new("conpty.dll")', text)
+
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/scripts/windows_smoke_conpty_path.ps1 b/scripts/windows_smoke_conpty_path.ps1
new file mode 100644
index 00000000..d3fceb3f
--- /dev/null
+++ b/scripts/windows_smoke_conpty_path.ps1
@@ -0,0 +1,105 @@
+param(
+ [Parameter(Mandatory = $true)]
+ [string] $ExePath,
+
+ [string] $Session = "ci-windows-$([guid]::NewGuid().ToString('N'))"
+)
+
+$ErrorActionPreference = "Stop"
+
+function Invoke-Checked {
+ param([string] $Command, [string[]] $Arguments)
+ & $Command @Arguments
+ if ($LASTEXITCODE -ne 0) {
+ throw "command failed with exit code $LASTEXITCODE`: $Command $($Arguments -join ' ')"
+ }
+}
+
+$exe = (Resolve-Path $ExePath).Path
+$fakeDir = Join-Path ([System.IO.Path]::GetTempPath()) "herdr-fake-conpty-$([guid]::NewGuid().ToString('N'))"
+New-Item -ItemType Directory -Force $fakeDir | Out-Null
+
+$fakeSource = Join-Path $fakeDir "fake_conpty.rs"
+$fakeDll = Join-Path $fakeDir "conpty.dll"
+@'
+#![allow(non_snake_case)]
+
+use std::ffi::c_void;
+
+#[repr(C)]
+pub struct COORD {
+ pub X: i16,
+ pub Y: i16,
+}
+
+type HANDLE = *mut c_void;
+type HRESULT = i32;
+
+#[no_mangle]
+pub extern "system" fn CreatePseudoConsole(
+ _size: COORD,
+ _h_input: HANDLE,
+ _h_output: HANDLE,
+ _flags: u32,
+ _hpc: *mut HANDLE,
+) -> HRESULT {
+ -2147467259
+}
+
+#[no_mangle]
+pub extern "system" fn ResizePseudoConsole(_hpc: HANDLE, _size: COORD) -> HRESULT {
+ -2147467259
+}
+
+#[no_mangle]
+pub extern "system" fn ClosePseudoConsole(_hpc: HANDLE) {}
+'@ | Set-Content -NoNewline -Encoding utf8 $fakeSource
+
+Invoke-Checked rustc @("--crate-type", "cdylib", "--edition", "2021", $fakeSource, "-o", $fakeDll)
+
+$oldPath = $env:PATH
+$oldSession = $env:HERDR_SESSION
+$env:PATH = "$fakeDir;$oldPath"
+$env:HERDR_SESSION = $Session
+
+$server = $null
+try {
+ Invoke-Checked $exe @("--version")
+ & $exe --default-config | Out-Null
+ if ($LASTEXITCODE -ne 0) {
+ throw "command failed with exit code $LASTEXITCODE`: $exe --default-config"
+ }
+
+ $server = Start-Process -FilePath $exe -ArgumentList "server" -PassThru -WindowStyle Hidden
+ $deadline = (Get-Date).AddSeconds(10)
+ do {
+ Start-Sleep -Milliseconds 250
+ $status = & $exe status server 2>&1
+ if ($LASTEXITCODE -eq 0 -and (($status -join "`n") -match "status: running")) {
+ break
+ }
+ } while ((Get-Date) -lt $deadline)
+
+ if ((Get-Date) -ge $deadline) {
+ throw "server did not become ready"
+ }
+
+ Invoke-Checked $exe @("agent", "start", "smoke", "--", $env:ComSpec, "/K", "dir")
+ Start-Sleep -Seconds 2
+ $read = & $exe agent read smoke --source recent --lines 40 --format text
+ if ($LASTEXITCODE -ne 0) {
+ throw "command failed with exit code $LASTEXITCODE`: $exe agent read smoke"
+ }
+ $text = $read -join "`n"
+ if ($text -notmatch "File\(s\)" -or $text -notmatch "Dir\(s\)") {
+ throw "pane read did not include cmd.exe directory output: $text"
+ }
+} finally {
+ if ($null -ne $server) {
+ & $exe server stop 2>$null
+ Wait-Process -Id $server.Id -Timeout 10 -ErrorAction SilentlyContinue
+ }
+ $env:PATH = $oldPath
+ $env:HERDR_SESSION = $oldSession
+ Remove-Item -Recurse -Force $fakeDir -ErrorAction SilentlyContinue
+}