From 04d978575c57ba0de82eb7866a2663f4f9cbe540 Mon Sep 17 00:00:00 2001 From: Alexander Medvednikov Date: Tue, 14 Apr 2026 23:17:50 +0300 Subject: [PATCH] os: fix problems executing programs (fixes #14486) --- vlib/os/os.v | 24 ++++++++++++------- vlib/os/process.c.v | 14 +++++++---- vlib/os/process_nix.c.v | 34 +++++++++++++++++++++----- vlib/os/process_test.v | 53 +++++++++++++++++++++++++++++++++++++++++ 4 files changed, 106 insertions(+), 19 deletions(-) diff --git a/vlib/os/os.v b/vlib/os/os.v index bc641be7c..dff0f6f98 100644 --- a/vlib/os/os.v +++ b/vlib/os/os.v @@ -585,20 +585,20 @@ fn error_failed_to_find_executable() IError { return &ExecutableNotFoundError{} } -// find_abs_path_of_executable searches the environment PATH for the absolute path of the given executable name. -pub fn find_abs_path_of_executable(exe_name string) !string { - if exe_name == '' { - return error('expected non empty `exe_name`') - } - +fn find_abs_path_of_executable_in_path_env(exe_name string, env_path string) !string { for suffix in executable_suffixes { fexepath := exe_name + suffix if is_abs_path(fexepath) { return fexepath } + if fexepath.contains(path_separator) { + if is_file(fexepath) && is_executable(fexepath) { + return abs_path(fexepath) + } + continue + } mut res := '' - path := getenv('PATH') - paths := path.split(path_delimiter) + paths := env_path.split(path_delimiter) for p in paths { found_abs_path := join_path_single(p, fexepath) $if trace_find_abs_path_of_executable ? { @@ -616,6 +616,14 @@ pub fn find_abs_path_of_executable(exe_name string) !string { return error_failed_to_find_executable() } +// find_abs_path_of_executable searches the environment PATH for the absolute path of the given executable name. +pub fn find_abs_path_of_executable(exe_name string) !string { + if exe_name == '' { + return error('expected non empty `exe_name`') + } + return find_abs_path_of_executable_in_path_env(exe_name, getenv('PATH')) +} + // exists_in_system_path returns `true` if `prog` exists in the system's PATH. pub fn exists_in_system_path(prog string) bool { find_abs_path_of_executable(prog) or { return false } diff --git a/vlib/os/process.c.v b/vlib/os/process.c.v index 126b3903d..d8f020dd8 100644 --- a/vlib/os/process.c.v +++ b/vlib/os/process.c.v @@ -66,21 +66,25 @@ pub fn (mut p Process) wait() { p._wait() } -// free the OS resources associated with the process. -// Can be called multiple times, but will free the resources just once. -// This sets the process state to .closed, which is final. +// close frees the OS resources associated with the process. +// It can be called multiple times, but will free the resources just once. +// If the process has already finished, this sets the process state to +// .closed, which is final. pub fn (mut p Process) close() { if p.status in [.not_started, .closed] { return } - p.status = .closed $if !windows { for i in 0 .. 3 { - if p.stdio_fd[i] != 0 { + if p.stdio_fd[i] != -1 { fd_close(p.stdio_fd[i]) + p.stdio_fd[i] = -1 } } } + if p.status !in [.running, .stopped] { + p.status = .closed + } } @[unsafe] diff --git a/vlib/os/process_nix.c.v b/vlib/os/process_nix.c.v index 0ffdda59c..81fe275b2 100644 --- a/vlib/os/process_nix.c.v +++ b/vlib/os/process_nix.c.v @@ -2,6 +2,30 @@ module os fn C.setpgid(pid i32, pgid i32) i32 +fn env_value_from_entries(env []string, name string) ?string { + prefix := '${name}=' + for entry in env { + if entry.starts_with(prefix) { + return entry[prefix.len..] + } + } + return none +} + +fn (p &Process) unix_resolve_filename() !string { + if is_abs_path(p.filename) { + return p.filename + } + if p.filename.contains(path_separator) { + if p.work_folder != '' { + return abs_path(p.filename) + } + return p.filename + } + path := env_value_from_entries(p.env, 'PATH') or { return error_failed_to_find_executable() } + return find_abs_path_of_executable_in_path_env(p.filename, path) +} + fn (mut p Process) unix_spawn_process() int { mut pipeset := [6]int{} if p.use_stdio_ctl { @@ -50,13 +74,11 @@ fn (mut p Process) unix_spawn_process() int { fd_close(pipeset[3]) fd_close(pipeset[5]) } + p.filename = p.unix_resolve_filename() or { + eprintln(err) + exit(1) + } 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 { diff --git a/vlib/os/process_test.v b/vlib/os/process_test.v index 937c0e2e9..627be5531 100644 --- a/vlib/os/process_test.v +++ b/vlib/os/process_test.v @@ -12,6 +12,8 @@ const delayed_output_exe_filename = os.join_path(tfolder, 'delayed_output.exe') const delayed_output_source_filename = os.join_path(tfolder, 'delayed_output.v') const utf16le_output_exe_filename = os.join_path(tfolder, 'utf16le_output.exe') const utf16le_output_source_filename = os.join_path(tfolder, 'utf16le_output.v') +const stdin_exit_exe_filename = os.join_path(tfolder, 'stdin_exit.exe') +const stdin_exit_source_filename = os.join_path(tfolder, 'stdin_exit.v') const echo_process_source_code = ' module main import io @@ -51,6 +53,16 @@ fn main() { } ' +const stdin_exit_source_code = ' +module main +import os + +fn main() { + _ = os.get_raw_line() + exit(7) +} +' + const echo_wait_timeout = 5 // seconds fn testsuite_begin() { @@ -78,6 +90,10 @@ fn testsuite_begin() { os.write_file(utf16le_output_source_filename, utf16le_output_source_code)! os.system('${os.quoted_path(vexe)} -o ${os.quoted_path(utf16le_output_exe_filename)} ${os.quoted_path(utf16le_output_source_filename)}') assert os.exists(utf16le_output_exe_filename) + + os.write_file(stdin_exit_source_filename, stdin_exit_source_code)! + os.system('${os.quoted_path(vexe)} -o ${os.quoted_path(stdin_exit_exe_filename)} ${os.quoted_path(stdin_exit_source_filename)}') + assert os.exists(stdin_exit_exe_filename) } fn testsuite_end() { @@ -170,6 +186,30 @@ fn test_new_process_uses_exact_executable_path_when_folder_contains_spaces() { assert !output.contains('stale-prefix-exe'), output } +fn test_new_process_uses_path_for_bare_command_names() { + $if windows { + return + } + eprintln(@FN) + original_path := os.getenv('PATH') + defer { + os.setenv('PATH', original_path, true) + } + path_dir := os.join_path(tfolder, 'path_bin') + os.rmdir_all(path_dir) or {} + os.mkdir_all(path_dir)! + path_exe := os.join_path(path_dir, 'process_from_path.exe') + os.cp(test_os_process, path_exe)! + os.setenv('PATH', '${path_dir}${os.path_delimiter}${original_path}', true) + mut p := os.new_process('process_from_path.exe') + p.set_args(['-exitcode', '7']) + p.set_work_folder(os.real_path(os.temp_dir())) + p.wait() + assert p.status == .exited + assert p.code == 7 + p.close() +} + fn test_run() { eprintln(@FN) mut p := os.new_process(test_os_process) @@ -270,6 +310,19 @@ fn test_stdin_write() { p.close() } +fn test_close_before_wait_preserves_exit_code() { + eprintln(@FN) + mut p := os.new_process(stdin_exit_exe_filename) + p.set_redirect_stdio() + p.run() + p.stdin_write('hello\n') + p.close() + p.wait() + assert p.status == .exited + assert p.code == 7 + p.close() +} + fn test_stdout_read_returns_immediately_when_no_data_is_pending() { eprintln(@FN) mut p := os.new_process(delayed_output_exe_filename) -- 2.39.5