Skip to content

Commit 642eaf9

Browse files
bartlomiejury
authored andcommitted
feat: redirect process stdio to file (#2554)
1 parent eb93dc5 commit 642eaf9

File tree

6 files changed

+189
-58
lines changed

6 files changed

+189
-58
lines changed

cli/msg.fbs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -538,6 +538,9 @@ table Run {
538538
stdin: ProcessStdio;
539539
stdout: ProcessStdio;
540540
stderr: ProcessStdio;
541+
stdin_rid: uint32;
542+
stdout_rid: uint32;
543+
stderr_rid: uint32;
541544
}
542545

543546
table RunRes {

cli/ops.rs

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1793,9 +1793,27 @@ fn op_run(
17931793
c.env(entry.key().unwrap(), entry.value().unwrap());
17941794
});
17951795

1796-
c.stdin(subprocess_stdio_map(inner.stdin()));
1797-
c.stdout(subprocess_stdio_map(inner.stdout()));
1798-
c.stderr(subprocess_stdio_map(inner.stderr()));
1796+
// TODO: make this work with other resources, eg. sockets
1797+
let stdin_rid = inner.stdin_rid();
1798+
if stdin_rid > 0 {
1799+
c.stdin(resources::get_file(stdin_rid)?);
1800+
} else {
1801+
c.stdin(subprocess_stdio_map(inner.stdin()));
1802+
}
1803+
1804+
let stdout_rid = inner.stdout_rid();
1805+
if stdout_rid > 0 {
1806+
c.stdout(resources::get_file(stdout_rid)?);
1807+
} else {
1808+
c.stdout(subprocess_stdio_map(inner.stdout()));
1809+
}
1810+
1811+
let stderr_rid = inner.stderr_rid();
1812+
if stderr_rid > 0 {
1813+
c.stderr(resources::get_file(stderr_rid)?);
1814+
} else {
1815+
c.stderr(subprocess_stdio_map(inner.stderr()));
1816+
}
17991817

18001818
// Spawn the command.
18011819
let child = c.spawn_async().map_err(DenoError::from)?;

cli/resources.rs

Lines changed: 47 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -492,29 +492,19 @@ pub fn get_repl(rid: ResourceId) -> DenoResult<Arc<Mutex<Repl>>> {
492492
}
493493
}
494494

