@bivy/bivy 0.17.2-staging.4 → 0.17.2-staging.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/command-launch.js +51 -6
- package/dist/runtime/process.js +2 -2
- package/dist/server.js +18 -27
- package/dist/session/event-log.js +74 -18
- package/dist/terminal.js +1 -1
- package/package.json +1 -1
- package/dist/pty-runner.py +0 -95
package/dist/command-launch.js
CHANGED
|
@@ -1,12 +1,57 @@
|
|
|
1
1
|
// SPDX-License-Identifier: AGPL-3.0-only
|
|
2
2
|
// Copyright (c) 2026 Petter André Sjulstad
|
|
3
|
+
import { spawn } from "node:child_process";
|
|
4
|
+
import os from "node:os";
|
|
5
|
+
import { loadPty } from "./terminal.js";
|
|
6
|
+
const signalNames = new Map(Object.entries(os.constants.signals).map(([name, num]) => [num, name]));
|
|
3
7
|
/**
|
|
4
8
|
* Keep terminal semantics opt-in for short-lived commands. Native interactive
|
|
5
|
-
* commands must set requiresTty
|
|
6
|
-
*
|
|
9
|
+
* commands must set requiresTty and run under node-pty (the same dependency the
|
|
10
|
+
* interactive terminals use); ordinary commands stay on direct pipes.
|
|
7
11
|
*/
|
|
8
|
-
export function
|
|
9
|
-
return requiresTty
|
|
10
|
-
|
|
11
|
-
|
|
12
|
+
export function launchCommand(spec, requiresTty, handlers) {
|
|
13
|
+
return requiresTty ? launchPty(spec, handlers) : launchPipes(spec, handlers);
|
|
14
|
+
}
|
|
15
|
+
function launchPipes({ command, args, cwd, env }, handlers) {
|
|
16
|
+
const child = spawn(command, args, { cwd, env });
|
|
17
|
+
child.stdout.on("data", (data) => handlers.onOutput("stdout", String(data)));
|
|
18
|
+
child.stderr.on("data", (data) => handlers.onOutput("stderr", String(data)));
|
|
19
|
+
child.on("error", handlers.onError);
|
|
20
|
+
child.on("exit", handlers.onExit);
|
|
21
|
+
return {
|
|
22
|
+
usesPty: false,
|
|
23
|
+
write: (text) => child.stdin.write(text),
|
|
24
|
+
interrupt: () => child.kill("SIGINT"),
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
function launchPty({ command, args, cwd, env }, handlers) {
|
|
28
|
+
let proc;
|
|
29
|
+
try {
|
|
30
|
+
proc = loadPty().spawn(command, args, { name: env.TERM ?? "xterm-256color", cols: 120, rows: 40, cwd, env });
|
|
31
|
+
}
|
|
32
|
+
catch (error) {
|
|
33
|
+
// Match child_process: a launch failure is reported asynchronously, not thrown.
|
|
34
|
+
queueMicrotask(() => handlers.onError(error instanceof Error ? error : new Error(String(error))));
|
|
35
|
+
return { usesPty: true, write: () => { }, interrupt: () => { } };
|
|
36
|
+
}
|
|
37
|
+
// A PTY merges stdout and stderr into one stream.
|
|
38
|
+
proc.onData((text) => handlers.onOutput("stdout", text));
|
|
39
|
+
proc.onExit(({ exitCode, signal }) => handlers.onExit(signal ? null : exitCode, signal ? signalNames.get(signal) ?? null : null));
|
|
40
|
+
return {
|
|
41
|
+
usesPty: true,
|
|
42
|
+
write: (text) => proc.write(text),
|
|
43
|
+
interrupt: () => {
|
|
44
|
+
// node-pty makes the child a session (and process-group) leader, so a
|
|
45
|
+
// negative pid reaches anything it forked too.
|
|
46
|
+
try {
|
|
47
|
+
process.kill(-proc.pid, "SIGINT");
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
try {
|
|
51
|
+
proc.kill("SIGINT");
|
|
52
|
+
}
|
|
53
|
+
catch { /* already exited */ }
|
|
54
|
+
}
|
|
55
|
+
},
|
|
56
|
+
};
|
|
12
57
|
}
|
package/dist/runtime/process.js
CHANGED
|
@@ -16,8 +16,8 @@ import { withExactCapabilitySurface } from "./types.js";
|
|
|
16
16
|
* worker processes) die with it instead of being orphaned. Relies on the child
|
|
17
17
|
* having been spawned `detached` (making it its own process-group leader, POSIX
|
|
18
18
|
* only — see the spawn() call in ProcessSession.prompt); `process.kill(-pid,
|
|
19
|
-
* signal)` then targets the whole group,
|
|
20
|
-
*
|
|
19
|
+
* signal)` then targets the whole group, as the PTY command launcher does
|
|
20
|
+
* (src/command-launch.ts). Falls back to killing just the direct child on Windows (no
|
|
21
21
|
* negative-pid group kill there) or if the group is already gone.
|
|
22
22
|
*/
|
|
23
23
|
function killProcessGroup(child, signal) {
|
package/dist/server.js
CHANGED
|
@@ -91,7 +91,7 @@ import { hostedCustodyNode } from "./hosted-custody.js";
|
|
|
91
91
|
import { PolicyEngine } from "./policy/policy-engine.js";
|
|
92
92
|
import { SessionAllowRules } from "./policy/session-allow.js";
|
|
93
93
|
import { TerminalManager } from "./terminal.js";
|
|
94
|
-
import {
|
|
94
|
+
import { launchCommand } from "./command-launch.js";
|
|
95
95
|
import { listMultiplexerSessions } from "./multiplexer.js";
|
|
96
96
|
import { createWorktree, removeWorktree, gitRepoRoot } from "./worktree.js";
|
|
97
97
|
import { HarnessManager } from "./harness/manager.js";
|
|
@@ -344,10 +344,6 @@ const worktreeCleanupSweepMs = Math.max(60 * 60 * 1000, Math.min(worktreeRetenti
|
|
|
344
344
|
// Native Pi commands run the operator-installed agent. Bivy does not substitute
|
|
345
345
|
// a private TUI; BIVY_PI_COMMAND is an explicit path override for managed nodes.
|
|
346
346
|
const piCommand = process.env.BIVY_PI_COMMAND?.trim() || "pi";
|
|
347
|
-
const ptyRunnerScript = process.env.BIVY_PTY_RUNNER ?? (fs.existsSync(path.join(assetRoot, "src", "pty-runner.py"))
|
|
348
|
-
? path.join(assetRoot, "src", "pty-runner.py")
|
|
349
|
-
: path.join(assetRoot, "dist", "pty-runner.py"));
|
|
350
|
-
const pythonCommand = process.env.PYTHON ?? "python3";
|
|
351
347
|
// Per-process secret gating the loopback bootstrap endpoint. Published to a
|
|
352
348
|
// 0600 file (and stdout) so only the launching user can read it.
|
|
353
349
|
const bootstrapSecret = randomBytes(32).toString("base64url");
|
|
@@ -1744,8 +1740,10 @@ function referencedAttachmentHashes() {
|
|
|
1744
1740
|
const ids = new Set(metadata.listSessions().map((session) => session.id));
|
|
1745
1741
|
for (const record of new Set(openSessions.values()))
|
|
1746
1742
|
ids.add(record.id);
|
|
1743
|
+
// `scan`, not `entries`: this visits every session ever created, and must not pin
|
|
1744
|
+
// each one's full log in the event-log cache.
|
|
1747
1745
|
for (const id of ids) {
|
|
1748
|
-
for (const entry of eventLog.
|
|
1746
|
+
for (const entry of eventLog.scan(id, ["attachment", "outbound-attachment", "inline-image"])) {
|
|
1749
1747
|
if (entry.bivyKind === "attachment")
|
|
1750
1748
|
for (const ref of entry.refs)
|
|
1751
1749
|
hashes.add(ref.hash);
|
|
@@ -5617,26 +5615,19 @@ function runNativeCommand(command) {
|
|
|
5617
5615
|
NO_COLOR: "1",
|
|
5618
5616
|
};
|
|
5619
5617
|
// Native commands (login/model/config/etc.) need a TTY for prompts and the
|
|
5620
|
-
// terminal UI
|
|
5621
|
-
|
|
5622
|
-
|
|
5623
|
-
|
|
5624
|
-
|
|
5625
|
-
|
|
5626
|
-
|
|
5627
|
-
|
|
5628
|
-
|
|
5629
|
-
|
|
5630
|
-
|
|
5631
|
-
});
|
|
5632
|
-
child.on("error", (error) => {
|
|
5633
|
-
commandProcesses.delete(runId);
|
|
5634
|
-
broadcast({ type: "command.error", runId, command: command.name, error: String(error?.stack ?? error) });
|
|
5635
|
-
});
|
|
5636
|
-
child.on("exit", (code, signal) => {
|
|
5637
|
-
commandProcesses.delete(runId);
|
|
5638
|
-
broadcast({ type: "command.exited", runId, command: command.name, code, signal });
|
|
5618
|
+
// terminal UI, so they run under a PTY; everything else uses ordinary pipes.
|
|
5619
|
+
const handle = launchCommand({ command: command.spawn.command, args: command.spawn.args, cwd: repoRoot, env }, command.spawn.requiresTty, {
|
|
5620
|
+
onOutput: (stream, text) => broadcast({ type: "command.output", runId, command: command.name, stream, text: stripAnsi(text) }),
|
|
5621
|
+
onError: (error) => {
|
|
5622
|
+
commandProcesses.delete(runId);
|
|
5623
|
+
broadcast({ type: "command.error", runId, command: command.name, error: String(error?.stack ?? error) });
|
|
5624
|
+
},
|
|
5625
|
+
onExit: (code, signal) => {
|
|
5626
|
+
commandProcesses.delete(runId);
|
|
5627
|
+
broadcast({ type: "command.exited", runId, command: command.name, code, signal });
|
|
5628
|
+
},
|
|
5639
5629
|
});
|
|
5630
|
+
commandProcesses.set(runId, handle);
|
|
5640
5631
|
return { ok: true, runId };
|
|
5641
5632
|
}
|
|
5642
5633
|
const guardianInterceptorImpl = async ({ sessionId, toolName, input, signal }) => {
|
|
@@ -11085,14 +11076,14 @@ app.post("/api/commands/:runId/input", (req, res) => {
|
|
|
11085
11076
|
const child = commandProcesses.get(req.params.runId);
|
|
11086
11077
|
if (!child)
|
|
11087
11078
|
return res.status(404).json({ error: "No active command process" });
|
|
11088
|
-
child.
|
|
11079
|
+
child.write(`${String(req.body?.text ?? "")}\n`);
|
|
11089
11080
|
res.json({ ok: true });
|
|
11090
11081
|
});
|
|
11091
11082
|
app.post("/api/commands/:runId/terminate", (req, res) => {
|
|
11092
11083
|
const child = commandProcesses.get(req.params.runId);
|
|
11093
11084
|
if (!child)
|
|
11094
11085
|
return res.status(404).json({ error: "No active command process" });
|
|
11095
|
-
child.
|
|
11086
|
+
child.interrupt();
|
|
11096
11087
|
res.json({ ok: true });
|
|
11097
11088
|
});
|
|
11098
11089
|
// On-demand "update GitHub status" for one session: force `refreshPullRequests`
|
|
@@ -397,12 +397,12 @@ export function baseReplay(entries) {
|
|
|
397
397
|
}
|
|
398
398
|
return base;
|
|
399
399
|
}
|
|
400
|
-
function parseLogDetailed(body) {
|
|
400
|
+
function parseLogDetailed(body, keep = () => true) {
|
|
401
401
|
const records = [];
|
|
402
402
|
let malformedLines = 0;
|
|
403
403
|
for (const line of body.split("\n")) {
|
|
404
404
|
const trimmed = line.trim();
|
|
405
|
-
if (!trimmed)
|
|
405
|
+
if (!trimmed || !keep(trimmed))
|
|
406
406
|
continue;
|
|
407
407
|
try {
|
|
408
408
|
const value = JSON.parse(trimmed);
|
|
@@ -429,6 +429,10 @@ export function parseLog(body) {
|
|
|
429
429
|
* `throttleMs` per session (a burst of same-id deltas within a window collapses to
|
|
430
430
|
* one appended line). Durability at boundaries is explicit `flush()` (turn-end,
|
|
431
431
|
* close, delete, shutdown), mirroring `SidecarStore`.
|
|
432
|
+
*
|
|
433
|
+
* The in-memory copy of flushed records is an LRU capped at `maxCachedBytes`
|
|
434
|
+
* (measured as log-file size): an evicted session simply re-reads its file on next
|
|
435
|
+
* use, so memory tracks the sessions in use, not the node's whole history.
|
|
432
436
|
*/
|
|
433
437
|
export class EventLog {
|
|
434
438
|
dir;
|
|
@@ -436,7 +440,10 @@ export class EventLog {
|
|
|
436
440
|
redact;
|
|
437
441
|
throttleMs;
|
|
438
442
|
onIssue;
|
|
443
|
+
maxCachedBytes;
|
|
439
444
|
disk = new Map();
|
|
445
|
+
diskBytes = new Map();
|
|
446
|
+
cachedBytes = 0;
|
|
440
447
|
pending = new Map();
|
|
441
448
|
timers = new Map();
|
|
442
449
|
lastFlush = new Map();
|
|
@@ -446,12 +453,13 @@ export class EventLog {
|
|
|
446
453
|
// on first use of a session after a restart.
|
|
447
454
|
baseKeys = new Map();
|
|
448
455
|
lastIssue;
|
|
449
|
-
constructor(dir, pathFor, redact = (t) => t, throttleMs = 500, onIssue = (issue) => console.error(`[event-log] ${issue.operation} failed for ${issue.sessionId}: ${issue.message}`)) {
|
|
456
|
+
constructor(dir, pathFor, redact = (t) => t, throttleMs = 500, onIssue = (issue) => console.error(`[event-log] ${issue.operation} failed for ${issue.sessionId}: ${issue.message}`), maxCachedBytes = 64 * 1024 * 1024) {
|
|
450
457
|
this.dir = dir;
|
|
451
458
|
this.pathFor = pathFor;
|
|
452
459
|
this.redact = redact;
|
|
453
460
|
this.throttleMs = throttleMs;
|
|
454
461
|
this.onIssue = onIssue;
|
|
462
|
+
this.maxCachedBytes = maxCachedBytes;
|
|
455
463
|
}
|
|
456
464
|
report(id, operation, error) {
|
|
457
465
|
const issue = {
|
|
@@ -493,23 +501,64 @@ export class EventLog {
|
|
|
493
501
|
}
|
|
494
502
|
return { files, bytes };
|
|
495
503
|
}
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
if (cached)
|
|
499
|
-
return cached;
|
|
500
|
-
let data = [];
|
|
504
|
+
/** Read + parse a session's log file (lines passing `keep`), reporting problems. */
|
|
505
|
+
readLog(id, keep) {
|
|
501
506
|
try {
|
|
502
|
-
const
|
|
503
|
-
|
|
507
|
+
const body = fs.readFileSync(this.pathFor(id), "utf8");
|
|
508
|
+
const parsed = parseLogDetailed(body, keep);
|
|
504
509
|
if (parsed.malformedLines > 0)
|
|
505
510
|
this.report(id, "parse", new Error(`${parsed.malformedLines} malformed record(s); valid history was recovered`));
|
|
511
|
+
return { records: parsed.records, bytes: body.length };
|
|
506
512
|
}
|
|
507
513
|
catch (error) {
|
|
508
514
|
if (error?.code !== "ENOENT")
|
|
509
515
|
this.report(id, "read", error);
|
|
516
|
+
return { records: [], bytes: 0 };
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
load(id) {
|
|
520
|
+
const cached = this.disk.get(id);
|
|
521
|
+
if (cached) {
|
|
522
|
+
// Re-insert to mark most-recently-used (Map iterates in insertion order).
|
|
523
|
+
this.disk.delete(id);
|
|
524
|
+
this.disk.set(id, cached);
|
|
525
|
+
return cached;
|
|
510
526
|
}
|
|
511
|
-
this.
|
|
512
|
-
|
|
527
|
+
const { records, bytes } = this.readLog(id);
|
|
528
|
+
this.cache(id, records, bytes);
|
|
529
|
+
return records;
|
|
530
|
+
}
|
|
531
|
+
/** Store a session's flushed records, then evict least-recently-used others past the cap. */
|
|
532
|
+
cache(id, records, bytes) {
|
|
533
|
+
this.uncache(id);
|
|
534
|
+
this.disk.set(id, records);
|
|
535
|
+
this.diskBytes.set(id, bytes);
|
|
536
|
+
this.cachedBytes += bytes;
|
|
537
|
+
for (const oldest of this.disk.keys()) {
|
|
538
|
+
if (this.cachedBytes <= this.maxCachedBytes || oldest === id)
|
|
539
|
+
break;
|
|
540
|
+
this.uncache(oldest);
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
uncache(id) {
|
|
544
|
+
this.cachedBytes -= this.diskBytes.get(id) ?? 0;
|
|
545
|
+
this.diskBytes.delete(id);
|
|
546
|
+
this.disk.delete(id);
|
|
547
|
+
}
|
|
548
|
+
/**
|
|
549
|
+
* The session's records of the given kinds, read WITHOUT caching the log. For
|
|
550
|
+
* whole-store sweeps (attachment GC) that visit every session, where `load` would
|
|
551
|
+
* pin every log in memory. Uses the cached copy when there is one, and always
|
|
552
|
+
* includes pending records so unflushed writes are never missed. Off the cache only
|
|
553
|
+
* candidate lines are parsed: those naming a wanted kind, plus torn lines (not
|
|
554
|
+
* ending in `}`) so a truncated write is still reported and callers can fail closed.
|
|
555
|
+
*/
|
|
556
|
+
scan(id, kinds) {
|
|
557
|
+
const wanted = new Set(kinds);
|
|
558
|
+
const markers = kinds.map((kind) => `"bivyKind":${JSON.stringify(kind)}`);
|
|
559
|
+
const flushed = this.disk.get(id) ?? this.readLog(id, (line) => !line.endsWith("}") || markers.some((m) => line.includes(m))).records;
|
|
560
|
+
const pending = this.pending.get(id)?.values() ?? [];
|
|
561
|
+
return [...flushed, ...pending].filter((record) => wanted.has(record.bivyKind));
|
|
513
562
|
}
|
|
514
563
|
/** Allocate a unique synthetic coalescing key (for id-less records). */
|
|
515
564
|
syntheticKey(id) {
|
|
@@ -681,9 +730,16 @@ export class EventLog {
|
|
|
681
730
|
const lines = [...batch.values()];
|
|
682
731
|
try {
|
|
683
732
|
fs.mkdirSync(this.dir, { recursive: true });
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
733
|
+
const body = this.redact(lines.map((e) => JSON.stringify(e)).join("\n") + "\n");
|
|
734
|
+
fs.appendFileSync(this.pathFor(id), body);
|
|
735
|
+
// Extend the cached copy only if it is still cached: an evicted session re-reads
|
|
736
|
+
// the file (which now holds these lines) on next use, so loading here would
|
|
737
|
+
// double-count them.
|
|
738
|
+
const cached = this.disk.get(id);
|
|
739
|
+
if (cached) {
|
|
740
|
+
cached.push(...lines);
|
|
741
|
+
this.cache(id, cached, (this.diskBytes.get(id) ?? 0) + body.length);
|
|
742
|
+
}
|
|
687
743
|
batch.clear();
|
|
688
744
|
this.lastFlush.set(id, Date.now());
|
|
689
745
|
}
|
|
@@ -708,10 +764,10 @@ export class EventLog {
|
|
|
708
764
|
}
|
|
709
765
|
this.pending.delete(id);
|
|
710
766
|
this.baseKeys.delete(id);
|
|
711
|
-
|
|
767
|
+
const body = copy.length ? copy.map((e) => JSON.stringify(e)).join("\n") + "\n" : "";
|
|
768
|
+
this.cache(id, copy, body.length);
|
|
712
769
|
try {
|
|
713
770
|
fs.mkdirSync(this.dir, { recursive: true });
|
|
714
|
-
const body = copy.length ? copy.map((e) => JSON.stringify(e)).join("\n") + "\n" : "";
|
|
715
771
|
fs.writeFileSync(this.pathFor(id), this.redact(body));
|
|
716
772
|
this.lastFlush.set(id, Date.now());
|
|
717
773
|
}
|
|
@@ -726,7 +782,7 @@ export class EventLog {
|
|
|
726
782
|
clearTimeout(timer);
|
|
727
783
|
this.timers.delete(id);
|
|
728
784
|
}
|
|
729
|
-
this.
|
|
785
|
+
this.uncache(id);
|
|
730
786
|
this.pending.delete(id);
|
|
731
787
|
this.lastFlush.delete(id);
|
|
732
788
|
this.counters.delete(id);
|
package/dist/terminal.js
CHANGED
|
@@ -6,7 +6,7 @@ import { createRequire } from "node:module";
|
|
|
6
6
|
import path from "node:path";
|
|
7
7
|
import { depCacheEnv } from "./harness/dep-cache.js";
|
|
8
8
|
let ptyModule;
|
|
9
|
-
function loadPty() {
|
|
9
|
+
export function loadPty() {
|
|
10
10
|
if (ptyModule)
|
|
11
11
|
return ptyModule;
|
|
12
12
|
if (ptyModule === null) {
|
package/package.json
CHANGED
package/dist/pty-runner.py
DELETED
|
@@ -1,95 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
"""Run a command under a pseudo-terminal while relaying stdio over pipes."""
|
|
3
|
-
|
|
4
|
-
import os
|
|
5
|
-
import pty
|
|
6
|
-
import select
|
|
7
|
-
import signal
|
|
8
|
-
import subprocess
|
|
9
|
-
import sys
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
def main() -> int:
|
|
13
|
-
if len(sys.argv) < 2:
|
|
14
|
-
print("usage: pty-runner.py command [args...]", file=sys.stderr)
|
|
15
|
-
return 2
|
|
16
|
-
|
|
17
|
-
master_fd, slave_fd = pty.openpty()
|
|
18
|
-
env = os.environ.copy()
|
|
19
|
-
env.setdefault("TERM", "xterm-256color")
|
|
20
|
-
|
|
21
|
-
proc = subprocess.Popen(
|
|
22
|
-
sys.argv[1:],
|
|
23
|
-
stdin=slave_fd,
|
|
24
|
-
stdout=slave_fd,
|
|
25
|
-
stderr=slave_fd,
|
|
26
|
-
env=env,
|
|
27
|
-
close_fds=True,
|
|
28
|
-
start_new_session=True,
|
|
29
|
-
)
|
|
30
|
-
os.close(slave_fd)
|
|
31
|
-
|
|
32
|
-
def forward_signal(signum, _frame):
|
|
33
|
-
try:
|
|
34
|
-
os.killpg(proc.pid, signum)
|
|
35
|
-
except ProcessLookupError:
|
|
36
|
-
pass
|
|
37
|
-
|
|
38
|
-
signal.signal(signal.SIGINT, forward_signal)
|
|
39
|
-
signal.signal(signal.SIGTERM, forward_signal)
|
|
40
|
-
|
|
41
|
-
stdin_fd = sys.stdin.fileno()
|
|
42
|
-
stdout_fd = sys.stdout.fileno()
|
|
43
|
-
stdin_open = True
|
|
44
|
-
|
|
45
|
-
while True:
|
|
46
|
-
read_fds = [master_fd]
|
|
47
|
-
if stdin_open:
|
|
48
|
-
read_fds.append(stdin_fd)
|
|
49
|
-
|
|
50
|
-
readable, _, _ = select.select(read_fds, [], [], 0.1)
|
|
51
|
-
|
|
52
|
-
if master_fd in readable:
|
|
53
|
-
try:
|
|
54
|
-
data = os.read(master_fd, 8192)
|
|
55
|
-
except OSError:
|
|
56
|
-
data = b""
|
|
57
|
-
if not data:
|
|
58
|
-
break
|
|
59
|
-
os.write(stdout_fd, data)
|
|
60
|
-
|
|
61
|
-
if stdin_open and stdin_fd in readable:
|
|
62
|
-
data = os.read(stdin_fd, 8192)
|
|
63
|
-
if not data:
|
|
64
|
-
stdin_open = False
|
|
65
|
-
else:
|
|
66
|
-
try:
|
|
67
|
-
os.write(master_fd, data)
|
|
68
|
-
except OSError:
|
|
69
|
-
break
|
|
70
|
-
|
|
71
|
-
if proc.poll() is not None:
|
|
72
|
-
# Drain any final PTY output.
|
|
73
|
-
while True:
|
|
74
|
-
readable, _, _ = select.select([master_fd], [], [], 0)
|
|
75
|
-
if not readable:
|
|
76
|
-
break
|
|
77
|
-
try:
|
|
78
|
-
data = os.read(master_fd, 8192)
|
|
79
|
-
except OSError:
|
|
80
|
-
break
|
|
81
|
-
if not data:
|
|
82
|
-
break
|
|
83
|
-
os.write(stdout_fd, data)
|
|
84
|
-
break
|
|
85
|
-
|
|
86
|
-
try:
|
|
87
|
-
os.close(master_fd)
|
|
88
|
-
except OSError:
|
|
89
|
-
pass
|
|
90
|
-
|
|
91
|
-
return proc.wait()
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
if __name__ == "__main__":
|
|
95
|
-
raise SystemExit(main())
|