From c6947fde57bb09b628b239d57415bdf19e2d438e Mon Sep 17 00:00:00 2001 From: Delyan Angelov Date: Thu, 13 Apr 2023 14:48:32 +0300 Subject: [PATCH] os: implement Process.set_work_folder/0 to set the initial working folder of the new child process (#17946) --- cmd/tools/test_os_process.v | 15 ++++++++++++++- vlib/os/process.v | 18 ++++++++++++++---- vlib/os/process_nix.c.v | 9 +++++++++ vlib/os/process_test.v | 33 +++++++++++++++++++++++++++++++++ vlib/os/process_windows.c.v | 22 ++++++++++++++++++++-- 5 files changed, 90 insertions(+), 7 deletions(-) diff --git a/cmd/tools/test_os_process.v b/cmd/tools/test_os_process.v index 90fa64f213..7113dadbb9 100644 --- a/cmd/tools/test_os_process.v +++ b/cmd/tools/test_os_process.v @@ -28,6 +28,8 @@ mut: target Target omode Target is_verbose bool + show_wd bool + show_env bool } fn (mut ctx Context) println(s string) { @@ -53,7 +55,7 @@ fn main() { args := os.args[1..] if '-h' in args || '--help' in args { println("Usage: - test_os_process [-v] [-h] [-target stderr/stdout/both/alternate] [-exitcode 0] [-timeout_ms 200] [-period_ms 50] + test_os_process [-v] [-h] [-target stderr/stdout/both/alternate] [-show_wd] [-show_env] [-exitcode 0] [-timeout_ms 200] [-period_ms 50] Prints lines periodically (-period_ms), to stdout/stderr (-target). After a while (-timeout_ms), exit with (-exitcode). This program is useful for platform independent testing @@ -63,6 +65,8 @@ fn main() { return } ctx.is_verbose = '-v' in args + ctx.show_wd = '-show_wd' in args + ctx.show_env = '-show_env' in args ctx.target = s2target(cmdline.option(args, '-target', 'both')) ctx.exitcode = cmdline.option(args, '-exitcode', '0').int() ctx.timeout_ms = cmdline.option(args, '-timeout_ms', '200').int() @@ -73,6 +77,15 @@ fn main() { if ctx.is_verbose { eprintln('> args: ${args} | context: ${ctx}') } + if ctx.show_wd { + ctx.println('WORK_DIR=${os.getwd()}') + } + if ctx.show_env { + all := os.environ() + for k, v in all { + ctx.println('${k}=${v}') + } + } spawn do_timeout(&ctx) for i := 1; true; i++ { ctx.println('${i}') diff --git a/vlib/os/process.v b/vlib/os/process.v index eba58ca5c9..d45de326db 100644 --- a/vlib/os/process.v +++ b/vlib/os/process.v @@ -17,16 +17,16 @@ pub enum ProcessState { [heap] pub struct Process { -pub: - filename string // the process's command file path pub mut: - pid int // the PID of the process - code int = -1 + filename string // the process's command file path + pid int // the PID of the process + code int = -1 // the exit code of the process, != -1 *only* when status is .exited *and* the process was not aborted status ProcessState = .not_started // the current status of the process err string // if the process fails, contains the reason why args []string // the arguments that the command takes + work_folder string // the initial working folder of the process. When '', reuse the same folder as the parent process. env_is_custom bool // true, when the environment was customized with .set_environment env []string // the environment with which the process was started (list of 'var=val') use_stdio_ctl bool // when true, then you can use p.stdin_write(), p.stdout_slurp() and p.stderr_slurp() @@ -57,6 +57,16 @@ pub fn (mut p Process) set_args(pargs []string) { return } +// set_work_folder - set the initial working folder for the new process +// If you do not set it, it will reuse the current working folder of the parent process. +pub fn (mut p Process) set_work_folder(path string) { + if p.status != .not_started { + return + } + p.work_folder = real_path(path) + return +} + // set_environment - set a custom environment variable mapping for the new process pub fn (mut p Process) set_environment(envs map[string]string) { if p.status != .not_started { diff --git a/vlib/os/process_nix.c.v b/vlib/os/process_nix.c.v index 43d69aaff0..009a61f66c 100644 --- a/vlib/os/process_nix.c.v +++ b/vlib/os/process_nix.c.v @@ -50,6 +50,15 @@ fn (mut p Process) unix_spawn_process() int { fd_close(pipeset[3]) fd_close(pipeset[5]) } + if p.work_folder != '' { + if !is_abs_path(p.filename) { + // Ensure p.filename contains an absolute path, so it + // can be located reliably, even after changing the + // current folder in the child process: + p.filename = abs_path(p.filename) + } + chdir(p.work_folder) or {} + } execve(p.filename, p.args, p.env) or { eprintln(err) exit(1) diff --git a/vlib/os/process_test.v b/vlib/os/process_test.v index 7a066f8d9b..a810dc72e0 100644 --- a/vlib/os/process_test.v +++ b/vlib/os/process_test.v @@ -36,6 +36,39 @@ fn test_getpid() { assert pid != 0 } +fn test_set_work_folder() { + new_work_folder := os.real_path(os.temp_dir()) + parent_working_folder := os.getwd() + dump(new_work_folder) + dump(parent_working_folder) + if new_work_folder == parent_working_folder { + eprintln('... skipping ${@METHOD} because the working folder is the temporary one') + return + } + mut p := os.new_process(test_os_process) + p.set_args(['-show_wd', '-target', 'stdout']) + p.set_work_folder(new_work_folder) + p.set_redirect_stdio() + p.wait() + assert p.code == 0 + output := p.stdout_slurp().trim_space() + p.close() + $if trace_process_output ? { + eprintln('p output: "${output}"') + } + child_work_folder := output.find_between('stdout, WORK_DIR=', '\n').trim_space() + dump(child_work_folder) + assert child_work_folder == new_work_folder + new_parent_work_folder := os.getwd() + dump(new_parent_work_folder) + assert new_parent_work_folder == parent_working_folder + assert new_parent_work_folder != child_work_folder +} + +fn test_done() { + exit(0) +} + fn test_run() { mut p := os.new_process(test_os_process) p.set_args(['-timeout_ms', '150', '-period_ms', '50']) diff --git a/vlib/os/process_windows.c.v b/vlib/os/process_windows.c.v index def17d62f7..6d9b67f596 100644 --- a/vlib/os/process_windows.c.v +++ b/vlib/os/process_windows.c.v @@ -57,7 +57,16 @@ pub mut: child_stderr_write &u32 = unsafe { nil } } +[manualfree] fn (mut p Process) win_spawn_process() int { + mut to_be_freed := []voidptr{cap: 5} + defer { + for idx := to_be_freed.len - 1; idx >= 0; idx-- { + unsafe { free(to_be_freed[idx]) } + } + unsafe { to_be_freed.free() } + } + p.filename = abs_path(p.filename) // expand the path to an absolute one, in case we later change the working folder mut wdata := &WProcess{ child_stdin: 0 child_stdout_read: 0 @@ -95,7 +104,9 @@ fn (mut p Process) win_spawn_process() int { start_info.dw_flags = u32(C.STARTF_USESTDHANDLES) } cmd := '${p.filename} ' + p.args.join(' ') - C.ExpandEnvironmentStringsW(cmd.to_wide(), voidptr(&wdata.command_line[0]), 32768) + cmd_wide_ptr := cmd.to_wide() + to_be_freed << cmd_wide_ptr + C.ExpandEnvironmentStringsW(cmd_wide_ptr, voidptr(&wdata.command_line[0]), 32768) mut creation_flags := if p.create_no_window { int(C.CREATE_NO_WINDOW) @@ -105,8 +116,15 @@ fn (mut p Process) win_spawn_process() int { if p.use_pgroup { creation_flags |= C.CREATE_NEW_PROCESS_GROUP } + + mut work_folder_ptr := voidptr(unsafe { nil }) + if p.work_folder != '' { + work_folder_ptr = p.work_folder.to_wide() + to_be_freed << work_folder_ptr + } + create_process_ok := C.CreateProcessW(0, &wdata.command_line[0], 0, 0, C.TRUE, creation_flags, - 0, 0, voidptr(&start_info), voidptr(&wdata.proc_info)) + 0, work_folder_ptr, voidptr(&start_info), voidptr(&wdata.proc_info)) failed_cfn_report_error(create_process_ok, 'CreateProcess') if p.use_stdio_ctl { close_valid_handle(&wdata.child_stdout_write)