diff options
| author | Ogulcan Celik <ogulcancelik@gmail.com> | 2026-06-23 21:43:23 +0300 |
|---|---|---|
| committer | Ogulcan Celik <ogulcancelik@gmail.com> | 2026-06-23 21:43:23 +0300 |
| commit | a66f4c64e9d1591f042a94d6f3bef3b4298a4d1c (patch) | |
| tree | 747de08b6ada4300afb4124da9feff0cc66b96a7 | |
| parent | 5981ba426e8ed8f6785195a03cc0d8a190173e61 (diff) | |
fix: force system conpty on windows
refs #761
23 files changed, 3276 insertions, 36 deletions
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e6c55dc7..e19e8c94 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -138,35 +138,5 @@ jobs: shell: pwsh run: | $ErrorActionPreference = "Stop" - $env:HERDR_SESSION = "ci-windows-$env:GITHUB_RUN_ID-$env:GITHUB_RUN_ATTEMPT" $exe = Join-Path $PWD "target\x86_64-pc-windows-msvc\debug\herdr.exe" - - & $exe --version - & $exe --default-config | Out-Null - - $server = Start-Process -FilePath $exe -ArgumentList "server" -PassThru -WindowStyle Hidden - try { - $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" - } - - & $exe agent start smoke -- "$env:ComSpec" /K dir - Start-Sleep -Seconds 2 - $read = & $exe agent read smoke --source recent --lines 40 --format text - $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 { - & $exe server stop 2>$null - Wait-Process -Id $server.Id -Timeout 10 -ErrorAction SilentlyContinue - } + .\scripts\windows_smoke_conpty_path.ps1 -ExePath $exe -Session "ci-windows-$env:GITHUB_RUN_ID-$env:GITHUB_RUN_ATTEMPT" @@ -1066,8 +1066,6 @@ checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" [[package]] name = "portable-pty" version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4a596a2b3d2752d94f51fac2d4a96737b8705dddd311a32b9af47211f08671e" dependencies = [ "anyhow", "bitflags 1.3.2", @@ -19,7 +19,7 @@ crossterm = "0.29" ctrlc = "3" interprocess = "2.4.2" libc = "0.2" -portable-pty = "0.9" +portable-pty = "=0.9.0" png = "0.17" ratatui = { version = "0.30", features = ["unstable-rendered-line-info"] } regex = "1" @@ -32,6 +32,9 @@ tracing = "0.1.44" tracing-subscriber = { version = "0.3.23", features = ["env-filter"] } unicode-width = "0.2" +[patch.crates-io] +portable-pty = { path = "vendor/portable-pty" } + [target.'cfg(windows)'.dependencies] windows-sys = { version = "0.61.2", features = [ "Wdk_System_Threading", @@ -3,7 +3,7 @@ # Run tests test: cargo nextest run --locked --status-level fail --final-status-level fail --failure-output final --success-output never - python3 -m unittest scripts.test_agent_detection_manifest_check scripts.test_changelog scripts.test_preview scripts.test_vendor_libghostty_vt + python3 -m unittest scripts.test_agent_detection_manifest_check scripts.test_changelog scripts.test_preview scripts.test_vendor_libghostty_vt scripts.test_vendor_portable_pty just plugin-marketplace-test # Run one nextest filter, e.g. `just test-one codex_stale_working` @@ -27,7 +27,7 @@ windows-lint: # Check formatting + run unit tests + Windows target lint + maintenance script tests check: ci windows-lint - python3 -m unittest scripts.test_agent_detection_manifest_check scripts.test_changelog scripts.test_preview scripts.test_vendor_libghostty_vt + python3 -m unittest scripts.test_agent_detection_manifest_check scripts.test_changelog scripts.test_preview scripts.test_vendor_libghostty_vt scripts.test_vendor_portable_pty @echo "docs reminder: if this changes user-facing behavior, make sure the relevant release docs are updated or called out before release." # Install repo-local git hooks 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 +} diff --git a/vendor/patches/portable-pty/0001-force-system-conpty.patch b/vendor/patches/portable-pty/0001-force-system-conpty.patch new file mode 100644 index 00000000..6074df57 --- /dev/null +++ b/vendor/patches/portable-pty/0001-force-system-conpty.patch @@ -0,0 +1,37 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: herdr maintainers <maintainers@herdr.dev> +Date: Tue, 23 Jun 2026 00:00:00 +0000 +Subject: [PATCH] force system ConPTY + +Herdr should use the ConPTY functions exported by kernel32.dll and must not +load a bare conpty.dll from PATH. portable-pty's sideload probe is intentional +for WezTerm's bundled OpenConsole.exe/conpty.dll pair, but Herdr does not ship +that pair. + +Herdr issue: https://github.com/ogulcancelik/herdr/issues/761 +Vendored base: portable-pty 0.9.0 +--- + vendor/portable-pty/src/win/psuedocon.rs | 9 +-------- + 1 file changed, 1 insertion(+), 8 deletions(-) + +diff --git a/vendor/portable-pty/src/win/psuedocon.rs b/vendor/portable-pty/src/win/psuedocon.rs +index 0000000..0000000 100644 +--- a/vendor/portable-pty/src/win/psuedocon.rs ++++ b/vendor/portable-pty/src/win/psuedocon.rs +@@ -49,14 +49,7 @@ fn load_conpty() -> ConPtyFuncs { + "this system does not support conpty. Windows 10 October 2018 or newer is required", + ); +- +- // We prefer to use a sideloaded conpty.dll and openconsole.exe host deployed +- // alongside the application. We check for this after checking for kernel +- // support so that we don't try to proceed and do something crazy. +- if let Ok(sideloaded) = ConPtyFuncs::open(Path::new("conpty.dll")) { +- sideloaded +- } else { +- kernel +- } ++ ++ kernel + } + + lazy_static! { diff --git a/vendor/portable-pty.patches.md b/vendor/portable-pty.patches.md new file mode 100644 index 00000000..61645310 --- /dev/null +++ b/vendor/portable-pty.patches.md @@ -0,0 +1,41 @@ +# portable-pty local patches + +This file tracks intentional local changes applied on top of the vendored +`portable-pty` source. Remove a patch only when the upstream crate contains an +equivalent fix or exposes an option that lets Herdr keep the same behavior. + +## 0001 force system ConPTY + +status: active + +patch: `vendor/patches/portable-pty/0001-force-system-conpty.patch` + +herdr issue: https://github.com/ogulcancelik/herdr/issues/761 + +upstream discussion: none found + +upstream pr: none + +vendored base: `portable-pty 0.9.0` + +local files: + +- `vendor/portable-pty/src/win/psuedocon.rs` + +reason: `portable-pty` intentionally probes a bare `conpty.dll` after verifying +that `kernel32.dll` exports the ConPTY API. That is useful for WezTerm's bundled +`OpenConsole.exe` and `conpty.dll` pair, but Herdr does not ship that pair and +must not load another application's `conpty.dll` from `PATH`. + +remove when: upstream `portable-pty` no longer loads bare `conpty.dll` from the +DLL search path, upstream exposes a way for consumers to force system ConPTY, or +Herdr replaces the Windows PTY backend. + +verification: + +```sh +python3 -m unittest scripts.test_vendor_portable_pty +``` + +On Windows, also verify that pane creation succeeds when `PATH` contains a +directory with `conpty.dll`. diff --git a/vendor/portable-pty/Cargo.toml b/vendor/portable-pty/Cargo.toml new file mode 100644 index 00000000..b8d11230 --- /dev/null +++ b/vendor/portable-pty/Cargo.toml @@ -0,0 +1,120 @@ +# THIS FILE IS AUTOMATICALLY GENERATED BY CARGO +# +# When uploading crates to the registry Cargo will automatically +# "normalize" Cargo.toml files for maximal compatibility +# with all versions of Cargo and also rewrite `path` dependencies +# to registry (e.g., crates.io) dependencies. +# +# If you are reading this file be aware that the original Cargo.toml +# will likely look very different (and much more reasonable). +# See Cargo.toml.orig for the original contents. + +[package] +edition = "2018" +name = "portable-pty" +version = "0.9.0" +authors = ["Wez Furlong"] +build = false +autolib = false +autobins = false +autoexamples = false +autotests = false +autobenches = false +description = "Cross platform pty interface" +documentation = "https://docs.rs/portable-pty" +readme = false +license = "MIT" +repository = "https://github.com/wezterm/wezterm" +resolver = "2" + +[lib] +name = "portable_pty" +path = "src/lib.rs" + +[[example]] +name = "bash" +path = "examples/bash.rs" + +[[example]] +name = "narrow" +path = "examples/narrow.rs" + +[[example]] +name = "whoami" +path = "examples/whoami.rs" + +[[example]] +name = "whoami_async" +path = "examples/whoami_async.rs" + +[dependencies.anyhow] +version = "1.0" + +[dependencies.downcast-rs] +version = "1.0" + +[dependencies.filedescriptor] +version = "0.8.3" + +[dependencies.libc] +version = "0.2" + +[dependencies.log] +version = "0.4" + +[dependencies.nix] +version = "0.28" +features = [ + "term", + "fs", +] + +[dependencies.serde] +version = "1.0" +optional = true + +[dependencies.serde_derive] +version = "1.0" +optional = true + +[dependencies.serial2] +version = "0.2" + +[dependencies.shell-words] +version = "1.1" + +[dev-dependencies.futures] +version = "0.3" + +[dev-dependencies.smol] +version = "2.0" + +[features] +default = [] +serde_support = [ + "serde", + "serde_derive", +] + +[target."cfg(windows)".dependencies.bitflags] +version = "1.3" + +[target."cfg(windows)".dependencies.lazy_static] +version = "1.4" + +[target."cfg(windows)".dependencies.shared_library] +version = "0.1" + +[target."cfg(windows)".dependencies.winapi] +version = "0.3" +features = [ + "winuser", + "consoleapi", + "handleapi", + "fileapi", + "namedpipeapi", + "synchapi", +] + +[target."cfg(windows)".dependencies.winreg] +version = "0.10" diff --git a/vendor/portable-pty/Cargo.toml.orig b/vendor/portable-pty/Cargo.toml.orig new file mode 100644 index 00000000..0c3d6b8d --- /dev/null +++ b/vendor/portable-pty/Cargo.toml.orig @@ -0,0 +1,43 @@ +[package] +name = "portable-pty" +version = "0.9.0" +authors = ["Wez Furlong"] +edition = "2018" +repository = "https://github.com/wezterm/wezterm" +description = "Cross platform pty interface" +license = "MIT" +documentation = "https://docs.rs/portable-pty" + +[dependencies] +anyhow = "1.0" +downcast-rs = "1.0" +filedescriptor = { version="0.8.3", path = "../filedescriptor" } +log = "0.4" +libc = "0.2" +nix = {version="0.28", features=["term", "fs"]} +shell-words = "1.1" +serde_derive = {version="1.0", optional=true} +serde = {version="1.0", optional=true} +serial2 = "0.2" + +[features] +default = [] +serde_support = ["serde", "serde_derive"] + +[target."cfg(windows)".dependencies] +bitflags = "1.3" +lazy_static = "1.4" +shared_library = "0.1" +winapi = { version = "0.3", features = [ + "winuser", + "consoleapi", + "handleapi", + "fileapi", + "namedpipeapi", + "synchapi", +]} +winreg = "0.10" + +[dev-dependencies] +smol = "2.0" +futures = "0.3" diff --git a/vendor/portable-pty/LICENSE.md b/vendor/portable-pty/LICENSE.md new file mode 100644 index 00000000..7a93d12f --- /dev/null +++ b/vendor/portable-pty/LICENSE.md @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2018 Wez Furlong + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/vendor/portable-pty/examples/bash.rs b/vendor/portable-pty/examples/bash.rs new file mode 100644 index 00000000..2066b7ec --- /dev/null +++ b/vendor/portable-pty/examples/bash.rs @@ -0,0 +1,85 @@ +//! This example demonstrates how to spawn a Bash shell using the `portable_pty` crate. +//! based on pty/examples/whoami.rs + +use portable_pty::{CommandBuilder, NativePtySystem, PtySize, PtySystem}; +use std::io::{Read, Write}; +use std::sync::mpsc::channel; +use std::thread; + +fn main() { + let pty_system = NativePtySystem::default(); + + // Open the PTY with specified size. + let pair = pty_system + .openpty(PtySize { + rows: 24, + cols: 80, + pixel_width: 0, + pixel_height: 0, + }) + .unwrap(); + + // Set up the command to launch Bash. + let cmd = CommandBuilder::new("bash"); + let mut child = pair.slave.spawn_command(cmd).unwrap(); + + drop(pair.slave); + + // Set up channels for reading and writing. + let (tx, rx) = channel::<String>(); + let mut reader = pair.master.try_clone_reader().unwrap(); + let master_writer = pair.master.take_writer().unwrap(); + + // Thread to read from the PTY and send data to the main thread. + thread::spawn(move || { + let mut buffer = [0u8; 1024]; + loop { + match reader.read(&mut buffer) { + Ok(0) => break, // EOF + Ok(n) => { + let output = String::from_utf8_lossy(&buffer[..n]); + println!("{}", output); // Print to stdout for visibility. + } + Err(e) => { + eprintln!("Error reading from PTY: {}", e); + break; + } + } + } + }); + + // Thread to write input into the PTY. + let tx_writer = thread::spawn(move || { + handle_input_stream(rx, master_writer); + }); + + println!("You can now type commands for Bash (type 'exit' to quit):"); + + // Main thread sends user input to the writer thread. + loop { + let mut input = String::new(); + std::io::stdin().read_line(&mut input).unwrap(); + + if input.trim() == "exit" { + break; + } + + tx.send(input).unwrap(); + } + + drop(tx); + tx_writer.join().unwrap(); + + println!("Waiting for Bash to exit..."); + let status = child.wait().unwrap(); + println!("Bash exited with status: {:?}", status); +} + +fn handle_input_stream(rx: std::sync::mpsc::Receiver<String>, mut writer: Box<dyn Write + Send>) { + for input in rx.iter() { + if writer.write_all(input.as_bytes()).is_err() { + eprintln!("Error writing to PTY"); + break; + } + } +} diff --git a/vendor/portable-pty/examples/narrow.rs b/vendor/portable-pty/examples/narrow.rs new file mode 100644 index 00000000..5793b9b7 --- /dev/null +++ b/vendor/portable-pty/examples/narrow.rs @@ -0,0 +1,92 @@ +//! Runs a command with a fixed terminal size. +//! This is used by wezterm's doc building automation to keep +//! the --help output within a reasonable width +use portable_pty::{CommandBuilder, NativePtySystem, PtySize, PtySystem}; +use std::sync::mpsc::channel; + +fn main() { + let pty_system = NativePtySystem::default(); + + let pair = pty_system + .openpty(PtySize { + rows: 24, + cols: 80, + pixel_width: 0, + pixel_height: 0, + }) + .unwrap(); + + let mut args = std::env::args_os().skip(1); + + let mut cmd = CommandBuilder::new(args.next().unwrap()); + cmd.args(args); + + let mut child = pair.slave.spawn_command(cmd).unwrap(); + + // Release any handles owned by the slave: we don't need it now + // that we've spawned the child. + drop(pair.slave); + + // Read the output in another thread. + // This is important because it is easy to encounter a situation + // where read/write buffers fill and block either your process + // or the spawned process. + let (tx, rx) = channel(); + let mut reader = pair.master.try_clone_reader().unwrap(); + std::thread::spawn(move || { + // Consume the output from the child + let mut s = String::new(); + reader.read_to_string(&mut s).unwrap(); + tx.send(s).unwrap(); + }); + + { + // Obtain the writer. + // When the writer is dropped, EOF will be sent to + // the program that was spawned. + // It is important to take the writer even if you don't + // send anything to its stdin so that EOF can be + // generated, otherwise you risk deadlocking yourself. + let mut writer = pair.master.take_writer().unwrap(); + + if cfg!(target_os = "macos") { + // macOS quirk: the child and reader must be started and + // allowed a brief grace period to run before we allow + // the writer to drop. Otherwise, the data we send to + // the kernel to trigger EOF is interleaved with the + // data read by the reader! WTF!? + // This appears to be a race condition for very short + // lived processes on macOS. + // I'd love to find a more deterministic solution to + // this than sleeping. + std::thread::sleep(std::time::Duration::from_millis(20)); + } + + // This example doesn't need to write anything, but if you + // want to send data to the child, you'd set `to_write` to + // that data and do it like this: + let to_write = ""; + if !to_write.is_empty() { + // To avoid deadlock, wrt. reading and waiting, we send + // data to the stdin of the child in a different thread. + std::thread::spawn(move || { + writer.write_all(to_write.as_bytes()).unwrap(); + }); + } + } + + // Wait for the child to complete + eprintln!("child status: {:?}", child.wait().unwrap()); + + // Take care to drop the master after our processes are + // done, as some platforms get unhappy if it is dropped + // sooner than that. + drop(pair.master); + + // Now wait for the output to be read by our reader thread + let output = rx.recv().unwrap(); + + let output = output.replace("\r\n", "\n"); + + print!("{output}"); +} diff --git a/vendor/portable-pty/examples/whoami.rs b/vendor/portable-pty/examples/whoami.rs new file mode 100644 index 00000000..f7e9cca0 --- /dev/null +++ b/vendor/portable-pty/examples/whoami.rs @@ -0,0 +1,94 @@ +//! This is a conceptually simple example that spawns the `whoami` program +//! to print your username. It is made more complex because there are multiple +//! pipes involved and it is easy to get blocked/deadlocked if care and attention +//! is not paid to those pipes! +use portable_pty::{CommandBuilder, NativePtySystem, PtySize, PtySystem}; +use std::sync::mpsc::channel; + +fn main() { + let pty_system = NativePtySystem::default(); + + let pair = pty_system + .openpty(PtySize { + rows: 24, + cols: 80, + pixel_width: 0, + pixel_height: 0, + }) + .unwrap(); + + let cmd = CommandBuilder::new("whoami"); + let mut child = pair.slave.spawn_command(cmd).unwrap(); + + // Release any handles owned by the slave: we don't need it now + // that we've spawned the child. + drop(pair.slave); + + // Read the output in another thread. + // This is important because it is easy to encounter a situation + // where read/write buffers fill and block either your process + // or the spawned process. + let (tx, rx) = channel(); + let mut reader = pair.master.try_clone_reader().unwrap(); + std::thread::spawn(move || { + // Consume the output from the child + let mut s = String::new(); + reader.read_to_string(&mut s).unwrap(); + tx.send(s).unwrap(); + }); + + { + // Obtain the writer. + // When the writer is dropped, EOF will be sent to + // the program that was spawned. + // It is important to take the writer even if you don't + // send anything to its stdin so that EOF can be + // generated, otherwise you risk deadlocking yourself. + let mut writer = pair.master.take_writer().unwrap(); + + if cfg!(target_os = "macos") { + // macOS quirk: the child and reader must be started and + // allowed a brief grace period to run before we allow + // the writer to drop. Otherwise, the data we send to + // the kernel to trigger EOF is interleaved with the + // data read by the reader! WTF!? + // This appears to be a race condition for very short + // lived processes on macOS. + // I'd love to find a more deterministic solution to + // this than sleeping. + std::thread::sleep(std::time::Duration::from_millis(20)); + } + + // This example doesn't need to write anything, but if you + // want to send data to the child, you'd set `to_write` to + // that data and do it like this: + let to_write = ""; + if !to_write.is_empty() { + // To avoid deadlock, wrt. reading and waiting, we send + // data to the stdin of the child in a different thread. + std::thread::spawn(move || { + writer.write_all(to_write.as_bytes()).unwrap(); + }); + } + } + + // Wait for the child to complete + println!("child status: {:?}", child.wait().unwrap()); + + // Take care to drop the master after our processes are + // done, as some platforms get unhappy if it is dropped + // sooner than that. + drop(pair.master); + + // Now wait for the output to be read by our reader thread + let output = rx.recv().unwrap(); + + // We print with escapes escaped because the windows conpty + // implementation synthesizes title change escape sequences + // in the output stream and it can be confusing to see those + // printed out raw in another terminal. + print!("output: "); + for c in output.escape_debug() { + print!("{}", c); + } +} diff --git a/vendor/portable-pty/examples/whoami_async.rs b/vendor/portable-pty/examples/whoami_async.rs new file mode 100644 index 00000000..e32b6a9c --- /dev/null +++ b/vendor/portable-pty/examples/whoami_async.rs @@ -0,0 +1,73 @@ +use anyhow::anyhow; +use futures::prelude::*; +use portable_pty::{native_pty_system, CommandBuilder, PtySize}; + +// This example shows how to use the `smol` crate to use portable_pty +// in an asynchronous application. + +fn main() -> anyhow::Result<()> { + smol::block_on(async { + let pty_system = native_pty_system(); + + let pair = pty_system.openpty(PtySize { + rows: 24, + cols: 80, + pixel_width: 0, + pixel_height: 0, + })?; + + let cmd = CommandBuilder::new("whoami"); + + // Move the slave to another thread to block and spawn a + // command. + // Note that this implicitly drops slave and closes out + // file handles which is important to avoid deadlock + // when waiting for the child process! + let slave = pair.slave; + let mut child = smol::unblock(move || slave.spawn_command(cmd)).await?; + + { + // Obtain the writer. + // When the writer is dropped, EOF will be sent to + // the program that was spawned. + // It is important to take the writer even if you don't + // send anything to its stdin so that EOF can be + // generated, otherwise you risk deadlocking yourself. + let writer = pair.master.take_writer()?; + + // Explicitly generate EOF + drop(writer); + } + + println!( + "child status: {:?}", + smol::unblock(move || child + .wait() + .map_err(|e| anyhow!("waiting for child: {}", e))) + .await? + ); + + let reader = pair.master.try_clone_reader()?; + + // Take care to drop the master after our processes are + // done, as some platforms get unhappy if it is dropped + // sooner than that. + drop(pair.master); + + let mut lines = smol::io::BufReader::new(smol::Unblock::new(reader)).lines(); + while let Some(line) = lines.next().await { + let line = line.map_err(|e| anyhow!("problem reading line: {}", e))?; + // We print with escapes escaped because the windows conpty + // implementation synthesizes title change escape sequences + // in the output stream and it can be confusing to see those + // printed out raw in another terminal. + print!("output: len={} ", line.len()); + for c in line.escape_debug() { + print!("{}", c); + } + println!(); + } + + Ok(()) + }) +} diff --git a/vendor/portable-pty/src/cmdbuilder.rs b/vendor/portable-pty/src/cmdbuilder.rs new file mode 100644 index 00000000..b4ce50a8 --- /dev/null +++ b/vendor/portable-pty/src/cmdbuilder.rs @@ -0,0 +1,823 @@ +#[cfg(unix)] +use anyhow::Context; +#[cfg(feature = "serde_support")] +use serde_derive::*; +use std::collections::BTreeMap; +use std::ffi::{OsStr, OsString}; +#[cfg(windows)] +use std::os::windows::ffi::OsStrExt; +#[cfg(unix)] +use std::path::Component; +use std::path::Path; + +/// Used to deal with Windows having case-insensitive environment variables. +#[derive(Clone, Debug, PartialEq, PartialOrd)] +#[cfg_attr(feature = "serde_support", derive(Serialize, Deserialize))] +struct EnvEntry { + /// Whether or not this environment variable came from the base environment, + /// as opposed to having been explicitly set by the caller. + is_from_base_env: bool, + + /// For case-insensitive platforms, the environment variable key in its preferred casing. + preferred_key: OsString, + + /// The environment variable value. + value: OsString, +} + +impl EnvEntry { + fn map_key(k: OsString) -> OsString { + if cfg!(windows) { + // Best-effort lowercase transformation of an os string + match k.to_str() { + Some(s) => s.to_lowercase().into(), + None => k, + } + } else { + k + } + } +} + +#[cfg(unix)] +fn get_shell() -> String { + use nix::unistd::{access, AccessFlags}; + use std::ffi::CStr; + use std::str; + + let ent = unsafe { libc::getpwuid(libc::getuid()) }; + if !ent.is_null() { + let shell = unsafe { CStr::from_ptr((*ent).pw_shell) }; + match shell.to_str().map(str::to_owned) { + Err(err) => { + log::warn!( + "passwd database shell could not be \ + represented as utf-8: {err:#}, \ + falling back to /bin/sh" + ); + } + Ok(shell) => { + if let Err(err) = access(Path::new(&shell), AccessFlags::X_OK) { + log::warn!( + "passwd database shell={shell:?} which is \ + not executable ({err:#}), falling back to /bin/sh" + ); + } else { + return shell; + } + } + } + } + "/bin/sh".into() +} + +fn get_base_env() -> BTreeMap<OsString, EnvEntry> { + let mut env: BTreeMap<OsString, EnvEntry> = std::env::vars_os() + .map(|(key, value)| { + ( + EnvEntry::map_key(key.clone()), + EnvEntry { + is_from_base_env: true, + preferred_key: key, + value, + }, + ) + }) + .collect(); + + #[cfg(unix)] + { + let key = EnvEntry::map_key("SHELL".into()); + // Only set the value of SHELL if it isn't already set + if !env.contains_key(&key) { + env.insert( + EnvEntry::map_key("SHELL".into()), + EnvEntry { + is_from_base_env: true, + preferred_key: "SHELL".into(), + value: get_shell().into(), + }, + ); + } + } + + #[cfg(windows)] + { + use std::os::windows::ffi::OsStringExt; + use winapi::um::processenv::ExpandEnvironmentStringsW; + use winreg::enums::{RegType, HKEY_CURRENT_USER, HKEY_LOCAL_MACHINE}; + use winreg::types::FromRegValue; + use winreg::{RegKey, RegValue}; + + fn reg_value_to_string(value: &RegValue) -> anyhow::Result<OsString> { + match value.vtype { + RegType::REG_EXPAND_SZ => { + let src = unsafe { + std::slice::from_raw_parts( + value.bytes.as_ptr() as *const u16, + value.bytes.len() / 2, + ) + }; + let size = + unsafe { ExpandEnvironmentStringsW(src.as_ptr(), std::ptr::null_mut(), 0) }; + let mut buf = vec![0u16; size as usize + 1]; + unsafe { + ExpandEnvironmentStringsW(src.as_ptr(), buf.as_mut_ptr(), buf.len() as u32) + }; + + let mut buf = buf.as_slice(); + while let Some(0) = buf.last() { + buf = &buf[0..buf.len() - 1]; + } + Ok(OsString::from_wide(buf)) + } + _ => Ok(OsString::from_reg_value(value)?), + } + } + + if let Ok(sys_env) = RegKey::predef(HKEY_LOCAL_MACHINE) + .open_subkey("System\\CurrentControlSet\\Control\\Session Manager\\Environment") + { + for res in sys_env.enum_values() { + if let Ok((name, value)) = res { + if name.to_ascii_lowercase() == "username" { + continue; + } + if let Ok(value) = reg_value_to_string(&value) { + log::trace!("adding SYS env: {:?} {:?}", name, value); + env.insert( + EnvEntry::map_key(name.clone().into()), + EnvEntry { + is_from_base_env: true, + preferred_key: name.into(), + value, + }, + ); + } + } + } + } + + if let Ok(sys_env) = RegKey::predef(HKEY_CURRENT_USER).open_subkey("Environment") { + for res in sys_env.enum_values() { + if let Ok((name, value)) = res { + if let Ok(value) = reg_value_to_string(&value) { + // Merge the system and user paths together + let value = if name.to_ascii_lowercase() == "path" { + match env.get(&EnvEntry::map_key(name.clone().into())) { + Some(entry) => { + let mut result = OsString::new(); + result.push(&entry.value); + result.push(";"); + result.push(&value); + result + } + None => value, + } + } else { + value + }; + + log::trace!("adding USER env: {:?} {:?}", name, value); + env.insert( + EnvEntry::map_key(name.clone().into()), + EnvEntry { + is_from_base_env: true, + preferred_key: name.into(), + value, + }, + ); + } + } + } + } + } + + env +} + +/// `CommandBuilder` is used to prepare a command to be spawned into a pty. +/// The interface is intentionally similar to that of `std::process::Command`. +#[derive(Clone, Debug, PartialEq)] +#[cfg_attr(feature = "serde_support", derive(Serialize, Deserialize))] +pub struct CommandBuilder { + args: Vec<OsString>, + envs: BTreeMap<OsString, EnvEntry>, + cwd: Option<OsString>, + #[cfg(unix)] + pub(crate) umask: Option<libc::mode_t>, + controlling_tty: bool, +} + +impl CommandBuilder { + /// Create a new builder instance with argv\[0\] set to the specified + /// program. + pub fn new<S: AsRef<OsStr>>(program: S) -> Self { + Self { + args: vec![program.as_ref().to_owned()], + envs: get_base_env(), + cwd: None, + #[cfg(unix)] + umask: None, + controlling_tty: true, + } + } + + /// Create a new builder instance from a pre-built argument vector + pub fn from_argv(args: Vec<OsString>) -> Self { + Self { + args, + envs: get_base_env(), + cwd: None, + #[cfg(unix)] + umask: None, + controlling_tty: true, + } + } + + /// Set whether we should set the pty as the controlling terminal. + /// The default is true, which is usually what you want, but you + /// may need to set this to false if you are crossing container + /// boundaries (eg: flatpak) to workaround issues like: + /// <https://github.com/flatpak/flatpak/issues/3697> + /// <https://github.com/flatpak/flatpak/issues/3285> + pub fn set_controlling_tty(&mut self, controlling_tty: bool) { + self.controlling_tty = controlling_tty; + } + + pub fn get_controlling_tty(&self) -> bool { + self.controlling_tty + } + + /// Create a new builder instance that will run some idea of a default + /// program. Such a builder will panic if `arg` is called on it. + pub fn new_default_prog() -> Self { + Self { + args: vec![], + envs: get_base_env(), + cwd: None, + #[cfg(unix)] + umask: None, + controlling_tty: true, + } + } + + /// Returns true if this builder was created via `new_default_prog` + pub fn is_default_prog(&self) -> bool { + self.args.is_empty() + } + + /// Append an argument to the current command line. + /// Will panic if called on a builder created via `new_default_prog`. + pub fn arg<S: AsRef<OsStr>>(&mut self, arg: S) { + if self.is_default_prog() { + panic!("attempted to add args to a default_prog builder"); + } + self.args.push(arg.as_ref().to_owned()); + } + + /// Append a sequence of arguments to the current command line + pub fn args<I, S>(&mut self, args: I) + where + I: IntoIterator<Item = S>, + S: AsRef<OsStr>, + { + for arg in args { + self.arg(arg); + } + } + + pub fn get_argv(&self) -> &Vec<OsString> { + &self.args + } + + pub fn get_argv_mut(&mut self) -> &mut Vec<OsString> { + &mut self.args + } + + /// Override the value of an environmental variable + pub fn env<K, V>(&mut self, key: K, value: V) + where + K: AsRef<OsStr>, + V: AsRef<OsStr>, + { + let key: OsString = key.as_ref().into(); + let value: OsString = value.as_ref().into(); + self.envs.insert( + EnvEntry::map_key(key.clone()), + EnvEntry { + is_from_base_env: false, + preferred_key: key, + value: value, + }, + ); + } + + pub fn env_remove<K>(&mut self, key: K) + where + K: AsRef<OsStr>, + { + let key = key.as_ref().into(); + self.envs.remove(&EnvEntry::map_key(key)); + } + + pub fn env_clear(&mut self) { + self.envs.clear(); + } + + pub fn get_env<K>(&self, key: K) -> Option<&OsStr> + where + K: AsRef<OsStr>, + { + let key = key.as_ref().into(); + self.envs.get(&EnvEntry::map_key(key)).map( + |EnvEntry { + is_from_base_env: _, + preferred_key: _, + value, + }| value.as_os_str(), + ) + } + + pub fn cwd<D>(&mut self, dir: D) + where + D: AsRef<OsStr>, + { + self.cwd = Some(dir.as_ref().to_owned()); + } + + pub fn clear_cwd(&mut self) { + self.cwd.take(); + } + + pub fn get_cwd(&self) -> Option<&OsString> { + self.cwd.as_ref() + } + + /// Iterate over the configured environment. Only includes environment + /// variables set by the caller via `env`, not variables set in the base + /// environment. + pub fn iter_extra_env_as_str(&self) -> impl Iterator<Item = (&str, &str)> { + self.envs.values().filter_map( + |EnvEntry { + is_from_base_env, + preferred_key, + value, + }| { + if *is_from_base_env { + None + } else { + let key = preferred_key.to_str()?; + let value = value.to_str()?; + Some((key, value)) + } + }, + ) + } + + pub fn iter_full_env_as_str(&self) -> impl Iterator<Item = (&str, &str)> { + self.envs.values().filter_map( + |EnvEntry { + preferred_key, + value, + .. + }| { + let key = preferred_key.to_str()?; + let value = value.to_str()?; + Some((key, value)) + }, + ) + } + + /// Return the configured command and arguments as a single string, + /// quoted per the unix shell conventions. + pub fn as_unix_command_line(&self) -> anyhow::Result<String> { + let mut strs = vec![]; + for arg in &self.args { + let s = arg + .to_str() + .ok_or_else(|| anyhow::anyhow!("argument cannot be represented as utf8"))?; + strs.push(s); + } + Ok(shell_words::join(strs)) + } +} + +#[cfg(unix)] +impl CommandBuilder { + pub fn umask(&mut self, mask: Option<libc::mode_t>) { + self.umask = mask; + } + + fn resolve_path(&self) -> Option<&OsStr> { + self.get_env("PATH") + } + + fn search_path(&self, exe: &OsStr, cwd: &OsStr) -> anyhow::Result<OsString> { + use nix::unistd::{access, AccessFlags}; + + let exe_path: &Path = exe.as_ref(); + if exe_path.is_relative() { + let cwd: &Path = cwd.as_ref(); + let mut errors = vec![]; + + // If the requested executable is explicitly relative to cwd, + // then check only there. + if is_cwd_relative_path(exe_path) { + let abs_path = cwd.join(exe_path); + + if abs_path.is_dir() { + anyhow::bail!( + "Unable to spawn {} because it is a directory", + abs_path.display() + ); + } else if access(&abs_path, AccessFlags::X_OK).is_ok() { + return Ok(abs_path.into_os_string()); + } else if access(&abs_path, AccessFlags::F_OK).is_ok() { + anyhow::bail!( + "Unable to spawn {} because it is not executable", + abs_path.display() + ); + } + + anyhow::bail!( + "Unable to spawn {} because it does not exist", + abs_path.display() + ); + } + + if let Some(path) = self.resolve_path() { + for path in std::env::split_paths(&path) { + let candidate = cwd.join(&path).join(&exe); + + if candidate.is_dir() { + errors.push(format!("{} exists but is a directory", candidate.display())); + } else if access(&candidate, AccessFlags::X_OK).is_ok() { + return Ok(candidate.into_os_string()); + } else if access(&candidate, AccessFlags::F_OK).is_ok() { + errors.push(format!( + "{} exists but is not executable", + candidate.display() + )); + } + } + errors.push(format!("No viable candidates found in PATH {path:?}")); + } else { + errors.push("Unable to resolve the PATH".to_string()); + } + anyhow::bail!( + "Unable to spawn {} because:\n{}", + exe_path.display(), + errors.join(".\n") + ); + } else if exe_path.is_dir() { + anyhow::bail!( + "Unable to spawn {} because it is a directory", + exe_path.display() + ); + } else { + if let Err(err) = access(exe_path, AccessFlags::X_OK) { + if access(exe_path, AccessFlags::F_OK).is_ok() { + anyhow::bail!( + "Unable to spawn {} because it is not executable ({err:#})", + exe_path.display() + ); + } else { + anyhow::bail!( + "Unable to spawn {} because it doesn't exist on the filesystem ({err:#})", + exe_path.display() + ); + } + } + + Ok(exe.to_owned()) + } + } + + /// Convert the CommandBuilder to a `std::process::Command` instance. + pub(crate) fn as_command(&self) -> anyhow::Result<std::process::Command> { + use std::os::unix::process::CommandExt; + + let home = self.get_home_dir()?; + let dir: &OsStr = self + .cwd + .as_ref() + .map(|dir| dir.as_os_str()) + .filter(|dir| std::path::Path::new(dir).is_dir()) + .unwrap_or(home.as_ref()); + let shell = self.get_shell(); + + let mut cmd = if self.is_default_prog() { + let mut cmd = std::process::Command::new(&shell); + + // Run the shell as a login shell by prefixing the shell's + // basename with `-` and setting that as argv0 + let basename = shell.rsplit('/').next().unwrap_or(&shell); + cmd.arg0(&format!("-{}", basename)); + cmd + } else { + let resolved = self.search_path(&self.args[0], dir)?; + let mut cmd = std::process::Command::new(&resolved); + cmd.arg0(&self.args[0]); + cmd.args(&self.args[1..]); + cmd + }; + + cmd.current_dir(dir); + + cmd.env_clear(); + cmd.env("SHELL", shell); + cmd.envs(self.envs.values().map( + |EnvEntry { + is_from_base_env: _, + preferred_key, + value, + }| (preferred_key.as_os_str(), value.as_os_str()), + )); + + Ok(cmd) + } + + /// Determine which shell to run. + /// We take the contents of the $SHELL env var first, then + /// fall back to looking it up from the password database. + pub fn get_shell(&self) -> String { + use nix::unistd::{access, AccessFlags}; + + if let Some(shell) = self.get_env("SHELL").and_then(OsStr::to_str) { + match access(shell, AccessFlags::X_OK) { + Ok(()) => return shell.into(), + Err(err) => log::warn!( + "$SHELL -> {shell:?} which is \ + not executable ({err:#}), falling back to password db lookup" + ), + } + } + + get_shell().into() + } + + fn get_home_dir(&self) -> anyhow::Result<String> { + if let Some(home_dir) = self.get_env("HOME").and_then(OsStr::to_str) { + return Ok(home_dir.into()); + } + + let ent = unsafe { libc::getpwuid(libc::getuid()) }; + if ent.is_null() { + Ok("/".into()) + } else { + use std::ffi::CStr; + use std::str; + let home = unsafe { CStr::from_ptr((*ent).pw_dir) }; + home.to_str() + .map(str::to_owned) + .context("failed to resolve home dir") + } + } +} + +#[cfg(windows)] +impl CommandBuilder { + fn search_path(&self, exe: &OsStr) -> OsString { + if let Some(path) = self.get_env("PATH") { + let extensions = self.get_env("PATHEXT").unwrap_or(OsStr::new(".EXE")); + for path in std::env::split_paths(&path) { + // Check for exactly the user's string in this path dir + let candidate = path.join(&exe); + if candidate.exists() { + return candidate.into_os_string(); + } + + // otherwise try tacking on some extensions. + // Note that this really replaces the extension in the + // user specified path, so this is potentially wrong. + for ext in std::env::split_paths(&extensions) { + // PATHEXT includes the leading `.`, but `with_extension` + // doesn't want that + let ext = ext.to_str().expect("PATHEXT entries must be utf8"); + let path = path.join(&exe).with_extension(&ext[1..]); + if path.exists() { + return path.into_os_string(); + } + } + } + } + + exe.to_owned() + } + + pub(crate) fn current_directory(&self) -> Option<Vec<u16>> { + let home: Option<&OsStr> = self + .get_env("USERPROFILE") + .filter(|path| Path::new(path).is_dir()); + let cwd: Option<&OsStr> = self.cwd.as_deref().filter(|path| Path::new(path).is_dir()); + let dir: Option<&OsStr> = cwd.or(home); + + dir.map(|dir| { + let mut wide = vec![]; + + if Path::new(dir).is_relative() { + if let Ok(ccwd) = std::env::current_dir() { + wide.extend(ccwd.join(dir).as_os_str().encode_wide()); + } else { + wide.extend(dir.encode_wide()); + } + } else { + wide.extend(dir.encode_wide()); + } + + wide.push(0); + wide + }) + } + + /// Constructs an environment block for this spawn attempt. + /// Uses the current process environment as the base and then + /// adds/replaces the environment that was specified via the + /// `env` methods. + pub(crate) fn environment_block(&self) -> Vec<u16> { + // encode the environment as wide characters + let mut block = vec![]; + + for EnvEntry { + is_from_base_env: _, + preferred_key, + value, + } in self.envs.values() + { + block.extend(preferred_key.encode_wide()); + block.push(b'=' as u16); + block.extend(value.encode_wide()); + block.push(0); + } + // and a final terminator for CreateProcessW + block.push(0); + + block + } + + pub fn get_shell(&self) -> String { + let exe: OsString = self + .get_env("ComSpec") + .unwrap_or(OsStr::new("cmd.exe")) + .into(); + exe.into_string() + .unwrap_or_else(|_| "%CompSpec%".to_string()) + } + + pub(crate) fn cmdline(&self) -> anyhow::Result<(Vec<u16>, Vec<u16>)> { + let mut cmdline = Vec::<u16>::new(); + + let exe: OsString = if self.is_default_prog() { + self.get_env("ComSpec") + .unwrap_or(OsStr::new("cmd.exe")) + .into() + } else { + self.search_path(&self.args[0]) + }; + + Self::append_quoted(&exe, &mut cmdline); + + // Ensure that we nul terminate the module name, otherwise we'll + // ask CreateProcessW to start something random! + let mut exe: Vec<u16> = exe.encode_wide().collect(); + exe.push(0); + + for arg in self.args.iter().skip(1) { + cmdline.push(' ' as u16); + anyhow::ensure!( + !arg.encode_wide().any(|c| c == 0), + "invalid encoding for command line argument {:?}", + arg + ); + Self::append_quoted(arg, &mut cmdline); + } + // Ensure that the command line is nul terminated too! + cmdline.push(0); + Ok((exe, cmdline)) + } + + // Borrowed from https://github.com/hniksic/rust-subprocess/blob/873dfed165173e52907beb87118b2c0c05d8b8a1/src/popen.rs#L1117 + // which in turn was translated from ArgvQuote at http://tinyurl.com/zmgtnls + fn append_quoted(arg: &OsStr, cmdline: &mut Vec<u16>) { + if !arg.is_empty() + && !arg.encode_wide().any(|c| { + c == ' ' as u16 + || c == '\t' as u16 + || c == '\n' as u16 + || c == '\x0b' as u16 + || c == '\"' as u16 + }) + { + cmdline.extend(arg.encode_wide()); + return; + } + cmdline.push('"' as u16); + + let arg: Vec<_> = arg.encode_wide().collect(); + let mut i = 0; + while i < arg.len() { + let mut num_backslashes = 0; + while i < arg.len() && arg[i] == '\\' as u16 { + i += 1; + num_backslashes += 1; + } + + if i == arg.len() { + for _ in 0..num_backslashes * 2 { + cmdline.push('\\' as u16); + } + break; + } else if arg[i] == b'"' as u16 { + for _ in 0..num_backslashes * 2 + 1 { + cmdline.push('\\' as u16); + } + cmdline.push(arg[i]); + } else { + for _ in 0..num_backslashes { + cmdline.push('\\' as u16); + } + cmdline.push(arg[i]); + } + i += 1; + } + cmdline.push('"' as u16); + } +} + +#[cfg(unix)] +/// Returns true if the path begins with `./` or `../` +fn is_cwd_relative_path<P: AsRef<Path>>(p: P) -> bool { + matches!( + p.as_ref().components().next(), + Some(Component::CurDir | Component::ParentDir) + ) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[cfg(unix)] + #[test] + fn test_cwd_relative() { + assert!(is_cwd_relative_path(".")); + assert!(is_cwd_relative_path("./foo")); + assert!(is_cwd_relative_path("../foo")); + assert!(!is_cwd_relative_path("foo")); + assert!(!is_cwd_relative_path("/foo")); + } + + #[test] + fn test_env() { + let mut cmd = CommandBuilder::new("dummy"); + let package_authors = cmd.get_env("CARGO_PKG_AUTHORS"); + println!("package_authors: {:?}", package_authors); + assert!(package_authors == Some(OsStr::new("Wez Furlong"))); + + cmd.env("foo key", "foo value"); + cmd.env("bar key", "bar value"); + + let iterated_envs = cmd.iter_extra_env_as_str().collect::<Vec<_>>(); + println!("iterated_envs: {:?}", iterated_envs); + assert!(iterated_envs == vec![("bar key", "bar value"), ("foo key", "foo value")]); + + { + let mut cmd = cmd.clone(); + cmd.env_remove("foo key"); + + let iterated_envs = cmd.iter_extra_env_as_str().collect::<Vec<_>>(); + println!("iterated_envs: {:?}", iterated_envs); + assert!(iterated_envs == vec![("bar key", "bar value")]); + } + + { + let mut cmd = cmd.clone(); + cmd.env_remove("bar key"); + + let iterated_envs = cmd.iter_extra_env_as_str().collect::<Vec<_>>(); + println!("iterated_envs: {:?}", iterated_envs); + assert!(iterated_envs == vec![("foo key", "foo value")]); + } + + { + let mut cmd = cmd.clone(); + cmd.env_clear(); + + let iterated_envs = cmd.iter_extra_env_as_str().collect::<Vec<_>>(); + println!("iterated_envs: {:?}", iterated_envs); + assert!(iterated_envs.is_empty()); + } + } + + #[cfg(windows)] + #[test] + fn test_env_case_insensitive_override() { + let mut cmd = CommandBuilder::new("dummy"); + cmd.env("Cargo_Pkg_Authors", "Not Wez"); + assert!(cmd.get_env("cargo_pkg_authors") == Some(OsStr::new("Not Wez"))); + + cmd.env_remove("cARGO_pKG_aUTHORS"); + assert!(cmd.get_env("CARGO_PKG_AUTHORS").is_none()); + } +} diff --git a/vendor/portable-pty/src/lib.rs b/vendor/portable-pty/src/lib.rs new file mode 100644 index 00000000..3c04da4b --- /dev/null +++ b/vendor/portable-pty/src/lib.rs @@ -0,0 +1,407 @@ +//! This crate provides a cross platform API for working with the +//! psuedo terminal (pty) interfaces provided by the system. +//! Unlike other crates in this space, this crate provides a set +//! of traits that allow selecting from different implementations +//! at runtime. +//! This crate is part of [wezterm](https://github.com/wezterm/wezterm). +//! +//! ```no_run +//! use portable_pty::{CommandBuilder, PtySize, native_pty_system, PtySystem}; +//! use anyhow::Error; +//! +//! // Use the native pty implementation for the system +//! let pty_system = native_pty_system(); +//! +//! // Create a new pty +//! let mut pair = pty_system.openpty(PtySize { +//! rows: 24, +//! cols: 80, +//! // Not all systems support pixel_width, pixel_height, +//! // but it is good practice to set it to something +//! // that matches the size of the selected font. That +//! // is more complex than can be shown here in this +//! // brief example though! +//! pixel_width: 0, +//! pixel_height: 0, +//! })?; +//! +//! // Spawn a shell into the pty +//! let cmd = CommandBuilder::new("bash"); +//! let child = pair.slave.spawn_command(cmd)?; +//! +//! // Read and parse output from the pty with reader +//! let mut reader = pair.master.try_clone_reader()?; +//! +//! // Send data to the pty by writing to the master +//! writeln!(pair.master.take_writer()?, "ls -l\r\n")?; +//! # Ok::<(), Error>(()) +//! ``` +//! +use anyhow::Error; +use downcast_rs::{impl_downcast, Downcast}; +#[cfg(unix)] +use libc; +#[cfg(feature = "serde_support")] +use serde_derive::*; +use std::io::Result as IoResult; +#[cfg(windows)] +use std::os::windows::prelude::{AsRawHandle, RawHandle}; + +pub mod cmdbuilder; +pub use cmdbuilder::CommandBuilder; + +#[cfg(unix)] +pub mod unix; +#[cfg(windows)] +pub mod win; + +pub mod serial; + +/// Represents the size of the visible display area in the pty +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[cfg_attr(feature = "serde_support", derive(Serialize, Deserialize))] +pub struct PtySize { + /// The number of lines of text + pub rows: u16, + /// The number of columns of text + pub cols: u16, + /// The width of a cell in pixels. Note that some systems never + /// fill this value and ignore it. + pub pixel_width: u16, + /// The height of a cell in pixels. Note that some systems never + /// fill this value and ignore it. + pub pixel_height: u16, +} + +impl Default for PtySize { + fn default() -> Self { + PtySize { + rows: 24, + cols: 80, + pixel_width: 0, + pixel_height: 0, + } + } +} + +/// Represents the master/control end of the pty +pub trait MasterPty: Downcast + Send { + /// Inform the kernel and thus the child process that the window resized. + /// It will update the winsize information maintained by the kernel, + /// and generate a signal for the child to notice and update its state. + fn resize(&self, size: PtySize) -> Result<(), Error>; + /// Retrieves the size of the pty as known by the kernel + fn get_size(&self) -> Result<PtySize, Error>; + /// Obtain a readable handle; output from the slave(s) is readable + /// via this stream. + fn try_clone_reader(&self) -> Result<Box<dyn std::io::Read + Send>, Error>; + /// Obtain a writable handle; writing to it will send data to the + /// slave end. + /// Dropping the writer will send EOF to the slave end. + /// It is invalid to take the writer more than once. + fn take_writer(&self) -> Result<Box<dyn std::io::Write + Send>, Error>; + + /// If applicable to the type of the tty, return the local process id + /// of the process group or session leader + #[cfg(unix)] + fn process_group_leader(&self) -> Option<libc::pid_t>; + + /// If get_termios() and process_group_leader() are both implemented and + /// return Some, then as_raw_fd() should return the same underlying fd + /// associated with the stream. This is to enable applications that + /// "know things" to query similar information for themselves. + #[cfg(unix)] + fn as_raw_fd(&self) -> Option<unix::RawFd>; + + #[cfg(unix)] + fn tty_name(&self) -> Option<std::path::PathBuf>; + + /// If applicable to the type of the tty, return the termios + /// associated with the stream + #[cfg(unix)] + fn get_termios(&self) -> Option<nix::sys::termios::Termios> { + None + } +} +impl_downcast!(MasterPty); + +/// Represents a child process spawned into the pty. +/// This handle can be used to wait for or terminate that child process. +pub trait Child: std::fmt::Debug + ChildKiller + Downcast + Send { + /// Poll the child to see if it has completed. + /// Does not block. + /// Returns None if the child has not yet terminated, + /// else returns its exit status. + fn try_wait(&mut self) -> IoResult<Option<ExitStatus>>; + /// Blocks execution until the child process has completed, + /// yielding its exit status. + fn wait(&mut self) -> IoResult<ExitStatus>; + /// Returns the process identifier of the child process, + /// if applicable + fn process_id(&self) -> Option<u32>; + /// Returns the process handle of the child process, if applicable. + /// Only available on Windows. + #[cfg(windows)] + fn as_raw_handle(&self) -> Option<std::os::windows::io::RawHandle>; +} +impl_downcast!(Child); + +/// Represents the ability to signal a Child to terminate +pub trait ChildKiller: std::fmt::Debug + Downcast + Send { + /// Terminate the child process + fn kill(&mut self) -> IoResult<()>; + + /// Clone an object that can be split out from the Child in order + /// to send it signals independently from a thread that may be + /// blocked in `.wait`. + fn clone_killer(&self) -> Box<dyn ChildKiller + Send + Sync>; +} +impl_downcast!(ChildKiller); + +/// Represents the slave side of a pty. +/// Can be used to spawn processes into the pty. +pub trait SlavePty { + /// Spawns the command specified by the provided CommandBuilder + fn spawn_command(&self, cmd: CommandBuilder) -> Result<Box<dyn Child + Send + Sync>, Error>; +} + +/// Represents the exit status of a child process. +#[derive(Debug, Clone)] +pub struct ExitStatus { + code: u32, + signal: Option<String>, +} + +impl ExitStatus { + /// Construct an ExitStatus from a process return code + pub fn with_exit_code(code: u32) -> Self { + Self { code, signal: None } + } + + /// Construct an ExitStatus from a signal name + pub fn with_signal(signal: &str) -> Self { + Self { + code: 1, + signal: Some(signal.to_string()), + } + } + + /// Returns true if the status indicates successful completion + pub fn success(&self) -> bool { + match self.signal { + None => self.code == 0, + Some(_) => false, + } + } + + /// Returns the exit code that this ExitStatus was constructed with + pub fn exit_code(&self) -> u32 { + self.code + } + + /// Returns the signal if present that this ExitStatus was constructed with + pub fn signal(&self) -> Option<&str> { + self.signal.as_deref() + } +} + +impl From<std::process::ExitStatus> for ExitStatus { + fn from(status: std::process::ExitStatus) -> ExitStatus { + #[cfg(unix)] + { + use std::os::unix::process::ExitStatusExt; + + if let Some(signal) = status.signal() { + let signame = unsafe { libc::strsignal(signal) }; + let signal = if signame.is_null() { + format!("Signal {}", signal) + } else { + let signame = unsafe { std::ffi::CStr::from_ptr(signame) }; + signame.to_string_lossy().to_string() + }; + + return ExitStatus { + code: status.code().map(|c| c as u32).unwrap_or(1), + signal: Some(signal), + }; + } + } + + let code = + status + .code() + .map(|c| c as u32) + .unwrap_or_else(|| if status.success() { 0 } else { 1 }); + + ExitStatus { code, signal: None } + } +} + +impl std::fmt::Display for ExitStatus { + fn fmt(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result { + if self.success() { + write!(fmt, "Success") + } else { + match &self.signal { + Some(sig) => write!(fmt, "Terminated by {}", sig), + None => write!(fmt, "Exited with code {}", self.code), + } + } + } +} + +pub struct PtyPair { + // slave is listed first so that it is dropped first. + // The drop order is stable and specified by rust rfc 1857 + pub slave: Box<dyn SlavePty + Send>, + pub master: Box<dyn MasterPty + Send>, +} + +/// The `PtySystem` trait allows an application to work with multiple +/// possible Pty implementations at runtime. This is important on +/// Windows systems which have a variety of implementations. +pub trait PtySystem: Downcast { + /// Create a new Pty instance with the window size set to the specified + /// dimensions. Returns a (master, slave) Pty pair. The master side + /// is used to drive the slave side. + fn openpty(&self, size: PtySize) -> anyhow::Result<PtyPair>; +} +impl_downcast!(PtySystem); + +impl Child for std::process::Child { + fn try_wait(&mut self) -> IoResult<Option<ExitStatus>> { + std::process::Child::try_wait(self).map(|s| match s { + Some(s) => Some(s.into()), + None => None, + }) + } + + fn wait(&mut self) -> IoResult<ExitStatus> { + std::process::Child::wait(self).map(Into::into) + } + + fn process_id(&self) -> Option<u32> { + Some(self.id()) + } + + #[cfg(windows)] + fn as_raw_handle(&self) -> Option<std::os::windows::io::RawHandle> { + Some(std::os::windows::io::AsRawHandle::as_raw_handle(self)) + } +} + +#[derive(Debug)] +struct ProcessSignaller { + pid: Option<u32>, + + #[cfg(windows)] + handle: Option<filedescriptor::OwnedHandle>, +} + +#[cfg(windows)] +impl ChildKiller for ProcessSignaller { + fn kill(&mut self) -> IoResult<()> { + if let Some(handle) = &self.handle { + unsafe { + if winapi::um::processthreadsapi::TerminateProcess(handle.as_raw_handle() as _, 127) + == 0 + { + return Err(std::io::Error::last_os_error()); + } + } + } + Ok(()) + } + fn clone_killer(&self) -> Box<dyn ChildKiller + Send + Sync> { + Box::new(Self { + pid: self.pid, + handle: self.handle.as_ref().and_then(|h| h.try_clone().ok()), + }) + } +} + +#[cfg(unix)] +impl ChildKiller for ProcessSignaller { + fn kill(&mut self) -> IoResult<()> { + if let Some(pid) = self.pid { + let result = unsafe { libc::kill(pid as i32, libc::SIGHUP) }; + if result != 0 { + return Err(std::io::Error::last_os_error()); + } + } + Ok(()) + } + + fn clone_killer(&self) -> Box<dyn ChildKiller + Send + Sync> { + Box::new(Self { pid: self.pid }) + } +} + +impl ChildKiller for std::process::Child { + fn kill(&mut self) -> IoResult<()> { + #[cfg(unix)] + { + // On unix, we send the SIGHUP signal instead of trying to kill + // the process. The default behavior of a process receiving this + // signal is to be killed unless it configured a signal handler. + let result = unsafe { libc::kill(self.id() as i32, libc::SIGHUP) }; + if result != 0 { + return Err(std::io::Error::last_os_error()); + } + + // We successfully delivered SIGHUP, but the semantics of Child::kill + // are that on success the process is dead or shortly about to + // terminate. Since SIGUP doesn't guarantee termination, we + // give the process a bit of a grace period to shutdown or do whatever + // it is doing in its signal handler befre we proceed with the + // full on kill. + for attempt in 0..5 { + if attempt > 0 { + std::thread::sleep(std::time::Duration::from_millis(50)); + } + + if let Ok(Some(_)) = self.try_wait() { + // It completed, so report success! + return Ok(()); + } + } + + // it's still alive after a grace period, so proceed with a kill + } + + std::process::Child::kill(self) + } + + #[cfg(windows)] + fn clone_killer(&self) -> Box<dyn ChildKiller + Send + Sync> { + struct RawDup(RawHandle); + impl AsRawHandle for RawDup { + fn as_raw_handle(&self) -> RawHandle { + self.0 + } + } + + Box::new(ProcessSignaller { + pid: self.process_id(), + handle: Child::as_raw_handle(self) + .as_ref() + .and_then(|h| filedescriptor::OwnedHandle::dup(&RawDup(*h)).ok()), + }) + } + + #[cfg(unix)] + fn clone_killer(&self) -> Box<dyn ChildKiller + Send + Sync> { + Box::new(ProcessSignaller { + pid: self.process_id(), + }) + } +} + +pub fn native_pty_system() -> Box<dyn PtySystem + Send> { + Box::new(NativePtySystem::default()) +} + +#[cfg(unix)] +pub type NativePtySystem = unix::UnixPtySystem; +#[cfg(windows)] +pub type NativePtySystem = win::conpty::ConPtySystem; diff --git a/vendor/portable-pty/src/serial.rs b/vendor/portable-pty/src/serial.rs new file mode 100644 index 00000000..948ba108 --- /dev/null +++ b/vendor/portable-pty/src/serial.rs @@ -0,0 +1,305 @@ +//! This module implements a serial port based tty. +//! This is a bit different from the other implementations in that +//! we cannot explicitly spawn a process into the serial connection, +//! so we can only use a `CommandBuilder::new_default_prog` with the +//! `openpty` method. +//! On most (all?) systems, attempting to open multiple instances of +//! the same serial port will fail. +use crate::{ + Child, ChildKiller, CommandBuilder, ExitStatus, MasterPty, PtyPair, PtySize, PtySystem, + SlavePty, +}; +use anyhow::{ensure, Context}; +use filedescriptor::FileDescriptor; +use serial2::{CharSize, FlowControl, Parity, SerialPort, StopBits}; +use std::cell::RefCell; +use std::ffi::{OsStr, OsString}; +use std::io::{Read, Result as IoResult, Write}; +#[cfg(unix)] +use std::path::PathBuf; +use std::sync::Arc; +use std::time::Duration; + +type Handle = Arc<SerialPort>; + +pub struct SerialTty { + port: OsString, + baud: u32, + char_size: CharSize, + parity: Parity, + stop_bits: StopBits, + flow_control: FlowControl, +} + +impl SerialTty { + pub fn new<T: AsRef<OsStr> + ?Sized>(port: &T) -> Self { + Self { + port: port.as_ref().to_owned(), + baud: 9600, + char_size: CharSize::Bits8, + parity: Parity::None, + stop_bits: StopBits::One, + flow_control: FlowControl::XonXoff, + } + } + + pub fn set_baud_rate(&mut self, baud: u32) { + self.baud = baud; + } + + pub fn set_char_size(&mut self, char_size: CharSize) { + self.char_size = char_size; + } + + pub fn set_parity(&mut self, parity: Parity) { + self.parity = parity; + } + + pub fn set_stop_bits(&mut self, stop_bits: StopBits) { + self.stop_bits = stop_bits; + } + + pub fn set_flow_control(&mut self, flow_control: FlowControl) { + self.flow_control = flow_control; + } +} + +impl PtySystem for SerialTty { + fn openpty(&self, _size: PtySize) -> anyhow::Result<PtyPair> { + let mut port = SerialPort::open(&self.port, self.baud) + .with_context(|| format!("openpty on serial port {:?}", self.port))?; + + let mut settings = port.get_configuration()?; + settings.set_raw(); + settings.set_baud_rate(self.baud)?; + settings.set_char_size(self.char_size); + settings.set_flow_control(self.flow_control); + settings.set_parity(self.parity); + settings.set_stop_bits(self.stop_bits); + log::debug!("serial settings: {:#?}", port.get_configuration()); + port.set_configuration(&settings)?; + + // The timeout needs to be rather short because, at least on Windows, + // a read with a long timeout will block a concurrent write from + // happening. In wezterm we tend to have a thread looping on read + // while writes happen occasionally from the gui thread, and if we + // make this timeout too long we can block the gui thread. + port.set_read_timeout(Duration::from_millis(50))?; + port.set_write_timeout(Duration::from_millis(50))?; + + let port: Handle = Arc::new(port); + + Ok(PtyPair { + slave: Box::new(Slave { + port: Arc::clone(&port), + }), + master: Box::new(Master { + port, + took_writer: RefCell::new(false), + }), + }) + } +} + +struct Slave { + port: Handle, +} + +impl SlavePty for Slave { + fn spawn_command(&self, cmd: CommandBuilder) -> anyhow::Result<Box<dyn Child + Send + Sync>> { + ensure!( + cmd.is_default_prog(), + "can only use default prog commands with serial tty implementations" + ); + Ok(Box::new(SerialChild { + port: Arc::clone(&self.port), + })) + } +} + +/// There isn't really a child process on the end of the serial connection, +/// so all of the Child trait impls are NOP +struct SerialChild { + port: Handle, +} + +// An anemic impl of Debug to satisfy some indirect trait bounds +impl std::fmt::Debug for SerialChild { + fn fmt(&self, fmt: &mut std::fmt::Formatter) -> Result<(), std::fmt::Error> { + fmt.debug_struct("SerialChild").finish() + } +} + +impl Child for SerialChild { + fn try_wait(&mut self) -> IoResult<Option<ExitStatus>> { + Ok(None) + } + + fn wait(&mut self) -> IoResult<ExitStatus> { + // There isn't really a child process to wait for, + // as the serial connection never really "dies", + // however, for something like a USB serial port, + // if it is unplugged then it logically is terminated. + // We read the CD (carrier detect) signal periodically + // to see if the device has gone away: we actually discard + // the CD value itself and just look for an error state. + // We could potentially also decide to call CD==false the + // same thing as the "child" completing. + loop { + std::thread::sleep(Duration::from_secs(5)); + + let port = &self.port; + if let Err(err) = port.read_cd() { + log::error!("Error reading carrier detect: {:#}", err); + return Ok(ExitStatus::with_exit_code(1)); + } + } + } + + fn process_id(&self) -> Option<u32> { + None + } + + #[cfg(windows)] + fn as_raw_handle(&self) -> Option<std::os::windows::io::RawHandle> { + None + } +} + +impl ChildKiller for SerialChild { + fn kill(&mut self) -> IoResult<()> { + Ok(()) + } + + fn clone_killer(&self) -> Box<dyn ChildKiller + Send + Sync> { + Box::new(SerialChildKiller) + } +} + +#[derive(Debug)] +struct SerialChildKiller; + +impl ChildKiller for SerialChildKiller { + fn kill(&mut self) -> IoResult<()> { + Ok(()) + } + + fn clone_killer(&self) -> Box<dyn ChildKiller + Send + Sync> { + Box::new(SerialChildKiller) + } +} + +struct Master { + port: Handle, + took_writer: RefCell<bool>, +} + +struct MasterWriter { + port: Handle, +} + +impl Write for MasterWriter { + fn write(&mut self, buf: &[u8]) -> Result<usize, std::io::Error> { + self.port.write(buf) + } + + fn flush(&mut self) -> Result<(), std::io::Error> { + self.port.flush() + } +} + +impl MasterPty for Master { + fn resize(&self, _size: PtySize) -> anyhow::Result<()> { + // Serial ports have no concept of size + Ok(()) + } + + fn get_size(&self) -> anyhow::Result<PtySize> { + // Serial ports have no concept of size + Ok(PtySize::default()) + } + + fn try_clone_reader(&self) -> anyhow::Result<Box<dyn std::io::Read + Send>> { + // We rely on the fact that SystemPort implements the traits + // that expose the underlying file descriptor, and that direct + // reads from that return the raw data that we want + let fd = FileDescriptor::dup(&*self.port)?; + Ok(Box::new(Reader { fd })) + } + + fn take_writer(&self) -> anyhow::Result<Box<dyn std::io::Write + Send>> { + if *self.took_writer.borrow() { + anyhow::bail!("cannot take writer more than once"); + } + *self.took_writer.borrow_mut() = true; + let port = Arc::clone(&self.port); + Ok(Box::new(MasterWriter { port })) + } + + #[cfg(unix)] + fn process_group_leader(&self) -> Option<libc::pid_t> { + // N/A: there is no local process + None + } + + #[cfg(unix)] + fn as_raw_fd(&self) -> Option<crate::unix::RawFd> { + None + } + + #[cfg(unix)] + fn tty_name(&self) -> Option<PathBuf> { + None + } +} + +struct Reader { + fd: FileDescriptor, +} + +impl Read for Reader { + fn read(&mut self, buf: &mut [u8]) -> Result<usize, std::io::Error> { + // On windows, this self.fd.read will block for up to the time we set + // as the timeout when we set up the port, but on unix it will + // never block. + loop { + #[cfg(unix)] + { + use filedescriptor::{poll, pollfd, AsRawSocketDescriptor, POLLIN}; + // The serial crate puts the serial port in non-blocking mode, + // so we must explicitly poll for ourselves here to avoid a + // busy loop. + let mut poll_array = [pollfd { + fd: self.fd.as_socket_descriptor(), + events: POLLIN, + revents: 0, + }]; + let _ = poll(&mut poll_array, None); + } + + match self.fd.read(buf) { + Ok(0) => { + if cfg!(windows) { + // Read timeout with no data available yet; + // loop and try again. + continue; + } + return Err(std::io::Error::new( + std::io::ErrorKind::UnexpectedEof, + "EOF on serial port", + )); + } + Ok(size) => { + return Ok(size); + } + Err(e) => { + if e.kind() == std::io::ErrorKind::WouldBlock { + continue; + } + log::error!("serial read error: {}", e); + return Err(e); + } + } + } + } +} diff --git a/vendor/portable-pty/src/unix.rs b/vendor/portable-pty/src/unix.rs new file mode 100644 index 00000000..cc458ce9 --- /dev/null +++ b/vendor/portable-pty/src/unix.rs @@ -0,0 +1,414 @@ +//! Working with pseudo-terminals + +use crate::{Child, CommandBuilder, MasterPty, PtyPair, PtySize, PtySystem, SlavePty}; +use anyhow::{bail, Error}; +use filedescriptor::FileDescriptor; +use libc::{self, winsize}; +use std::cell::RefCell; +use std::ffi::OsStr; +use std::io::{Read, Write}; +use std::os::fd::AsFd; +use std::os::unix::ffi::OsStrExt; +use std::os::unix::io::{AsRawFd, FromRawFd}; +use std::os::unix::process::CommandExt; +use std::path::PathBuf; +use std::{io, mem, ptr}; + +pub use std::os::unix::io::RawFd; + +#[derive(Default)] +pub struct UnixPtySystem {} + +fn openpty(size: PtySize) -> anyhow::Result<(UnixMasterPty, UnixSlavePty)> { + let mut master: RawFd = -1; + let mut slave: RawFd = -1; + + let mut size = winsize { + ws_row: size.rows, + ws_col: size.cols, + ws_xpixel: size.pixel_width, + ws_ypixel: size.pixel_height, + }; + + let result = unsafe { + // BSDish systems may require mut pointers to some args + #[allow(clippy::unnecessary_mut_passed)] + libc::openpty( + &mut master, + &mut slave, + ptr::null_mut(), + ptr::null_mut(), + &mut size, + ) + }; + + if result != 0 { + bail!("failed to openpty: {:?}", io::Error::last_os_error()); + } + + let tty_name = tty_name(slave); + + let master = UnixMasterPty { + fd: PtyFd(unsafe { FileDescriptor::from_raw_fd(master) }), + took_writer: RefCell::new(false), + tty_name, + }; + let slave = UnixSlavePty { + fd: PtyFd(unsafe { FileDescriptor::from_raw_fd(slave) }), + }; + + // Ensure that these descriptors will get closed when we execute + // the child process. This is done after constructing the Pty + // instances so that we ensure that the Ptys get drop()'d if + // the cloexec() functions fail (unlikely!). + cloexec(master.fd.as_raw_fd())?; + cloexec(slave.fd.as_raw_fd())?; + + Ok((master, slave)) +} + +impl PtySystem for UnixPtySystem { + fn openpty(&self, size: PtySize) -> anyhow::Result<PtyPair> { + let (master, slave) = openpty(size)?; + Ok(PtyPair { + master: Box::new(master), + slave: Box::new(slave), + }) + } +} + +struct PtyFd(pub FileDescriptor); +impl std::ops::Deref for PtyFd { + type Target = FileDescriptor; + fn deref(&self) -> &FileDescriptor { + &self.0 + } +} +impl std::ops::DerefMut for PtyFd { + fn deref_mut(&mut self) -> &mut FileDescriptor { + &mut self.0 + } +} + +impl Read for PtyFd { + fn read(&mut self, buf: &mut [u8]) -> Result<usize, io::Error> { + match self.0.read(buf) { + Err(ref e) if e.raw_os_error() == Some(libc::EIO) => { + // EIO indicates that the slave pty has been closed. + // Treat this as EOF so that std::io::Read::read_to_string + // and similar functions gracefully terminate when they + // encounter this condition + Ok(0) + } + x => x, + } + } +} + +fn tty_name(fd: RawFd) -> Option<PathBuf> { + let mut buf = vec![0 as std::ffi::c_char; 128]; + + loop { + let res = unsafe { libc::ttyname_r(fd, buf.as_mut_ptr(), buf.len()) }; + + if res == libc::ERANGE { + if buf.len() > 64 * 1024 { + // on macOS, if the buf is "too big", ttyname_r can + // return ERANGE, even though that is supposed to + // indicate buf is "too small". + return None; + } + buf.resize(buf.len() * 2, 0 as std::ffi::c_char); + continue; + } + + return if res == 0 { + let cstr = unsafe { std::ffi::CStr::from_ptr(buf.as_ptr()) }; + let osstr = OsStr::from_bytes(cstr.to_bytes()); + Some(PathBuf::from(osstr)) + } else { + None + }; + } +} + +/// On Big Sur, Cocoa leaks various file descriptors to child processes, +/// so we need to make a pass through the open descriptors beyond just the +/// stdio descriptors and close them all out. +/// This is approximately equivalent to the darwin `posix_spawnattr_setflags` +/// option POSIX_SPAWN_CLOEXEC_DEFAULT which is used as a bit of a cheat +/// on macOS. +/// On Linux, gnome/mutter leak shell extension fds to wezterm too, so we +/// also need to make an effort to clean up the mess. +/// +/// This function enumerates the open filedescriptors in the current process +/// and then will forcibly call close(2) on each open fd that is numbered +/// 3 or higher, effectively closing all descriptors except for the stdio +/// streams. +/// +/// The implementation of this function relies on `/dev/fd` being available +/// to provide the list of open fds. Any errors in enumerating or closing +/// the fds are silently ignored. +pub fn close_random_fds() { + // FreeBSD, macOS and presumably other BSDish systems have /dev/fd as + // a directory listing the current fd numbers for the process. + // + // On Linux, /dev/fd is a symlink to /proc/self/fd + if let Ok(dir) = std::fs::read_dir("/dev/fd") { + let mut fds = vec![]; + for entry in dir { + if let Some(num) = entry + .ok() + .map(|e| e.file_name()) + .and_then(|s| s.into_string().ok()) + .and_then(|n| n.parse::<libc::c_int>().ok()) + { + if num > 2 { + fds.push(num); + } + } + } + for fd in fds { + unsafe { + libc::close(fd); + } + } + } +} + +impl PtyFd { + fn resize(&self, size: PtySize) -> Result<(), Error> { + let ws_size = winsize { + ws_row: size.rows, + ws_col: size.cols, + ws_xpixel: size.pixel_width, + ws_ypixel: size.pixel_height, + }; + + if unsafe { + libc::ioctl( + self.0.as_raw_fd(), + libc::TIOCSWINSZ as _, + &ws_size as *const _, + ) + } != 0 + { + bail!( + "failed to ioctl(TIOCSWINSZ): {:?}", + io::Error::last_os_error() + ); + } + + Ok(()) + } + + fn get_size(&self) -> Result<PtySize, Error> { + let mut size: winsize = unsafe { mem::zeroed() }; + if unsafe { + libc::ioctl( + self.0.as_raw_fd(), + libc::TIOCGWINSZ as _, + &mut size as *mut _, + ) + } != 0 + { + bail!( + "failed to ioctl(TIOCGWINSZ): {:?}", + io::Error::last_os_error() + ); + } + Ok(PtySize { + rows: size.ws_row, + cols: size.ws_col, + pixel_width: size.ws_xpixel, + pixel_height: size.ws_ypixel, + }) + } + + fn spawn_command(&self, builder: CommandBuilder) -> anyhow::Result<std::process::Child> { + let configured_umask = builder.umask; + + let mut cmd = builder.as_command()?; + let controlling_tty = builder.get_controlling_tty(); + + unsafe { + cmd.stdin(self.as_stdio()?) + .stdout(self.as_stdio()?) + .stderr(self.as_stdio()?) + .pre_exec(move || { + // Clean up a few things before we exec the program + // Clear out any potentially problematic signal + // dispositions that we might have inherited + for signo in &[ + libc::SIGCHLD, + libc::SIGHUP, + libc::SIGINT, + libc::SIGQUIT, + libc::SIGTERM, + libc::SIGALRM, + ] { + libc::signal(*signo, libc::SIG_DFL); + } + + let empty_set: libc::sigset_t = std::mem::zeroed(); + libc::sigprocmask(libc::SIG_SETMASK, &empty_set, std::ptr::null_mut()); + + // Establish ourselves as a session leader. + if libc::setsid() == -1 { + return Err(io::Error::last_os_error()); + } + + // Clippy wants us to explicitly cast TIOCSCTTY using + // type::from(), but the size and potentially signedness + // are system dependent, which is why we're using `as _`. + // Suppress this lint for this section of code. + #[allow(clippy::cast_lossless)] + if controlling_tty { + // Set the pty as the controlling terminal. + // Failure to do this means that delivery of + // SIGWINCH won't happen when we resize the + // terminal, among other undesirable effects. + if libc::ioctl(0, libc::TIOCSCTTY as _, 0) == -1 { + return Err(io::Error::last_os_error()); + } + } + + close_random_fds(); + + if let Some(mask) = configured_umask { + libc::umask(mask); + } + + Ok(()) + }) + }; + + let mut child = cmd.spawn()?; + + // Ensure that we close out the slave fds that Child retains; + // they are not what we need (we need the master side to reference + // them) and won't work in the usual way anyway. + // In practice these are None, but it seems best to be move them + // out in case the behavior of Command changes in the future. + child.stdin.take(); + child.stdout.take(); + child.stderr.take(); + + Ok(child) + } +} + +/// Represents the master end of a pty. +/// The file descriptor will be closed when the Pty is dropped. +struct UnixMasterPty { + fd: PtyFd, + took_writer: RefCell<bool>, + tty_name: Option<PathBuf>, +} + +/// Represents the slave end of a pty. +/// The file descriptor will be closed when the Pty is dropped. +struct UnixSlavePty { + fd: PtyFd, +} + +/// Helper function to set the close-on-exec flag for a raw descriptor +fn cloexec(fd: RawFd) -> Result<(), Error> { + let flags = unsafe { libc::fcntl(fd, libc::F_GETFD) }; + if flags == -1 { + bail!( + "fcntl to read flags failed: {:?}", + io::Error::last_os_error() + ); + } + let result = unsafe { libc::fcntl(fd, libc::F_SETFD, flags | libc::FD_CLOEXEC) }; + if result == -1 { + bail!( + "fcntl to set CLOEXEC failed: {:?}", + io::Error::last_os_error() + ); + } + Ok(()) +} + +impl SlavePty for UnixSlavePty { + fn spawn_command( + &self, + builder: CommandBuilder, + ) -> Result<Box<dyn Child + Send + Sync>, Error> { + Ok(Box::new(self.fd.spawn_command(builder)?)) + } +} + +impl MasterPty for UnixMasterPty { + fn resize(&self, size: PtySize) -> Result<(), Error> { + self.fd.resize(size) + } + + fn get_size(&self) -> Result<PtySize, Error> { + self.fd.get_size() + } + + fn try_clone_reader(&self) -> Result<Box<dyn Read + Send>, Error> { + let fd = PtyFd(self.fd.try_clone()?); + Ok(Box::new(fd)) + } + + fn take_writer(&self) -> Result<Box<dyn Write + Send>, Error> { + if *self.took_writer.borrow() { + anyhow::bail!("cannot take writer more than once"); + } + *self.took_writer.borrow_mut() = true; + let fd = PtyFd(self.fd.try_clone()?); + Ok(Box::new(UnixMasterWriter { fd })) + } + + fn as_raw_fd(&self) -> Option<RawFd> { + Some(self.fd.0.as_raw_fd()) + } + + fn tty_name(&self) -> Option<PathBuf> { + self.tty_name.clone() + } + + fn process_group_leader(&self) -> Option<libc::pid_t> { + match unsafe { libc::tcgetpgrp(self.fd.0.as_raw_fd()) } { + pid if pid > 0 => Some(pid), + _ => None, + } + } + + fn get_termios(&self) -> Option<nix::sys::termios::Termios> { + nix::sys::termios::tcgetattr(self.fd.0.as_fd()).ok() + } +} + +/// Represents the master end of a pty. +/// EOT will be sent, and then the file descriptor will be closed when +/// the Pty is dropped. +struct UnixMasterWriter { + fd: PtyFd, +} + +impl Drop for UnixMasterWriter { + fn drop(&mut self) { + let mut t: libc::termios = unsafe { std::mem::MaybeUninit::zeroed().assume_init() }; + if unsafe { libc::tcgetattr(self.fd.0.as_raw_fd(), &mut t) } == 0 { + // EOF is only interpreted after a newline, so if it is set, + // we send a newline followed by EOF. + let eot = t.c_cc[libc::VEOF]; + if eot != 0 { + let _ = self.fd.0.write_all(&[b'\n', eot]); + } + } + } +} + +impl Write for UnixMasterWriter { + fn write(&mut self, buf: &[u8]) -> Result<usize, io::Error> { + self.fd.write(buf) + } + fn flush(&mut self) -> Result<(), io::Error> { + self.fd.flush() + } +} diff --git a/vendor/portable-pty/src/win/conpty.rs b/vendor/portable-pty/src/win/conpty.rs new file mode 100644 index 00000000..0e9a18c6 --- /dev/null +++ b/vendor/portable-pty/src/win/conpty.rs @@ -0,0 +1,117 @@ +use crate::cmdbuilder::CommandBuilder; +use crate::win::psuedocon::PsuedoCon; +use crate::{Child, MasterPty, PtyPair, PtySize, PtySystem, SlavePty}; +use anyhow::Error; +use filedescriptor::{FileDescriptor, Pipe}; +use std::sync::{Arc, Mutex}; +use winapi::um::wincon::COORD; + +#[derive(Default)] +pub struct ConPtySystem {} + +impl PtySystem for ConPtySystem { + fn openpty(&self, size: PtySize) -> anyhow::Result<PtyPair> { + let stdin = Pipe::new()?; + let stdout = Pipe::new()?; + + let con = PsuedoCon::new( + COORD { + X: size.cols as i16, + Y: size.rows as i16, + }, + stdin.read, + stdout.write, + )?; + + let master = ConPtyMasterPty { + inner: Arc::new(Mutex::new(Inner { + con, + readable: stdout.read, + writable: Some(stdin.write), + size, + })), + }; + + let slave = ConPtySlavePty { + inner: master.inner.clone(), + }; + + Ok(PtyPair { + master: Box::new(master), + slave: Box::new(slave), + }) + } +} + +struct Inner { + con: PsuedoCon, + readable: FileDescriptor, + writable: Option<FileDescriptor>, + size: PtySize, +} + +impl Inner { + pub fn resize( + &mut self, + num_rows: u16, + num_cols: u16, + pixel_width: u16, + pixel_height: u16, + ) -> Result<(), Error> { + self.con.resize(COORD { + X: num_cols as i16, + Y: num_rows as i16, + })?; + self.size = PtySize { + rows: num_rows, + cols: num_cols, + pixel_width, + pixel_height, + }; + Ok(()) + } +} + +#[derive(Clone)] +pub struct ConPtyMasterPty { + inner: Arc<Mutex<Inner>>, +} + +pub struct ConPtySlavePty { + inner: Arc<Mutex<Inner>>, +} + +impl MasterPty for ConPtyMasterPty { + fn resize(&self, size: PtySize) -> anyhow::Result<()> { + let mut inner = self.inner.lock().unwrap(); + inner.resize(size.rows, size.cols, size.pixel_width, size.pixel_height) + } + + fn get_size(&self) -> Result<PtySize, Error> { + let inner = self.inner.lock().unwrap(); + Ok(inner.size.clone()) + } + + fn try_clone_reader(&self) -> anyhow::Result<Box<dyn std::io::Read + Send>> { + Ok(Box::new(self.inner.lock().unwrap().readable.try_clone()?)) + } + + fn take_writer(&self) -> anyhow::Result<Box<dyn std::io::Write + Send>> { + Ok(Box::new( + self.inner + .lock() + .unwrap() + .writable + .take() + .ok_or_else(|| anyhow::anyhow!("writer already taken"))?, + )) + } +} + +impl SlavePty for ConPtySlavePty { + fn spawn_command(&self, cmd: CommandBuilder) -> anyhow::Result<Box<dyn Child + Send + Sync>> { + let inner = self.inner.lock().unwrap(); + let child = inner.con.spawn_command(cmd)?; + Ok(Box::new(child)) + } +} diff --git a/vendor/portable-pty/src/win/mod.rs b/vendor/portable-pty/src/win/mod.rs new file mode 100644 index 00000000..24f8f536 --- /dev/null +++ b/vendor/portable-pty/src/win/mod.rs @@ -0,0 +1,149 @@ +use crate::{Child, ChildKiller, ExitStatus}; +use anyhow::Context as _; +use std::io::{Error as IoError, Result as IoResult}; +use std::os::windows::io::{AsRawHandle, RawHandle}; +use std::pin::Pin; +use std::sync::Mutex; +use std::task::{Context, Poll}; +use winapi::shared::minwindef::DWORD; +use winapi::um::minwinbase::STILL_ACTIVE; +use winapi::um::processthreadsapi::*; +use winapi::um::synchapi::WaitForSingleObject; +use winapi::um::winbase::INFINITE; + +pub mod conpty; +mod procthreadattr; +mod psuedocon; + +use filedescriptor::OwnedHandle; + +#[derive(Debug)] +pub struct WinChild { + proc: Mutex<OwnedHandle>, +} + +impl WinChild { + fn is_complete(&mut self) -> IoResult<Option<ExitStatus>> { + let mut status: DWORD = 0; + let proc = self.proc.lock().unwrap().try_clone().unwrap(); + let res = unsafe { GetExitCodeProcess(proc.as_raw_handle() as _, &mut status) }; + if res != 0 { + if status == STILL_ACTIVE { + Ok(None) + } else { + Ok(Some(ExitStatus::with_exit_code(status))) + } + } else { + Ok(None) + } + } + + fn do_kill(&mut self) -> IoResult<()> { + let proc = self.proc.lock().unwrap().try_clone().unwrap(); + let res = unsafe { TerminateProcess(proc.as_raw_handle() as _, 1) }; + let err = IoError::last_os_error(); + if res != 0 { + Err(err) + } else { + Ok(()) + } + } +} + +impl ChildKiller for WinChild { + fn kill(&mut self) -> IoResult<()> { + self.do_kill().ok(); + Ok(()) + } + + fn clone_killer(&self) -> Box<dyn ChildKiller + Send + Sync> { + let proc = self.proc.lock().unwrap().try_clone().unwrap(); + Box::new(WinChildKiller { proc }) + } +} + +#[derive(Debug)] +pub struct WinChildKiller { + proc: OwnedHandle, +} + +impl ChildKiller for WinChildKiller { + fn kill(&mut self) -> IoResult<()> { + let res = unsafe { TerminateProcess(self.proc.as_raw_handle() as _, 1) }; + let err = IoError::last_os_error(); + if res != 0 { + Err(err) + } else { + Ok(()) + } + } + + fn clone_killer(&self) -> Box<dyn ChildKiller + Send + Sync> { + let proc = self.proc.try_clone().unwrap(); + Box::new(WinChildKiller { proc }) + } +} + +impl Child for WinChild { + fn try_wait(&mut self) -> IoResult<Option<ExitStatus>> { + self.is_complete() + } + + fn wait(&mut self) -> IoResult<ExitStatus> { + if let Ok(Some(status)) = self.try_wait() { + return Ok(status); + } + let proc = self.proc.lock().unwrap().try_clone().unwrap(); + unsafe { + WaitForSingleObject(proc.as_raw_handle() as _, INFINITE); + } + let mut status: DWORD = 0; + let res = unsafe { GetExitCodeProcess(proc.as_raw_handle() as _, &mut status) }; + if res != 0 { + Ok(ExitStatus::with_exit_code(status)) + } else { + Err(IoError::last_os_error()) + } + } + + fn process_id(&self) -> Option<u32> { + let res = unsafe { GetProcessId(self.proc.lock().unwrap().as_raw_handle() as _) }; + if res == 0 { + None + } else { + Some(res) + } + } + + fn as_raw_handle(&self) -> Option<std::os::windows::io::RawHandle> { + let proc = self.proc.lock().unwrap(); + Some(proc.as_raw_handle()) + } +} + +impl std::future::Future for WinChild { + type Output = anyhow::Result<ExitStatus>; + + fn poll(mut self: Pin<&mut Self>, cx: &mut Context) -> Poll<anyhow::Result<ExitStatus>> { + match self.is_complete() { + Ok(Some(status)) => Poll::Ready(Ok(status)), + Err(err) => Poll::Ready(Err(err).context("Failed to retrieve process exit status")), + Ok(None) => { + struct PassRawHandleToWaiterThread(pub RawHandle); + unsafe impl Send for PassRawHandleToWaiterThread {} + + let proc = self.proc.lock().unwrap().try_clone()?; + let handle = PassRawHandleToWaiterThread(proc.as_raw_handle()); + + let waker = cx.waker().clone(); + std::thread::spawn(move || { + unsafe { + WaitForSingleObject(handle.0 as _, INFINITE); + } + waker.wake(); + }); + Poll::Pending + } + } + } +} diff --git a/vendor/portable-pty/src/win/procthreadattr.rs b/vendor/portable-pty/src/win/procthreadattr.rs new file mode 100644 index 00000000..ad97d582 --- /dev/null +++ b/vendor/portable-pty/src/win/procthreadattr.rs @@ -0,0 +1,72 @@ +use crate::win::psuedocon::HPCON; +use anyhow::{ensure, Error}; +use std::io::Error as IoError; +use std::{mem, ptr}; +use winapi::shared::minwindef::DWORD; +use winapi::um::processthreadsapi::*; + +const PROC_THREAD_ATTRIBUTE_PSEUDOCONSOLE: usize = 0x00020016; + +pub struct ProcThreadAttributeList { + data: Vec<u8>, +} + +impl ProcThreadAttributeList { + pub fn with_capacity(num_attributes: DWORD) -> Result<Self, Error> { + let mut bytes_required: usize = 0; + unsafe { + InitializeProcThreadAttributeList( + ptr::null_mut(), + num_attributes, + 0, + &mut bytes_required, + ) + }; + let mut data = Vec::with_capacity(bytes_required); + // We have the right capacity, so force the vec to consider itself + // that length. The contents of those bytes will be maintained + // by the win32 apis used in this impl. + unsafe { data.set_len(bytes_required) }; + + let attr_ptr = data.as_mut_slice().as_mut_ptr() as *mut _; + let res = unsafe { + InitializeProcThreadAttributeList(attr_ptr, num_attributes, 0, &mut bytes_required) + }; + ensure!( + res != 0, + "InitializeProcThreadAttributeList failed: {}", + IoError::last_os_error() + ); + Ok(Self { data }) + } + + pub fn as_mut_ptr(&mut self) -> LPPROC_THREAD_ATTRIBUTE_LIST { + self.data.as_mut_slice().as_mut_ptr() as *mut _ + } + + pub fn set_pty(&mut self, con: HPCON) -> Result<(), Error> { + let res = unsafe { + UpdateProcThreadAttribute( + self.as_mut_ptr(), + 0, + PROC_THREAD_ATTRIBUTE_PSEUDOCONSOLE, + con, + mem::size_of::<HPCON>(), + ptr::null_mut(), + ptr::null_mut(), + ) + }; + ensure!( + res != 0, + "UpdateProcThreadAttribute failed: {}", + IoError::last_os_error() + ); + Ok(()) + } +} + +impl Drop for ProcThreadAttributeList { + fn drop(&mut self) { + unsafe { DeleteProcThreadAttributeList(self.as_mut_ptr()) }; + } +} diff --git a/vendor/portable-pty/src/win/psuedocon.rs b/vendor/portable-pty/src/win/psuedocon.rs new file mode 100644 index 00000000..133eab52 --- /dev/null +++ b/vendor/portable-pty/src/win/psuedocon.rs @@ -0,0 +1,168 @@ +use super::WinChild; +use crate::cmdbuilder::CommandBuilder; +use crate::win::procthreadattr::ProcThreadAttributeList; +use anyhow::{bail, ensure, Error}; +use filedescriptor::{FileDescriptor, OwnedHandle}; +use lazy_static::lazy_static; +use shared_library::shared_library; +use std::ffi::OsString; +use std::io::Error as IoError; +use std::os::windows::ffi::OsStringExt; +use std::os::windows::io::{AsRawHandle, FromRawHandle}; +use std::path::Path; +use std::sync::Mutex; +use std::{mem, ptr}; +use winapi::shared::minwindef::DWORD; +use winapi::shared::winerror::{HRESULT, S_OK}; +use winapi::um::handleapi::*; +use winapi::um::processthreadsapi::*; +use winapi::um::winbase::{ + CREATE_UNICODE_ENVIRONMENT, EXTENDED_STARTUPINFO_PRESENT, STARTF_USESTDHANDLES, STARTUPINFOEXW, +}; +use winapi::um::wincon::COORD; +use winapi::um::winnt::HANDLE; + +pub type HPCON = HANDLE; + +pub const PSUEDOCONSOLE_INHERIT_CURSOR: DWORD = 0x1; +pub const PSEUDOCONSOLE_RESIZE_QUIRK: DWORD = 0x2; +pub const PSEUDOCONSOLE_WIN32_INPUT_MODE: DWORD = 0x4; +#[allow(dead_code)] +pub const PSEUDOCONSOLE_PASSTHROUGH_MODE: DWORD = 0x8; + +shared_library!(ConPtyFuncs, + pub fn CreatePseudoConsole( + size: COORD, + hInput: HANDLE, + hOutput: HANDLE, + flags: DWORD, + hpc: *mut HPCON + ) -> HRESULT, + pub fn ResizePseudoConsole(hpc: HPCON, size: COORD) -> HRESULT, + pub fn ClosePseudoConsole(hpc: HPCON), +); + +fn load_conpty() -> ConPtyFuncs { + // If the kernel doesn't export these functions then their system is + // too old and we cannot run. + let kernel = ConPtyFuncs::open(Path::new("kernel32.dll")).expect( + "this system does not support conpty. Windows 10 October 2018 or newer is required", + ); + + kernel +} + +lazy_static! { + static ref CONPTY: ConPtyFuncs = load_conpty(); +} + +pub struct PsuedoCon { + con: HPCON, +} + +unsafe impl Send for PsuedoCon {} +unsafe impl Sync for PsuedoCon {} + +impl Drop for PsuedoCon { + fn drop(&mut self) { + unsafe { (CONPTY.ClosePseudoConsole)(self.con) }; + } +} + +impl PsuedoCon { + pub fn new(size: COORD, input: FileDescriptor, output: FileDescriptor) -> Result<Self, Error> { + let mut con: HPCON = INVALID_HANDLE_VALUE; + let result = unsafe { + (CONPTY.CreatePseudoConsole)( + size, + input.as_raw_handle() as _, + output.as_raw_handle() as _, + PSUEDOCONSOLE_INHERIT_CURSOR + | PSEUDOCONSOLE_RESIZE_QUIRK + | PSEUDOCONSOLE_WIN32_INPUT_MODE, + &mut con, + ) + }; + ensure!( + result == S_OK, + "failed to create psuedo console: HRESULT {}", + result + ); + Ok(Self { con }) + } + + pub fn resize(&self, size: COORD) -> Result<(), Error> { + let result = unsafe { (CONPTY.ResizePseudoConsole)(self.con, size) }; + ensure!( + result == S_OK, + "failed to resize console to {}x{}: HRESULT: {}", + size.X, + size.Y, + result + ); + Ok(()) + } + + pub fn spawn_command(&self, cmd: CommandBuilder) -> anyhow::Result<WinChild> { + let mut si: STARTUPINFOEXW = unsafe { mem::zeroed() }; + si.StartupInfo.cb = mem::size_of::<STARTUPINFOEXW>() as u32; + // Explicitly set the stdio handles as invalid handles otherwise + // we can end up with a weird state where the spawned process can + // inherit the explicitly redirected output handles from its parent. + // For example, when daemonizing wezterm-mux-server, the stdio handles + // are redirected to a log file and the spawned process would end up + // writing its output there instead of to the pty we just created. + si.StartupInfo.dwFlags = STARTF_USESTDHANDLES; + si.StartupInfo.hStdInput = INVALID_HANDLE_VALUE; + si.StartupInfo.hStdOutput = INVALID_HANDLE_VALUE; + si.StartupInfo.hStdError = INVALID_HANDLE_VALUE; + + let mut attrs = ProcThreadAttributeList::with_capacity(1)?; + attrs.set_pty(self.con)?; + si.lpAttributeList = attrs.as_mut_ptr(); + + let mut pi: PROCESS_INFORMATION = unsafe { mem::zeroed() }; + + let (mut exe, mut cmdline) = cmd.cmdline()?; + let cmd_os = OsString::from_wide(&cmdline); + + let cwd = cmd.current_directory(); + + let res = unsafe { + CreateProcessW( + exe.as_mut_slice().as_mut_ptr(), + cmdline.as_mut_slice().as_mut_ptr(), + ptr::null_mut(), + ptr::null_mut(), + 0, + EXTENDED_STARTUPINFO_PRESENT | CREATE_UNICODE_ENVIRONMENT, + cmd.environment_block().as_mut_slice().as_mut_ptr() as *mut _, + cwd.as_ref() + .map(|c| c.as_slice().as_ptr()) + .unwrap_or(ptr::null()), + &mut si.StartupInfo, + &mut pi, + ) + }; + if res == 0 { + let err = IoError::last_os_error(); + let msg = format!( + "CreateProcessW `{:?}` in cwd `{:?}` failed: {}", + cmd_os, + cwd.as_ref().map(|c| OsString::from_wide(c)), + err + ); + log::error!("{}", msg); + bail!("{}", msg); + } + + // Make sure we close out the thread handle so we don't leak it; + // we do this simply by making it owned + let _main_thread = unsafe { OwnedHandle::from_raw_handle(pi.hThread as _) }; + let proc = unsafe { OwnedHandle::from_raw_handle(pi.hProcess as _) }; + + Ok(WinChild { + proc: Mutex::new(proc), + }) + } +} |