495-
pub fn lookup(rid: ResourceId) -> Option<Resource> {
496-
debug!("resource lookup {}", rid);
497-
let table = RESOURCE_TABLE.lock().unwrap();
498-
table.get(&rid).map(|_| Resource { rid })
499-
}
500-
501-
// TODO(kevinkassimo): revamp this after the following lands:
495+
// TODO: revamp this after the following lands:
502496
// https://github.com/tokio-rs/tokio/pull/785
503-
pub fn seek(
504-
resource: Resource,
505-
offset: i32,
506-
whence: u32,
507-
) -> Box<dyn Future<Item = (), Error = DenoError> + Send> {
497+
pub fn get_file(rid: ResourceId) -> DenoResult<std::fs::File> {
508498
let mut table = RESOURCE_TABLE.lock().unwrap();
509499
// We take ownership of File here.
510500
// It is put back below while still holding the lock.
511-
let maybe_repr = table.remove(&resource.rid);
501+
let maybe_repr = table.remove(&rid);
502+
512503
match maybe_repr {
513-
None => panic!("bad rid"),
514-
Some(Repr::FsFile(f)) => {
504+
Some(Repr::FsFile(r)) => {
515505
// Trait Clone not implemented on tokio::fs::File,
516506
// so convert to std File first.
517-
let std_file = f.into_std();
507+
let std_file = r.into_std();
518508
// Create a copy and immediately put back.
519509
// We don't want to block other resource ops.
520510
// try_clone() would yield a copy containing the same
@@ -523,36 +513,49 @@ pub fn seek(
523513
// to write back.
524514
let maybe_std_file_copy = std_file.try_clone();
525515
// Insert the entry back with the same rid.
526-
table.insert(
527-
resource.rid,
528-
Repr::FsFile(tokio_fs::File::from_std(std_file)),
529-
);
530-
// Translate seek mode to Rust repr.
531-
let seek_from = match whence {
532-
0 => SeekFrom::Start(offset as u64),
533-
1 => SeekFrom::Current(i64::from(offset)),
534-
2 => SeekFrom::End(i64::from(offset)),
535-
_ => {
536-
return Box::new(futures::future::err(deno_error::new(
537-
deno_error::ErrorKind::InvalidSeekMode,
538-
format!("Invalid seek mode: {}", whence),
539-
)));
540-
}
541-
};
516+
table.insert(rid, Repr::FsFile(tokio_fs::File::from_std(std_file)));
517+
542518
if maybe_std_file_copy.is_err() {
543-
return Box::new(futures::future::err(DenoError::from(
544-
maybe_std_file_copy.unwrap_err(),
545-
)));
519+
return Err(DenoError::from(maybe_std_file_copy.unwrap_err()));
546520
}
547-
let mut std_file_copy = maybe_std_file_copy.unwrap();
548-
Box::new(futures::future::lazy(move || {
549-
let result = std_file_copy
550-
.seek(seek_from)
551-
.map(|_| {})
552-
.map_err(DenoError::from);
553-
futures::future::result(result)
554-
}))
521+
522+
let std_file_copy = maybe_std_file_copy.unwrap();
523+
524+
Ok(std_file_copy)
555525
}
556-
_ => panic!("cannot seek"),
526+
_ => Err(bad_resource()),
527+
}
528+
}
529+
530+
pub fn lookup(rid: ResourceId) -> Option<Resource> {
531+
debug!("resource lookup {}", rid);
532+
let table = RESOURCE_TABLE.lock().unwrap();
533+
table.get(&rid).map(|_| Resource { rid })
534+
}
535+
536+
pub fn seek(
537+
resource: Resource,
538+
offset: i32,
539+
whence: u32,
540+
) -> Box<dyn Future<Item = (), Error = DenoError> + Send> {
541+
// Translate seek mode to Rust repr.
542+
let seek_from = match whence {
543+
0 => SeekFrom::Start(offset as u64),
544+
1 => SeekFrom::Current(i64::from(offset)),
545+
2 => SeekFrom::End(i64::from(offset)),
546+
_ => {
547+
return Box::new(futures::future::err(deno_error::new(
548+
deno_error::ErrorKind::InvalidSeekMode,
549+
format!("Invalid seek mode: {}", whence),
550+
)));
551+
}
552+
};
553+
554+
match get_file(resource.rid) {
555+
Ok(mut file) => Box::new(futures::future::lazy(move || {
556+
let result = file.seek(seek_from).map(|_| {}).map_err(DenoError::from);
557+
futures::future::result(result)
558+
})),
559+
Err(err) => Box::new(futures::future::err(err)),
557560
}
558561
}

js/process.ts

Lines changed: 47 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -28,9 +28,9 @@ export interface RunOptions {
2828
args: string[];
2929
cwd?: string;
3030
env?: { [key: string]: string };
31-
stdout?: ProcessStdio;
32-
stderr?: ProcessStdio;
33-
stdin?: ProcessStdio;
31+
stdout?: ProcessStdio | number;
32+
stderr?: ProcessStdio | number;
33+
stdin?: ProcessStdio | number;
3434
}
3535

3636
async function runStatus(rid: number): Promise<ProcessStatus> {
@@ -149,6 +149,10 @@ function stdioMap(s: ProcessStdio): msg.ProcessStdio {
149149
}
150150
}
151151

152+
function isRid(arg: unknown): arg is number {
153+
return !isNaN(arg as number);
154+
}
155+
152156
/**
153157
* Spawns new subprocess.
154158
*
@@ -159,7 +163,8 @@ function stdioMap(s: ProcessStdio): msg.ProcessStdio {
159163
* mapping.
160164
*
161165
* By default subprocess inherits stdio of parent process. To change that
162-
* `opt.stdout`, `opt.stderr` and `opt.stdin` can be specified independently.
166+
* `opt.stdout`, `opt.stderr` and `opt.stdin` can be specified independently -
167+
* they can be set to either `ProcessStdio` or `rid` of open file.
163168
*/
164169
export function run(opt: RunOptions): Process {
165170
const builder = flatbuffers.createBuilder();
@@ -177,14 +182,49 @@ export function run(opt: RunOptions): Process {
177182
}
178183
}
179184
const envOffset = msg.Run.createEnvVector(builder, kvOffset);
185+
186+
let stdInOffset = stdioMap("inherit");
187+
let stdOutOffset = stdioMap("inherit");
188+
let stdErrOffset = stdioMap("inherit");
189+
let stdinRidOffset = 0;
190+
let stdoutRidOffset = 0;
191+
let stderrRidOffset = 0;
192+
193+
if (opt.stdin) {
194+
if (isRid(opt.stdin)) {
195+
stdinRidOffset = opt.stdin;
196+
} else {
197+
stdInOffset = stdioMap(opt.stdin);
198+
}
199+
}
200+
201+
if (opt.stdout) {
202+
if (isRid(opt.stdout)) {
203+
stdoutRidOffset = opt.stdout;
204+
} else {
205+
stdOutOffset = stdioMap(opt.stdout);
206+
}
207+
}
208+
209+
if (opt.stderr) {
210+
if (isRid(opt.stderr)) {
211+
stderrRidOffset = opt.stderr;
212+
} else {
213+
stdErrOffset = stdioMap(opt.stderr);
214+
}
215+
}
216+
180217
const inner = msg.Run.createRun(
181218
builder,
182219
argsOffset,
183220
cwdOffset,
184221
envOffset,
185-
opt.stdin ? stdioMap(opt.stdin) : stdioMap("inherit"),
186-
opt.stdout ? stdioMap(opt.stdout) : stdioMap("inherit"),
187-
opt.stderr ? stdioMap(opt.stderr) : stdioMap("inherit")
222+
stdInOffset,
223+
stdOutOffset,
224+
stdErrOffset,
225+
stdinRidOffset,
226+
stdoutRidOffset,
227+
stderrRidOffset
188228
);
189229
const baseRes = dispatch.sendSync(builder, msg.Any.Run, inner);
190230
assert(baseRes != null);

js/process_test.ts

Lines changed: 69 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,21 @@
11
// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license.
2-
import { test, testPerm, assert, assertEquals } from "./test_util.ts";
3-
const { kill, run, DenoError, ErrorKind } = Deno;
2+
import {
3+
test,
4+
testPerm,
5+
assert,
6+
assertEquals,
7+
assertStrContains
8+
} from "./test_util.ts";
9+
const {
10+
kill,
11+
run,
12+
DenoError,
13+
ErrorKind,
14+
readFile,
15+
open,
16+
makeTempDir,
17+
writeFile
18+
} = Deno;
419

520
test(function runPermissions(): void {
621
let caughtError = false;
@@ -71,7 +86,7 @@ testPerm(
7186
{ write: true, run: true },
7287
async function runWithCwdIsAsync(): Promise<void> {
7388
const enc = new TextEncoder();
74-
const cwd = Deno.makeTempDirSync({ prefix: "deno_command_test" });
89+
const cwd = await makeTempDir({ prefix: "deno_command_test" });
7590

7691
const exitCodeFile = "deno_was_here";
7792
const pyProgramFile = "poll_exit.py";
@@ -205,6 +220,57 @@ testPerm({ run: true }, async function runStderrOutput(): Promise<void> {
205220
p.close();
206221
});
207222

223+
testPerm(
224+
{ run: true, write: true, read: true },
225+
async function runRedirectStdoutStderr(): Promise<void> {
226+
const tempDir = await makeTempDir();
227+
const fileName = tempDir + "/redirected_stdio.txt";
228+
const file = await open(fileName, "w");
229+
230+
const p = run({
231+
args: [
232+
"python",
233+
"-c",
234+
"import sys; sys.stderr.write('error\\n'); sys.stdout.write('output\\n');"
235+
],
236+
stdout: file.rid,
237+
stderr: file.rid
238+
});
239+
240+
await p.status();
241+
p.close();
242+
file.close();
243+
244+
const fileContents = await readFile(fileName);
245+
const decoder = new TextDecoder();
246+
const text = decoder.decode(fileContents);
247+
248+
assertStrContains(text, "error");
249+
assertStrContains(text, "output");
250+
}
251+
);
252+
253+
testPerm(
254+
{ run: true, write: true, read: true },
255+
async function runRedirectStdin(): Promise<void> {
256+
const tempDir = await makeTempDir();
257+
const fileName = tempDir + "/redirected_stdio.txt";
258+
const encoder = new TextEncoder();
259+
await writeFile(fileName, encoder.encode("hello"));
260+
const file = await open(fileName, "r");
261+
262+
const p = run({
263+
args: ["python", "-c", "import sys; assert 'hello' == sys.stdin.read();"],
264+
stdin: file.rid
265+
});
266+
267+
const status = await p.status();
268+
assertEquals(status.code, 0);
269+
p.close();
270+
file.close();
271+
}
272+
);
273+
208274
testPerm({ run: true }, async function runEnv(): Promise<void> {
209275
const p = run({
210276
args: [

js/test_util.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,8 @@ export {
1616
assert,
1717
assertEquals,
1818
assertNotEquals,
19-
assertStrictEq
19+
assertStrictEq,
20+
assertStrContains
2021
} from "./deps/https/deno.land/std/testing/asserts.ts";
2122

2223
interface TestPermissions {

0 commit comments

Comments
 (0)