Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

First pass on running subprocesses #1156

Merged
merged 2 commits into from
Nov 16, 2018
Merged

First pass on running subprocesses #1156

merged 2 commits into from
Nov 16, 2018

Conversation

piscisaureus
Copy link
Member

@piscisaureus piscisaureus commented Nov 5, 2018

No description provided.

@piscisaureus piscisaureus changed the title First pass on running child processes [WIP] first pass on running child processes Nov 5, 2018
Copy link
Member

@ry ry left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cool! Needs tests

js/command.ts Outdated Show resolved Hide resolved
src/ops.rs Outdated Show resolved Hide resolved
src/ops.rs Outdated Show resolved Hide resolved
Copy link
Contributor

@kitsonk kitsonk left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Couple comments. 🎉 was wanting this!

js/deno.ts Outdated Show resolved Hide resolved
js/command.ts Outdated Show resolved Hide resolved
js/command.ts Outdated Show resolved Hide resolved
@piscisaureus piscisaureus changed the title [WIP] first pass on running child processes First pass on running child processes Nov 8, 2018
@piscisaureus piscisaureus requested a review from ry November 8, 2018 22:30
js/command_test.ts Outdated Show resolved Hide resolved
js/command.ts Outdated Show resolved Hide resolved
js/command.ts Outdated Show resolved Hide resolved
js/command.ts Outdated Show resolved Hide resolved
src/ops.rs Outdated Show resolved Hide resolved
js/command_test.ts Outdated Show resolved Hide resolved
@piscisaureus
Copy link
Member Author

https://code.visualstudio.com/docs/editor/tasks-appendix#_schema-for-tasksjson

^-- Also uses a CommandOptions interface

@ry
Copy link
Member

ry commented Nov 12, 2018

https://code.visualstudio.com/docs/editor/tasks-appendix#_schema-for-tasksjson

^-- Also uses a CommandOptions interface

Very nice! Be sure to add that link in the source code so we maintain compatibility.

@ry
Copy link
Member

ry commented Nov 12, 2018

I've refactored this API into two ops, so that child processes can be stored on the resources table. run() is now a sync op, which return a Child object.
await child.status() allows you to get the exit status
child.close() removes the resource.

I'm not so happy with all these different names "Command", "Run", "Child". I think we should use a single name for all these concepts. Does anyone have opinions on this?

@ry ry force-pushed the run branch 2 times, most recently from 5a09bf8 to 0ad8c3f Compare November 13, 2018 01:53
@ry
Copy link
Member

ry commented Nov 13, 2018

There is a lot to consider in this interface. We should name carefully. There are two distinct things to name in the low-level interface - the function which starts the subprocess - and the name of the subprocess object (a noun). Furthermore have the name of the command-line argument/permission. Ideally we want to introduce as few names as possible, we also want to be intelligible to people coming from different languages.

Here are a few options.

Option 1 Just call everything subprocess. Familiar to python people, correct terminology-wise. However it's very long.

--allow-subprocess   

let sp = deno.subprocess();
let status = await sp.status();
sp.close();

Option 2 Use "Subprocess" for the object but use "exec()" for the function. Exec isn't exactly right, as it has a fairly specific meaning - which we might want to expose at some point (but probably not). However it's short.

--allow-exec

let subprocess = deno.exec({ args: ["python"] });
let status = await subprocess.status();
subprocess.close();

Option 3 Use "Process" for the object but use "run()" for the function. run is short and doesn't overload concepts in Node. (Current naming)

--allow-run

let p = deno.run({ args: ["python"] });
let status = await p.status();
p.close();

@kitsonk
Copy link
Contributor

kitsonk commented Nov 13, 2018

We have compiler.run(), and while it is internal, it is what is used for executing a Deno programme at the moment. We don't really have the API's for running other JavaScript/Deno programmes yet, but it seems like something in the future.

I would vote for Option 1. It is clear in intent. The only other thought is going less functional and more class based and simply. I guess how much consistency do we want between constructors or factories for things?

const p = new Subprocess({ args: ["python"] });
let status = await p.status();
p.close();

@ry ry force-pushed the run branch 5 times, most recently from 0bcaaaa to f12b112 Compare November 13, 2018 10:35
@ry ry changed the title First pass on running child processes First pass on running subprocesses Nov 13, 2018
js/subprocess.ts Outdated
}
}

