From f5c4671bfbad96bf346bd7e9a21fc4317b4959df Mon Sep 17 00:00:00 2001 From: Indrajith K L Date: Sat, 3 Dec 2022 17:00:20 +0530 Subject: Adds most of the tools --- v_windows/v/vlib/os/args.v | 51 ++ v_windows/v/vlib/os/bare/bare_example_linux.v | 8 + v_windows/v/vlib/os/cmdline/cmdline.v | 82 ++ v_windows/v/vlib/os/cmdline/cmdline_test.v | 37 + v_windows/v/vlib/os/const.v | 1 + v_windows/v/vlib/os/const_nix.c.v | 16 + v_windows/v/vlib/os/const_windows.c.v | 161 ++++ v_windows/v/vlib/os/environment.c.v | 108 +++ v_windows/v/vlib/os/environment.js.v | 37 + v_windows/v/vlib/os/environment_test.v | 49 ++ v_windows/v/vlib/os/fd.c.v | 61 ++ v_windows/v/vlib/os/file.c.v | 787 ++++++++++++++++++ v_windows/v/vlib/os/file.js.v | 136 ++++ v_windows/v/vlib/os/file_test.v | 372 +++++++++ v_windows/v/vlib/os/filelock/filelock_test.v | 27 + v_windows/v/vlib/os/filelock/lib.v | 14 + v_windows/v/vlib/os/filelock/lib_nix.c.v | 82 ++ v_windows/v/vlib/os/filelock/lib_windows.c.v | 75 ++ v_windows/v/vlib/os/glob_test.v | 80 ++ v_windows/v/vlib/os/inode.c.v | 92 +++ v_windows/v/vlib/os/inode_test.v | 43 + v_windows/v/vlib/os/notify/backend_default.c.v | 6 + v_windows/v/vlib/os/notify/backend_linux.c.v | 206 +++++ v_windows/v/vlib/os/notify/notify.v | 35 + v_windows/v/vlib/os/notify/notify_test.v | 155 ++++ v_windows/v/vlib/os/os.c.v | 1010 ++++++++++++++++++++++++ v_windows/v/vlib/os/os.js.v | 97 +++ v_windows/v/vlib/os/os.v | 633 +++++++++++++++ v_windows/v/vlib/os/os_android.c.v | 39 + v_windows/v/vlib/os/os_darwin.c.v | 18 + v_windows/v/vlib/os/os_darwin.m | 7 + v_windows/v/vlib/os/os_js.js.v | 127 +++ v_windows/v/vlib/os/os_linux.c.v | 19 + v_windows/v/vlib/os/os_nix.c.v | 549 +++++++++++++ v_windows/v/vlib/os/os_test.v | 752 ++++++++++++++++++ v_windows/v/vlib/os/os_windows.c.v | 544 +++++++++++++ v_windows/v/vlib/os/process.c.v | 248 ++++++ v_windows/v/vlib/os/process.js.v | 117 +++ v_windows/v/vlib/os/process.v | 70 ++ v_windows/v/vlib/os/process_nix.c.v | 146 ++++ v_windows/v/vlib/os/process_test.v | 96 +++ v_windows/v/vlib/os/process_windows.c.v | 243 ++++++ v_windows/v/vlib/os/signal.c.v | 58 ++ v_windows/v/vlib/os/signal_test.v | 35 + 44 files changed, 7529 insertions(+) create mode 100644 v_windows/v/vlib/os/args.v create mode 100644 v_windows/v/vlib/os/bare/bare_example_linux.v create mode 100644 v_windows/v/vlib/os/cmdline/cmdline.v create mode 100644 v_windows/v/vlib/os/cmdline/cmdline_test.v create mode 100644 v_windows/v/vlib/os/const.v create mode 100644 v_windows/v/vlib/os/const_nix.c.v create mode 100644 v_windows/v/vlib/os/const_windows.c.v create mode 100644 v_windows/v/vlib/os/environment.c.v create mode 100644 v_windows/v/vlib/os/environment.js.v create mode 100644 v_windows/v/vlib/os/environment_test.v create mode 100644 v_windows/v/vlib/os/fd.c.v create mode 100644 v_windows/v/vlib/os/file.c.v create mode 100644 v_windows/v/vlib/os/file.js.v create mode 100644 v_windows/v/vlib/os/file_test.v create mode 100644 v_windows/v/vlib/os/filelock/filelock_test.v create mode 100644 v_windows/v/vlib/os/filelock/lib.v create mode 100644 v_windows/v/vlib/os/filelock/lib_nix.c.v create mode 100644 v_windows/v/vlib/os/filelock/lib_windows.c.v create mode 100644 v_windows/v/vlib/os/glob_test.v create mode 100644 v_windows/v/vlib/os/inode.c.v create mode 100644 v_windows/v/vlib/os/inode_test.v create mode 100644 v_windows/v/vlib/os/notify/backend_default.c.v create mode 100644 v_windows/v/vlib/os/notify/backend_linux.c.v create mode 100644 v_windows/v/vlib/os/notify/notify.v create mode 100644 v_windows/v/vlib/os/notify/notify_test.v create mode 100644 v_windows/v/vlib/os/os.c.v create mode 100644 v_windows/v/vlib/os/os.js.v create mode 100644 v_windows/v/vlib/os/os.v create mode 100644 v_windows/v/vlib/os/os_android.c.v create mode 100644 v_windows/v/vlib/os/os_darwin.c.v create mode 100644 v_windows/v/vlib/os/os_darwin.m create mode 100644 v_windows/v/vlib/os/os_js.js.v create mode 100644 v_windows/v/vlib/os/os_linux.c.v create mode 100644 v_windows/v/vlib/os/os_nix.c.v create mode 100644 v_windows/v/vlib/os/os_test.v create mode 100644 v_windows/v/vlib/os/os_windows.c.v create mode 100644 v_windows/v/vlib/os/process.c.v create mode 100644 v_windows/v/vlib/os/process.js.v create mode 100644 v_windows/v/vlib/os/process.v create mode 100644 v_windows/v/vlib/os/process_nix.c.v create mode 100644 v_windows/v/vlib/os/process_test.v create mode 100644 v_windows/v/vlib/os/process_windows.c.v create mode 100644 v_windows/v/vlib/os/signal.c.v create mode 100644 v_windows/v/vlib/os/signal_test.v (limited to 'v_windows/v/vlib/os') diff --git a/v_windows/v/vlib/os/args.v b/v_windows/v/vlib/os/args.v new file mode 100644 index 0000000..597637c --- /dev/null +++ b/v_windows/v/vlib/os/args.v @@ -0,0 +1,51 @@ +// Copyright (c) 2019-2021 Alexander Medvednikov. All rights reserved. +// Use of this source code is governed by an MIT license +// that can be found in the LICENSE file. +module os + +// args_after returns all os.args, located *after* a specified `cut_word`. +// When `cut_word` is NOT found, os.args is returned unmodified. +pub fn args_after(cut_word string) []string { + if args.len == 0 { + return []string{} + } + mut cargs := []string{} + if cut_word !in args { + cargs = args.clone() + } else { + mut found := false + cargs << args[0] + for a in args[1..] { + if a == cut_word { + found = true + continue + } + if !found { + continue + } + cargs << a + } + } + return cargs +} + +// args_after returns all os.args, located *before* a specified `cut_word`. +// When `cut_word` is NOT found, os.args is returned unmodified. +pub fn args_before(cut_word string) []string { + if args.len == 0 { + return []string{} + } + mut cargs := []string{} + if cut_word !in args { + cargs = args.clone() + } else { + cargs << args[0] + for a in args[1..] { + if a == cut_word { + break + } + cargs << a + } + } + return cargs +} diff --git a/v_windows/v/vlib/os/bare/bare_example_linux.v b/v_windows/v/vlib/os/bare/bare_example_linux.v new file mode 100644 index 0000000..0aa92dd --- /dev/null +++ b/v_windows/v/vlib/os/bare/bare_example_linux.v @@ -0,0 +1,8 @@ +fn main() { + sys_write(1, 'hello\n'.str, 6) + s := 'test string\n' + sys_write(1, s.str, u64(s.len)) + a := s[0] + println('Hello freestanding!') + println(a) +} diff --git a/v_windows/v/vlib/os/cmdline/cmdline.v b/v_windows/v/vlib/os/cmdline/cmdline.v new file mode 100644 index 0000000..96bcb27 --- /dev/null +++ b/v_windows/v/vlib/os/cmdline/cmdline.v @@ -0,0 +1,82 @@ +module cmdline + +// Fetch multiple option by param, e.g. +// args: ['v', '-d', 'aa', '-d', 'bb', '-d', 'cc'] +// param: '-d' +// ret: ['aa', 'bb', 'cc'] +pub fn options(args []string, param string) []string { + mut flags := []string{} + for i, v in args { + if v == param { + if i + 1 < args.len { + flags << args[i + 1] + } + } + } + return flags +} + +// Fetch option by param, e.g. +// args: ['v', '-d', 'aa'] +// param: '-d' +// def: '' +// ret: 'aa' +pub fn option(args []string, param string, def string) string { + mut found := false + for arg in args { + if found { + return arg + } else if param == arg { + found = true + } + } + return def +} + +// Fetch all options before what params, e.g. +// args: ['-stat', 'test', 'aaa.v'] +// what: ['test'] +// ret: ['-stat'] +pub fn options_before(args []string, what []string) []string { + mut args_before := []string{} + for a in args { + if a in what { + break + } + args_before << a + } + return args_before +} + +// Fetch all options after what params, e.g. +// args: ['-stat', 'test', 'aaa.v'] +// what: ['test'] +// ret: ['aaa.v'] +pub fn options_after(args []string, what []string) []string { + mut found := false + mut args_after := []string{} + for a in args { + if a in what { + found = true + continue + } + if found { + args_after << a + } + } + return args_after +} + +// Fetch all options not start with '-', e.g. +// args: ['-d', 'aa', '--help', 'bb'] +// ret: ['aa', 'bb'] +pub fn only_non_options(args []string) []string { + return args.filter(!it.starts_with('-')) +} + +// Fetch all options start with '-', e.g. +// args: ['-d', 'aa', '--help', 'bb'] +// ret: ['-d', '--help'] +pub fn only_options(args []string) []string { + return args.filter(it.starts_with('-')) +} diff --git a/v_windows/v/vlib/os/cmdline/cmdline_test.v b/v_windows/v/vlib/os/cmdline/cmdline_test.v new file mode 100644 index 0000000..50c99e2 --- /dev/null +++ b/v_windows/v/vlib/os/cmdline/cmdline_test.v @@ -0,0 +1,37 @@ +import os.cmdline + +fn test_options() { + args := ['v', '-d', 'aa', '-d', 'bb', '-d', 'cc'] + ret := cmdline.options(args, '-d') + assert ret == ['aa', 'bb', 'cc'] +} + +fn test_option() { + args := ['v', '-d', 'aa'] + ret := cmdline.option(args, '-d', '') + assert ret == 'aa' +} + +fn test_options_before() { + args := ['-stat', 'test', 'aaa.v'] + ret := cmdline.options_before(args, ['test']) + assert ret == ['-stat'] +} + +fn test_options_after() { + args := ['-stat', 'test', 'aaa.v'] + ret := cmdline.options_after(args, ['test']) + assert ret == ['aaa.v'] +} + +fn test_only_non_options() { + args := ['-d', 'aa', '--help', 'bb'] + ret := cmdline.only_non_options(args) + assert ret == ['aa', 'bb'] +} + +fn test_only_options() { + args := ['-d', 'aa', '--help', 'bb'] + ret := cmdline.only_options(args) + assert ret == ['-d', '--help'] +} diff --git a/v_windows/v/vlib/os/const.v b/v_windows/v/vlib/os/const.v new file mode 100644 index 0000000..bcf59cf --- /dev/null +++ b/v_windows/v/vlib/os/const.v @@ -0,0 +1 @@ +module os diff --git a/v_windows/v/vlib/os/const_nix.c.v b/v_windows/v/vlib/os/const_nix.c.v new file mode 100644 index 0000000..275df70 --- /dev/null +++ b/v_windows/v/vlib/os/const_nix.c.v @@ -0,0 +1,16 @@ +module os + +// File modes +const ( + o_rdonly = 0o00000000 // open the file read-only. + o_wronly = 0o00000001 // open the file write-only. + o_rdwr = 0o00000002 // open the file read-write. + o_binary = 0o00000000 // input and output is not translated; the default on unix + o_create = 0o00000100 // create a new file if none exists. + o_excl = 0o00000200 // used with o_create, file must not exist. + o_noctty = 0o00000400 // if file is terminal, don't make it the controller terminal + o_trunc = 0o00001000 // truncate regular writable file when opened. + o_append = 0o00002000 // append data to the file when writing. + o_nonblock = 0o00004000 // prevents blocking when opening files + o_sync = 0o04010000 // open for synchronous I/O. +) diff --git a/v_windows/v/vlib/os/const_windows.c.v b/v_windows/v/vlib/os/const_windows.c.v new file mode 100644 index 0000000..4b87c8b --- /dev/null +++ b/v_windows/v/vlib/os/const_windows.c.v @@ -0,0 +1,161 @@ +module os + +// Ref - winnt.h +const ( + success = 0x0000 // ERROR_SUCCESS + error_insufficient_buffer = 0x0082 +) + +const ( + handle_generic_read = 0x80000000 + handle_open_existing = 0x00000003 +) + +const ( + file_share_read = 0x01 + file_share_write = 0x02 + file_share_delete = 0x04 +) + +const ( + file_notify_change_file_name = 0x01 + file_notify_change_dir_name = 0x02 + file_notify_change_attributes = 0x04 + file_notify_change_size = 0x08 + file_notify_change_last_write = 0x10 + file_notify_change_last_access = 0x20 + file_notify_change_creation = 0x40 + file_notify_change_security = 0x80 +) + +const ( + file_action_added = 0x01 + file_action_removed = 0x02 + file_action_modified = 0x03 + file_action_renamed_old_name = 0x04 + file_action_renamed_new_name = 0x05 +) + +const ( + file_attr_readonly = 0x00000001 + file_attr_hidden = 0x00000002 + file_attr_system = 0x00000004 + file_attr_directory = 0x00000010 + file_attr_archive = 0x00000020 + file_attr_device = 0x00000040 + file_attr_normal = 0x00000080 + file_attr_temporary = 0x00000100 + file_attr_sparse_file = 0x00000200 + file_attr_reparse_point = 0x00000400 + file_attr_compressed = 0x00000800 + file_attr_offline = 0x00001000 + file_attr_not_content_indexed = 0x00002000 + file_attr_encrypted = 0x00004000 + file_attr_integrity_stream = 0x00008000 + file_attr_virtual = 0x00010000 + file_attr_no_scrub_data = 0x00020000 + // file_attr_recall_on_open = u32(0x...) + // file_attr_recall_on_data_access = u32(0x...) +) + +const ( + file_type_unknown = 0x00 + file_type_disk = 0x01 + file_type_char = 0x02 + file_type_pipe = 0x03 +) + +const ( + file_invalid_file_id = (-1) +) + +const ( + invalid_handle_value = voidptr(-1) +) + +// https://docs.microsoft.com/en-us/windows/console/setconsolemode +const ( + // Input Buffer + enable_echo_input = 0x0004 + enable_extended_flags = 0x0080 + enable_insert_mode = 0x0020 + enable_line_input = 0x0002 + enable_mouse_input = 0x0010 + enable_processed_input = 0x0001 + enable_quick_edit_mode = 0x0040 + enable_window_input = 0x0008 + enable_virtual_terminal_input = 0x0200 + // Output Screen Buffer + enable_processed_output = 0x01 + enable_wrap_at_eol_output = 0x02 + enable_virtual_terminal_processing = 0x04 + disable_newline_auto_return = 0x08 + enable_lvb_grid_worldwide = 0x10 +) + +// File modes +const ( + o_rdonly = 0x0000 // open the file read-only. + o_wronly = 0x0001 // open the file write-only. + o_rdwr = 0x0002 // open the file read-write. + o_append = 0x0008 // append data to the file when writing. + o_create = 0x0100 // create a new file if none exists. + o_binary = 0x8000 // input and output is not translated. + o_trunc = 0x0200 // truncate regular writable file when opened. + o_excl = 0x0400 // used with o_create, file must not exist. + o_sync = 0x0000 // open for synchronous I/O (ignored on Windows) + o_noctty = 0x0000 // make file non-controlling tty (ignored on Windows) + o_nonblock = 0x0000 // don't block on opening file (ignored on Windows) +) + +const ( + status_access_violation = 0xC0000005 + status_in_page_error = 0xC0000006 + status_invalid_handle = 0xC0000008 + status_invalid_parameter = 0xC000000D + status_no_memory = 0xC0000017 + status_illegal_instruction = 0xC000001D + status_noncontinuable_exception = 0xC0000025 + status_invalid_disposition = 0xC0000026 + status_array_bounds_exceeded = 0xC000008C + status_float_denormal_operand = 0xC000008D + status_float_divide_by_zero = 0xC000008E + status_float_inexact_result = 0xC000008F + status_float_invalid_operation = 0xC0000090 + status_float_overflow = 0xC0000091 + status_float_stack_check = 0xC0000092 + status_float_underflow = 0xC0000093 + status_integer_divide_by_zero = 0xC0000094 + status_integer_overflow = 0xC0000095 + status_privileged_instruction = 0xC0000096 + status_stack_overflow = 0xC00000FD + status_dll_not_found = 0xC0000135 + status_ordinal_not_found = 0xC0000138 + status_entrypoint_not_found = 0xC0000139 + status_control_c_exit = 0xC000013A + status_dll_init_failed = 0xC0000142 + status_float_multiple_faults = 0xC00002B4 + status_float_multiple_traps = 0xC00002B5 + status_reg_nat_consumption = 0xC00002C9 + status_heap_corruption = 0xC0000374 + status_stack_buffer_overrun = 0xC0000409 + status_invalid_cruntime_parameter = 0xC0000417 + status_assertion_failure = 0xC0000420 +) + +// Windows Registry Constants +pub const ( + hkey_local_machine = voidptr(0x80000002) + hkey_current_user = voidptr(0x80000001) + key_query_value = 0x0001 + key_set_value = 0x0002 + key_enumerate_sub_keys = 0x0008 + key_wow64_32key = 0x0200 +) + +// Windows Messages +pub const ( + hwnd_broadcast = voidptr(0xFFFF) + wm_settingchange = 0x001A + smto_abortifhung = 0x0002 +) diff --git a/v_windows/v/vlib/os/environment.c.v b/v_windows/v/vlib/os/environment.c.v new file mode 100644 index 0000000..1b65a27 --- /dev/null +++ b/v_windows/v/vlib/os/environment.c.v @@ -0,0 +1,108 @@ +// Copyright (c) 2019-2021 Alexander Medvednikov. All rights reserved. +// Use of this source code is governed by an MIT license +// that can be found in the LICENSE file. +module os + +fn C.getenv(&char) &char + +// C.GetEnvironmentStringsW & C.FreeEnvironmentStringsW are defined only on windows +fn C.GetEnvironmentStringsW() &u16 + +fn C.FreeEnvironmentStringsW(&u16) int + +// `getenv` returns the value of the environment variable named by the key. +pub fn getenv(key string) string { + unsafe { + $if windows { + s := C._wgetenv(key.to_wide()) + if s == 0 { + return '' + } + return string_from_wide(s) + } $else { + s := C.getenv(&char(key.str)) + if s == voidptr(0) { + return '' + } + // NB: C.getenv *requires* that the result be copied. + return cstring_to_vstring(s) + } + } +} + +// os.setenv sets the value of an environment variable with `name` to `value`. +pub fn setenv(name string, value string, overwrite bool) int { + $if windows { + format := '$name=$value' + if overwrite { + unsafe { + return C._putenv(&char(format.str)) + } + } else { + if getenv(name).len == 0 { + unsafe { + return C._putenv(&char(format.str)) + } + } + } + return -1 + } $else { + unsafe { + return C.setenv(&char(name.str), &char(value.str), overwrite) + } + } +} + +// os.unsetenv clears an environment variable with `name`. +pub fn unsetenv(name string) int { + $if windows { + format := '$name=' + return C._putenv(&char(format.str)) + } $else { + return C.unsetenv(&char(name.str)) + } +} + +// See: https://linux.die.net/man/5/environ for unix platforms. +// See: https://docs.microsoft.com/bg-bg/windows/win32/api/processenv/nf-processenv-getenvironmentstrings +// os.environ returns a map of all the current environment variables + +fn unix_environ() &&char { + // TODO: remove this helper function, when `&&char(C.environ)` works properly + return voidptr(C.environ) +} + +pub fn environ() map[string]string { + mut res := map[string]string{} + $if windows { + mut estrings := C.GetEnvironmentStringsW() + mut eline := '' + for c := estrings; *c != 0; { + eline = unsafe { string_from_wide(c) } + eq_index := eline.index_byte(`=`) + if eq_index > 0 { + res[eline[0..eq_index]] = eline[eq_index + 1..] + } + unsafe { + c = c + eline.len + 1 + } + } + C.FreeEnvironmentStringsW(estrings) + } $else { + start := unix_environ() + mut i := 0 + for { + x := unsafe { start[i] } + if x == 0 { + break + } + eline := unsafe { cstring_to_vstring(x) } + eq_index := eline.index_byte(`=`) + if eq_index > 0 { + res[eline[0..eq_index]] = eline[eq_index + 1..] + } + i++ + } + } + return res +} diff --git a/v_windows/v/vlib/os/environment.js.v b/v_windows/v/vlib/os/environment.js.v new file mode 100644 index 0000000..ac760a5 --- /dev/null +++ b/v_windows/v/vlib/os/environment.js.v @@ -0,0 +1,37 @@ +module os + +$if js_node { + #global.$ENV = $process.env +} $else { + #global.$ENV = {} +} + +// setenv sets the value of an environment variable with `name` to `value`. +pub fn setenv(key string, val string, overwrite bool) { + #if ($ENV[key] && !(overwrite.valueOf())) return; + #$ENV[key] = val + ''; +} + +// `getenv` returns the value of the environment variable named by the key. +pub fn getenv(key string) string { + mut res := '' + #if ($ENV[key]) res = new builtin.string($ENV[key]) + + return res +} + +// unsetenv clears an environment variable with `name`. +pub fn unsetenv(name string) int { + #$ENV[name] = "" + + return 1 +} + +pub fn environ() map[string]string { + mut res := map[string]string{} + #for (const key in $ENV) { + #res.map.set(key,$ENV[key]) + #} + + return res +} diff --git a/v_windows/v/vlib/os/environment_test.v b/v_windows/v/vlib/os/environment_test.v new file mode 100644 index 0000000..5324371 --- /dev/null +++ b/v_windows/v/vlib/os/environment_test.v @@ -0,0 +1,49 @@ +import os +import time + +fn test_getenv() { + // VEXE is set by the V builtin test runner + assert os.getenv('VEXE').len > 0 + assert os.getenv('PATH').len > 0 +} + +fn test_setenv() { + os.setenv('foo', 'bar', true) + assert os.getenv('foo') == 'bar' + // `setenv` should not set if `overwrite` is false + os.setenv('foo', 'bar2', false) + assert os.getenv('foo') == 'bar' + // `setenv` should overwrite if `overwrite` is true + os.setenv('foo', 'bar2', true) + assert os.getenv('foo') == 'bar2' +} + +fn test_unsetenv() { + os.setenv('foo', 'bar', true) + os.unsetenv('foo') + assert os.getenv('foo') == '' +} + +fn test_environ() { + os.setenv('myvar1', 'bar1', true) + os.setenv('myvar2', 'bar2', true) + assert os.getenv('myvar1') == 'bar1' + assert os.getenv('myvar2') == 'bar2' + assert os.getenv('myvar_not_defined') == '' + all := os.environ() + assert all['myvar1'] == 'bar1' + assert all['myvar2'] == 'bar2' + assert all['myvar_not_defined'] == '' +} + +fn test_setenv_var_not_exists() { + key := time.new_time(time.now()).unix + os.setenv('foo$key', 'bar', false) + assert os.getenv('foo$key') == 'bar' +} + +fn test_getenv_empty_var() { + key := time.new_time(time.now()).unix + os.setenv('empty$key', '""', false) + assert os.getenv('empty$key') == '""' +} diff --git a/v_windows/v/vlib/os/fd.c.v b/v_windows/v/vlib/os/fd.c.v new file mode 100644 index 0000000..de69e38 --- /dev/null +++ b/v_windows/v/vlib/os/fd.c.v @@ -0,0 +1,61 @@ +module os + +// file descriptor based operations: + +// 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 { + written := C.write(fd, sp, remaining) + if written < 0 { + return + } + remaining = remaining - written + sp = unsafe { sp + written } + } +} + +// 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 { + break + } + res << s + } + return res +} + +// 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_noscan(maxbytes + 1) + nbytes := C.read(fd, buf, maxbytes) + if nbytes < 0 { + free(buf) + return '', nbytes + } + buf[nbytes] = 0 + return tos(buf, nbytes), nbytes + } +} diff --git a/v_windows/v/vlib/os/file.c.v b/v_windows/v/vlib/os/file.c.v new file mode 100644 index 0000000..3de2279 --- /dev/null +++ b/v_windows/v/vlib/os/file.c.v @@ -0,0 +1,787 @@ +module os + +pub struct File { + cfile voidptr // Using void* instead of FILE* +pub: + fd int +pub mut: + is_opened bool +} + +struct FileInfo { + name string + size int +} + +fn C.fseeko(&C.FILE, u64, int) int + +fn C._fseeki64(&C.FILE, u64, int) int + +fn C.getc(&C.FILE) int + +// open_file can be used to open or create a file with custom flags and permissions and returns a `File` object. +pub fn open_file(path string, mode string, options ...int) ?File { + mut flags := 0 + for m in mode { + match m { + `w` { flags |= o_create | o_trunc } + `a` { flags |= o_create | o_append } + `r` { flags |= o_rdonly } + `b` { flags |= o_binary } + `s` { flags |= o_sync } + `n` { flags |= o_nonblock } + `c` { flags |= o_noctty } + `+` { flags |= o_rdwr } + else {} + } + } + if mode == 'r+' { + flags = o_rdwr + } + if mode == 'w' { + flags = o_wronly | o_create | o_trunc + } + if mode == 'a' { + flags = o_wronly | o_create | o_append + } + mut permission := 0o666 + if options.len > 0 { + permission = options[0] + } + $if windows { + if permission < 0o600 { + permission = 0x0100 + } else { + permission = 0x0100 | 0x0080 + } + } + mut p := path + $if windows { + p = path.replace('/', '\\') + } + fd := C.open(&char(p.str), flags, permission) + if fd == -1 { + return error(posix_get_error_msg(C.errno)) + } + cfile := C.fdopen(fd, &char(mode.str)) + if isnil(cfile) { + return error('Failed to open or create file "$path"') + } + return File{ + cfile: cfile + fd: fd + is_opened: true + } +} + +// open tries to open a file for reading and returns back a read-only `File` object. +pub fn open(path string) ?File { + /* + $if linux { + $if !android { + fd := C.syscall(sys_open, path.str, 511) + if fd == -1 { + return error('failed to open file "$path"') + } + return File{ + fd: fd + is_opened: true + } + } + } + */ + cfile := vfopen(path, 'rb') ? + fd := fileno(cfile) + return File{ + cfile: cfile + fd: fd + is_opened: true + } +} + +// create creates or opens a file at a specified location and returns a write-only `File` object. +pub fn create(path string) ?File { + /* + // NB: android/termux/bionic is also a kind of linux, + // but linux syscalls there sometimes fail, + // while the libc version should work. + $if linux { + $if !android { + //$if macos { + // fd = C.syscall(398, path.str, 0x601, 0x1b6) + //} + //$if linux { + fd = C.syscall(sys_creat, path.str, 511) + //} + if fd == -1 { + return error('failed to create file "$path"') + } + file = File{ + fd: fd + is_opened: true + } + return file + } + } + */ + cfile := vfopen(path, 'wb') ? + fd := fileno(cfile) + return File{ + cfile: cfile + fd: fd + is_opened: true + } +} + +// stdin - return an os.File for stdin, so that you can use .get_line on it too. +pub fn stdin() File { + return File{ + fd: 0 + cfile: C.stdin + is_opened: true + } +} + +// stdout - return an os.File for stdout +pub fn stdout() File { + return File{ + fd: 1 + cfile: C.stdout + is_opened: true + } +} + +// stderr - return an os.File for stderr +pub fn stderr() File { + return File{ + fd: 2 + cfile: C.stderr + is_opened: true + } +} + +// read implements the Reader interface. +pub fn (f &File) read(mut buf []byte) ?int { + if buf.len == 0 { + return 0 + } + nbytes := fread(buf.data, 1, buf.len, f.cfile) ? + return nbytes +} + +// **************************** Write ops *************************** +// write implements the Writer interface. +// It returns how many bytes were actually written. +pub fn (mut f File) write(buf []byte) ?int { + if !f.is_opened { + return error_file_not_opened() + } + /* + $if linux { + $if !android { + res := C.syscall(sys_write, f.fd, s.str, s.len) + return res + } + } + */ + written := int(C.fwrite(buf.data, 1, buf.len, f.cfile)) + if written == 0 && buf.len != 0 { + return error('0 bytes written') + } + return written +} + +// writeln writes the string `s` into the file, and appends a \n character. +// It returns how many bytes were written, including the \n character. +pub fn (mut f File) writeln(s string) ?int { + if !f.is_opened { + return error_file_not_opened() + } + /* + $if linux { + $if !android { + snl := s + '\n' + C.syscall(sys_write, f.fd, snl.str, snl.len) + return + } + } + */ + // TODO perf + written := int(C.fwrite(s.str, 1, s.len, f.cfile)) + if written == 0 && s.len != 0 { + return error('0 bytes written') + } + x := C.fputs(c'\n', f.cfile) + if x < 0 { + return error('could not add newline') + } + return (written + 1) +} + +// write_string writes the string `s` into the file +// It returns how many bytes were actually written. +pub fn (mut f File) write_string(s string) ?int { + unsafe { f.write_full_buffer(s.str, size_t(s.len)) ? } + return s.len +} + +// write_to implements the RandomWriter interface. +// It returns how many bytes were actually written. +// It resets the seek position to the end of the file. +pub fn (mut f File) write_to(pos u64, buf []byte) ?int { + if !f.is_opened { + return error_file_not_opened() + } + $if x64 { + $if windows { + C._fseeki64(f.cfile, pos, C.SEEK_SET) + res := int(C.fwrite(buf.data, 1, buf.len, f.cfile)) + if res == 0 && buf.len != 0 { + return error('0 bytes written') + } + C._fseeki64(f.cfile, 0, C.SEEK_END) + return res + } $else { + C.fseeko(f.cfile, pos, C.SEEK_SET) + res := int(C.fwrite(buf.data, 1, buf.len, f.cfile)) + if res == 0 && buf.len != 0 { + return error('0 bytes written') + } + C.fseeko(f.cfile, 0, C.SEEK_END) + return res + } + } + $if x32 { + C.fseek(f.cfile, pos, C.SEEK_SET) + res := int(C.fwrite(buf.data, 1, buf.len, f.cfile)) + if res == 0 && buf.len != 0 { + return error('0 bytes written') + } + C.fseek(f.cfile, 0, C.SEEK_END) + return res + } + return error('Could not write to file') +} + +// write_ptr writes `size` bytes to the file, starting from the address in `data`. +// NB: write_ptr is unsafe and should be used carefully, since if you pass invalid +// pointers to it, it will cause your programs to segfault. +[unsafe] +pub fn (mut f File) write_ptr(data voidptr, size int) int { + return int(C.fwrite(data, 1, size, f.cfile)) +} + +// write_full_buffer writes a whole buffer of data to the file, starting from the +// address in `buffer`, no matter how many tries/partial writes it would take. +[unsafe] +pub fn (mut f File) write_full_buffer(buffer voidptr, buffer_len size_t) ? { + if buffer_len <= size_t(0) { + return + } + if !f.is_opened { + return error_file_not_opened() + } + mut ptr := &byte(buffer) + mut remaining_bytes := i64(buffer_len) + for remaining_bytes > 0 { + unsafe { + x := i64(C.fwrite(ptr, 1, remaining_bytes, f.cfile)) + ptr += x + remaining_bytes -= x + if x <= 0 { + return error('C.fwrite returned 0') + } + } + } +} + +// write_ptr_at writes `size` bytes to the file, starting from the address in `data`, +// at byte offset `pos`, counting from the start of the file (pos 0). +// NB: write_ptr_at is unsafe and should be used carefully, since if you pass invalid +// pointers to it, it will cause your programs to segfault. +[unsafe] +pub fn (mut f File) write_ptr_at(data voidptr, size int, pos u64) int { + $if x64 { + $if windows { + C._fseeki64(f.cfile, pos, C.SEEK_SET) + res := int(C.fwrite(data, 1, size, f.cfile)) + C._fseeki64(f.cfile, 0, C.SEEK_END) + return res + } $else { + C.fseeko(f.cfile, pos, C.SEEK_SET) + res := int(C.fwrite(data, 1, size, f.cfile)) + C.fseeko(f.cfile, 0, C.SEEK_END) + return res + } + } + $if x32 { + C.fseek(f.cfile, pos, C.SEEK_SET) + res := int(C.fwrite(data, 1, size, f.cfile)) + C.fseek(f.cfile, 0, C.SEEK_END) + return res + } + return 0 +} + +// **************************** Read ops *************************** + +// fread wraps C.fread and handles error and end-of-file detection. +fn fread(ptr voidptr, item_size int, items int, stream &C.FILE) ?int { + nbytes := int(C.fread(ptr, item_size, items, stream)) + // If no bytes were read, check for errors and end-of-file. + if nbytes <= 0 { + // If fread encountered end-of-file return the none error. Note that fread + // may read data and encounter the end-of-file, but we shouldn't return none + // in that case which is why we only check for end-of-file if no data was + // read. The caller will get none on their next call because there will be + // no data available and the end-of-file will be encountered again. + if C.feof(stream) != 0 { + return none + } + // If fread encountered an error, return it. Note that fread and ferror do + // not tell us what the error was, so we can't return anything more specific + // than there was an error. This is because fread and ferror do not set + // errno. + if C.ferror(stream) != 0 { + return error('file read error') + } + } + return nbytes +} + +// read_bytes reads bytes from the beginning of the file. +// Utility method, same as .read_bytes_at(size, 0). +pub fn (f &File) read_bytes(size int) []byte { + return f.read_bytes_at(size, 0) +} + +// read_bytes_at reads `size` bytes at the given position in the file. +pub fn (f &File) read_bytes_at(size int, pos u64) []byte { + mut arr := []byte{len: size} + nreadbytes := f.read_bytes_into(pos, mut arr) or { + // return err + return [] + } + return arr[0..nreadbytes] +} + +// read_bytes_into_newline reads from the beginning of the file into the provided buffer. +// Each consecutive call on the same file continues reading where it previously ended. +// A read call is either stopped, if the buffer is full, a newline was read or EOF. +pub fn (f &File) read_bytes_into_newline(mut buf []byte) ?int { + if buf.len == 0 { + panic(@FN + ': `buf.len` == 0') + } + newline := 10 + mut c := 0 + mut buf_ptr := 0 + mut nbytes := 0 + + stream := &C.FILE(f.cfile) + for (buf_ptr < buf.len) { + c = C.getc(stream) + match c { + C.EOF { + if C.feof(stream) != 0 { + return nbytes + } + if C.ferror(stream) != 0 { + return error('file read error') + } + } + newline { + buf[buf_ptr] = byte(c) + nbytes++ + return nbytes + } + else { + buf[buf_ptr] = byte(c) + buf_ptr++ + nbytes++ + } + } + } + return nbytes +} + +// read_bytes_into fills `buf` with bytes at the given position in the file. +// `buf` *must* have length greater than zero. +// Returns the number of read bytes, or an error. +pub fn (f &File) read_bytes_into(pos u64, mut buf []byte) ?int { + if buf.len == 0 { + panic(@FN + ': `buf.len` == 0') + } + $if x64 { + $if windows { + // Note: fseek errors if pos == os.file_size, which we accept + C._fseeki64(f.cfile, pos, C.SEEK_SET) + nbytes := fread(buf.data, 1, buf.len, f.cfile) ? + $if debug { + C._fseeki64(f.cfile, 0, C.SEEK_SET) + } + return nbytes + } $else { + C.fseeko(f.cfile, pos, C.SEEK_SET) + nbytes := fread(buf.data, 1, buf.len, f.cfile) ? + $if debug { + C.fseeko(f.cfile, 0, C.SEEK_SET) + } + return nbytes + } + } + $if x32 { + C.fseek(f.cfile, pos, C.SEEK_SET) + nbytes := fread(buf.data, 1, buf.len, f.cfile) ? + $if debug { + C.fseek(f.cfile, 0, C.SEEK_SET) + } + return nbytes + } + return error('Could not read file') +} + +// read_from implements the RandomReader interface. +pub fn (f &File) read_from(pos u64, mut buf []byte) ?int { + if buf.len == 0 { + return 0 + } + $if x64 { + $if windows { + C._fseeki64(f.cfile, pos, C.SEEK_SET) + } $else { + C.fseeko(f.cfile, pos, C.SEEK_SET) + } + + nbytes := fread(buf.data, 1, buf.len, f.cfile) ? + return nbytes + } + $if x32 { + C.fseek(f.cfile, pos, C.SEEK_SET) + nbytes := fread(buf.data, 1, buf.len, f.cfile) ? + return nbytes + } + return error('Could not read file') +} + +// read_into_ptr reads at most max_size bytes from the file and writes it into ptr. +// Returns the amount of bytes read or an error. +pub fn (f &File) read_into_ptr(ptr &byte, max_size int) ?int { + return fread(ptr, 1, max_size, f.cfile) +} + +// **************************** Utility ops *********************** +// flush writes any buffered unwritten data left in the file stream. +pub fn (mut f File) flush() { + if !f.is_opened { + return + } + C.fflush(f.cfile) +} + +pub struct ErrFileNotOpened { + msg string = 'os: file not opened' + code int +} + +pub struct ErrSizeOfTypeIs0 { + msg string = 'os: size of type is 0' + code int +} + +fn error_file_not_opened() IError { + return IError(&ErrFileNotOpened{}) +} + +fn error_size_of_type_0() IError { + return IError(&ErrSizeOfTypeIs0{}) +} + +// read_struct reads a single struct of type `T` +pub fn (mut f File) read_struct(mut t T) ? { + if !f.is_opened { + return error_file_not_opened() + } + tsize := int(sizeof(*t)) + if tsize == 0 { + return error_size_of_type_0() + } + nbytes := fread(t, 1, tsize, f.cfile) ? + if nbytes != tsize { + return error_with_code('incomplete struct read', nbytes) + } +} + +// read_struct_at reads a single struct of type `T` at position specified in file +pub fn (mut f File) read_struct_at(mut t T, pos u64) ? { + if !f.is_opened { + return error_file_not_opened() + } + tsize := int(sizeof(*t)) + if tsize == 0 { + return error_size_of_type_0() + } + mut nbytes := 0 + $if x64 { + $if windows { + C._fseeki64(f.cfile, pos, C.SEEK_SET) + nbytes = fread(t, 1, tsize, f.cfile) ? + C._fseeki64(f.cfile, 0, C.SEEK_END) + } $else { + C.fseeko(f.cfile, pos, C.SEEK_SET) + nbytes = fread(t, 1, tsize, f.cfile) ? + C.fseeko(f.cfile, 0, C.SEEK_END) + } + } + $if x32 { + C.fseek(f.cfile, pos, C.SEEK_SET) + nbytes = fread(t, 1, tsize, f.cfile) ? + C.fseek(f.cfile, 0, C.SEEK_END) + } + if nbytes != tsize { + return error_with_code('incomplete struct read', nbytes) + } +} + +// read_raw reads and returns a single instance of type `T` +pub fn (mut f File) read_raw() ?T { + if !f.is_opened { + return error_file_not_opened() + } + tsize := int(sizeof(T)) + if tsize == 0 { + return error_size_of_type_0() + } + mut t := T{} + nbytes := fread(&t, 1, tsize, f.cfile) ? + if nbytes != tsize { + return error_with_code('incomplete struct read', nbytes) + } + return t +} + +// read_raw_at reads and returns a single instance of type `T` starting at file byte offset `pos` +pub fn (mut f File) read_raw_at(pos u64) ?T { + if !f.is_opened { + return error_file_not_opened() + } + tsize := int(sizeof(T)) + if tsize == 0 { + return error_size_of_type_0() + } + mut nbytes := 0 + mut t := T{} + $if x64 { + $if windows { + if C._fseeki64(f.cfile, pos, C.SEEK_SET) != 0 { + return error(posix_get_error_msg(C.errno)) + } + nbytes = fread(&t, 1, tsize, f.cfile) ? + if C._fseeki64(f.cfile, 0, C.SEEK_END) != 0 { + return error(posix_get_error_msg(C.errno)) + } + } $else { + if C.fseeko(f.cfile, pos, C.SEEK_SET) != 0 { + return error(posix_get_error_msg(C.errno)) + } + nbytes = fread(&t, 1, tsize, f.cfile) ? + if C.fseeko(f.cfile, 0, C.SEEK_END) != 0 { + return error(posix_get_error_msg(C.errno)) + } + } + } + $if x32 { + if C.fseek(f.cfile, pos, C.SEEK_SET) != 0 { + return error(posix_get_error_msg(C.errno)) + } + nbytes = fread(&t, 1, tsize, f.cfile) ? + if C.fseek(f.cfile, 0, C.SEEK_END) != 0 { + return error(posix_get_error_msg(C.errno)) + } + } + + if nbytes != tsize { + return error_with_code('incomplete struct read', nbytes) + } + return t +} + +// write_struct writes a single struct of type `T` +pub fn (mut f File) write_struct(t &T) ? { + if !f.is_opened { + return error_file_not_opened() + } + tsize := int(sizeof(T)) + if tsize == 0 { + return error_size_of_type_0() + } + C.errno = 0 + nbytes := int(C.fwrite(t, 1, tsize, f.cfile)) + if C.errno != 0 { + return error(posix_get_error_msg(C.errno)) + } + if nbytes != tsize { + return error_with_code('incomplete struct write', nbytes) + } +} + +// write_struct_at writes a single struct of type `T` at position specified in file +pub fn (mut f File) write_struct_at(t &T, pos u64) ? { + if !f.is_opened { + return error_file_not_opened() + } + tsize := int(sizeof(T)) + if tsize == 0 { + return error_size_of_type_0() + } + C.errno = 0 + mut nbytes := 0 + $if x64 { + $if windows { + C._fseeki64(f.cfile, pos, C.SEEK_SET) + nbytes = int(C.fwrite(t, 1, tsize, f.cfile)) + C._fseeki64(f.cfile, 0, C.SEEK_END) + } $else { + C.fseeko(f.cfile, pos, C.SEEK_SET) + nbytes = int(C.fwrite(t, 1, tsize, f.cfile)) + C.fseeko(f.cfile, 0, C.SEEK_END) + } + } + $if x32 { + C.fseek(f.cfile, pos, C.SEEK_SET) + nbytes = int(C.fwrite(t, 1, tsize, f.cfile)) + C.fseek(f.cfile, 0, C.SEEK_END) + } + if C.errno != 0 { + return error(posix_get_error_msg(C.errno)) + } + if nbytes != tsize { + return error_with_code('incomplete struct write', nbytes) + } +} + +// TODO `write_raw[_at]` implementations are copy-pasted from `write_struct[_at]` + +// write_raw writes a single instance of type `T` +pub fn (mut f File) write_raw(t &T) ? { + if !f.is_opened { + return error_file_not_opened() + } + tsize := int(sizeof(T)) + if tsize == 0 { + return error_size_of_type_0() + } + C.errno = 0 + nbytes := int(C.fwrite(t, 1, tsize, f.cfile)) + if C.errno != 0 { + return error(posix_get_error_msg(C.errno)) + } + if nbytes != tsize { + return error_with_code('incomplete struct write', nbytes) + } +} + +// write_raw_at writes a single instance of type `T` starting at file byte offset `pos` +pub fn (mut f File) write_raw_at(t &T, pos u64) ? { + if !f.is_opened { + return error_file_not_opened() + } + tsize := int(sizeof(T)) + if tsize == 0 { + return error_size_of_type_0() + } + mut nbytes := 0 + + $if x64 { + $if windows { + if C._fseeki64(f.cfile, pos, C.SEEK_SET) != 0 { + return error(posix_get_error_msg(C.errno)) + } + nbytes = int(C.fwrite(t, 1, tsize, f.cfile)) + if C.errno != 0 { + return error(posix_get_error_msg(C.errno)) + } + if C._fseeki64(f.cfile, 0, C.SEEK_END) != 0 { + return error(posix_get_error_msg(C.errno)) + } + } $else { + if C.fseeko(f.cfile, pos, C.SEEK_SET) != 0 { + return error(posix_get_error_msg(C.errno)) + } + nbytes = int(C.fwrite(t, 1, tsize, f.cfile)) + if C.errno != 0 { + return error(posix_get_error_msg(C.errno)) + } + if C.fseeko(f.cfile, 0, C.SEEK_END) != 0 { + return error(posix_get_error_msg(C.errno)) + } + } + } + $if x32 { + if C.fseek(f.cfile, pos, C.SEEK_SET) != 0 { + return error(posix_get_error_msg(C.errno)) + } + nbytes = int(C.fwrite(t, 1, tsize, f.cfile)) + if C.errno != 0 { + return error(posix_get_error_msg(C.errno)) + } + if C.fseek(f.cfile, 0, C.SEEK_END) != 0 { + return error(posix_get_error_msg(C.errno)) + } + } + + if nbytes != tsize { + return error_with_code('incomplete struct write', nbytes) + } +} + +pub enum SeekMode { + start + current + end +} + +// seek moves the file cursor (if any) associated with a file +// to a new location, offset `pos` bytes from the origin. The origin +// is dependent on the `mode` and can be: +// .start -> the origin is the start of the file +// .current -> the current position/cursor in the file +// .end -> the end of the file +// If the file is not seek-able, or an error occures, the error will +// be returned to the caller. +// A successful call to the fseek() function clears the end-of-file +// indicator for the file. +pub fn (mut f File) seek(pos i64, mode SeekMode) ? { + if !f.is_opened { + return error_file_not_opened() + } + whence := int(mode) + mut res := 0 + $if x64 { + $if windows { + res = C._fseeki64(f.cfile, pos, whence) + } $else { + res = C.fseeko(f.cfile, pos, whence) + } + } + $if x32 { + res = C.fseek(f.cfile, pos, whence) + } + if res == -1 { + return error(posix_get_error_msg(C.errno)) + } +} + +// tell will return the current offset of the file cursor measured from +// the start of the file, in bytes. It is complementary to seek, i.e. +// you can use the return value as the `pos` parameter to .seek( pos, .start ), +// so that your next read will happen from the same place. +pub fn (f &File) tell() ?i64 { + if !f.is_opened { + return error_file_not_opened() + } + pos := C.ftell(f.cfile) + if pos == -1 { + return error(posix_get_error_msg(C.errno)) + } + return pos +} diff --git a/v_windows/v/vlib/os/file.js.v b/v_windows/v/vlib/os/file.js.v new file mode 100644 index 0000000..abaeeab --- /dev/null +++ b/v_windows/v/vlib/os/file.js.v @@ -0,0 +1,136 @@ +module os + +pub struct File { +pub: + fd int +pub mut: + is_opened bool +} + +#const $buffer = require('buffer'); + +// todo(playX): __as_cast is broken here +/* +pub struct ErrFileNotOpened { + msg string = 'os: file not opened' + code int +} +pub struct ErrSizeOfTypeIs0 { + msg string = 'os: size of type is 0' + code int +} +fn error_file_not_opened() IError { + return IError(&ErrFileNotOpened{}) +} +fn error_size_of_type_0() IError { + return IError(&ErrSizeOfTypeIs0{}) +} +*/ +pub fn open_file(path string, mode string, options ...int) ?File { + mut res := File{} + $if js_node { + #if (!options) { options = new array([]); } + #let permissions = 0o666 + #if (options.arr.length > 0) { permissions = options.arr[0]; } + #try { + #res.fd = new int($fs.openSync(''+path,''+mode,permissions)) + #} catch (e) { + #return builtin.error('' + e); + #} + + res.is_opened = true + } $else { + error('cannot open file on non NodeJS runtime') + } + return res +} + +// open tries to open a file for reading and returns back a read-only `File` object. +pub fn open(path string) ?File { + f := open_file(path, 'r') ? + return f +} + +pub fn create(path string) ?File { + f := open_file(path, 'w') ? + return f +} + +pub fn stdin() File { + return File{ + fd: 0 + is_opened: true + } +} + +pub fn stdout() File { + return File{ + fd: 1 + is_opened: true + } +} + +pub fn stderr() File { + return File{ + fd: 2 + is_opened: true + } +} + +pub fn (f &File) read(mut buf []byte) ?int { + if buf.len == 0 { + return 0 + } + mut nbytes := 0 + #try { + #let buffer = $fs.readFileSync(f.fd.valueOf()); + # + #for (const val of buffer.values()) { buf.arr[nbytes++] = val; } + #} + #catch (e) { return builtin.error('' + e); } + + return nbytes +} + +pub fn (mut f File) write(buf []byte) ?int { + if !f.is_opened { + return error('file is not opened') + } + mut nbytes := 0 + #const b = $buffer.Buffer.from(buf.arr.map((x) => x.valueOf())) + #try { $fs.writeSync(f.fd.valueOf(),b,0,buf.len.valueOf(),0); } catch (e) { return builtin.error('' + e); } + + return nbytes +} + +// writeln writes the string `s` into the file, and appends a \n character. +// It returns how many bytes were written, including the \n character. +pub fn (mut f File) writeln(s string) ?int { + mut nbytes := f.write(s.bytes()) ? + nbytes += f.write('\n'.bytes()) ? + return nbytes +} + +pub fn (mut f File) write_to(pos u64, buf []byte) ?int { + if !f.is_opened { + return error('file is not opened') + } + mut nbytes := 0 + #const b = $buffer.Buffer.from(buf.arr.map((x) => x.valueOf())) + #try { $fs.writeSync(f.fd.valueOf(),b,0,buf.len.valueOf(),pos.valueOf()); } catch (e) { return builtin.error('' + e); } + + return nbytes +} + +// write_string writes the string `s` into the file +// It returns how many bytes were actually written. +pub fn (mut f File) write_string(s string) ?int { + nbytes := f.write(s.bytes()) ? + return nbytes +} + +pub fn (mut f File) close() { + #f.valueOf().fd.close() +} + +pub fn (mut f File) write_full_buffer(s voidptr, buffer_len size_t) ? {} diff --git a/v_windows/v/vlib/os/file_test.v b/v_windows/v/vlib/os/file_test.v new file mode 100644 index 0000000..3339ad8 --- /dev/null +++ b/v_windows/v/vlib/os/file_test.v @@ -0,0 +1,372 @@ +import os + +struct Point { + x f64 + y f64 + z f64 +} + +struct Extended_Point { + a f64 + b f64 + c f64 + d f64 + e f64 + f f64 + g f64 + h f64 + i f64 +} + +enum Color { + red + green + blue +} + +[flag] +enum Permissions { + read + write + execute +} + +const ( + unit_point = Point{1.0, 1.0, 1.0} + another_point = Point{0.25, 2.25, 6.25} + extended_point = Extended_Point{1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0} + another_byte = byte(123) + another_color = Color.red + another_permission = Permissions.read | .write +) + +const ( + tfolder = os.join_path(os.temp_dir(), 'os_file_test') + tfile = os.join_path(tfolder, 'test_file') +) + +fn testsuite_begin() ? { + os.rmdir_all(tfolder) or {} + assert !os.is_dir(tfolder) + os.mkdir_all(tfolder) ? + os.chdir(tfolder) ? + assert os.is_dir(tfolder) +} + +fn testsuite_end() ? { + os.chdir(os.wd_at_startup) ? + os.rmdir_all(tfolder) ? + assert !os.is_dir(tfolder) +} + +// test_read_bytes_into_newline_text tests reading text from a file with newlines. +// This test simulates reading a larger text file step by step into a buffer and +// returning on each newline, even before the buffer is full, and reaching EOF before +// the buffer is completely filled. +fn test_read_bytes_into_newline_text() ? { + mut f := os.open_file(tfile, 'w') ? + f.write_string('Hello World!\nGood\r morning.') ? + f.close() + + f = os.open_file(tfile, 'r') ? + mut buf := []byte{len: 8} + + n0 := f.read_bytes_into_newline(mut buf) ? + assert n0 == 8 + + n1 := f.read_bytes_into_newline(mut buf) ? + assert n1 == 5 + + n2 := f.read_bytes_into_newline(mut buf) ? + assert n2 == 8 + + n3 := f.read_bytes_into_newline(mut buf) ? + assert n3 == 6 + + f.close() +} + +// test_read_bytes_into_newline_binary tests reading a binary file with NUL bytes. +// This test simulates the scenario when a byte stream is read and a newline byte +// appears in that stream and an EOF occurs before the buffer is full. +fn test_read_bytes_into_newline_binary() ? { + os.rm(tfile) or {} // FIXME This is a workaround for macos, because the file isn't truncated when open with 'w' + mut bw := []byte{len: 15} + bw[9] = 0xff + bw[12] = 10 // newline + + n0_bytes := bw[0..10] + n1_bytes := bw[10..13] + n2_bytes := bw[13..] + + mut f := os.open_file(tfile, 'w') ? + f.write(bw) ? + f.close() + + f = os.open_file(tfile, 'r') ? + mut buf := []byte{len: 10} + + n0 := f.read_bytes_into_newline(mut buf) ? + assert n0 == 10 + assert buf[..n0] == n0_bytes + + n1 := f.read_bytes_into_newline(mut buf) ? + assert n1 == 3 + assert buf[..n1] == n1_bytes + + n2 := f.read_bytes_into_newline(mut buf) ? + assert n2 == 2 + assert buf[..n2] == n2_bytes + f.close() +} + +// test_read_eof_last_read_partial_buffer_fill tests that when reading a file +// the end-of-file is detected and results in a none error being returned. This +// test simulates file reading where the end-of-file is reached inside an fread +// containing data. +fn test_read_eof_last_read_partial_buffer_fill() ? { + mut f := os.open_file(tfile, 'w') ? + bw := []byte{len: 199, init: 5} + f.write(bw) ? + f.close() + + f = os.open_file(tfile, 'r') ? + mut br := []byte{len: 100} + // Read first 100 bytes of 199 byte file, should fill buffer with no error. + n0 := f.read(mut br) ? + assert n0 == 100 + // Read remaining 99 bytes of 199 byte file, should fill buffer with no + // error, even though end-of-file was reached. + n1 := f.read(mut br) ? + assert n1 == 99 + // Read again, end-of-file was previously reached so should return none + // error. + if _ := f.read(mut br) { + // This is not intended behavior because the read function should + // not return a number of bytes read when end-of-file is reached. + assert false + } else { + // Expect none to have been returned when end-of-file. + assert err is none + } + f.close() +} + +// test_read_eof_last_read_full_buffer_fill tests that when reading a file the +// end-of-file is detected and results in a none error being returned. This test +// simulates file reading where the end-of-file is reached at the beinning of an +// fread that returns no data. +fn test_read_eof_last_read_full_buffer_fill() ? { + mut f := os.open_file(tfile, 'w') ? + bw := []byte{len: 200, init: 5} + f.write(bw) ? + f.close() + + f = os.open_file(tfile, 'r') ? + mut br := []byte{len: 100} + // Read first 100 bytes of 200 byte file, should fill buffer with no error. + n0 := f.read(mut br) ? + assert n0 == 100 + // Read remaining 100 bytes of 200 byte file, should fill buffer with no + // error. The end-of-file isn't reached yet, but there is no more data. + n1 := f.read(mut br) ? + assert n1 == 100 + // Read again, end-of-file was previously reached so should return none + // error. + if _ := f.read(mut br) { + // This is not intended behavior because the read function should + // not return a number of bytes read when end-of-file is reached. + assert false + } else { + // Expect none to have been returned when end-of-file. + assert err is none + } + f.close() +} + +fn test_write_struct() ? { + os.rm(tfile) or {} // FIXME This is a workaround for macos, because the file isn't truncated when open with 'w' + size_of_point := int(sizeof(Point)) + mut f := os.open_file(tfile, 'w') ? + f.write_struct(another_point) ? + f.close() + x := os.read_file(tfile) ? + pcopy := unsafe { &byte(memdup(&another_point, size_of_point)) } + y := unsafe { pcopy.vstring_with_len(size_of_point) } + assert x == y + $if debug { + eprintln(x.bytes()) + eprintln(y.bytes()) + } +} + +fn test_write_struct_at() ? { + mut f := os.open_file(tfile, 'w') ? + f.write_struct(extended_point) ? + f.write_struct_at(another_point, 3) ? + f.close() + f = os.open_file(tfile, 'r') ? + mut p := Point{} + f.read_struct_at(mut p, 3) ? + f.close() + + assert p == another_point +} + +fn test_read_struct() ? { + mut f := os.open_file(tfile, 'w') ? + f.write_struct(another_point) ? + f.close() + + f = os.open_file(tfile, 'r') ? + mut p := Point{} + f.read_struct(mut p) ? + f.close() + + assert p == another_point +} + +fn test_read_struct_at() ? { + mut f := os.open_file(tfile, 'w') ? + f.write([byte(1), 2, 3]) ? + f.write_struct(another_point) ? + f.close() + f = os.open_file(tfile, 'r') ? + mut p := Point{} + f.read_struct_at(mut p, 3) ? + f.close() + + assert p == another_point +} + +fn test_write_raw() ? { + os.rm(tfile) or {} // FIXME This is a workaround for macos, because the file isn't truncated when open with 'w' + size_of_point := int(sizeof(Point)) + mut f := os.open_file(tfile, 'w') ? + f.write_raw(another_point) ? + f.close() + x := os.read_file(tfile) ? + pcopy := unsafe { &byte(memdup(&another_point, size_of_point)) } + y := unsafe { pcopy.vstring_with_len(size_of_point) } + assert x == y + $if debug { + eprintln(x.bytes()) + eprintln(y.bytes()) + } +} + +fn test_write_raw_at() ? { + mut f := os.open_file(tfile, 'w') ? + f.write_raw(extended_point) ? + f.write_raw_at(another_point, 3) ? + f.close() + f = os.open_file(tfile, 'r') ? + mut p := Point{} + f.read_struct_at(mut p, 3) ? + f.close() + + assert p == another_point +} + +fn test_write_raw_at_negative_pos() ? { + mut f := os.open_file(tfile, 'w') ? + if _ := f.write_raw_at(another_point, -1) { + assert false + } + f.write_raw_at(another_point, -234) or { assert err.msg == 'Invalid argument' } + f.close() +} + +fn test_read_raw() ? { + mut f := os.open_file(tfile, 'w') ? + f.write_raw(another_point) ? + f.write_raw(another_byte) ? + f.write_raw(another_color) ? + f.write_raw(another_permission) ? + f.close() + f = os.open_file(tfile, 'r') ? + p := f.read_raw() ? + b := f.read_raw() ? + c := f.read_raw() ? + x := f.read_raw() ? + f.close() + + assert p == another_point + assert b == another_byte + assert c == another_color + assert x == another_permission +} + +fn test_read_raw_at() ? { + mut f := os.open_file(tfile, 'w') ? + f.write([byte(1), 2, 3]) ? + f.write_raw(another_point) ? + f.write_raw(another_byte) ? + f.write_raw(another_color) ? + f.write_raw(another_permission) ? + f.close() + f = os.open_file(tfile, 'r') ? + mut at := u64(3) + p := f.read_raw_at(at) ? + at += sizeof(Point) + b := f.read_raw_at(at) ? + at += sizeof(byte) + c := f.read_raw_at(at) ? + at += sizeof(Color) + x := f.read_raw_at(at) ? + at += sizeof(Permissions) + f.close() + + assert p == another_point + assert b == another_byte + assert c == another_color + assert x == another_permission +} + +fn test_read_raw_at_negative_pos() ? { + mut f := os.open_file(tfile, 'r') ? + if _ := f.read_raw_at(-1) { + assert false + } + f.read_raw_at(-234) or { assert err.msg == 'Invalid argument' } + f.close() +} + +fn test_seek() ? { + mut f := os.open_file(tfile, 'w') ? + f.write_raw(another_point) ? + f.write_raw(another_byte) ? + f.write_raw(another_color) ? + f.write_raw(another_permission) ? + f.close() + + // println('> ${sizeof(Point)} ${sizeof(byte)} ${sizeof(Color)} ${sizeof(Permissions)}') + f = os.open_file(tfile, 'r') ? + // + f.seek(i64(sizeof(Point)), .start) ? + assert f.tell() ? == sizeof(Point) + b := f.read_raw() ? + assert b == another_byte + + f.seek(i64(sizeof(Color)), .current) ? + x := f.read_raw() ? + assert x == another_permission + // + f.close() +} + +fn test_tell() ? { + for size in 10 .. 30 { + s := 'x'.repeat(size) + os.write_file(tfile, s) ? + fs := os.file_size(tfile) + assert int(fs) == size + // + mut f := os.open_file(tfile, 'r') ? + f.seek(-5, .end) ? + pos := f.tell() ? + f.close() + // dump(pos) + assert pos == size - 5 + } +} diff --git a/v_windows/v/vlib/os/filelock/filelock_test.v b/v_windows/v/vlib/os/filelock/filelock_test.v new file mode 100644 index 0000000..658d3aa --- /dev/null +++ b/v_windows/v/vlib/os/filelock/filelock_test.v @@ -0,0 +1,27 @@ +import os +import os.filelock + +fn test_flock() { + lockfile := 'test.lock' + mut l := filelock.new(lockfile) + assert !os.exists(lockfile) + l.acquire() or { panic(err) } + assert os.exists(lockfile) + // do stuff + l.release() + assert !os.exists(lockfile) +} + +fn test_flock_try() { + lockfile := 'test-try.lock' + mut l := filelock.new(lockfile) + assert l.try_acquire() + l.release() + assert !os.exists(lockfile) + assert l.try_acquire() + assert os.exists(lockfile) + l.release() + assert l.try_acquire() + l.release() + assert !os.exists(lockfile) +} diff --git a/v_windows/v/vlib/os/filelock/lib.v b/v_windows/v/vlib/os/filelock/lib.v new file mode 100644 index 0000000..5a89ad8 --- /dev/null +++ b/v_windows/v/vlib/os/filelock/lib.v @@ -0,0 +1,14 @@ +module filelock + +pub struct FileLock { + name string +mut: + fd int +} + +pub fn new(fileName string) FileLock { + return FileLock{ + name: fileName + fd: -1 + } +} diff --git a/v_windows/v/vlib/os/filelock/lib_nix.c.v b/v_windows/v/vlib/os/filelock/lib_nix.c.v new file mode 100644 index 0000000..1af9916 --- /dev/null +++ b/v_windows/v/vlib/os/filelock/lib_nix.c.v @@ -0,0 +1,82 @@ +module filelock + +import time + +#include + +fn C.unlink(&char) int +fn C.open(&char, int, int) int +fn C.flock(int, int) int + +[unsafe] +pub fn (mut l FileLock) unlink() { + if l.fd != -1 { + C.close(l.fd) + l.fd = -1 + } + C.unlink(&char(l.name.str)) +} + +pub fn (mut l FileLock) acquire() ?bool { + if l.fd != -1 { + // lock already acquired by this instance + return false + } + fd := open_lockfile(l.name) + if fd == -1 { + return error('cannot create lock file $l.name') + } + if C.flock(fd, C.LOCK_EX) == -1 { + C.close(fd) + return error('cannot lock') + } + l.fd = fd + return true +} + +pub fn (mut l FileLock) release() bool { + if l.fd != -1 { + unsafe { + l.unlink() + } + return true + } + return false +} + +pub fn (mut l FileLock) wait_acquire(s int) ?bool { + fin := time.now().add(s) + for time.now() < fin { + if l.try_acquire() { + return true + } + C.usleep(1000) + } + return false +} + +fn open_lockfile(f string) int { + mut fd := C.open(&char(f.str), C.O_CREAT, 0o644) + if fd == -1 { + // if stat is too old delete lockfile + fd = C.open(&char(f.str), C.O_RDONLY, 0) + } + return fd +} + +pub fn (mut l FileLock) try_acquire() bool { + if l.fd != -1 { + return true + } + fd := open_lockfile('$l.name') + if fd != -1 { + err := C.flock(fd, C.LOCK_EX | C.LOCK_NB) + if err == -1 { + C.close(fd) + return false + } + l.fd = fd + return true + } + return false +} diff --git a/v_windows/v/vlib/os/filelock/lib_windows.c.v b/v_windows/v/vlib/os/filelock/lib_windows.c.v new file mode 100644 index 0000000..56cbace --- /dev/null +++ b/v_windows/v/vlib/os/filelock/lib_windows.c.v @@ -0,0 +1,75 @@ +module filelock + +import time + +fn C.DeleteFileW(&u16) bool +fn C.CreateFileW(&u16, u32, u32, voidptr, u32, u32, voidptr) voidptr +fn C.CloseHandle(voidptr) bool + +pub fn (mut l FileLock) unlink() { + if l.fd != -1 { + C.CloseHandle(l.fd) + l.fd = -1 + } + t_wide := l.name.to_wide() + C.DeleteFileW(t_wide) +} + +pub fn (mut l FileLock) acquire() ?bool { + if l.fd != -1 { + // lock already acquired by this instance + return false + } + fd := open(l.name) + if fd == -1 { + return error('cannot create lock file $l.name') + } + l.fd = fd + return true +} + +pub fn (mut l FileLock) release() bool { + if l.fd != -1 { + C.CloseHandle(l.fd) + l.fd = -1 + t_wide := l.name.to_wide() + C.DeleteFileW(t_wide) + return true + } + return false +} + +pub fn (mut l FileLock) wait_acquire(s int) ?bool { + fin := time.now().add(s) + for time.now() < fin { + if l.try_acquire() { + return true + } + time.sleep(1 * time.millisecond) + } + return false +} + +fn open(f string) voidptr { + f_wide := f.to_wide() + // locking it + fd := C.CreateFileW(f_wide, C.GENERIC_READ | C.GENERIC_WRITE, 0, 0, C.OPEN_ALWAYS, + C.FILE_ATTRIBUTE_NORMAL, 0) + if fd == C.INVALID_HANDLE_VALUE { + fd == -1 + } + return fd +} + +pub fn (mut l FileLock) try_acquire() bool { + if l.fd != -1 { + // lock already acquired by this instance + return false + } + fd := open(l.name) + if fd == -1 { + return false + } + l.fd = fd + return true +} diff --git a/v_windows/v/vlib/os/glob_test.v b/v_windows/v/vlib/os/glob_test.v new file mode 100644 index 0000000..889900c --- /dev/null +++ b/v_windows/v/vlib/os/glob_test.v @@ -0,0 +1,80 @@ +import os + +fn deep_glob() ? { + os.chdir(@VMODROOT) ? + matches := os.glob('vlib/v/*/*.v') or { panic(err) } + assert matches.len > 10 + assert 'vlib/v/ast/ast.v' in matches + assert 'vlib/v/ast/table.v' in matches + assert 'vlib/v/token/token.v' in matches + for f in matches { + if !f.starts_with('vlib/v/') { + assert false + } + assert f.ends_with('.v') + } +} + +fn redeep_glob() ? { + os.chdir(@VMODROOT) ? + matches := os.glob('vlib/v/**/*.v') or { panic(err) } + assert matches.len > 10 + assert 'vlib/v/ast/ast.v' in matches + assert 'vlib/v/ast/table.v' in matches + assert 'vlib/v/token/token.v' in matches + for f in matches { + if !f.starts_with('vlib/v/') { + assert false + } + assert f.ends_with('.v') + } +} + +fn test_glob_can_find_v_files_3_levels_deep() ? { + $if !windows { + deep_glob() ? + redeep_glob() ? + } + assert true +} + +fn test_glob_can_find_files_in_current_folder() ? { + os.chdir(@VMODROOT) ? + matches := os.glob('*') ? + assert '.gitignore' in matches + assert 'make.bat' in matches + assert 'Makefile' in matches + assert 'Dockerfile' in matches + assert 'README.md' in matches + assert 'v.mod' in matches + assert 'cmd/' in matches + assert 'vlib/' in matches + assert 'thirdparty/' in matches +} + +fn test_glob_can_be_used_with_multiple_patterns() ? { + os.chdir(@VMODROOT) ? + matches := os.glob('*', 'cmd/tools/*') ? + assert 'README.md' in matches + assert 'Makefile' in matches + $if !windows { + assert 'cmd/tools/test_if_v_test_system_works.v' in matches + } + $if windows { + assert 'test_if_v_test_system_works.v' in matches + } +} + +fn test_glob_star() ? { + os.chdir(@VMODROOT) ? + matches := os.glob('*ake*') ? + assert 'Makefile' in matches + assert 'make.bat' in matches +} + +fn test_glob_not_found() ? { + os.glob('an_unknown_folder/*.v') or { + assert true + return + } +} diff --git a/v_windows/v/vlib/os/inode.c.v b/v_windows/v/vlib/os/inode.c.v new file mode 100644 index 0000000..3c6b19b --- /dev/null +++ b/v_windows/v/vlib/os/inode.c.v @@ -0,0 +1,92 @@ +// Copyright (c) 2019-2021 Alexander Medvednikov. All rights reserved. +// Use of this source code is governed by an MIT license +// that can be found in the LICENSE file. +module os + +enum FileType { + regular + directory + character_device + block_device + fifo + symbolic_link + socket +} + +struct FilePermission { +pub: + read bool + write bool + execute bool +} + +struct FileMode { +pub: + typ FileType + owner FilePermission + group FilePermission + others FilePermission +} + +// inode returns the mode of the file/inode containing inode type and permission information +// it supports windows for regular files but it doesn't matter if you use owner, group or others when checking permissions on windows +pub fn inode(path string) FileMode { + mut attr := C.stat{} + unsafe { C.stat(&char(path.str), &attr) } + mut typ := FileType.regular + if attr.st_mode & u32(C.S_IFMT) == u32(C.S_IFDIR) { + typ = .directory + } + $if !windows { + if attr.st_mode & u32(C.S_IFMT) == u32(C.S_IFCHR) { + typ = .character_device + } else if attr.st_mode & u32(C.S_IFMT) == u32(C.S_IFBLK) { + typ = .block_device + } else if attr.st_mode & u32(C.S_IFMT) == u32(C.S_IFIFO) { + typ = .fifo + } else if attr.st_mode & u32(C.S_IFMT) == u32(C.S_IFLNK) { + typ = .symbolic_link + } else if attr.st_mode & u32(C.S_IFMT) == u32(C.S_IFSOCK) { + typ = .socket + } + } + $if windows { + return FileMode{ + typ: typ + owner: FilePermission{ + read: (attr.st_mode & u32(C.S_IREAD)) != 0 + write: (attr.st_mode & u32(C.S_IWRITE)) != 0 + execute: (attr.st_mode & u32(C.S_IEXEC)) != 0 + } + group: FilePermission{ + read: (attr.st_mode & u32(C.S_IREAD)) != 0 + write: (attr.st_mode & u32(C.S_IWRITE)) != 0 + execute: (attr.st_mode & u32(C.S_IEXEC)) != 0 + } + others: FilePermission{ + read: (attr.st_mode & u32(C.S_IREAD)) != 0 + write: (attr.st_mode & u32(C.S_IWRITE)) != 0 + execute: (attr.st_mode & u32(C.S_IEXEC)) != 0 + } + } + } $else { + return FileMode{ + typ: typ + owner: FilePermission{ + read: (attr.st_mode & u32(C.S_IRUSR)) != 0 + write: (attr.st_mode & u32(C.S_IWUSR)) != 0 + execute: (attr.st_mode & u32(C.S_IXUSR)) != 0 + } + group: FilePermission{ + read: (attr.st_mode & u32(C.S_IRGRP)) != 0 + write: (attr.st_mode & u32(C.S_IWGRP)) != 0 + execute: (attr.st_mode & u32(C.S_IXGRP)) != 0 + } + others: FilePermission{ + read: (attr.st_mode & u32(C.S_IROTH)) != 0 + write: (attr.st_mode & u32(C.S_IWOTH)) != 0 + execute: (attr.st_mode & u32(C.S_IXOTH)) != 0 + } + } + } +} diff --git a/v_windows/v/vlib/os/inode_test.v b/v_windows/v/vlib/os/inode_test.v new file mode 100644 index 0000000..f716bc2 --- /dev/null +++ b/v_windows/v/vlib/os/inode_test.v @@ -0,0 +1,43 @@ +module os + +const ( + // tfolder will contain all the temporary files/subfolders made by + // the different tests. It would be removed in testsuite_end(), so + // individual os tests do not need to clean up after themselves. + tfolder = join_path(temp_dir(), 'v', 'tests', 'inode_test') +) + +fn testsuite_begin() { + eprintln('testsuite_begin, tfolder = $os.tfolder') + rmdir_all(os.tfolder) or {} + assert !is_dir(os.tfolder) + mkdir_all(os.tfolder) or { panic(err) } + chdir(os.tfolder) or {} + assert is_dir(os.tfolder) +} + +fn testsuite_end() { + chdir(wd_at_startup) or {} + rmdir_all(os.tfolder) or { panic(err) } + assert !is_dir(os.tfolder) +} + +fn test_inode_file_type() { + filename := './test1.txt' + mut file := open_file(filename, 'w', 0o600) or { return } + file.close() + mode := inode(filename) + rm(filename) or { panic(err) } + assert mode.typ == .regular +} + +fn test_inode_file_owner_permission() { + filename := './test2.txt' + mut file := open_file(filename, 'w', 0o600) or { return } + file.close() + mode := inode(filename) + rm(filename) or {} + assert mode.owner.read + assert mode.owner.write + assert !mode.owner.execute +} diff --git a/v_windows/v/vlib/os/notify/backend_default.c.v b/v_windows/v/vlib/os/notify/backend_default.c.v new file mode 100644 index 0000000..1a35c50 --- /dev/null +++ b/v_windows/v/vlib/os/notify/backend_default.c.v @@ -0,0 +1,6 @@ +module notify + +// Implement the API +pub fn new() ?FdNotifier { + panic('unsupported') +} diff --git a/v_windows/v/vlib/os/notify/backend_linux.c.v b/v_windows/v/vlib/os/notify/backend_linux.c.v new file mode 100644 index 0000000..1913913 --- /dev/null +++ b/v_windows/v/vlib/os/notify/backend_linux.c.v @@ -0,0 +1,206 @@ +module notify + +import time +import os + +#include + +struct C.epoll_event { + events u32 + data C.epoll_data_t +} + +[typedef] +union C.epoll_data_t { + ptr voidptr + fd int + u32 u32 + u64 u64 +} + +fn C.epoll_create1(int) int + +fn C.epoll_ctl(int, int, int, &C.epoll_event) int + +fn C.epoll_wait(int, &C.epoll_event, int, int) int + +// EpollNotifier provides methods that implement FdNotifier using the +// epoll I/O event notification facility (linux only) +[noinit] +struct EpollNotifier { + epoll_fd int +} + +// EpollEvent describes an event that occurred for a file descriptor in +// the watch list +[noinit] +struct EpollEvent { +pub: + fd int + kind FdEventType +} + +// new creates a new EpollNotifier +// The FdNotifier interface is returned to allow OS specific +// implementations without exposing the concrete type +pub fn new() ?FdNotifier { + fd := C.epoll_create1(0) // 0 indicates default behavior + if fd == -1 { + return error(os.posix_get_error_msg(C.errno)) + } + // Needed to circumvent V limitations + x := &EpollNotifier{ + epoll_fd: fd + } + return x +} + +const ( + epoll_read = u32(C.EPOLLIN) + epoll_write = u32(C.EPOLLOUT) + epoll_peer_hangup = u32(C.EPOLLRDHUP) + epoll_exception = u32(C.EPOLLPRI) + epoll_error = u32(C.EPOLLERR) + epoll_hangup = u32(C.EPOLLHUP) + epoll_edge_trigger = u32(C.EPOLLET) + epoll_one_shot = u32(C.EPOLLONESHOT) + epoll_wake_up = u32(C.EPOLLWAKEUP) + epoll_exclusive = u32(C.EPOLLEXCLUSIVE) +) + +// ctl is a helper method for add, modify, and remove +fn (mut en EpollNotifier) ctl(fd int, op int, mask u32) ? { + event := C.epoll_event{ + events: mask + data: C.epoll_data_t{ + fd: fd + } + } + if C.epoll_ctl(en.epoll_fd, op, fd, &event) == -1 { + return error(os.posix_get_error_msg(C.errno)) + } +} + +// add adds a file descriptor to the watch list +fn (mut en EpollNotifier) add(fd int, events FdEventType, conf ...FdConfigFlags) ? { + mask := flags_to_mask(events, ...conf) + en.ctl(fd, C.EPOLL_CTL_ADD, mask) ? +} + +// modify sets an existing entry in the watch list to the provided events and configuration +fn (mut en EpollNotifier) modify(fd int, events FdEventType, conf ...FdConfigFlags) ? { + mask := flags_to_mask(events, ...conf) + en.ctl(fd, C.EPOLL_CTL_MOD, mask) ? +} + +// remove removes a file descriptor from the watch list +fn (mut en EpollNotifier) remove(fd int) ? { + en.ctl(fd, C.EPOLL_CTL_DEL, 0) ? +} + +// wait waits to be notified of events on the watch list, +// returns at most 512 events +fn (mut en EpollNotifier) wait(timeout time.Duration) []FdEvent { + // arbitrary 512 limit; events will round robin on successive + // waits if the number exceeds this + // NOTE: we use a fixed size array here for stack allocation; this has + // the added bonus of making EpollNotifier thread safe + events := [512]C.epoll_event{} + // populate events with the new events + to := timeout.sys_milliseconds() + count := C.epoll_wait(en.epoll_fd, &events[0], events.len, to) + + if count > 0 { + mut arr := []FdEvent{cap: count} + for i := 0; i < count; i++ { + fd := unsafe { events[i].data.fd } + kind := event_mask_to_flag(events[i].events) + if kind.is_empty() { + // NOTE: tcc only reports the first event for some + // reason, leaving subsequent structs in the array as 0 + // (or possibly garbage) + panic('encountered an empty event kind; this is most likely due to using tcc') + } + arr << &EpollEvent{ + fd: fd + kind: kind + } + } + return arr + } + return [] +} + +// close closes the EpollNotifier, +// any successive calls to add, modify, remove, and wait should fail +fn (mut en EpollNotifier) close() ? { + if C.close(en.epoll_fd) == -1 { + return error(os.posix_get_error_msg(C.errno)) + } +} + +// event_mask_to_flag is a helper function that converts a bitmask +// returned by epoll_wait to FdEventType +fn event_mask_to_flag(mask u32) FdEventType { + mut flags := FdEventType{} + + if mask & notify.epoll_read != 0 { + flags.set(.read) + } + if mask & notify.epoll_write != 0 { + flags.set(.write) + } + if mask & notify.epoll_peer_hangup != 0 { + flags.set(.peer_hangup) + } + if mask & notify.epoll_exception != 0 { + flags.set(.exception) + } + if mask & notify.epoll_error != 0 { + flags.set(.error) + } + if mask & notify.epoll_hangup != 0 { + flags.set(.hangup) + } + + return flags +} + +// flags_to_mask is a helper function that converts FdEventType and +// FdConfigFlags to a bitmask used by the C functions +fn flags_to_mask(events FdEventType, confs ...FdConfigFlags) u32 { + mut mask := u32(0) + if events.has(.read) { + mask |= notify.epoll_read + } + if events.has(.write) { + mask |= notify.epoll_write + } + if events.has(.peer_hangup) { + mask |= notify.epoll_peer_hangup + } + if events.has(.exception) { + mask |= notify.epoll_exception + } + if events.has(.error) { + mask |= notify.epoll_error + } + if events.has(.hangup) { + mask |= notify.epoll_hangup + } + for conf in confs { + if conf.has(.edge_trigger) { + mask |= notify.epoll_edge_trigger + } + if conf.has(.one_shot) { + mask |= notify.epoll_one_shot + } + if conf.has(.wake_up) { + mask |= notify.epoll_wake_up + } + if conf.has(.exclusive) { + mask |= notify.epoll_exclusive + } + } + return mask +} diff --git a/v_windows/v/vlib/os/notify/notify.v b/v_windows/v/vlib/os/notify/notify.v new file mode 100644 index 0000000..b49dad3 --- /dev/null +++ b/v_windows/v/vlib/os/notify/notify.v @@ -0,0 +1,35 @@ +module notify + +import time + +// Backends should provide a `new() ?FdNotifier` function +pub interface FdNotifier { + add(fd int, events FdEventType, conf ...FdConfigFlags) ? + modify(fd int, events FdEventType, conf ...FdConfigFlags) ? + remove(fd int) ? + wait(timeout time.Duration) []FdEvent + close() ? +} + +pub interface FdEvent { + fd int + kind FdEventType +} + +[flag] +pub enum FdEventType { + read + write + peer_hangup + exception + error + hangup +} + +[flag] +pub enum FdConfigFlags { + edge_trigger + one_shot + wake_up + exclusive +} diff --git a/v_windows/v/vlib/os/notify/notify_test.v b/v_windows/v/vlib/os/notify/notify_test.v new file mode 100644 index 0000000..253c94f --- /dev/null +++ b/v_windows/v/vlib/os/notify/notify_test.v @@ -0,0 +1,155 @@ +import os +import os.notify + +// make a pipe and return the (read, write) file descriptors +fn make_pipe() ?(int, int) { + $if linux { + pipefd := [2]int{} + if C.pipe(&pipefd[0]) != 0 { + return error('error $C.errno: ' + os.posix_get_error_msg(C.errno)) + } + return pipefd[0], pipefd[1] + } + return -1, -1 +} + +fn test_level_trigger() ? { + // currently only linux is supported + $if linux { + mut notifier := notify.new() ? + reader, writer := make_pipe() ? + defer { + os.fd_close(reader) + os.fd_close(writer) + notifier.close() or {} + } + notifier.add(reader, .read) ? + + os.fd_write(writer, 'foobar') + check_read_event(notifier, reader, 'foo') + check_read_event(notifier, reader, 'bar') + + assert notifier.wait(0).len == 0 + } +} + +fn test_edge_trigger() ? { + // currently only linux is supported + $if linux { + mut notifier := notify.new() ? + reader, writer := make_pipe() ? + defer { + os.fd_close(reader) + os.fd_close(writer) + notifier.close() or {} + } + notifier.add(reader, .read, .edge_trigger) ? + + os.fd_write(writer, 'foobar') + check_read_event(notifier, reader, 'foo') + + assert notifier.wait(0).len == 0 + + os.fd_write(writer, 'baz') + // we do not get an event because there is still data + // to be read + assert notifier.wait(0).len == 0 + } +} + +fn test_one_shot() ? { + $if linux { + mut notifier := notify.new() ? + reader, writer := make_pipe() ? + defer { + os.fd_close(reader) + os.fd_close(writer) + notifier.close() or {} + } + notifier.add(reader, .read, .one_shot) ? + + os.fd_write(writer, 'foobar') + check_read_event(notifier, reader, 'foo') + os.fd_write(writer, 'baz') + + assert notifier.wait(0).len == 0 + + // rearm + notifier.modify(reader, .read) ? + check_read_event(notifier, reader, 'barbaz') + } +} + +fn test_hangup() ? { + $if linux { + mut notifier := notify.new() ? + reader, writer := make_pipe() ? + defer { + os.fd_close(reader) + notifier.close() or {} + } + notifier.add(reader, .hangup) ? + + assert notifier.wait(0).len == 0 + + // closing on the writer end of the pipe will + // cause a hangup on the reader end + os.fd_close(writer) + events := notifier.wait(0) + assert events.len == 1 + assert events[0].fd == reader + assert events[0].kind.has(.hangup) + } +} + +fn test_write() ? { + $if linux { + mut notifier := notify.new() ? + reader, writer := make_pipe() ? + defer { + os.fd_close(reader) + os.fd_close(writer) + notifier.close() or {} + } + + notifier.add(reader, .write) ? + assert notifier.wait(0).len == 0 + + notifier.add(writer, .write) ? + events := notifier.wait(0) + assert events.len == 1 + assert events[0].fd == writer + assert events[0].kind.has(.write) + } +} + +fn test_remove() ? { + $if linux { + mut notifier := notify.new() ? + reader, writer := make_pipe() ? + defer { + os.fd_close(reader) + os.fd_close(writer) + notifier.close() or {} + } + + // level triggered - will keep getting events while + // there is data to read + notifier.add(reader, .read) ? + os.fd_write(writer, 'foobar') + assert notifier.wait(0).len == 1 + assert notifier.wait(0).len == 1 + + notifier.remove(reader) ? + assert notifier.wait(0).len == 0 + } +} + +fn check_read_event(notifier notify.FdNotifier, reader_fd int, expected string) { + events := notifier.wait(0) + assert events.len == 1 + assert events[0].fd == reader_fd + assert events[0].kind.has(.read) + s, _ := os.fd_read(events[0].fd, expected.len) + assert s == expected +} diff --git a/v_windows/v/vlib/os/os.c.v b/v_windows/v/vlib/os/os.c.v new file mode 100644 index 0000000..12369d0 --- /dev/null +++ b/v_windows/v/vlib/os/os.c.v @@ -0,0 +1,1010 @@ +module os + +#include // #include +#include + +pub const ( + args = []string{} +) + +struct C.dirent { + d_name [256]char +} + +fn C.readdir(voidptr) &C.dirent + +fn C.readlink(pathname &char, buf &char, bufsiz size_t) int + +fn C.getline(voidptr, voidptr, voidptr) int + +fn C.ftell(fp voidptr) i64 + +fn C.sigaction(int, voidptr, int) int + +fn C.open(&char, int, ...int) int + +fn C.fdopen(fd int, mode &char) &C.FILE + +fn C.ferror(stream &C.FILE) int + +fn C.feof(stream &C.FILE) int + +fn C.CopyFile(&u16, &u16, bool) int + +// fn C.lstat(charptr, voidptr) u64 + +fn C._wstat64(&u16, voidptr) u64 + +fn C.chown(&char, int, int) int + +fn C.ftruncate(voidptr, u64) int + +fn C._chsize_s(voidptr, u64) int + +// fn C.proc_pidpath(int, byteptr, int) int +struct C.stat { + st_size u64 + st_mode u32 + st_mtime int +} + +struct C.__stat64 { + st_size u64 + st_mode u32 + st_mtime int +} + +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 { + d_name &byte +} + +// read_bytes returns all bytes read from file in `path`. +[manualfree] +pub fn read_bytes(path string) ?[]byte { + mut fp := vfopen(path, 'rb') ? + defer { + C.fclose(fp) + } + cseek := C.fseek(fp, 0, C.SEEK_END) + if cseek != 0 { + return error('fseek failed') + } + fsize := C.ftell(fp) + if fsize < 0 { + return error('ftell failed') + } + C.rewind(fp) + mut res := []byte{len: int(fsize)} + nr_read_elements := int(C.fread(res.data, fsize, 1, fp)) + if nr_read_elements == 0 && fsize > 0 { + return error('fread failed') + } + res.trim(nr_read_elements * int(fsize)) + return res +} + +// read_file reads the file in `path` and returns the contents. +pub fn read_file(path string) ?string { + mode := 'rb' + mut fp := vfopen(path, mode) ? + defer { + C.fclose(fp) + } + cseek := C.fseek(fp, 0, C.SEEK_END) + if cseek != 0 { + return error('fseek failed') + } + fsize := C.ftell(fp) + if fsize < 0 { + return error('ftell failed') + } + // C.fseek(fp, 0, SEEK_SET) // same as `C.rewind(fp)` below + C.rewind(fp) + unsafe { + mut str := malloc_noscan(int(fsize) + 1) + nelements := int(C.fread(str, 1, fsize, fp)) + is_eof := int(C.feof(fp)) + is_error := int(C.ferror(fp)) + if is_eof == 0 && is_error != 0 { + free(str) + return error('fread failed') + } + str[nelements] = 0 + if nelements == 0 { + // It is highly likely that the file was a virtual file from + // /sys or /proc, with information generated on the fly, so + // fsize was not reliably reported. Using vstring() here is + // slower (it calls strlen internally), but will return more + // consistent results. + // For example reading from /sys/class/sound/card0/id produces + // a `PCH\n` string, but fsize is 4096, and otherwise you would + // get a V string with .len = 4096 and .str = "PCH\n\\000". + return str.vstring() + } + return str.vstring_with_len(nelements) + } +} + +// ***************************** OS ops ************************ +// +// truncate changes the size of the file located in `path` to `len`. +// Note that changing symbolic links on Windows only works as admin. +pub fn truncate(path string, len u64) ? { + fp := C.open(&char(path.str), o_wronly | o_trunc, 0) + defer { + C.close(fp) + } + if fp < 0 { + return error_with_code(posix_get_error_msg(C.errno), C.errno) + } + $if windows { + if C._chsize_s(fp, len) != 0 { + return error_with_code(posix_get_error_msg(C.errno), C.errno) + } + } $else { + if C.ftruncate(fp, len) != 0 { + return error_with_code(posix_get_error_msg(C.errno), C.errno) + } + } +} + +fn eprintln_unknown_file_size() { + eprintln('os.file_size() Cannot determine file-size: ' + posix_get_error_msg(C.errno)) +} + +// file_size returns the size of the file located in `path`. +// If an error occurs it returns 0. +// Note that use of this on symbolic links on Windows returns always 0. +pub fn file_size(path string) u64 { + mut s := C.stat{} + unsafe { + $if x64 { + $if windows { + mut swin := C.__stat64{} + if C._wstat64(path.to_wide(), voidptr(&swin)) != 0 { + eprintln_unknown_file_size() + return 0 + } + return swin.st_size + } $else { + if C.stat(&char(path.str), &s) != 0 { + eprintln_unknown_file_size() + return 0 + } + return u64(s.st_size) + } + } + $if x32 { + $if debug { + eprintln('Using os.file_size() on 32bit systems may not work on big files.') + } + $if windows { + if C._wstat(path.to_wide(), voidptr(&s)) != 0 { + eprintln_unknown_file_size() + return 0 + } + return u64(s.st_size) + } $else { + if C.stat(&char(path.str), &s) != 0 { + eprintln_unknown_file_size() + return 0 + } + return u64(s.st_size) + } + } + } + return 0 +} + +// mv moves files or folders from `src` to `dst`. +pub fn mv(src string, dst string) ? { + mut rdst := dst + if is_dir(rdst) { + rdst = join_path(rdst.trim_right(path_separator), file_name(src.trim_right(path_separator))) + } + $if windows { + w_src := src.replace('/', '\\') + w_dst := rdst.replace('/', '\\') + ret := C._wrename(w_src.to_wide(), w_dst.to_wide()) + if ret != 0 { + return error_with_code('failed to rename $src to $dst', int(ret)) + } + } $else { + ret := C.rename(&char(src.str), &char(rdst.str)) + if ret != 0 { + return error_with_code('failed to rename $src to $dst', int(ret)) + } + } +} + +// cp copies files or folders from `src` to `dst`. +pub fn cp(src string, dst string) ? { + $if windows { + w_src := src.replace('/', '\\') + w_dst := dst.replace('/', '\\') + if C.CopyFile(w_src.to_wide(), w_dst.to_wide(), false) == 0 { + result := C.GetLastError() + return error_with_code('failed to copy $src to $dst', int(result)) + } + } $else { + fp_from := C.open(&char(src.str), C.O_RDONLY, 0) + if fp_from < 0 { // Check if file opened + return error_with_code('cp: failed to open $src', int(fp_from)) + } + fp_to := C.open(&char(dst.str), C.O_WRONLY | C.O_CREAT | C.O_TRUNC, C.S_IWUSR | C.S_IRUSR) + if fp_to < 0 { // Check if file opened (permissions problems ...) + C.close(fp_from) + return error_with_code('cp (permission): failed to write to $dst (fp_to: $fp_to)', + int(fp_to)) + } + // TODO use defer{} to close files in case of error or return. + // Currently there is a C-Error when building. + mut buf := [1024]byte{} + mut count := 0 + for { + count = C.read(fp_from, &buf[0], sizeof(buf)) + if count == 0 { + break + } + if C.write(fp_to, &buf[0], count) < 0 { + C.close(fp_to) + C.close(fp_from) + return error_with_code('cp: failed to write to $dst', int(-1)) + } + } + from_attr := C.stat{} + unsafe { + C.stat(&char(src.str), &from_attr) + } + if C.chmod(&char(dst.str), from_attr.st_mode) < 0 { + C.close(fp_to) + C.close(fp_from) + return error_with_code('failed to set permissions for $dst', int(-1)) + } + C.close(fp_to) + C.close(fp_from) + } +} + +// vfopen returns an opened C file, given its path and open mode. +// NB: os.vfopen is useful for compatibility with C libraries, that expect `FILE *`. +// If you write pure V code, os.create or os.open are more convenient. +pub fn vfopen(path string, mode string) ?&C.FILE { + if path.len == 0 { + return error('vfopen called with ""') + } + mut fp := voidptr(0) + $if windows { + fp = C._wfopen(path.to_wide(), mode.to_wide()) + } $else { + fp = C.fopen(&char(path.str), &char(mode.str)) + } + if isnil(fp) { + return error('failed to open file "$path"') + } else { + return fp + } +} + +// fileno returns the file descriptor of an opened C file. +pub fn fileno(cfile voidptr) int { + $if windows { + return C._fileno(cfile) + } $else { + mut cfile_casted := &C.FILE(0) // FILE* cfile_casted = 0; + cfile_casted = cfile + // Required on FreeBSD/OpenBSD/NetBSD as stdio.h defines fileno(..) with a macro + // that performs a field access on its argument without casting from void*. + return C.fileno(cfile_casted) + } +} + +// vpopen system starts the specified command, waits for it to complete, and returns its code. +fn vpopen(path string) voidptr { + // *C.FILE { + $if windows { + mode := 'rb' + wpath := path.to_wide() + return C._wpopen(wpath, mode.to_wide()) + } $else { + cpath := path.str + return C.popen(&char(cpath), c'r') + } +} + +fn posix_wait4_to_exit_status(waitret int) (int, bool) { + $if windows { + return waitret, false + } $else { + mut ret := 0 + mut is_signaled := true + // (see man system, man 2 waitpid: C macro WEXITSTATUS section) + if C.WIFEXITED(waitret) { + ret = C.WEXITSTATUS(waitret) + is_signaled = false + } else if C.WIFSIGNALED(waitret) { + ret = C.WTERMSIG(waitret) + is_signaled = true + } + return ret, is_signaled + } +} + +// posix_get_error_msg return error code representation in string. +pub fn posix_get_error_msg(code int) string { + ptr_text := C.strerror(code) // voidptr? + if ptr_text == 0 { + return '' + } + return unsafe { tos3(ptr_text) } +} + +// vpclose will close a file pointer opened with `vpopen`. +fn vpclose(f voidptr) int { + $if windows { + return C._pclose(f) + } $else { + ret, _ := posix_wait4_to_exit_status(C.pclose(f)) + return ret + } +} + +// system works like `exec`, but only returns a return code. +pub fn system(cmd string) int { + // if cmd.contains(';') || cmd.contains('&&') || cmd.contains('||') || cmd.contains('\n') { + // TODO remove panic + // panic(';, &&, || and \\n are not allowed in shell commands') + // } + mut ret := 0 + $if windows { + // overcome bug in system & _wsystem (cmd) when first char is quote `"` + wcmd := if cmd.len > 1 && cmd[0] == `"` && cmd[1] != `"` { '"$cmd"' } else { cmd } + unsafe { + ret = C._wsystem(wcmd.to_wide()) + } + } $else { + $if ios { + unsafe { + arg := [c'/bin/sh', c'-c', &byte(cmd.str), 0] + pid := 0 + ret = C.posix_spawn(&pid, c'/bin/sh', 0, 0, arg.data, 0) + status := 0 + ret = C.waitpid(pid, &status, 0) + if C.WIFEXITED(status) { + ret = C.WEXITSTATUS(status) + } + } + } $else { + unsafe { + ret = C.system(&char(cmd.str)) + } + } + } + if ret == -1 { + print_c_errno() + } + $if !windows { + pret, is_signaled := posix_wait4_to_exit_status(ret) + if is_signaled { + println('Terminated by signal ${ret:2d} (' + sigint_to_signal_name(pret) + ')') + } + ret = pret + } + return ret +} + +// exists returns true if `path` (file or directory) exists. +pub fn exists(path string) bool { + $if windows { + p := path.replace('/', '\\') + return C._waccess(p.to_wide(), f_ok) != -1 + } $else { + return C.access(&char(path.str), f_ok) != -1 + } +} + +// is_executable returns `true` if `path` is executable. +pub fn is_executable(path string) bool { + $if windows { + // NB: https://docs.microsoft.com/en-us/cpp/c-runtime-library/reference/access-waccess?view=vs-2019 + // i.e. there is no X bit there, the modes can be: + // 00 Existence only + // 02 Write-only + // 04 Read-only + // 06 Read and write + p := real_path(path) + return (exists(p) && p.ends_with('.exe')) + } + $if solaris { + statbuf := C.stat{} + unsafe { + if C.stat(&char(path.str), &statbuf) != 0 { + return false + } + } + return (int(statbuf.st_mode) & (s_ixusr | s_ixgrp | s_ixoth)) != 0 + } + return C.access(&char(path.str), x_ok) != -1 +} + +// is_writable returns `true` if `path` is writable. +pub fn is_writable(path string) bool { + $if windows { + p := path.replace('/', '\\') + return C._waccess(p.to_wide(), w_ok) != -1 + } $else { + return C.access(&char(path.str), w_ok) != -1 + } +} + +// is_readable returns `true` if `path` is readable. +pub fn is_readable(path string) bool { + $if windows { + p := path.replace('/', '\\') + return C._waccess(p.to_wide(), r_ok) != -1 + } $else { + return C.access(&char(path.str), r_ok) != -1 + } +} + +// rm removes file in `path`. +pub fn rm(path string) ? { + mut rc := 0 + $if windows { + rc = C._wremove(path.to_wide()) + } $else { + rc = C.remove(&char(path.str)) + } + if rc == -1 { + return error('Failed to remove "$path": ' + posix_get_error_msg(C.errno)) + } + // C.unlink(path.cstr()) +} + +// rmdir removes a specified directory. +pub fn rmdir(path string) ? { + $if windows { + rc := C.RemoveDirectory(path.to_wide()) + if rc == 0 { + // https://docs.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-removedirectorya - 0 is failure + return error('Failed to remove "$path": ' + posix_get_error_msg(C.errno)) + } + } $else { + rc := C.rmdir(&char(path.str)) + if rc == -1 { + return error(posix_get_error_msg(C.errno)) + } + } +} + +// print_c_errno will print the current value of `C.errno`. +fn print_c_errno() { + e := C.errno + se := unsafe { tos_clone(&byte(C.strerror(e))) } + println('errno=$e err=$se') +} + +// get_raw_line returns a one-line string from stdin along with '\n' if there is any. +pub fn get_raw_line() string { + $if windows { + unsafe { + max_line_chars := 256 + buf := malloc_noscan(max_line_chars * 2) + h_input := C.GetStdHandle(C.STD_INPUT_HANDLE) + mut bytes_read := u32(0) + if is_atty(0) > 0 { + x := C.ReadConsole(h_input, buf, max_line_chars * 2, &bytes_read, 0) + if !x { + return tos(buf, 0) + } + return string_from_wide2(&u16(buf), int(bytes_read)) + } + mut offset := 0 + for { + pos := buf + offset + res := C.ReadFile(h_input, pos, 1, C.LPDWORD(&bytes_read), 0) + if !res && offset == 0 { + return tos(buf, 0) + } + if !res || bytes_read == 0 { + break + } + if *pos == `\n` || *pos == `\r` { + offset++ + break + } + offset++ + } + return buf.vstring_with_len(offset) + } + } $else { + max := size_t(0) + buf := &char(0) + nr_chars := unsafe { C.getline(&buf, &max, C.stdin) } + return unsafe { tos(&byte(buf), if nr_chars < 0 { 0 } else { nr_chars }) } + } +} + +// get_raw_stdin will get the raw input from stdin. +pub fn get_raw_stdin() []byte { + $if windows { + unsafe { + block_bytes := 512 + mut old_size := block_bytes + mut buf := malloc_noscan(block_bytes) + h_input := C.GetStdHandle(C.STD_INPUT_HANDLE) + mut bytes_read := 0 + mut offset := 0 + for { + pos := buf + offset + res := C.ReadFile(h_input, pos, block_bytes, C.LPDWORD(&bytes_read), 0) + offset += bytes_read + if !res { + break + } + new_size := offset + block_bytes + (block_bytes - bytes_read) + buf = realloc_data(buf, old_size, new_size) + old_size = new_size + } + return array{ + element_size: 1 + data: voidptr(buf) + len: offset + cap: offset + } + } + } $else { + max := size_t(0) + buf := &char(0) + nr_chars := unsafe { C.getline(&buf, &max, C.stdin) } + return array{ + element_size: 1 + data: voidptr(buf) + len: if nr_chars < 0 { 0 } else { nr_chars } + cap: int(max) + } + } +} + +// read_file_array reads an array of `T` values from file `path`. +pub fn read_file_array(path string) []T { + a := T{} + tsize := int(sizeof(a)) + // prepare for reading, get current file size + mut fp := vfopen(path, 'rb') or { return []T{} } + C.fseek(fp, 0, C.SEEK_END) + fsize := C.ftell(fp) + C.rewind(fp) + // read the actual data from the file + len := fsize / tsize + buf := unsafe { malloc_noscan(int(fsize)) } + nread := C.fread(buf, tsize, len, fp) + C.fclose(fp) + return unsafe { + array{ + element_size: tsize + data: buf + len: int(nread) + cap: int(len) + } + } +} + +pub fn on_segfault(f voidptr) { + $if windows { + return + } + $if macos { + C.printf(c'TODO') + /* + mut sa := C.sigaction{} + C.memset(&sa, 0, sizeof(C.sigaction_size)) + C.sigemptyset(&sa.sa_mask) + sa.sa_sigaction = f + sa.sa_flags = C.SA_SIGINFO + C.sigaction(C.SIGSEGV, &sa, 0) + */ + } +} + +// executable returns the path name of the executable that started the current +// process. +[manualfree] +pub fn executable() string { + $if linux { + mut xresult := vcalloc_noscan(max_path_len) + count := C.readlink(c'/proc/self/exe', &char(xresult), max_path_len) + if count < 0 { + eprintln('os.executable() failed at reading /proc/self/exe to get exe path') + return executable_fallback() + } + return unsafe { xresult.vstring() } + } + $if windows { + max := 512 + size := max * 2 // max_path_len * sizeof(wchar_t) + mut result := unsafe { &u16(vcalloc_noscan(size)) } + len := C.GetModuleFileName(0, result, max) + // determine if the file is a windows symlink + attrs := C.GetFileAttributesW(result) + is_set := attrs & 0x400 // FILE_ATTRIBUTE_REPARSE_POINT + if is_set != 0 { // it's a windows symlink + // gets handle with GENERIC_READ, FILE_SHARE_READ, 0, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, 0 + file := C.CreateFile(result, 0x80000000, 1, 0, 3, 0x80, 0) + if file != voidptr(-1) { + final_path := unsafe { &u16(vcalloc_noscan(size)) } + // https://docs.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-getfinalpathnamebyhandlew + final_len := C.GetFinalPathNameByHandleW(file, final_path, size, 0) + if final_len < size { + ret := unsafe { string_from_wide2(final_path, final_len) } + // remove '\\?\' from beginning (see link above) + return ret[4..] + } else { + eprintln('os.executable() saw that the executable file path was too long') + } + } + C.CloseHandle(file) + } + return unsafe { string_from_wide2(result, len) } + } + $if macos { + mut result := vcalloc_noscan(max_path_len) + pid := C.getpid() + ret := proc_pidpath(pid, result, max_path_len) + if ret <= 0 { + eprintln('os.executable() failed at calling proc_pidpath with pid: $pid . proc_pidpath returned $ret ') + return executable_fallback() + } + return unsafe { result.vstring() } + } + $if freebsd { + mut result := vcalloc_noscan(max_path_len) + mib := [1 /* CTL_KERN */, 14 /* KERN_PROC */, 12 /* KERN_PROC_PATHNAME */, -1] + size := max_path_len + unsafe { C.sysctl(mib.data, 4, result, &size, 0, 0) } + return unsafe { result.vstring() } + } + // "Sadly there is no way to get the full path of the executed file in OpenBSD." + $if openbsd { + } + $if solaris { + } + $if haiku { + } + $if netbsd { + mut result := vcalloc_noscan(max_path_len) + count := C.readlink(c'/proc/curproc/exe', &char(result), max_path_len) + if count < 0 { + eprintln('os.executable() failed at reading /proc/curproc/exe to get exe path') + return executable_fallback() + } + return unsafe { result.vstring_with_len(count) } + } + $if dragonfly { + mut result := vcalloc_noscan(max_path_len) + count := C.readlink(c'/proc/curproc/file', &char(result), max_path_len) + if count < 0 { + eprintln('os.executable() failed at reading /proc/curproc/file to get exe path') + return executable_fallback() + } + return unsafe { result.vstring_with_len(count) } + } + return executable_fallback() +} + +// is_dir returns a `bool` indicating whether the given `path` is a directory. +pub fn is_dir(path string) bool { + $if windows { + w_path := path.replace('/', '\\') + attr := C.GetFileAttributesW(w_path.to_wide()) + if attr == u32(C.INVALID_FILE_ATTRIBUTES) { + return false + } + if int(attr) & C.FILE_ATTRIBUTE_DIRECTORY != 0 { + return true + } + return false + } $else { + statbuf := C.stat{} + if unsafe { C.stat(&char(path.str), &statbuf) } != 0 { + return false + } + // ref: https://code.woboq.org/gcc/include/sys/stat.h.html + val := int(statbuf.st_mode) & s_ifmt + return val == s_ifdir + } +} + +// is_link returns a boolean indicating whether `path` is a link. +pub fn is_link(path string) bool { + $if windows { + path_ := path.replace('/', '\\') + attr := C.GetFileAttributesW(path_.to_wide()) + return int(attr) != int(C.INVALID_FILE_ATTRIBUTES) && (attr & 0x400) != 0 + } $else { + statbuf := C.stat{} + if C.lstat(&char(path.str), &statbuf) != 0 { + return false + } + return int(statbuf.st_mode) & s_ifmt == s_iflnk + } +} + +// chdir changes the current working directory to the new directory in `path`. +pub fn chdir(path string) ? { + ret := $if windows { C._wchdir(path.to_wide()) } $else { C.chdir(&char(path.str)) } + if ret == -1 { + return error_with_code(posix_get_error_msg(C.errno), C.errno) + } +} + +// getwd returns the absolute path of the current directory. +pub fn getwd() string { + $if windows { + max := 512 // max_path_len * sizeof(wchar_t) + unsafe { + buf := &u16(vcalloc_noscan(max * 2)) + if C._wgetcwd(buf, max) == 0 { + free(buf) + return '' + } + return string_from_wide(buf) + } + } $else { + buf := vcalloc_noscan(max_path_len) + unsafe { + if C.getcwd(&char(buf), max_path_len) == 0 { + free(buf) + return '' + } + return buf.vstring() + } + } +} + +// real_path returns the full absolute path for fpath, with all relative ../../, symlinks and so on resolved. +// See http://pubs.opengroup.org/onlinepubs/9699919799/functions/realpath.html +// Also https://insanecoding.blogspot.com/2007/11/pathmax-simply-isnt.html +// and https://insanecoding.blogspot.com/2007/11/implementing-realpath-in-c.html +// NB: this particular rabbit hole is *deep* ... +[manualfree] +pub fn real_path(fpath string) string { + mut res := '' + $if windows { + size := max_path_len * 2 + // gets handle with GENERIC_READ, FILE_SHARE_READ, 0, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, 0 + // use C.CreateFile(fpath.to_wide(), 0x80000000, 1, 0, 3, 0x80, 0) instead of get_file_handle + // try to open the file to get symbolic link path + file := C.CreateFile(fpath.to_wide(), 0x80000000, 1, 0, 3, 0x80, 0) + if file != voidptr(-1) { + mut fullpath := unsafe { &u16(vcalloc_noscan(size)) } + // https://docs.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-getfinalpathnamebyhandlew + final_len := C.GetFinalPathNameByHandleW(file, fullpath, size, 0) + C.CloseHandle(file) + if final_len < size { + rt := unsafe { string_from_wide2(fullpath, final_len) } + res = rt[4..] + } else { + unsafe { free(fullpath) } + eprintln('os.real_path() saw that the file path was too long') + return fpath.clone() + } + } else { + // if it is not a file C.CreateFile doesn't gets a file handle, use GetFullPath instead + mut fullpath := unsafe { &u16(vcalloc_noscan(max_path_len * 2)) } + // TODO: check errors if path len is not enough + ret := C.GetFullPathName(fpath.to_wide(), max_path_len, fullpath, 0) + if ret == 0 { + unsafe { free(fullpath) } + return fpath.clone() + } + res = unsafe { string_from_wide(fullpath) } + } + } $else { + mut fullpath := vcalloc_noscan(max_path_len) + ret := &char(C.realpath(&char(fpath.str), &char(fullpath))) + if ret == 0 { + unsafe { free(fullpath) } + return fpath.clone() + } + res = unsafe { fullpath.vstring() } + } + unsafe { normalize_drive_letter(res) } + return res +} + +[direct_array_access; manualfree; unsafe] +fn normalize_drive_letter(path string) { + $if !windows { + return + } + // normalize_drive_letter is needed, because + // a path like c:\nv\.bin (note the small `c`) in %PATH, + // is NOT recognized by cmd.exe (and probably other programs too)... + // Capital drive letters do work fine. + if path.len > 2 && path[0] >= `a` && path[0] <= `z` && path[1] == `:` + && path[2] == path_separator[0] { + unsafe { + x := &path.str[0] + (*x) = *x - 32 + } + } +} + +// fork will fork the current system process and return the pid of the fork. +pub fn fork() int { + mut pid := -1 + $if !windows { + pid = C.fork() + } + $if windows { + panic('os.fork not supported in windows') // TODO + } + return pid +} + +// wait blocks the calling process until one of its child processes exits or a signal is received. +// After child process terminates, parent continues its execution after wait system call instruction. +pub fn wait() int { + mut pid := -1 + $if !windows { + pid = C.wait(0) + } + $if windows { + panic('os.wait not supported in windows') // TODO + } + return pid +} + +// file_last_mod_unix returns the "last modified" time stamp of file in `path`. +pub fn file_last_mod_unix(path string) int { + attr := C.stat{} + // # struct stat attr; + unsafe { C.stat(&char(path.str), &attr) } + // # stat(path.str, &attr); + return attr.st_mtime + // # return attr.st_mtime ; +} + +// flush will flush the stdout buffer. +pub fn flush() { + C.fflush(C.stdout) +} + +// chmod change file access attributes of `path` to `mode`. +// Octals like `0o600` can be used. +pub fn chmod(path string, mode int) ? { + if C.chmod(&char(path.str), mode) != 0 { + return error_with_code('chmod failed: ' + posix_get_error_msg(C.errno), C.errno) + } +} + +// chown changes the owner and group attributes of `path` to `owner` and `group`. +pub fn chown(path string, owner int, group int) ? { + $if windows { + return error('os.chown() not implemented for Windows') + } $else { + if C.chown(&char(path.str), owner, group) != 0 { + return error_with_code(posix_get_error_msg(C.errno), C.errno) + } + } +} + +// open_append opens `path` file for appending. +pub fn open_append(path string) ?File { + mut file := File{} + $if windows { + wpath := path.replace('/', '\\').to_wide() + mode := 'ab' + file = File{ + cfile: C._wfopen(wpath, mode.to_wide()) + } + } $else { + cpath := path.str + file = File{ + cfile: C.fopen(&char(cpath), c'ab') + } + } + if isnil(file.cfile) { + return error('failed to create(append) file "$path"') + } + file.is_opened = true + return file +} + +// execvp - loads and executes a new child process, *in place* of the current process. +// The child process executable is located in `cmdpath`. +// The arguments, that will be passed to it are in `args`. +// NB: this function will NOT return when successfull, since +// the child process will take control over execution. +pub fn execvp(cmdpath string, cmdargs []string) ? { + mut cargs := []&char{} + cargs << &char(cmdpath.str) + for i in 0 .. cmdargs.len { + cargs << &char(cmdargs[i].str) + } + cargs << &char(0) + mut res := int(0) + $if windows { + res = C._execvp(&char(cmdpath.str), cargs.data) + } $else { + res = C.execvp(&char(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. +// The child process executable is located in `cmdpath`. +// The arguments, that will be passed to it are in `args`. +// You can pass environment variables to through `envs`. +// NB: this function will NOT return when successfull, since +// the child process will take control over execution. +pub fn execve(cmdpath string, cmdargs []string, envs []string) ? { + mut cargv := []&char{} + mut cenvs := []&char{} + cargv << &char(cmdpath.str) + for i in 0 .. cmdargs.len { + cargv << &char(cmdargs[i].str) + } + for i in 0 .. envs.len { + cenvs << &char(envs[i].str) + } + cargv << &char(0) + cenvs << &char(0) + mut res := int(0) + $if windows { + res = C._execve(&char(cmdpath.str), cargv.data, cenvs.data) + } $else { + res = C.execve(&char(cmdpath.str), cargv.data, cenvs.data) + } + // NB: normally execve does not return at all. + // If it returns, then something went wrong... + if res == -1 { + return error_with_code(posix_get_error_msg(C.errno), C.errno) + } +} + +// is_atty returns 1 if the `fd` file descriptor is open and refers to a terminal +pub fn is_atty(fd int) int { + $if windows { + mut mode := u32(0) + osfh := voidptr(C._get_osfhandle(fd)) + C.GetConsoleMode(osfh, voidptr(&mode)) + return int(mode) + } $else { + return C.isatty(fd) + } +} + +// write_file_array writes the data in `buffer` to a file in `path`. +pub fn write_file_array(path string, buffer array) ? { + mut f := create(path) ? + unsafe { f.write_full_buffer(buffer.data, size_t(buffer.len * buffer.element_size)) ? } + f.close() +} + +pub fn glob(patterns ...string) ?[]string { + mut matches := []string{} + for pattern in patterns { + native_glob_pattern(pattern, mut matches) ? + } + matches.sort() + return matches +} diff --git a/v_windows/v/vlib/os/os.js.v b/v_windows/v/vlib/os/os.js.v new file mode 100644 index 0000000..6ba3c77 --- /dev/null +++ b/v_windows/v/vlib/os/os.js.v @@ -0,0 +1,97 @@ +module os + +#const $fs = require('fs'); +#const $path = require('path'); + +pub const ( + path_delimiter = '/' + path_separator = '/' + args = []string{} +) + +$if js_node { + #$process.argv.forEach(function(val,index) { args.arr[index] = new string(val); }) +} + +// real_path returns the full absolute path for fpath, with all relative ../../, symlinks and so on resolved. +// See http://pubs.opengroup.org/onlinepubs/9699919799/functions/realpath.html +// Also https://insanecoding.blogspot.com/2007/11/pathmax-simply-isnt.html +// and https://insanecoding.blogspot.com/2007/11/implementing-realpath-in-c.html +// NB: this particular rabbit hole is *deep* ... +pub fn real_path(fpath string) string { + $if js_node { + mut res := '' + #res = new string( $fs.realpathSync(fpath)) + + return res + } $else { + return fpath + } +} + +// flush will flush the stdout buffer. +pub fn flush() { + $if js_node { + #$process.stdout.write('') + } +} + +// chmod change file access attributes of `path` to `mode`. +// Octals like `0o600` can be used. +pub fn chmod(path string, mode int) { + $if js_node { + #$fs.chmodSync(''+path,mode.valueOf()) + } +} + +// chown changes the owner and group attributes of `path` to `owner` and `group`. +// Octals like `0o600` can be used. +pub fn chown(path string, owner int, group int) { + $if js_node { + #$fs.chownSync(''+path,owner.valueOf(),group.valueOf()) + } +} + +pub fn temp_dir() string { + mut res := '' + $if js_node { + #res = new builtin.string($os.tmpdir()) + } + return res +} + +pub fn home_dir() string { + mut res := '' + $if js_node { + #res = new builtin.string($os.homedir()) + } + return res +} + +// join_path returns a path as string from input string parameter(s). +pub fn join_path(base string, dirs ...string) string { + mut result := []string{} + result << base.trim_right('\\/') + for d in dirs { + result << d + } + mut path_sep := '' + #path_sep = $path.sep; + + res := result.join(path_sep) + return res +} + +pub fn execute(cmd string) Result { + mut exit_code := 0 + mut stdout := '' + #let commands = cmd.str.split(' '); + #let output = $child_process.spawnSync(commands[0],commands.slice(1,commands.length)); + #exit_code = new builtin.int(output.status) + #stdout = new builtin.string(output.stdout + '') + + return Result{ + exit_code: exit_code + output: stdout + } +} diff --git a/v_windows/v/vlib/os/os.v b/v_windows/v/vlib/os/os.v new file mode 100644 index 0000000..4574156 --- /dev/null +++ b/v_windows/v/vlib/os/os.v @@ -0,0 +1,633 @@ +// Copyright (c) 2019-2021 Alexander Medvednikov. All rights reserved. +// Use of this source code is governed by an MIT license +// that can be found in the LICENSE file. +module os + +pub const ( + max_path_len = 4096 + wd_at_startup = getwd() +) + +const ( + f_ok = 0 + x_ok = 1 + w_ok = 2 + 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) ? { + source_path := real_path(src) + dest_path := real_path(dst) + if !exists(source_path) { + return error("Source path doesn't exist") + } + // single file copy + if !is_dir(source_path) { + adjusted_path := if is_dir(dest_path) { + join_path(dest_path, file_name(source_path)) + } else { + dest_path + } + if exists(adjusted_path) { + if overwrite { + rm(adjusted_path) ? + } else { + return error('Destination file path already exist') + } + } + cp(source_path, adjusted_path) ? + return + } + if !exists(dest_path) { + mkdir(dest_path) ? + } + if !is_dir(dest_path) { + return error('Destination path is not a valid directory') + } + files := ls(source_path) ? + for file in files { + sp := join_path(source_path, file) + dp := join_path(dest_path, file) + if is_dir(sp) { + if !exists(dp) { + mkdir(dp) ? + } + } + cp_all(sp, dp, overwrite) or { + rmdir(dp) or { return err } + return err + } + } +} + +// mv_by_cp first copies the source file, and if it is copied successfully, deletes the source file. +// may be used when you are not sure that the source and target are on the same mount/partition. +pub fn mv_by_cp(source string, target string) ? { + cp(source, target) ? + rm(source) ? +} + +// read_lines reads the file in `path` into an array of lines. +pub fn read_lines(path string) ?[]string { + buf := read_file(path) ? + res := buf.split_into_lines() + unsafe { buf.free() } + return res +} + +// sigint_to_signal_name will translate `si` signal integer code to it's string code representation. +pub fn sigint_to_signal_name(si int) string { + // POSIX signals: + match si { + 1 { return 'SIGHUP' } + 2 { return 'SIGINT' } + 3 { return 'SIGQUIT' } + 4 { return 'SIGILL' } + 6 { return 'SIGABRT' } + 8 { return 'SIGFPE' } + 9 { return 'SIGKILL' } + 11 { return 'SIGSEGV' } + 13 { return 'SIGPIPE' } + 14 { return 'SIGALRM' } + 15 { return 'SIGTERM' } + else {} + } + $if linux { + // From `man 7 signal` on linux: + match si { + // TODO dependent on platform + // works only on x86/ARM/most others + 10 /* , 30, 16 */ { return 'SIGUSR1' } + 12 /* , 31, 17 */ { return 'SIGUSR2' } + 17 /* , 20, 18 */ { return 'SIGCHLD' } + 18 /* , 19, 25 */ { return 'SIGCONT' } + 19 /* , 17, 23 */ { return 'SIGSTOP' } + 20 /* , 18, 24 */ { return 'SIGTSTP' } + 21 /* , 26 */ { return 'SIGTTIN' } + 22 /* , 27 */ { return 'SIGTTOU' } + // ///////////////////////////// + 5 { return 'SIGTRAP' } + 7 { return 'SIGBUS' } + else {} + } + } + return 'unknown' +} + +// rmdir_all recursively removes the specified directory. +pub fn rmdir_all(path string) ? { + mut ret_err := '' + items := ls(path) ? + for item in items { + fullpath := join_path(path, item) + if is_dir(fullpath) { + rmdir_all(fullpath) or { ret_err = err.msg } + } else { + rm(fullpath) or { ret_err = err.msg } + } + } + rmdir(path) or { ret_err = err.msg } + if ret_err.len > 0 { + return error(ret_err) + } +} + +// is_dir_empty will return a `bool` whether or not `path` is empty. +pub fn is_dir_empty(path string) bool { + items := ls(path) or { return true } + return items.len == 0 +} + +// file_ext will return the part after the last occurence of `.` in `path`. +// The `.` is included. +pub fn file_ext(path string) string { + pos := path.last_index('.') or { return '' } + return path[pos..] +} + +// dir returns all but the last element of path, typically the path's directory. +// After dropping the final element, trailing slashes are removed. +// If the path is empty, dir returns ".". If the path consists entirely of separators, +// dir returns a single separator. +// The returned path does not end in a separator unless it is the root directory. +pub fn dir(opath string) string { + if opath == '' { + return '.' + } + path := opath.replace_each(['/', path_separator, r'\', path_separator]) + pos := path.last_index(path_separator) or { return '.' } + if pos == 0 && path_separator == '/' { + return '/' + } + return path[..pos] +} + +// base returns the last element of path. +// Trailing path separators are removed before extracting the last element. +// If the path is empty, base returns ".". If the path consists entirely of separators, base returns a +// single separator. +pub fn base(opath string) string { + if opath == '' { + return '.' + } + path := opath.replace_each(['/', path_separator, r'\', path_separator]) + if path == path_separator { + return path_separator + } + if path.ends_with(path_separator) { + path2 := path[..path.len - 1] + pos := path2.last_index(path_separator) or { return path2.clone() } + return path2[pos + 1..] + } + pos := path.last_index(path_separator) or { return path.clone() } + return path[pos + 1..] +} + +// file_name will return all characters found after the last occurence of `path_separator`. +// file extension is included. +pub fn file_name(opath string) string { + path := opath.replace_each(['/', path_separator, r'\', path_separator]) + return path.all_after_last(path_separator) +} + +// input_opt returns a one-line string from stdin, after printing a prompt. +// In the event of error (end of input), it returns `none`. +pub fn input_opt(prompt string) ?string { + print(prompt) + flush() + res := get_raw_line() + if res.len > 0 { + return res.trim_right('\r\n') + } + return none +} + +// input returns a one-line string from stdin, after printing a prompt. +// In the event of error (end of input), it returns ''. +pub fn input(prompt string) string { + res := input_opt(prompt) or { return '' } + return res +} + +// get_line returns a one-line string from stdin +pub fn get_line() string { + str := get_raw_line() + $if windows { + return str.trim_right('\r\n') + } + return str.trim_right('\n') +} + +// get_lines returns an array of strings read from from stdin. +// reading is stopped when an empty line is read. +pub fn get_lines() []string { + mut line := '' + mut inputstr := []string{} + for { + line = get_line() + if line.len <= 0 { + break + } + line = line.trim_space() + inputstr << line + } + return inputstr +} + +// get_lines_joined returns a string of the values read from from stdin. +// reading is stopped when an empty line is read. +pub fn get_lines_joined() string { + mut line := '' + mut inputstr := '' + for { + line = get_line() + if line.len <= 0 { + break + } + line = line.trim_space() + inputstr += line + } + return inputstr +} + +// get_raw_lines_joined reads *all* input lines from stdin. +// It returns them as one large string. NB: unlike os.get_lines_joined, +// empty lines (that contain only `\r\n` or `\n`), will be present in +// the output. +// Reading is stopped, only on EOF of stdin. +pub fn get_raw_lines_joined() string { + mut line := '' + mut lines := []string{} + for { + line = get_raw_line() + if line.len <= 0 { + break + } + lines << line + } + res := lines.join('') + return res +} + +// user_os returns current user operating system name. +pub fn user_os() string { + $if linux { + return 'linux' + } + $if macos { + return 'macos' + } + $if windows { + return 'windows' + } + $if freebsd { + return 'freebsd' + } + $if openbsd { + return 'openbsd' + } + $if netbsd { + return 'netbsd' + } + $if dragonfly { + return 'dragonfly' + } + $if android { + return 'android' + } + $if solaris { + return 'solaris' + } + $if haiku { + return 'haiku' + } + $if serenity { + return 'serenity' + } + $if vinix { + return 'vinix' + } + return 'unknown' +} + +// home_dir returns path to the user's home directory. +pub fn home_dir() string { + $if windows { + return getenv('USERPROFILE') + } $else { + // println('home_dir() call') + // res:= os.getenv('HOME') + // println('res="$res"') + return getenv('HOME') + } +} + +// write_file writes `text` data to a file in `path`. +pub fn write_file(path string, text string) ? { + mut f := create(path) ? + unsafe { f.write_full_buffer(text.str, size_t(text.len)) ? } + f.close() +} + +// executable_fallback is used when there is not a more platform specific and accurate implementation. +// It relies on path manipulation of os.args[0] and os.wd_at_startup, so it may not work properly in +// all cases, but it should be better, than just using os.args[0] directly. +fn executable_fallback() string { + if args.len == 0 { + // we are early in the bootstrap, os.args has not been initialized yet :-| + return '' + } + mut exepath := args[0] + $if windows { + if !exepath.contains('.exe') { + exepath += '.exe' + } + } + if !is_abs_path(exepath) { + rexepath := exepath.replace_each(['/', path_separator, r'\', path_separator]) + if rexepath.contains(path_separator) { + exepath = join_path(os.wd_at_startup, exepath) + } else { + // no choice but to try to walk the PATH folders :-| ... + foundpath := find_abs_path_of_executable(exepath) or { '' } + if foundpath.len > 0 { + exepath = foundpath + } + } + } + exepath = real_path(exepath) + return exepath +} + +// find_exe_path walks the environment PATH, just like most shell do, it returns +// the absolute path of the executable if found +pub fn find_abs_path_of_executable(exepath string) ?string { + if exepath == '' { + return error('expected non empty `exepath`') + } + if is_abs_path(exepath) { + return real_path(exepath) + } + mut res := '' + paths := getenv('PATH').split(path_delimiter) + for p in paths { + found_abs_path := join_path(p, exepath) + if exists(found_abs_path) && is_executable(found_abs_path) { + res = found_abs_path + break + } + } + if res.len > 0 { + return real_path(res) + } + return error('failed to find executable') +} + +// 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 } + return true +} + +// is_file returns a `bool` indicating whether the given `path` is a file. +pub fn is_file(path string) bool { + return exists(path) && !is_dir(path) +} + +// is_abs_path returns `true` if `path` is absolute. +pub fn is_abs_path(path string) bool { + if path.len == 0 { + return false + } + $if windows { + return path[0] == `/` || // incase we're in MingGW bash + (path[0].is_letter() && path.len > 1 && path[1] == `:`) + } + return path[0] == `/` +} + +// join_path returns a path as string from input string parameter(s). +[manualfree] +pub fn join_path(base string, dirs ...string) string { + mut result := []string{} + result << base.trim_right('\\/') + for d in dirs { + result << d + } + res := result.join(path_separator) + unsafe { result.free() } + return res +} + +// walk_ext returns a recursive list of all files in `path` ending with `ext`. +pub fn walk_ext(path string, ext string) []string { + if !is_dir(path) { + return [] + } + mut files := ls(path) or { return [] } + mut res := []string{} + separator := if path.ends_with(path_separator) { '' } else { path_separator } + for file in files { + if file.starts_with('.') { + continue + } + p := path + separator + file + if is_dir(p) && !is_link(p) { + res << walk_ext(p, ext) + } else if file.ends_with(ext) { + res << p + } + } + return res +} + +// walk recursively traverses the given directory `path`. +// When a file is encountred it will call the callback function with current file as argument. +pub fn walk(path string, f fn (string)) { + if !is_dir(path) { + return + } + mut files := ls(path) or { return } + mut local_path_separator := path_separator + if path.ends_with(path_separator) { + local_path_separator = '' + } + for file in files { + p := path + local_path_separator + file + if is_dir(p) && !is_link(p) { + walk(p, f) + } else if exists(p) { + f(p) + } + } + return +} + +// log will print "os.log: "+`s` ... +pub fn log(s string) { + //$if macos { + // Use NSLog() on macos + // C.darwin_log(s) + //} $else { + println('os.log: ' + s) + //} +} + +// mkdir_all will create a valid full path of all directories given in `path`. +pub fn mkdir_all(path string) ? { + mut p := if path.starts_with(path_separator) { path_separator } else { '' } + path_parts := path.trim_left(path_separator).split(path_separator) + for subdir in path_parts { + p += subdir + path_separator + if exists(p) && is_dir(p) { + continue + } + mkdir(p) or { return error('folder: $p, error: $err') } + } +} + +// cache_dir returns the path to a *writable* user specific folder, suitable for writing non-essential data. +pub fn cache_dir() string { + // See: https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html + // There is a single base directory relative to which user-specific non-essential + // (cached) data should be written. This directory is defined by the environment + // variable $XDG_CACHE_HOME. + // $XDG_CACHE_HOME defines the base directory relative to which user specific + // non-essential data files should be stored. If $XDG_CACHE_HOME is either not set + // or empty, a default equal to $HOME/.cache should be used. + $if !windows { + xdg_cache_home := getenv('XDG_CACHE_HOME') + if xdg_cache_home != '' { + return xdg_cache_home + } + } + cdir := join_path(home_dir(), '.cache') + if !is_dir(cdir) && !is_link(cdir) { + mkdir(cdir) or { panic(err) } + } + return cdir +} + +// temp_dir returns the path to a folder, that is suitable for storing temporary files. +pub fn temp_dir() string { + mut path := getenv('TMPDIR') + $if windows { + if path == '' { + // TODO see Qt's implementation? + // https://doc.qt.io/qt-5/qdir.html#tempPath + // https://github.com/qt/qtbase/blob/e164d61ca8263fc4b46fdd916e1ea77c7dd2b735/src/corelib/io/qfilesystemengine_win.cpp#L1275 + path = getenv('TEMP') + if path == '' { + path = getenv('TMP') + } + if path == '' { + path = 'C:/tmp' + } + } + } + $if macos { + // avoid /var/folders/6j/cmsk8gd90pd.... on macs + return '/tmp' + } + $if android { + // TODO test+use '/data/local/tmp' on Android before using cache_dir() + if path == '' { + path = cache_dir() + } + } + if path == '' { + path = '/tmp' + } + return path +} + +fn default_vmodules_path() string { + return join_path(home_dir(), '.vmodules') +} + +// vmodules_dir returns the path to a folder, where v stores its global modules. +pub fn vmodules_dir() string { + paths := vmodules_paths() + if paths.len > 0 { + return paths[0] + } + return default_vmodules_path() +} + +// vmodules_paths returns a list of paths, where v looks up for modules. +// You can customize it through setting the environment variable VMODULES +pub fn vmodules_paths() []string { + mut path := getenv('VMODULES') + if path == '' { + path = default_vmodules_path() + } + list := path.split(path_delimiter).map(it.trim_right(path_separator)) + return list +} + +// resource_abs_path returns an absolute path, for the given `path`. +// (the path is expected to be relative to the executable program) +// See https://discordapp.com/channels/592103645835821068/592294828432424960/630806741373943808 +// It gives a convenient way to access program resources like images, fonts, sounds and so on, +// *no matter* how the program was started, and what is the current working directory. +[manualfree] +pub fn resource_abs_path(path string) string { + exe := executable() + dexe := dir(exe) + mut base_path := real_path(dexe) + vresource := getenv('V_RESOURCE_PATH') + if vresource.len != 0 { + base_path = vresource + } + fp := join_path(base_path, path) + res := real_path(fp) + unsafe { + fp.free() + base_path.free() + } + return res +} + +pub struct Uname { +pub mut: + sysname string + nodename string + release string + version string + machine string +} + +pub fn execute_or_panic(cmd string) Result { + res := execute(cmd) + if res.exit_code != 0 { + eprintln('failed cmd: $cmd') + eprintln('failed code: $res.exit_code') + panic(res.output) + } + return res +} + +pub fn execute_or_exit(cmd string) Result { + res := execute(cmd) + if res.exit_code != 0 { + eprintln('failed cmd: $cmd') + eprintln('failed code: $res.exit_code') + eprintln(res.output) + exit(1) + } + return res +} diff --git a/v_windows/v/vlib/os/os_android.c.v b/v_windows/v/vlib/os/os_android.c.v new file mode 100644 index 0000000..30825ea --- /dev/null +++ b/v_windows/v/vlib/os/os_android.c.v @@ -0,0 +1,39 @@ +module os + +struct C.AAsset { +} + +struct C.AAssetManager { +} + +struct C.ANativeActivity { + assetManager voidptr +} + +fn C.AAssetManager_open(&C.AAssetManager, &char, int) &C.AAsset + +fn C.AAsset_getLength(&C.AAsset) int + +fn C.AAsset_read(&C.AAsset, voidptr, int) int + +fn C.AAsset_close(&C.AAsset) + +pub fn read_apk_asset(file string) ?[]byte { + act := &C.ANativeActivity(C.sapp_android_get_native_activity()) + if isnil(act) { + return error('Could not get reference to Android activity') + } + asset := C.AAssetManager_open(&C.AAssetManager(act.assetManager), file.str, C.AASSET_MODE_STREAMING) + if isnil(asset) { + return error('File `$file` not found') + } + len := C.AAsset_getLength(asset) + buf := []byte{len: len} + for { + if C.AAsset_read(asset, buf.data, len) > 0 { + break + } + } + C.AAsset_close(asset) + return buf +} diff --git a/v_windows/v/vlib/os/os_darwin.c.v b/v_windows/v/vlib/os/os_darwin.c.v new file mode 100644 index 0000000..8635c63 --- /dev/null +++ b/v_windows/v/vlib/os/os_darwin.c.v @@ -0,0 +1,18 @@ +// Copyright (c) 2019-2021 Alexander Medvednikov. All rights reserved. +// Use of this source code is governed by an MIT license +// that can be found in the LICENSE file. +module os + +#include "@VROOT/vlib/os/os_darwin.m" + +pub const ( + sys_write = 4 + sys_open = 5 + sys_close = 6 + sys_mkdir = 136 + sys_creat = 8 + sys_open_nocancel = 398 + sys_stat64 = 338 +) + +fn C.darwin_log(s string) diff --git a/v_windows/v/vlib/os/os_darwin.m b/v_windows/v/vlib/os/os_darwin.m new file mode 100644 index 0000000..a1de752 --- /dev/null +++ b/v_windows/v/vlib/os/os_darwin.m @@ -0,0 +1,7 @@ +/* +NSString* nsstring(string s); + +void darwin_log(string s) { + NSLog(nsstring(s)); +} +*/ diff --git a/v_windows/v/vlib/os/os_js.js.v b/v_windows/v/vlib/os/os_js.js.v new file mode 100644 index 0000000..008a7c1 --- /dev/null +++ b/v_windows/v/vlib/os/os_js.js.v @@ -0,0 +1,127 @@ +module os + +pub fn mkdir(path string) ?bool { + $if js_node { + if path == '.' { + return true + } + #$fs.mkdirSync(path.valueOf()) + + return true + } $else { + return false + } +} + +pub fn is_dir(path string) bool { + res := false + $if js_node { + #res.val = $fs.existsSync(path,str) && $fs.lstatSync(path.str).isDirectory() + } + return res +} + +pub fn is_link(path string) bool { + res := false + $if js_node { + #res.val = $fs.existsSync(path.str) && $fs.lstatSync(path.str).isSymbolicLink() + } + return res +} + +pub fn exists(path string) bool { + res := false + $if js_node { + #res.val = $fs.existsSync(path.str) + } + return res +} + +pub fn ls(path string) ?[]string { + if !is_dir(path) { + return error('ls(): cannot open dir $dir') + } + + result := []string{} + $if js_node { + #let i = 0 + #$fs.readdirSync(path.str).forEach((path) => result.arr[i++] = new builtin.string(path)) + } + return result +} + +pub fn get_raw_line() string { + return '' +} + +pub fn executable() string { + return '' +} + +pub fn is_executable(path string) bool { + eprintln('TODO: There is no isExecutable on fs.stats') + return false +} + +pub fn rmdir(path string) ? { + $if js_node { + err := '' + #try { + #$fs.rmdirSync(path.str) + #return; + #} catch (e) { + #err.str = 'Failed to remove "' + path.str + '": ' + e.toString() + #} + + return error(err) + } +} + +pub fn rm(path string) ? { + $if js_node { + err := '' + #try { + #$fs.rmSync(path.str) + #return; + #} catch (e) { + #err.str = 'Failed to remove "' + path.str + '": ' + e.toString() + #} + + return error(err) + } +} + +pub fn cp(src string, dst string) ? { + $if js_node { + err := '' + #try { + #$fs.cpSync(src.str,dst.str); + #return; + #} catch (e) { + #err.str = 'failed to copy ' + src.str + ' to ' + dst.str + ': ' + e.toString(); + #} + + return error(err) + } +} + +pub fn read_file(s string) ?string { + mut err := '' + err = err + res := '' + #try { + #res.str = $fs.readFileSync(s.str).toString() + #} catch (e) { + #err.str = 'Failed to read file: ' + e.toString() + #return error(err) + #} + + return res +} + +pub fn getwd() string { + res := '' + #res.str = $process.cwd() + + return res +} diff --git a/v_windows/v/vlib/os/os_linux.c.v b/v_windows/v/vlib/os/os_linux.c.v new file mode 100644 index 0000000..e178795 --- /dev/null +++ b/v_windows/v/vlib/os/os_linux.c.v @@ -0,0 +1,19 @@ +// Copyright (c) 2019-2021 Alexander Medvednikov. All rights reserved. +// Use of this source code is governed by an MIT license +// that can be found in the LICENSE file. +module os + +const ( + prot_read = 1 + prot_write = 2 + map_private = 0x02 + map_anonymous = 0x20 +) + +pub const ( + sys_write = 1 + sys_open = 2 + sys_close = 3 + sys_mkdir = 83 + sys_creat = 85 +) diff --git a/v_windows/v/vlib/os/os_nix.c.v b/v_windows/v/vlib/os/os_nix.c.v new file mode 100644 index 0000000..6640ec8 --- /dev/null +++ b/v_windows/v/vlib/os/os_nix.c.v @@ -0,0 +1,549 @@ +module os + +import strings + +#include +#include +#include +#include +#include +#include +$if !solaris && !haiku { + #include +} + +pub const ( + path_separator = '/' + path_delimiter = ':' +) + +const ( + stdin_value = 0 + stdout_value = 1 + stderr_value = 2 +) + +// (Must be realized in Syscall) (Must be specified) +// ref: http://www.ccfit.nsu.ru/~deviv/courses/unix/unix/ng7c229.html +pub const ( + s_ifmt = 0xF000 // type of file + s_ifdir = 0x4000 // directory + s_iflnk = 0xa000 // link + s_isuid = 0o4000 // SUID + s_isgid = 0o2000 // SGID + s_isvtx = 0o1000 // Sticky + s_irusr = 0o0400 // Read by owner + s_iwusr = 0o0200 // Write by owner + s_ixusr = 0o0100 // Execute by owner + s_irgrp = 0o0040 // Read by group + s_iwgrp = 0o0020 // Write by group + s_ixgrp = 0o0010 // Execute by group + s_iroth = 0o0004 // Read by others + s_iwoth = 0o0002 // Write by others + s_ixoth = 0o0001 // Execute by others +) + +struct C.utsname { +mut: + sysname &char + nodename &char + release &char + version &char + machine &char +} + +struct C.utimbuf { + actime int + modtime int +} + +fn C.utime(&char, voidptr) int + +fn C.uname(name voidptr) int + +fn C.symlink(&char, &char) int + +fn C.link(&char, &char) int + +fn C.gethostname(&char, int) int + +// NB: not available on Android fn C.getlogin_r(&char, int) int +fn C.getlogin() &char + +fn C.getppid() int + +fn C.getgid() int + +fn C.getegid() int + +fn C.ptrace(u32, u32, voidptr, int) u64 + +enum GlobMatch { + exact + ends_with + starts_with + start_and_ends_with + contains + any +} + +fn glob_match(dir string, pattern string, next_pattern string, mut matches []string) []string { + mut subdirs := []string{} + if is_file(dir) { + return subdirs + } + mut files := ls(dir) or { return subdirs } + mut mode := GlobMatch.exact + mut pat := pattern + if pat == '*' { + mode = GlobMatch.any + if next_pattern != pattern && next_pattern != '' { + for file in files { + if is_dir('$dir/$file') { + subdirs << '$dir/$file' + } + } + return subdirs + } + } + if pat == '**' { + files = walk_ext(dir, '') + pat = next_pattern + } + if pat.starts_with('*') { + mode = .ends_with + pat = pat[1..] + } + if pat.ends_with('*') { + mode = if mode == .ends_with { GlobMatch.contains } else { GlobMatch.starts_with } + pat = pat[..pat.len - 1] + } + if pat.contains('*') { + mode = .start_and_ends_with + } + for file in files { + mut fpath := file + f := if file.contains(os.path_separator) { + pathwalk := file.split(os.path_separator) + pathwalk[pathwalk.len - 1] + } else { + fpath = if dir == '.' { file } else { '$dir/$file' } + file + } + if f in ['.', '..'] || f == '' { + continue + } + hit := match mode { + .any { + true + } + .exact { + f == pat + } + .starts_with { + f.starts_with(pat) + } + .ends_with { + f.ends_with(pat) + } + .start_and_ends_with { + p := pat.split('*') + f.starts_with(p[0]) && f.ends_with(p[1]) + } + .contains { + f.contains(pat) + } + } + if hit { + if is_dir(fpath) { + subdirs << fpath + if next_pattern == pattern && next_pattern != '' { + matches << '$fpath$os.path_separator' + } + } else { + matches << fpath + } + } + } + return subdirs +} + +fn native_glob_pattern(pattern string, mut matches []string) ? { + steps := pattern.split(os.path_separator) + mut cwd := if pattern.starts_with(os.path_separator) { os.path_separator } else { '.' } + mut subdirs := [cwd] + for i := 0; i < steps.len; i++ { + step := steps[i] + step2 := if i + 1 == steps.len { step } else { steps[i + 1] } + if step == '' { + continue + } + if is_dir('$cwd$os.path_separator$step') { + dd := if cwd == '/' { + step + } else { + if cwd == '.' || cwd == '' { + step + } else { + if step == '.' || step == '/' { cwd } else { '$cwd/$step' } + } + } + if i + 1 != steps.len { + if dd !in subdirs { + subdirs << dd + } + } + } + mut subs := []string{} + for sd in subdirs { + d := if cwd == '/' { + sd + } else { + if cwd == '.' || cwd == '' { + sd + } else { + if sd == '.' || sd == '/' { cwd } else { '$cwd/$sd' } + } + } + subs << glob_match(d.replace('//', '/'), step, step2, mut matches) + } + subdirs = subs.clone() + } +} + +pub fn utime(path string, actime int, modtime int) ? { + mut u := C.utimbuf{actime, modtime} + if C.utime(&char(path.str), voidptr(&u)) != 0 { + return error_with_code(posix_get_error_msg(C.errno), C.errno) + } +} + +pub fn uname() Uname { + mut u := Uname{} + utsize := sizeof(C.utsname) + unsafe { + x := malloc_noscan(int(utsize)) + d := &C.utsname(x) + if C.uname(d) == 0 { + u.sysname = cstring_to_vstring(d.sysname) + u.nodename = cstring_to_vstring(d.nodename) + u.release = cstring_to_vstring(d.release) + u.version = cstring_to_vstring(d.version) + u.machine = cstring_to_vstring(d.machine) + } + free(d) + } + return u +} + +pub fn hostname() string { + mut hstnme := '' + size := 256 + mut buf := unsafe { &char(malloc_noscan(size)) } + if C.gethostname(buf, size) == 0 { + hstnme = unsafe { cstring_to_vstring(buf) } + unsafe { free(buf) } + return hstnme + } + return '' +} + +pub fn loginname() string { + x := C.getlogin() + if !isnil(x) { + return unsafe { cstring_to_vstring(x) } + } + return '' +} + +fn init_os_args(argc int, argv &&byte) []string { + mut args_ := []string{} + // mut args := []string(make(0, argc, sizeof(string))) + // mut args := []string{len:argc} + for i in 0 .. argc { + // args [i] = argv[i].vstring() + unsafe { args_ << (&byte(argv[i])).vstring_literal() } + } + return args_ +} + +pub fn ls(path string) ?[]string { + mut res := []string{} + dir := unsafe { C.opendir(&char(path.str)) } + if isnil(dir) { + return error('ls() couldnt open dir "$path"') + } + mut ent := &C.dirent(0) + // mut ent := &C.dirent{!} + for { + ent = C.readdir(dir) + if isnil(ent) { + break + } + unsafe { + bptr := &byte(&ent.d_name[0]) + if bptr[0] == 0 || (bptr[0] == `.` && bptr[1] == 0) + || (bptr[0] == `.` && bptr[1] == `.` && bptr[2] == 0) { + continue + } + res << tos_clone(bptr) + } + } + C.closedir(dir) + return res +} + +/* +pub fn is_dir(path string) bool { + //$if linux { + //C.syscall(4, path.str) // sys_newstat + //} + dir := C.opendir(path.str) + res := !isnil(dir) + if res { + C.closedir(dir) + } + return res +} +*/ + +// mkdir creates a new directory with the specified path. +pub fn mkdir(path string) ?bool { + if path == '.' { + return true + } + /* + mut k := 0 + defer { + k = 1 + } + */ + apath := real_path(path) + // defer { + // apath.free() + //} + /* + $if linux { + $if !android { + ret := C.syscall(sys_mkdir, apath.str, 511) + if ret == -1 { + return error(posix_get_error_msg(C.errno)) + } + return true + } + } + */ + r := unsafe { C.mkdir(&char(apath.str), 511) } + if r == -1 { + return error(posix_get_error_msg(C.errno)) + } + return true +} + +// execute starts the specified command, waits for it to complete, and returns its output. +[manualfree] +pub fn execute(cmd string) Result { + // if cmd.contains(';') || cmd.contains('&&') || cmd.contains('||') || cmd.contains('\n') { + // return Result{ exit_code: -1, output: ';, &&, || and \\n are not allowed in shell commands' } + // } + pcmd := if cmd.contains('2>') { cmd } else { '$cmd 2>&1' } + f := vpopen(pcmd) + if isnil(f) { + return Result{ + exit_code: -1 + output: 'exec("$cmd") failed' + } + } + buf := unsafe { malloc_noscan(4096) } + mut res := strings.new_builder(1024) + defer { + unsafe { res.free() } + } + unsafe { + bufbp := buf + for C.fgets(&char(bufbp), 4096, f) != 0 { + buflen := vstrlen(bufbp) + res.write_ptr(bufbp, buflen) + } + } + soutput := res.str() + exit_code := vpclose(f) + unsafe { free(buf) } + return Result{ + exit_code: exit_code + output: soutput + } +} + +pub struct Command { +mut: + f voidptr +pub mut: + eof bool +pub: + path string + redirect_stdout bool +} + +[manualfree] +pub fn (mut c Command) start() ? { + pcmd := c.path + ' 2>&1' + defer { + unsafe { pcmd.free() } + } + c.f = vpopen(pcmd) + if isnil(c.f) { + return error('exec("$c.path") failed') + } +} + +[manualfree] +pub fn (mut c Command) read_line() string { + buf := [4096]byte{} + mut res := strings.new_builder(1024) + defer { + unsafe { res.free() } + } + unsafe { + bufbp := &buf[0] + for C.fgets(&char(bufbp), 4096, c.f) != 0 { + len := vstrlen(bufbp) + for i in 0 .. len { + if bufbp[i] == `\n` { + res.write_ptr(bufbp, i) + final := res.str() + return final + } + } + res.write_ptr(bufbp, len) + } + } + c.eof = true + final := res.str() + return final +} + +pub fn (c &Command) close() ? { + exit_code := vpclose(c.f) + if exit_code == 127 { + return error_with_code('error', 127) + } +} + +pub fn symlink(origin string, target string) ?bool { + res := C.symlink(&char(origin.str), &char(target.str)) + if res == 0 { + return true + } + return error(posix_get_error_msg(C.errno)) +} + +pub fn link(origin string, target string) ?bool { + res := C.link(&char(origin.str), &char(target.str)) + if res == 0 { + return true + } + return error(posix_get_error_msg(C.errno)) +} + +// get_error_msg return error code representation in string. +pub fn get_error_msg(code int) string { + return posix_get_error_msg(code) +} + +pub fn (mut f File) close() { + if !f.is_opened { + return + } + f.is_opened = false + /* + $if linux { + $if !android { + C.syscall(sys_close, f.fd) + return + } + } + */ + C.fflush(f.cfile) + C.fclose(f.cfile) +} + +[inline] +pub fn debugger_present() bool { + // check if the parent could trace its process, + // if not a debugger must be present + $if linux { + return C.ptrace(C.PTRACE_TRACEME, 0, 1, 0) == -1 + } $else $if macos { + return C.ptrace(C.PT_TRACE_ME, 0, voidptr(1), 0) == -1 + } + return false +} + +fn C.mkstemp(stemplate &byte) int + +// `is_writable_folder` - `folder` exists and is writable to the process +pub fn is_writable_folder(folder string) ?bool { + if !exists(folder) { + return error('`$folder` does not exist') + } + if !is_dir(folder) { + return error('`folder` is not a folder') + } + tmp_perm_check := join_path(folder, 'XXXXXX') + unsafe { + x := C.mkstemp(&char(tmp_perm_check.str)) + if -1 == x { + return error('folder `$folder` is not writable') + } + C.close(x) + } + rm(tmp_perm_check) ? + return true +} + +[inline] +pub fn getpid() int { + return C.getpid() +} + +[inline] +pub fn getppid() int { + return C.getppid() +} + +[inline] +pub fn getuid() int { + return C.getuid() +} + +[inline] +pub fn geteuid() int { + return C.geteuid() +} + +[inline] +pub fn getgid() int { + return C.getgid() +} + +[inline] +pub fn getegid() int { + return C.getegid() +} + +// Turns the given bit on or off, depending on the `enable` parameter +pub fn posix_set_permission_bit(path_s string, mode u32, enable bool) { + mut s := C.stat{} + mut new_mode := u32(0) + path := &char(path_s.str) + unsafe { + C.stat(path, &s) + new_mode = s.st_mode + } + match enable { + true { new_mode |= mode } + false { new_mode &= (0o7777 - mode) } + } + C.chmod(path, int(new_mode)) +} diff --git a/v_windows/v/vlib/os/os_test.v b/v_windows/v/vlib/os/os_test.v new file mode 100644 index 0000000..2fff68e --- /dev/null +++ b/v_windows/v/vlib/os/os_test.v @@ -0,0 +1,752 @@ +import os +import time + +const ( + // tfolder will contain all the temporary files/subfolders made by + // the different tests. It would be removed in testsuite_end(), so + // individual os tests do not need to clean up after themselves. + tfolder = os.join_path(os.temp_dir(), 'v', 'tests', 'os_test') +) + +// os.args has to be *already initialized* with the program's argc/argv at this point +// thus it can be used for other consts too: +const args_at_start = os.args.clone() + +fn testsuite_begin() { + eprintln('testsuite_begin, tfolder = $tfolder') + os.rmdir_all(tfolder) or {} + assert !os.is_dir(tfolder) + os.mkdir_all(tfolder) or { panic(err) } + os.chdir(tfolder) or {} + assert os.is_dir(tfolder) + // println('args_at_start: $args_at_start') + assert args_at_start.len > 0 + assert args_at_start == os.args +} + +fn testsuite_end() { + os.chdir(os.wd_at_startup) or {} + os.rmdir_all(tfolder) or {} + assert !os.is_dir(tfolder) + // eprintln('testsuite_end , tfolder = $tfolder removed.') +} + +fn test_open_file() { + filename := './test1.txt' + hello := 'hello world!' + os.open_file(filename, 'r+', 0o666) or { + assert err.msg == 'No such file or directory' + os.File{} + } + mut file := os.open_file(filename, 'w+', 0o666) or { panic(err) } + file.write_string(hello) or { panic(err) } + file.close() + assert hello.len == os.file_size(filename) + read_hello := os.read_file(filename) or { panic('error reading file $filename') } + assert hello == read_hello + os.rm(filename) or { panic(err) } +} + +fn test_open_file_binary() { + filename := './test1.dat' + hello := 'hello \n world!' + os.open_file(filename, 'r+', 0o666) or { + assert err.msg == 'No such file or directory' + os.File{} + } + mut file := os.open_file(filename, 'wb+', 0o666) or { panic(err) } + bytes := hello.bytes() + unsafe { file.write_ptr(bytes.data, bytes.len) } + file.close() + assert hello.len == os.file_size(filename) + read_hello := os.read_bytes(filename) or { panic('error reading file $filename') } + assert bytes == read_hello + os.rm(filename) or { panic(err) } +} + +// fn test_file_get_line() { +// filename := './fgetline.txt' +// os.write_file(filename, 'line 1\nline 2') +// mut f := os.open_file(filename, 'r', 0) or { +// assert false +// return +// } +// line1 := f.get_line() or { +// '' +// } +// line2 := f.get_line() or { +// '' +// } +// f.close() +// // +// eprintln('line1: $line1 $line1.bytes()') +// eprintln('line2: $line2 $line2.bytes()') +// assert line1 == 'line 1\n' +// assert line2 == 'line 2' +// } +fn test_create_file() { + filename := './test1.txt' + hello := 'hello world!' + mut f := os.create(filename) or { panic(err) } + f.write_string(hello) or { panic(err) } + f.close() + assert hello.len == os.file_size(filename) + os.rm(filename) or { panic(err) } +} + +fn test_is_file() { + // Setup + work_dir := os.join_path(os.getwd(), 'is_file_test') + os.mkdir_all(work_dir) or { panic(err) } + tfile := os.join_path(work_dir, 'tmp_file') + // Test things that shouldn't be a file + assert os.is_file(work_dir) == false + assert os.is_file('non-existent_file.tmp') == false + // Test file + tfile_content := 'temporary file' + os.write_file(tfile, tfile_content) or { panic(err) } + assert os.is_file(tfile) + // Test dir symlinks + $if windows { + assert true + } $else { + dsymlink := os.join_path(work_dir, 'dir_symlink') + os.symlink(work_dir, dsymlink) or { panic(err) } + assert os.is_file(dsymlink) == false + } + // Test file symlinks + $if windows { + assert true + } $else { + fsymlink := os.join_path(work_dir, 'file_symlink') + os.symlink(tfile, fsymlink) or { panic(err) } + assert os.is_file(fsymlink) + } +} + +fn test_write_and_read_string_to_file() { + filename := './test1.txt' + hello := 'hello world!' + os.write_file(filename, hello) or { panic(err) } + assert hello.len == os.file_size(filename) + read_hello := os.read_file(filename) or { panic('error reading file $filename') } + assert hello == read_hello + os.rm(filename) or { panic(err) } +} + +// test_write_and_read_bytes checks for regressions made in the functions +// read_bytes, read_bytes_at and write_bytes. +fn test_write_and_read_bytes() { + file_name := './byte_reader_writer.tst' + payload := [byte(`I`), `D`, `D`, `Q`, `D`] + mut file_write := os.create(os.real_path(file_name)) or { + eprintln('failed to create file $file_name') + return + } + // We use the standard write_bytes function to write the payload and + // compare the length of the array with the file size (have to match). + unsafe { file_write.write_ptr(payload.data, 5) } + file_write.close() + assert payload.len == os.file_size(file_name) + mut file_read := os.open(os.real_path(file_name)) or { + eprintln('failed to open file $file_name') + return + } + // We only need to test read_bytes because this function calls + // read_bytes_at with second parameter zeroed (size, 0). + rbytes := file_read.read_bytes(5) + // eprintln('rbytes: $rbytes') + // eprintln('payload: $payload') + assert rbytes == payload + // check that trying to read data from EOF doesn't error and returns 0 + mut a := []byte{len: 5} + nread := file_read.read_bytes_into(5, mut a) or { + n := if err is none { + int(0) + } else { + eprintln(err) + int(-1) + } + n + } + assert nread == 0 + file_read.close() + // We finally delete the test file. + os.rm(file_name) or { panic(err) } +} + +fn test_create_and_delete_folder() { + folder := './test1' + os.mkdir(folder) or { panic(err) } + assert os.is_dir(folder) + folder_contents := os.ls(folder) or { panic(err) } + assert folder_contents.len == 0 + os.rmdir(folder) or { panic(err) } + folder_exists := os.is_dir(folder) + assert folder_exists == false +} + +fn walk_callback(file string) { + if file == '.' || file == '..' { + return + } + assert file == 'test_walk' + os.path_separator + 'test1' +} + +fn test_walk() { + folder := 'test_walk' + os.mkdir(folder) or { panic(err) } + file1 := folder + os.path_separator + 'test1' + os.write_file(file1, 'test-1') or { panic(err) } + os.walk(folder, walk_callback) + os.rm(file1) or { panic(err) } + os.rmdir(folder) or { panic(err) } +} + +fn test_cp() { + old_file_name := 'cp_example.txt' + new_file_name := 'cp_new_example.txt' + os.write_file(old_file_name, 'Test data 1 2 3, V is awesome #$%^[]!~⭐') or { panic(err) } + os.cp(old_file_name, new_file_name) or { panic('$err') } + old_file := os.read_file(old_file_name) or { panic(err) } + new_file := os.read_file(new_file_name) or { panic(err) } + assert old_file == new_file + os.rm(old_file_name) or { panic(err) } + os.rm(new_file_name) or { panic(err) } +} + +fn test_mv() { + work_dir := os.join_path(os.getwd(), 'mv_test') + os.mkdir_all(work_dir) or { panic(err) } + // Setup test files + tfile1 := os.join_path(work_dir, 'file') + tfile2 := os.join_path(work_dir, 'file.test') + tfile3 := os.join_path(work_dir, 'file.3') + tfile_content := 'temporary file' + os.write_file(tfile1, tfile_content) or { panic(err) } + os.write_file(tfile2, tfile_content) or { panic(err) } + // Setup test dirs + tdir1 := os.join_path(work_dir, 'dir') + tdir2 := os.join_path(work_dir, 'dir2') + tdir3 := os.join_path(work_dir, 'dir3') + os.mkdir(tdir1) or { panic(err) } + os.mkdir(tdir2) or { panic(err) } + // Move file with no extension to dir + os.mv(tfile1, tdir1) or { panic(err) } + mut expected := os.join_path(tdir1, 'file') + assert os.exists(expected) + assert !os.is_dir(expected) + // Move dir with contents to other dir + os.mv(tdir1, tdir2) or { panic(err) } + expected = os.join_path(tdir2, 'dir') + assert os.exists(expected) + assert os.is_dir(expected) + expected = os.join_path(tdir2, 'dir', 'file') + assert os.exists(expected) + assert !os.is_dir(expected) + // Move dir with contents to other dir (by renaming) + os.mv(os.join_path(tdir2, 'dir'), tdir3) or { panic(err) } + expected = tdir3 + assert os.exists(expected) + assert os.is_dir(expected) + assert os.is_dir_empty(tdir2) + // Move file with extension to dir + os.mv(tfile2, tdir2) or { panic(err) } + expected = os.join_path(tdir2, 'file.test') + assert os.exists(expected) + assert !os.is_dir(expected) + // Move file to dir (by renaming) + os.mv(os.join_path(tdir2, 'file.test'), tfile3) or { panic(err) } + expected = tfile3 + assert os.exists(expected) + assert !os.is_dir(expected) +} + +fn test_cp_all() { + // fileX -> dir/fileX + // NB: clean up of the files happens inside the cleanup_leftovers function + os.write_file('ex1.txt', 'wow!') or { panic(err) } + os.mkdir('ex') or { panic(err) } + os.cp_all('ex1.txt', 'ex', false) or { panic(err) } + old := os.read_file('ex1.txt') or { panic(err) } + new := os.read_file('ex/ex1.txt') or { panic(err) } + assert old == new + os.mkdir('ex/ex2') or { panic(err) } + os.write_file('ex2.txt', 'great!') or { panic(err) } + os.cp_all('ex2.txt', 'ex/ex2', false) or { panic(err) } + old2 := os.read_file('ex2.txt') or { panic(err) } + new2 := os.read_file('ex/ex2/ex2.txt') or { panic(err) } + assert old2 == new2 + // recurring on dir -> local dir + os.cp_all('ex', './', true) or { panic(err) } + // regression test for executive runs with overwrite := true + os.cp_all('ex', './', true) or { panic(err) } + os.cp_all('ex', 'nonexisting', true) or { panic(err) } + assert os.exists(os.join_path('nonexisting', 'ex1.txt')) +} + +fn test_realpath_of_empty_string_works() { + assert os.real_path('') == '' +} + +fn test_realpath_non_existing() { + non_existing_path := 'sdyfuisd_non_existing_file' + rpath := os.real_path(non_existing_path) + $if windows { + // on windows, the workdir is prepended, so the result is absolute: + assert rpath.len > non_existing_path.len + } + $if !windows { + // on unix, the workdir is NOT prepended for now, so the result remains the same. + // TODO: the windows behaviour seems saner, think about normalising the unix case to do the same. + assert os.real_path(non_existing_path) == non_existing_path + } +} + +fn test_realpath_existing() { + existing_file_name := 'existing_file.txt' + existing_file := os.join_path(os.temp_dir(), existing_file_name) + os.rm(existing_file) or {} + os.write_file(existing_file, 'abc') or {} + assert os.exists(existing_file) + rpath := os.real_path(existing_file) + assert os.is_abs_path(rpath) + assert rpath.ends_with(existing_file_name) + os.rm(existing_file) or {} +} + +fn test_realpath_removes_dots() { + examples_folder := os.join_path(@VEXEROOT, 'vlib', 'v', '..', '..', 'cmd', '.', '..', + 'examples') + real_path_of_examples_folder := os.real_path(examples_folder) + assert real_path_of_examples_folder.len < examples_folder.len + assert !real_path_of_examples_folder.contains('..') +} + +fn test_realpath_absolutizes_existing_relative_paths() { + old_wd := os.getwd() + defer { + os.chdir(old_wd) or { panic(err) } + } + os.chdir(@VEXEROOT) or { panic(err) } + examples_folder := os.join_path('vlib', 'v', '..', '..', 'cmd', '.', '..', 'examples') + real_path_of_examples_folder := os.real_path(examples_folder) + assert os.is_abs_path(real_path_of_examples_folder) +} + +// TODO: think much more about whether this is desirable: +fn test_realpath_does_not_absolutize_non_existing_relative_paths() { + relative_path := os.join_path('one', 'nonexisting_folder', '..', 'something') + $if !windows { + assert os.real_path(relative_path).contains('..') + assert os.real_path(relative_path) == relative_path + } +} + +fn test_realpath_absolutepath_symlink() ? { + file_name := 'tolink_file.txt' + symlink_name := 'symlink.txt' + mut f := os.create(file_name) ? + f.close() + assert os.symlink(file_name, symlink_name) ? + rpath := os.real_path(symlink_name) + println(rpath) + assert os.is_abs_path(rpath) + assert rpath.ends_with(file_name) + os.rm(symlink_name) or {} + os.rm(file_name) or {} +} + +fn test_tmpdir() { + t := os.temp_dir() + assert t.len > 0 + assert os.is_dir(t) + tfile := t + os.path_separator + 'tmpfile.txt' + os.rm(tfile) or {} // just in case + tfile_content := 'this is a temporary file' + os.write_file(tfile, tfile_content) or { panic(err) } + tfile_content_read := os.read_file(tfile) or { panic(err) } + assert tfile_content_read == tfile_content + os.rm(tfile) or { panic(err) } +} + +fn test_is_writable_folder() { + tmp := os.temp_dir() + f := os.is_writable_folder(tmp) or { + eprintln('err: $err') + false + } + assert f +} + +fn test_make_symlink_check_is_link_and_remove_symlink() { + folder := 'tfolder' + symlink := 'tsymlink' + // windows creates a directory symlink, so delete it with rmdir() + $if windows { + os.rmdir(symlink) or {} + } $else { + os.rm(symlink) or {} + } + os.rmdir(folder) or {} + os.mkdir(folder) or { panic(err) } + folder_contents := os.ls(folder) or { panic(err) } + assert folder_contents.len == 0 + os.symlink(folder, symlink) or { panic(err) } + assert os.is_link(symlink) + $if windows { + os.rmdir(symlink) or { panic(err) } + } $else { + os.rm(symlink) or { panic(err) } + } + os.rmdir(folder) or { panic(err) } + folder_exists := os.is_dir(folder) + assert folder_exists == false + symlink_exists := os.is_link(symlink) + assert symlink_exists == false +} + +fn test_make_symlink_check_is_link_and_remove_symlink_with_file() { + file := 'tfile' + symlink := 'tsymlink' + os.rm(symlink) or {} + os.rm(file) or {} + mut f := os.create(file) or { panic(err) } + f.close() + os.symlink(file, symlink) or { panic(err) } + assert os.is_link(symlink) + os.rm(symlink) or { panic(err) } + os.rm(file) or { panic(err) } + symlink_exists := os.is_link(symlink) + assert symlink_exists == false +} + +fn test_make_hardlink_check_is_link_and_remove_hardlink_with_file() { + file := 'tfile' + symlink := 'tsymlink' + os.rm(symlink) or {} + os.rm(file) or {} + mut f := os.create(file) or { panic(err) } + f.close() + os.link(file, symlink) or { panic(err) } + assert os.exists(symlink) + os.rm(symlink) or { panic(err) } + os.rm(file) or { panic(err) } + symlink_exists := os.is_link(symlink) + assert symlink_exists == false +} + +// fn test_fork() { +// pid := os.fork() +// if pid == 0 { +// println('Child') +// } +// else { +// println('Parent') +// } +// } +// fn test_wait() { +// pid := os.fork() +// if pid == 0 { +// println('Child') +// exit(0) +// } +// else { +// cpid := os.wait() +// println('Parent') +// println(cpid) +// } +// } +fn test_symlink() { + os.mkdir('symlink') or { panic(err) } + os.symlink('symlink', 'symlink2') or { panic(err) } + assert os.exists('symlink2') + // cleanup + os.rmdir('symlink') or { panic(err) } + $if windows { + os.rmdir('symlink2') or { panic(err) } + } $else { + os.rm('symlink2') or { panic(err) } + } +} + +fn test_is_executable_writable_readable() { + file_name := 'rwxfile.exe' + mut f := os.create(file_name) or { + eprintln('failed to create file $file_name') + return + } + f.close() + $if !windows { + os.chmod(file_name, 0o600) or {} // mark as readable && writable, but NOT executable + assert os.is_writable(file_name) + assert os.is_readable(file_name) + assert !os.is_executable(file_name) + os.chmod(file_name, 0o700) or {} // mark as executable too + assert os.is_executable(file_name) + } $else { + assert os.is_writable(file_name) + assert os.is_readable(file_name) + assert os.is_executable(file_name) + } + // We finally delete the test file. + os.rm(file_name) or { panic(err) } +} + +fn test_ext() { + assert os.file_ext('file.v') == '.v' + assert os.file_ext('file') == '' +} + +fn test_is_abs() { + assert os.is_abs_path('/home/user') + assert os.is_abs_path('v/vlib') == false + $if windows { + assert os.is_abs_path('C:\\Windows\\') + } +} + +fn test_join() { + $if windows { + assert os.join_path('v', 'vlib', 'os') == 'v\\vlib\\os' + } $else { + assert os.join_path('v', 'vlib', 'os') == 'v/vlib/os' + } +} + +fn test_rmdir_all() { + mut dirs := ['some/dir', 'some/.hidden/directory'] + $if windows { + for mut d in dirs { + d = d.replace('/', '\\') + } + } + for d in dirs { + os.mkdir_all(d) or { panic(err) } + assert os.is_dir(d) + } + os.rmdir_all('some') or { assert false } + assert !os.exists('some') +} + +fn test_dir() { + $if windows { + assert os.dir('C:\\a\\b\\c') == 'C:\\a\\b' + assert os.dir('C:\\a\\b\\') == 'C:\\a\\b' + assert os.dir('C:/a/b/c') == 'C:\\a\\b' + assert os.dir('C:/a/b/') == 'C:\\a\\b' + } $else { + assert os.dir('/') == '/' + assert os.dir('/abc') == '/' + assert os.dir('/var/tmp/foo') == '/var/tmp' + assert os.dir('/var/tmp/') == '/var/tmp' + assert os.dir('C:\\a\\b\\c') == 'C:/a/b' + assert os.dir('C:\\a\\b\\') == 'C:/a/b' + } + assert os.dir('os') == '.' +} + +fn test_base() { + $if windows { + assert os.base('v\\vlib\\os') == 'os' + assert os.base('v\\vlib\\os\\') == 'os' + assert os.base('v/vlib/os') == 'os' + assert os.base('v/vlib/os/') == 'os' + } $else { + assert os.base('v/vlib/os') == 'os' + assert os.base('v/vlib/os/') == 'os' + assert os.base('v\\vlib\\os') == 'os' + assert os.base('v\\vlib\\os\\') == 'os' + } + assert os.base('filename') == 'filename' +} + +fn test_file_name() { + $if windows { + assert os.file_name('v\\vlib\\os\\os.v') == 'os.v' + assert os.file_name('v\\vlib\\os\\') == '' + assert os.file_name('v\\vlib\\os') == 'os' + } $else { + assert os.file_name('v/vlib/os/os.v') == 'os.v' + assert os.file_name('v/vlib/os/') == '' + assert os.file_name('v/vlib/os') == 'os' + } + assert os.file_name('filename') == 'filename' +} + +fn test_uname() { + u := os.uname() + assert u.sysname.len > 0 + assert u.nodename.len > 0 + assert u.release.len > 0 + assert u.version.len > 0 + assert u.machine.len > 0 +} + +// tests for write_file_array and read_file_array: +const ( + maxn = 3 +) + +struct IntPoint { + x int + y int +} + +fn test_write_file_array_bytes() { + fpath := './abytes.bin' + mut arr := []byte{len: maxn} + for i in 0 .. maxn { + arr[i] = 65 + byte(i) + } + os.write_file_array(fpath, arr) or { panic(err) } + rarr := os.read_bytes(fpath) or { panic(err) } + assert arr == rarr + // eprintln(arr.str()) + // eprintln(rarr.str()) +} + +fn test_write_file_array_structs() { + fpath := './astructs.bin' + mut arr := []IntPoint{len: maxn} + for i in 0 .. maxn { + arr[i] = IntPoint{65 + i, 65 + i + 10} + } + os.write_file_array(fpath, arr) or { panic(err) } + rarr := os.read_file_array(fpath) + assert rarr == arr + assert rarr.len == maxn + // eprintln( rarr.str().replace('\n', ' ').replace('},', '},\n')) +} + +fn test_stdout_capture() { + /* + mut cmd := os.Command{ + path:'cat' + redirect_stdout: true +} +cmd.start() +for !cmd.eof { + line := cmd.read_line() + println('line="$line"') +} +cmd.close() + */ +} + +fn test_posix_set_bit() { + $if windows { + assert true + } $else { + fpath := '/tmp/permtest' + os.create(fpath) or { panic("Couldn't create file") } + os.chmod(fpath, 0o0777) or { panic(err) } + c_fpath := &char(fpath.str) + mut s := C.stat{} + unsafe { + C.stat(c_fpath, &s) + } + // Take the permissions part of the mode + mut mode := u32(s.st_mode) & 0o0777 + assert mode == 0o0777 + // `chmod u-r` + os.posix_set_permission_bit(fpath, os.s_irusr, false) + unsafe { + C.stat(c_fpath, &s) + } + mode = u32(s.st_mode) & 0o0777 + assert mode == 0o0377 + // `chmod u+r` + os.posix_set_permission_bit(fpath, os.s_irusr, true) + unsafe { + C.stat(c_fpath, &s) + } + mode = u32(s.st_mode) & 0o0777 + assert mode == 0o0777 + // NB: setting the sticky bit is platform dependend + // `chmod -s -g -t` + os.posix_set_permission_bit(fpath, os.s_isuid, false) + os.posix_set_permission_bit(fpath, os.s_isgid, false) + os.posix_set_permission_bit(fpath, os.s_isvtx, false) + unsafe { + C.stat(c_fpath, &s) + } + mode = u32(s.st_mode) & 0o0777 + assert mode == 0o0777 + // `chmod g-w o-w` + os.posix_set_permission_bit(fpath, os.s_iwgrp, false) + os.posix_set_permission_bit(fpath, os.s_iwoth, false) + unsafe { + C.stat(c_fpath, &s) + } + mode = u32(s.st_mode) & 0o7777 + assert mode == 0o0755 + os.rm(fpath) or {} + } +} + +fn test_exists_in_system_path() { + assert os.exists_in_system_path('') == false + $if windows { + assert os.exists_in_system_path('cmd.exe') + return + } + assert os.exists_in_system_path('ls') +} + +fn test_truncate() { + filename := './test_trunc.txt' + hello := 'hello world!' + mut f := os.create(filename) or { panic(err) } + f.write_string(hello) or { panic(err) } + f.close() + assert hello.len == os.file_size(filename) + newlen := u64(40000) + os.truncate(filename, newlen) or { panic(err) } + assert newlen == os.file_size(filename) + os.rm(filename) or { panic(err) } +} + +fn test_hostname() { + assert os.hostname().len > 2 +} + +fn test_glob() { + os.mkdir('test_dir') or { panic(err) } + for i in 0 .. 4 { + if i == 3 { + mut f := os.create('test_dir/test0_another') or { panic(err) } + f.close() + mut f1 := os.create('test_dir/test') or { panic(err) } + f1.close() + } else { + mut f := os.create('test_dir/test' + i.str()) or { panic(err) } + f.close() + } + } + files := os.glob('test_dir/t*') or { panic(err) } + assert files.len == 5 + assert os.base(files[0]) == 'test' + + for i in 0 .. 3 { + os.rm('test_dir/test' + i.str()) or { panic(err) } + } + os.rm('test_dir/test0_another') or { panic(err) } + os.rm('test_dir/test') or { panic(err) } + os.rmdir_all('test_dir') or { panic(err) } +} + +fn test_utime() { + filename := './test_utime.txt' + hello := 'hello world!' + mut f := os.create(filename) or { panic(err) } + defer { + f.close() + os.rm(filename) or { panic(err) } + } + f.write_string(hello) or { panic(err) } + atime := time.now().add_days(2).unix_time() + mtime := time.now().add_days(4).unix_time() + os.utime(filename, int(atime), int(mtime)) or { panic(err) } + assert os.file_last_mod_unix(filename) == mtime +} diff --git a/v_windows/v/vlib/os/os_windows.c.v b/v_windows/v/vlib/os/os_windows.c.v new file mode 100644 index 0000000..a920601 --- /dev/null +++ b/v_windows/v/vlib/os/os_windows.c.v @@ -0,0 +1,544 @@ +module os + +import strings + +#flag windows -l advapi32 +#include +#include + +// See https://docs.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-createsymboliclinkw +fn C.CreateSymbolicLinkW(&u16, &u16, u32) int + +// See https://docs.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-createhardlinkw +fn C.CreateHardLinkW(&u16, &u16, C.SECURITY_ATTRIBUTES) int + +fn C._getpid() int + +pub const ( + path_separator = '\\' + path_delimiter = ';' +) + +// 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 +struct Filetime { + dw_low_date_time u32 + dw_high_date_time u32 +} + +// win: WIN32_FIND_DATA +// https://docs.microsoft.com/en-us/windows/win32/api/minwinbase/ns-minwinbase-win32_find_dataw +struct Win32finddata { +mut: + dw_file_attributes u32 + ft_creation_time Filetime + ft_last_access_time Filetime + ft_last_write_time Filetime + n_file_size_high u32 + n_file_size_low u32 + dw_reserved0 u32 + dw_reserved1 u32 + c_file_name [260]u16 // max_path_len = 260 + c_alternate_file_name [14]u16 // 14 + dw_file_type u32 + dw_creator_type u32 + w_finder_flags u16 +} + +struct ProcessInformation { +mut: + h_process voidptr + h_thread voidptr + dw_process_id u32 + dw_thread_id u32 +} + +struct StartupInfo { +mut: + cb u32 + lp_reserved &u16 + lp_desktop &u16 + lp_title &u16 + dw_x u32 + dw_y u32 + dw_x_size u32 + dw_y_size u32 + dw_x_count_chars u32 + dw_y_count_chars u32 + dw_fill_attributes u32 + dw_flags u32 + w_show_window u16 + cb_reserved2 u16 + lp_reserved2 &byte + h_std_input voidptr + h_std_output voidptr + h_std_error voidptr +} + +struct SecurityAttributes { +mut: + n_length u32 + lp_security_descriptor voidptr + b_inherit_handle bool +} + +struct C._utimbuf { + actime int + modtime int +} + +fn C._utime(&char, voidptr) int + +fn init_os_args_wide(argc int, argv &&byte) []string { + mut args_ := []string{} + for i in 0 .. argc { + args_ << unsafe { string_from_wide(&u16(argv[i])) } + } + return args_ +} + +fn native_glob_pattern(pattern string, mut matches []string) ? { + $if debug { + // FindFirstFile() and FindNextFile() both have a globbing function. + // Unfortunately this is not as pronounced as under Unix, but should provide some functionality + eprintln('os.glob() does not have all the features on Windows as it has on Unix operating systems') + } + mut find_file_data := Win32finddata{} + wpattern := pattern.replace('/', '\\').to_wide() + h_find_files := C.FindFirstFile(wpattern, voidptr(&find_file_data)) + + defer { + C.FindClose(h_find_files) + } + + if h_find_files == C.INVALID_HANDLE_VALUE { + return error('os.glob(): Could not get a file handle: ' + + get_error_msg(int(C.GetLastError()))) + } + + // save first finding + fname := unsafe { string_from_wide(&find_file_data.c_file_name[0]) } + if fname !in ['.', '..'] { + mut fp := fname.replace('\\', '/') + if find_file_data.dw_file_attributes & u32(C.FILE_ATTRIBUTE_DIRECTORY) > 0 { + fp += '/' + } + matches << fp + } + + // check and save next findings + for i := 0; C.FindNextFile(h_find_files, voidptr(&find_file_data)) > 0; i++ { + filename := unsafe { string_from_wide(&find_file_data.c_file_name[0]) } + if filename in ['.', '..'] { + continue + } + mut fpath := filename.replace('\\', '/') + if find_file_data.dw_file_attributes & u32(C.FILE_ATTRIBUTE_DIRECTORY) > 0 { + fpath += '/' + } + matches << fpath + } +} + +pub fn utime(path string, actime int, modtime int) ? { + mut u := C._utimbuf{actime, modtime} + if C._utime(&char(path.str), voidptr(&u)) != 0 { + return error_with_code(posix_get_error_msg(C.errno), C.errno) + } +} + +pub fn ls(path string) ?[]string { + mut find_file_data := Win32finddata{} + mut dir_files := []string{} + // We can also check if the handle is valid. but using is_dir instead + // h_find_dir := C.FindFirstFile(path.str, &find_file_data) + // if (invalid_handle_value == h_find_dir) { + // return dir_files + // } + // C.FindClose(h_find_dir) + if !is_dir(path) { + return error('ls() couldnt open dir "$path": directory does not exist') + } + // NOTE: Should eventually have path struct & os dependant path seperator (eg os.PATH_SEPERATOR) + // we need to add files to path eg. c:\windows\*.dll or :\windows\* + path_files := '$path\\*' + // NOTE:TODO: once we have a way to convert utf16 wide character to utf8 + // we should use FindFirstFileW and FindNextFileW + h_find_files := C.FindFirstFile(path_files.to_wide(), voidptr(&find_file_data)) + first_filename := unsafe { string_from_wide(&find_file_data.c_file_name[0]) } + if first_filename != '.' && first_filename != '..' { + dir_files << first_filename + } + for C.FindNextFile(h_find_files, voidptr(&find_file_data)) > 0 { + filename := unsafe { string_from_wide(&find_file_data.c_file_name[0]) } + if filename != '.' && filename != '..' { + dir_files << filename.clone() + } + } + C.FindClose(h_find_files) + return dir_files +} + +/* +pub fn is_dir(path string) bool { + _path := path.replace('/', '\\') + attr := C.GetFileAttributesW(_path.to_wide()) + if int(attr) == int(C.INVALID_FILE_ATTRIBUTES) { + return false + } + if (int(attr) & C.FILE_ATTRIBUTE_DIRECTORY) != 0 { + return true + } + return false +} +*/ +// mkdir creates a new directory with the specified path. +pub fn mkdir(path string) ?bool { + if path == '.' { + return true + } + apath := real_path(path) + if !C.CreateDirectory(apath.to_wide(), 0) { + return error('mkdir failed for "$apath", because CreateDirectory returned: ' + + get_error_msg(int(C.GetLastError()))) + } + return true +} + +// Ref - https://docs.microsoft.com/en-us/cpp/c-runtime-library/reference/get-osfhandle?view=vs-2019 +// get_file_handle retrieves the operating-system file handle that is associated with the specified file descriptor. +pub fn get_file_handle(path string) HANDLE { + cfile := vfopen(path, 'rb') or { return HANDLE(invalid_handle_value) } + handle := HANDLE(C._get_osfhandle(fileno(cfile))) // CreateFile? - hah, no -_- + return handle +} + +// Ref - https://docs.microsoft.com/en-us/windows/win32/api/libloaderapi/nf-libloaderapi-getmodulefilenamea +// get_module_filename retrieves the fully qualified path for the file that contains the specified module. +// The module must have been loaded by the current process. +pub fn get_module_filename(handle HANDLE) ?string { + unsafe { + mut sz := 4096 // Optimized length + mut buf := &u16(malloc_noscan(4096)) + for { + status := int(C.GetModuleFileNameW(handle, voidptr(&buf), sz)) + match status { + success { + return string_from_wide2(buf, sz) + } + else { + // Must handled with GetLastError and converted by FormatMessage + return error('Cannot get file name from handle') + } + } + } + } + panic('this should be unreachable') // TODO remove unreachable after loop +} + +// Ref - https://docs.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-formatmessagea#parameters +const ( + format_message_allocate_buffer = 0x00000100 + format_message_argument_array = 0x00002000 + format_message_from_hmodule = 0x00000800 + format_message_from_string = 0x00000400 + format_message_from_system = 0x00001000 + format_message_ignore_inserts = 0x00000200 +) + +// Ref - winnt.h +const ( + sublang_neutral = 0x00 + sublang_default = 0x01 + lang_neutral = sublang_neutral +) + +// Ref - https://docs.microsoft.com/en-us/windows/win32/debug/system-error-codes--12000-15999- +const ( + max_error_code = 15841 // ERROR_API_UNAVAILABLE +) + +// ptr_win_get_error_msg return string (voidptr) +// representation of error, only for windows. +fn ptr_win_get_error_msg(code u32) voidptr { + mut buf := voidptr(0) + // Check for code overflow + if code > u32(os.max_error_code) { + return buf + } + C.FormatMessage(os.format_message_allocate_buffer | os.format_message_from_system | os.format_message_ignore_inserts, + 0, code, C.MAKELANGID(os.lang_neutral, os.sublang_default), voidptr(&buf), 0, + 0) + return buf +} + +// get_error_msg return error code representation in string. +pub fn get_error_msg(code int) string { + if code < 0 { // skip negative + return '' + } + ptr_text := ptr_win_get_error_msg(u32(code)) + if ptr_text == 0 { // compare with null + return '' + } + return unsafe { string_from_wide(ptr_text) } +} + +// execute starts the specified command, waits for it to complete, and returns its output. +pub fn execute(cmd string) Result { + if cmd.contains(';') || cmd.contains('&&') || cmd.contains('||') || cmd.contains('\n') { + return Result{ + exit_code: -1 + output: ';, &&, || and \\n are not allowed in shell commands' + } + } + mut child_stdin := &u32(0) + mut child_stdout_read := &u32(0) + mut child_stdout_write := &u32(0) + mut sa := SecurityAttributes{} + sa.n_length = sizeof(C.SECURITY_ATTRIBUTES) + sa.b_inherit_handle = true + create_pipe_ok := C.CreatePipe(voidptr(&child_stdout_read), voidptr(&child_stdout_write), + voidptr(&sa), 0) + if !create_pipe_ok { + error_num := int(C.GetLastError()) + error_msg := get_error_msg(error_num) + return Result{ + exit_code: error_num + output: 'exec failed (CreatePipe): $error_msg' + } + } + set_handle_info_ok := C.SetHandleInformation(child_stdout_read, C.HANDLE_FLAG_INHERIT, + 0) + if !set_handle_info_ok { + error_num := int(C.GetLastError()) + error_msg := get_error_msg(error_num) + return Result{ + exit_code: error_num + output: 'exec failed (SetHandleInformation): $error_msg' + } + } + proc_info := ProcessInformation{} + start_info := StartupInfo{ + lp_reserved2: 0 + lp_reserved: 0 + lp_desktop: 0 + lp_title: 0 + cb: sizeof(C.PROCESS_INFORMATION) + h_std_input: child_stdin + h_std_output: child_stdout_write + h_std_error: child_stdout_write + dw_flags: u32(C.STARTF_USESTDHANDLES) + } + command_line := [32768]u16{} + C.ExpandEnvironmentStringsW(cmd.to_wide(), voidptr(&command_line), 32768) + create_process_ok := C.CreateProcessW(0, &command_line[0], 0, 0, C.TRUE, 0, 0, 0, + voidptr(&start_info), voidptr(&proc_info)) + if !create_process_ok { + error_num := int(C.GetLastError()) + error_msg := get_error_msg(error_num) + return Result{ + exit_code: error_num + output: 'exec failed (CreateProcess) with code $error_num: $error_msg cmd: $cmd' + } + } + C.CloseHandle(child_stdin) + C.CloseHandle(child_stdout_write) + buf := [4096]byte{} + mut bytes_read := u32(0) + mut read_data := strings.new_builder(1024) + for { + mut result := false + unsafe { + result = C.ReadFile(child_stdout_read, &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().trim_space() + unsafe { read_data.free() } + exit_code := u32(0) + C.WaitForSingleObject(proc_info.h_process, C.INFINITE) + C.GetExitCodeProcess(proc_info.h_process, voidptr(&exit_code)) + C.CloseHandle(proc_info.h_process) + C.CloseHandle(proc_info.h_thread) + return Result{ + output: soutput + exit_code: int(exit_code) + } +} + +pub fn symlink(origin string, target string) ?bool { + // this is a temporary fix for TCC32 due to runtime error + // TODO: find the cause why TCC32 for Windows does not work without the compiletime option + $if x64 || x32 { + mut flags := 0 + if is_dir(origin) { + flags ^= 1 + } + + flags ^= 2 // SYMBOLIC_LINK_FLAG_ALLOW_UNPRIVILEGED_CREATE + res := C.CreateSymbolicLinkW(target.to_wide(), origin.to_wide(), flags) + + // 1 = success, != 1 failure => https://stackoverflow.com/questions/33010440/createsymboliclink-on-windows-10 + if res != 1 { + return error(get_error_msg(int(C.GetLastError()))) + } + if !exists(target) { + return error('C.CreateSymbolicLinkW reported success, but symlink still does not exist') + } + return true + } + return false +} + +pub fn link(origin string, target string) ?bool { + res := C.CreateHardLinkW(target.to_wide(), origin.to_wide(), C.NULL) + // 1 = success, != 1 failure => https://stackoverflow.com/questions/33010440/createsymboliclink-on-windows-10 + if res != 1 { + return error(get_error_msg(int(C.GetLastError()))) + } + if !exists(target) { + return error('C.CreateHardLinkW reported success, but link still does not exist') + } + return true +} + +pub fn (mut f File) close() { + if !f.is_opened { + return + } + f.is_opened = false + C.fflush(f.cfile) + C.fclose(f.cfile) +} + +pub struct ExceptionRecord { +pub: + // status_ constants + code u32 + flags u32 + record &ExceptionRecord + address voidptr + param_count u32 + // params []voidptr +} + +pub struct ContextRecord { + // TODO +} + +pub struct ExceptionPointers { +pub: + exception_record &ExceptionRecord + context_record &ContextRecord +} + +pub type VectoredExceptionHandler = fn (&ExceptionPointers) u32 + +// This is defined in builtin because we use vectored exception handling +// for our unhandled exception handler on windows +// As a result this definition is commented out to prevent +// duplicate definitions from displeasing the compiler +// fn C.AddVectoredExceptionHandler(u32, VectoredExceptionHandler) +pub fn add_vectored_exception_handler(first bool, handler VectoredExceptionHandler) { + C.AddVectoredExceptionHandler(u32(first), C.PVECTORED_EXCEPTION_HANDLER(handler)) +} + +// this is defined in builtin_windows.c.v in builtin +// fn C.IsDebuggerPresent() bool +pub fn debugger_present() bool { + return C.IsDebuggerPresent() +} + +pub fn uname() Uname { + sys_and_ver := execute('cmd /c ver').output.split('[') + nodename := hostname() + machine := getenv('PROCESSOR_ARCHITECTURE') + return Uname{ + sysname: sys_and_ver[0].trim_space() + nodename: nodename + release: sys_and_ver[1].replace(']', '') + version: sys_and_ver[0] + '[' + sys_and_ver[1] + machine: machine + } +} + +pub fn hostname() string { + hostname := [255]u16{} + size := u32(255) + res := C.GetComputerNameW(&hostname[0], &size) + if !res { + return get_error_msg(int(C.GetLastError())) + } + return unsafe { string_from_wide(&hostname[0]) } +} + +pub fn loginname() string { + loginname := [255]u16{} + size := u32(255) + res := C.GetUserNameW(&loginname[0], &size) + if !res { + return get_error_msg(int(C.GetLastError())) + } + return unsafe { string_from_wide(&loginname[0]) } +} + +// `is_writable_folder` - `folder` exists and is writable to the process +pub fn is_writable_folder(folder string) ?bool { + if !exists(folder) { + return error('`$folder` does not exist') + } + if !is_dir(folder) { + return error('`folder` is not a folder') + } + tmp_perm_check := join_path(folder, 'tmp_perm_check_pid_' + getpid().str()) + mut f := open_file(tmp_perm_check, 'w+', 0o700) or { + return error('cannot write to folder $folder: $err') + } + f.close() + rm(tmp_perm_check) ? + return true +} + +[inline] +pub fn getpid() int { + return C._getpid() +} + +[inline] +pub fn getppid() int { + return 0 +} + +[inline] +pub fn getuid() int { + return 0 +} + +[inline] +pub fn geteuid() int { + return 0 +} + +[inline] +pub fn getgid() int { + return 0 +} + +[inline] +pub fn getegid() int { + return 0 +} + +pub fn posix_set_permission_bit(path_s string, mode u32, enable bool) { + // windows has no concept of a permission mask, so do nothing +} diff --git a/v_windows/v/vlib/os/process.c.v b/v_windows/v/vlib/os/process.c.v new file mode 100644 index 0000000..c7e2b98 --- /dev/null +++ b/v_windows/v/vlib/os/process.c.v @@ -0,0 +1,248 @@ +module os + +// signal_kill - kills the process, after that it is no longer running +pub fn (mut p Process) signal_kill() { + if p.status !in [.running, .stopped] { + return + } + p._signal_kill() + p.status = .aborted + 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 { + return + } + p._signal_stop() + p.status = .stopped + return +} + +// signal_continue - tell a stopped process to continue/resume its work +pub fn (mut p Process) signal_continue() { + if p.status != .stopped { + return + } + p._signal_continue() + p.status = .running + return +} + +// wait - wait for a process to finish. +// NB: You have to call p.wait(), otherwise a finished process +// would get to a zombie state, and its resources will not get +// released fully, until its parent process exits. +// NB: This call will block the calling process until the child +// process is finished. +pub fn (mut p Process) wait() { + if p.status == .not_started { + p._spawn() + } + if p.status !in [.running, .stopped] { + return + } + p._wait() + return +} + +// close - 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. +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 { + fd_close(p.stdio_fd[i]) + } + } + } +} + +[unsafe] +pub fn (mut p Process) free() { + p.close() + unsafe { + p.filename.free() + p.err.free() + p.args.free() + p.env.free() + } +} + +// +// _spawn - should not be called directly, but only by p.run()/p.wait() . +// It encapsulates the fork/execve mechanism that allows the +// asynchronous starting of the new child process. +fn (mut p Process) _spawn() int { + if !p.env_is_custom { + p.env = []string{} + current_environment := environ() + for k, v in current_environment { + p.env << '$k=$v' + } + } + mut pid := 0 + $if windows { + pid = p.win_spawn_process() + } $else { + pid = p.unix_spawn_process() + } + p.pid = pid + p.status = .running + return 0 +} + +// is_alive - query whether the process p.pid is still alive +pub fn (mut p Process) is_alive() bool { + if p.status in [.running, .stopped] { + return p._is_alive() + } + return false +} + +// +pub fn (mut p Process) set_redirect_stdio() { + p.use_stdio_ctl = true + return +} + +pub fn (mut p Process) stdin_write(s string) { + p._check_redirection_call('stdin_write') + $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') + $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') + $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') + $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') + $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 +fn (mut p Process) _check_redirection_call(fn_name string) { + if !p.use_stdio_ctl { + panic('Call p.set_redirect_stdio() before calling p.$fn_name') + } + if p.status == .not_started { + panic('Call p.${fn_name}() after you have called p.run()') + } +} + +// _signal_stop - should not be called directly, except by p.signal_stop +fn (mut p Process) _signal_stop() { + $if windows { + p.win_stop_process() + } $else { + p.unix_stop_process() + } +} + +// _signal_continue - should not be called directly, just by p.signal_continue +fn (mut p Process) _signal_continue() { + $if windows { + p.win_resume_process() + } $else { + p.unix_resume_process() + } +} + +// _signal_kill - should not be called directly, except by p.signal_kill +fn (mut p Process) _signal_kill() { + $if windows { + p.win_kill_process() + } $else { + p.unix_kill_process() + } +} + +// _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 { + p.win_wait() + } $else { + p.unix_wait() + } +} + +// _is_alive - should not be called directly, except by p.is_alive() +fn (mut p Process) _is_alive() bool { + $if windows { + return p.win_is_alive() + } $else { + return p.unix_is_alive() + } +} + +// run - starts the new process +pub fn (mut p Process) run() { + if p.status != .not_started { + return + } + p._spawn() + return +} diff --git a/v_windows/v/vlib/os/process.js.v b/v_windows/v/vlib/os/process.js.v new file mode 100644 index 0000000..dc15c8b --- /dev/null +++ b/v_windows/v/vlib/os/process.js.v @@ -0,0 +1,117 @@ +module os + +#const $child_process = require('child_process') + +// new_process - create a new process descriptor +// NB: new does NOT start the new process. +// That is done because you may want to customize it first, +// by calling different set_ methods on it. +// In order to start it, call p.run() or p.wait() +pub fn new_process(filename string) &Process { + return &Process{ + filename: filename + stdio_fd: [-1, -1, -1]! + } +} + +fn (mut p Process) spawn_internal() { + #p.val.pid = $child_process.spawn( + #p.val.filename+'', + #p.val.args.arr.map((x) => x.valueOf() + ''), + #{ + #env: (p.val.env_is_custom ? p.val.env : $process.env), + #}) + #p.val.pid.on('error', function (err) { builtin.panic('Failed to start subprocess') }) + + p.status = .running + // todo(playX): stderr,stdin + if p.use_stdio_ctl { + #p.val.pid.stdout.pipe(process.stdout) + #p.val.pid.stdin.pipe(process.stdin) + #p.val.pid.stderr.pipe(process.stderr) + } +} + +pub fn (mut p Process) run() { + if p.status != .not_started { + return + } + p.spawn_internal() + return +} + +pub fn (mut p Process) signal_kill() { + if p.status !in [.running, .stopped] { + return + } + #p.val.pid.kill('SIGKILL'); + + p.status = .aborted +} + +pub fn (mut p Process) signal_stop() { + if p.status !in [.running, .stopped] { + return + } + #p.val.pid.kill('SIGSTOP'); + + p.status = .aborted +} + +pub fn (mut p Process) signal_continue() { + if p.status != .stopped { + return + } + #p.val.pid.kill('SIGCONT'); + + p.status = .running + return +} + +pub fn (mut p Process) wait() { + if p.status == .not_started { + p.spawn_internal() + } + if p.status !in [.running, .stopped] { + return + } + + p.wait_internal() + return +} + +fn (mut p Process) wait_internal() { + #p.val.pid.on('exit', function (code) { console.log(code) }) +} + +pub fn (mut p Process) set_redirect_stdio() { + p.use_stdio_ctl = true + return +} + +pub fn (mut p Process) stdin_write(s string) { + p.check_redirection_call('stdin_write') + #p.val.pid.stdin.write(s) +} + +// todo(playX): probably does not work + +// 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') + mut res := '' + #p.val.pid.stdout.on('data', function (data) { res = new builtin.string(data) }) + + return res +} + +// _check_redirection_call - should be called just by stdxxx methods +fn (mut p Process) check_redirection_call(fn_name string) { + if !p.use_stdio_ctl { + panic('Call p.set_redirect_stdio() before calling p.$fn_name') + } + if p.status == .not_started { + panic('Call p.${fn_name}() after you have called p.run()') + } +} diff --git a/v_windows/v/vlib/os/process.v b/v_windows/v/vlib/os/process.v new file mode 100644 index 0000000..3f88398 --- /dev/null +++ b/v_windows/v/vlib/os/process.v @@ -0,0 +1,70 @@ +module os + +// ProcessState.not_started - the process has not yet started +// ProcessState.running - the process is currently running +// ProcessState.stopped - the process was running, but was stopped temporarily +// ProcessState.exited - the process has finished/exited +// ProcessState.aborted - the process was terminated by a signal +// ProcessState.closed - the process resources like opened file descriptors were freed/discarded, final state. +pub enum ProcessState { + not_started + running + stopped + exited + aborted + closed +} + +[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 + // 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 + 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() + 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 +// NB: new does NOT start the new process. +// That is done because you may want to customize it first, +// by calling different set_ methods on it. +// In order to start it, call p.run() or p.wait() +pub fn new_process(filename string) &Process { + return &Process{ + filename: filename + stdio_fd: [-1, -1, -1]! + } +} + +// set_args - set the arguments for the new process +pub fn (mut p Process) set_args(pargs []string) { + if p.status != .not_started { + return + } + p.args = pargs + 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 { + return + } + p.env_is_custom = true + p.env = []string{} + for k, v in envs { + p.env << '$k=$v' + } + return +} diff --git a/v_windows/v/vlib/os/process_nix.c.v b/v_windows/v/vlib/os/process_nix.c.v new file mode 100644 index 0000000..74140d1 --- /dev/null +++ b/v_windows/v/vlib/os/process_nix.c.v @@ -0,0 +1,146 @@ +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 { + _ = C.pipe(&pipeset[0]) // pipe read end 0 <- 1 pipe write end + _ = C.pipe(&pipeset[2]) // pipe read end 2 <- 3 pipe write end + _ = C.pipe(&pipeset[4]) // pipe read end 4 <- 5 pipe write end + } + pid := fork() + if pid != 0 { + // This is the parent process after the fork. + // NB: pid contains the process ID of the child process + if p.use_stdio_ctl { + p.stdio_fd[0] = pipeset[1] // store the write end of child's in + p.stdio_fd[1] = pipeset[2] // store the read end of child's out + p.stdio_fd[2] = pipeset[4] // store the read end of child's err + // close the rest of the pipe fds, the parent does not need them + fd_close(pipeset[0]) + fd_close(pipeset[3]) + fd_close(pipeset[5]) + } + return pid + } + // + // Here, we are in the child process. + // 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. + // Close the parent's pipe fds, the child do not need them: + fd_close(pipeset[1]) + fd_close(pipeset[2]) + fd_close(pipeset[4]) + // redirect the pipe fds to the child's in/out/err fds: + C.dup2(pipeset[0], 0) + C.dup2(pipeset[3], 1) + C.dup2(pipeset[5], 2) + // close the pipe fdsx after the redirection + fd_close(pipeset[0]) + fd_close(pipeset[3]) + fd_close(pipeset[5]) + } + execve(p.filename, p.args, p.env) or { + eprintln(err) + exit(1) + } + return 0 +} + +fn (mut p Process) unix_stop_process() { + C.kill(p.pid, C.SIGSTOP) +} + +fn (mut p Process) unix_resume_process() { + C.kill(p.pid, C.SIGCONT) +} + +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) + if ret == -1 { + p.err = posix_get_error_msg(C.errno) + return + } + pret, is_signaled := posix_wait4_to_exit_status(cstatus) + if is_signaled { + p.status = .aborted + p.err = 'Terminated by signal ${ret:2d} (${sigint_to_signal_name(pret)})' + } else { + p.status = .exited + } + p.code = pret +} + +fn (mut p Process) unix_is_alive() bool { + cstatus := 0 + ret := C.waitpid(p.pid, &cstatus, C.WNOHANG) + if ret == -1 { + p.err = posix_get_error_msg(C.errno) + return false + } + if ret == 0 { + return true + } + pret, is_signaled := posix_wait4_to_exit_status(cstatus) + if is_signaled { + p.status = .aborted + p.err = 'Terminated by signal ${ret:2d} (${sigint_to_signal_name(pret)})' + } else { + p.status = .exited + } + p.code = pret + return false +} + +// these are here to make v_win.c/v.c generation work in all cases: +fn (mut p Process) win_spawn_process() int { + return 0 +} + +fn (mut p Process) win_stop_process() { +} + +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/v_windows/v/vlib/os/process_test.v b/v_windows/v/vlib/os/process_test.v new file mode 100644 index 0000000..5301472 --- /dev/null +++ b/v_windows/v/vlib/os/process_test.v @@ -0,0 +1,96 @@ +import os +import time + +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') +) + +fn testsuite_begin() ? { + os.rm(test_os_process) or {} + 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() { + pid := os.getpid() + eprintln('current pid: $pid') + assert pid != 0 +} + +fn test_run() { + mut p := os.new_process(test_os_process) + p.set_args(['-timeout_ms', '150', '-period_ms', '50']) + p.run() + assert p.status == .running + assert p.pid > 0 + assert p.pid != os.getpid() + mut i := 0 + for { + if !p.is_alive() { + break + } + $if trace_process_output ? { + os.system('ps -opid= -oppid= -ouser= -onice= -of= -ovsz= -orss= -otime= -oargs= -p $p.pid') + } + time.sleep(50 * time.millisecond) + i++ + } + p.wait() + assert p.code == 0 + assert p.status == .exited + // + eprintln('polling iterations: $i') + assert i < 50 + p.close() +} + +fn test_wait() { + mut p := os.new_process(test_os_process) + assert p.status != .exited + p.wait() + assert p.status == .exited + assert p.code == 0 + assert p.pid != os.getpid() + p.close() +} + +fn test_slurping_output() { + mut p := os.new_process(test_os_process) + p.set_args(['-timeout_ms', '500', '-period_ms', '50']) + p.set_redirect_stdio() + assert p.status != .exited + p.wait() + assert p.status == .exited + assert p.code == 0 + output := p.stdout_slurp().trim_space() + errors := p.stderr_slurp().trim_space() + p.close() + $if trace_process_output ? { + eprintln('---------------------------') + eprintln('p output: "$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') + assert errors.contains('stderr, 4') +} diff --git a/v_windows/v/vlib/os/process_windows.c.v b/v_windows/v/vlib/os/process_windows.c.v new file mode 100644 index 0000000..bcdf971 --- /dev/null +++ b/v_windows/v/vlib/os/process_windows.c.v @@ -0,0 +1,243 @@ +module os + +import strings + +fn C.GenerateConsoleCtrlEvent(event u32, pgid u32) bool +fn C.GetModuleHandleA(name &char) HMODULE +fn C.GetProcAddress(handle voidptr, procname &byte) voidptr +fn C.TerminateProcess(process HANDLE, exit_code u32) bool + +type FN_NTSuspendResume = fn (voidptr) + +fn ntdll_fn(name &char) 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 { + 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_reserved2: 0 + 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() { + 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() { + 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() { + 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() { + 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 = int(exit_code) +} + +fn (mut p Process) win_is_alive() bool { + 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 { + return 0 +} + +fn (mut p Process) unix_stop_process() { +} + +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() { +} + +fn (mut p Process) unix_is_alive() bool { + return false +} diff --git a/v_windows/v/vlib/os/signal.c.v b/v_windows/v/vlib/os/signal.c.v new file mode 100644 index 0000000..5dda3bd --- /dev/null +++ b/v_windows/v/vlib/os/signal.c.v @@ -0,0 +1,58 @@ +module os + +#include + +// os.Signal - enumerate possible POSIX signals and +// their integer codes. +// NB: the integer codes are given here explicitly, +// to make it easier to lookup, without needing to +// consult man pages / signal.h . + +pub enum Signal { + hup = 1 + int = 2 + quit = 3 + ill = 4 + trap = 5 + abrt = 6 + bus = 7 + fpe = 8 + kill = 9 + usr1 = 10 + segv = 11 + usr2 = 12 + pipe = 13 + alrm = 14 + term = 15 + stkflt = 16 + chld = 17 + cont = 18 + stop = 19 + tstp = 20 + ttin = 21 + ttou = 22 + urg = 23 + xcpu = 24 + xfsz = 25 + vtalrm = 26 + prof = 27 + winch = 28 + poll = 29 + pwr = 30 + sys = 31 +} + +type SignalHandler = fn (Signal) + +fn C.signal(signal int, handlercb SignalHandler) voidptr + +// signal will assign `handler` callback to be called when `signum` signal is received. +pub fn signal_opt(signum Signal, handler SignalHandler) ?SignalHandler { + C.errno = 0 + prev_handler := C.signal(int(signum), handler) + if prev_handler == C.SIG_ERR { + // errno isn't correctly set on Windows, but EINVAL is this only possible value it can take anyway + return error_with_code(posix_get_error_msg(C.EINVAL), C.EINVAL) + } + return SignalHandler(prev_handler) +} diff --git a/v_windows/v/vlib/os/signal_test.v b/v_windows/v/vlib/os/signal_test.v new file mode 100644 index 0000000..1c56540 --- /dev/null +++ b/v_windows/v/vlib/os/signal_test.v @@ -0,0 +1,35 @@ +import os + +fn former_handler(signal os.Signal) { + println('former_handler') + exit(0) +} + +fn default_handler(signal os.Signal) { + println('default_handler') + exit(0) +} + +fn test_signal_opt() { + os.signal_opt(.int, default_handler) or { assert false } +} + +fn test_signal_opt_invalid_argument() { + // Can't register a signal on SIGKILL + if _ := os.signal_opt(.kill, default_handler) { + assert false + } + os.signal_opt(.kill, default_handler) or { + assert err.msg == 'Invalid argument' + assert err.code == 22 + } +} + +fn test_signal_opt_return_former_handler() { + func1 := os.signal_opt(.term, former_handler) or { panic('unexpected error') } + assert isnil(func1) + func2 := os.signal_opt(.term, default_handler) or { panic('unexpected error') } + assert !isnil(func2) + // this should work, but makes the CI fail because of a bug in clang -fsanitize=memory + // assert func2 == former_handler +} -- cgit v1.2.3