diff --git a/Cargo.lock b/Cargo.lock index 46754b8b51..9c73d7ad24 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -225,6 +225,7 @@ dependencies = [ "lazy_static 1.3.0 (registry+https://github.com/rust-lang/crates.io-index)", "libc 0.2.51 (registry+https://github.com/rust-lang/crates.io-index)", "log 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)", + "nix 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)", "rand 0.6.5 (registry+https://github.com/rust-lang/crates.io-index)", "regex 1.1.6 (registry+https://github.com/rust-lang/crates.io-index)", "remove_dir_all 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)", diff --git a/cli/BUILD.gn b/cli/BUILD.gn index a292e559fa..7bf34dec34 100644 --- a/cli/BUILD.gn +++ b/cli/BUILD.gn @@ -42,6 +42,9 @@ main_extern = [ if (is_win) { main_extern += [ "$rust_build:winapi" ] } +if (is_posix) { + main_extern += [ "$rust_build:nix" ] +} ts_sources = [ "../js/assets.ts", diff --git a/cli/Cargo.toml b/cli/Cargo.toml index a7a7806188..ad921cbddf 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -50,3 +50,6 @@ url = "1.7.2" [target.'cfg(windows)'.dependencies] winapi = "0.3.7" + +[target.'cfg(unix)'.dependencies] +nix = "0.11.0" diff --git a/cli/errors.rs b/cli/errors.rs index bd3e7ba738..4240915840 100644 --- a/cli/errors.rs +++ b/cli/errors.rs @@ -4,6 +4,8 @@ pub use crate::msg::ErrorKind; use crate::resolve_addr::ResolveAddrError; use deno::JSError; use hyper; +#[cfg(unix)] +use nix::{errno::Errno, Error as UnixError}; use std; use std::fmt; use std::io; @@ -168,6 +170,32 @@ impl From for DenoError { } } +#[cfg(unix)] +impl From for DenoError { + fn from(e: UnixError) -> Self { + match e { + UnixError::Sys(Errno::EPERM) => Self { + repr: Repr::Simple( + ErrorKind::PermissionDenied, + Errno::EPERM.desc().to_owned(), + ), + }, + UnixError::Sys(Errno::EINVAL) => Self { + repr: Repr::Simple( + ErrorKind::InvalidInput, + Errno::EINVAL.desc().to_owned(), + ), + }, + UnixError::Sys(err) => Self { + repr: Repr::Simple(ErrorKind::UnixError, err.desc().to_owned()), + }, + _ => Self { + repr: Repr::Simple(ErrorKind::Other, format!("{}", e)), + }, + } + } +} + pub fn bad_resource() -> DenoError { new(ErrorKind::BadResource, String::from("bad resource id")) } diff --git a/cli/main.rs b/cli/main.rs index 4d12b6c898..41b04785c4 100644 --- a/cli/main.rs +++ b/cli/main.rs @@ -9,6 +9,8 @@ extern crate futures; extern crate serde_json; extern crate clap; extern crate deno; +#[cfg(unix)] +extern crate nix; mod ansi; pub mod compiler; @@ -27,6 +29,7 @@ pub mod permissions; mod repl; pub mod resolve_addr; pub mod resources; +mod signal; mod startup_data; pub mod state; mod tokio_util; diff --git a/cli/msg.fbs b/cli/msg.fbs index 4eca8dcf85..d217fc7ba7 100644 --- a/cli/msg.fbs +++ b/cli/msg.fbs @@ -21,6 +21,7 @@ union Any { GlobalTimerStop, IsTTY, IsTTYRes, + Kill, Link, Listen, ListenRes, @@ -129,7 +130,8 @@ enum ErrorKind: byte { InvalidUri, InvalidSeekMode, OpNotAvaiable, - WorkerInitFailed + WorkerInitFailed, + UnixError, } table Cwd {} @@ -453,6 +455,11 @@ table Close { rid: uint32; } +table Kill { + pid: int32; + signo: int32; +} + table Shutdown { rid: uint32; how: uint; diff --git a/cli/ops.rs b/cli/ops.rs index 044e0fc20a..bc06a2fb71 100644 --- a/cli/ops.rs +++ b/cli/ops.rs @@ -14,6 +14,7 @@ use crate::resolve_addr::resolve_addr; use crate::resources; use crate::resources::table_entries; use crate::resources::Resource; +use crate::signal::kill; use crate::startup_data; use crate::state::ThreadSafeState; use crate::tokio_util; @@ -171,6 +172,7 @@ pub fn op_selector_std(inner_type: msg::Any) -> Option { msg::Any::GlobalTimer => Some(op_global_timer), msg::Any::GlobalTimerStop => Some(op_global_timer_stop), msg::Any::IsTTY => Some(op_is_tty), + msg::Any::Kill => Some(op_kill), msg::Any::Link => Some(op_link), msg::Any::Listen => Some(op_listen), msg::Any::MakeTempDir => Some(op_make_temp_dir), @@ -906,6 +908,21 @@ fn op_close( } } +fn op_kill( + _state: &ThreadSafeState, + base: &msg::Base<'_>, + data: deno_buf, +) -> Box { + assert_eq!(data.len(), 0); + let inner = base.inner_as_kill().unwrap(); + let pid = inner.pid(); + let signo = inner.signo(); + match kill(pid, signo) { + Ok(_) => ok_future(empty_buf()), + Err(e) => odd_future(e), + } +} + fn op_shutdown( _state: &ThreadSafeState, base: &msg::Base<'_>, diff --git a/cli/signal.rs b/cli/signal.rs new file mode 100644 index 0000000000..7d67ba7433 --- /dev/null +++ b/cli/signal.rs @@ -0,0 +1,20 @@ +#[cfg(unix)] +use nix::sys::signal::{kill as unix_kill, Signal}; +#[cfg(unix)] +use nix::unistd::Pid; + +use crate::errors::DenoResult; + +#[cfg(unix)] +pub fn kill(pid: i32, signo: i32) -> DenoResult<()> { + use crate::errors::DenoError; + let sig = Signal::from_c_int(signo)?; + unix_kill(Pid::from_raw(pid), Option::Some(sig)).map_err(DenoError::from) +} + +#[cfg(not(unix))] +pub fn kill(_pid: i32, _signal: i32) -> DenoResult<()> { + // NOOP + // TODO: implement this for windows + Ok(()) +} diff --git a/js/deno.ts b/js/deno.ts index 51cc0791a2..46f018afc6 100644 --- a/js/deno.ts +++ b/js/deno.ts @@ -68,7 +68,14 @@ export { FileInfo } from "./file_info"; export { connect, dial, listen, Listener, Conn } from "./net"; export { metrics, Metrics } from "./metrics"; export { resources } from "./resources"; -export { run, RunOptions, Process, ProcessStatus } from "./process"; +export { + kill, + run, + RunOptions, + Process, + ProcessStatus, + Signal +} from "./process"; export { inspect } from "./console"; export { build, platform, OperatingSystem, Arch } from "./build"; export { version } from "./version"; diff --git a/js/process.ts b/js/process.ts index a0eef63ddd..c0a66f3118 100644 --- a/js/process.ts +++ b/js/process.ts @@ -7,6 +7,7 @@ import { File, close } from "./files"; import { ReadCloser, WriteCloser } from "./io"; import { readAll } from "./buffer"; import { assert, unreachable } from "./util"; +import { platform } from "./build"; /** How to handle subprocess stdio. * @@ -51,6 +52,16 @@ async function runStatus(rid: number): Promise { } } +/** Send a signal to process under given PID. Unix only at this moment. + * If pid is negative, the signal will be sent to the process group identified + * by -pid. + */ +export function kill(pid: number, signo: number): void { + const builder = flatbuffers.createBuilder(); + const inner = msg.Kill.createKill(builder, pid, signo); + dispatch.sendSync(builder, msg.Any.Kill, inner); +} + export class Process { readonly rid: number; readonly pid: number; @@ -113,6 +124,10 @@ export class Process { close(): void { close(this.rid); } + + kill(signo: number): void { + kill(this.pid, signo); + } } export interface ProcessStatus { @@ -179,3 +194,77 @@ export function run(opt: RunOptions): Process { return new Process(res); } + +// From `kill -l` +enum LinuxSignal { + SIGHUP = 1, + SIGINT = 2, + SIGQUIT = 3, + SIGILL = 4, + SIGTRAP = 5, + SIGABRT = 6, + SIGBUS = 7, + SIGFPE = 8, + SIGKILL = 9, + SIGUSR1 = 10, + SIGSEGV = 11, + SIGUSR2 = 12, + SIGPIPE = 13, + SIGALRM = 14, + SIGTERM = 15, + SIGSTKFLT = 16, + SIGCHLD = 17, + SIGCONT = 18, + SIGSTOP = 19, + SIGTSTP = 20, + SIGTTIN = 21, + SIGTTOU = 22, + SIGURG = 23, + SIGXCPU = 24, + SIGXFSZ = 25, + SIGVTALRM = 26, + SIGPROF = 27, + SIGWINCH = 28, + SIGIO = 29, + SIGPWR = 30, + SIGSYS = 31 +} + +// From `kill -l` +enum MacOSSignal { + SIGHUP = 1, + SIGINT = 2, + SIGQUIT = 3, + SIGILL = 4, + SIGTRAP = 5, + SIGABRT = 6, + SIGEMT = 7, + SIGFPE = 8, + SIGKILL = 9, + SIGBUS = 10, + SIGSEGV = 11, + SIGSYS = 12, + SIGPIPE = 13, + SIGALRM = 14, + SIGTERM = 15, + SIGURG = 16, + SIGSTOP = 17, + SIGTSTP = 18, + SIGCONT = 19, + SIGCHLD = 20, + SIGTTIN = 21, + SIGTTOU = 22, + SIGIO = 23, + SIGXCPU = 24, + SIGXFSZ = 25, + SIGVTALRM = 26, + SIGPROF = 27, + SIGWINCH = 28, + SIGINFO = 29, + SIGUSR1 = 30, + SIGUSR2 = 31 +} + +/** Signals numbers. This is platform dependent. + */ +export const Signal = platform.os === "mac" ? MacOSSignal : LinuxSignal; diff --git a/js/process_test.ts b/js/process_test.ts index c89e1cae75..8d266617aa 100644 --- a/js/process_test.ts +++ b/js/process_test.ts @@ -1,6 +1,6 @@ // Copyright 2018-2019 the Deno authors. All rights reserved. MIT license. import { test, testPerm, assert, assertEquals } from "./test_util.ts"; -const { run, DenoError, ErrorKind } = Deno; +const { kill, run, DenoError, ErrorKind } = Deno; test(function runPermissions(): void { let caughtError = false; @@ -223,3 +223,69 @@ testPerm({ run: true }, async function runEnv(): Promise { assertEquals(s, "01234567"); p.close(); }); + +testPerm({ run: true }, async function runClose(): Promise { + const p = run({ + args: [ + "python", + "-c", + "from time import sleep; import sys; sleep(10000); sys.stderr.write('error')" + ], + stderr: "piped" + }); + assert(!p.stdin); + assert(!p.stdout); + + p.close(); + + const data = new Uint8Array(10); + let r = await p.stderr.read(data); + assertEquals(r.nread, 0); + assertEquals(r.eof, true); +}); + +test(function signalNumbers(): void { + if (Deno.platform.os === "mac") { + assertEquals(Deno.Signal.SIGSTOP, 17); + } else if (Deno.platform.os === "linux") { + assertEquals(Deno.Signal.SIGSTOP, 19); + } +}); + +// Ignore signal tests on windows for now... +if (Deno.platform.os !== "win") { + testPerm({ run: true }, async function killSuccess(): Promise { + const p = run({ + args: ["python", "-c", "from time import sleep; sleep(10000)"] + }); + + assertEquals(Deno.Signal.SIGINT, 2); + kill(p.pid, Deno.Signal.SIGINT); + const status = await p.status(); + + assertEquals(status.success, false); + assertEquals(status.code, undefined); + assertEquals(status.signal, Deno.Signal.SIGINT); + }); + + testPerm({ run: true }, async function killFailed(): Promise { + const p = run({ + args: ["python", "-c", "from time import sleep; sleep(10000)"] + }); + assert(!p.stdin); + assert(!p.stdout); + + let err; + try { + kill(p.pid, 12345); + } catch (e) { + err = e; + } + + assert(!!err); + assertEquals(err.kind, Deno.ErrorKind.InvalidInput); + assertEquals(err.name, "InvalidInput"); + + p.close(); + }); +}