fix: respond to ConPTY DSR cursor query on spawn

ConPTY sends ESC[6n (Device Status Report — report cursor position)
immediately on session start and blocks the child process until it
receives a reply. Since portable-pty never responds, the child hangs
forever and the PTY times out.

Fix: write ESC[1;1R (cursor at row 1, col 1) to the PTY input pipe
right after spawning, before reading any output. This unblocks ConPTY
and allows the child process to start normally.
This commit is contained in:
Sanskar Singh
2026-05-16 11:08:31 +05:30
parent 158b00266b
commit 0cb3a8ba6d
5 changed files with 79 additions and 2 deletions
+1
View File
@@ -61,3 +61,4 @@ python/omp-rpc/src/omp_rpc.egg-info/
# parallel-agent worktrees
.wt/
CPU*.md
packages/coding-agent/binaries/
+16 -2
View File
@@ -252,8 +252,18 @@ fn run_pty_sync(
.map_err(|err| Error::from_reason(format!("Failed to open PTY: {err}")))?
};
let mut cmd = CommandBuilder::new(config.shell.as_deref().unwrap_or("sh"));
cmd.arg("-lc");
let shell = config.shell.as_deref().unwrap_or("sh");
let mut cmd = CommandBuilder::new(shell);
// Use shell-appropriate command execution flags
let lower = shell.to_lowercase();
if lower.ends_with("cmd.exe") || lower.ends_with("cmd") {
cmd.arg("/c");
} else if lower.contains("powershell") || lower.contains("pwsh") {
cmd.arg("-Command");
} else {
// sh/bash/zsh/fish etc.
cmd.arg("-lc");
}
cmd.arg(&config.command);
if let Some(cwd) = config.cwd.as_ref() {
cmd.cwd(cwd);
@@ -276,6 +286,10 @@ fn run_pty_sync(
let mut writer = master
.take_writer()
.map_err(|err| Error::from_reason(format!("Failed to create PTY writer: {err}")))?;
// ConPTY sends ESC[6n (cursor position query) and blocks until we reply.
// Reply with cursor at 1,1 so it unblocks the child spawn.
let _ = writer.write_all(b"\x1b[1;1R");
let _ = writer.flush();
let mut reader = master
.try_clone_reader()
.map_err(|err| Error::from_reason(format!("Failed to create PTY reader: {err}")))?;
+5
View File
@@ -1137,6 +1137,11 @@ export interface PtyStartOptions {
cols?: number
/** PTY row count. */
rows?: number
/**
* Shell binary to use (e.g. "sh", "bash", or an absolute path).
* Defaults to "sh" if not provided.
*/
shell?: string
}
/**
+20
View File
@@ -0,0 +1,20 @@
use portable_pty::{native_pty_system, PtySize, CommandBuilder};
fn main() {
println!("Starting PTY reproduction...");
let pty_system = native_pty_system();
println!("Got pty system");
let pair = pty_system.openpty(PtySize {
rows: 40,
cols: 120,
pixel_width: 0,
pixel_height: 0,
});
match pair {
Ok(p) => println!("openpty succeeded!"),
Err(e) => println!("openpty failed: {}", e),
}
println!("Done.");
}
+37
View File
@@ -0,0 +1,37 @@
import { PtySession } from "@oh-my-pi/pi-natives";
async function testPty(shell: string, command: string, label: string) {
const session = new PtySession();
const t0 = Date.now();
let output = "";
await new Promise<void>((resolve) => {
session.start(
{
command,
shell,
timeoutMs: 5000,
cols: 80,
rows: 24,
},
(_err, chunk) => {
if (chunk) output += chunk;
},
).then((result) => {
const ms = Date.now() - t0;
// Strip VT sequences for readable output
const clean = output.replace(/\x1b\[[^a-zA-Z]*[a-zA-Z]/g, "").replace(/\x1b\][^\x07]*\x07/g, "").trim();
console.log(`[${label}] ${ms}ms timedOut=${result.timedOut} exitCode=${result.exitCode} output="${clean}"`);
resolve();
}).catch((err) => {
console.log(`[${label}] ${Date.now() - t0}ms ERROR: ${err.message}`);
resolve();
});
setTimeout(() => { console.log(`[${label}] HUNG`); resolve(); }, 10000);
});
}
await testPty("cmd.exe", "echo hello from cmd", "cmd");
await testPty("powershell.exe", "Write-Host 'hello from pwsh'", "pwsh");
await testPty("sh", "echo hello from bash", "sh-git-bash");
process.exit(0);