diff --git a/cmd/tools/vwatch.v b/cmd/tools/vwatch.v new file mode 100644 index 0000000000..0db0a4a86b --- /dev/null +++ b/cmd/tools/vwatch.v @@ -0,0 +1,288 @@ +module main + +import os +import time + +const scan_timeout_s = 5 * 60 + +const max_v_cycles = 1000 + +const scan_frequency_hz = 4 + +const scan_period_ms = 1000 / scan_frequency_hz + +const max_scan_cycles = scan_timeout_s * scan_frequency_hz + +// +// Implements `v -watch file.v` , `v -watch run file.v` etc. +// With this command, V will collect all .v files that are needed for the +// compilation, then it will enter an infinite loop, monitoring them for +// changes. +// +// When a change is detected, it will stop the current process, if it is +// still running, then rerun/recompile/etc. +// +// In effect, this makes it easy to have an editor session and a separate +// terminal, running just `v -watch run file.v`, and you will see your +// changes right after you save your .v file in your editor. +// +// +// Since -gc boehm is not available on all platforms yet, +// and this program leaks ~8MB/minute without it, the implementation here +// is done similarly to vfmt in 2 modes, in the same executable: +// +// a) A parent/manager process that only manages a single worker +// process. The parent process does mostly nothing except restarting +// workers, thus it does not leak much. +// +// b) A worker process, doing the actual monitoring/polling. +// NB: *workers are started with the -vwatchworker option* +// +// Worker processes will run for a limited number of iterations, then +// they will do exit(255), and then the parent will start a new worker. +// Exiting by any other code will cause the parent to also exit with the +// same error code. This limits the potential leak that a worker process +// can do, even without using the garbage collection mode. +// + +struct VFileStat { + path string + mtime int +} + +[unsafe] +fn (mut vfs VFileStat) free() { + unsafe { vfs.path.free() } +} + +enum RerunCommand { + restart + quit +} + +struct Context { +mut: + pid int // the pid of the current process; useful while debugging manager/worker interactions + is_worker bool // true in the workers, false in the manager process + check_period_ms int = scan_period_ms + vexe string + affected_paths []string + vfiles []VFileStat + opts []string + rerun_channel chan RerunCommand + child_process &os.Process + is_exiting bool // set by SIGINT/Ctrl-C + v_cycles int // how many times the worker has restarted the V compiler + scan_cycles int // how many times the worker has scanned for source file changes +} + +[if debug_vwatch] +fn (mut context Context) elog(msg string) { + eprintln('> vredo $context.pid, $msg') +} + +fn (context &Context) str() string { + return 'Context{ pid: $context.pid, is_worker: $context.is_worker, check_period_ms: $context.check_period_ms, vexe: $context.vexe, opts: $context.opts, is_exiting: $context.is_exiting, vfiles: $context.vfiles' +} + +fn (mut context Context) get_stats_for_affected_vfiles() []VFileStat { + if context.affected_paths.len == 0 { + mut apaths := map[string]bool{} + // The next command will make V parse the program, and print all .v files, + // needed for its compilation, without actually compiling it. + copts := context.opts.join(' ') + cmd := '"$context.vexe" -silent -print-v-files $copts' + // context.elog('> cmd: $cmd') + mut vfiles := os.execute(cmd) + if vfiles.exit_code == 0 { + paths_trimmed := vfiles.output.trim_space() + mut paths := paths_trimmed.split('\n') + for vf in paths { + apaths[os.real_path(os.dir(vf))] = true + } + } + context.affected_paths = apaths.keys() + // context.elog('vfiles paths to be scanned: $context.affected_paths') + } + // scan all files in the found folders + mut newstats := []VFileStat{} + for path in context.affected_paths { + mut files := os.ls(path) or { []string{} } + for pf in files { + pf_ext := os.file_ext(pf).to_lower() + if pf_ext in ['', '.bak', '.exe', '.dll', '.so', '.def'] { + continue + } + if pf.starts_with('.#') { + continue + } + if pf.ends_with('~') { + continue + } + f := os.join_path(path, pf) + fullpath := os.real_path(f) + mtime := os.file_last_mod_unix(fullpath) + newstats << VFileStat{fullpath, mtime} + } + } + return newstats +} + +fn (mut context Context) get_changed_vfiles() int { + mut changed := 0 + newfiles := context.get_stats_for_affected_vfiles() + for vfs in newfiles { + mut found := false + for existing_vfs in context.vfiles { + if existing_vfs.path == vfs.path { + found = true + if existing_vfs.mtime != vfs.mtime { + context.elog('> new updates for file: $vfs') + changed++ + } + break + } + } + if !found { + changed++ + continue + } + } + context.vfiles = newfiles + if changed > 0 { + context.elog('> get_changed_vfiles: $changed') + } + return changed +} + +fn change_detection_loop(ocontext &Context) { + mut context := ocontext + for { + if context.v_cycles >= max_v_cycles || context.scan_cycles >= max_scan_cycles { + context.is_exiting = true + context.kill_pgroup() + time.sleep(50 * time.millisecond) + exit(255) + } + if context.is_exiting { + return + } + changes := context.get_changed_vfiles() + if changes > 0 { + context.rerun_channel <- RerunCommand.restart + } + time.sleep(context.check_period_ms * time.millisecond) + context.scan_cycles++ + } +} + +fn (mut context Context) kill_pgroup() { + if context.child_process == 0 { + return + } + if context.child_process.is_alive() { + context.child_process.signal_pgkill() + } + context.child_process.wait() +} + +fn (mut context Context) compilation_runner_loop() { + cmd := '"$context.vexe" ${context.opts.join(' ')}' + _ := <-context.rerun_channel + for { + context.elog('>> loop: v_cycles: $context.v_cycles') + timestamp := time.now().format_ss_milli() + context.child_process = os.new_process(context.vexe) + context.child_process.use_pgroup = true + context.child_process.set_args(context.opts) + context.child_process.run() + eprintln('$timestamp: $cmd | pid: ${context.child_process.pid:7d} | reload cycle: ${context.v_cycles:5d}') + for { + mut cmds := []RerunCommand{} + for { + if context.is_exiting { + return + } + if !context.child_process.is_alive() { + context.child_process.wait() + } + select { + action := <-context.rerun_channel { + cmds << action + if action == .quit { + context.kill_pgroup() + return + } + } + > 100 * time.millisecond { + should_restart := RerunCommand.restart in cmds + cmds = [] + if should_restart { + // context.elog('>>>>>>>> KILLING $context.child_process.pid') + context.kill_pgroup() + break + } + } + } + } + if !context.child_process.is_alive() { + context.child_process.wait() + break + } + } + context.v_cycles++ + } +} + +const ccontext = Context{ + child_process: 0 +} + +fn main() { + mut context := &ccontext + context.pid = os.getpid() + context.vexe = os.getenv('VEXE') + context.is_worker = os.args.contains('-vwatchworker') + context.opts = os.args[1..].filter(it != '-vwatchworker') + context.elog('>>> context.pid: $context.pid') + context.elog('>>> context.vexe: $context.vexe') + context.elog('>>> context.opts: $context.opts') + context.elog('>>> context.is_worker: $context.is_worker') + if context.is_worker { + context.worker_main() + } else { + context.manager_main() + } +} + +fn (mut context Context) manager_main() { + myexecutable := os.executable() + mut worker_opts := ['-vwatchworker'] + worker_opts << context.opts + for { + mut worker_process := os.new_process(myexecutable) + worker_process.set_args(worker_opts) + worker_process.run() + for { + if !worker_process.is_alive() { + worker_process.wait() + break + } + time.sleep(200 * time.millisecond) + } + if !(worker_process.code == 255 && worker_process.status == .exited) { + break + } + } +} + +fn (mut context Context) worker_main() { + context.rerun_channel = chan RerunCommand{cap: 10} + os.signal(C.SIGINT, fn () { + mut context := &ccontext + context.is_exiting = true + context.kill_pgroup() + }) + go context.compilation_runner_loop() + change_detection_loop(context) +} diff --git a/cmd/v/help/default.txt b/cmd/v/help/default.txt index e5d838e75e..80492e9a3c 100644 --- a/cmd/v/help/default.txt +++ b/cmd/v/help/default.txt @@ -8,6 +8,9 @@ Examples: v run hello.v Same as above but also run the produced executable immediately after compilation. v -cg run hello.v Same as above, but make debugging easier (in case your program crashes). v -o h.c hello.v Translate `hello.v` to `h.c`. Do not compile further. + + v -watch hello.v Re-compiles over and over the same compilation, when a source change is detected. + v -watch run file.v Re-runs over and over the same file.v, when a source change is detected. V supports the following commands: * New project scaffolding: diff --git a/cmd/v/v.v b/cmd/v/v.v index ebd648b64c..40bf682d9f 100644 --- a/cmd/v/v.v +++ b/cmd/v/v.v @@ -71,6 +71,15 @@ fn main() { } args_and_flags := util.join_env_vflags_and_os_args()[1..] prefs, command := pref.parse_args(external_tools, args_and_flags) + if prefs.is_watch { + util.launch_tool(prefs.is_verbose, 'vwatch', os.args[1..].filter(it != '-watch')) + } + if prefs.is_verbose { + // println('args= ') + // println(args) // QTODO + // println('prefs= ') + // println(prefs) // QTODO + } if prefs.use_cache && os.user_os() == 'windows' { eprintln('-usecache is currently disabled on windows') exit(1) diff --git a/make.bat b/make.bat index 2680e5efd4..0be61f583d 100644 --- a/make.bat +++ b/make.bat @@ -162,15 +162,7 @@ if !flag_local! NEQ 1 ( cd ..>>"!log_file!" 2>NUL ) popd - ) || ( - echo Cloning vc... - echo ^> Cloning from remote !vc_url! - if !flag_verbose! EQU 1 ( - echo [Debug] git clone --depth 1 --quiet %vc_url%>>"!log_file!" - echo git clone --depth 1 --quiet %vc_url% - ) - git clone --depth 1 --quiet %vc_url%>>"!log_file!" 2>NUL - ) + ) || call :cloning_vc echo. ) @@ -178,7 +170,6 @@ echo Building V... if not [!compiler!] == [] goto :!compiler!_strap - REM By default, use tcc, since we have it prebuilt: :tcc_strap :tcc32_strap @@ -308,8 +299,6 @@ del %ObjFile%>>"!log_file!" 2>>&1 if %ERRORLEVEL% NEQ 0 goto :compile_error goto :success - - :download_tcc pushd %tcc_dir% 2>NUL && ( echo Updating TCC @@ -320,16 +309,8 @@ pushd %tcc_dir% 2>NUL && ( ) git pull --quiet>>"!log_file!" 2>NUL popd -) || ( - echo Bootstraping TCC... - echo ^> TCC not found - if "!tcc_branch!" == "thirdparty-windows-i386" ( echo ^> Downloading TCC32 from !tcc_url! ) else ( echo ^> Downloading TCC64 from !tcc_url! ) - if !flag_verbose! EQU 1 ( - echo [Debug] git clone --depth 1 --quiet --single-branch --branch !tcc_branch! !tcc_url! "%tcc_dir%">>"!log_file!" - echo git clone --depth 1 --quiet --single-branch --branch !tcc_branch! !tcc_url! "%tcc_dir%" - ) - git clone --depth 1 --quiet --single-branch --branch !tcc_branch! !tcc_url! "%tcc_dir%">>"!log_file!" 2>NUL -) +) || call :bootstrap_tcc + for /f "usebackq delims=" %%i in (`dir "%tcc_dir%" /b /a /s tcc.exe`) do ( set "attrib=%%~ai" set "dattrib=%attrib:~0,1%" @@ -426,6 +407,27 @@ echo file echo --verbose Output compilation commands to stdout exit /b 0 +:bootstrap_tcc +echo Bootstraping TCC... +echo ^> TCC not found +if "!tcc_branch!" == "thirdparty-windows-i386" ( echo ^> Downloading TCC32 from !tcc_url! ) else ( echo ^> Downloading TCC64 from !tcc_url! ) +if !flag_verbose! EQU 1 ( + echo [Debug] git clone --depth 1 --quiet --single-branch --branch !tcc_branch! !tcc_url! "%tcc_dir%">>"!log_file!" + echo git clone --depth 1 --quiet --single-branch --branch !tcc_branch! !tcc_url! "%tcc_dir%" +) +git clone --depth 1 --quiet --single-branch --branch !tcc_branch! !tcc_url! "%tcc_dir%">>"!log_file!" 2>NUL +exit /b 0 + +:cloning_vc +echo Cloning vc... +echo ^> Cloning from remote !vc_url! +if !flag_verbose! EQU 1 ( + echo [Debug] git clone --depth 1 --quiet %vc_url%>>"!log_file!" + echo git clone --depth 1 --quiet %vc_url% +) +git clone --depth 1 --quiet %vc_url%>>"!log_file!" 2>NUL +exit /b 0 + :eof popd endlocal diff --git a/vlib/builtin/builtin.c.v b/vlib/builtin/builtin.c.v index a5d71aa96f..8813380364 100644 --- a/vlib/builtin/builtin.c.v +++ b/vlib/builtin/builtin.c.v @@ -177,7 +177,7 @@ pub fn malloc(n int) byteptr { } $if trace_malloc ? { total_m += n - C.fprintf(C.stderr, c'v_malloc %d total %d\n', n, total_m) + C.fprintf(C.stderr, c'v_malloc %6d total %10d\n', n, total_m) // print_backtrace() } mut res := byteptr(0) diff --git a/vlib/builtin/cfns.c.v b/vlib/builtin/cfns.c.v index 02e3380e5a..11cc4ba118 100644 --- a/vlib/builtin/cfns.c.v +++ b/vlib/builtin/cfns.c.v @@ -72,6 +72,7 @@ fn C.fclose(stream &C.FILE) int fn C.pclose(stream &C.FILE) int // process execution, os.process: +[trusted] fn C.getpid() int fn C.system(cmd charptr) int @@ -82,6 +83,12 @@ fn C.posix_spawnp(child_pid &int, exefile charptr, file_actions voidptr, attrp v fn C.execve(cmd_path charptr, args voidptr, envs voidptr) int +fn C.execvp(cmd_path charptr, args &charptr) int + +fn C._execve(cmd_path charptr, args voidptr, envs voidptr) int + +fn C._execvp(cmd_path charptr, args &charptr) int + [trusted] fn C.fork() int @@ -89,6 +96,7 @@ fn C.wait(status &int) int fn C.waitpid(pid int, status &int, options int) int +[trusted] fn C.kill(pid int, sig int) int fn C.setenv(charptr, charptr, int) int @@ -115,12 +123,14 @@ fn C.fgets(str charptr, n int, stream &C.FILE) int fn C.memset(str voidptr, c int, n size_t) int +[trusted] fn C.sigemptyset() int fn C.getcwd(buf charptr, size size_t) charptr fn C.signal(signal int, handlercb voidptr) voidptr +[trusted] fn C.mktime() int fn C.gettimeofday(tv &C.timeval, tz &C.timezone) int @@ -129,6 +139,7 @@ fn C.gettimeofday(tv &C.timeval, tz &C.timezone) int fn C.sleep(seconds u32) u32 // fn C.usleep(usec useconds_t) int +[trusted] fn C.usleep(usec u32) int fn C.opendir(charptr) voidptr @@ -148,8 +159,10 @@ fn C.srand(seed u32) fn C.atof(str charptr) f64 +[trusted] fn C.tolower(c int) int +[trusted] fn C.toupper(c int) int [trusted] @@ -162,20 +175,26 @@ fn C.snprintf(str charptr, size size_t, format charptr, opt ...voidptr) int fn C.fprintf(byteptr, ...byteptr) +[trusted] fn C.WIFEXITED(status int) bool +[trusted] fn C.WEXITSTATUS(status int) int +[trusted] fn C.WIFSIGNALED(status int) bool +[trusted] fn C.WTERMSIG(status int) int +[trusted] fn C.isatty(fd int) int fn C.syscall(number int, va ...voidptr) int fn C.sysctl(name &int, namelen u32, oldp voidptr, oldlenp voidptr, newp voidptr, newlen size_t) int +[trusted] fn C._fileno(int) int fn C._get_osfhandle(fd int) C.intptr_t @@ -196,6 +215,7 @@ fn C.SetHandleInformation(hObject voidptr, dwMask u32, dw_flags u32) bool fn C.ExpandEnvironmentStringsW(lpSrc &u16, lpDst &u16, nSize u32) u32 +[trusted] fn C.SendMessageTimeout() u32 fn C.SendMessageTimeoutW(hWnd voidptr, Msg u32, wParam &u16, lParam &u32, fuFlags u32, uTimeout u32, lpdwResult &u64) u32 @@ -231,6 +251,7 @@ fn C.SetConsoleMode(voidptr, u32) int // fn C.GetConsoleMode() int fn C.GetConsoleMode(voidptr, &u32) int +[trusted] fn C.GetCurrentProcessId() int fn C.wprintf() @@ -280,6 +301,7 @@ fn C._fullpath() int fn C.GetFullPathName(voidptr, u32, voidptr, voidptr) u32 +[trusted] fn C.GetCommandLine() voidptr fn C.LocalFree() @@ -301,8 +323,10 @@ fn C.CloseHandle(voidptr) int fn C.GetExitCodeProcess(hProcess voidptr, lpExitCode &u32) +[trusted] fn C.GetTickCount() i64 +[trusted] fn C.Sleep(dwMilliseconds u32) fn C.WSAStartup(u16, &voidptr) int diff --git a/vlib/os/fd.c.v b/vlib/os/fd.c.v index b10abdfec5..8eeb1a2d33 100644 --- a/vlib/os/fd.c.v +++ b/vlib/os/fd.c.v @@ -4,10 +4,16 @@ module os // close filedescriptor pub fn fd_close(fd int) int { + if fd == -1 { + return 0 + } return C.close(fd) } pub fn fd_write(fd int, s string) { + if fd == -1 { + return + } mut sp := s.str mut remaining := s.len for remaining > 0 { @@ -23,6 +29,9 @@ pub fn fd_write(fd int, s string) { // read from filedescriptor, block until data pub fn fd_slurp(fd int) []string { mut res := []string{} + if fd == -1 { + return res + } for { s, b := fd_read(fd, 4096) if b <= 0 { @@ -36,6 +45,9 @@ pub fn fd_slurp(fd int) []string { // read from filedescriptor, don't block // return [bytestring,nrbytes] pub fn fd_read(fd int, maxbytes int) (string, int) { + if fd == -1 { + return '', 0 + } unsafe { mut buf := malloc(maxbytes) nbytes := C.read(fd, buf, maxbytes) diff --git a/vlib/os/os.v b/vlib/os/os.v index b7ee35e78c..0caa5bb015 100644 --- a/vlib/os/os.v +++ b/vlib/os/os.v @@ -3,13 +3,6 @@ // that can be found in the LICENSE file. module os -pub struct Result { -pub: - exit_code int - output string - // stderr string // TODO -} - pub const ( args = []string{} max_path_len = 4096 @@ -23,6 +16,18 @@ const ( r_ok = 4 ) +pub struct Result { +pub: + exit_code int + output string + // stderr string // TODO +} + +[unsafe] +pub fn (mut result Result) free() { + unsafe { result.output.free() } +} + // cp_all will recursively copy `src` to `dst`, // optionally overwriting files or dirs in `dst`. pub fn cp_all(src string, dst string, overwrite bool) ? { diff --git a/vlib/os/os_c.v b/vlib/os/os_c.v index ee3782702b..77d6f33337 100644 --- a/vlib/os/os_c.v +++ b/vlib/os/os_c.v @@ -15,7 +15,7 @@ fn C.getline(voidptr, voidptr, voidptr) int fn C.ftell(fp voidptr) int -fn C.sigaction(int, voidptr, int) +fn C.sigaction(int, voidptr, int) int fn C.open(charptr, int, ...int) int @@ -23,7 +23,7 @@ fn C.fdopen(fd int, mode charptr) &C.FILE fn C.CopyFile(&u32, &u32, int) int -fn C.execvp(file charptr, argv &charptr) int +// fn C.lstat(charptr, voidptr) u64 fn C._wstat64(charptr, voidptr) u64 @@ -43,11 +43,14 @@ struct C.__stat64 { struct C.DIR { } +type FN_SA_Handler = fn (sig int) + struct C.sigaction { mut: sa_mask int sa_sigaction int sa_flags int + sa_handler FN_SA_Handler } struct C.dirent { @@ -758,9 +761,10 @@ fn normalize_drive_letter(path string) string { return path } -// signal will assign `handler` callback to be called when `signum` signal is recieved. -pub fn signal(signum int, handler voidptr) { - unsafe { C.signal(signum, handler) } +// signal will assign `handler` callback to be called when `signum` signal is received. +pub fn signal(signum int, handler voidptr) voidptr { + res := unsafe { C.signal(signum, handler) } + return res } // fork will fork the current system process and return the pid of the fork. @@ -843,10 +847,17 @@ pub fn execvp(cmdpath string, args []string) ? { cargs << charptr(args[i].str) } cargs << charptr(0) - res := C.execvp(charptr(cmdpath.str), cargs.data) + mut res := int(0) + $if windows { + res = C._execvp(charptr(cmdpath.str), cargs.data) + } $else { + res = C.execvp(charptr(cmdpath.str), cargs.data) + } if res == -1 { return error_with_code(posix_get_error_msg(C.errno), C.errno) } + // just in case C._execvp returned ... that happens on windows ... + exit(res) } // execve - loads and executes a new child process, *in place* of the current process. @@ -867,7 +878,12 @@ pub fn execve(cmdpath string, args []string, envs []string) ? { } cargv << charptr(0) cenvs << charptr(0) - res := C.execve(charptr(cmdpath.str), cargv.data, cenvs.data) + mut res := int(0) + $if windows { + res = C._execve(charptr(cmdpath.str), cargv.data, cenvs.data) + } $else { + res = C.execve(charptr(cmdpath.str), cargv.data, cenvs.data) + } // NB: normally execve does not return at all. // If it returns, then something went wrong... if res == -1 { diff --git a/vlib/os/os_windows.c.v b/vlib/os/os_windows.c.v index b0c9bf4da0..a7573eeca7 100644 --- a/vlib/os/os_windows.c.v +++ b/vlib/os/os_windows.c.v @@ -12,6 +12,7 @@ pub const ( // Ref - https://docs.microsoft.com/en-us/windows/desktop/winprog/windows-data-types // A handle to an object. pub type HANDLE = voidptr +pub type HMODULE = voidptr // win: FILETIME // https://docs.microsoft.com/en-us/windows/win32/api/minwinbase/ns-minwinbase-filetime diff --git a/vlib/os/process.v b/vlib/os/process.v index b7c2fddead..a9df41a1b2 100644 --- a/vlib/os/process.v +++ b/vlib/os/process.v @@ -28,7 +28,9 @@ pub mut: 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() - stdio_fd [3]int // the file descriptors + use_pgroup bool // when true, the process will create a new process group, enabling .signal_pgkill() + stdio_fd [3]int // the stdio file descriptors for the child process, used only by the nix implementation + wdata voidptr // the WProcess; used only by the windows implementation } // new_process - create a new process descriptor @@ -39,6 +41,7 @@ pub mut: pub fn new_process(filename string) &Process { return &Process{ filename: filename + stdio_fd: [-1, -1, -1]! } } @@ -83,6 +86,15 @@ pub fn (mut p Process) signal_kill() { return } +// signal_pgkill - kills the whole process group +pub fn (mut p Process) signal_pgkill() { + if p.status !in [.running, .stopped] { + return + } + p._signal_pgkill() + return +} + // signal_stop - stops the process, you can resume it with p.signal_continue() pub fn (mut p Process) signal_stop() { if p.status != .running { @@ -159,33 +171,55 @@ pub fn (mut p Process) set_redirect_stdio() { pub fn (mut p Process) stdin_write(s string) { p._check_redirection_call('stdin_write') - fd_write(p.stdio_fd[0], s) + $if windows { + p.win_write_string(0, s) + } $else { + fd_write(p.stdio_fd[0], s) + } } // will read from stdout pipe, will only return when EOF (end of file) or data // means this will block unless there is data pub fn (mut p Process) stdout_slurp() string { p._check_redirection_call('stdout_slurp') - return fd_slurp(p.stdio_fd[1]).join('') + $if windows { + return p.win_slurp(1) + } $else { + return fd_slurp(p.stdio_fd[1]).join('') + } } // read from stderr pipe, wait for data or EOF pub fn (mut p Process) stderr_slurp() string { p._check_redirection_call('stderr_slurp') - return fd_slurp(p.stdio_fd[2]).join('') + $if windows { + return p.win_slurp(2) + } $else { + return fd_slurp(p.stdio_fd[2]).join('') + } } // read from stdout, return if data or not pub fn (mut p Process) stdout_read() string { p._check_redirection_call('stdout_read') - s, _ := fd_read(p.stdio_fd[1], 4096) - return s + $if windows { + s, _ := p.win_read_string(1, 4096) + return s + } $else { + s, _ := fd_read(p.stdio_fd[1], 4096) + return s + } } pub fn (mut p Process) stderr_read() string { p._check_redirection_call('stderr_read') - s, _ := fd_read(p.stdio_fd[2], 4096) - return s + $if windows { + s, _ := p.win_read_string(2, 4096) + return s + } $else { + s, _ := fd_read(p.stdio_fd[2], 4096) + return s + } } // _check_redirection_call - should be called just by stdxxx methods @@ -225,6 +259,15 @@ fn (mut p Process) _signal_kill() { } } +// _signal_pgkill - should not be called directly, except by p.signal_pgkill +fn (mut p Process) _signal_pgkill() { + $if windows { + p.win_kill_pgroup() + } $else { + p.unix_kill_pgroup() + } +} + // _wait - should not be called directly, except by p.wait() fn (mut p Process) _wait() { $if windows { diff --git a/vlib/os/process_nix.c.v b/vlib/os/process_nix.c.v index f54e24e0e7..02e7c8808e 100644 --- a/vlib/os/process_nix.c.v +++ b/vlib/os/process_nix.c.v @@ -1,5 +1,7 @@ module os +fn C.setpgid(pid int, pgid int) int + fn (mut p Process) unix_spawn_process() int { mut pipeset := [6]int{} if p.use_stdio_ctl { @@ -27,6 +29,10 @@ fn (mut p Process) unix_spawn_process() int { // It still shares file descriptors with the parent process, // but it is otherwise independant and can do stuff *without* // affecting the parent process. + // + if p.use_pgroup { + C.setpgid(0, 0) + } if p.use_stdio_ctl { // Redirect the child standart in/out/err to the pipes that // were created in the parent. @@ -62,6 +68,10 @@ fn (mut p Process) unix_kill_process() { C.kill(p.pid, C.SIGKILL) } +fn (mut p Process) unix_kill_pgroup() { + C.kill(-p.pid, C.SIGKILL) +} + fn (mut p Process) unix_wait() { cstatus := 0 ret := C.waitpid(p.pid, &cstatus, 0) @@ -114,9 +124,23 @@ fn (mut p Process) win_resume_process() { fn (mut p Process) win_kill_process() { } +fn (mut p Process) win_kill_pgroup() { +} + fn (mut p Process) win_wait() { } fn (mut p Process) win_is_alive() bool { return false } + +fn (mut p Process) win_write_string(idx int, s string) { +} + +fn (mut p Process) win_read_string(idx int, maxbytes int) (string, int) { + return '', 0 +} + +fn (mut p Process) win_slurp(idx int) string { + return '' +} diff --git a/vlib/os/process_test.v b/vlib/os/process_test.v index ca11d2ae61..7250d778ed 100644 --- a/vlib/os/process_test.v +++ b/vlib/os/process_test.v @@ -1,17 +1,25 @@ import os import time -const vexe = os.getenv('VEXE') +const ( + vexe = os.getenv('VEXE') + vroot = os.dir(vexe) + test_os_process = os.join_path(os.temp_dir(), 'v', 'test_os_process.exe') + test_os_process_source = os.join_path(vroot, 'cmd/tools/test_os_process.v') +) -const vroot = os.dir(vexe) - -const test_os_process = os.join_path(os.temp_dir(), 'v', 'test_os_process.exe') - -const test_os_process_source = os.join_path(vroot, 'cmd/tools/test_os_process.v') - -fn testsuite_begin() { +fn testsuite_begin() ? { os.rm(test_os_process) or {} - assert os.system('$vexe -o $test_os_process $test_os_process_source') == 0 + if os.getenv('WINE_TEST_OS_PROCESS_EXE') != '' { + // Make it easier to run the test under wine emulation, by just + // prebuilding the executable with: + // v -os windows -o x.exe cmd/tools/test_os_process.v + // WINE_TEST_OS_PROCESS_EXE=x.exe ./v -os windows vlib/os/process_test.v + os.cp(os.getenv('WINE_TEST_OS_PROCESS_EXE'), test_os_process) ? + } else { + os.system('$vexe -o $test_os_process $test_os_process_source') + } + assert os.exists(test_os_process) } fn test_getpid() { @@ -21,10 +29,6 @@ fn test_getpid() { } fn test_run() { - if os.user_os() == 'windows' { - return - } - // mut p := os.new_process(test_os_process) p.set_args(['-timeout_ms', '150', '-period_ms', '50']) p.run() @@ -51,9 +55,6 @@ fn test_run() { } fn test_wait() { - if os.user_os() == 'windows' { - return - } mut p := os.new_process(test_os_process) assert p.status != .exited p.wait() @@ -63,9 +64,6 @@ fn test_wait() { } fn test_slurping_output() { - if os.user_os() == 'windows' { - return - } mut p := os.new_process(test_os_process) p.set_args(['-timeout_ms', '500', '-period_ms', '50']) p.set_redirect_stdio() @@ -81,10 +79,13 @@ fn test_slurping_output() { eprintln('p errors: "$errors"') eprintln('---------------------------') } + // dump(output) assert output.contains('stdout, 1') assert output.contains('stdout, 2') assert output.contains('stdout, 3') assert output.contains('stdout, 4') + // + // dump(errors) assert errors.contains('stderr, 1') assert errors.contains('stderr, 2') assert errors.contains('stderr, 3') diff --git a/vlib/os/process_windows.c.v b/vlib/os/process_windows.c.v index 55d2a8c1f1..47f359de78 100644 --- a/vlib/os/process_windows.c.v +++ b/vlib/os/process_windows.c.v @@ -1,33 +1,221 @@ module os +import strings + +fn C.GenerateConsoleCtrlEvent(event u32, pgid u32) bool +fn C.GetModuleHandleA(name charptr) HMODULE +fn C.GetProcAddress(handle voidptr, procname byteptr) voidptr +fn C.TerminateProcess(process HANDLE, exit_code u32) bool + +type FN_NTSuspendResume = fn (voidptr) + +fn ntdll_fn(name charptr) FN_NTSuspendResume { + ntdll := C.GetModuleHandleA(c'NTDLL') + if ntdll == 0 { + return FN_NTSuspendResume(0) + } + the_fn := FN_NTSuspendResume(C.GetProcAddress(ntdll, name)) + return the_fn +} + +fn failed_cfn_report_error(ok bool, label string) { + if ok { + return + } + error_num := int(C.GetLastError()) + error_msg := get_error_msg(error_num) + eprintln('failed $label: $error_msg') + exit(1) +} + +type PU32 = &u32 + +// TODO: the PU32 alias is used to compensate for the wrong number of &/* +// that V does when doing: `h := &&u32(p)`, which should have casted +// p to a double pointer. +fn close_valid_handle(p voidptr) { + h := &PU32(p) + if *h != &u32(0) { + C.CloseHandle(*h) + unsafe { + *h = &u32(0) + } + } +} + +pub struct WProcess { +pub mut: + proc_info ProcessInformation + command_line [65536]byte + child_stdin &u32 + // + child_stdout_read &u32 + child_stdout_write &u32 + // + child_stderr_read &u32 + child_stderr_write &u32 +} + fn (mut p Process) win_spawn_process() int { - eprintln('TODO implement waiting for a process on windows') - return 12345 + mut wdata := &WProcess{ + child_stdin: 0 + child_stdout_read: 0 + child_stdout_write: 0 + child_stderr_read: 0 + child_stderr_write: 0 + } + p.wdata = voidptr(wdata) + mut start_info := StartupInfo{ + lp_reserved: 0 + lp_desktop: 0 + lp_title: 0 + cb: sizeof(C.PROCESS_INFORMATION) + } + if p.use_stdio_ctl { + mut sa := SecurityAttributes{} + sa.n_length = sizeof(C.SECURITY_ATTRIBUTES) + sa.b_inherit_handle = true + create_pipe_ok1 := C.CreatePipe(voidptr(&wdata.child_stdout_read), voidptr(&wdata.child_stdout_write), + voidptr(&sa), 0) + failed_cfn_report_error(create_pipe_ok1, 'CreatePipe stdout') + set_handle_info_ok1 := C.SetHandleInformation(wdata.child_stdout_read, C.HANDLE_FLAG_INHERIT, + 0) + failed_cfn_report_error(set_handle_info_ok1, 'SetHandleInformation') + create_pipe_ok2 := C.CreatePipe(voidptr(&wdata.child_stderr_read), voidptr(&wdata.child_stderr_write), + voidptr(&sa), 0) + failed_cfn_report_error(create_pipe_ok2, 'CreatePipe stderr') + set_handle_info_ok2 := C.SetHandleInformation(wdata.child_stderr_read, C.HANDLE_FLAG_INHERIT, + 0) + failed_cfn_report_error(set_handle_info_ok2, 'SetHandleInformation stderr') + start_info.h_std_input = wdata.child_stdin + start_info.h_std_output = wdata.child_stdout_write + start_info.h_std_error = wdata.child_stderr_write + 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) + + mut creation_flags := int(C.NORMAL_PRIORITY_CLASS) + if p.use_pgroup { + creation_flags |= C.CREATE_NEW_PROCESS_GROUP + } + 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)) + failed_cfn_report_error(create_process_ok, 'CreateProcess') + if p.use_stdio_ctl { + close_valid_handle(&wdata.child_stdout_write) + close_valid_handle(&wdata.child_stderr_write) + } + p.pid = int(wdata.proc_info.dw_process_id) + return p.pid } fn (mut p Process) win_stop_process() { - eprintln('TODO implement stopping a process on windows') + the_fn := ntdll_fn(c'NtSuspendProcess') + if voidptr(the_fn) == 0 { + return + } + wdata := &WProcess(p.wdata) + the_fn(wdata.proc_info.h_process) } fn (mut p Process) win_resume_process() { - eprintln('TODO implement resuming a process on windows') + the_fn := ntdll_fn(c'NtResumeProcess') + if voidptr(the_fn) == 0 { + return + } + wdata := &WProcess(p.wdata) + the_fn(wdata.proc_info.h_process) } fn (mut p Process) win_kill_process() { - eprintln('TODO implement killing a process on windows') + wdata := &WProcess(p.wdata) + C.TerminateProcess(wdata.proc_info.h_process, 3) +} + +fn (mut p Process) win_kill_pgroup() { + wdata := &WProcess(p.wdata) + C.GenerateConsoleCtrlEvent(C.CTRL_BREAK_EVENT, wdata.proc_info.dw_process_id) + C.Sleep(20) + C.TerminateProcess(wdata.proc_info.h_process, 3) } fn (mut p Process) win_wait() { - eprintln('TODO implement waiting for a process on windows') + exit_code := u32(1) + mut wdata := &WProcess(p.wdata) + if p.wdata != 0 { + C.WaitForSingleObject(wdata.proc_info.h_process, C.INFINITE) + C.GetExitCodeProcess(wdata.proc_info.h_process, voidptr(&exit_code)) + close_valid_handle(&wdata.child_stdin) + close_valid_handle(&wdata.child_stdout_write) + close_valid_handle(&wdata.child_stderr_write) + close_valid_handle(&wdata.proc_info.h_process) + close_valid_handle(&wdata.proc_info.h_thread) + } p.status = .exited - p.code = 0 + p.code = int(exit_code) } fn (mut p Process) win_is_alive() bool { - eprintln('TODO implement checking whether the process is still alive on windows') + exit_code := u32(0) + wdata := &WProcess(p.wdata) + C.GetExitCodeProcess(wdata.proc_info.h_process, voidptr(&exit_code)) + if exit_code == C.STILL_ACTIVE { + return true + } return false } +/////////////// + +fn (mut p Process) win_write_string(idx int, s string) { + panic('Process.write_string $idx is not implemented yet') +} + +fn (mut p Process) win_read_string(idx int, maxbytes int) (string, int) { + panic('WProcess.read_string $idx is not implemented yet') + return '', 0 +} + +fn (mut p Process) win_slurp(idx int) string { + mut wdata := &WProcess(p.wdata) + if wdata == 0 { + return '' + } + mut rhandle := &u32(0) + if idx == 1 { + rhandle = wdata.child_stdout_read + } + if idx == 2 { + rhandle = wdata.child_stderr_read + } + if rhandle == 0 { + return '' + } + mut bytes_read := u32(0) + buf := [4096]byte{} + mut read_data := strings.new_builder(1024) + for { + mut result := false + unsafe { + result = C.ReadFile(rhandle, &buf[0], 1000, voidptr(&bytes_read), 0) + read_data.write_ptr(&buf[0], int(bytes_read)) + } + if result == false || int(bytes_read) == 0 { + break + } + } + soutput := read_data.str() + unsafe { read_data.free() } + if idx == 1 { + close_valid_handle(&wdata.child_stdout_read) + } + if idx == 2 { + close_valid_handle(&wdata.child_stderr_read) + } + return soutput +} + // // these are here to make v_win.c/v.c generation work in all cases: fn (mut p Process) unix_spawn_process() int { @@ -43,6 +231,9 @@ fn (mut p Process) unix_resume_process() { fn (mut p Process) unix_kill_process() { } +fn (mut p Process) unix_kill_pgroup() { +} + fn (mut p Process) unix_wait() { } diff --git a/vlib/v/pref/pref.v b/vlib/v/pref/pref.v index 0e41352fff..8d66d6d59a 100644 --- a/vlib/v/pref/pref.v +++ b/vlib/v/pref/pref.v @@ -76,6 +76,7 @@ pub mut: output_mode OutputMode = .stdout // verbosity VerboseLevel is_verbose bool + is_watch bool // -watch mode, implemented by cmd/tools/watch.v // nofmt bool // disable vfmt is_test bool // `v test string_test.v` is_script bool // single file mode (`v program.v`), main function can be skipped @@ -391,6 +392,9 @@ pub fn parse_args(known_external_commands []string, args []string) (&Preferences '-w' { res.skip_warnings = true } + '-watch' { + res.is_watch = true + } '-print-v-files' { res.print_v_files = true } diff --git a/vlib/v/util/util.v b/vlib/v/util/util.v index a33bae70d9..2251424e33 100644 --- a/vlib/v/util/util.v +++ b/vlib/v/util/util.v @@ -215,13 +215,12 @@ pub fn launch_tool(is_verbose bool, tool_name string, args []string) { tool_exe = path_of_executable(tool_basename) tool_source = tool_basename + '.v' } - tool_command := '"$tool_exe" $tool_args' if is_verbose { println('launch_tool vexe : $vroot') println('launch_tool vroot : $vroot') - println('launch_tool tool_args : $tool_args') println('launch_tool tool_source : $tool_source') - println('launch_tool tool_command: $tool_command') + println('launch_tool tool_exe : $tool_exe') + println('launch_tool tool_args : $tool_args') } disabling_file := recompilation.disabling_file(vroot) is_recompilation_disabled := os.exists(disabling_file) @@ -254,10 +253,11 @@ pub fn launch_tool(is_verbose bool, tool_name string, args []string) { exit(1) } } - if is_verbose { - println('launch_tool running tool command: $tool_command ...') + $if windows { + exit(os.system('"$tool_exe" $tool_args')) + } $else { + os.execvp(tool_exe, args) or { panic(err) } } - exit(os.system(tool_command)) } // NB: should_recompile_tool/4 compares unix timestamps that have 1 second resolution