aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorOgulcan Celik <ogulcancelik@gmail.com>2026-06-23 21:43:23 +0300
committerOgulcan Celik <ogulcancelik@gmail.com>2026-06-23 21:43:23 +0300
commita66f4c64e9d1591f042a94d6f3bef3b4298a4d1c (patch)
tree747de08b6ada4300afb4124da9feff0cc66b96a7
parent5981ba426e8ed8f6785195a03cc0d8a190173e61 (diff)
fix: force system conpty on windows
refs #761
-rw-r--r--.github/workflows/ci.yml32
-rw-r--r--Cargo.lock2
-rw-r--r--Cargo.toml5
-rw-r--r--justfile4
-rw-r--r--scripts/test_vendor_portable_pty.py103
-rw-r--r--scripts/windows_smoke_conpty_path.ps1105
-rw-r--r--vendor/patches/portable-pty/0001-force-system-conpty.patch37
-rw-r--r--vendor/portable-pty.patches.md41
-rw-r--r--vendor/portable-pty/Cargo.toml120
-rw-r--r--vendor/portable-pty/Cargo.toml.orig43
-rw-r--r--vendor/portable-pty/LICENSE.md21
-rw-r--r--vendor/portable-pty/examples/bash.rs85
-rw-r--r--vendor/portable-pty/examples/narrow.rs92
-rw-r--r--vendor/portable-pty/examples/whoami.rs94
-rw-r--r--vendor/portable-pty/examples/whoami_async.rs73
-rw-r--r--vendor/portable-pty/src/cmdbuilder.rs823
-rw-r--r--vendor/portable-pty/src/lib.rs407
-rw-r--r--vendor/portable-pty/src/serial.rs305
-rw-r--r--vendor/portable-pty/src/unix.rs414
-rw-r--r--vendor/portable-pty/src/win/conpty.rs117
-rw-r--r--vendor/portable-pty/src/win/mod.rs149
-rw-r--r--vendor/portable-pty/src/win/procthreadattr.rs72
-rw-r--r--vendor/portable-pty/src/win/psuedocon.rs168
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"
diff --git a/Cargo.lock b/Cargo.lock
index 82b6c38c..7296fb4e 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -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",
diff --git a/Cargo.toml b/Cargo.toml
index 3647e067..366f277e 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -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",
diff --git a/justfile b/justfile
index 245c48be..8c9fdf6b 100644
--- a/justfile
+++ b/justfile
@@ -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),
+ })
+ }
+}