export interface SubprocessStatus {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This type doesn't properly capture that either code or signal will have a value.
Thus the user won't be able to do:

declare function handleExitCode(code: number);
declare function handleExitSignal(sig: number);

if (status.code != null) {
  handleExitCode(status.code);
} else {
  handleExitSignal(status.signal);
}

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That code works currently.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I concede -- It works because we using typescript in sloppy mode.
OK for now then.

js/subprocess.ts Outdated Show resolved Hide resolved
js/subprocess.ts Outdated Show resolved Hide resolved
base: &msg::Base,
data: &'static mut [u8],
) -> Box<Op> {
assert!(base.sync());
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@piscisaureus Note that it is fundamentally sync. I don't think it blocks tho. Can you elaborate on your problem with it being sync?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay if we hereby agree that RIDs should be assigned in on the JS side and not on the rust side and we'll make that in a follow-up patch.
Otherwise this adds a low level API that is fundamentally sync, which I will not approve.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The issue is that run() cannot return until the rust side has assigned a RID to the to the SubProcess object.
So that gets in the way op queuing/batching, out-of-process backend etc. that we may want to do in the future.
I'm not against sync ops but I am against ops that can only be sync.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, I see what you mean. Yes, in order to support "dispatchv" we would need to allocate RIDs on the JS side. I'm ok with that. However there are many places currently where we're allocating RIDs and I think we should change them all to be allocated in JS at once. Therefore I'd argue in favor of keeping this as it is for now.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, in order to support "dispatchv" we would need to allocate RIDs on the JS side. I'm ok with that.

Ok, then we're golden.

test(async function runPermissions() {
let caughtError = false;
try {
deno.run({ args: ["python", "-c", "print('hello world')"] });
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Other tests are using imported run function, but this one uses deno.run. I suggest using deno.run in all tests since it's better shows how deno's public api is used.

Copy link
Member Author

@piscisaureus piscisaureus left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM. Can't approve since it's my own PR.

@ry
Copy link
Member

ry commented Nov 14, 2018

I want to propose the following API. Which is based very closely on Go.

let cmd = deno.command("python", "-c", "print 'hi'");
let status = await cmd.run(); 
// run() starts the specified command and waits for it to complete.

Or if you want to start it and later wait for it to complete

let cmd = deno.command("python", "-c", "print 'hi'");
cmd.start();
let status = await cmd.exit(); 
// Go uses "Wait()" instead of "exit()". I replaced it because it sounds strange with await. 

Setting environmental variables (which won't be done in this patch)

let cmd = deno.command("prog");
cmd.env = Object.assign(deno.env(), {
  "FOO": "BAR"
});
let status = await cmd.run();

For stdin, stdout, stderr we would do the following to inherit from the parent process

let cmd = deno.command("prog");
cmd.stdout = deno.stdout;
cmd.stderr = deno.stderr;

Or to do the "piped" behavior

let cmd = deno.command("sh", "-c", "echo stdout; echo 1>&2 stderr");
let stderr = cmd.stderrPipe();
cmd.start();
let slurp = await deno.readAll(stderr); // Note readAll not implemented yet...

The Go API supports assigning an arbitrary File (in our case deno.File) to the stdio properties. We would not be able to achieve that with the current implementation of tokio_process.... But I think we should strive for ideal APIs and just die with notImplemented if someone supplies, for example, a File other than deno.stderr for cmd.stderr. Eventually we can replace tokio_process with something that matches these semantics better.

I think this API is straightforward and well thought-out. (Like most Go APIs.) It makes sense given how many other Go-inspired APIs we're using. It provides guidance in the future when expanding it (like in the case of env)

@ry
Copy link
Member

ry commented Nov 14, 2018

Go also has a lower-level API: https://golang.org/pkg/os/#Process
Let me translate some of these:

let p = deno.startProcess("/usr/bin/python", ["python", "-c", "print 'hi'"], {
  // chdir into /tmp
  dir: "/tmp",
  // Inherit stdout from parent, stdin and stderr are null.
  files: [null, deno.stdout, null], 
});
// To kill a subprocess
p.kill();
// To detach a subprocess
p.release();
// Send a signal
p.signal("SIGINT");
// Wait for completion
let status = await p.exit();

@piscisaureus
Copy link
Member Author

let p = deno.startProcess("/usr/bin/python", ["python", "-c", "print 'hi'"], { // chdir into /tmp dir: "/tmp", // Inherit stdout from parent, stdin and stderr are null. files: [null, deno.stdout, null], });

This is pretty similar to nodejs' spawn() api, except that here argv[0] is explicit.
That creates an awful mess when parsing the arguments. For a low level API we should really do just the options object:

startProcess({
  exe: "/usr/bin/python",  // Optional; if omitted, will be taken from args[0]
  args: ["python", "-c", "print 'hi'"],
  cwd: "/tmp"
});

@ry
Copy link
Member

ry commented Nov 15, 2018

That creates an awful mess when parsing the arguments.

I would argue that the startProcess() with the three arguments (program, argv, options) is simpler than the options object alone - there isn't anything implicit in the former - nothing that has to be explained to the user like "Optional; if omitted, will be taken from args[0]"

Although it's not a very important use-case, it's worth noting that argv[0] can be different than the executed program. So it makes sense for the low-level API to separate the two concepts.

Copy link
Member

@ry ry left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM - let's land this very useful patch and leave open the possibility for reworking these APIs later. I outlined my proposal in a comment above - after this is landed I will start an issue where we can discuss further.

@ry ry merged commit 48bf406 into denoland:master Nov 16, 2018
ry added a commit to ry/deno that referenced this pull request Nov 16, 2018
Changes since v0.1.12:
- First pass at running subprocesses (denoland#1156)
- Improve flag parsing (denoland#1200)
- Improve fetch() (denoland#1194 denoland#1188 denoland#1102)
- Support shebang (denoland#1197)
@ry ry mentioned this pull request Nov 16, 2018
ry added a commit that referenced this pull request Nov 16, 2018
Changes since v0.1.12:
- First pass at running subprocesses (#1156)
- Improve flag parsing (#1200)
- Improve fetch() (#1194 #1188 #1102)
- Support shebang (#1197)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

5 participants