diff --git a/src/compiletest/procsrv.rs b/src/compiletest/procsrv.rs
index 2a4cd08c124..a059f97daab 100644
--- a/src/compiletest/procsrv.rs
+++ b/src/compiletest/procsrv.rs
@@ -10,8 +10,6 @@
 
 use core::prelude::*;
 
-use core::libc::c_int;
-use core::run::spawn_process;
 use core::run;
 
 #[cfg(target_os = "win32")]
@@ -38,86 +36,35 @@ fn target_env(lib_path: &str, prog: &str) -> ~[(~str,~str)] {
 #[cfg(target_os = "macos")]
 #[cfg(target_os = "freebsd")]
 fn target_env(_lib_path: &str, _prog: &str) -> ~[(~str,~str)] {
-    ~[]
+    os::env()
 }
 
 pub struct Result {status: int, out: ~str, err: ~str}
 
-// FIXME (#2659): This code is duplicated in core::run::program_output
 pub fn run(lib_path: &str,
            prog: &str,
            args: &[~str],
            env: ~[(~str, ~str)],
            input: Option<~str>) -> Result {
-    let pipe_in = os::pipe();
-    let pipe_out = os::pipe();
-    let pipe_err = os::pipe();
-    let pid = spawn_process(prog, args,
-                            &Some(env + target_env(lib_path, prog)),
-                            &None, pipe_in.in, pipe_out.out, pipe_err.out);
 
-    os::close(pipe_in.in);
-    os::close(pipe_out.out);
-    os::close(pipe_err.out);
-    if pid == -1i32 {
-        os::close(pipe_in.out);
-        os::close(pipe_out.in);
-        os::close(pipe_err.in);
-        fail!();
+    let env = env + target_env(lib_path, prog);
+    let mut proc = run::Process::new(prog, args, run::ProcessOptions {
+        env: Some(env.slice(0, env.len())),
+        dir: None,
+        in_fd: None,
+        out_fd: None,
+        err_fd: None
+    });
+
+    for input.each |input| {
+        proc.input().write_str(*input);
     }
+    let output = proc.finish_with_output();
 
-
-    writeclose(pipe_in.out, input);
-    let p = comm::PortSet::new();
-    let ch = p.chan();
-    do task::spawn_sched(task::SingleThreaded) || {
-        let errput = readclose(pipe_err.in);
-        ch.send((2, errput));
-    }
-    let ch = p.chan();
-    do task::spawn_sched(task::SingleThreaded) || {
-        let output = readclose(pipe_out.in);
-        ch.send((1, output));
-    }
-    let status = run::waitpid(pid);
-    let mut errs = ~"";
-    let mut outs = ~"";
-    let mut count = 2;
-    while count > 0 {
-        match p.recv() {
-          (1, s) => {
-            outs = s;
-          }
-          (2, s) => {
-            errs = s;
-          }
-          _ => { fail!() }
-        };
-        count -= 1;
-    };
-    return Result {status: status, out: outs, err: errs};
-}
-
-fn writeclose(fd: c_int, s: Option<~str>) {
-    if s.is_some() {
-        let writer = io::fd_writer(fd, false);
-        writer.write_str(s.get());
-    }
-
-    os::close(fd);
-}
-
-fn readclose(fd: c_int) -> ~str {
-    unsafe {
-        // Copied from run::program_output
-        let file = os::fdopen(fd);
-        let reader = io::FILE_reader(file, false);
-        let mut buf = ~"";
-        while !reader.eof() {
-            let bytes = reader.read_bytes(4096u);
-            str::push_str(&mut buf, str::from_bytes(bytes));
-        }
-        os::fclose(file);
-        return buf;
+    Result {
+        status: output.status,
+        out: str::from_bytes(output.output),
+        err: str::from_bytes(output.error)
     }
 }
+
diff --git a/src/libextra/workcache.rs b/src/libextra/workcache.rs
index 3c10b3d2ca7..9df9ffd9465 100644
--- a/src/libextra/workcache.rs
+++ b/src/libextra/workcache.rs
@@ -415,7 +415,7 @@ fn test() {
         prep.declare_input("file", pth.to_str(), digest_file(&pth));
         do prep.exec |_exe| {
             let out = Path("foo.o");
-            run::run_program("gcc", [~"foo.c", ~"-o", out.to_str()]);
+            run::process_status("gcc", [~"foo.c", ~"-o", out.to_str()]);
             out.to_str()
         }
     };
diff --git a/src/libfuzzer/fuzzer.rc b/src/libfuzzer/fuzzer.rc
index 3be55937933..90a93a9e57c 100644
--- a/src/libfuzzer/fuzzer.rc
+++ b/src/libfuzzer/fuzzer.rc
@@ -412,20 +412,20 @@ pub fn check_whole_compiler(code: &str,
 pub fn removeIfExists(filename: &Path) {
     // So sketchy!
     assert!(!contains(filename.to_str(), " "));
-    run::program_output("bash", [~"-c", ~"rm " + filename.to_str()]);
+    run::process_status("bash", [~"-c", ~"rm " + filename.to_str()]);
 }
 
 pub fn removeDirIfExists(filename: &Path) {
     // So sketchy!
     assert!(!contains(filename.to_str(), " "));
-    run::program_output("bash", [~"-c", ~"rm -r " + filename.to_str()]);
+    run::process_status("bash", [~"-c", ~"rm -r " + filename.to_str()]);
 }
 
 pub fn check_running(exe_filename: &Path) -> happiness {
-    let p = run::program_output(
+    let p = run::process_output(
         "/Users/jruderman/scripts/timed_run_rust_program.py",
         [exe_filename.to_str()]);
-    let comb = p.out + ~"\n" + p.err;
+    let comb = str::from_bytes(p.output) + ~"\n" + str::from_bytes(p.error);
     if str::len(comb) > 1u {
         error!("comb comb comb: %?", comb);
     }
@@ -461,33 +461,35 @@ pub fn check_running(exe_filename: &Path) -> happiness {
 }
 
 pub fn check_compiling(filename: &Path) -> happiness {
-    let p = run::program_output(
-        "/Users/jruderman/code/rust/build/x86_64-apple-darwin/\
-         stage1/bin/rustc",
+    let p = run::process_output(
+        "/Users/jruderman/code/rust/build/x86_64-apple-darwin/stage1/bin/rustc",
         [filename.to_str()]);
 
+    let out = str::from_bytes(p.output);
+    let err = str::from_bytes(p.error);
+
     //error!("Status: %d", p.status);
     if p.status == 0 {
         passed
-    } else if p.err != ~"" {
-        if contains(p.err, "error:") {
+    } else if !err.is_empty() {
+        if err.contains("error:") {
             cleanly_rejected(~"rejected with span_error")
         } else {
-            error!("Stderr: %?", p.err);
+            error!("Stderr: %?", err);
             failed(~"Unfamiliar error message")
         }
-    } else if contains(p.out, "Assertion") && contains(p.out, "failed") {
-        error!("Stdout: %?", p.out);
+    } else if out.contains("Assertion") && out.contains("failed") {
+        error!("Stdout: %?", out);
         failed(~"Looks like an llvm assertion failure")
-    } else if contains(p.out, "internal compiler error unimplemented") {
+    } else if out.contains("internal compiler error unimplemented") {
         known_bug(~"Something unimplemented")
-    } else if contains(p.out, "internal compiler error") {
-        error!("Stdout: %?", p.out);
+    } else if out.contains("internal compiler error") {
+        error!("Stdout: %?", out);
         failed(~"internal compiler error")
 
     } else {
         error!("%?", p.status);
-        error!("!Stdout: %?", p.out);
+        error!("!Stdout: %?", out);
         failed(~"What happened?")
     }
 }
@@ -608,9 +610,7 @@ pub fn check_roundtrip_convergence(code: @~str, maxIters: uint) {
         error!("Did not converge after %u iterations!", i);
         write_file(&Path("round-trip-a.rs"), *oldv);
         write_file(&Path("round-trip-b.rs"), *newv);
-        run::run_program("diff",
-                         [~"-w", ~"-u", ~"round-trip-a.rs",
-                          ~"round-trip-b.rs"]);
+        run::process_status("diff", [~"-w", ~"-u", ~"round-trip-a.rs", ~"round-trip-b.rs"]);
         fail!("Mismatch");
     }
 }
diff --git a/src/librust/rust.rc b/src/librust/rust.rc
index 6449958e09c..36246b7a9a1 100644
--- a/src/librust/rust.rc
+++ b/src/librust/rust.rc
@@ -163,7 +163,7 @@ fn cmd_test(args: &[~str]) -> ValidUsage {
             let test_exec = Path(filename).filestem().unwrap() + "test~";
             invoke("rustc", &[~"--test", filename.to_owned(),
                               ~"-o", test_exec.to_owned()], rustc::main);
-            let exit_code = run::run_program(~"./" + test_exec, []);
+            let exit_code = run::process_status(~"./" + test_exec, []);
             Valid(exit_code)
         }
         _ => Invalid
@@ -176,7 +176,7 @@ fn cmd_run(args: &[~str]) -> ValidUsage {
             let exec = Path(filename).filestem().unwrap() + "~";
             invoke("rustc", &[filename.to_owned(), ~"-o", exec.to_owned()],
                    rustc::main);
-            let exit_code = run::run_program(~"./"+exec, prog_args);
+            let exit_code = run::process_status(~"./"+exec, prog_args);
             Valid(exit_code)
         }
         _ => Invalid
diff --git a/src/librustc/back/link.rs b/src/librustc/back/link.rs
index e230d839879..0dddbf395e0 100644
--- a/src/librustc/back/link.rs
+++ b/src/librustc/back/link.rs
@@ -403,14 +403,14 @@ pub mod write {
         cc_args.push(object.to_str());
         cc_args.push(assembly.to_str());
 
-        let prog = run::program_output(cc_prog, cc_args);
+        let prog = run::process_output(cc_prog, cc_args);
 
         if prog.status != 0 {
             sess.err(fmt!("building with `%s` failed with code %d",
                         cc_prog, prog.status));
             sess.note(fmt!("%s arguments: %s",
                         cc_prog, str::connect(cc_args, " ")));
-            sess.note(prog.err + prog.out);
+            sess.note(str::from_bytes(prog.error + prog.output));
             sess.abort_if_errors();
         }
     }
@@ -817,19 +817,19 @@ pub fn link_binary(sess: Session,
     let cc_args = link_args(sess, obj_filename, out_filename, lm);
     debug!("%s link args: %s", cc_prog, str::connect(cc_args, " "));
     // We run 'cc' here
-    let prog = run::program_output(cc_prog, cc_args);
+    let prog = run::process_output(cc_prog, cc_args);
     if 0 != prog.status {
         sess.err(fmt!("linking with `%s` failed with code %d",
                       cc_prog, prog.status));
         sess.note(fmt!("%s arguments: %s",
                        cc_prog, str::connect(cc_args, " ")));
-        sess.note(prog.err + prog.out);
+        sess.note(str::from_bytes(prog.error + prog.output));
         sess.abort_if_errors();
     }
 
     // Clean up on Darwin
     if sess.targ_cfg.os == session::os_macos {
-        run::run_program("dsymutil", [output.to_str()]);
+        run::process_status("dsymutil", [output.to_str()]);
     }
 
     // Remove the temporary object file if we aren't saving temps
diff --git a/src/librustdoc/config.rs b/src/librustdoc/config.rs
index 7366aca4c4f..603ce0de67d 100644
--- a/src/librustdoc/config.rs
+++ b/src/librustdoc/config.rs
@@ -12,7 +12,7 @@ use core::prelude::*;
 
 use core::cell::Cell;
 use core::run;
-use core::run::ProgramOutput;
+use core::run::ProcessOutput;
 use core::result::Result;
 use extra::getopts;
 
@@ -89,27 +89,27 @@ pub fn default_config(input_crate: &Path) -> Config {
     }
 }
 
-type Process = ~fn((&str), (&[~str])) -> ProgramOutput;
+type Process = ~fn((&str), (&[~str])) -> ProcessOutput;
 
-pub fn mock_program_output(_prog: &str, _args: &[~str]) -> ProgramOutput {
-    ProgramOutput {
+pub fn mock_process_output(_prog: &str, _args: &[~str]) -> ProcessOutput {
+    ProcessOutput {
         status: 0,
-        out: ~"",
-        err: ~""
+        output: ~[],
+        error: ~[]
     }
 }
 
-pub fn program_output(prog: &str, args: &[~str]) -> ProgramOutput {
-    run::program_output(prog, args)
+pub fn process_output(prog: &str, args: &[~str]) -> ProcessOutput {
+    run::process_output(prog, args)
 }
 
 pub fn parse_config(args: &[~str]) -> Result<Config, ~str> {
-    parse_config_(args, program_output)
+    parse_config_(args, process_output)
 }
 
 pub fn parse_config_(
     args: &[~str],
-    program_output: Process
+    process_output: Process
 ) -> Result<Config, ~str> {
     let args = args.tail();
     let opts = vec::unzip(opts()).first();
@@ -117,7 +117,7 @@ pub fn parse_config_(
         Ok(matches) => {
             if matches.free.len() == 1 {
                 let input_crate = Path(*matches.free.head());
-                config_from_opts(&input_crate, &matches, program_output)
+                config_from_opts(&input_crate, &matches, process_output)
             } else if matches.free.is_empty() {
                 Err(~"no crates specified")
             } else {
@@ -133,7 +133,7 @@ pub fn parse_config_(
 fn config_from_opts(
     input_crate: &Path,
     matches: &getopts::Matches,
-    program_output: Process
+    process_output: Process
 ) -> Result<Config, ~str> {
 
     let config = default_config(input_crate);
@@ -175,11 +175,11 @@ fn config_from_opts(
             }
         }
     };
-    let program_output = Cell(program_output);
+    let process_output = Cell(process_output);
     let result = do result::chain(result) |config| {
         let pandoc_cmd = getopts::opt_maybe_str(matches, opt_pandoc_cmd());
         let pandoc_cmd = maybe_find_pandoc(
-            &config, pandoc_cmd, program_output.take());
+            &config, pandoc_cmd, process_output.take());
         do result::chain(pandoc_cmd) |pandoc_cmd| {
             result::Ok(Config {
                 pandoc_cmd: pandoc_cmd,
@@ -209,7 +209,7 @@ fn parse_output_style(output_style: &str) -> Result<OutputStyle, ~str> {
 pub fn maybe_find_pandoc(
     config: &Config,
     maybe_pandoc_cmd: Option<~str>,
-    program_output: Process
+    process_output: Process
 ) -> Result<Option<~str>, ~str> {
     if config.output_format != PandocHtml {
         return result::Ok(maybe_pandoc_cmd);
@@ -228,7 +228,7 @@ pub fn maybe_find_pandoc(
     };
 
     let pandoc = do vec::find(possible_pandocs) |pandoc| {
-        let output = program_output(*pandoc, [~"--version"]);
+        let output = process_output(*pandoc, [~"--version"]);
         debug!("testing pandoc cmd %s: %?", *pandoc, output);
         output.status == 0
     };
@@ -244,10 +244,10 @@ pub fn maybe_find_pandoc(
 mod test {
     use core::prelude::*;
     use config::*;
-    use core::run::ProgramOutput;
+    use core::run::ProcessOutput;
 
     fn parse_config(args: &[~str]) -> Result<Config, ~str> {
-        parse_config_(args, mock_program_output)
+        parse_config_(args, mock_process_output)
     }
 
     #[test]
@@ -256,10 +256,10 @@ mod test {
             output_format: PandocHtml,
             .. default_config(&Path("test"))
         };
-        let mock_program_output: ~fn(&str, &[~str]) -> ProgramOutput = |_, _| {
-            ProgramOutput { status: 0, out: ~"pandoc 1.8.2.1", err: ~"" }
+        let mock_process_output: ~fn(&str, &[~str]) -> ProcessOutput = |_, _| {
+            ProcessOutput { status: 0, output: "pandoc 1.8.2.1".to_bytes(), error: ~[] }
         };
-        let result = maybe_find_pandoc(&config, None, mock_program_output);
+        let result = maybe_find_pandoc(&config, None, mock_process_output);
         assert!(result == result::Ok(Some(~"pandoc")));
     }
 
@@ -269,10 +269,10 @@ mod test {
             output_format: PandocHtml,
             .. default_config(&Path("test"))
         };
-        let mock_program_output: ~fn(&str, &[~str]) -> ProgramOutput = |_, _| {
-            ProgramOutput { status: 1, out: ~"", err: ~"" }
+        let mock_process_output: ~fn(&str, &[~str]) -> ProcessOutput = |_, _| {
+            ProcessOutput { status: 1, output: ~[], error: ~[] }
         };
-        let result = maybe_find_pandoc(&config, None, mock_program_output);
+        let result = maybe_find_pandoc(&config, None, mock_process_output);
         assert!(result == result::Err(~"couldn't find pandoc"));
     }
 
diff --git a/src/librustdoc/markdown_writer.rs b/src/librustdoc/markdown_writer.rs
index 3e90c1a53f4..8149dc61fb4 100644
--- a/src/librustdoc/markdown_writer.rs
+++ b/src/librustdoc/markdown_writer.rs
@@ -14,7 +14,6 @@ use config;
 use doc::ItemUtils;
 use doc;
 
-use core::libc;
 use core::run;
 use core::comm::*;
 use extra::future;
@@ -105,60 +104,20 @@ fn pandoc_writer(
         debug!("pandoc cmd: %s", pandoc_cmd);
         debug!("pandoc args: %s", str::connect(pandoc_args, " "));
 
-        let pipe_in = os::pipe();
-        let pipe_out = os::pipe();
-        let pipe_err = os::pipe();
-        let pid = run::spawn_process(
-            pandoc_cmd, pandoc_args, &None, &None,
-            pipe_in.in, pipe_out.out, pipe_err.out);
+        let mut proc = run::Process::new(pandoc_cmd, pandoc_args, run::ProcessOptions::new());
 
-        let writer = io::fd_writer(pipe_in.out, false);
-        writer.write_str(markdown);
+        proc.input().write_str(markdown);
+        let output = proc.finish_with_output();
 
-        os::close(pipe_in.in);
-        os::close(pipe_out.out);
-        os::close(pipe_err.out);
-        os::close(pipe_in.out);
-
-        let (stdout_po, stdout_ch) = comm::stream();
-        do task::spawn_sched(task::SingleThreaded) || {
-            stdout_ch.send(readclose(pipe_out.in));
-        }
-
-        let (stderr_po, stderr_ch) = comm::stream();
-        do task::spawn_sched(task::SingleThreaded) || {
-            stderr_ch.send(readclose(pipe_err.in));
-        }
-        let stdout = stdout_po.recv();
-        let stderr = stderr_po.recv();
-
-        let status = run::waitpid(pid);
-        debug!("pandoc result: %i", status);
-        if status != 0 {
-            error!("pandoc-out: %s", stdout);
-            error!("pandoc-err: %s", stderr);
+        debug!("pandoc result: %i", output.status);
+        if output.status != 0 {
+            error!("pandoc-out: %s", str::from_bytes(output.output));
+            error!("pandoc-err: %s", str::from_bytes(output.error));
             fail!("pandoc failed");
         }
     }
 }
 
-fn readclose(fd: libc::c_int) -> ~str {
-    // Copied from run::program_output
-    unsafe {
-        let file = os::fdopen(fd);
-        let reader = io::FILE_reader(file, false);
-        let buf = io::with_bytes_writer(|writer| {
-            let mut bytes = [0, ..4096];
-            while !reader.eof() {
-                let nread = reader.read(bytes, bytes.len());
-                writer.write(bytes.slice(0, nread).to_owned());
-            }
-        });
-        os::fclose(file);
-        str::from_bytes(buf)
-    }
-}
-
 fn generic_writer(process: ~fn(markdown: ~str)) -> Writer {
     let (po, ch) = stream::<WriteInstr>();
     do task::spawn || {
diff --git a/src/librustpkg/rustpkg.rc b/src/librustpkg/rustpkg.rc
index 79e978c01d1..a419bffece7 100644
--- a/src/librustpkg/rustpkg.rc
+++ b/src/librustpkg/rustpkg.rc
@@ -131,17 +131,17 @@ impl<'self> PkgScript<'self> {
                                                &exe, @copy os::args()[0],
                                                driver::cu_everything);
                 debug!("Running program: %s %s %s", exe.to_str(), root.to_str(), what);
-                let status = run::run_program(exe.to_str(), [root.to_str(), what]);
+                let status = run::process_status(exe.to_str(), [root.to_str(), what]);
                 if status != 0 {
                     return (~[], status);
                 }
                 else {
                     debug!("Running program (configs): %s %s %s",
                            exe.to_str(), root.to_str(), "configs");
-                    let output = run::program_output(exe.to_str(), [root.to_str(), ~"configs"]);
+                    let output = run::process_output(exe.to_str(), [root.to_str(), ~"configs"]);
                     // Run the configs() function to get the configs
                     let mut cfgs = ~[];
-                    for str::each_word(output.out) |w| {
+                    for str::each_word(str::from_bytes(output.output)) |w| {
                         cfgs.push(w.to_owned());
                     }
                     (cfgs, output.status)
@@ -551,7 +551,7 @@ impl PkgSrc {
         let url = fmt!("https://%s", self.id.remote_path.to_str());
         util::note(fmt!("git clone %s %s", url, local.to_str()));
 
-        if run::program_output("git", [~"clone", copy url, local.to_str()]).status != 0 {
+        if run::process_output("git", [~"clone", copy url, local.to_str()]).status != 0 {
             util::note(fmt!("fetching %s failed: can't clone repository", url));
             return false;
         }
diff --git a/src/librustpkg/testsuite/pass/src/fancy-lib/pkg.rs b/src/librustpkg/testsuite/pass/src/fancy-lib/pkg.rs
index eeaa0f68ed5..2d3a75d9197 100644
--- a/src/librustpkg/testsuite/pass/src/fancy-lib/pkg.rs
+++ b/src/librustpkg/testsuite/pass/src/fancy-lib/pkg.rs
@@ -23,6 +23,6 @@ pub fn main() {
     file.write_str("pub fn wheeeee() { for [1, 2, 3].each() |_| { assert!(true); } }");
 
     // now compile the crate itself
-    run::run_program("rustc", ~[~"src/fancy-lib/fancy-lib.rs", ~"--lib",
-                                ~"-o", out_path.push(~"fancy_lib").to_str()]);
+    run::process_status("rustc", [~"src/fancy-lib/fancy-lib.rs", ~"--lib", ~"-o",
+                        out_path.push(~"fancy_lib").to_str()]);
 }
\ No newline at end of file
diff --git a/src/libstd/os.rs b/src/libstd/os.rs
index 44acdd4d617..49e3c17be1b 100644
--- a/src/libstd/os.rs
+++ b/src/libstd/os.rs
@@ -1671,7 +1671,7 @@ mod tests {
             fail!("%s doesn't exist", in.to_str());
           }
           assert!((rs));
-          let rslt = run::run_program("diff", [in.to_str(), out.to_str()]);
+          let rslt = run::process_status("diff", [in.to_str(), out.to_str()]);
           assert_eq!(rslt, 0);
           assert_eq!(out.get_mode(), in_mode);
           assert!((remove_file(&in)));
diff --git a/src/libstd/run.rs b/src/libstd/run.rs
index 02757ab4899..1956f4baba6 100644
--- a/src/libstd/run.rs
+++ b/src/libstd/run.rs
@@ -8,7 +8,8 @@
 // option. This file may not be copied, modified, or distributed
 // except according to those terms.
 
-//! Process spawning
+//! Process spawning.
+
 use cast;
 use io;
 use libc;
@@ -22,80 +23,367 @@ use str;
 use task;
 use vec;
 
-/// A value representing a child process
-pub struct Program {
+/**
+ * A value representing a child process.
+ *
+ * The lifetime of this value is linked to the lifetime of the actual
+ * process - the Process destructor calls self.finish() which waits
+ * for the process to terminate.
+ */
+pub struct Process {
+
+    /// The unique id of the process (this should never be negative).
     priv pid: pid_t,
+
+    /**
+     * A handle to the process - on unix this will always be NULL, but on
+     * windows it will be a HANDLE to the process, which will prevent the
+     * pid being re-used until the handle is closed.
+     */
     priv handle: *(),
-    priv in_fd: c_int,
-    priv out_file: *libc::FILE,
-    priv err_file: *libc::FILE,
-    priv finished: bool,
+
+    /// Some(fd), or None when stdin is being redirected from a fd not created by Process::new.
+    priv input: Option<c_int>,
+
+    /// Some(fd), or None when stdout is being redirected to a fd not created by Process::new.
+    priv output: Option<*libc::FILE>,
+
+    /// Some(fd), or None when stderr is being redirected to a fd not created by Process::new.
+    priv error: Option<*libc::FILE>,
+
+    /// None until finish() is called.
+    priv exit_code: Option<int>,
 }
 
-impl Drop for Program {
-    fn finalize(&self) {
-        // FIXME #4943: transmute is bad.
-        let mut_self: &mut Program = unsafe { cast::transmute(self) };
+/// Options that can be given when starting a Process.
+pub struct ProcessOptions<'self> {
 
-        mut_self.finish();
-        mut_self.close_outputs();
-        free_handle(self.handle);
+    /**
+     * If this is None then the new process will have the same initial
+     * environment as the parent process.
+     *
+     * If this is Some(vec-of-names-and-values) then the new process will
+     * have an environment containing the given named values only.
+     */
+    env: Option<&'self [(~str, ~str)]>,
+
+    /**
+     * If this is None then the new process will use the same initial working
+     * directory as the parent process.
+     *
+     * If this is Some(path) then the new process will use the given path
+     * for its initial working directory.
+     */
+    dir: Option<&'self Path>,
+
+    /**
+     * If this is None then a new pipe will be created for the new process's
+     * input and Process.input() will provide a Writer to write to this pipe.
+     *
+     * If this is Some(file-descriptor) then the new process will read its input
+     * from the given file descriptor, Process.input_redirected() will return
+     * true, and Process.input() will fail.
+     */
+    in_fd: Option<c_int>,
+
+    /**
+     * If this is None then a new pipe will be created for the new progam's
+     * output and Process.output() will provide a Reader to read from this pipe.
+     *
+     * If this is Some(file-descriptor) then the new process will write its output
+     * to the given file descriptor, Process.output_redirected() will return
+     * true, and Process.output() will fail.
+     */
+    out_fd: Option<c_int>,
+
+    /**
+     * If this is None then a new pipe will be created for the new progam's
+     * error stream and Process.error() will provide a Reader to read from this pipe.
+     *
+     * If this is Some(file-descriptor) then the new process will write its error output
+     * to the given file descriptor, Process.error_redirected() will return true, and
+     * and Process.error() will fail.
+     */
+    err_fd: Option<c_int>,
+}
+
+impl <'self> ProcessOptions<'self> {
+    /// Return a ProcessOptions that has None in every field.
+    pub fn new<'a>() -> ProcessOptions<'a> {
+        ProcessOptions {
+            env: None,
+            dir: None,
+            in_fd: None,
+            out_fd: None,
+            err_fd: None,
+        }
     }
 }
 
-pub impl Program {
+/// The output of a finished process.
+pub struct ProcessOutput {
 
-    /// Returns the process id of the program
-    fn get_id(&mut self) -> pid_t { self.pid }
+    /// The status (exit code) of the process.
+    status: int,
 
-    /// Returns an io::Writer that can be used to write to stdin
-    fn input(&mut self) -> @io::Writer {
-        io::fd_writer(self.in_fd, false)
-    }
+    /// The data that the process wrote to stdout.
+    output: ~[u8],
 
-    /// Returns an io::Reader that can be used to read from stdout
-    fn output(&mut self) -> @io::Reader {
-        io::FILE_reader(self.out_file, false)
-    }
+    /// The data that the process wrote to stderr.
+    error: ~[u8],
+}
 
-    /// Returns an io::Reader that can be used to read from stderr
-    fn err(&mut self) -> @io::Reader {
-        io::FILE_reader(self.err_file, false)
-    }
+pub impl Process {
 
-    /// Closes the handle to the child processes standard input
-    fn close_input(&mut self) {
-        let invalid_fd = -1i32;
-        if self.in_fd != invalid_fd {
-            unsafe {
-                libc::close(self.in_fd);
-            }
-            self.in_fd = invalid_fd;
+    /**
+     * Spawns a new Process.
+     *
+     * # Arguments
+     *
+     * * prog - The path to an executable.
+     * * args - Vector of arguments to pass to the child process.
+     * * options - Options to configure the environment of the process,
+     *             the working directory and the standard IO streams.
+     */
+    pub fn new(prog: &str, args: &[~str], options: ProcessOptions) -> Process {
+
+        let (in_pipe, in_fd) = match options.in_fd {
+            None => {
+                let pipe = os::pipe();
+                (Some(pipe), pipe.in)
+            },
+            Some(fd) => (None, fd)
+        };
+        let (out_pipe, out_fd) = match options.out_fd {
+            None => {
+                let pipe = os::pipe();
+                (Some(pipe), pipe.out)
+            },
+            Some(fd) => (None, fd)
+        };
+        let (err_pipe, err_fd) = match options.err_fd {
+            None => {
+                let pipe = os::pipe();
+                (Some(pipe), pipe.out)
+            },
+            Some(fd) => (None, fd)
+        };
+
+        let res = spawn_process_os(prog, args, options.env, options.dir,
+                                   in_fd, out_fd, err_fd);
+
+        unsafe {
+            for in_pipe.each  |pipe| { libc::close(pipe.in); }
+            for out_pipe.each |pipe| { libc::close(pipe.out); }
+            for err_pipe.each |pipe| { libc::close(pipe.out); }
+        }
+
+        Process {
+            pid: res.pid,
+            handle: res.handle,
+            input: in_pipe.map(|pipe| pipe.out),
+            output: out_pipe.map(|pipe| os::fdopen(pipe.in)),
+            error: err_pipe.map(|pipe| os::fdopen(pipe.in)),
+            exit_code: None,
         }
     }
 
-    priv fn close_outputs(&mut self) {
-        unsafe {
-            fclose_and_null(&mut self.out_file);
-            fclose_and_null(&mut self.err_file);
+    /// Returns the unique id of the process
+    fn get_id(&self) -> pid_t { self.pid }
+
+    priv fn unwrap_input(&mut self) -> c_int {
+        match self.input {
+            Some(fd) => fd,
+            None => fail!("This Process's stdin was redirected to an \
+                           existing file descriptor.")
+        }
+    }
+
+    priv fn unwrap_output(&mut self) -> *libc::FILE {
+        match self.output {
+            Some(file) => file,
+            None => fail!("This Process's stdout was redirected to an \
+                           existing file descriptor.")
+        }
+    }
+
+    priv fn unwrap_error(&mut self) -> *libc::FILE {
+        match self.error {
+            Some(file) => file,
+            None => fail!("This Process's stderr was redirected to an \
+                           existing file descriptor.")
         }
     }
 
     /**
-     * Waits for the child process to terminate. Closes the handle
-     * to stdin if necessary.
+     * Returns whether this process is reading its stdin from an existing file
+     * descriptor rather than a pipe that was created specifically for this
+     * process.
+     *
+     * If this method returns true then self.input() will fail.
+     */
+    fn input_redirected(&self) -> bool {
+        self.input.is_none()
+    }
+
+    /**
+     * Returns whether this process is writing its stdout to an existing file
+     * descriptor rather than a pipe that was created specifically for this
+     * process.
+     *
+     * If this method returns true then self.output() will fail.
+     */
+    fn output_redirected(&self) -> bool {
+        self.output.is_none()
+    }
+
+    /**
+     * Returns whether this process is writing its stderr to an existing file
+     * descriptor rather than a pipe that was created specifically for this
+     * process.
+     *
+     * If this method returns true then self.error() will fail.
+     */
+    fn error_redirected(&self) -> bool {
+        self.error.is_none()
+    }
+
+    /**
+     * Returns an io::Writer that can be used to write to this Process's stdin.
+     *
+     * Fails if this Process's stdin was redirected to an existing file descriptor.
+     */
+    fn input(&mut self) -> @io::Writer {
+        // FIXME: the Writer can still be used after self is destroyed: #2625
+       io::fd_writer(self.unwrap_input(), false)
+    }
+
+    /**
+     * Returns an io::Reader that can be used to read from this Process's stdout.
+     *
+     * Fails if this Process's stdout was redirected to an existing file descriptor.
+     */
+    fn output(&mut self) -> @io::Reader {
+        // FIXME: the Reader can still be used after self is destroyed: #2625
+        io::FILE_reader(self.unwrap_output(), false)
+    }
+
+    /**
+     * Returns an io::Reader that can be used to read from this Process's stderr.
+     *
+     * Fails if this Process's stderr was redirected to an existing file descriptor.
+     */
+    fn error(&mut self) -> @io::Reader {
+        // FIXME: the Reader can still be used after self is destroyed: #2625
+        io::FILE_reader(self.unwrap_error(), false)
+    }
+
+    /**
+     * Closes the handle to the child process's stdin.
+     *
+     * If this process is reading its stdin from an existing file descriptor, then this
+     * method does nothing.
+     */
+    fn close_input(&mut self) {
+        match self.input {
+            Some(-1) | None => (),
+            Some(fd) => {
+                unsafe {
+                    libc::close(fd);
+                }
+                self.input = Some(-1);
+            }
+        }
+    }
+
+    priv fn close_outputs(&mut self) {
+        fclose_and_null(&mut self.output);
+        fclose_and_null(&mut self.error);
+
+        fn fclose_and_null(f_opt: &mut Option<*libc::FILE>) {
+            match *f_opt {
+                Some(f) if !f.is_null() => {
+                    unsafe {
+                        libc::fclose(f);
+                        *f_opt = Some(0 as *libc::FILE);
+                    }
+                },
+                _ => ()
+            }
+        }
+    }
+
+    /**
+     * Closes the handle to stdin, waits for the child process to terminate,
+     * and returns the exit code.
+     *
+     * If the child has already been finished then the exit code is returned.
      */
     fn finish(&mut self) -> int {
-        if self.finished { return 0; }
-        self.finished = true;
+        for self.exit_code.each |&code| {
+            return code;
+        }
         self.close_input();
-        return waitpid(self.pid);
+        let code = waitpid(self.pid);
+        self.exit_code = Some(code);
+        return code;
+    }
+
+    /**
+     * Closes the handle to stdin, waits for the child process to terminate, and reads
+     * and returns all remaining output of stdout and stderr, along with the exit code.
+     *
+     * If the child has already been finished then the exit code and any remaining
+     * unread output of stdout and stderr will be returned.
+     *
+     * This method will fail if the child process's stdout or stderr streams were
+     * redirected to existing file descriptors.
+     */
+    fn finish_with_output(&mut self) -> ProcessOutput {
+
+        let output_file = self.unwrap_output();
+        let error_file = self.unwrap_error();
+
+        // Spawn two entire schedulers to read both stdout and sterr
+        // in parallel so we don't deadlock while blocking on one
+        // or the other. FIXME (#2625): Surely there's a much more
+        // clever way to do this.
+        let (p, ch) = stream();
+        let ch = SharedChan::new(ch);
+        let ch_clone = ch.clone();
+        do task::spawn_sched(task::SingleThreaded) {
+            let errput = io::FILE_reader(error_file, false);
+            ch.send((2, errput.read_whole_stream()));
+        }
+        do task::spawn_sched(task::SingleThreaded) {
+            let output = io::FILE_reader(output_file, false);
+            ch_clone.send((1, output.read_whole_stream()));
+        }
+
+        let status = self.finish();
+
+        let (errs, outs) = match (p.recv(), p.recv()) {
+            ((1, o), (2, e)) => (e, o),
+            ((2, e), (1, o)) => (e, o),
+            ((x, _), (y, _)) => {
+                fail!("unexpected file numbers: %u, %u", x, y);
+            }
+        };
+
+        return ProcessOutput {status: status,
+                              output: outs,
+                              error: errs};
     }
 
     priv fn destroy_internal(&mut self, force: bool) {
-        killpid(self.pid, force);
-        self.finish();
-        self.close_outputs();
+
+        // if the process has finished, and therefore had waitpid called,
+        // and we kill it, then on unix we might ending up killing a
+        // newer process that happens to have the same (re-used) id
+        if self.exit_code.is_none() {
+            killpid(self.pid, force);
+            self.finish();
+        }
 
         #[cfg(windows)]
         fn killpid(pid: pid_t, _force: bool) {
@@ -120,7 +408,7 @@ pub impl Program {
     }
 
     /**
-     * Terminate the program, giving it a chance to clean itself up if
+     * Terminates the process, giving it a chance to clean itself up if
      * this is supported by the operating system.
      *
      * On Posix OSs SIGTERM will be sent to the process. On Win32
@@ -129,7 +417,7 @@ pub impl Program {
     fn destroy(&mut self) { self.destroy_internal(false); }
 
     /**
-     * Terminate the program as soon as possible without giving it a
+     * Terminates the process as soon as possible without giving it a
      * chance to clean itself up.
      *
      * On Posix OSs SIGKILL will be sent to the process. On Win32
@@ -138,47 +426,27 @@ pub impl Program {
     fn force_destroy(&mut self) { self.destroy_internal(true); }
 }
 
+impl Drop for Process {
+    fn finalize(&self) {
+        // FIXME #4943: transmute is bad.
+        let mut_self: &mut Process = unsafe { cast::transmute(self) };
 
-/**
- * Run a program, providing stdin, stdout and stderr handles
- *
- * # Arguments
- *
- * * prog - The path to an executable
- * * args - Vector of arguments to pass to the child process
- * * env - optional env-modification for child
- * * dir - optional dir to run child in (default current dir)
- * * in_fd - A file descriptor for the child to use as std input
- * * out_fd - A file descriptor for the child to use as std output
- * * err_fd - A file descriptor for the child to use as std error
- *
- * # Return value
- *
- * The process id of the spawned process
- */
-pub fn spawn_process(prog: &str, args: &[~str],
-                     env: &Option<~[(~str,~str)]>,
-                     dir: &Option<~str>,
-                     in_fd: c_int, out_fd: c_int, err_fd: c_int) -> pid_t {
-
-    let res = spawn_process_internal(prog, args, env, dir, in_fd, out_fd, err_fd);
-    free_handle(res.handle);
-    return res.pid;
+        mut_self.finish();
+        mut_self.close_outputs();
+        free_handle(self.handle);
+    }
 }
 
-struct RunProgramResult {
-    // the process id of the program (this should never be negative)
+struct SpawnProcessResult {
     pid: pid_t,
-    // a handle to the process - on unix this will always be NULL, but on windows it will be a
-    // HANDLE to the process, which will prevent the pid being re-used until the handle is closed.
     handle: *(),
 }
 
 #[cfg(windows)]
-fn spawn_process_internal(prog: &str, args: &[~str],
-                          env: &Option<~[(~str,~str)]>,
-                          dir: &Option<~str>,
-                          in_fd: c_int, out_fd: c_int, err_fd: c_int) -> RunProgramResult {
+fn spawn_process_os(prog: &str, args: &[~str],
+                    env: Option<&[(~str, ~str)]>,
+                    dir: Option<&Path>,
+                    in_fd: c_int, out_fd: c_int, err_fd: c_int) -> SpawnProcessResult {
 
     use libc::types::os::arch::extra::{DWORD, HANDLE, STARTUPINFO};
     use libc::consts::os::extra::{
@@ -203,7 +471,7 @@ fn spawn_process_internal(prog: &str, args: &[~str],
 
         let cur_proc = GetCurrentProcess();
 
-        let orig_std_in = get_osfhandle(if in_fd > 0 { in_fd } else { 0 }) as HANDLE;
+        let orig_std_in = get_osfhandle(in_fd) as HANDLE;
         if orig_std_in == INVALID_HANDLE_VALUE as HANDLE {
             fail!("failure in get_osfhandle: %s", os::last_os_error());
         }
@@ -212,7 +480,7 @@ fn spawn_process_internal(prog: &str, args: &[~str],
             fail!("failure in DuplicateHandle: %s", os::last_os_error());
         }
 
-        let orig_std_out = get_osfhandle(if out_fd > 0 { out_fd } else { 1 }) as HANDLE;
+        let orig_std_out = get_osfhandle(out_fd) as HANDLE;
         if orig_std_out == INVALID_HANDLE_VALUE as HANDLE {
             fail!("failure in get_osfhandle: %s", os::last_os_error());
         }
@@ -221,8 +489,8 @@ fn spawn_process_internal(prog: &str, args: &[~str],
             fail!("failure in DuplicateHandle: %s", os::last_os_error());
         }
 
-        let orig_std_err = get_osfhandle(if err_fd > 0 { err_fd } else { 2 }) as HANDLE;
-        if orig_std_err as HANDLE == INVALID_HANDLE_VALUE as HANDLE {
+        let orig_std_err = get_osfhandle(err_fd) as HANDLE;
+        if orig_std_err == INVALID_HANDLE_VALUE as HANDLE {
             fail!("failure in get_osfhandle: %s", os::last_os_error());
         }
         if DuplicateHandle(cur_proc, orig_std_err, cur_proc, &mut si.hStdError,
@@ -261,7 +529,7 @@ fn spawn_process_internal(prog: &str, args: &[~str],
         // until the calling code closes the process handle.
         CloseHandle(pi.hThread);
 
-        RunProgramResult {
+        SpawnProcessResult {
             pid: pi.dwProcessId as pid_t,
             handle: pi.hProcess as *()
         }
@@ -357,10 +625,10 @@ pub fn make_command_line(prog: &str, args: &[~str]) -> ~str {
 }
 
 #[cfg(unix)]
-fn spawn_process_internal(prog: &str, args: &[~str],
-                          env: &Option<~[(~str,~str)]>,
-                          dir: &Option<~str>,
-                          in_fd: c_int, out_fd: c_int, err_fd: c_int) -> RunProgramResult {
+fn spawn_process_os(prog: &str, args: &[~str],
+                    env: Option<&[(~str, ~str)]>,
+                    dir: Option<&Path>,
+                    in_fd: c_int, out_fd: c_int, err_fd: c_int) -> SpawnProcessResult {
 
     use libc::funcs::posix88::unistd::{fork, dup2, close, chdir, execvp};
     use libc::funcs::bsd44::getdtablesize;
@@ -381,18 +649,18 @@ fn spawn_process_internal(prog: &str, args: &[~str],
         if pid < 0 {
             fail!("failure in fork: %s", os::last_os_error());
         } else if pid > 0 {
-            return RunProgramResult {pid: pid, handle: ptr::null()};
+            return SpawnProcessResult {pid: pid, handle: ptr::null()};
         }
 
         rustrt::rust_unset_sigprocmask();
 
-        if in_fd > 0 && dup2(in_fd, 0) == -1 {
+        if dup2(in_fd, 0) == -1 {
             fail!("failure in dup2(in_fd, 0): %s", os::last_os_error());
         }
-        if out_fd > 0 && dup2(out_fd, 1) == -1 {
+        if dup2(out_fd, 1) == -1 {
             fail!("failure in dup2(out_fd, 1): %s", os::last_os_error());
         }
-        if err_fd > 0 && dup2(err_fd, 2) == -1 {
+        if dup2(err_fd, 2) == -1 {
             fail!("failure in dup3(err_fd, 2): %s", os::last_os_error());
         }
         // close all other fds
@@ -400,11 +668,9 @@ fn spawn_process_internal(prog: &str, args: &[~str],
             close(fd as c_int);
         }
 
-        for dir.each |dir| {
-            do str::as_c_str(*dir) |dirp| {
-                if chdir(dirp) == -1 {
-                    fail!("failure in chdir: %s", os::last_os_error());
-                }
+        do with_dirp(dir) |dirp| {
+            if !dirp.is_null() && chdir(dirp) == -1 {
+                fail!("failure in chdir: %s", os::last_os_error());
             }
         }
 
@@ -424,33 +690,32 @@ fn spawn_process_internal(prog: &str, args: &[~str],
 #[cfg(unix)]
 fn with_argv<T>(prog: &str, args: &[~str],
                 cb: &fn(**libc::c_char) -> T) -> T {
-    let mut argptrs = str::as_c_str(prog, |b| ~[b]);
+    let mut argptrs = ~[str::as_c_str(prog, |b| b)];
     let mut tmps = ~[];
     for args.each |arg| {
         let t = @copy *arg;
         tmps.push(t);
-        argptrs.push_all(str::as_c_str(*t, |b| ~[b]));
+        argptrs.push(str::as_c_str(*t, |b| b));
     }
     argptrs.push(ptr::null());
     vec::as_imm_buf(argptrs, |buf, _len| cb(buf))
 }
 
 #[cfg(unix)]
-fn with_envp<T>(env: &Option<~[(~str,~str)]>,
-                cb: &fn(*c_void) -> T) -> T {
+fn with_envp<T>(env: Option<&[(~str, ~str)]>, cb: &fn(*c_void) -> T) -> T {
     // On posixy systems we can pass a char** for envp, which is
     // a null-terminated array of "k=v\n" strings.
-    match *env {
-      Some(ref es) if !vec::is_empty(*es) => {
+    match env {
+      Some(es) => {
         let mut tmps = ~[];
         let mut ptrs = ~[];
 
-        for (*es).each |e| {
-            let (k,v) = copy *e;
-            let t = @(fmt!("%s=%s", k, v));
-            tmps.push(t);
-            ptrs.push_all(str::as_c_str(*t, |b| ~[b]));
+        for es.each |&(k, v)| {
+            let kv = @fmt!("%s=%s", k, v);
+            tmps.push(kv);
+            ptrs.push(str::as_c_str(*kv, |b| b));
         }
+
         ptrs.push(ptr::null());
         vec::as_imm_buf(ptrs, |p, _len|
             unsafe { cb(::cast::transmute(p)) }
@@ -461,47 +726,35 @@ fn with_envp<T>(env: &Option<~[(~str,~str)]>,
 }
 
 #[cfg(windows)]
-fn with_envp<T>(env: &Option<~[(~str,~str)]>,
-                cb: &fn(*mut c_void) -> T) -> T {
+fn with_envp<T>(env: Option<&[(~str, ~str)]>, cb: &fn(*mut c_void) -> T) -> T {
     // On win32 we pass an "environment block" which is not a char**, but
     // rather a concatenation of null-terminated k=v\0 sequences, with a final
     // \0 to terminate.
-    unsafe {
-        match *env {
-          Some(ref es) if !vec::is_empty(*es) => {
-            let mut blk : ~[u8] = ~[];
-            for (*es).each |e| {
-                let (k,v) = copy *e;
-                let t = fmt!("%s=%s", k, v);
-                let mut v : ~[u8] = ::cast::transmute(t);
-                blk += v;
-                ::cast::forget(v);
-            }
-            blk += ~[0_u8];
-            vec::as_imm_buf(blk, |p, _len| cb(::cast::transmute(p)))
-          }
-          _ => cb(ptr::mut_null())
+    match env {
+      Some(es) => {
+        let mut blk = ~[];
+        for es.each |&(k, v)| {
+            let kv = fmt!("%s=%s", k, v);
+            blk.push_all(str::as_bytes_slice(kv));
+            blk.push(0);
         }
+        blk.push(0);
+        vec::as_imm_buf(blk, |p, _len|
+            unsafe { cb(::cast::transmute(p)) }
+        )
+      }
+      _ => cb(ptr::mut_null())
     }
 }
 
-#[cfg(windows)]
-fn with_dirp<T>(d: &Option<~str>,
+fn with_dirp<T>(d: Option<&Path>,
                 cb: &fn(*libc::c_char) -> T) -> T {
-    match *d {
-      Some(ref dir) => str::as_c_str(*dir, cb),
+    match d {
+      Some(dir) => str::as_c_str(dir.to_str(), cb),
       None => cb(ptr::null())
     }
 }
 
-/// helper function that closes non-NULL files and then makes them NULL
-priv unsafe fn fclose_and_null(f: &mut *libc::FILE) {
-    if *f != 0 as *libc::FILE {
-        libc::fclose(*f);
-        *f = 0 as *libc::FILE;
-    }
-}
-
 #[cfg(windows)]
 priv fn free_handle(handle: *()) {
     unsafe {
@@ -515,7 +768,8 @@ priv fn free_handle(_handle: *()) {
 }
 
 /**
- * Spawns a process and waits for it to terminate
+ * Spawns a process and waits for it to terminate. The process will
+ * inherit the current stdin/stdout/stderr file descriptors.
  *
  * # Arguments
  *
@@ -526,71 +780,19 @@ priv fn free_handle(_handle: *()) {
  *
  * The process's exit code
  */
-pub fn run_program(prog: &str, args: &[~str]) -> int {
-    let res = spawn_process_internal(prog, args, &None, &None,
-                                     0i32, 0i32, 0i32);
-    let code = waitpid(res.pid);
-    free_handle(res.handle);
-    return code;
-}
-
-/**
- * Spawns a process and returns a Program
- *
- * The returned value is a <Program> object that can be used for sending and
- * receiving data over the standard file descriptors.  The class will ensure
- * that file descriptors are closed properly.
- *
- * # Arguments
- *
- * * prog - The path to an executable
- * * args - Vector of arguments to pass to the child process
- *
- * # Return value
- *
- * A <Program> object
- */
-pub fn start_program(prog: &str, args: &[~str]) -> Program {
-    let pipe_input = os::pipe();
-    let pipe_output = os::pipe();
-    let pipe_err = os::pipe();
-    let res =
-        spawn_process_internal(prog, args, &None, &None,
-                               pipe_input.in, pipe_output.out,
-                               pipe_err.out);
-
-    unsafe {
-        libc::close(pipe_input.in);
-        libc::close(pipe_output.out);
-        libc::close(pipe_err.out);
-    }
-
-    Program {
-        pid: res.pid,
-        handle: res.handle,
-        in_fd: pipe_input.out,
-        out_file: os::fdopen(pipe_output.in),
-        err_file: os::fdopen(pipe_err.in),
-        finished: false,
-    }
-}
-
-fn read_all(rd: @io::Reader) -> ~str {
-    let buf = io::with_bytes_writer(|wr| {
-        let mut bytes = [0, ..4096];
-        while !rd.eof() {
-            let nread = rd.read(bytes, bytes.len());
-            wr.write(bytes.slice(0, nread));
-        }
+pub fn process_status(prog: &str, args: &[~str]) -> int {
+    let mut prog = Process::new(prog, args, ProcessOptions {
+        env: None,
+        dir: None,
+        in_fd: Some(0),
+        out_fd: Some(1),
+        err_fd: Some(2)
     });
-    str::from_bytes(buf)
+    prog.finish()
 }
 
-pub struct ProgramOutput {status: int, out: ~str, err: ~str}
-
 /**
- * Spawns a process, waits for it to exit, and returns the exit code, and
- * contents of stdout and stderr.
+ * Spawns a process, records all its output, and waits for it to terminate.
  *
  * # Arguments
  *
@@ -599,94 +801,24 @@ pub struct ProgramOutput {status: int, out: ~str, err: ~str}
  *
  * # Return value
  *
- * A record, {status: int, out: str, err: str} containing the exit code,
- * the contents of stdout and the contents of stderr.
+ * The process's stdout/stderr output and exit code.
  */
-pub fn program_output(prog: &str, args: &[~str]) -> ProgramOutput {
-    let pipe_in = os::pipe();
-    let pipe_out = os::pipe();
-    let pipe_err = os::pipe();
-    let res = spawn_process_internal(prog, args, &None, &None,
-                                     pipe_in.in, pipe_out.out, pipe_err.out);
-
-    os::close(pipe_in.in);
-    os::close(pipe_out.out);
-    os::close(pipe_err.out);
-    os::close(pipe_in.out);
-
-    // Spawn two entire schedulers to read both stdout and sterr
-    // in parallel so we don't deadlock while blocking on one
-    // or the other. FIXME (#2625): Surely there's a much more
-    // clever way to do this.
-    let (p, ch) = stream();
-    let ch = SharedChan::new(ch);
-    let ch_clone = ch.clone();
-    do task::spawn_sched(task::SingleThreaded) {
-        let errput = readclose(pipe_err.in);
-        ch.send((2, errput));
-    };
-    do task::spawn_sched(task::SingleThreaded) {
-        let output = readclose(pipe_out.in);
-        ch_clone.send((1, output));
-    };
-
-    let status = waitpid(res.pid);
-    free_handle(res.handle);
-
-    let mut errs = ~"";
-    let mut outs = ~"";
-    let mut count = 2;
-    while count > 0 {
-        let stream = p.recv();
-        match stream {
-            (1, copy s) => {
-                outs = s;
-            }
-            (2, copy s) => {
-                errs = s;
-            }
-            (n, _) => {
-                fail!("program_output received an unexpected file number: %u", n);
-            }
-        };
-        count -= 1;
-    };
-    return ProgramOutput {status: status,
-                          out: outs,
-                          err: errs};
-}
-
-pub fn writeclose(fd: c_int, s: ~str) {
-    use io::WriterUtil;
-
-    error!("writeclose %d, %s", fd as int, s);
-    let writer = io::fd_writer(fd, false);
-    writer.write_str(s);
-
-    os::close(fd);
-}
-
-pub fn readclose(fd: c_int) -> ~str {
-    unsafe {
-        let file = os::fdopen(fd);
-        let reader = io::FILE_reader(file, false);
-        let buf = io::with_bytes_writer(|writer| {
-            let mut bytes = [0, ..4096];
-            while !reader.eof() {
-                let nread = reader.read(bytes, bytes.len());
-                writer.write(bytes.slice(0, nread));
-            }
-        });
-        os::fclose(file);
-        str::from_bytes(buf)
-    }
+pub fn process_output(prog: &str, args: &[~str]) -> ProcessOutput {
+    let mut prog = Process::new(prog, args, ProcessOptions::new());
+    prog.finish_with_output()
 }
 
 /**
  * Waits for a process to exit and returns the exit code, failing
  * if there is no process with the specified id.
+ *
+ * Note that this is private to avoid race conditions on unix where if
+ * a user calls waitpid(some_process.get_id()) then some_process.finish()
+ * and some_process.destroy() and some_process.drop() will then either
+ * operate on a none-existant process or, even worse, on a newer process
+ * with the same id.
  */
-pub fn waitpid(pid: pid_t) -> int {
+priv fn waitpid(pid: pid_t) -> int {
     return waitpid_os(pid);
 
     #[cfg(windows)]
@@ -777,10 +909,13 @@ pub fn waitpid(pid: pid_t) -> int {
 
 #[cfg(test)]
 mod tests {
-    use option::None;
+    use io;
+    use libc::{c_int};
+    use option::{Option, None, Some};
     use os;
-    use run::{readclose, writeclose};
+    use path::Path;
     use run;
+    use str;
 
     #[test]
     #[cfg(windows)]
@@ -803,54 +938,222 @@ mod tests {
         );
     }
 
-    // Regression test for memory leaks
     #[test]
-    fn test_leaks() {
-        run::run_program("echo", []);
-        run::start_program("echo", []);
-        run::program_output("echo", []);
+    fn test_process_status() {
+        assert_eq!(run::process_status("false", []), 1);
+        assert_eq!(run::process_status("true", []), 0);
+    }
+
+    #[test]
+    fn test_process_output_output() {
+
+        let run::ProcessOutput {status, output, error}
+             = run::process_output("echo", [~"hello"]);
+        let output_str = str::from_bytes(output);
+
+        assert_eq!(status, 0);
+        assert_eq!(output_str.trim().to_owned(), ~"hello");
+        assert_eq!(error, ~[]);
+    }
+
+    #[test]
+    fn test_process_output_error() {
+
+        let run::ProcessOutput {status, output, error}
+             = run::process_output("mkdir", [~"."]);
+
+        assert_eq!(status, 1);
+        assert_eq!(output, ~[]);
+        assert!(!error.is_empty());
     }
 
     #[test]
-    #[allow(non_implicitly_copyable_typarams)]
     fn test_pipes() {
+
         let pipe_in = os::pipe();
         let pipe_out = os::pipe();
         let pipe_err = os::pipe();
 
-        let pid =
-            run::spawn_process(
-                "cat", [], &None, &None,
-                pipe_in.in, pipe_out.out, pipe_err.out);
+        let mut proc = run::Process::new("cat", [], run::ProcessOptions {
+            dir: None,
+            env: None,
+            in_fd: Some(pipe_in.in),
+            out_fd: Some(pipe_out.out),
+            err_fd: Some(pipe_err.out)
+        });
+
+        assert!(proc.input_redirected());
+        assert!(proc.output_redirected());
+        assert!(proc.error_redirected());
+
         os::close(pipe_in.in);
         os::close(pipe_out.out);
         os::close(pipe_err.out);
 
-        if pid == -1i32 { fail!(); }
         let expected = ~"test";
-        writeclose(pipe_in.out, copy expected);
+        writeclose(pipe_in.out, expected);
         let actual = readclose(pipe_out.in);
         readclose(pipe_err.in);
-        run::waitpid(pid);
+        proc.finish();
 
-        debug!(copy expected);
-        debug!(copy actual);
         assert_eq!(expected, actual);
     }
 
+    fn writeclose(fd: c_int, s: &str) {
+        let writer = io::fd_writer(fd, false);
+        writer.write_str(s);
+        os::close(fd);
+    }
+
+    fn readclose(fd: c_int) -> ~str {
+        unsafe {
+            let file = os::fdopen(fd);
+            let reader = io::FILE_reader(file, false);
+            let buf = reader.read_whole_stream();
+            os::fclose(file);
+            str::from_bytes(buf)
+        }
+    }
+
     #[test]
-    fn waitpid() {
-        let pid = run::spawn_process("false", [],
-                                     &None, &None,
-                                     0i32, 0i32, 0i32);
-        let status = run::waitpid(pid);
-        assert_eq!(status, 1);
+    fn test_finish_once() {
+        let mut prog = run::Process::new("false", [], run::ProcessOptions::new());
+        assert_eq!(prog.finish(), 1);
+    }
+
+    #[test]
+    fn test_finish_twice() {
+        let mut prog = run::Process::new("false", [], run::ProcessOptions::new());
+        assert_eq!(prog.finish(), 1);
+        assert_eq!(prog.finish(), 1);
+    }
+
+    #[test]
+    fn test_finish_with_output_once() {
+
+        let mut prog = run::Process::new("echo", [~"hello"], run::ProcessOptions::new());
+        let run::ProcessOutput {status, output, error}
+            = prog.finish_with_output();
+        let output_str = str::from_bytes(output);
+
+        assert_eq!(status, 0);
+        assert_eq!(output_str.trim().to_owned(), ~"hello");
+        assert_eq!(error, ~[]);
+    }
+
+    #[test]
+    fn test_finish_with_output_twice() {
+
+        let mut prog = run::Process::new("echo", [~"hello"], run::ProcessOptions::new());
+        let run::ProcessOutput {status, output, error}
+            = prog.finish_with_output();
+
+        let output_str = str::from_bytes(output);
+
+        assert_eq!(status, 0);
+        assert_eq!(output_str.trim().to_owned(), ~"hello");
+        assert_eq!(error, ~[]);
+
+        let run::ProcessOutput {status, output, error}
+            = prog.finish_with_output();
+
+        assert_eq!(status, 0);
+        assert_eq!(output, ~[]);
+        assert_eq!(error, ~[]);
     }
 
     #[test]
     #[should_fail]
-    #[ignore(cfg(windows))]
-    fn waitpid_non_existant_pid() {
-        run::waitpid(123456789); // assume that this pid doesn't exist
+    #[cfg(not(windows))]
+    fn test_finish_with_output_redirected() {
+        let mut prog = run::Process::new("echo", [~"hello"], run::ProcessOptions {
+            env: None,
+            dir: None,
+            in_fd: Some(0),
+            out_fd: Some(1),
+            err_fd: Some(2)
+        });
+        // this should fail because it is not valid to read the output when it was redirected
+        prog.finish_with_output();
+    }
+
+    #[cfg(unix)]
+    fn run_pwd(dir: Option<&Path>) -> run::Process {
+        run::Process::new("pwd", [], run::ProcessOptions {
+            dir: dir,
+            .. run::ProcessOptions::new()
+        })
+    }
+
+    #[cfg(windows)]
+    fn run_pwd(dir: Option<&Path>) -> run::Process {
+        run::Process::new("cmd", [~"/c", ~"cd"], run::ProcessOptions {
+            dir: dir,
+            .. run::ProcessOptions::new()
+        })
+    }
+
+    #[test]
+    fn test_keep_current_working_dir() {
+
+        let mut prog = run_pwd(None);
+
+        let output = str::from_bytes(prog.finish_with_output().output);
+        let parent_dir = os::getcwd().normalize();
+        let child_dir = Path(output.trim()).normalize();
+
+        assert_eq!(child_dir.to_str(), parent_dir.to_str());
+    }
+
+    #[test]
+    fn test_change_working_directory() {
+
+        let tmp_path = os::tmpdir().normalize();
+        let mut prog = run_pwd(Some(&tmp_path));
+
+        let output = str::from_bytes(prog.finish_with_output().output);
+        let child_dir = Path(output.trim()).normalize();
+
+        assert_eq!(child_dir.to_str(), tmp_path.to_str());
+    }
+
+    #[cfg(unix)]
+    fn run_env(env: Option<&[(~str, ~str)]>) -> run::Process {
+        run::Process::new("env", [], run::ProcessOptions {
+            env: env,
+            .. run::ProcessOptions::new()
+        })
+    }
+
+    #[cfg(windows)]
+    fn run_env(env: Option<&[(~str, ~str)]>) -> run::Process {
+        run::Process::new("cmd", [~"/c", ~"set"], run::ProcessOptions {
+            env: env,
+            .. run::ProcessOptions::new()
+        })
+    }
+
+    #[test]
+    fn test_inherit_env() {
+
+        let mut prog = run_env(None);
+        let output = str::from_bytes(prog.finish_with_output().output);
+
+        for os::env().each |&(k, v)| {
+            // don't check windows magical empty-named variables
+            assert!(k.is_empty() || output.contains(fmt!("%s=%s", k, v)));
+        }
+    }
+
+    #[test]
+    fn test_add_to_env() {
+
+        let mut new_env = os::env();
+        new_env.push((~"RUN_TEST_NEW_ENV", ~"123"));
+
+        let mut prog = run_env(Some(new_env.slice(0, new_env.len())));
+        let output = str::from_bytes(prog.finish_with_output().output);
+
+        assert!(output.contains("RUN_TEST_NEW_ENV=123"));
     }
 }
diff --git a/src/test/run-pass/core-run-destroy.rs b/src/test/run-pass/core-run-destroy.rs
index b2f444d39e2..a80d9661aa2 100644
--- a/src/test/run-pass/core-run-destroy.rs
+++ b/src/test/run-pass/core-run-destroy.rs
@@ -20,13 +20,13 @@ use std::run::*;
 
 #[test]
 fn test_destroy_once() {
-    let mut p = run::start_program("echo", []);
+    let mut p = run::Process::new("echo", [], run::ProcessOptions::new());
     p.destroy(); // this shouldn't crash (and nor should the destructor)
 }
 
 #[test]
 fn test_destroy_twice() {
-    let mut p = run::start_program("echo", []);
+    let mut p = run::Process::new("echo", [], run::ProcessOptions::new());
     p.destroy(); // this shouldnt crash...
     p.destroy(); // ...and nor should this (and nor should the destructor)
 }
@@ -41,7 +41,8 @@ fn test_destroy_actually_kills(force: bool) {
 
     #[cfg(unix)]
     fn process_exists(pid: libc::pid_t) -> bool {
-        run::program_output("ps", [~"-p", pid.to_str()]).out.contains(pid.to_str())
+        let run::ProcessOutput {output, _} = run::process_output("ps", [~"-p", pid.to_str()]);
+        str::from_bytes(output).contains(pid.to_str())
     }
 
     #[cfg(windows)]
@@ -64,8 +65,8 @@ fn test_destroy_actually_kills(force: bool) {
         }
     }
 
-    // this program will stay alive indefinitely trying to read from stdin
-    let mut p = run::start_program(BLOCK_COMMAND, []);
+    // this process will stay alive indefinitely trying to read from stdin
+    let mut p = run::Process::new(BLOCK_COMMAND, [], run::ProcessOptions::new());
 
     assert!(process_exists(p.get_id()));