Initial project import
This commit is contained in:
commit
23f2d29e32
11957 files changed
+1754577
No files matched your search
@@ -0,0 +1,76 @@
|
||||
## Description
|
||||
|
||||
`os` provides common OS/platform independent functions for accessing
|
||||
command line arguments, reading/writing files, listing folders,
|
||||
handling processes etc.
|
||||
|
||||
On Windows, `os.data_dir()` uses `%LocalAppData%` for user-specific
|
||||
application data.
|
||||
|
||||
### Running commands
|
||||
|
||||
Use `os.exec(['program', 'arg 1', 'arg 2'])` when the command and its arguments
|
||||
are already separate values. It runs the program directly and does not invoke a
|
||||
shell, so spaces and shell metacharacters inside arguments are passed literally.
|
||||
|
||||
Use `os.execute('command string')` only when shell syntax is intended.
|
||||
|
||||
---
|
||||
|
||||
### Security advice related to TOCTOU attacks
|
||||
|
||||
A few `os` module functions can lead to the **TOCTOU** vulnerability if used incorrectly.
|
||||
**TOCTOU** (Time-of-Check-to-Time-of-Use problem) can occur when a file, folder or similar
|
||||
is checked for certain specifications (e.g. read, write permissions) and a change is made
|
||||
afterwards.
|
||||
In the time between the initial check and the edit, an attacker can then cause damage.
|
||||
The following example shows an attack strategy on the left and an improved variant on the right
|
||||
so that **TOCTOU** is no longer possible.
|
||||
|
||||
**Example** <br>
|
||||
*Hint*: `os.create()` opens a file in write-only mode
|
||||
|
||||
<table>
|
||||
<tr>
|
||||
<td>Possibility for TOCTOU attack</td>
|
||||
<td>TOCTOU not possible</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
```v ignore
|
||||
if os.is_writable("file") {
|
||||
// time to make a quick attack
|
||||
// (e.g. symlink /etc/passwd to `file`)
|
||||
|
||||
mut f := os.create('path/to/file')!
|
||||
// do something with file
|
||||
f.close()
|
||||
}
|
||||
```
|
||||
|
||||
</td>
|
||||
<td>
|
||||
|
||||
```v ignore
|
||||
mut f := os.create('path/to/file') or {
|
||||
println("file not writable")
|
||||
}
|
||||
|
||||
// file is locked
|
||||
// do something with file
|
||||
|
||||
f.close()
|
||||
```
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
**Proven affected functions** <br>
|
||||
The following functions should be used with care and only when used correctly.
|
||||
|
||||
- os.is_readable()
|
||||
- os.is_writable()
|
||||
- os.is_executable()
|
||||
- os.is_link()
|
||||
@@ -0,0 +1,51 @@
|
||||
// Copyright (c) 2019-2024 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_before 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
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
# Asset
|
||||
|
||||
The `asset` module provides a cross platform way to read assets, without cluttering
|
||||
the user code with comptime conditionals, like `$if ios {` or `$if android {` etc.
|
||||
|
||||
Currently it supports Android assets, and desktop applications, that do not use
|
||||
archived/zipped files, but could be extended to support archived files too in the future.
|
||||
|
||||
It relies on the assumption that each platform has a way to either read files, relative
|
||||
to the location of the executable, or a platform specific way, to package all asset files
|
||||
into the application archive/package/executable, that is then distributed to the end users.
|
||||
@@ -0,0 +1,44 @@
|
||||
module asset
|
||||
|
||||
import os
|
||||
|
||||
// get_path returns a platform specific path, based on the given asset base folder and relative path
|
||||
// of a resource in it. On desktop systems, it returns a path relative to the location of the executable.
|
||||
// On Android, it will return just the relative_path segment, allowing you to later use os.read_apk_asset
|
||||
// to read from it.
|
||||
@[manualfree]
|
||||
pub fn get_path(base_folder string, relative_path string) string {
|
||||
$if android {
|
||||
return relative_path.clone()
|
||||
} $else {
|
||||
fpath := os.join_path_single(base_folder, relative_path)
|
||||
defer { unsafe { fpath.free() } }
|
||||
return os.resource_abs_path(fpath)
|
||||
}
|
||||
}
|
||||
|
||||
// read_bytes will read all of the given asset, specified by its base_folder and relative path in it.
|
||||
// On Android, it will use os.read_apk_asset, relying that the asset base folder has been prepared, and
|
||||
// prepackaged inside your APK. On desktop systems, it will use the base_folder and relative_path, to
|
||||
// locate the file, in a way, that is relative to the executable.
|
||||
@[manualfree]
|
||||
pub fn read_bytes(base_folder string, relative_path string) ![]u8 {
|
||||
fpath := get_path(base_folder, relative_path)
|
||||
defer { unsafe { fpath.free() } }
|
||||
mut f_read := os.read_bytes
|
||||
$if android {
|
||||
f_read = os.read_apk_asset
|
||||
}
|
||||
res := f_read(fpath)!
|
||||
return res
|
||||
}
|
||||
|
||||
// read_text will return the full content of the given asset as a string.
|
||||
// See also read_bytes.
|
||||
@[manualfree]
|
||||
pub fn read_text(base_folder string, relative_path string) !string {
|
||||
bytes := read_bytes(base_folder, relative_path)!
|
||||
defer { unsafe { bytes.free() } }
|
||||
res := bytes.bytestr()
|
||||
return res
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
fn main() {
|
||||
sys_write(1, c'hello\n', 6)
|
||||
s := 'test string\n'
|
||||
sys_write(1, s.str, u64(s.len))
|
||||
a := s[0]
|
||||
println('Hello freestanding!')
|
||||
println(a)
|
||||
}
|
||||
@@ -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('-'))
|
||||
}
|
||||
@@ -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']
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
module os
|
||||
|
||||
import strings
|
||||
|
||||
// Command represents a running shell command, that the parent process
|
||||
// wishes to monitor for output on its stdout pipe.
|
||||
pub struct Command {
|
||||
mut:
|
||||
f voidptr
|
||||
pub mut:
|
||||
eof bool
|
||||
exit_code int
|
||||
pub:
|
||||
path string
|
||||
redirect_stdout bool
|
||||
}
|
||||
|
||||
// start_new_command will create a new os.Command, and start it right away.
|
||||
// The command will represent a process running the passed shell command `cmd`,
|
||||
// in a way that you can later call c.read_line() to intercept the new child process
|
||||
// output line by line, streaming while the command is running.
|
||||
// See also c.eof, c.read_line(), c.close() and c.exit_code .
|
||||
pub fn start_new_command(cmd string) !Command {
|
||||
mut res := Command{
|
||||
path: cmd
|
||||
}
|
||||
res.start()!
|
||||
return res
|
||||
}
|
||||
|
||||
// start will start the command. Use start_new_command/1 instead.
|
||||
@[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')
|
||||
}
|
||||
}
|
||||
|
||||
// read_line returns a single line from the stdout of the running command.
|
||||
// Note: c.eof will be set to true, if the command ended while a line was read.
|
||||
// The returned line will contain all of the accumulated output before the process ended.
|
||||
// In practice, that often means, you will get a single '' and c.eof == true at the end.
|
||||
@[manualfree]
|
||||
pub fn (mut c Command) read_line() string {
|
||||
buf := [4096]u8{}
|
||||
mut res := strings.new_builder(1024)
|
||||
defer { unsafe { res.free() } }
|
||||
unsafe {
|
||||
bufbp := &u8(&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
|
||||
}
|
||||
|
||||
// close will close the pipe to the command, and wait for the command to finish,
|
||||
// then set .exit_code according to how its final process status.
|
||||
pub fn (mut c Command) close() ! {
|
||||
c.exit_code = vpclose(c.f)
|
||||
c.f = unsafe { nil }
|
||||
if c.exit_code == 127 {
|
||||
return error_with_code('error', 127)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
import os
|
||||
|
||||
fn test_command() {
|
||||
if os.user_os() == 'windows' {
|
||||
eprintln('>>> skipping command test on windows')
|
||||
return
|
||||
}
|
||||
|
||||
mut cmd := os.start_new_command('ls')!
|
||||
for !cmd.eof {
|
||||
line := cmd.read_line()
|
||||
if line == '' {
|
||||
continue
|
||||
}
|
||||
dump(line)
|
||||
}
|
||||
cmd.close()!
|
||||
assert cmd.exit_code == 0
|
||||
|
||||
eprintln('-------------------------')
|
||||
// This will return a non 0 code
|
||||
mut cmd_to_fail := os.start_new_command('ls -M')!
|
||||
for !cmd_to_fail.eof {
|
||||
line := cmd_to_fail.read_line()
|
||||
if line == '' {
|
||||
continue
|
||||
}
|
||||
dump(line)
|
||||
}
|
||||
cmd_to_fail.close()!
|
||||
assert cmd_to_fail.exit_code != 0 // 2 on linux, 1 on macos
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
module os
|
||||
@@ -0,0 +1,25 @@
|
||||
module os
|
||||
|
||||
const max_path_buffer_size = max_path_len
|
||||
|
||||
const o_binary = 0 // input and output is not translated; the default on unix
|
||||
|
||||
const o_rdonly = C.O_RDONLY // open the file read-only.
|
||||
|
||||
const o_wronly = C.O_WRONLY // open the file write-only.
|
||||
|
||||
const o_rdwr = C.O_RDWR // open the file read-write.
|
||||
|
||||
const o_create = C.O_CREAT // create a new file if none exists.
|
||||
|
||||
const o_excl = C.O_EXCL // used with o_create, file must not exist.
|
||||
|
||||
const o_noctty = C.O_NOCTTY // if file is terminal, don't make it the controller terminal
|
||||
|
||||
const o_trunc = C.O_TRUNC // truncate regular writable file when opened.
|
||||
|
||||
const o_append = C.O_APPEND // append data to the file when writing.
|
||||
|
||||
const o_nonblock = C.O_NONBLOCK // prevents blocking when opening files
|
||||
|
||||
const o_sync = C.O_SYNC
|
||||
@@ -0,0 +1,144 @@
|
||||
module os
|
||||
|
||||
const max_path_buffer_size = 2 * max_path_len
|
||||
|
||||
// Ref - winnt.h
|
||||
const success = 0x0000 // ERROR_SUCCESS
|
||||
|
||||
const error_insufficient_buffer = 0x0082
|
||||
|
||||
const handle_generic_read = u32(0x80000000)
|
||||
const handle_open_existing = 0x00000003
|
||||
|
||||
const file_share_read = 0x01
|
||||
const file_share_write = 0x02
|
||||
const file_share_delete = 0x04
|
||||
|
||||
const file_notify_change_file_name = 0x01
|
||||
const file_notify_change_dir_name = 0x02
|
||||
const file_notify_change_attributes = 0x04
|
||||
const file_notify_change_size = 0x08
|
||||
const file_notify_change_last_write = 0x10
|
||||
const file_notify_change_last_access = 0x20
|
||||
const file_notify_change_creation = 0x40
|
||||
const file_notify_change_security = 0x80
|
||||
|
||||
const file_action_added = 0x01
|
||||
const file_action_removed = 0x02
|
||||
const file_action_modified = 0x03
|
||||
const file_action_renamed_old_name = 0x04
|
||||
const file_action_renamed_new_name = 0x05
|
||||
|
||||
const file_attr_readonly = 0x00000001
|
||||
const file_attr_hidden = 0x00000002
|
||||
const file_attr_system = 0x00000004
|
||||
const file_attr_directory = 0x00000010
|
||||
const file_attr_archive = 0x00000020
|
||||
const file_attr_device = 0x00000040
|
||||
const file_attr_normal = 0x00000080
|
||||
const file_attr_temporary = 0x00000100
|
||||
const file_attr_sparse_file = 0x00000200
|
||||
const file_attr_reparse_point = 0x00000400
|
||||
const file_attr_compressed = 0x00000800
|
||||
const file_attr_offline = 0x00001000
|
||||
const file_attr_not_content_indexed = 0x00002000
|
||||
const file_attr_encrypted = 0x00004000
|
||||
const file_attr_integrity_stream = 0x00008000
|
||||
const file_attr_virtual = 0x00010000
|
||||
const file_attr_no_scrub_data = 0x00020000
|
||||
|
||||
const file_type_unknown = 0x00
|
||||
const file_type_disk = 0x01
|
||||
const file_type_char = 0x02
|
||||
const 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
|
||||
// Input Buffer
|
||||
const enable_echo_input = 0x0004
|
||||
const enable_extended_flags = 0x0080
|
||||
const enable_insert_mode = 0x0020
|
||||
const enable_line_input = 0x0002
|
||||
const enable_mouse_input = 0x0010
|
||||
const enable_processed_input = 0x0001
|
||||
const enable_quick_edit_mode = 0x0040
|
||||
const enable_window_input = 0x0008
|
||||
const enable_virtual_terminal_input = 0x0200
|
||||
// Output Screen Buffer
|
||||
const enable_processed_output = 0x01
|
||||
const enable_wrap_at_eol_output = 0x02
|
||||
const enable_virtual_terminal_processing = 0x04
|
||||
const disable_newline_auto_return = 0x08
|
||||
const enable_lvb_grid_worldwide = 0x10
|
||||
|
||||
// File modes
|
||||
const o_rdonly = 0x0000 // open the file read-only.
|
||||
|
||||
const o_wronly = 0x0001 // open the file write-only.
|
||||
|
||||
const o_rdwr = 0x0002 // open the file read-write.
|
||||
|
||||
const o_append = 0x0008 // append data to the file when writing.
|
||||
|
||||
const o_create = 0x0100 // create a new file if none exists.
|
||||
|
||||
const o_binary = 0x8000 // input and output is not translated.
|
||||
|
||||
const o_trunc = 0x0200 // truncate regular writable file when opened.
|
||||
|
||||
const o_excl = 0x0400 // used with o_create, file must not exist.
|
||||
|
||||
const o_sync = 0x0000 // open for synchronous I/O (ignored on Windows)
|
||||
|
||||
const o_noctty = 0x0000 // make file non-controlling tty (ignored on Windows)
|
||||
|
||||
const o_nonblock = 0x0000
|
||||
|
||||
const status_access_violation = u32(0xC0000005)
|
||||
const status_in_page_error = u32(0xC0000006)
|
||||
const status_invalid_handle = u32(0xC0000008)
|
||||
const status_invalid_parameter = u32(0xC000000D)
|
||||
const status_no_memory = u32(0xC0000017)
|
||||
const status_illegal_instruction = u32(0xC000001D)
|
||||
const status_noncontinuable_exception = u32(0xC0000025)
|
||||
const status_invalid_disposition = u32(0xC0000026)
|
||||
const status_array_bounds_exceeded = u32(0xC000008C)
|
||||
const status_float_denormal_operand = u32(0xC000008D)
|
||||
const status_float_divide_by_zero = u32(0xC000008E)
|
||||
const status_float_inexact_result = u32(0xC000008F)
|
||||
const status_float_invalid_operation = u32(0xC0000090)
|
||||
const status_float_overflow = u32(0xC0000091)
|
||||
const status_float_stack_check = u32(0xC0000092)
|
||||
const status_float_underflow = u32(0xC0000093)
|
||||
const status_integer_divide_by_zero = u32(0xC0000094)
|
||||
const status_integer_overflow = u32(0xC0000095)
|
||||
const status_privileged_instruction = u32(0xC0000096)
|
||||
const status_stack_overflow = u32(0xC00000FD)
|
||||
const status_dll_not_found = u32(0xC0000135)
|
||||
const status_ordinal_not_found = u32(0xC0000138)
|
||||
const status_entrypoint_not_found = u32(0xC0000139)
|
||||
const status_control_c_exit = u32(0xC000013A)
|
||||
const status_dll_init_failed = u32(0xC0000142)
|
||||
const status_float_multiple_faults = u32(0xC00002B4)
|
||||
const status_float_multiple_traps = u32(0xC00002B5)
|
||||
const status_reg_nat_consumption = u32(0xC00002C9)
|
||||
const status_heap_corruption = u32(0xC0000374)
|
||||
const status_stack_buffer_overrun = u32(0xC0000409)
|
||||
const status_invalid_cruntime_parameter = u32(0xC0000417)
|
||||
const status_assertion_failure = u32(0xC0000420)
|
||||
|
||||
// Windows Registry Constants
|
||||
pub const hkey_local_machine = voidptr(0x80000002)
|
||||
pub const hkey_current_user = voidptr(0x80000001)
|
||||
pub const key_query_value = 0x0001
|
||||
pub const key_set_value = 0x0002
|
||||
pub const key_enumerate_sub_keys = 0x0008
|
||||
pub const key_wow64_32key = 0x0200
|
||||
|
||||
// Windows Messages
|
||||
pub const hwnd_broadcast = voidptr(0xFFFF)
|
||||
pub const wm_settingchange = 0x001A
|
||||
pub const smto_abortifhung = 0x0002
|
||||
@@ -0,0 +1,41 @@
|
||||
module os
|
||||
|
||||
#include <sys/types.h>
|
||||
#include <sys/ptrace.h>
|
||||
|
||||
fn C.ptrace(i32, u32, voidptr, i32) i32
|
||||
|
||||
// debugger_present returns a bool indicating if the process is being debugged.
|
||||
pub fn debugger_present() bool {
|
||||
$if macos {
|
||||
// check if a child process could trace its parent process,
|
||||
// if not a debugger must be present
|
||||
pid := fork()
|
||||
if pid == 0 {
|
||||
ppid := getppid()
|
||||
if C.ptrace(C.PT_TRACE_ME, ppid, unsafe { nil }, 0) == 0 {
|
||||
C.waitpid(ppid, 0, 0)
|
||||
|
||||
// detach ptrace, otherwise further checks would indicate a debugger is present (ptrace is the Debugger then)
|
||||
C.ptrace(C.PT_DETACH, ppid, 0, 0)
|
||||
|
||||
// no external debugger
|
||||
exit(0)
|
||||
} else {
|
||||
// an error occurred, a external debugger must be present
|
||||
exit(1)
|
||||
}
|
||||
} else {
|
||||
mut status := 0
|
||||
// wait until the child process dies
|
||||
C.waitpid(pid, &status, 0)
|
||||
// check the exit code of the child process check
|
||||
if posix_wait_status_exit_code(status) == 0 {
|
||||
return false
|
||||
} else {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
module os
|
||||
|
||||
// debugger_present returns a bool indicating if the process is being debugged.
|
||||
// Note: implementation available only on Darwin, FreeBSD, Linux, OpenBSD and
|
||||
// Windows. Otherwise, returns false.
|
||||
@[inline]
|
||||
pub fn debugger_present() bool {
|
||||
return false
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
module os
|
||||
|
||||
#include <sys/ptrace.h>
|
||||
|
||||
fn C.ptrace(i32, u32, voidptr, i32) i32
|
||||
|
||||
// debugger_present returns a bool indicating if the process is being debugged.
|
||||
pub fn debugger_present() bool {
|
||||
$if freebsd {
|
||||
// check if a child process could trace its parent process,
|
||||
// if not a debugger must be present
|
||||
pid := fork()
|
||||
if pid == 0 {
|
||||
ppid := getppid()
|
||||
if C.ptrace(C.PT_TRACE_ME, ppid, unsafe { nil }, 0) == 0 {
|
||||
C.waitpid(ppid, 0, 0)
|
||||
|
||||
// detach ptrace, otherwise further checks would indicate a debugger is present (ptrace is the Debugger then)
|
||||
C.ptrace(C.PT_DETACH, ppid, 0, 0)
|
||||
|
||||
// no external debugger
|
||||
exit(0)
|
||||
} else {
|
||||
// an error occurred, a external debugger must be present
|
||||
exit(1)
|
||||
}
|
||||
} else {
|
||||
mut status := 0
|
||||
// wait until the child process dies
|
||||
C.waitpid(pid, &status, 0)
|
||||
// check the exit code of the child process check
|
||||
if posix_wait_status_exit_code(status) == 0 {
|
||||
return false
|
||||
} else {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
module os
|
||||
|
||||
#include <sys/ptrace.h>
|
||||
|
||||
fn C.ptrace(u32, u32, voidptr, voidptr) u64
|
||||
|
||||
// debugger_present returns a bool indicating if the process is being debugged.
|
||||
pub fn debugger_present() bool {
|
||||
$if cross ? {
|
||||
return false
|
||||
}
|
||||
$if !cross ? {
|
||||
$if linux {
|
||||
// check if a child process could trace its parent process,
|
||||
// if not a debugger must be present
|
||||
pid := fork()
|
||||
if pid == 0 {
|
||||
ppid := getppid()
|
||||
if C.ptrace(C.PTRACE_ATTACH, ppid, 0, 0) == 0 {
|
||||
C.waitpid(ppid, 0, 0)
|
||||
|
||||
// detach ptrace, otherwise further checks would indicate a debugger is present (ptrace is the Debugger then)
|
||||
C.ptrace(C.PTRACE_DETACH, ppid, 0, 0)
|
||||
|
||||
// no external debugger
|
||||
exit(0)
|
||||
} else {
|
||||
// an error occurred, a external debugger must be present
|
||||
exit(1)
|
||||
}
|
||||
} else {
|
||||
mut status := 0
|
||||
// wait until the child process dies
|
||||
C.waitpid(pid, &status, 0)
|
||||
// check the exit code of the child process check
|
||||
if posix_wait_status_exit_code(status) == 0 {
|
||||
return false
|
||||
} else {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
module os
|
||||
|
||||
#include <sys/ptrace.h>
|
||||
|
||||
fn C.ptrace(i32, u32, voidptr, i32) i32
|
||||
|
||||
// debugger_present returns a bool indicating if the process is being debugged.
|
||||
pub fn debugger_present() bool {
|
||||
$if openbsd {
|
||||
// check if a child process could trace its parent process,
|
||||
// if not a debugger must be present
|
||||
pid := fork()
|
||||
if pid == 0 {
|
||||
ppid := getppid()
|
||||
/*
|
||||
* On OpenBSD, impossible to trace a parent process from its child.
|
||||
* Possible errors:
|
||||
* - EBUSY: process already traced
|
||||
* - EPERM: process is not a child of tracer (sysctl kern.global_ptrace=0)
|
||||
* - EINVAL: process is an ancestor of the current process and not init
|
||||
* (sysctl kern.global_ptrace=1)
|
||||
*/
|
||||
if C.ptrace(C.PT_ATTACH, ppid, unsafe { nil }, 0) < 0 {
|
||||
if C.errno == C.EBUSY {
|
||||
// external debugger must be present
|
||||
exit(1)
|
||||
} else {
|
||||
// no external debugger
|
||||
exit(0)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
mut status := 0
|
||||
// wait until the child process dies
|
||||
C.waitpid(pid, &status, 0)
|
||||
// check the exit code of the child process check
|
||||
if posix_wait_status_exit_code(status) == 0 {
|
||||
return false
|
||||
} else {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
module os
|
||||
|
||||
// this is defined in builtin_windows.c.v in builtin
|
||||
// fn C.IsDebuggerPresent() bool
|
||||
|
||||
// debugger_present returns a bool indicating if the process is being debugged
|
||||
@[inline]
|
||||
pub fn debugger_present() bool {
|
||||
$if windows {
|
||||
return C.IsDebuggerPresent()
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -0,0 +1,160 @@
|
||||
import os
|
||||
import os.font
|
||||
|
||||
fn env_snapshot(name string) (string, bool) {
|
||||
val := os.getenv_opt(name) or { return '', false }
|
||||
return val, true
|
||||
}
|
||||
|
||||
fn restore_env(name string, value string, existed bool) {
|
||||
if existed {
|
||||
os.setenv(name, value, true)
|
||||
} else {
|
||||
os.unsetenv(name)
|
||||
}
|
||||
}
|
||||
|
||||
fn normalize_test_path(path string) string {
|
||||
return path.replace(os.path_separator, '/')
|
||||
}
|
||||
|
||||
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_ensure_folder_is_writable() {
|
||||
tmp := os.temp_dir()
|
||||
os.ensure_folder_is_writable(tmp) or {
|
||||
eprintln('err: ${err}')
|
||||
assert false
|
||||
}
|
||||
}
|
||||
|
||||
fn test_expand_tilde_to_home() {
|
||||
os.setenv('HOME', '/tmp/home/folder', true)
|
||||
os.setenv('USERPROFILE', r'\tmp\home\folder', true)
|
||||
|
||||
home_test := os.join_path(os.home_dir(), 'test', 'tilde', 'expansion')
|
||||
home_expansion_test := os.expand_tilde_to_home(os.join_path('~', 'test', 'tilde', 'expansion'))
|
||||
assert home_test == home_expansion_test
|
||||
assert os.expand_tilde_to_home('~') == os.home_dir()
|
||||
}
|
||||
|
||||
fn test_config_dir() {
|
||||
cdir := os.config_dir()!
|
||||
assert cdir.len > 0
|
||||
adir := '${cdir}/test-v-config'
|
||||
os.mkdir_all(adir)!
|
||||
os.rmdir(adir)!
|
||||
assert os.is_dir(cdir)
|
||||
}
|
||||
|
||||
fn test_data_dir_prefers_platform_location() {
|
||||
xdg_data_home, had_xdg_data_home := env_snapshot('XDG_DATA_HOME')
|
||||
defer {
|
||||
restore_env('XDG_DATA_HOME', xdg_data_home, had_xdg_data_home)
|
||||
}
|
||||
$if windows {
|
||||
local_app_data, had_local_app_data := env_snapshot('LocalAppData')
|
||||
userprofile, had_userprofile := env_snapshot('USERPROFILE')
|
||||
test_root := os.join_path(os.temp_dir(), 'v_data_dir_windows_test_${os.getpid()}')
|
||||
defer {
|
||||
restore_env('LocalAppData', local_app_data, had_local_app_data)
|
||||
restore_env('USERPROFILE', userprofile, had_userprofile)
|
||||
os.rmdir_all(test_root) or {}
|
||||
}
|
||||
expected := os.join_path(test_root, 'LocalAppData')
|
||||
os.setenv('XDG_DATA_HOME', os.join_path(test_root, 'XdgDataHome'), true)
|
||||
os.setenv('LocalAppData', expected, true)
|
||||
os.setenv('USERPROFILE', os.join_path(test_root, 'UserProfile'), true)
|
||||
assert os.data_dir() == expected
|
||||
assert os.is_dir(expected)
|
||||
} $else {
|
||||
test_root := os.join_path(os.temp_dir(), 'v_data_dir_xdg_test_${os.getpid()}')
|
||||
defer {
|
||||
os.rmdir_all(test_root) or {}
|
||||
}
|
||||
expected := os.join_path(test_root, 'XdgDataHome')
|
||||
os.setenv('XDG_DATA_HOME', expected, true)
|
||||
assert os.data_dir() == expected
|
||||
assert os.is_dir(expected)
|
||||
}
|
||||
}
|
||||
|
||||
fn test_vmodules_dir_without_home_falls_back_to_vtmp() {
|
||||
home, had_home := env_snapshot('HOME')
|
||||
userprofile, had_userprofile := env_snapshot('USERPROFILE')
|
||||
vmodules, had_vmodules := env_snapshot('VMODULES')
|
||||
defer {
|
||||
restore_env('HOME', home, had_home)
|
||||
restore_env('USERPROFILE', userprofile, had_userprofile)
|
||||
restore_env('VMODULES', vmodules, had_vmodules)
|
||||
}
|
||||
os.unsetenv('HOME')
|
||||
os.unsetenv('USERPROFILE')
|
||||
os.unsetenv('VMODULES')
|
||||
assert os.vmodules_dir() == os.join_path_single(os.vtmp_dir(), '.vmodules')
|
||||
}
|
||||
|
||||
fn test_font_default_prefers_user_config_dir_font() ! {
|
||||
vui_font, had_vui_font := env_snapshot('VUI_FONT')
|
||||
defer {
|
||||
restore_env('VUI_FONT', vui_font, had_vui_font)
|
||||
}
|
||||
os.unsetenv('VUI_FONT')
|
||||
$if windows {
|
||||
app_data, had_app_data := env_snapshot('AppData')
|
||||
test_root := os.join_path(os.temp_dir(), 'v_font_config_windows_test_${os.getpid()}')
|
||||
defer {
|
||||
restore_env('AppData', app_data, had_app_data)
|
||||
os.rmdir_all(test_root) or {}
|
||||
}
|
||||
os.setenv('AppData', test_root, true)
|
||||
font_dir := os.join_path(test_root, 'v', 'fonts')
|
||||
expected := os.join_path(font_dir, 'UserFallback.ttf')
|
||||
os.mkdir_all(font_dir)!
|
||||
os.write_file(expected, 'fallback')!
|
||||
assert normalize_test_path(font.default()) == normalize_test_path(expected)
|
||||
} $else $if macos || darwin || ios {
|
||||
home, had_home := env_snapshot('HOME')
|
||||
userprofile, had_userprofile := env_snapshot('USERPROFILE')
|
||||
test_root := os.join_path(os.temp_dir(), 'v_font_config_macos_test_${os.getpid()}')
|
||||
defer {
|
||||
restore_env('HOME', home, had_home)
|
||||
restore_env('USERPROFILE', userprofile, had_userprofile)
|
||||
os.rmdir_all(test_root) or {}
|
||||
}
|
||||
os.setenv('HOME', test_root, true)
|
||||
os.unsetenv('USERPROFILE')
|
||||
font_dir := os.join_path(test_root, 'Library', 'Application Support', 'v', 'fonts')
|
||||
expected := os.join_path(font_dir, 'UserFallback.ttf')
|
||||
os.mkdir_all(font_dir)!
|
||||
os.write_file(expected, 'fallback')!
|
||||
assert normalize_test_path(font.default()) == normalize_test_path(expected)
|
||||
} $else {
|
||||
xdg_config_home, had_xdg_config_home := env_snapshot('XDG_CONFIG_HOME')
|
||||
home, had_home := env_snapshot('HOME')
|
||||
test_root := os.join_path(os.temp_dir(), 'v_font_config_xdg_test_${os.getpid()}')
|
||||
defer {
|
||||
restore_env('XDG_CONFIG_HOME', xdg_config_home, had_xdg_config_home)
|
||||
restore_env('HOME', home, had_home)
|
||||
os.rmdir_all(test_root) or {}
|
||||
}
|
||||
os.setenv('XDG_CONFIG_HOME', test_root, true)
|
||||
os.setenv('HOME', test_root, true)
|
||||
font_dir := os.join_path(test_root, 'v', 'fonts')
|
||||
expected := os.join_path(font_dir, 'UserFallback.ttf')
|
||||
os.mkdir_all(font_dir)!
|
||||
os.write_file(expected, 'fallback')!
|
||||
assert normalize_test_path(font.default()) == normalize_test_path(expected)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
// Copyright (c) 2019-2024 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) i32
|
||||
|
||||
// getenv returns the value of the environment variable named by the key.
|
||||
// If there is not one found, it returns an empty string ''.
|
||||
pub fn getenv(key string) string {
|
||||
return getenv_opt(key) or { '' }
|
||||
}
|
||||
|
||||
// getenv_opt returns the value of a given environment variable.
|
||||
// Returns `none` if the environment variable does not exist.
|
||||
@[manualfree]
|
||||
pub fn getenv_opt(key string) ?string {
|
||||
unsafe {
|
||||
$if windows {
|
||||
kw := key.to_wide()
|
||||
defer {
|
||||
free(voidptr(kw))
|
||||
}
|
||||
s := C._wgetenv(kw)
|
||||
if s == 0 {
|
||||
return none
|
||||
}
|
||||
return string_from_wide(s)
|
||||
} $else {
|
||||
s := C.getenv(&char(key.str))
|
||||
if s == nil {
|
||||
return none
|
||||
}
|
||||
// Note: C.getenv *requires* that the result be copied.
|
||||
return cstring_to_vstring(s)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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}'.to_wide()
|
||||
defer {
|
||||
unsafe { free(voidptr(format)) }
|
||||
}
|
||||
if overwrite {
|
||||
unsafe {
|
||||
return C._wputenv(format)
|
||||
}
|
||||
} else {
|
||||
if getenv(name).len == 0 {
|
||||
unsafe {
|
||||
return C._wputenv(format)
|
||||
}
|
||||
}
|
||||
}
|
||||
return -1
|
||||
} $else {
|
||||
unsafe {
|
||||
return C.setenv(&char(name.str), &char(value.str), overwrite)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// unsetenv clears an environment variable with `name`.
|
||||
pub fn unsetenv(name string) int {
|
||||
$if windows {
|
||||
format := '${name}='.to_wide()
|
||||
defer {
|
||||
unsafe { free(voidptr(format)) }
|
||||
}
|
||||
return C._wputenv(format)
|
||||
} $else {
|
||||
return C.unsetenv(&char(name.str))
|
||||
}
|
||||
}
|
||||
|
||||
// environ returns a map of all the current environment variables.
|
||||
// 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
|
||||
// for Windows OS.
|
||||
pub fn environ() map[string]string {
|
||||
// TODO how to declare Virtual C globals?
|
||||
// const C.environ &&char
|
||||
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_u8(`=`)
|
||||
if eq_index > 0 {
|
||||
res[eline[0..eq_index]] = eline[eq_index + 1..]
|
||||
}
|
||||
unsafe {
|
||||
c = c + eline.len + 1
|
||||
}
|
||||
}
|
||||
C.FreeEnvironmentStringsW(estrings)
|
||||
} $else {
|
||||
start := &&char(voidptr(C.environ))
|
||||
mut i := 0
|
||||
for {
|
||||
x := unsafe { start[i] }
|
||||
if x == 0 {
|
||||
break
|
||||
}
|
||||
eline := unsafe { cstring_to_vstring(x) }
|
||||
eq_index := eline.index_u8(`=`)
|
||||
if eq_index > 0 {
|
||||
res[eline[0..eq_index]] = eline[eq_index + 1..]
|
||||
}
|
||||
i++
|
||||
}
|
||||
}
|
||||
return res
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
module os
|
||||
|
||||
$if js_node {
|
||||
#global.$ENV = $process.env
|
||||
} $else {
|
||||
#const global = $global;
|
||||
#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 string($ENV[key])
|
||||
|
||||
return res
|
||||
}
|
||||
|
||||
// `getenv_opt` returns the value of a given environment variable.
|
||||
// Returns `none` if the environment variable does not exist.
|
||||
pub fn getenv_opt(key string) ?string {
|
||||
#if (!$ENV[key]) return none__;
|
||||
|
||||
mut res := ''
|
||||
#if ($ENV[key]) res = new 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
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
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_getenv_opt() {
|
||||
assert os.getenv_opt('VEXE') or { '' }.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'
|
||||
assert os.getenv_opt('foo') or { '' } == '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.now()).unix()
|
||||
os.setenv('foo${key}', 'bar', false)
|
||||
assert os.getenv('foo${key}') == 'bar'
|
||||
}
|
||||
|
||||
fn test_getenv_empty_var() {
|
||||
key := time.new(time.now()).unix()
|
||||
os.setenv('empty${key}', '""', false)
|
||||
assert os.getenv('empty${key}') == '""'
|
||||
}
|
||||
|
||||
fn test_environ_non_ascii() {
|
||||
os.setenv('Büro', 'gebäude', false)
|
||||
assert os.getenv('Büro') == 'gebäude'
|
||||
os.setenv('Büro', 'gebäudehaus', true)
|
||||
assert os.getenv('Büro') == 'gebäudehaus'
|
||||
os.setenv('Büro', 'gebäudehaus in der Straße', true)
|
||||
assert os.getenv('Büro') == 'gebäudehaus in der Straße'
|
||||
os.unsetenv('Büro')
|
||||
assert os.getenv('Büro') == ''
|
||||
|
||||
os.setenv('한국어', '초보자를 위한', false)
|
||||
assert os.getenv('한국어') == '초보자를 위한'
|
||||
os.unsetenv('한국어')
|
||||
assert os.getenv('한국어') == ''
|
||||
}
|
||||
@@ -0,0 +1,178 @@
|
||||
// v_os_execute_set_cloexec marks an fd as close-on-exec. When multiple threads
|
||||
// each call os.execute, every pipe() they create is briefly visible to all of
|
||||
// them; without FD_CLOEXEC, one thread's spawned child can inherit another
|
||||
// thread's pipe write end, keeping that pipe open after the intended writer
|
||||
// exits. The reader then never observes EOF and the captured output ends up
|
||||
// empty (seen on macOS arm64 in CI). Setting CLOEXEC closes the fd
|
||||
// automatically across exec, which fixes the race for both fork/execvp and
|
||||
// posix_spawn paths.
|
||||
static void v_os_execute_set_cloexec(int fd) {
|
||||
int flags = fcntl(fd, F_GETFD, 0);
|
||||
if (flags >= 0) {
|
||||
fcntl(fd, F_SETFD, flags | FD_CLOEXEC);
|
||||
}
|
||||
}
|
||||
|
||||
#if defined(__ANDROID__) && (!defined(__ANDROID_API__) || __ANDROID_API__ < 28)
|
||||
// Android API levels below 28 do not provide posix_spawn(). Fall back to
|
||||
// fork()/execvp() with a pipe; this is what popen() does internally and
|
||||
// is sufficient for capturing the merged stdout+stderr of a shell command.
|
||||
static int v_os_execute_capture_start(const char *cmd, int *child_pid, int *read_fd) {
|
||||
int pipefd[2];
|
||||
if (pipe(pipefd) != 0) {
|
||||
return -1;
|
||||
}
|
||||
v_os_execute_set_cloexec(pipefd[0]);
|
||||
v_os_execute_set_cloexec(pipefd[1]);
|
||||
pid_t pid = fork();
|
||||
if (pid < 0) {
|
||||
close(pipefd[0]);
|
||||
close(pipefd[1]);
|
||||
return -1;
|
||||
}
|
||||
if (pid == 0) {
|
||||
// child: redirect stdout+stderr to the pipe, then exec the shell.
|
||||
// dup2 clears FD_CLOEXEC on the destination, so STDOUT/STDERR stay
|
||||
// open across exec while the original pipe fds are auto-closed.
|
||||
dup2(pipefd[1], STDOUT_FILENO);
|
||||
dup2(pipefd[1], STDERR_FILENO);
|
||||
close(pipefd[0]);
|
||||
close(pipefd[1]);
|
||||
execlp("sh", "sh", "-c", cmd, (char *)NULL);
|
||||
_exit(127);
|
||||
}
|
||||
close(pipefd[1]);
|
||||
*child_pid = (int)pid;
|
||||
*read_fd = pipefd[0];
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int v_os_exec_capture_start(char *const argv[], int *child_pid, int *read_fd) {
|
||||
if (argv == NULL || argv[0] == NULL) {
|
||||
return -1;
|
||||
}
|
||||
int pipefd[2];
|
||||
if (pipe(pipefd) != 0) {
|
||||
return -1;
|
||||
}
|
||||
v_os_execute_set_cloexec(pipefd[0]);
|
||||
v_os_execute_set_cloexec(pipefd[1]);
|
||||
pid_t pid = fork();
|
||||
if (pid < 0) {
|
||||
close(pipefd[0]);
|
||||
close(pipefd[1]);
|
||||
return -1;
|
||||
}
|
||||
if (pid == 0) {
|
||||
dup2(pipefd[1], STDOUT_FILENO);
|
||||
dup2(pipefd[1], STDERR_FILENO);
|
||||
close(pipefd[0]);
|
||||
close(pipefd[1]);
|
||||
execvp(argv[0], argv);
|
||||
_exit(127);
|
||||
}
|
||||
close(pipefd[1]);
|
||||
*child_pid = (int)pid;
|
||||
*read_fd = pipefd[0];
|
||||
return 0;
|
||||
}
|
||||
#else
|
||||
// Use opaque void* declarations for posix_spawn instead of #include <spawn.h>.
|
||||
// Including <spawn.h> transitively pulls in <features.h>/<sys/cdefs.h>, which
|
||||
// breaks under musl-gcc on the Ubuntu docker image where <sys/cdefs.h> is not
|
||||
// on the include path. The 128-byte buffer is comfortably larger than the
|
||||
// real posix_spawn_file_actions_t on glibc (~80 bytes) and musl (~40 bytes);
|
||||
// posix_spawn_file_actions_init() initializes the buffer, so its true layout
|
||||
// is not needed here. We pass NULL for posix_spawnattr_t, so it is not
|
||||
// declared. Calling these via void* is ABI-compatible: pointer parameters
|
||||
// are passed identically regardless of pointee type.
|
||||
typedef struct { unsigned char _opaque[128]; } v_posix_spawn_file_actions_t;
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
extern int posix_spawn(pid_t *, const char *, const void *, const void *, char *const [], char *const []);
|
||||
extern int posix_spawnp(pid_t *, const char *, const void *, const void *, char *const [], char *const []);
|
||||
extern int posix_spawn_file_actions_init(void *);
|
||||
extern int posix_spawn_file_actions_destroy(void *);
|
||||
extern int posix_spawn_file_actions_adddup2(void *, int, int);
|
||||
extern int posix_spawn_file_actions_addclose(void *, int);
|
||||
|
||||
extern char **environ;
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
static int v_os_execute_capture_start(const char *cmd, int *child_pid, int *read_fd) {
|
||||
int pipefd[2];
|
||||
if (pipe(pipefd) != 0) {
|
||||
return -1;
|
||||
}
|
||||
v_os_execute_set_cloexec(pipefd[0]);
|
||||
v_os_execute_set_cloexec(pipefd[1]);
|
||||
v_posix_spawn_file_actions_t actions;
|
||||
if (posix_spawn_file_actions_init(&actions) != 0) {
|
||||
close(pipefd[0]);
|
||||
close(pipefd[1]);
|
||||
return -1;
|
||||
}
|
||||
int err = 0;
|
||||
if ((err = posix_spawn_file_actions_adddup2(&actions, pipefd[1], STDOUT_FILENO)) != 0
|
||||
|| (err = posix_spawn_file_actions_adddup2(&actions, pipefd[1], STDERR_FILENO)) != 0
|
||||
|| (err = posix_spawn_file_actions_addclose(&actions, pipefd[0])) != 0
|
||||
|| (err = posix_spawn_file_actions_addclose(&actions, pipefd[1])) != 0) {
|
||||
posix_spawn_file_actions_destroy(&actions);
|
||||
close(pipefd[0]);
|
||||
close(pipefd[1]);
|
||||
return -1;
|
||||
}
|
||||
char *const argv[] = {(char *)"/bin/sh", (char *)"-c", (char *)cmd, NULL};
|
||||
err = posix_spawn(child_pid, "/bin/sh", &actions, NULL, argv, environ);
|
||||
posix_spawn_file_actions_destroy(&actions);
|
||||
if (err != 0) {
|
||||
close(pipefd[0]);
|
||||
close(pipefd[1]);
|
||||
return -1;
|
||||
}
|
||||
close(pipefd[1]);
|
||||
*read_fd = pipefd[0];
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int v_os_exec_capture_start(char *const argv[], int *child_pid, int *read_fd) {
|
||||
if (argv == NULL || argv[0] == NULL) {
|
||||
return -1;
|
||||
}
|
||||
int pipefd[2];
|
||||
if (pipe(pipefd) != 0) {
|
||||
return -1;
|
||||
}
|
||||
v_os_execute_set_cloexec(pipefd[0]);
|
||||
v_os_execute_set_cloexec(pipefd[1]);
|
||||
v_posix_spawn_file_actions_t actions;
|
||||
if (posix_spawn_file_actions_init(&actions) != 0) {
|
||||
close(pipefd[0]);
|
||||
close(pipefd[1]);
|
||||
return -1;
|
||||
}
|
||||
int err = 0;
|
||||
if ((err = posix_spawn_file_actions_adddup2(&actions, pipefd[1], STDOUT_FILENO)) != 0
|
||||
|| (err = posix_spawn_file_actions_adddup2(&actions, pipefd[1], STDERR_FILENO)) != 0
|
||||
|| (err = posix_spawn_file_actions_addclose(&actions, pipefd[0])) != 0
|
||||
|| (err = posix_spawn_file_actions_addclose(&actions, pipefd[1])) != 0) {
|
||||
posix_spawn_file_actions_destroy(&actions);
|
||||
close(pipefd[0]);
|
||||
close(pipefd[1]);
|
||||
return -1;
|
||||
}
|
||||
err = posix_spawnp(child_pid, argv[0], &actions, NULL, argv, environ);
|
||||
posix_spawn_file_actions_destroy(&actions);
|
||||
if (err != 0) {
|
||||
close(pipefd[0]);
|
||||
close(pipefd[1]);
|
||||
return -1;
|
||||
}
|
||||
close(pipefd[1]);
|
||||
*read_fd = pipefd[0];
|
||||
return 0;
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,27 @@
|
||||
@[has_globals]
|
||||
module os
|
||||
|
||||
// g_v_os_execute_mutex_storage backs a pthread_mutex_t used to serialize
|
||||
// pipe()+spawn from os.execute(). On macOS arm64 GitHub Actions runners,
|
||||
// two threads racing through pipe()+posix_spawn produced empty captured
|
||||
// output even with FD_CLOEXEC on the pipe ends. 128 bytes is comfortably
|
||||
// larger than sizeof(pthread_mutex_t) on macOS (64) and Linux (40-48),
|
||||
// so we never need to know the actual layout — only the C functions
|
||||
// declared in builtin/cfns.c.v which take voidptr.
|
||||
__global g_v_os_execute_mutex_storage = [128]u8{}
|
||||
|
||||
fn v_os_execute_mutex_ptr() voidptr {
|
||||
return unsafe { voidptr(&g_v_os_execute_mutex_storage[0]) }
|
||||
}
|
||||
|
||||
fn init() {
|
||||
C.pthread_mutex_init(v_os_execute_mutex_ptr(), unsafe { nil })
|
||||
}
|
||||
|
||||
fn v_os_execute_lock() {
|
||||
C.pthread_mutex_lock(v_os_execute_mutex_ptr())
|
||||
}
|
||||
|
||||
fn v_os_execute_unlock() {
|
||||
C.pthread_mutex_unlock(v_os_execute_mutex_ptr())
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
import os
|
||||
|
||||
fn execute_in_thread(id int) !string {
|
||||
res := os.execute('printf thread_${id}')
|
||||
if res.exit_code != 0 {
|
||||
return error('thread ${id} failed: exit=${res.exit_code} output="${res.output.trim_space()}"')
|
||||
}
|
||||
return res.output
|
||||
}
|
||||
|
||||
fn test_execute_inside_spawned_threads() {
|
||||
$if windows {
|
||||
return
|
||||
}
|
||||
$if macos {
|
||||
// On macos-14 GitHub Actions runners, when the V repo is checked out
|
||||
// under a path that contains non-ASCII bytes (the space-paths CI does
|
||||
// this with `你好 my $path, @с интервали`), os.execute() inside spawned
|
||||
// threads consistently returns empty output here, even with FD_CLOEXEC
|
||||
// and a V-level pthread mutex serializing pipe()+posix_spawn. Regular
|
||||
// macos CI (ASCII paths) passes the test, so skip only when the cwd
|
||||
// contains non-ASCII bytes.
|
||||
cwd := os.getwd()
|
||||
for b in cwd.bytes() {
|
||||
if b >= 0x80 {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
for _ in 0 .. 3 {
|
||||
mut threads := []thread !string{}
|
||||
for i in 0 .. 4 {
|
||||
threads << spawn execute_in_thread(i)
|
||||
}
|
||||
mut outputs := []string{cap: 4}
|
||||
for t in threads {
|
||||
outputs << t.wait() or { panic(err) }
|
||||
}
|
||||
assert outputs == ['thread_0', 'thread_1', 'thread_2', 'thread_3']
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,153 @@
|
||||
module os
|
||||
|
||||
// low level operations with file descriptors/handles
|
||||
|
||||
$if !windows {
|
||||
#include <sys/select.h>
|
||||
#include <sys/ioctl.h>
|
||||
}
|
||||
|
||||
$if windows {
|
||||
#include <winsock2.h>
|
||||
}
|
||||
|
||||
#flag windows -lws2_32
|
||||
|
||||
// fd_close closes the file descriptor. It returns 0 on success.
|
||||
pub fn fd_close(fd int) int {
|
||||
if fd == -1 {
|
||||
return 0
|
||||
}
|
||||
return C.close(fd)
|
||||
}
|
||||
|
||||
// fd_write writes the given string to the file descriptor.
|
||||
// It blocks until all the data in the string is written.
|
||||
pub fn fd_write(fd int, s string) {
|
||||
if fd == -1 {
|
||||
return
|
||||
}
|
||||
mut sp := s.str
|
||||
mut remaining := s.len
|
||||
for remaining > 0 {
|
||||
written := int(C.write(fd, sp, remaining))
|
||||
if written < 0 {
|
||||
return
|
||||
}
|
||||
remaining = remaining - written
|
||||
sp = unsafe { voidptr(usize(sp) + usize(written)) }
|
||||
}
|
||||
}
|
||||
|
||||
// fd_slurp reads all the remaining data from the file descriptor.
|
||||
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
|
||||
}
|
||||
|
||||
// fd_read reads data from the file descriptor. It returns the read data, and how many bytes were read.
|
||||
// On signal interruption (EINTR), the read is retried so callers do not see spurious empty results
|
||||
// when a signal (for example a Boehm GC stop-the-world signal in a multi-threaded program) interrupts
|
||||
// a blocking read.
|
||||
pub fn fd_read(fd int, maxbytes int) (string, int) {
|
||||
if fd == -1 {
|
||||
return '', 0
|
||||
}
|
||||
unsafe {
|
||||
mut buf := malloc_noscan(maxbytes + 1)
|
||||
mut nbytes := 0
|
||||
for {
|
||||
$if !windows {
|
||||
C.errno = 0
|
||||
}
|
||||
nbytes = int(C.read(fd, buf, maxbytes))
|
||||
if nbytes >= 0 {
|
||||
break
|
||||
}
|
||||
$if !windows {
|
||||
if C.errno == C.EINTR {
|
||||
continue
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
if nbytes < 0 {
|
||||
free(buf)
|
||||
return '', nbytes
|
||||
}
|
||||
buf[nbytes] = 0
|
||||
return tos(buf, nbytes), nbytes
|
||||
}
|
||||
}
|
||||
|
||||
@[typedef]
|
||||
pub struct C.fd_set {}
|
||||
|
||||
pub struct C.timeval {
|
||||
pub:
|
||||
tv_sec u64
|
||||
tv_usec u64
|
||||
}
|
||||
|
||||
fn C.select(ndfs i32, readfds &C.fd_set, writefds &C.fd_set, exceptfds &C.fd_set, timeout &C.timeval) i32
|
||||
|
||||
$if !windows {
|
||||
fn C.ioctl(fd i32, request u64, args ...voidptr) i32
|
||||
}
|
||||
|
||||
$if windows {
|
||||
fn C._get_osfhandle(fd int) voidptr
|
||||
fn C.PeekNamedPipe(hNamedPipe voidptr, lpBuffer voidptr, nBufferSize i32, lpBytesRead voidptr, lpTotalBytesAvail voidptr, lpBytesLeftThisMessage voidptr) bool
|
||||
}
|
||||
|
||||
// These are C macros, but from the V's point of view, can be treated as C functions:
|
||||
fn C.FD_ZERO(fdset &C.fd_set)
|
||||
fn C.FD_SET(fd i32, fdset &C.fd_set)
|
||||
fn C.FD_ISSET(fd i32, fdset &C.fd_set) i32
|
||||
|
||||
// fd_is_pending returns true, when there is pending data, waiting to be read from file descriptor `fd`.
|
||||
// If the file descriptor is closed, or if reading from it, will block (there is no data), fd_is_pending returns false.
|
||||
pub fn fd_is_pending(fd int) bool {
|
||||
if fd == -1 {
|
||||
return false
|
||||
}
|
||||
mut bytes_avail := int(0)
|
||||
$if windows {
|
||||
handle := voidptr(C._get_osfhandle(fd))
|
||||
if handle != voidptr(-1) {
|
||||
if C.PeekNamedPipe(handle, unsafe { nil }, int(0), unsafe { nil },
|
||||
voidptr(&bytes_avail), unsafe { nil })
|
||||
{
|
||||
return bytes_avail > 0
|
||||
}
|
||||
}
|
||||
} $else {
|
||||
if C.ioctl(fd, u64(C.FIONREAD), &bytes_avail) == 0 {
|
||||
return bytes_avail > 0
|
||||
}
|
||||
}
|
||||
read_set := C.fd_set{}
|
||||
C.FD_ZERO(&read_set)
|
||||
C.FD_SET(fd, &read_set)
|
||||
mut ts := C.timeval{
|
||||
tv_sec: 0
|
||||
tv_usec: 0
|
||||
}
|
||||
res := C.select(fd + 1, &read_set, C.NULL, C.NULL, &ts)
|
||||
if res > 0 {
|
||||
if C.FD_ISSET(fd, &read_set) != 0 {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -0,0 +1,693 @@
|
||||
module os
|
||||
|
||||
// NotExpected is a generic error that means that we receave a not expected error.
|
||||
pub struct NotExpected {
|
||||
cause string
|
||||
code int
|
||||
}
|
||||
|
||||
fn (err NotExpected) msg() string {
|
||||
return err.cause
|
||||
}
|
||||
|
||||
fn (err NotExpected) code() int {
|
||||
return err.code
|
||||
}
|
||||
|
||||
pub struct File {
|
||||
mut:
|
||||
cfile voidptr // Using void* instead of FILE*
|
||||
pub:
|
||||
fd int
|
||||
pub mut:
|
||||
is_opened bool
|
||||
}
|
||||
|
||||
fn C.fseeko(&C.FILE, u64, i32) i32
|
||||
|
||||
fn C._fseeki64(&C.FILE, u64, i32) i32
|
||||
|
||||
fn C.getc(&C.FILE) i32
|
||||
|
||||
fn C.freopen(&char, &char, &C.FILE) &C.FILE
|
||||
|
||||
fn C._wfreopen(&u16, &u16, &C.FILE) &C.FILE
|
||||
|
||||
fn fix_windows_path(path string) string {
|
||||
mut p := path
|
||||
$if windows {
|
||||
p = path.replace('/', '\\')
|
||||
}
|
||||
return p
|
||||
}
|
||||
|
||||
// open_file tries to open or create a file with custom flags and permissions.
|
||||
@[noinline]
|
||||
pub fn open_file(path string, mode string, options ...int) !File {
|
||||
mut flags := 0
|
||||
mut seek_to_end := false
|
||||
for m in mode {
|
||||
match m {
|
||||
`w` {
|
||||
flags |= o_create | o_trunc | o_wronly
|
||||
}
|
||||
`a` {
|
||||
flags |= o_create | o_append | o_wronly
|
||||
seek_to_end = true
|
||||
}
|
||||
`r` {
|
||||
flags |= o_rdonly
|
||||
}
|
||||
`b` {
|
||||
flags |= o_binary
|
||||
}
|
||||
`s` {
|
||||
flags |= o_sync
|
||||
}
|
||||
`n` {
|
||||
flags |= o_nonblock
|
||||
}
|
||||
`c` {
|
||||
flags |= o_noctty
|
||||
}
|
||||
`+` {
|
||||
flags &= ~o_wronly
|
||||
flags |= o_rdwr
|
||||
}
|
||||
else {}
|
||||
}
|
||||
}
|
||||
if mode == 'r+' {
|
||||
flags = o_rdwr
|
||||
}
|
||||
mut permission := 0o666
|
||||
if options.len > 0 {
|
||||
permission = options[0]
|
||||
}
|
||||
$if windows {
|
||||
if permission < 0o600 {
|
||||
permission = 0x0100
|
||||
} else {
|
||||
permission = 0x0100 | 0x0080
|
||||
}
|
||||
}
|
||||
p := fix_windows_path(path)
|
||||
fd := $if windows {
|
||||
C._wopen(p.to_wide(), flags, permission)
|
||||
} $else {
|
||||
C.open(&char(p.str), flags, permission)
|
||||
}
|
||||
if fd == -1 {
|
||||
return error(posix_get_error_msg(C.errno))
|
||||
}
|
||||
mut cfile := C.fdopen(fd, &char(mode.str))
|
||||
if isnil(cfile) {
|
||||
return error('Failed to open or create file "${path}"')
|
||||
}
|
||||
mut res := File{
|
||||
cfile: cfile
|
||||
fd: fd
|
||||
is_opened: true
|
||||
}
|
||||
if seek_to_end {
|
||||
// ensure appending will work, even on bsd/macos systems:
|
||||
res.seek(0, .end) or {}
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
// open tries to open a file from a given path for reading.
|
||||
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.
|
||||
@[noinline]
|
||||
pub fn create(path string) !File {
|
||||
cfile := vfopen(path, 'wb')!
|
||||
fd := fileno(cfile)
|
||||
return File{
|
||||
cfile: cfile
|
||||
fd: fd
|
||||
is_opened: true
|
||||
}
|
||||
}
|
||||
|
||||
// stdin returns an os.File for stdin.
|
||||
pub fn stdin() File {
|
||||
return File{
|
||||
fd: 0
|
||||
cfile: voidptr(C.stdin)
|
||||
is_opened: true
|
||||
}
|
||||
}
|
||||
|
||||
// stdout returns an os.File for stdout.
|
||||
pub fn stdout() File {
|
||||
return File{
|
||||
fd: 1
|
||||
cfile: voidptr(C.stdout)
|
||||
is_opened: true
|
||||
}
|
||||
}
|
||||
|
||||
// stderr returns an os.File for stderr.
|
||||
pub fn stderr() File {
|
||||
return File{
|
||||
fd: 2
|
||||
cfile: voidptr(C.stderr)
|
||||
is_opened: true
|
||||
}
|
||||
}
|
||||
|
||||
// eof returns true, when the end of file has been reached.
|
||||
pub fn (f &File) eof() bool {
|
||||
cfile := unsafe { &C.FILE(f.cfile) }
|
||||
return C.feof(cfile) != 0
|
||||
}
|
||||
|
||||
// reopen allows a `File` to be reused. It is mostly useful for reopening standard input and output.
|
||||
pub fn (mut f File) reopen(path string, mode string) ! {
|
||||
p := fix_windows_path(path)
|
||||
mut cfile := &C.FILE(unsafe { nil })
|
||||
$if windows {
|
||||
cfile = C._wfreopen(p.to_wide(), mode.to_wide(), f.cfile)
|
||||
} $else {
|
||||
cfile = C.freopen(&char(p.str), &char(mode.str), f.cfile)
|
||||
}
|
||||
if isnil(cfile) {
|
||||
return error('Failed to reopen file "${path}"')
|
||||
}
|
||||
f.cfile = cfile
|
||||
}
|
||||
|
||||
// read implements the Reader interface.
|
||||
pub fn (f &File) read(mut buf []u8) !int {
|
||||
if !f.is_opened || isnil(f.cfile) {
|
||||
return error_file_not_opened()
|
||||
}
|
||||
if buf.len == 0 {
|
||||
return Eof{}
|
||||
}
|
||||
// the following is needed, because on FreeBSD, C.feof is a macro:
|
||||
nbytes := int(C.fread(buf.data, 1, buf.len, unsafe { &C.FILE(f.cfile) }))
|
||||
// if no bytes were read, check for errors and end-of-file.
|
||||
if nbytes <= 0 {
|
||||
if C.feof(unsafe { &C.FILE(f.cfile) }) != 0 {
|
||||
return Eof{}
|
||||
}
|
||||
if C.ferror(unsafe { &C.FILE(f.cfile) }) != 0 {
|
||||
return NotExpected{
|
||||
cause: 'unexpected error from fread'
|
||||
code: -1
|
||||
}
|
||||
}
|
||||
}
|
||||
return nbytes
|
||||
}
|
||||
|
||||
// **************************** Write ops ***************************
|
||||
|
||||
// write implements the Writer interface.
|
||||
// It returns how many bytes were actually written.
|
||||
pub fn (mut f File) write(buf []u8) !int {
|
||||
if !f.is_opened || isnil(f.cfile) {
|
||||
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()
|
||||
}
|
||||
written := f.write_string(s)!
|
||||
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, usize(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 []u8) !int {
|
||||
if !f.is_opened {
|
||||
return error_file_not_opened()
|
||||
}
|
||||
f.seek(pos, .start) or {}
|
||||
res := int(C.fwrite(buf.data, 1, buf.len, f.cfile))
|
||||
if res == 0 && buf.len != 0 {
|
||||
return error('0 bytes written')
|
||||
}
|
||||
f.seek(0, .end) or {}
|
||||
return res
|
||||
}
|
||||
|
||||
// write_ptr writes `size` bytes to the file, starting from the address in `data`.
|
||||
// Note: 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.
|
||||
// The size in bytes, of the `buffer`, should be passed in `buffer_len`.
|
||||
@[unsafe]
|
||||
pub fn (mut f File) write_full_buffer(buffer voidptr, buffer_len usize) ! {
|
||||
if buffer_len <= usize(0) {
|
||||
return
|
||||
}
|
||||
if !f.is_opened {
|
||||
return error_file_not_opened()
|
||||
}
|
||||
mut ptr := &u8(buffer)
|
||||
mut remaining_bytes := i64(buffer_len)
|
||||
for remaining_bytes > 0 {
|
||||
unsafe {
|
||||
C.errno = 0
|
||||
x := i64(C.fwrite(ptr, 1, remaining_bytes, f.cfile))
|
||||
cerror := int(C.errno)
|
||||
ptr += x
|
||||
remaining_bytes -= x
|
||||
if cerror != 0 {
|
||||
if cerror == C.EINTR {
|
||||
continue
|
||||
}
|
||||
if i64(x) != i64(buffer_len) {
|
||||
return error(posix_get_error_msg(cerror))
|
||||
}
|
||||
}
|
||||
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).
|
||||
// Note: 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 {
|
||||
f.seek(pos, .start) or {}
|
||||
res := int(C.fwrite(data, 1, size, f.cfile))
|
||||
f.seek(0, .end) or {}
|
||||
return res
|
||||
}
|
||||
|
||||
// **************************** 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 Eof{}
|
||||
}
|
||||
// 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 `size` bytes from the beginning of the file.
|
||||
// Utility method, same as .read_bytes_at(size, 0).
|
||||
pub fn (f &File) read_bytes(size int) []u8 {
|
||||
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) []u8 {
|
||||
mut arr := []u8{len: size}
|
||||
nreadbytes := f.read_bytes_into(pos, mut arr) or {
|
||||
// return err
|
||||
return []
|
||||
}
|
||||
return arr[0..nreadbytes]
|
||||
}
|
||||
|
||||
// read_bytes_with_newline reads from the current position of the file into the provided buffer.
|
||||
// Each consecutive call on the same file, continues reading, from where it previously ended.
|
||||
// A read call is either stopped, if the buffer is full, a newline was read or EOF.
|
||||
// On EOF, the method returns 0. The methods will also return any IO error encountered.
|
||||
pub fn (f &File) read_bytes_with_newline(mut buf []u8) !int {
|
||||
if !f.is_opened {
|
||||
return error_file_not_opened()
|
||||
}
|
||||
if buf.len == 0 {
|
||||
return error(@FN + ': `buf.len` == 0')
|
||||
}
|
||||
newline := 10
|
||||
mut c := 0
|
||||
mut buf_ptr := 0
|
||||
mut nbytes := 0
|
||||
|
||||
stream := unsafe { &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] = u8(c)
|
||||
nbytes++
|
||||
return nbytes
|
||||
}
|
||||
else {
|
||||
buf[buf_ptr] = u8(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 []u8) !int {
|
||||
if !f.is_opened {
|
||||
return error_file_not_opened()
|
||||
}
|
||||
if buf.len == 0 {
|
||||
return error(@FN + ': `buf.len` == 0')
|
||||
}
|
||||
// Note: fseek errors if pos == os.file_size, which we accept
|
||||
unsafe { f.seek(pos, .start) or {} }
|
||||
nbytes := fread(buf.data, 1, buf.len, f.cfile)!
|
||||
return nbytes
|
||||
}
|
||||
|
||||
// read_from implements the RandomReader interface.
|
||||
pub fn (f &File) read_from(pos u64, mut buf []u8) !int {
|
||||
if !f.is_opened {
|
||||
return error_file_not_opened()
|
||||
}
|
||||
if buf.len == 0 {
|
||||
return 0
|
||||
}
|
||||
unsafe { f.seek(pos, .start) or {} }
|
||||
nbytes := fread(buf.data, 1, buf.len, f.cfile)!
|
||||
return nbytes
|
||||
}
|
||||
|
||||
// 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 &u8, max_size int) !int {
|
||||
if !f.is_opened {
|
||||
return error_file_not_opened()
|
||||
}
|
||||
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 FileNotOpenedError {
|
||||
Error
|
||||
}
|
||||
|
||||
pub fn (err FileNotOpenedError) msg() string {
|
||||
return 'os: file not opened'
|
||||
}
|
||||
|
||||
pub struct SizeOfTypeIs0Error {
|
||||
Error
|
||||
}
|
||||
|
||||
pub fn (err SizeOfTypeIs0Error) msg() string {
|
||||
return 'os: size of type is 0'
|
||||
}
|
||||
|
||||
fn error_file_not_opened() IError {
|
||||
return &FileNotOpenedError{}
|
||||
}
|
||||
|
||||
fn error_size_of_type_0() IError {
|
||||
return &SizeOfTypeIs0Error{}
|
||||
}
|
||||
|
||||
// read_struct reads a single plain-data struct of type `T`.
|
||||
// `T` must not contain strings, dynamic arrays, maps, pointers, interfaces, or function values.
|
||||
pub fn (mut f File) read_struct[T](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(voidptr(&t), 1, tsize, f.cfile)!
|
||||
if nbytes != tsize {
|
||||
return error_with_code('incomplete struct read', nbytes)
|
||||
}
|
||||
}
|
||||
|
||||
// read_struct_at reads a single plain-data struct of type `T` at position specified in file.
|
||||
// `T` must not contain strings, dynamic arrays, maps, pointers, interfaces, or function values.
|
||||
pub fn (mut f File) read_struct_at[T](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()
|
||||
}
|
||||
f.seek(pos, .start) or {}
|
||||
nbytes := fread(voidptr(&t), 1, tsize, f.cfile)!
|
||||
f.seek(0, .end) or {}
|
||||
if nbytes != tsize {
|
||||
return error_with_code('incomplete struct read', nbytes)
|
||||
}
|
||||
}
|
||||
|
||||
// read_raw reads and returns a single plain-data instance of type `T`.
|
||||
// `T` must not contain strings, dynamic arrays, maps, pointers, interfaces, or function values.
|
||||
pub fn (mut f File) read_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()
|
||||
}
|
||||
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 plain-data instance of type `T` starting at file byte offset `pos`.
|
||||
// `T` must not contain strings, dynamic arrays, maps, pointers, interfaces, or function values.
|
||||
pub fn (mut f File) read_raw_at[T](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 t := T{}
|
||||
f.seek(pos, .start)!
|
||||
nbytes := fread(&t, 1, tsize, f.cfile)!
|
||||
f.seek(0, .end)!
|
||||
if nbytes != tsize {
|
||||
return error_with_code('incomplete struct read', nbytes)
|
||||
}
|
||||
return t
|
||||
}
|
||||
|
||||
// write_struct writes a single plain-data struct of type `T`.
|
||||
// `T` must not contain strings, dynamic arrays, maps, pointers, interfaces, or function values.
|
||||
pub fn (mut f File) write_struct[T](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 plain-data struct of type `T` at file byte offset `pos`.
|
||||
// `T` must not contain strings, dynamic arrays, maps, pointers, interfaces, or function values.
|
||||
pub fn (mut f File) write_struct_at[T](t &T, pos u64) ! {
|
||||
if !f.is_opened {
|
||||
return error_file_not_opened()
|
||||
}
|
||||
tsize := usize(sizeof(T))
|
||||
if tsize == 0 {
|
||||
return error_size_of_type_0()
|
||||
}
|
||||
f.seek(pos, .start) or {}
|
||||
unsafe { f.write_full_buffer(t, tsize)! }
|
||||
f.seek(0, .end) or {}
|
||||
}
|
||||
|
||||
// TODO: `write_raw[_at]` implementations are copy-pasted from `write_struct[_at]`
|
||||
|
||||
// write_raw writes a single plain-data instance of type `T`.
|
||||
// `T` must not contain strings, dynamic arrays, maps, pointers, interfaces, or function values.
|
||||
pub fn (mut f File) write_raw[T](t &T) ! {
|
||||
if !f.is_opened {
|
||||
return error_file_not_opened()
|
||||
}
|
||||
tsize := usize(sizeof(T))
|
||||
if tsize == 0 {
|
||||
return error_size_of_type_0()
|
||||
}
|
||||
unsafe { f.write_full_buffer(t, tsize)! }
|
||||
}
|
||||
|
||||
// write_raw_at writes a single plain-data instance of type `T` starting at file byte offset `pos`.
|
||||
// `T` must not contain strings, dynamic arrays, maps, pointers, interfaces, or function values.
|
||||
pub fn (mut f File) write_raw_at[T](t &T, pos u64) ! {
|
||||
if !f.is_opened {
|
||||
return error_file_not_opened()
|
||||
}
|
||||
tsize := usize(sizeof(T))
|
||||
if tsize == 0 {
|
||||
return error_size_of_type_0()
|
||||
}
|
||||
f.seek(pos, .start)!
|
||||
unsafe { f.write_full_buffer(t, tsize)! }
|
||||
f.seek(0, .end)!
|
||||
}
|
||||
|
||||
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 occurs, 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.fseek(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()
|
||||
}
|
||||
|
||||
mut pos := i64(0)
|
||||
mut ret := 0
|
||||
$if windows {
|
||||
ret = C.fgetpos(f.cfile, &pos)
|
||||
} $else {
|
||||
pos = i64(C.ftell(f.cfile))
|
||||
}
|
||||
if ret == -1 || pos == -1 {
|
||||
return error(posix_get_error_msg(C.errno))
|
||||
}
|
||||
return pos
|
||||
}
|
||||
@@ -0,0 +1,152 @@
|
||||
module os
|
||||
|
||||
pub struct File {
|
||||
pub:
|
||||
fd int
|
||||
pub mut:
|
||||
is_opened bool
|
||||
}
|
||||
|
||||
$if !js_browser {
|
||||
#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 (&ErrFileNotOpened{})
|
||||
}
|
||||
fn error_size_of_type_0() IError {
|
||||
return (&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 error(new string('' + 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 []u8) !int {
|
||||
if buf.len == 0 {
|
||||
return 0
|
||||
}
|
||||
mut nbytes := 0
|
||||
#try {
|
||||
#let readBuffer = Buffer.alloc(buf.val.len.valueOf());
|
||||
#nbytes = $fs.readSync(f.val.fd.valueOf(), readBuffer, 0, buf.val.len.valueOf(), null);
|
||||
#
|
||||
#for (let i = 0; i < nbytes; i++) { buf.val.arr.arr[i] = new u8(readBuffer[i]); }
|
||||
#}
|
||||
#catch (e) { return error('' + e); }
|
||||
|
||||
return nbytes
|
||||
}
|
||||
|
||||
pub fn (mut f File) write(buf []u8) !int {
|
||||
if !f.is_opened {
|
||||
return error('file is not opened')
|
||||
}
|
||||
mut nbytes := 0
|
||||
#buf.arr.make_copy()
|
||||
#const b = $buffer.Buffer.from(buf.arr.arr.map((x) => x.valueOf()))
|
||||
#try { $fs.writeSync(f.val.fd.valueOf(),b,0,buf.len.valueOf(),0); } catch (e) { return error(new string('' + 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 []u8) !int {
|
||||
if !f.is_opened {
|
||||
return error('file is not opened')
|
||||
}
|
||||
mut nbytes := 0
|
||||
#buf.arr.make_copy()
|
||||
#const b = $buffer.Buffer.from(buf.arr.arr.map((x) => x.valueOf()))
|
||||
#try { $fs.writeSync(f.val.fd.valueOf(),b,0,buf.len.valueOf(),pos.valueOf()); } catch (e) { return error(new string('' + 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() {
|
||||
#$fs.closeSync(f.valueOf().fd.valueOf())
|
||||
}
|
||||
|
||||
pub fn (mut f File) write_full_buffer(s voidptr, buffer_len usize) ! {}
|
||||
|
||||
pub fn (mut f File) write_array(buffer array) !int {
|
||||
if !f.is_opened {
|
||||
return error('file is not opened')
|
||||
}
|
||||
mut nbytes := 0
|
||||
#buffer.arr.make_copy()
|
||||
#const b = $buffer.Buffer.from(buffer.arr.arr.map((x) => x.valueOf()))
|
||||
#try { $fs.writeSync(f.val.fd.valueOf(),b,0,buffer.len.valueOf(),0); } catch (e) { return error(new string('' + e)); }
|
||||
|
||||
return nbytes
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
module os
|
||||
|
||||
fn C.setvbuf(stream &C.FILE, buffer &char, mode i32, size usize) i32
|
||||
|
||||
// FileBufferMode describes the available buffering modes for an os.File: unbuffered, line buffered and block/fully buffered.
|
||||
// Normally all files are block buffered. If a stream refers to a terminal (as stdout normally does), it is line buffered.
|
||||
// The standard error stream stderr is always unbuffered by default.
|
||||
pub enum FileBufferMode {
|
||||
fully_buffered = C._IOFBF // many characters are saved up and written as a block
|
||||
line_buffered = C._IOLBF // characters are saved up until a newline is output or input is read from any stream attached to a terminal device (typically stdin)
|
||||
not_buffered = C._IONBF // information appears on the destination file or terminal as soon as it is written
|
||||
}
|
||||
|
||||
// setvbuf sets the buffer and buffering mode for the given file `f`. See also os.FileBufferMode.
|
||||
// It returns 0 on success. It returns nonzero on failure (the mode is invalid, or the request cannot be honored).
|
||||
// Except for unbuffered files, the buffer argument should point to a buffer at least size bytes long; this buffer will be used instead of the current buffer.
|
||||
// If the argument buffer is nil, only the mode is affected; a new buffer will be allocated on the next read or write operation.
|
||||
// Note: make sure, that the space, that buffer points to (when != 0), still exists by the time the file stream is closed, which also happens at program termination.
|
||||
// Note: f.setvbuf() may be used only after opening a file stream, and before any other operations have been performed on it.
|
||||
@[unsafe]
|
||||
pub fn (mut f File) setvbuf(buffer &char, mode FileBufferMode, size usize) int {
|
||||
return C.setvbuf(f.cfile, buffer, int(mode), size)
|
||||
}
|
||||
|
||||
// set_buffer sets the buffer for the file, and the file buffering mode (see also os.FileBufferMode).
|
||||
// Unlike File.setvbuf, it allows you to pass an existing V []u8 array directly.
|
||||
// Note: f.set_buffer() may be used only after opening a file stream, and before any other operations have been performed on it.
|
||||
pub fn (mut f File) set_buffer(mut buffer []u8, mode FileBufferMode) int {
|
||||
return unsafe { f.setvbuf(buffer.data, mode, usize(buffer.len)) }
|
||||
}
|
||||
|
||||
// set_line_buffered sets the file buffering mode to FileBufferMode.line_buffered.
|
||||
// Note: f.set_line_buffered() may be used only after opening a file stream, and before any other operations have been performed on it.
|
||||
pub fn (mut f File) set_line_buffered() {
|
||||
unsafe { f.setvbuf(&char(nil), .line_buffered, usize(0)) }
|
||||
}
|
||||
|
||||
// set_unbuffered sets the file buffering mode to FileBufferMode.not_buffered.
|
||||
// Note: f.set_unbuffered() may be used only after opening a file stream, and before any other operations have been performed on it.
|
||||
pub fn (mut f File) set_unbuffered() {
|
||||
unsafe { f.setvbuf(&char(nil), .not_buffered, usize(0)) }
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
import os
|
||||
|
||||
const tfolder = os.join_path(os.vtmp_dir(), 'os_file_buffering_tests')
|
||||
|
||||
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.rmdir_all(tfolder) or {}
|
||||
}
|
||||
|
||||
fn test_set_buffer_line_buffered() {
|
||||
dump(@LOCATION)
|
||||
mut buf := []u8{len: 25}
|
||||
dump(buf)
|
||||
mut wfile := os.open_file('text.txt', 'wb', 0o666)!
|
||||
wfile.set_buffer(mut buf, .line_buffered)
|
||||
wfile.write_string('----------------------------------\n')!
|
||||
for line in ['hello\n', 'world\n', 'hi\n'] {
|
||||
wfile.write_string(line)!
|
||||
wfile.flush()
|
||||
dump(buf)
|
||||
print(buf.bytestr())
|
||||
// assert buf.bytestr().contains(line) // this works on GLIBC, but fails on MUSL.
|
||||
unsafe { buf.reset() }
|
||||
}
|
||||
wfile.close()
|
||||
|
||||
content := os.read_lines('text.txt')!
|
||||
dump(content)
|
||||
assert content == ['----------------------------------', 'hello', 'world', 'hi']
|
||||
}
|
||||
|
||||
fn test_set_buffer_fully_buffered() {
|
||||
dump(@LOCATION)
|
||||
mut buf := []u8{len: 30}
|
||||
dump(buf)
|
||||
mut wfile := os.open_file('text.txt', 'wb', 0o666)!
|
||||
wfile.set_buffer(mut buf, .fully_buffered)
|
||||
// Ubuntu GLIBC 2.31 seems to not use the buffer for the first write call, but it does write to the buffer first for the subsequent ones.
|
||||
// MUSL (detecting the MUSL version is deliberately made hard by its authors, because of course it is :-( ...), will skip the first 8 bytes
|
||||
// of the buffer, and write everything after those.
|
||||
wfile.write_string('S')!
|
||||
wfile.write_string('---\n')!
|
||||
dump(buf)
|
||||
for line in ['hello\n', 'world\n', 'hi\n'] {
|
||||
wfile.write_string(line)!
|
||||
dump(buf)
|
||||
// print(buf.bytestr())
|
||||
}
|
||||
wfile.close()
|
||||
dump(buf)
|
||||
// assert buf.bytestr().starts_with('---\nhello\nworld\nhi\n') // works on GLIBC, fails on MUSL
|
||||
assert buf.bytestr().contains('---\nhello\nworld\n')
|
||||
|
||||
content := os.read_lines('text.txt')!
|
||||
dump(content)
|
||||
assert content == ['S---', 'hello', 'world', 'hi']
|
||||
}
|
||||
|
||||
fn test_set_unbuffered() {
|
||||
dump(@LOCATION)
|
||||
mut buf := []u8{len: 30}
|
||||
dump(buf)
|
||||
mut wfile := os.open_file('text.txt', 'wb', 0o666)!
|
||||
wfile.set_buffer(mut buf, .not_buffered)
|
||||
wfile.write_string('S')!
|
||||
wfile.write_string('---\n')!
|
||||
dump(buf)
|
||||
for line in ['hello\n', 'world\n', 'hi\n'] {
|
||||
wfile.write_string(line)!
|
||||
dump(buf)
|
||||
// print(buf.bytestr())
|
||||
}
|
||||
wfile.close()
|
||||
// dump(buf.bytestr())
|
||||
assert buf.all(it == 0)
|
||||
|
||||
content := os.read_lines('text.txt')!
|
||||
dump(content)
|
||||
assert content == ['S---', 'hello', 'world', 'hi']
|
||||
}
|
||||
@@ -0,0 +1,146 @@
|
||||
module os
|
||||
|
||||
// write_le writes an unsigned number value to the file.
|
||||
// It assumes that the value should be stored in a *little endian order*.
|
||||
// If the machine is a big endian one, it will first convert the big endian value to a little endian one.
|
||||
// It is safe to use as a cross platform way to write data, for which you have to use a predefined order (defined in a file format spec).
|
||||
pub fn (mut f File) write_le[T](data T) ! {
|
||||
mut serialized := data
|
||||
$if big_endian {
|
||||
serialized = swap_bytes(serialized)
|
||||
}
|
||||
C.errno = 0 // needed for tcc
|
||||
check_fwrite(C.fwrite(voidptr(&serialized), sizeof(T), 1, f.cfile))!
|
||||
}
|
||||
|
||||
// write_be writes an unsigned number value to the file.
|
||||
// It assumes that the value should be stored in a *big endian order*.
|
||||
// If the machine is a little endian one, it will first convert the little endian value to a big endian one.
|
||||
// It is safe to use as a cross platform way to write data, for which you have to use a predefined order (defined in a file format spec).
|
||||
pub fn (mut f File) write_be[T](data T) ! {
|
||||
mut serialized := data
|
||||
$if little_endian {
|
||||
serialized = swap_bytes(serialized)
|
||||
}
|
||||
C.errno = 0 // needed for tcc
|
||||
check_fwrite(C.fwrite(voidptr(&serialized), sizeof(T), 1, f.cfile))!
|
||||
}
|
||||
|
||||
// read_le reads an unsigned number value from the file.
|
||||
// It assumes that the value was stored in a *little endian order*.
|
||||
// If the machine is a big endian one, it will convert the value to a big endian one.
|
||||
// It is intended to use as a cross platform way to read data, for which the order is known (from the file format spec).
|
||||
pub fn (mut f File) read_le[T]() !T {
|
||||
mut serialized := T(0)
|
||||
C.errno = 0 // needed for tcc
|
||||
check_fread(C.fread(voidptr(&serialized), sizeof(T), 1, f.cfile))!
|
||||
$if big_endian {
|
||||
return swap_bytes(serialized)
|
||||
}
|
||||
return serialized
|
||||
}
|
||||
|
||||
// read_be reads an unsigned number value from the file.
|
||||
// It assumes that the value was stored in a *big endian order*.
|
||||
// If the machine is a little endian one, it will convert the value to a little endian one.
|
||||
// It is intended to use as a cross platform way to read data, for which the order is known (from the file format spec).
|
||||
pub fn (mut f File) read_be[T]() !T {
|
||||
mut serialized := T(0)
|
||||
C.errno = 0 // needed for tcc
|
||||
check_fread(C.fread(voidptr(&serialized), sizeof(T), 1, f.cfile))!
|
||||
$if little_endian {
|
||||
return swap_bytes(serialized)
|
||||
}
|
||||
return serialized
|
||||
}
|
||||
|
||||
// write_u8 writes a single byte value to the file `f`.
|
||||
// Note: if possible, use some of the other APIs, that write larger chunks of data, before using write_u8/1.
|
||||
pub fn (mut f File) write_u8(b u8) ! {
|
||||
C.errno = 0 // needed for tcc
|
||||
check_fwrite(C.fwrite(voidptr(&b), 1, 1, f.cfile))!
|
||||
}
|
||||
|
||||
// read_u8 reads a single byte value from the file `f`.
|
||||
// Note: if possible, use some of the other APIs, that read larger chunks of data, before using read_u8/1.
|
||||
pub fn (mut f File) read_u8() !u8 {
|
||||
mut res := u8(0)
|
||||
C.errno = 0 // needed for tcc
|
||||
check_fread(C.fread(voidptr(&res), 1, 1, f.cfile))!
|
||||
return res
|
||||
}
|
||||
|
||||
// private helpers
|
||||
|
||||
@[inline]
|
||||
fn swap_bytes_u16(x u16) u16 {
|
||||
// vfmt off
|
||||
return ((x >> 8) & 0x00FF) |
|
||||
((x << 8) & 0xFF00)
|
||||
// vfmt on
|
||||
}
|
||||
|
||||
@[inline]
|
||||
fn swap_bytes_u32(x u32) u32 {
|
||||
// vfmt off
|
||||
return ((x >> 24) & 0x0000_00FF) |
|
||||
((x >> 8) & 0x0000_FF00) |
|
||||
((x << 8) & 0x00FF_0000) |
|
||||
((x << 24) & 0xFF00_0000)
|
||||
// vfmt on
|
||||
}
|
||||
|
||||
@[inline]
|
||||
fn swap_bytes_u64(x u64) u64 {
|
||||
// vfmt off
|
||||
return ((x >> 56) & 0x00000000_000000FF) |
|
||||
((x >> 40) & 0x00000000_0000FF00) |
|
||||
((x >> 24) & 0x00000000_00FF0000) |
|
||||
((x >> 8) & 0x00000000_FF000000) |
|
||||
((x << 8) & 0x000000FF_00000000) |
|
||||
((x << 24) & 0x0000FF00_00000000) |
|
||||
((x << 40) & 0x00FF0000_00000000) |
|
||||
((x << 56) & 0xFF000000_00000000)
|
||||
// vfmt on
|
||||
}
|
||||
|
||||
fn swap_bytes[T](input T) T {
|
||||
$if T is u8 {
|
||||
return input
|
||||
} $else $if T is i8 {
|
||||
return input
|
||||
} $else $if T is u16 {
|
||||
return swap_bytes_u16(input)
|
||||
} $else $if T is u32 {
|
||||
return swap_bytes_u32(input)
|
||||
} $else $if T is u64 {
|
||||
return swap_bytes_u64(input)
|
||||
} $else $if T is i16 {
|
||||
return i16(swap_bytes_u16(u16(input)))
|
||||
} $else $if T is i32 {
|
||||
return i32(swap_bytes_u32(u32(input)))
|
||||
} $else $if T is int {
|
||||
return i32(swap_bytes_u32(u32(input)))
|
||||
} $else $if T is i64 {
|
||||
return i64(swap_bytes_u64(u64(input)))
|
||||
} $else {
|
||||
panic('type is not supported: ' + typeof[T]().str())
|
||||
}
|
||||
}
|
||||
|
||||
fn check_cf(x usize, label string) ! {
|
||||
if C.errno != 0 {
|
||||
return error(posix_get_error_msg(C.errno))
|
||||
}
|
||||
if x == 0 {
|
||||
return error(label)
|
||||
}
|
||||
}
|
||||
|
||||
fn check_fwrite(x usize) ! {
|
||||
check_cf(x, 'fwrite')!
|
||||
}
|
||||
|
||||
fn check_fread(x usize) ! {
|
||||
check_cf(x, 'fread')!
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
import os
|
||||
|
||||
const tfolder = os.join_path(os.vtmp_dir(), 'os_file_le_be')
|
||||
|
||||
fn testsuite_begin() {
|
||||
os.mkdir_all(tfolder) or {}
|
||||
os.chdir(tfolder)!
|
||||
dump(tfolder)
|
||||
assert os.is_dir(tfolder)
|
||||
}
|
||||
|
||||
fn testsuite_end() {
|
||||
os.rmdir_all(tfolder) or {}
|
||||
}
|
||||
|
||||
fn test_write_be_read_be() {
|
||||
fname := os.join_path(tfolder, 'f_be')
|
||||
os.write_file(os.join_path(tfolder, 'abc'), 'hello')!
|
||||
mut f := os.open_file(fname, 'wb')!
|
||||
f.write_be[u8](0x08)!
|
||||
f.write_be[u16](0x1617)!
|
||||
f.write_be[u32](0x30313233)!
|
||||
f.write_be[u64](0x5859606162636465)!
|
||||
f.write_u8(123)!
|
||||
f.write_u8(42)!
|
||||
f.close()
|
||||
// vfmt off
|
||||
assert os.read_bytes(fname)! == [
|
||||
u8(0x08),
|
||||
0x16, 0x17,
|
||||
0x30, 0x31, 0x32, 0x33,
|
||||
0x58, 0x59, 0x60, 0x61, 0x62, 0x63, 0x64, 0x65,
|
||||
123, 42
|
||||
]
|
||||
// vfmt on
|
||||
mut r := os.open_file(fname, 'rb')!
|
||||
assert r.read_be[u8]()! == 0x08
|
||||
assert r.read_be[u16]()! == 0x1617
|
||||
assert r.read_be[u32]()! == 0x30313233
|
||||
assert r.read_be[u64]()! == 0x5859606162636465
|
||||
assert r.read_u8()! == 123
|
||||
assert r.read_u8()! == 42
|
||||
r.close()
|
||||
}
|
||||
|
||||
fn test_write_le_read_le() {
|
||||
fname := os.join_path(tfolder, 'f_le')
|
||||
mut f := os.open_file(fname, 'wb')!
|
||||
f.write_le[u8](0x08)!
|
||||
f.write_le[u16](0x1617)!
|
||||
f.write_le[u32](0x30313233)!
|
||||
f.write_le[u64](0x5859606162636465)!
|
||||
f.write_u8(12)!
|
||||
f.write_u8(34)!
|
||||
f.close()
|
||||
// vfmt off
|
||||
assert os.read_bytes(fname)! == [
|
||||
u8(0x08),
|
||||
0x17, 0x16,
|
||||
0x33, 0x32, 0x31, 0x30,
|
||||
0x65, 0x64, 0x63, 0x62, 0x61, 0x60, 0x59, 0x58,
|
||||
12, 34
|
||||
]
|
||||
// vfmt on
|
||||
mut r := os.open_file(fname, 'rb')!
|
||||
assert r.read_le[u8]()! == 0x08
|
||||
assert r.read_le[u16]()! == 0x1617
|
||||
assert r.read_le[u32]()! == 0x30313233
|
||||
assert r.read_le[u64]()! == 0x5859606162636465
|
||||
assert r.read_u8()! == 12
|
||||
assert r.read_u8()! == 34
|
||||
r.close()
|
||||
}
|
||||
@@ -0,0 +1,542 @@
|
||||
// vtest retry: 2
|
||||
// vtest flaky: true
|
||||
import os
|
||||
|
||||
const tfolder = os.join_path(os.vtmp_dir(), 'os_file_tests')
|
||||
const tfile = os.join_path_single(tfolder, 'test_file')
|
||||
|
||||
fn testsuite_begin() {
|
||||
os.mkdir_all(tfolder) or {}
|
||||
os.chdir(tfolder)!
|
||||
assert os.is_dir(tfolder)
|
||||
}
|
||||
|
||||
fn testsuite_end() {
|
||||
os.rmdir_all(tfolder) or {}
|
||||
}
|
||||
|
||||
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}
|
||||
const another_point = Point{0.25, 2.25, 6.25}
|
||||
const extended_point = Extended_Point{1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0}
|
||||
const another_byte = u8(123)
|
||||
const another_color = Color.red
|
||||
const another_permission = Permissions.read | .write
|
||||
|
||||
// test_read_bytes_with_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_with_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 := []u8{len: 8}
|
||||
|
||||
n0 := f.read_bytes_with_newline(mut buf)!
|
||||
assert n0 == 8
|
||||
|
||||
n1 := f.read_bytes_with_newline(mut buf)!
|
||||
assert n1 == 5
|
||||
|
||||
n2 := f.read_bytes_with_newline(mut buf)!
|
||||
assert n2 == 8
|
||||
|
||||
n3 := f.read_bytes_with_newline(mut buf)!
|
||||
assert n3 == 6
|
||||
|
||||
f.close()
|
||||
}
|
||||
|
||||
// test_read_bytes_with_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_with_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 := []u8{len: 15}
|
||||
bw[9] = 0xff
|
||||
bw[12] = 10 // newline
|
||||
|
||||
n0_bytes := unsafe { bw[0..10] }
|
||||
n1_bytes := unsafe { bw[10..13] }
|
||||
n2_bytes := unsafe { bw[13..] }
|
||||
|
||||
mut f := os.open_file(tfile, 'w')!
|
||||
f.write(bw)!
|
||||
f.close()
|
||||
|
||||
f = os.open_file(tfile, 'r')!
|
||||
mut buf := []u8{len: 10}
|
||||
|
||||
n0 := f.read_bytes_with_newline(mut buf)!
|
||||
assert n0 == 10
|
||||
assert buf[..n0] == n0_bytes
|
||||
|
||||
n1 := f.read_bytes_with_newline(mut buf)!
|
||||
assert n1 == 3
|
||||
assert buf[..n1] == n1_bytes
|
||||
|
||||
n2 := f.read_bytes_with_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 := []u8{len: 199, init: 5}
|
||||
f.write(bw)!
|
||||
f.close()
|
||||
|
||||
f = os.open_file(tfile, 'r')!
|
||||
mut br := []u8{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 {
|
||||
// Expected an error when received end-of-file.
|
||||
assert err is os.Eof
|
||||
}
|
||||
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 beginning of an
|
||||
// fread that returns no data.
|
||||
fn test_read_eof_last_read_full_buffer_fill() {
|
||||
mut f := os.open_file(tfile, 'w')!
|
||||
bw := []u8{len: 200, init: 5}
|
||||
f.write(bw)!
|
||||
f.close()
|
||||
|
||||
f = os.open_file(tfile, 'r')!
|
||||
mut br := []u8{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 an error at EOF.
|
||||
assert err is os.Eof
|
||||
}
|
||||
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 { &u8(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([u8(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 { &u8(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, u64(-1)) {
|
||||
assert false
|
||||
}
|
||||
f.write_raw_at(another_point, u64(-1)) 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[Point]()!
|
||||
b := f.read_raw[u8]()!
|
||||
c := f.read_raw[Color]()!
|
||||
x := f.read_raw[Permissions]()!
|
||||
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([u8(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[Point](at)!
|
||||
at += sizeof(Point)
|
||||
b := f.read_raw_at[u8](at)!
|
||||
at += sizeof(u8)
|
||||
c := f.read_raw_at[Color](at)!
|
||||
at += sizeof(Color)
|
||||
x := f.read_raw_at[Permissions](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[Point](u64(-1)) {
|
||||
assert false
|
||||
}
|
||||
f.read_raw_at[Point](u64(-1)) 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[u8]()!
|
||||
assert b == another_byte
|
||||
|
||||
f.seek(i64(sizeof(Color)), .current)!
|
||||
x := f.read_raw[Permissions]()!
|
||||
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.seek(0, .start)!
|
||||
c1 := f.tell()!
|
||||
_ := f.read_bytes(8)
|
||||
c2 := f.tell()!
|
||||
assert c1 == 0
|
||||
assert c2 == 8
|
||||
f.close()
|
||||
// dump(pos)
|
||||
assert pos == size - 5
|
||||
}
|
||||
}
|
||||
|
||||
fn test_reopen() {
|
||||
tfile1 := os.join_path_single(tfolder, 'tfile1')
|
||||
tfile2 := os.join_path_single(tfolder, 'tfile2')
|
||||
os.write_file(tfile1, 'Hello World!\nGood\r morning.\nBye 1.')!
|
||||
os.write_file(tfile2, 'Another file\nAnother line.\nBye 2.')!
|
||||
assert os.file_size(tfile1) > 0
|
||||
assert os.file_size(tfile2) > 0
|
||||
|
||||
mut line_buffer := []u8{len: 1024}
|
||||
|
||||
mut f2 := os.open(tfile2)!
|
||||
x := f2.read_bytes_with_newline(mut line_buffer)!
|
||||
assert !f2.eof()
|
||||
assert x > 0
|
||||
assert line_buffer#[..x].bytestr() == 'Another file\n'
|
||||
|
||||
// Note: after this call, f2 should be using the file `tfile1`:
|
||||
f2.reopen(tfile1, 'r')!
|
||||
assert !f2.eof()
|
||||
|
||||
z := f2.read(mut line_buffer) or { panic(err) }
|
||||
assert f2.eof()
|
||||
assert z > 0
|
||||
content := line_buffer#[..z].bytestr()
|
||||
// dump(content)
|
||||
assert content.starts_with('Hello World')
|
||||
assert content.ends_with('Bye 1.')
|
||||
}
|
||||
|
||||
fn test_eof() {
|
||||
os.write_file(tfile, 'Hello World!\n')!
|
||||
|
||||
mut f := os.open(tfile)!
|
||||
f.read_bytes(10)
|
||||
assert !f.eof()
|
||||
x := f.read_bytes(100)
|
||||
dump(x)
|
||||
dump(x.len)
|
||||
assert f.eof()
|
||||
f.close()
|
||||
}
|
||||
|
||||
fn test_open_file_wb_ab() {
|
||||
os.rm(tfile) or {}
|
||||
mut wfile := os.open_file('text.txt', 'wb', 0o666)!
|
||||
wfile.write_string('hello')!
|
||||
wfile.close()
|
||||
assert os.read_file('text.txt')! == 'hello'
|
||||
|
||||
mut afile := os.open_file('text.txt', 'ab', 0o666)!
|
||||
afile.write_string('hello')!
|
||||
afile.close()
|
||||
assert os.read_file('text.txt')! == 'hellohello'
|
||||
}
|
||||
|
||||
fn test_open_append() {
|
||||
os.rm(tfile) or {}
|
||||
mut f1 := os.open_append(tfile)!
|
||||
f1.write_string('abc\n')!
|
||||
f1.close()
|
||||
assert os.read_lines(tfile)! == ['abc']
|
||||
|
||||
mut f2 := os.open_append(tfile)!
|
||||
f2.write_string('abc\n')!
|
||||
f2.close()
|
||||
assert os.read_lines(tfile)! == ['abc', 'abc']
|
||||
|
||||
mut f3 := os.open_append(tfile)!
|
||||
f3.write_string('def\n')!
|
||||
f3.close()
|
||||
assert os.read_lines(tfile)! == ['abc', 'abc', 'def']
|
||||
}
|
||||
|
||||
fn test_open_file_on_chinese_windows() {
|
||||
$if windows {
|
||||
os.rm('中文.txt') or {}
|
||||
mut f1 := os.open_file('中文.txt', 'w+', 0x666) or { panic(err) }
|
||||
f1.write_string('test')!
|
||||
f1.close()
|
||||
|
||||
assert os.read_file('中文.txt')! == 'test'
|
||||
assert os.file_size('中文.txt') == 4
|
||||
|
||||
os.truncate('中文.txt', 2)!
|
||||
assert os.file_size('中文.txt') == 2
|
||||
}
|
||||
}
|
||||
|
||||
fn test_open_file_crlf_binary_mode() {
|
||||
teststr := 'hello\r\n'
|
||||
fname := 'text.txt'
|
||||
|
||||
mut wfile := os.open_file(fname, 'w', 0o666)!
|
||||
wfile.write_string(teststr)!
|
||||
wfile.close()
|
||||
|
||||
mut fcont_w := os.read_file(fname)!
|
||||
|
||||
os.rm(fname) or {}
|
||||
|
||||
mut wbfile := os.open_file(fname, 'wb', 0o666)!
|
||||
wbfile.write_string(teststr)!
|
||||
wbfile.close()
|
||||
|
||||
mut fcont_wb := os.read_file(fname)!
|
||||
|
||||
os.rm(fname) or {}
|
||||
|
||||
$if windows {
|
||||
assert fcont_w != teststr
|
||||
}
|
||||
|
||||
assert fcont_wb == teststr
|
||||
}
|
||||
|
||||
fn test_path_devnull() {
|
||||
$if windows {
|
||||
// Reading device files like \\.\nul crashes on Windows (TCC, GCC/MinGW).
|
||||
return
|
||||
}
|
||||
dump(os.path_devnull)
|
||||
content := os.read_file(os.path_devnull)!
|
||||
// dump(content)
|
||||
// dump(content.len)
|
||||
|
||||
os.write_file(os.path_devnull, 'something')!
|
||||
|
||||
content_after := os.read_file(os.path_devnull)!
|
||||
// dump(content_after)
|
||||
// dump(content_after.len)
|
||||
assert content.len == 0
|
||||
assert content_after.len == 0
|
||||
}
|
||||
|
||||
const some_lines_content = 'line 11\nline 22\nline33\n'
|
||||
|
||||
fn test_read_lines() {
|
||||
os.write_file(tfile, some_lines_content)!
|
||||
lines := os.read_lines(tfile)!
|
||||
assert lines == some_lines_content.split_into_lines()
|
||||
}
|
||||
|
||||
fn test_read_lines_with_long_line_and_mixed_line_endings() {
|
||||
long_line := 'a'.repeat(200_000)
|
||||
content := '${long_line}\r\nmiddle\rlast\n\n'
|
||||
os.write_file(tfile, content)!
|
||||
lines := os.read_lines(tfile)!
|
||||
assert lines == content.split_into_lines()
|
||||
}
|
||||
|
||||
fn test_write_lines() {
|
||||
wline1_file := os.join_path_single(tfolder, 'wline1.txt')
|
||||
wline2_file := os.join_path_single(tfolder, 'wline2.txt')
|
||||
os.write_file(wline1_file, some_lines_content)!
|
||||
lines := os.read_lines(wline1_file)!
|
||||
os.write_lines(wline2_file, lines)!
|
||||
c1 := os.read_file(wline1_file)!
|
||||
c2 := os.read_file(wline2_file)!
|
||||
assert c1 == c2
|
||||
assert c1.split_into_lines() == some_lines_content.split_into_lines()
|
||||
}
|
||||
|
||||
fn test_read_from_closed_file() {
|
||||
os.write_file(tfile, 'test')!
|
||||
mut f := os.open(tfile)!
|
||||
f.close()
|
||||
mut buf := []u8{len: 64}
|
||||
assert f.read(mut buf) or { -1 } == -1
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
#ifndef V_OS_FILELOCK_HELPERS_H
|
||||
#define V_OS_FILELOCK_HELPERS_H
|
||||
|
||||
#include <string.h>
|
||||
|
||||
#ifdef _WIN32
|
||||
#include <windows.h>
|
||||
|
||||
static int v_filelock_lock(HANDLE handle, int exclusive, int immediate,
|
||||
unsigned long long start, unsigned long long len) {
|
||||
OVERLAPPED overlap;
|
||||
memset(&overlap, 0, sizeof(overlap));
|
||||
overlap.Offset = (DWORD)(start & 0xffffffffULL);
|
||||
overlap.OffsetHigh = (DWORD)(start >> 32);
|
||||
DWORD flags = immediate ? LOCKFILE_FAIL_IMMEDIATELY : 0;
|
||||
if (exclusive) {
|
||||
flags |= LOCKFILE_EXCLUSIVE_LOCK;
|
||||
}
|
||||
DWORD low = len == 0 ? MAXDWORD : (DWORD)(len & 0xffffffffULL);
|
||||
DWORD high = len == 0 ? MAXDWORD : (DWORD)(len >> 32);
|
||||
return LockFileEx(handle, flags, 0, low, high, &overlap) ? 0 : -1;
|
||||
}
|
||||
|
||||
static int v_filelock_unlock(HANDLE handle, unsigned long long start,
|
||||
unsigned long long len) {
|
||||
OVERLAPPED overlap;
|
||||
memset(&overlap, 0, sizeof(overlap));
|
||||
overlap.Offset = (DWORD)(start & 0xffffffffULL);
|
||||
overlap.OffsetHigh = (DWORD)(start >> 32);
|
||||
DWORD low = len == 0 ? MAXDWORD : (DWORD)(len & 0xffffffffULL);
|
||||
DWORD high = len == 0 ? MAXDWORD : (DWORD)(len >> 32);
|
||||
return UnlockFileEx(handle, 0, low, high, &overlap) ? 0 : -1;
|
||||
}
|
||||
|
||||
#else
|
||||
#include <fcntl.h>
|
||||
#include <unistd.h>
|
||||
|
||||
static int v_filelock_lock(int fd, int exclusive, int immediate,
|
||||
unsigned long long start, unsigned long long len) {
|
||||
struct flock fl;
|
||||
memset(&fl, 0, sizeof(fl));
|
||||
fl.l_type = exclusive ? F_WRLCK : F_RDLCK;
|
||||
fl.l_whence = SEEK_SET;
|
||||
fl.l_start = (off_t)start;
|
||||
fl.l_len = len == 0 ? 0 : (off_t)len;
|
||||
return fcntl(fd, immediate ? F_SETLK : F_SETLKW, &fl);
|
||||
}
|
||||
|
||||
static int v_filelock_unlock(int fd, unsigned long long start,
|
||||
unsigned long long len) {
|
||||
struct flock fl;
|
||||
memset(&fl, 0, sizeof(fl));
|
||||
fl.l_type = F_UNLCK;
|
||||
fl.l_whence = SEEK_SET;
|
||||
fl.l_start = (off_t)start;
|
||||
fl.l_len = len == 0 ? 0 : (off_t)len;
|
||||
return fcntl(fd, F_SETLK, &fl);
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,174 @@
|
||||
import os
|
||||
import os.filelock
|
||||
import time
|
||||
|
||||
const test_dir = os.join_path(os.vtmp_dir(), 'filelock_tests')
|
||||
|
||||
const helper_mode_env = 'V_FILELOCK_HELPER_MODE'
|
||||
const helper_path_env = 'V_FILELOCK_HELPER_PATH'
|
||||
const helper_lock_mode_env = 'V_FILELOCK_LOCK_MODE'
|
||||
const helper_start_env = 'V_FILELOCK_START'
|
||||
const helper_len_env = 'V_FILELOCK_LEN'
|
||||
|
||||
fn testsuite_begin() {
|
||||
if os.getenv(helper_mode_env) != '' {
|
||||
run_helper_process()
|
||||
}
|
||||
os.mkdir_all(test_dir) or {}
|
||||
}
|
||||
|
||||
fn testsuite_end() {
|
||||
os.rmdir_all(test_dir) or {}
|
||||
}
|
||||
|
||||
fn run_helper_process() {
|
||||
path := os.getenv(helper_path_env)
|
||||
lock_mode := if os.getenv(helper_lock_mode_env) == 'shared' {
|
||||
filelock.LockMode.shared
|
||||
} else {
|
||||
filelock.LockMode.exclusive
|
||||
}
|
||||
start := os.getenv(helper_start_env).u64()
|
||||
len := os.getenv(helper_len_env).u64()
|
||||
mut file_lock := filelock.new_file(path, filelock.LockOptions{
|
||||
mode: lock_mode
|
||||
start: start
|
||||
len: len
|
||||
})
|
||||
match os.getenv(helper_mode_env) {
|
||||
'hold' {
|
||||
file_lock.acquire() or {
|
||||
eprintln(err.msg())
|
||||
exit(2)
|
||||
}
|
||||
println('locked')
|
||||
os.flush()
|
||||
time.sleep(300 * time.millisecond)
|
||||
file_lock.release()
|
||||
exit(0)
|
||||
}
|
||||
'try' {
|
||||
exit(if file_lock.try_acquire() {
|
||||
file_lock.release()
|
||||
0
|
||||
} else {
|
||||
1
|
||||
})
|
||||
}
|
||||
else {
|
||||
exit(3)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn helper_path(name string) string {
|
||||
return os.join_path(test_dir, name)
|
||||
}
|
||||
|
||||
fn new_helper_process(path string, lock_mode filelock.LockMode, start u64, len u64, mode string) &os.Process {
|
||||
mut p := os.new_process(os.executable())
|
||||
mut env := os.environ()
|
||||
env[helper_mode_env] = mode
|
||||
env[helper_path_env] = path
|
||||
env[helper_lock_mode_env] = lock_mode.str()
|
||||
env[helper_start_env] = start.str()
|
||||
env[helper_len_env] = len.str()
|
||||
p.set_environment(env)
|
||||
p.set_redirect_stdio()
|
||||
return p
|
||||
}
|
||||
|
||||
fn wait_for_lock(mut p os.Process) {
|
||||
mut output := ''
|
||||
for _ in 0 .. 50 {
|
||||
if p.is_pending(.stdout) {
|
||||
output += p.stdout_read()
|
||||
if output.contains('locked') {
|
||||
return
|
||||
}
|
||||
}
|
||||
if !p.is_alive() {
|
||||
break
|
||||
}
|
||||
time.sleep(10 * time.millisecond)
|
||||
}
|
||||
p.wait()
|
||||
stderr := p.stderr_slurp().trim_space()
|
||||
assert false, 'helper failed to acquire lock; exit=${p.code} stdout="${output.trim_space()}" stderr="${stderr}"'
|
||||
}
|
||||
|
||||
fn try_lock_from_helper(path string, lock_mode filelock.LockMode, start u64, len u64) int {
|
||||
mut p := new_helper_process(path, lock_mode, start, len, 'try')
|
||||
p.run()
|
||||
p.wait()
|
||||
stderr := p.stderr_slurp().trim_space()
|
||||
code := p.code
|
||||
p.close()
|
||||
assert stderr == ''
|
||||
return code
|
||||
}
|
||||
|
||||
fn test_flock() {
|
||||
lockfile := helper_path('test.lock')
|
||||
os.rm(lockfile) or {}
|
||||
mut l := filelock.new(lockfile)
|
||||
assert !os.exists(lockfile)
|
||||
l.acquire() or { panic(err) }
|
||||
assert os.exists(lockfile)
|
||||
l.release()
|
||||
assert !os.exists(lockfile)
|
||||
}
|
||||
|
||||
fn test_flock_try() {
|
||||
lockfile := helper_path('test-try.lock')
|
||||
os.rm(lockfile) or {}
|
||||
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)
|
||||
}
|
||||
|
||||
fn test_existing_file_lock_range() {
|
||||
target := helper_path('existing-range.txt')
|
||||
os.write_file(target, '0123456789abcdef')!
|
||||
mut holder := new_helper_process(target, .exclusive, 0, 5, 'hold')
|
||||
holder.run()
|
||||
defer {
|
||||
if holder.status == .running || holder.is_alive() {
|
||||
holder.wait()
|
||||
}
|
||||
holder.close()
|
||||
}
|
||||
wait_for_lock(mut holder)
|
||||
assert try_lock_from_helper(target, .exclusive, 0, 5) == 1
|
||||
assert try_lock_from_helper(target, .exclusive, 6, 5) == 0
|
||||
holder.wait()
|
||||
assert holder.code == 0
|
||||
assert os.exists(target)
|
||||
assert os.read_file(target)! == '0123456789abcdef'
|
||||
}
|
||||
|
||||
fn test_existing_file_lock_shared_mode() {
|
||||
target := helper_path('existing-shared.txt')
|
||||
os.write_file(target, '0123456789abcdef')!
|
||||
mut holder := new_helper_process(target, .shared, 0, 5, 'hold')
|
||||
holder.run()
|
||||
defer {
|
||||
if holder.status == .running || holder.is_alive() {
|
||||
holder.wait()
|
||||
}
|
||||
holder.close()
|
||||
}
|
||||
wait_for_lock(mut holder)
|
||||
assert try_lock_from_helper(target, .shared, 0, 5) == 0
|
||||
assert try_lock_from_helper(target, .exclusive, 0, 5) == 1
|
||||
holder.wait()
|
||||
assert holder.code == 0
|
||||
assert os.exists(target)
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
module filelock
|
||||
|
||||
import time
|
||||
|
||||
// LockMode describes whether an existing file lock is shared/read or exclusive/write.
|
||||
pub enum LockMode {
|
||||
shared
|
||||
exclusive
|
||||
}
|
||||
|
||||
// LockOptions configures how `new_file` locks an existing file.
|
||||
@[params]
|
||||
pub struct LockOptions {
|
||||
pub:
|
||||
mode LockMode = .exclusive
|
||||
start u64
|
||||
len u64
|
||||
}
|
||||
|
||||
enum LockTarget {
|
||||
sidecar
|
||||
file
|
||||
}
|
||||
|
||||
pub struct FileLock {
|
||||
name string
|
||||
mode LockMode = .exclusive
|
||||
start u64
|
||||
len u64
|
||||
target LockTarget = .sidecar
|
||||
mut:
|
||||
fd i64
|
||||
}
|
||||
|
||||
// new creates a sidecar lock file that is removed again when the lock is released.
|
||||
pub fn new(file_name string) FileLock {
|
||||
return FileLock{
|
||||
name: file_name
|
||||
fd: -1
|
||||
}
|
||||
}
|
||||
|
||||
// new_file creates a lock for an existing file path using OS-level file locking.
|
||||
pub fn new_file(path string, options LockOptions) FileLock {
|
||||
return FileLock{
|
||||
name: path
|
||||
mode: options.mode
|
||||
start: options.start
|
||||
len: options.len
|
||||
target: .file
|
||||
fd: -1
|
||||
}
|
||||
}
|
||||
|
||||
// wait_acquire keeps trying to acquire the lock until `timeout` expires.
|
||||
pub fn (mut l FileLock) wait_acquire(timeout time.Duration) bool {
|
||||
fin := time.now().add(timeout)
|
||||
for time.now() < fin {
|
||||
if l.try_acquire() {
|
||||
return true
|
||||
}
|
||||
time.sleep(1 * time.millisecond)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// release unlocks the file and closes the underlying file descriptor or handle.
|
||||
pub fn (mut l FileLock) release() bool {
|
||||
if l.fd != -1 {
|
||||
l.close_lock()
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
module filelock
|
||||
|
||||
#include <sys/file.h>
|
||||
#insert "@VEXEROOT/vlib/os/filelock/filelock_helpers.h"
|
||||
|
||||
fn C.unlink(&char) i32
|
||||
fn C.open(&char, i32, i32) i32
|
||||
fn C.close(i32) i32
|
||||
fn C.flock(i32, i32) i32
|
||||
fn C.v_filelock_lock(i32, i32, i32, u64, u64) i32
|
||||
fn C.v_filelock_unlock(i32, u64, u64) i32
|
||||
|
||||
fn (l &FileLock) open_lock() int {
|
||||
if l.target == .file {
|
||||
return open_existing_file(l.name, l.mode)
|
||||
}
|
||||
return open_lockfile(l.name)
|
||||
}
|
||||
|
||||
fn open_lockfile(path string) int {
|
||||
mut fd := C.open(&char(path.str), C.O_CREAT | C.O_RDONLY, 0o644)
|
||||
if fd == -1 {
|
||||
fd = C.open(&char(path.str), C.O_RDONLY, 0)
|
||||
}
|
||||
return fd
|
||||
}
|
||||
|
||||
fn open_existing_file(path string, mode LockMode) int {
|
||||
flags := if mode == .shared { C.O_RDONLY } else { C.O_RDWR }
|
||||
return C.open(&char(path.str), flags, 0)
|
||||
}
|
||||
|
||||
fn (l &FileLock) lock_fd(fd int, immediate bool) bool {
|
||||
if l.target == .file {
|
||||
return C.v_filelock_lock(fd, int(l.mode == .exclusive), int(immediate), l.start, l.len) == 0
|
||||
}
|
||||
flags := if immediate { C.LOCK_EX | C.LOCK_NB } else { C.LOCK_EX }
|
||||
return C.flock(fd, flags) == 0
|
||||
}
|
||||
|
||||
fn (mut l FileLock) close_lock() {
|
||||
if l.fd == -1 {
|
||||
return
|
||||
}
|
||||
fd := int(l.fd)
|
||||
if l.target == .file {
|
||||
C.v_filelock_unlock(fd, l.start, l.len)
|
||||
C.close(fd)
|
||||
} else {
|
||||
C.close(fd)
|
||||
C.unlink(&char(l.name.str))
|
||||
}
|
||||
l.fd = -1
|
||||
}
|
||||
|
||||
// acquire blocks until the lock is acquired.
|
||||
pub fn (mut l FileLock) acquire() ! {
|
||||
if l.fd != -1 {
|
||||
return error_with_code('lock already acquired by this instance', 1)
|
||||
}
|
||||
fd := l.open_lock()
|
||||
if fd == -1 {
|
||||
msg := if l.target == .file {
|
||||
'cannot open file ${l.name}'
|
||||
} else {
|
||||
'cannot create lock file ${l.name}'
|
||||
}
|
||||
return error_with_code(msg, -1)
|
||||
}
|
||||
if !l.lock_fd(fd, false) {
|
||||
C.close(fd)
|
||||
return error_with_code('cannot lock ${l.name}', -2)
|
||||
}
|
||||
l.fd = fd
|
||||
}
|
||||
|
||||
// try_acquire tries to acquire the lock without blocking.
|
||||
pub fn (mut l FileLock) try_acquire() bool {
|
||||
if l.fd != -1 {
|
||||
return true
|
||||
}
|
||||
fd := l.open_lock()
|
||||
if fd == -1 {
|
||||
return false
|
||||
}
|
||||
if !l.lock_fd(fd, true) {
|
||||
C.close(fd)
|
||||
return false
|
||||
}
|
||||
l.fd = fd
|
||||
return true
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
module filelock
|
||||
|
||||
#insert "@VEXEROOT/vlib/os/filelock/filelock_helpers.h"
|
||||
|
||||
fn C.DeleteFileW(&u16) bool
|
||||
fn C.CreateFileW(&u16, u32, u32, voidptr, u32, u32, voidptr) voidptr
|
||||
fn C.CloseHandle(voidptr) bool
|
||||
fn C.v_filelock_lock(voidptr, int, int, u64, u64) int
|
||||
fn C.v_filelock_unlock(voidptr, u64, u64) int
|
||||
|
||||
fn (l &FileLock) open_lock() i64 {
|
||||
if l.target == .file {
|
||||
return open_existing_file(l.name, l.mode)
|
||||
}
|
||||
return open_lockfile(l.name)
|
||||
}
|
||||
|
||||
fn open_lockfile(path string) i64 {
|
||||
path_wide := path.to_wide()
|
||||
handle := C.CreateFileW(path_wide, C.GENERIC_READ | C.GENERIC_WRITE, 0, 0, C.OPEN_ALWAYS,
|
||||
C.FILE_ATTRIBUTE_NORMAL, 0)
|
||||
return file_handle(handle)
|
||||
}
|
||||
|
||||
fn open_existing_file(path string, mode LockMode) i64 {
|
||||
path_wide := path.to_wide()
|
||||
access := if mode == .shared { C.GENERIC_READ } else { C.GENERIC_READ | C.GENERIC_WRITE }
|
||||
share_mode := C.FILE_SHARE_READ | C.FILE_SHARE_WRITE | C.FILE_SHARE_DELETE
|
||||
handle := C.CreateFileW(path_wide, access, share_mode, 0, C.OPEN_EXISTING,
|
||||
C.FILE_ATTRIBUTE_NORMAL, 0)
|
||||
return file_handle(handle)
|
||||
}
|
||||
|
||||
fn file_handle(handle voidptr) i64 {
|
||||
return if handle == voidptr(-1) { -1 } else { i64(handle) }
|
||||
}
|
||||
|
||||
fn (l &FileLock) lock_handle(handle voidptr, immediate bool) bool {
|
||||
if l.target != .file {
|
||||
return true
|
||||
}
|
||||
return C.v_filelock_lock(handle, int(l.mode == .exclusive), int(immediate), l.start, l.len) == 0
|
||||
}
|
||||
|
||||
fn (mut l FileLock) close_lock() {
|
||||
if l.fd == -1 {
|
||||
return
|
||||
}
|
||||
handle := voidptr(l.fd)
|
||||
if l.target == .file {
|
||||
C.v_filelock_unlock(handle, l.start, l.len)
|
||||
C.CloseHandle(handle)
|
||||
} else {
|
||||
C.CloseHandle(handle)
|
||||
path_wide := l.name.to_wide()
|
||||
C.DeleteFileW(path_wide)
|
||||
}
|
||||
l.fd = -1
|
||||
}
|
||||
|
||||
// acquire blocks until the lock is acquired.
|
||||
pub fn (mut l FileLock) acquire() ! {
|
||||
if l.fd != -1 {
|
||||
return error_with_code('lock already acquired by this instance', 1)
|
||||
}
|
||||
fd := l.open_lock()
|
||||
if fd == -1 {
|
||||
msg := if l.target == .file {
|
||||
'cannot open file ${l.name}'
|
||||
} else {
|
||||
'cannot create lock file ${l.name}'
|
||||
}
|
||||
return error_with_code(msg, -1)
|
||||
}
|
||||
handle := voidptr(fd)
|
||||
if !l.lock_handle(handle, false) {
|
||||
C.CloseHandle(handle)
|
||||
return error_with_code('cannot lock ${l.name}', -2)
|
||||
}
|
||||
l.fd = fd
|
||||
}
|
||||
|
||||
// try_acquire tries to acquire the lock without blocking.
|
||||
pub fn (mut l FileLock) try_acquire() bool {
|
||||
if l.fd != -1 {
|
||||
return true
|
||||
}
|
||||
fd := l.open_lock()
|
||||
if fd == -1 {
|
||||
return false
|
||||
}
|
||||
handle := voidptr(fd)
|
||||
if !l.lock_handle(handle, true) {
|
||||
C.CloseHandle(handle)
|
||||
return false
|
||||
}
|
||||
l.fd = fd
|
||||
return true
|
||||
}
|
||||
@@ -0,0 +1,313 @@
|
||||
module os
|
||||
|
||||
import strings
|
||||
import strings.textscanner
|
||||
|
||||
// Collection of useful functions for manipulation, validation and analysis of system paths.
|
||||
// The following functions handle paths depending on the operating system,
|
||||
// therefore results may be different for certain operating systems.
|
||||
|
||||
const fslash = `/`
|
||||
const bslash = `\\`
|
||||
const dot = `.`
|
||||
const qmark = `?`
|
||||
const fslash_str = '/'
|
||||
const dot_dot = '..'
|
||||
const empty_str = ''
|
||||
const dot_str = '.'
|
||||
|
||||
// is_abs_path returns `true` if the given `path` is absolute.
|
||||
pub fn is_abs_path(path string) bool {
|
||||
if path == '' {
|
||||
return false
|
||||
}
|
||||
$if windows {
|
||||
return is_unc_path(path) || is_drive_rooted(path) || is_normal_path(path)
|
||||
}
|
||||
return path[0] == fslash
|
||||
}
|
||||
|
||||
// abs_path joins the current working directory with the given `path` (if the `path` is relative), and returns the absolute path representation.
|
||||
pub fn abs_path(path string) string {
|
||||
wd := getwd()
|
||||
if path == '' {
|
||||
return wd
|
||||
}
|
||||
npath := norm_path(path)
|
||||
if npath == dot_str {
|
||||
return wd
|
||||
}
|
||||
if !is_abs_path(npath) {
|
||||
mut sb := strings.new_builder(npath.len)
|
||||
sb.write_string(wd)
|
||||
sb.write_string(path_separator)
|
||||
sb.write_string(npath)
|
||||
return norm_path(sb.str())
|
||||
}
|
||||
return npath
|
||||
}
|
||||
|
||||
// norm_path returns the normalized version of the given `path`
|
||||
// by resolving backlinks (..), turning forward slashes into
|
||||
// back slashes on a Windows system and eliminating:
|
||||
// - references to current directories (.)
|
||||
// - redundant path separators
|
||||
// - the last path separator
|
||||
@[direct_array_access]
|
||||
pub fn norm_path(path string) string {
|
||||
if path == '' {
|
||||
return dot_str
|
||||
}
|
||||
rooted := is_abs_path(path)
|
||||
// get the volume name from the path
|
||||
// if the current operating system is Windows
|
||||
volume_len := win_volume_len(path)
|
||||
mut volume := path[..volume_len]
|
||||
if volume_len != 0 && volume.contains(fslash_str) {
|
||||
volume = volume.replace(fslash_str, path_separator)
|
||||
}
|
||||
cpath := clean_path(path[volume_len..])
|
||||
if cpath == '' && volume_len == 0 {
|
||||
return dot_str
|
||||
}
|
||||
spath := cpath.split(path_separator)
|
||||
if dot_dot !in spath {
|
||||
return if volume_len != 0 { volume + cpath } else { cpath }
|
||||
}
|
||||
// resolve backlinks (..)
|
||||
spath_len := spath.len
|
||||
mut sb := strings.new_builder(cpath.len)
|
||||
if rooted {
|
||||
sb.write_string(path_separator)
|
||||
}
|
||||
mut new_path := []string{cap: spath_len}
|
||||
mut backlink_count := 0
|
||||
for i := spath_len - 1; i >= 0; i-- {
|
||||
part := spath[i]
|
||||
if part == empty_str {
|
||||
continue
|
||||
}
|
||||
if part == dot_dot {
|
||||
backlink_count++
|
||||
continue
|
||||
}
|
||||
if backlink_count != 0 {
|
||||
backlink_count--
|
||||
continue
|
||||
}
|
||||
new_path.prepend(part)
|
||||
}
|
||||
// append backlink(s) to the path if backtracking
|
||||
// is not possible and the given path is not rooted
|
||||
if backlink_count != 0 && !rooted {
|
||||
for i in 0 .. backlink_count {
|
||||
sb.write_string(dot_dot)
|
||||
if new_path.len == 0 && i == backlink_count - 1 {
|
||||
break
|
||||
}
|
||||
sb.write_string(path_separator)
|
||||
}
|
||||
}
|
||||
sb.write_string(new_path.join(path_separator))
|
||||
res := sb.str()
|
||||
if res.len == 0 {
|
||||
if volume_len != 0 {
|
||||
return volume
|
||||
}
|
||||
if !rooted {
|
||||
return dot_str
|
||||
}
|
||||
return path_separator
|
||||
}
|
||||
if volume_len != 0 {
|
||||
return volume + res
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
// existing_path returns the existing part of the given `path`.
|
||||
// An error is returned if there is no existing part of the given `path`.
|
||||
pub fn existing_path(path string) !string {
|
||||
err := error('path does not exist')
|
||||
if path == '' {
|
||||
return err
|
||||
}
|
||||
if exists(path) {
|
||||
return path
|
||||
}
|
||||
mut volume_len := 0
|
||||
$if windows {
|
||||
volume_len = win_volume_len(path)
|
||||
}
|
||||
if volume_len > 0 && is_slash(path[volume_len - 1]) {
|
||||
volume_len++
|
||||
}
|
||||
mut sc := textscanner.new(path[volume_len..])
|
||||
mut recent_path := path[..volume_len]
|
||||
for sc.next() != -1 {
|
||||
curr := u8(sc.current())
|
||||
peek := sc.peek()
|
||||
back := sc.peek_back()
|
||||
if is_curr_dir_ref(back, curr, peek) {
|
||||
continue
|
||||
}
|
||||
range := sc.ilen - sc.remaining() + volume_len
|
||||
if is_slash(curr) && !is_slash(u8(peek)) {
|
||||
recent_path = path[..range]
|
||||
continue
|
||||
}
|
||||
if !is_slash(curr) && (peek == -1 || is_slash(u8(peek))) {
|
||||
curr_path := path[..range]
|
||||
if exists(curr_path) {
|
||||
recent_path = curr_path
|
||||
continue
|
||||
}
|
||||
if recent_path == '' {
|
||||
break
|
||||
}
|
||||
return recent_path
|
||||
}
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// clean_path returns the "cleaned" version of the given `path`
|
||||
// by turning forward slashes into back slashes
|
||||
// on a Windows system and eliminating:
|
||||
// - references to current directories (.)
|
||||
// - redundant separators
|
||||
// - the last path separator
|
||||
fn clean_path(path string) string {
|
||||
if path == '' {
|
||||
return empty_str
|
||||
}
|
||||
mut sb := strings.new_builder(path.len)
|
||||
mut sc := textscanner.new(path)
|
||||
for sc.next() != -1 {
|
||||
curr := u8(sc.current())
|
||||
back := sc.peek_back()
|
||||
peek := sc.peek()
|
||||
// skip current path separator if last byte was a path separator
|
||||
if back != -1 && is_slash(u8(back)) && is_slash(curr) {
|
||||
continue
|
||||
}
|
||||
// skip reference to current dir (.)
|
||||
if is_curr_dir_ref(back, curr, peek) {
|
||||
// skip if the next byte is a path separator
|
||||
if peek != -1 && is_slash(u8(peek)) {
|
||||
sc.skip_n(1)
|
||||
}
|
||||
continue
|
||||
}
|
||||
// turn forward slash into a back slash on a Windows system
|
||||
$if windows {
|
||||
if curr == fslash {
|
||||
sb.write_u8(bslash)
|
||||
continue
|
||||
}
|
||||
}
|
||||
sb.write_u8(u8(sc.current()))
|
||||
}
|
||||
res := sb.str()
|
||||
// eliminate the last path separator
|
||||
if res.len > 1 && is_slash(res[res.len - 1]) {
|
||||
return res[..res.len - 1]
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
// to_slash returns the result of replacing each separator character in path with a slash (`/`).
|
||||
pub fn to_slash(path string) string {
|
||||
return $if windows {
|
||||
path.replace(path_separator, '/')
|
||||
} $else {
|
||||
path
|
||||
}
|
||||
}
|
||||
|
||||
// from_slash returns the result of replacing each slash (`/`) character is path with a separator character.
|
||||
pub fn from_slash(path string) string {
|
||||
return $if windows {
|
||||
path.replace('/', path_separator)
|
||||
} $else {
|
||||
path
|
||||
}
|
||||
}
|
||||
|
||||
// win_volume_len returns the length of the
|
||||
// Windows volume/drive from the given `path`.
|
||||
fn win_volume_len(path string) int {
|
||||
$if !windows {
|
||||
return 0
|
||||
}
|
||||
plen := path.len
|
||||
if plen < 2 {
|
||||
return 0
|
||||
}
|
||||
if has_drive_letter(path) {
|
||||
return 2
|
||||
}
|
||||
// its UNC path / DOS device path?
|
||||
if plen >= 5 && starts_w_slash_slash(path) && !is_slash(path[2]) {
|
||||
for i := 3; i < plen; i++ {
|
||||
if is_slash(path[i]) {
|
||||
if i + 1 >= plen || is_slash(path[i + 1]) {
|
||||
break
|
||||
}
|
||||
i++
|
||||
for ; i < plen; i++ {
|
||||
if is_slash(path[i]) {
|
||||
return i
|
||||
}
|
||||
}
|
||||
return i
|
||||
}
|
||||
}
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
fn is_slash(b u8) bool {
|
||||
$if windows {
|
||||
return b == bslash || b == fslash
|
||||
}
|
||||
return b == fslash
|
||||
}
|
||||
|
||||
fn is_unc_path(path string) bool {
|
||||
return win_volume_len(path) >= 5 && starts_w_slash_slash(path)
|
||||
}
|
||||
|
||||
fn has_drive_letter(path string) bool {
|
||||
return path.len >= 2 && path[0].is_letter() && path[1] == `:`
|
||||
}
|
||||
|
||||
fn starts_w_slash_slash(path string) bool {
|
||||
return path.len >= 2 && is_slash(path[0]) && is_slash(path[1])
|
||||
}
|
||||
|
||||
fn is_drive_rooted(path string) bool {
|
||||
return path.len >= 3 && has_drive_letter(path) && is_slash(path[2])
|
||||
}
|
||||
|
||||
// is_normal_path returns `true` if the given
|
||||
// `path` is NOT a network or Windows device path.
|
||||
fn is_normal_path(path string) bool {
|
||||
plen := path.len
|
||||
if plen == 0 {
|
||||
return false
|
||||
}
|
||||
// vfmt off
|
||||
return (plen == 1 && is_slash(path[0])) || (plen >= 2 && is_slash(path[0]) && !is_slash(path[1]))
|
||||
// vfmt on
|
||||
}
|
||||
|
||||
// is_curr_dir_ref returns `true` if the 3 given integer construct
|
||||
// a reference to a current directory (.).
|
||||
// NOTE: a negative integer means that no byte is present
|
||||
fn is_curr_dir_ref(byte_one int, byte_two int, byte_three int) bool {
|
||||
if u8(byte_two) != dot {
|
||||
return false
|
||||
}
|
||||
return (byte_one < 0 || is_slash(u8(byte_one))) && (byte_three < 0 || is_slash(u8(byte_three)))
|
||||
}
|
||||
@@ -0,0 +1,197 @@
|
||||
module os
|
||||
|
||||
fn test_is_abs_path() {
|
||||
$if windows {
|
||||
assert is_abs_path('/')
|
||||
assert is_abs_path('\\')
|
||||
assert !is_abs_path('\\\\')
|
||||
assert is_abs_path(r'C:\path\to\files\file.v')
|
||||
assert is_abs_path(r'\\Host\share')
|
||||
assert is_abs_path(r'//Host\share\files\file.v')
|
||||
assert is_abs_path(r'\\.\BootPartition\Windows')
|
||||
assert !is_abs_path(r'\\.\')
|
||||
assert !is_abs_path(r'\\?\\')
|
||||
assert !is_abs_path(r'C:path\to\dir')
|
||||
assert !is_abs_path(r'dir')
|
||||
assert !is_abs_path(r'.\')
|
||||
assert !is_abs_path(r'.')
|
||||
assert !is_abs_path(r'\\Host')
|
||||
assert !is_abs_path(r'\\Host\')
|
||||
return
|
||||
}
|
||||
assert is_abs_path('/')
|
||||
assert is_abs_path('/path/to/files/file.v')
|
||||
assert !is_abs_path('\\')
|
||||
assert !is_abs_path('path/to/files/file.v')
|
||||
assert !is_abs_path('dir')
|
||||
assert !is_abs_path('./')
|
||||
assert !is_abs_path('.')
|
||||
}
|
||||
|
||||
fn test_clean_path() {
|
||||
$if windows {
|
||||
assert clean_path(r'\\path\to\files/file.v') == r'\path\to\files\file.v'
|
||||
assert clean_path(r'\/\//\/') == '\\'
|
||||
assert clean_path(r'./path\\dir/\\./\/\\/file.v\.\\\.') == r'path\dir\file.v'
|
||||
assert clean_path(r'\./path/dir\\file.exe') == r'\path\dir\file.exe'
|
||||
assert clean_path(r'.') == ''
|
||||
assert clean_path(r'./') == ''
|
||||
assert clean_path('') == ''
|
||||
assert clean_path(r'\./') == '\\'
|
||||
assert clean_path(r'//\/\/////') == '\\'
|
||||
return
|
||||
}
|
||||
assert clean_path('./../.././././//') == '../..'
|
||||
assert clean_path('') == ''
|
||||
assert clean_path('.') == ''
|
||||
assert clean_path('./path/to/file.v//./') == 'path/to/file.v'
|
||||
assert clean_path('./') == ''
|
||||
assert clean_path('/.') == '/'
|
||||
assert clean_path('//path/./to/.///files/file.v///') == '/path/to/files/file.v'
|
||||
assert clean_path('path/./to/.///files/.././file.v///') == 'path/to/files/../file.v'
|
||||
assert clean_path('\\') == '\\'
|
||||
assert clean_path('//////////') == '/'
|
||||
}
|
||||
|
||||
fn test_to_slash() {
|
||||
sep := path_separator
|
||||
assert to_slash('') == ''
|
||||
assert to_slash(sep) == ('/')
|
||||
assert to_slash([sep, 'a', sep, 'b'].join('')) == '/a/b'
|
||||
assert to_slash(['a', sep, sep, 'b'].join('')) == 'a//b'
|
||||
}
|
||||
|
||||
fn test_from_slash() {
|
||||
sep := path_separator
|
||||
assert from_slash('') == ''
|
||||
assert from_slash('/') == sep
|
||||
assert from_slash('/a/b') == [sep, 'a', sep, 'b'].join('')
|
||||
assert from_slash('a//b') == ['a', sep, sep, 'b'].join('')
|
||||
}
|
||||
|
||||
fn test_norm_path() {
|
||||
$if windows {
|
||||
assert norm_path(r'C:/path/to//file.v\\') == r'C:\path\to\file.v'
|
||||
assert norm_path(r'C:path\.\..\\\.\to//file.v') == r'C:to\file.v'
|
||||
assert norm_path(r'D:path\.\..\..\\\\.\to//dir/..\') == r'D:..\to'
|
||||
assert norm_path(r'D:/path\.\..\/..\file.v') == r'D:\file.v'
|
||||
assert norm_path(r'') == '.'
|
||||
assert norm_path(r'/') == '\\'
|
||||
assert norm_path(r'\/') == '\\'
|
||||
assert norm_path(r'path\../dir\..') == '.'
|
||||
assert norm_path(r'.\.\') == '.'
|
||||
assert norm_path(r'G:.\.\dir\././\.\.\\\\///to/././\file.v/./\\') == r'G:dir\to\file.v'
|
||||
assert norm_path(r'G:\..\..\.\.\file.v\\\.') == r'G:\file.v'
|
||||
assert norm_path(r'\\Server\share\\\dir/..\file.v\./.') == r'\\Server\share\file.v'
|
||||
assert norm_path(r'\\.\device\\\dir/to/./file.v\.') == r'\\.\device\dir\to\file.v'
|
||||
assert norm_path(r'C:dir/../dir2/../../../file.v') == r'C:..\..\file.v'
|
||||
assert norm_path(r'\\.\C:\\\Users/\Documents//..') == r'\\.\C:\Users'
|
||||
assert norm_path(r'\\.\C:\Users') == r'\\.\C:\Users'
|
||||
assert norm_path(r'\\') == '\\'
|
||||
assert norm_path(r'//') == '\\'
|
||||
assert norm_path(r'\\\') == '\\'
|
||||
assert norm_path(r'.') == '.'
|
||||
assert norm_path(r'\\Server') == '\\Server'
|
||||
assert norm_path(r'\\Server\') == '\\Server'
|
||||
return
|
||||
}
|
||||
assert norm_path('/path/././../to/file//file.v/.') == '/to/file/file.v'
|
||||
assert norm_path('path/././to/files/../../file.v/.') == 'path/file.v'
|
||||
assert norm_path('path/././/../../to/file.v/.') == '../to/file.v'
|
||||
assert norm_path('/path/././/../..///.././file.v/././') == '/file.v'
|
||||
assert norm_path('path/././//../../../to/dir//.././file.v/././') == '../../to/file.v'
|
||||
assert norm_path('path/../dir/..') == '.'
|
||||
assert norm_path('../dir/..') == '..'
|
||||
assert norm_path('/../dir/..') == '/'
|
||||
assert norm_path('//././dir/../files/././/file.v') == '/files/file.v'
|
||||
assert norm_path('/\\../dir/////////.') == '/\\../dir'
|
||||
assert norm_path('/home/') == '/home'
|
||||
assert norm_path('/home/////./.') == '/home'
|
||||
assert norm_path('...') == '...'
|
||||
}
|
||||
|
||||
fn test_abs_path() {
|
||||
wd := getwd()
|
||||
wd_w_sep := wd + path_separator
|
||||
$if windows {
|
||||
assert abs_path('path/to/file.v') == '${wd_w_sep}path\\to\\file.v'
|
||||
assert abs_path('path/to/file.v') == '${wd_w_sep}path\\to\\file.v'
|
||||
assert abs_path('/') == r'\'
|
||||
assert abs_path(r'C:\path\to\files\file.v') == r'C:\path\to\files\file.v'
|
||||
assert abs_path(r'C:\/\path\.\to\../files\file.v\.\\\.\') == r'C:\path\files\file.v'
|
||||
assert abs_path(r'\\Host\share\files\..\..\.') == r'\\Host\share\'
|
||||
assert abs_path(r'\\.\HardDiskvolume2\files\..\..\.') == r'\\.\HardDiskvolume2\'
|
||||
assert abs_path(r'\\?\share') == r'\\?\share'
|
||||
assert abs_path(r'\\.\') == r'\'
|
||||
assert abs_path(r'G:/\..\\..\.\.\file.v\\.\.\\\\') == r'G:\file.v'
|
||||
assert abs_path('files') == '${wd_w_sep}files'
|
||||
assert abs_path('') == wd
|
||||
assert abs_path('.') == wd
|
||||
assert abs_path('files/../file.v') == '${wd_w_sep}file.v'
|
||||
assert abs_path('///') == r'\'
|
||||
assert abs_path('/path/to/file.v') == r'\path\to\file.v'
|
||||
assert abs_path('D:/') == r'D:\'
|
||||
assert abs_path(r'\\.\HardiskVolume6') == r'\\.\HardiskVolume6'
|
||||
return
|
||||
}
|
||||
assert abs_path('/') == '/'
|
||||
assert abs_path('.') == wd
|
||||
assert abs_path('files') == '${wd_w_sep}files'
|
||||
assert abs_path('') == wd
|
||||
assert abs_path('files/../file.v') == '${wd_w_sep}file.v'
|
||||
assert abs_path('///') == '/'
|
||||
assert abs_path('/path/to/file.v') == '/path/to/file.v'
|
||||
assert abs_path('/path/to/file.v/../..') == '/path'
|
||||
assert abs_path('path/../file.v/..') == wd
|
||||
assert abs_path('///') == '/'
|
||||
}
|
||||
|
||||
fn test_existing_path() {
|
||||
wd := getwd()
|
||||
$if windows {
|
||||
assert existing_path('') or { '' } == ''
|
||||
assert existing_path('..') or { '' } == '..'
|
||||
assert existing_path('.') or { '' } == '.'
|
||||
assert existing_path(wd) or { '' } == wd
|
||||
assert existing_path('\\') or { '' } == '\\'
|
||||
assert existing_path('${wd}\\.\\\\does/not/exist\\.\\') or { '' } == '${wd}\\.\\\\'
|
||||
assert existing_path('${wd}\\\\/\\.\\.\\/.') or { '' } == '${wd}\\\\/\\.\\.\\/.'
|
||||
assert existing_path('${wd}\\././/\\/oh') or { '' } == '${wd}\\././/\\/'
|
||||
return
|
||||
}
|
||||
assert existing_path('') or { '' } == ''
|
||||
assert existing_path('..') or { '' } == '..'
|
||||
assert existing_path('.') or { '' } == '.'
|
||||
assert existing_path(wd) or { '' } == wd
|
||||
assert existing_path('/') or { '' } == '/'
|
||||
assert existing_path('${wd}/does/.///not/exist///.//') or { '' } == '${wd}/'
|
||||
assert existing_path('${wd}//././/.//') or { '' } == '${wd}//././/.//'
|
||||
assert existing_path('${wd}//././/.//oh') or { '' } == '${wd}//././/.//'
|
||||
}
|
||||
|
||||
fn test_windows_volume() {
|
||||
$if windows {
|
||||
assert windows_volume('C:/path\\to/file.v') == 'C:'
|
||||
assert windows_volume('D:\\.\\') == 'D:'
|
||||
assert windows_volume('G:') == 'G:'
|
||||
assert windows_volume('G') == ''
|
||||
assert windows_volume(r'\\Host\share\files\file.v') == r'\\Host\share'
|
||||
assert windows_volume('\\\\Host\\') == ''
|
||||
assert windows_volume(r'\\.\BootPartition2\\files\.\\') == r'\\.\BootPartition2'
|
||||
assert windows_volume(r'\/.\BootPartition2\\files\.\\') == r'\/.\BootPartition2'
|
||||
assert windows_volume(r'\\\.\BootPartition2\\files\.\\') == ''
|
||||
assert windows_volume('') == ''
|
||||
assert windows_volume('\\') == ''
|
||||
assert windows_volume('/') == ''
|
||||
}
|
||||
}
|
||||
|
||||
fn test_trim_extended_length_path_prefix() {
|
||||
$if windows {
|
||||
assert trim_extended_length_path_prefix(r'\\?\C:\path\to\file.v') == r'C:\path\to\file.v'
|
||||
assert trim_extended_length_path_prefix(r'\\?\UNC\host\share\file.v') == r'\\host\share\file.v'
|
||||
assert trim_extended_length_path_prefix(r'\\?\Volume{01234567-89ab-cdef-0123-456789abcdef}\file.v') == r'\\?\Volume{01234567-89ab-cdef-0123-456789abcdef}\file.v'
|
||||
assert trim_extended_length_path_prefix(r'C:\path\to\file.v') == r'C:\path\to\file.v'
|
||||
assert trim_extended_length_path_prefix('') == ''
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
module os
|
||||
|
||||
// windows_volume returns the volume name from the given `path` on a Windows system.
|
||||
// An empty string is returned if no Windows volume is present.
|
||||
// Examples (on a Windows system):
|
||||
// ```v
|
||||
// assert os.windows_volume(r'C:\path\to\file.v') == 'C:'
|
||||
// assert os.windows_volume('D:') == 'D:'
|
||||
// assert os.windows_volume(r'\\Host\share\files\file.v') == r'\\Host\share'
|
||||
// ```
|
||||
pub fn windows_volume(path string) string {
|
||||
volume_len := win_volume_len(path)
|
||||
if volume_len == 0 {
|
||||
return empty_str
|
||||
}
|
||||
return path[..volume_len]
|
||||
}
|
||||
|
||||
// trim_extended_length_path_prefix converts Win32 extended-length DOS/UNC
|
||||
// paths returned by APIs like GetFinalPathNameByHandleW into regular paths.
|
||||
// Paths that are not standard DOS or UNC paths are left unchanged.
|
||||
fn trim_extended_length_path_prefix(path string) string {
|
||||
$if !windows {
|
||||
return path
|
||||
}
|
||||
if path.len < 4 || !starts_w_slash_slash(path) || path[2] != qmark || !is_slash(path[3]) {
|
||||
return path
|
||||
}
|
||||
if path.len >= 8 && (path[4] == `U` || path[4] == `u`) && (path[5] == `N` || path[5] == `n`)
|
||||
&& (path[6] == `C` || path[6] == `c`) && is_slash(path[7]) {
|
||||
return '\\\\' + path[8..]
|
||||
}
|
||||
if path.len >= 7 && path[4].is_letter() && path[5] == `:` && is_slash(path[6]) {
|
||||
return path[4..]
|
||||
}
|
||||
return path
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
import os
|
||||
|
||||
const tfolder = os.join_path(os.real_path(os.vtmp_dir()), 'filepath_tests')
|
||||
const original_path = os.getenv('PATH')
|
||||
|
||||
fn testsuite_begin() {
|
||||
eprintln('testsuite_begin, tfolder = ${tfolder}')
|
||||
os.rmdir_all(tfolder) or {}
|
||||
os.mkdir_all(tfolder) or { panic(err) }
|
||||
os.chdir(tfolder) or { panic(err) }
|
||||
}
|
||||
|
||||
fn testsuite_end() {
|
||||
os.chdir(os.wd_at_startup) or {}
|
||||
os.rmdir_all(tfolder) or {}
|
||||
}
|
||||
|
||||
fn test_find_abs_path_of_executable() {
|
||||
exefolder := os.join_path(tfolder, 'exe')
|
||||
os.mkdir(exefolder) or { panic(err) }
|
||||
|
||||
mut myclang_file := os.join_path(exefolder, 'myclang')
|
||||
$if windows {
|
||||
myclang_file += '.bat'
|
||||
}
|
||||
|
||||
mut mylink_file := os.join_path(tfolder, 'mylink')
|
||||
$if windows {
|
||||
mylink_file += '.bat'
|
||||
}
|
||||
|
||||
os.write_file(myclang_file, 'echo hello')!
|
||||
os.chmod(myclang_file, 0o0777)!
|
||||
dump(os.abs_path(myclang_file))
|
||||
dump(os.real_path(myclang_file))
|
||||
dump(os.is_executable(myclang_file))
|
||||
|
||||
$if windows {
|
||||
eprintln('Windows requires admin privileges in order to create symlinks, related tests will be faked.')
|
||||
os.cp(myclang_file, mylink_file) or { panic(err) }
|
||||
} $else {
|
||||
os.symlink(myclang_file, mylink_file) or { panic(err) }
|
||||
}
|
||||
|
||||
dump(os.abs_path(mylink_file))
|
||||
dump(os.real_path(mylink_file))
|
||||
dump(os.is_executable(mylink_file))
|
||||
|
||||
prepend_to_original_path(exefolder)
|
||||
assert find_and_check('myclang')? == myclang_file
|
||||
assert find_and_check('mylink') == none
|
||||
|
||||
prepend_to_original_path('.')
|
||||
assert find_and_check('mylink')? == mylink_file
|
||||
assert find_and_check('myclang') == none
|
||||
|
||||
os.chdir(exefolder) or { panic(err) }
|
||||
assert find_and_check('myclang')? == myclang_file
|
||||
assert find_and_check('mylink') == none
|
||||
|
||||
prepend_to_original_path(tfolder)
|
||||
assert find_and_check('mylink')? == mylink_file
|
||||
assert find_and_check('myclang') == none
|
||||
|
||||
restore_original_path()
|
||||
os.chdir(os.home_dir())! // Change to a *completely* different folder, just in case the original PATH contains `.`
|
||||
assert find_and_check('myclang') == none
|
||||
assert find_and_check('mylink') == none
|
||||
}
|
||||
|
||||
fn find_and_check(executable string) ?string {
|
||||
fpath := os.find_abs_path_of_executable(executable) or { return none }
|
||||
|
||||
dump(fpath)
|
||||
assert os.is_abs_path(fpath)
|
||||
|
||||
return fpath
|
||||
}
|
||||
|
||||
fn restore_original_path() {
|
||||
os.setenv('PATH', original_path, true)
|
||||
}
|
||||
|
||||
fn prepend_to_original_path(to_prepend string) {
|
||||
new_path := to_prepend + os.path_delimiter + original_path
|
||||
os.setenv('PATH', new_path, true)
|
||||
}
|
||||
@@ -0,0 +1,262 @@
|
||||
// Copyright (c) 2019-2024 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 font
|
||||
|
||||
import os
|
||||
|
||||
// Variant enumerates the different variants a font can have.
|
||||
pub enum Variant {
|
||||
normal = 0
|
||||
bold
|
||||
mono
|
||||
italic
|
||||
}
|
||||
|
||||
@[if debug_font ?]
|
||||
fn debug_font_println(s string) {
|
||||
println(s)
|
||||
}
|
||||
|
||||
fn is_readable_font_file(font_path string) bool {
|
||||
if font_path == '' {
|
||||
return false
|
||||
}
|
||||
return !font_path.to_lower().ends_with('.ttc') && os.is_file(font_path)
|
||||
&& os.is_readable(font_path)
|
||||
}
|
||||
|
||||
fn use_font_if_readable(font_path string) string {
|
||||
if is_readable_font_file(font_path) {
|
||||
debug_font_println('Using font "${font_path}"')
|
||||
return font_path
|
||||
}
|
||||
return ''
|
||||
}
|
||||
|
||||
fn find_first_font_in_paths(font_paths []string) string {
|
||||
for font_path in font_paths {
|
||||
if is_readable_font_file(font_path) {
|
||||
return font_path.clone()
|
||||
}
|
||||
}
|
||||
return ''
|
||||
}
|
||||
|
||||
fn find_first_font_in_dirs(font_dirs []string) string {
|
||||
for font_dir in font_dirs {
|
||||
if !os.is_dir(font_dir) {
|
||||
continue
|
||||
}
|
||||
mut font_paths := os.walk_ext(font_dir, '.ttf')
|
||||
font_paths.sort()
|
||||
font_path := find_first_font_in_paths(font_paths)
|
||||
if font_path != '' {
|
||||
return font_path
|
||||
}
|
||||
}
|
||||
return ''
|
||||
}
|
||||
|
||||
fn default_user_font_dirs() []string {
|
||||
mut font_dirs := []string{}
|
||||
if config_dir := os.config_dir() {
|
||||
font_dirs << os.join_path(config_dir, 'v', 'fonts')
|
||||
}
|
||||
return font_dirs
|
||||
}
|
||||
|
||||
fn bundled_font_dirs() []string {
|
||||
return [os.join_path(@VEXEROOT, 'examples', 'assets', 'fonts')]
|
||||
}
|
||||
|
||||
fn find_fc_match_font(output string) string {
|
||||
return find_first_font_in_paths(output.split('\n'))
|
||||
}
|
||||
|
||||
// default returns an absolute path to a readable default font.
|
||||
// Search order:
|
||||
// 1. The env variable `VUI_FONT`.
|
||||
// 2. A user-provided fallback font under `${os.config_dir()}/v/fonts`.
|
||||
// 3. Platform defaults and `fc-match`.
|
||||
// 4. Bundled fonts under `@VEXEROOT/examples/assets/fonts`.
|
||||
// NOTE that, in some cases, the function calls out to external OS programs
|
||||
// so running this in a hot loop is not advised.
|
||||
@[manualfree]
|
||||
pub fn default() string {
|
||||
env_font := os.getenv('VUI_FONT')
|
||||
if env_font != '' && os.is_file(env_font) && os.is_readable(env_font) {
|
||||
debug_font_println('Using font "${env_font}"')
|
||||
return env_font
|
||||
}
|
||||
unsafe { env_font.free() }
|
||||
user_font_path := find_first_font_in_dirs(default_user_font_dirs())
|
||||
if user_font_path != '' {
|
||||
debug_font_println('Using font "${user_font_path}"')
|
||||
return user_font_path
|
||||
}
|
||||
$if windows {
|
||||
fonts := ['C:\\Windows\\Fonts\\segoeui.ttf', 'C:\\Windows\\Fonts\\arial.ttf']
|
||||
for system_font in fonts {
|
||||
existing_font := use_font_if_readable(system_font)
|
||||
if existing_font != '' {
|
||||
return existing_font
|
||||
}
|
||||
}
|
||||
}
|
||||
$if macos {
|
||||
fonts := ['/System/Library/Fonts/SFNS.ttf', '/System/Library/Fonts/SFNSText.ttf',
|
||||
'/Library/Fonts/Arial.ttf']
|
||||
for system_font in fonts {
|
||||
existing_font := use_font_if_readable(system_font)
|
||||
if existing_font != '' {
|
||||
return existing_font
|
||||
}
|
||||
}
|
||||
unsafe { fonts.free() }
|
||||
}
|
||||
$if android {
|
||||
xml_files := ['/system/etc/system_fonts.xml', '/system/etc/fonts.xml',
|
||||
'/etc/system_fonts.xml', '/etc/fonts.xml', '/data/fonts/fonts.xml',
|
||||
'/etc/fallback_fonts.xml']
|
||||
font_locations := ['/system/fonts', '/data/fonts']
|
||||
for xml_file in xml_files {
|
||||
if os.is_file(xml_file) && os.is_readable(xml_file) {
|
||||
xml := os.read_file(xml_file) or { continue }
|
||||
lines := xml.split('\n')
|
||||
mut candidate_font := ''
|
||||
for line in lines {
|
||||
if line.contains('<font') {
|
||||
tmp1 := line.all_after('>')
|
||||
tmp2 := tmp1.all_before('<')
|
||||
tmp3 := tmp2.trim(' \n\t\r')
|
||||
mut_assign(candidate_font, tmp3)
|
||||
if candidate_font.contains('.ttf') {
|
||||
for location in font_locations {
|
||||
candidate_path := os.join_path_single(location, candidate_font)
|
||||
existing_font := use_font_if_readable(candidate_path)
|
||||
if existing_font != '' {
|
||||
return existing_font
|
||||
}
|
||||
unsafe { candidate_path.free() }
|
||||
}
|
||||
}
|
||||
unsafe { tmp3.free() }
|
||||
unsafe { tmp2.free() }
|
||||
unsafe { tmp1.free() }
|
||||
}
|
||||
}
|
||||
unsafe { candidate_font.free() }
|
||||
unsafe { lines.free() }
|
||||
unsafe { xml.free() }
|
||||
}
|
||||
}
|
||||
unsafe { font_locations.free() }
|
||||
unsafe { xml_files.free() }
|
||||
}
|
||||
mut fm := os.execute("fc-match --format='%{file}\n' -s")
|
||||
if fm.exit_code == 0 {
|
||||
fc_match_font_path := find_fc_match_font(fm.output)
|
||||
if fc_match_font_path != '' {
|
||||
unsafe { fm.free() }
|
||||
debug_font_println('Using font "${fc_match_font_path}"')
|
||||
return fc_match_font_path
|
||||
}
|
||||
} else {
|
||||
debug_font_println('fc-match failed to fetch system font')
|
||||
}
|
||||
unsafe { fm.free() }
|
||||
bundled_font_path := find_first_font_in_dirs(bundled_font_dirs())
|
||||
if bundled_font_path != '' {
|
||||
debug_font_println('Using font "${bundled_font_path}"')
|
||||
return bundled_font_path
|
||||
}
|
||||
panic('failed to init the font')
|
||||
}
|
||||
|
||||
// get_path_variant returns the `font_path` file name replaced with the
|
||||
// file name of the font's `variant` version if it exists.
|
||||
@[manualfree]
|
||||
pub fn get_path_variant(font_path string, variant Variant) string {
|
||||
// TODO: find some way to make this shorter and more eye-pleasant
|
||||
// NotoSans, LiberationSans, DejaVuSans, Arial and SFNS should work
|
||||
mut file := os.file_name(font_path)
|
||||
defer { unsafe { file.free() } }
|
||||
|
||||
mut fpath := font_path.replace(file, '')
|
||||
defer { unsafe { fpath.free() } }
|
||||
|
||||
mut_replace(file, '.ttf', '')
|
||||
|
||||
flower := file.to_lower()
|
||||
defer { unsafe { flower.free() } }
|
||||
|
||||
match variant {
|
||||
.normal {}
|
||||
.bold {
|
||||
if fpath.ends_with('-Regular') {
|
||||
mut_replace(file, '-Regular', '-Bold')
|
||||
} else if file.starts_with('DejaVuSans') {
|
||||
mut_plus(file, '-Bold')
|
||||
} else if flower.starts_with('arial') {
|
||||
mut_plus(file, 'bd')
|
||||
} else {
|
||||
mut_plus(file, '-bold')
|
||||
}
|
||||
$if macos {
|
||||
if os.exists('SFNS-bold') {
|
||||
mut_assign(file, 'SFNS-bold')
|
||||
}
|
||||
}
|
||||
}
|
||||
.italic {
|
||||
if file.ends_with('-Regular') {
|
||||
mut_replace(file, '-Regular', '-Italic')
|
||||
} else if file.starts_with('DejaVuSans') {
|
||||
mut_plus(file, '-Oblique')
|
||||
} else if flower.starts_with('arial') {
|
||||
mut_plus(file, 'i')
|
||||
} else {
|
||||
mut_plus(file, 'Italic')
|
||||
}
|
||||
}
|
||||
.mono {
|
||||
if !file.ends_with('Mono-Regular') && file.ends_with('-Regular') {
|
||||
mut_replace(file, '-Regular', 'Mono-Regular')
|
||||
} else if flower.starts_with('arial') {
|
||||
// Arial has no mono variant
|
||||
} else {
|
||||
mut_plus(file, 'Mono')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
res := '${fpath}${file}.ttf'
|
||||
return res
|
||||
}
|
||||
|
||||
@[manualfree]
|
||||
fn mut_replace(s &string, find string, replacement string) {
|
||||
new := (*s).replace(find, replacement)
|
||||
unsafe { s.free() }
|
||||
unsafe {
|
||||
*s = new
|
||||
}
|
||||
}
|
||||
|
||||
@[manualfree]
|
||||
fn mut_plus(s &string, tail string) {
|
||||
new := (*s) + tail
|
||||
unsafe { s.free() }
|
||||
unsafe {
|
||||
*s = new
|
||||
}
|
||||
}
|
||||
|
||||
@[manualfree]
|
||||
fn mut_assign(s &string, value string) {
|
||||
unsafe { s.free() }
|
||||
unsafe {
|
||||
*s = value.clone()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
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 'makev.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
|
||||
assert 'cmd/tools/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 'makev.bat' in matches
|
||||
}
|
||||
|
||||
fn test_glob_not_found() {
|
||||
os.glob('an_unknown_folder/*.v') or {
|
||||
assert true
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
// Copyright (c) 2019-2024 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 struct FileMode {
|
||||
pub:
|
||||
typ FileType
|
||||
owner FilePermission
|
||||
group FilePermission
|
||||
others FilePermission
|
||||
}
|
||||
|
||||
pub struct FileInfo {
|
||||
FileMode
|
||||
pub:
|
||||
size u64 // size of the file in bytes
|
||||
mtime i64 // last modification time in seconds after the Unix epoch
|
||||
}
|
||||
|
||||
pub enum FileType {
|
||||
unknown
|
||||
regular
|
||||
directory
|
||||
character_device
|
||||
block_device
|
||||
fifo
|
||||
symbolic_link
|
||||
socket
|
||||
}
|
||||
|
||||
pub struct FilePermission {
|
||||
pub:
|
||||
read bool
|
||||
write bool
|
||||
execute bool
|
||||
}
|
||||
|
||||
// bitmask returns a 3 bit sequence in the order RWE, where the bit is set to 1 if the value is true or 0 otherwise.
|
||||
pub fn (p FilePermission) bitmask() u32 {
|
||||
mut mask := u32(0)
|
||||
if p.read {
|
||||
mask |= 4
|
||||
}
|
||||
if p.write {
|
||||
mask |= 2
|
||||
}
|
||||
if p.execute {
|
||||
mask |= 1
|
||||
}
|
||||
return mask
|
||||
}
|
||||
|
||||
// bitmask returns a 9 bit sequence in the order owner + group + others.
|
||||
// This is a valid bitmask to use with `os.chmod`.
|
||||
pub fn (m FileMode) bitmask() u32 {
|
||||
return m.owner.bitmask() << 6 | m.group.bitmask() << 3 | m.others.bitmask()
|
||||
}
|
||||
|
||||
// inode returns the metadata of the file/inode, containing inode type, permission information, size and modification time.
|
||||
// It supports Windows for regular files, but it doesn't matter if you use owner, group or others when checking permissions on Windows.
|
||||
// if a symlink is targeted, it returns info on the link, not the target.
|
||||
pub fn inode(path string) FileInfo {
|
||||
attr := lstat(path) or { Stat{} }
|
||||
fm := attr.get_mode()
|
||||
return FileInfo{
|
||||
typ: fm.typ
|
||||
owner: fm.owner
|
||||
group: fm.group
|
||||
others: fm.others
|
||||
size: attr.size
|
||||
mtime: attr.mtime
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
import os
|
||||
|
||||
// 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.
|
||||
const tfolder = os.join_path(os.vtmp_dir(), 'inode_tests')
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
fn testsuite_end() {
|
||||
os.chdir(os.wd_at_startup) or {}
|
||||
os.rmdir_all(tfolder) or { panic(err) }
|
||||
assert !os.is_dir(tfolder)
|
||||
}
|
||||
|
||||
fn test_inode_file_type() {
|
||||
filename := './test1.txt'
|
||||
mut file := os.open_file(filename, 'w', 0o600) or { return }
|
||||
file.close()
|
||||
mode := os.inode(filename)
|
||||
os.rm(filename) or { panic(err) }
|
||||
assert mode.typ == .regular
|
||||
}
|
||||
|
||||
fn test_inode_file_owner_permission() {
|
||||
filename := './test2.txt'
|
||||
mut file := os.open_file(filename, 'w', 0o600) or { return }
|
||||
file.close()
|
||||
mode := os.inode(filename)
|
||||
os.rm(filename) or {}
|
||||
assert mode.owner.read
|
||||
assert mode.owner.write
|
||||
assert !mode.owner.execute
|
||||
}
|
||||
|
||||
fn test_inode_file_permissions_bitmask() {
|
||||
if os.user_os() == 'windows' {
|
||||
println('> skipping ${@FN} on windows')
|
||||
return
|
||||
}
|
||||
filename := './test3.txt'
|
||||
mut file := os.open_file(filename, 'w', 0o641) or { return }
|
||||
file.close()
|
||||
mode := os.inode(filename)
|
||||
os.rm(filename) or {}
|
||||
assert mode.bitmask() == 0o641
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
import os
|
||||
|
||||
fn test_join_path() {
|
||||
assert os.join_path('', '', '') == ''
|
||||
assert os.join_path('', '') == ''
|
||||
assert os.join_path('') == ''
|
||||
assert os.join_path('b', '', '') == 'b'
|
||||
assert os.join_path('b', '') == 'b'
|
||||
assert os.join_path('b') == 'b'
|
||||
assert os.join_path('', '', './b') == 'b'
|
||||
assert os.join_path('', '', '/b') == 'b'
|
||||
assert os.join_path('', '', 'b') == 'b'
|
||||
assert os.join_path('', './b') == 'b'
|
||||
assert os.join_path('', '/b') == 'b'
|
||||
assert os.join_path('', 'b') == 'b'
|
||||
assert os.join_path('b', '') == 'b'
|
||||
$if windows {
|
||||
assert os.join_path('./b', '') == r'.\b'
|
||||
assert os.join_path('/b', '') == r'\b'
|
||||
assert os.join_path('./a', './b') == r'.\a\b'
|
||||
assert os.join_path('/', 'test') == r'\test'
|
||||
assert os.join_path('v', 'vlib', 'os') == r'v\vlib\os'
|
||||
assert os.join_path('', 'f1', 'f2') == r'f1\f2'
|
||||
assert os.join_path('v', '', 'dir') == r'v\dir'
|
||||
assert os.join_path(r'foo\bar', r'.\file.txt') == r'foo\bar\file.txt'
|
||||
assert os.join_path('foo/bar', './file.txt') == r'foo\bar\file.txt'
|
||||
assert os.join_path('/opt/v', './x') == r'\opt\v\x'
|
||||
assert os.join_path('v', 'foo/bar', 'dir') == r'v\foo\bar\dir'
|
||||
assert os.join_path('v', 'foo/bar\\baz', '/dir') == r'v\foo\bar\baz\dir'
|
||||
assert os.join_path('C:', 'f1\\..', 'f2') == r'C:\f1\..\f2'
|
||||
} $else {
|
||||
assert os.join_path('./b', '') == './b'
|
||||
assert os.join_path('/b', '') == '/b'
|
||||
assert os.join_path('./a', './b') == './a/b'
|
||||
assert os.join_path('/', 'test') == '/test'
|
||||
assert os.join_path('v', 'vlib', 'os') == 'v/vlib/os'
|
||||
assert os.join_path('', 'f1', 'f2') == 'f1/f2'
|
||||
assert os.join_path('v', '', 'dir') == 'v/dir'
|
||||
assert os.join_path('/foo/bar', './file.txt') == '/foo/bar/file.txt'
|
||||
assert os.join_path('foo/bar', './file.txt') == 'foo/bar/file.txt'
|
||||
assert os.join_path('/opt/v', './x') == '/opt/v/x'
|
||||
assert os.join_path('/foo/bar', './.././file.txt') == '/foo/bar/../file.txt'
|
||||
assert os.join_path('v', 'foo/bar\\baz', '/dir') == r'v/foo/bar/baz/dir'
|
||||
}
|
||||
}
|
||||
|
||||
fn test_join_path_single() {
|
||||
assert os.join_path_single('', '') == ''
|
||||
assert os.join_path_single('', './b') == 'b'
|
||||
assert os.join_path_single('', '/b') == 'b'
|
||||
assert os.join_path_single('', 'b') == 'b'
|
||||
assert os.join_path_single('b', '') == 'b'
|
||||
$if windows {
|
||||
assert os.join_path_single('./b', '') == r'.\b'
|
||||
assert os.join_path_single('/b', '') == r'\b'
|
||||
assert os.join_path_single('/foo/bar', './file.txt') == r'\foo\bar\file.txt'
|
||||
assert os.join_path_single('/', 'test') == r'\test'
|
||||
assert os.join_path_single('foo\\bar', '.\\file.txt') == r'foo\bar\file.txt'
|
||||
assert os.join_path_single('/opt/v', './x') == r'\opt\v\x'
|
||||
} $else {
|
||||
assert os.join_path_single('./b', '') == r'./b'
|
||||
assert os.join_path_single('/b', '') == r'/b'
|
||||
assert os.join_path_single('/foo/bar', './file.txt') == '/foo/bar/file.txt'
|
||||
assert os.join_path_single('/', 'test') == '/test'
|
||||
assert os.join_path_single('foo/bar', './file.txt') == 'foo/bar/file.txt'
|
||||
assert os.join_path_single('/opt/v', './x') == '/opt/v/x'
|
||||
assert os.join_path_single('./a', './b') == './a/b'
|
||||
assert os.join_path_single('a', './b') == 'a/b'
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,227 @@
|
||||
module notify
|
||||
|
||||
import time
|
||||
import os
|
||||
|
||||
#insert "@VEXEROOT/vlib/os/notify/kqueue.h"
|
||||
|
||||
pub struct C.kevent {
|
||||
mut:
|
||||
ident u32
|
||||
filter i16
|
||||
flags u16
|
||||
fflags u32
|
||||
data int
|
||||
udata voidptr
|
||||
}
|
||||
|
||||
fn C.kqueue() i32
|
||||
fn C.__kevent__(i32, voidptr, i32, voidptr, i32, voidptr) i32
|
||||
fn C.EV_SET(voidptr, u32, i16, u16, u32, i32, voidptr)
|
||||
|
||||
// KqueueNotifier provides methods that implement FdNotifier using the
|
||||
// kqueue I/O event notification facility (macos, freeBSD, xxxBSD...unix only)
|
||||
struct KqueueNotifier {
|
||||
kqueue_fd int
|
||||
}
|
||||
|
||||
// KqueueEvent describes an event that occurred for a file descriptor in
|
||||
// the watch list
|
||||
struct KqueueEvent {
|
||||
pub:
|
||||
fd int
|
||||
kind FdEventType
|
||||
}
|
||||
|
||||
// new creates a new KqueueNotifier.
|
||||
// The FdNotifier interface is returned to allow OS specific
|
||||
// implementations without exposing the concrete type
|
||||
pub fn new() !FdNotifier {
|
||||
fd := C.kqueue()
|
||||
if fd == -1 {
|
||||
return error(os.posix_get_error_msg(C.errno))
|
||||
}
|
||||
// Needed to circumvent V limitations
|
||||
x := &KqueueNotifier{
|
||||
kqueue_fd: fd
|
||||
}
|
||||
return x
|
||||
}
|
||||
|
||||
// filter types
|
||||
const kqueue_read = i16(C.EVFILT_READ)
|
||||
const kqueue_write = i16(C.EVFILT_WRITE)
|
||||
const kqueue_aio = i16(C.EVFILT_AIO)
|
||||
const kqueue_vnode = i16(C.EVFILT_VNODE)
|
||||
const kqueue_proc = i16(C.EVFILT_PROC)
|
||||
const kqueue_signal = i16(C.EVFILT_SIGNAL)
|
||||
const kqueue_timer = i16(C.EVFILT_TIMER)
|
||||
const kqueue_machport = i16(C.EVFILT_MACHPORT)
|
||||
const kqueue_fs = i16(C.EVFILT_FS)
|
||||
const kqueue_user = i16(C.EVFILT_USER)
|
||||
const kqueue_vm = i16(C.EVFILT_VM)
|
||||
const kqueue_exception = i16(C.EVFILT_EXCEPT)
|
||||
const kqueue_syscount = i16(C.EVFILT_SYSCOUNT)
|
||||
|
||||
// actions
|
||||
const kqueue_add = u16(C.EV_ADD)
|
||||
const kqueue_delete = u16(C.EV_DELETE)
|
||||
const kqueue_enable = u16(C.EV_ENABLE)
|
||||
const kqueue_disable = u16(C.EV_DISABLE)
|
||||
|
||||
// flags
|
||||
const kqueue_oneshot = u16(C.EV_ONESHOT)
|
||||
const kqueue_edge_trigger = u16(C.EV_CLEAR) // kqueue_clear
|
||||
|
||||
const kqueue_receipt = u16(C.EV_RECEIPT)
|
||||
const kqueue_dispatch = u16(C.EV_DISPATCH)
|
||||
const kqueue_udata_specific = u16(C.EV_UDATA_SPECIFIC)
|
||||
const kqueue_dispatch2 = u16(C.EV_DISPATCH | C.EV_UDATA_SPECIFIC)
|
||||
const kqueue_vanished = u16(C.EV_VANISHED)
|
||||
const kqueue_sysflags = u16(C.EV_SYSFLAGS)
|
||||
const kqueue_flag0 = u16(C.EV_FLAG0)
|
||||
const kqueue_flag1 = u16(C.EV_FLAG1)
|
||||
|
||||
// returned values
|
||||
const kqueue_eof = u16(C.EV_EOF)
|
||||
const kqueue_error = u16(C.EV_ERROR)
|
||||
|
||||
// ctl is a helper method for add, modify, and remove
|
||||
fn (mut kn KqueueNotifier) ctl(fd int, filter i16, flags u16) ! {
|
||||
event := [1]C.kevent{}
|
||||
C.EV_SET(&event[0], fd, filter, flags, 0, 0, unsafe { nil })
|
||||
if C.__kevent__(kn.kqueue_fd, &event[0], 1, unsafe { nil }, 0, unsafe { nil }) == -1 {
|
||||
return error(os.posix_get_error_msg(C.errno))
|
||||
}
|
||||
}
|
||||
|
||||
// add adds a file descriptor to the watch list
|
||||
fn (mut kn KqueueNotifier) add(fd int, events FdEventType, conf ...FdConfigFlags) ! {
|
||||
filter := filter_to_mask(events)
|
||||
flags := flags_to_mask(...conf)
|
||||
kn.ctl(fd, filter, flags)!
|
||||
}
|
||||
|
||||
// modify sets an existing entry in the watch list to the provided events and configuration
|
||||
fn (mut kn KqueueNotifier) modify(fd int, events FdEventType, conf ...FdConfigFlags) ! {
|
||||
kn.add(fd, events, ...conf)!
|
||||
}
|
||||
|
||||
// remove removes a file descriptor from the watch list
|
||||
fn (mut kn KqueueNotifier) remove(fd int) ! {
|
||||
filter := kqueue_read | kqueue_write | kqueue_exception
|
||||
flags := kqueue_delete
|
||||
kn.ctl(fd, filter, flags)!
|
||||
}
|
||||
|
||||
// wait waits to be notified of events on the watch list,
|
||||
// returns at most 512 events
|
||||
fn (mut kn KqueueNotifier) 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 KqueueNotifier thread safe
|
||||
events := [512]C.kevent{}
|
||||
// populate events with the new events
|
||||
to := &C.timespec{0, timeout.nanoseconds()}
|
||||
count := C.__kevent__(kn.kqueue_fd, unsafe { nil }, 0, &events[0], events.len, to)
|
||||
|
||||
if count > 0 {
|
||||
mut arr := []FdEvent{cap: count}
|
||||
for i := 0; i < count; i++ {
|
||||
fd := int(events[i].ident)
|
||||
kind := event_mask_to_flag(events[i].filter, events[i].flags)
|
||||
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 << &KqueueEvent{
|
||||
fd: fd
|
||||
kind: kind
|
||||
}
|
||||
}
|
||||
return arr
|
||||
}
|
||||
return []
|
||||
}
|
||||
|
||||
// close closes the KqueueNotifier,
|
||||
// any successive calls to add, modify, remove, and wait should fail
|
||||
fn (mut kn KqueueNotifier) close() ! {
|
||||
if C.close(kn.kqueue_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 kevent() wait to FdEventType
|
||||
fn event_mask_to_flag(filter i16, flags u16) FdEventType {
|
||||
mut res := unsafe { FdEventType(0) }
|
||||
|
||||
if filter & kqueue_read != 0 {
|
||||
res.set(.read)
|
||||
}
|
||||
if filter & kqueue_write != 0 {
|
||||
res.set(.write)
|
||||
}
|
||||
if filter & kqueue_exception != 0 {
|
||||
res.set(.exception)
|
||||
}
|
||||
|
||||
if flags & kqueue_eof != 0 {
|
||||
res.set(.hangup)
|
||||
}
|
||||
if flags & kqueue_error != 0 {
|
||||
res.set(.error)
|
||||
}
|
||||
|
||||
return res
|
||||
}
|
||||
|
||||
// filter_to_mask is a helper function that converts FdEventType
|
||||
// to a bitmask used by the C functions
|
||||
fn filter_to_mask(events FdEventType) i16 {
|
||||
mut mask := i16(0)
|
||||
if events.has(.read) {
|
||||
mask |= kqueue_read
|
||||
}
|
||||
if events.has(.write) {
|
||||
mask |= kqueue_write
|
||||
}
|
||||
if events.has(.exception) {
|
||||
mask |= kqueue_exception
|
||||
}
|
||||
if events.has(.peer_hangup) {
|
||||
panic("Kqueue does not support 'peer_hangup' event type.")
|
||||
}
|
||||
if events.has(.error) {
|
||||
panic("Kqueue does not support 'error' event type.")
|
||||
}
|
||||
if events.has(.hangup) {
|
||||
panic("Kqueue does not support 'hangup' event type.")
|
||||
}
|
||||
return mask
|
||||
}
|
||||
|
||||
// flags_to_mask is a helper function that converts FdConfigFlags
|
||||
// to a bitmask used by the C functions
|
||||
fn flags_to_mask(confs ...FdConfigFlags) u16 {
|
||||
mut mask := kqueue_add | kqueue_enable
|
||||
for conf in confs {
|
||||
if conf.has(.edge_trigger) {
|
||||
mask |= kqueue_edge_trigger
|
||||
}
|
||||
if conf.has(.one_shot) {
|
||||
mask |= kqueue_oneshot
|
||||
}
|
||||
if conf.has(.wake_up) {
|
||||
panic("Kqueue does not support 'wake_up' flag.")
|
||||
}
|
||||
if conf.has(.exclusive) {
|
||||
panic("Kqueue does not support 'exclusive' flag.")
|
||||
}
|
||||
}
|
||||
return mask
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
module notify
|
||||
|
||||
// new returns an error when the current platform has no notifier backend.
|
||||
pub fn new() !FdNotifier {
|
||||
return error('os.notify is not supported on this platform')
|
||||
}
|
||||
@@ -0,0 +1,202 @@
|
||||
module notify
|
||||
|
||||
import time
|
||||
import os
|
||||
|
||||
#include <sys/epoll.h>
|
||||
|
||||
pub 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(i32) i32
|
||||
|
||||
fn C.epoll_ctl(i32, i32, i32, &C.epoll_event) i32
|
||||
|
||||
fn C.epoll_wait(i32, &C.epoll_event, i32, i32) i32
|
||||
|
||||
// EpollNotifier provides methods that implement FdNotifier using the
|
||||
// epoll I/O event notification facility (linux only)
|
||||
struct EpollNotifier {
|
||||
epoll_fd int
|
||||
}
|
||||
|
||||
// EpollEvent describes an event that occurred for a file descriptor in
|
||||
// the watch list
|
||||
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)
|
||||
const epoll_write = u32(C.EPOLLOUT)
|
||||
const epoll_peer_hangup = u32(C.EPOLLRDHUP)
|
||||
const epoll_exception = u32(C.EPOLLPRI)
|
||||
const epoll_error = u32(C.EPOLLERR)
|
||||
const epoll_hangup = u32(C.EPOLLHUP)
|
||||
const epoll_edge_trigger = u32(C.EPOLLET)
|
||||
const epoll_one_shot = u32(C.EPOLLONESHOT)
|
||||
const epoll_wake_up = u32(C.EPOLLWAKEUP)
|
||||
const 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 := unsafe { FdEventType(0) }
|
||||
|
||||
if mask & epoll_read != 0 {
|
||||
flags.set(.read)
|
||||
}
|
||||
if mask & epoll_write != 0 {
|
||||
flags.set(.write)
|
||||
}
|
||||
if mask & epoll_peer_hangup != 0 {
|
||||
flags.set(.peer_hangup)
|
||||
}
|
||||
if mask & epoll_exception != 0 {
|
||||
flags.set(.exception)
|
||||
}
|
||||
if mask & epoll_error != 0 {
|
||||
flags.set(.error)
|
||||
}
|
||||
if mask & 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 |= epoll_read
|
||||
}
|
||||
if events.has(.write) {
|
||||
mask |= epoll_write
|
||||
}
|
||||
if events.has(.peer_hangup) {
|
||||
mask |= epoll_peer_hangup
|
||||
}
|
||||
if events.has(.exception) {
|
||||
mask |= epoll_exception
|
||||
}
|
||||
if events.has(.error) {
|
||||
mask |= epoll_error
|
||||
}
|
||||
if events.has(.hangup) {
|
||||
mask |= epoll_hangup
|
||||
}
|
||||
for conf in confs {
|
||||
if conf.has(.edge_trigger) {
|
||||
mask |= epoll_edge_trigger
|
||||
}
|
||||
if conf.has(.one_shot) {
|
||||
mask |= epoll_one_shot
|
||||
}
|
||||
if conf.has(.wake_up) {
|
||||
mask |= epoll_wake_up
|
||||
}
|
||||
if conf.has(.exclusive) {
|
||||
mask |= epoll_exclusive
|
||||
}
|
||||
}
|
||||
return mask
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
#ifndef __KQUEUE_H
|
||||
#define __KQUEUE_H
|
||||
|
||||
#include <sys/event.h>
|
||||
|
||||
// Due to the renaming of 'struct kevent' and function 'kevent',
|
||||
// they are wrapped here to avoid conflicts.
|
||||
int __kevent__(int handle, const struct kevent* changelist, int nchanges, struct kevent* eventlist, int nevents, const struct timespec* timeout) {
|
||||
return kevent(handle, changelist, nchanges, eventlist, nevents, timeout);
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,36 @@
|
||||
module notify
|
||||
|
||||
import time
|
||||
|
||||
// Backends should provide a `new()?FdNotifier` function
|
||||
pub interface FdNotifier {
|
||||
mut:
|
||||
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
|
||||
}
|
||||
@@ -0,0 +1,169 @@
|
||||
// vtest flaky: true
|
||||
// vtest retry: 3
|
||||
import os
|
||||
import os.notify
|
||||
|
||||
fn test_level_trigger() {
|
||||
// currently only linux and macos are supported
|
||||
$if linux || macos {
|
||||
mut notifier := notify.new()!
|
||||
mut pipe := os.pipe()!
|
||||
defer {
|
||||
pipe.close()
|
||||
notifier.close() or {}
|
||||
}
|
||||
notifier.add(pipe.read_fd, .read)!
|
||||
|
||||
pipe.write_string('foobar')!
|
||||
mut n := ¬ifier
|
||||
check_read_event(mut n, pipe.read_fd, 'foo')
|
||||
check_read_event(mut n, pipe.read_fd, 'bar')
|
||||
|
||||
assert notifier.wait(0).len == 0
|
||||
}
|
||||
}
|
||||
|
||||
fn test_edge_trigger() {
|
||||
// currently only linux and macos are supported
|
||||
$if linux || macos {
|
||||
mut notifier := notify.new()!
|
||||
mut pipe := os.pipe()!
|
||||
defer {
|
||||
pipe.close()
|
||||
notifier.close() or {}
|
||||
}
|
||||
notifier.add(pipe.read_fd, .read, .edge_trigger)!
|
||||
|
||||
mut n := ¬ifier
|
||||
|
||||
pipe.write_string('foobar')!
|
||||
check_read_event(mut n, pipe.read_fd, 'foo')
|
||||
|
||||
$if linux {
|
||||
assert notifier.wait(0).len == 0
|
||||
}
|
||||
$if macos {
|
||||
/*
|
||||
In the kqueue of macos, EV_CLEAR flag represents a clear event,
|
||||
which is mainly used for pipeline and socket class events. When this flag is set,
|
||||
kqueue will trigger the corresponding event when the data is readable or writable,
|
||||
but it is not guaranteed that the event will only be triggered once.
|
||||
Compared to EPOLLET, EV_CLEAR's behavior varies. In epoll, the edge triggered mode only triggers
|
||||
an event once when the state changes from unreadable/non writable to readable/writable,
|
||||
that is, when the data changes from unreadable to readable,
|
||||
or when the data changes from unreadable to writable. In the kqueue of macos,
|
||||
EV_CLEAR does not possess this precise edge triggering behavior.
|
||||
Therefore, in the kqueue of macos, even if the data is not completely read,
|
||||
it is possible to continue triggering read events. This means that if you don't process all the data,
|
||||
the next kqueue event notification may still be triggered
|
||||
*/
|
||||
|
||||
// notifier.wait(0).len == 1 or 0
|
||||
}
|
||||
|
||||
pipe.write_string('baz')!
|
||||
check_read_event(mut n, pipe.read_fd, 'barbaz')
|
||||
// we do not get an event because there is still data
|
||||
// to be read
|
||||
// assert notifier.wait(0).len == 0
|
||||
// TODO: investigage why the above assert suddenly started failing on the latest Ubuntu kernel update:
|
||||
// 5.11.0-37-generic #41~20.04.2-Ubuntu SMP Fri Sep 24 09:06:38 UTC 2021 x86_64 x86_64 x86_64 GNU/Linux
|
||||
}
|
||||
}
|
||||
|
||||
fn test_one_shot() {
|
||||
$if linux || macos {
|
||||
mut notifier := notify.new()!
|
||||
mut pipe := os.pipe()!
|
||||
defer {
|
||||
pipe.close()
|
||||
notifier.close() or {}
|
||||
}
|
||||
notifier.add(pipe.read_fd, .read, .one_shot)!
|
||||
|
||||
mut n := ¬ifier
|
||||
|
||||
pipe.write_string('foobar')!
|
||||
check_read_event(mut n, pipe.read_fd, 'foo')
|
||||
pipe.write_string('baz')!
|
||||
|
||||
assert notifier.wait(0).len == 0
|
||||
|
||||
// rearm
|
||||
notifier.modify(pipe.read_fd, .read)!
|
||||
check_read_event(mut n, pipe.read_fd, 'barbaz')
|
||||
}
|
||||
}
|
||||
|
||||
// Kqueue does not support 'hangup' event type.
|
||||
fn test_hangup() {
|
||||
$if linux {
|
||||
mut notifier := notify.new()!
|
||||
mut pipe := os.pipe()!
|
||||
defer {
|
||||
os.fd_close(pipe.read_fd)
|
||||
notifier.close() or {}
|
||||
}
|
||||
notifier.add(pipe.read_fd, .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(pipe.write_fd)
|
||||
events := notifier.wait(0)
|
||||
assert events.len == 1
|
||||
assert events[0].fd == pipe.read_fd
|
||||
assert events[0].kind.has(.hangup)
|
||||
}
|
||||
}
|
||||
|
||||
fn test_write() {
|
||||
$if linux || macos {
|
||||
mut notifier := notify.new()!
|
||||
mut pipe := os.pipe()!
|
||||
defer {
|
||||
pipe.close()
|
||||
notifier.close() or {}
|
||||
}
|
||||
|
||||
notifier.add(pipe.read_fd, .write)!
|
||||
assert notifier.wait(0).len == 0
|
||||
|
||||
notifier.add(pipe.write_fd, .write)!
|
||||
events := notifier.wait(0)
|
||||
assert events.len == 1
|
||||
assert events[0].fd == pipe.write_fd
|
||||
assert events[0].kind.has(.write)
|
||||
}
|
||||
}
|
||||
|
||||
fn test_remove() {
|
||||
$if linux || macos {
|
||||
mut notifier := notify.new()!
|
||||
mut pipe := os.pipe()!
|
||||
defer {
|
||||
pipe.close()
|
||||
notifier.close() or {}
|
||||
}
|
||||
|
||||
// level triggered - will keep getting events while
|
||||
// there is data to read
|
||||
notifier.add(pipe.read_fd, .read)!
|
||||
pipe.write_string('foobar')!
|
||||
assert notifier.wait(0).len == 1
|
||||
assert notifier.wait(0).len == 1
|
||||
|
||||
notifier.remove(pipe.read_fd)!
|
||||
assert notifier.wait(0).len == 0
|
||||
}
|
||||
}
|
||||
|
||||
fn check_read_event(mut 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
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
// vtest build: present_node?
|
||||
import os
|
||||
|
||||
fn test_read_from_file() {
|
||||
mut buf := []u8{len: 10}
|
||||
f := os.open(@FILE)!
|
||||
n := f.read(mut &buf)!
|
||||
println(buf[..n])
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
module os
|
||||
|
||||
// open_uri opens a given uri.
|
||||
pub fn open_uri(uri string) ! {
|
||||
mut vopen_uri_cmd := getenv('VOPEN_URI_CMD')
|
||||
if vopen_uri_cmd == '' {
|
||||
$if macos {
|
||||
vopen_uri_cmd = 'open'
|
||||
} $else $if freebsd || openbsd {
|
||||
vopen_uri_cmd = 'xdg-open'
|
||||
} $else $if linux {
|
||||
providers := ['xdg-open', 'x-www-browser', 'www-browser', 'wslview', 'exo-open']
|
||||
// There are multiple possible providers to open a browser on linux
|
||||
// One of them is xdg-open, another is x-www-browser, then there's www-browser, etc.
|
||||
// Look for one that exists and run it
|
||||
for provider in providers {
|
||||
if exists_in_system_path(provider) {
|
||||
vopen_uri_cmd = provider
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if vopen_uri_cmd == '' {
|
||||
return error('unsupported platform')
|
||||
}
|
||||
result := execute('${vopen_uri_cmd} "${uri}"')
|
||||
if result.exit_code != 0 {
|
||||
return error('unable to open url: ${result.output}')
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
module os
|
||||
|
||||
import dl
|
||||
|
||||
type ShellExecuteWin = fn (voidptr, &u16, &u16, &u16, &u16, int)
|
||||
|
||||
// open_uri opens a given uri.
|
||||
pub fn open_uri(uri string) ! {
|
||||
mut vopen_uri_cmd := getenv('VOPEN_URI_CMD')
|
||||
if vopen_uri_cmd != '' {
|
||||
result := execute('${vopen_uri_cmd} "${uri}"')
|
||||
if result.exit_code != 0 {
|
||||
return error('unable to open url: ${result.output}')
|
||||
}
|
||||
return
|
||||
}
|
||||
handle := dl.open_opt('shell32', dl.rtld_now)!
|
||||
// https://docs.microsoft.com/en-us/windows/win32/api/shellapi/nf-shellapi-shellexecutew
|
||||
func := ShellExecuteWin(dl.sym_opt(handle, 'ShellExecuteW')!)
|
||||
// 1 is SW_SHOWNORMAL, but it avoids including winuser.h, and in turn windows.h, which conflicts with raylib
|
||||
func(C.NULL, 'open'.to_wide(), uri.to_wide(), C.NULL, C.NULL, 1)
|
||||
dl.close(handle)
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,207 @@
|
||||
module os
|
||||
|
||||
$if js_node {
|
||||
#var $fs = require('fs');
|
||||
#var $path = require('path');
|
||||
#var tty = require('tty')
|
||||
}
|
||||
|
||||
pub const path_delimiter = get_path_delimiter()
|
||||
pub const path_separator = get_path_separator()
|
||||
pub const path_devnull = '/dev/null' // TODO
|
||||
|
||||
pub const args = []string{}
|
||||
|
||||
const executable_suffixes = ['']
|
||||
|
||||
fn get_path_delimiter() string {
|
||||
delimiter := ':'
|
||||
$if js_node {
|
||||
#delimiter.str = $path.delimiter
|
||||
}
|
||||
return delimiter
|
||||
}
|
||||
|
||||
fn get_path_separator() string {
|
||||
separator := '/'
|
||||
$if js_node {
|
||||
#separator.str = $path.sep
|
||||
}
|
||||
return separator
|
||||
}
|
||||
|
||||
fn init() {
|
||||
$if js_node {
|
||||
#$process.argv.forEach(function(val,index) { os__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
|
||||
// Note: 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('')
|
||||
}
|
||||
}
|
||||
|
||||
pub fn getpid() int {
|
||||
res := 0
|
||||
#res.val = $process.pid
|
||||
|
||||
return res
|
||||
}
|
||||
|
||||
// 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 {
|
||||
#try {
|
||||
#$fs.chmodSync(''+path,mode.valueOf())
|
||||
#} catch (error) {
|
||||
#return error_with_code(new string("chmod failed: " + error.message),new int(error.code))
|
||||
#}
|
||||
} $else {
|
||||
return error('os.chmod() is available only for NodeJS')
|
||||
}
|
||||
}
|
||||
|
||||
// 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 {
|
||||
#try {
|
||||
#$fs.chownSync(''+path,owner.valueOf(),group.valueOf())
|
||||
#} catch (error) { return error_with_code(new string("chown failed: " + error.message),new int(error.code)) }
|
||||
} $else {
|
||||
return error('os.chown() is available only for NodeJS')
|
||||
}
|
||||
}
|
||||
|
||||
pub fn temp_dir() string {
|
||||
mut res := ''
|
||||
$if js_node {
|
||||
#res = new string($os.tmpdir())
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
pub fn home_dir() string {
|
||||
mut res := ''
|
||||
$if js_node {
|
||||
#res = new 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 join_path_single(base string, elem string) string {
|
||||
// TODO: deprecate this
|
||||
return join_path(base, elem)
|
||||
}
|
||||
|
||||
pub fn execute(cmd string) Result {
|
||||
mut exit_code := 0
|
||||
mut out := ''
|
||||
#let commands = cmd.str.split(' ');
|
||||
#let output = $child_process.spawnSync(commands[0],commands.slice(1,commands.length));
|
||||
#exit_code = new int(output.status)
|
||||
#out = new string(output.stdout + '')
|
||||
|
||||
return Result{
|
||||
exit_code: exit_code
|
||||
output: out
|
||||
}
|
||||
}
|
||||
|
||||
// exec starts the specified command with arguments, waits for it to complete, and returns its output.
|
||||
pub fn exec(args []string) Result {
|
||||
if args.len == 0 {
|
||||
return Result{
|
||||
exit_code: -1
|
||||
output: 'exec requires at least one argument'
|
||||
}
|
||||
}
|
||||
mut exit_code := 0
|
||||
mut out := ''
|
||||
#let commands = args.arr.map((x) => x.valueOf() + '');
|
||||
#let output = $child_process.spawnSync(commands[0], commands.slice(1, commands.length));
|
||||
#exit_code = new int(output.status === null ? -1 : output.status)
|
||||
#out = new string((output.stdout ? output.stdout + '' : '') + (output.stderr ? output.stderr + '' : ''))
|
||||
|
||||
return Result{
|
||||
exit_code: exit_code
|
||||
output: out
|
||||
}
|
||||
}
|
||||
|
||||
pub fn system(cmd string) int {
|
||||
exit_code := 0
|
||||
#let commands = cmd.str.split(' ');
|
||||
#exit_code.val = $child_process.execSync(commands[0],commands.slice(1,commands.length));
|
||||
|
||||
return exit_code
|
||||
}
|
||||
|
||||
pub fn is_atty(fd int) int {
|
||||
res := 0
|
||||
#res.val = +tty.isatty(fd.val)
|
||||
|
||||
return res
|
||||
}
|
||||
|
||||
pub fn glob(patterns ...string) ![]string {
|
||||
panic('not yet implemented')
|
||||
return error('not yet implemented')
|
||||
}
|
||||
|
||||
pub fn write_file_array(path string, buffer array) ! {
|
||||
mut f := create(path)!
|
||||
f.write_array(buffer)!
|
||||
f.close()
|
||||
}
|
||||
|
||||
pub fn chdir(s string) ! {
|
||||
#try { $process.chdir(s.str); } catch (e) { return error(new string('' + s)) }
|
||||
}
|
||||
|
||||
pub fn file_last_mod_unix(path string) int {
|
||||
mtime := 0
|
||||
#mtime.val = Math.floor($fs.lstatSync(path.str).mtime.getTime() / 1000)
|
||||
|
||||
return mtime
|
||||
}
|
||||
|
||||
pub fn ensure_folder_is_writable(path string) ! {
|
||||
fpath := join_path(path, 'some_newfile')
|
||||
#try { $fs.writeFileSync(fpath); $fs.unlinkSync(fpath) } catch(e) { return error(new string('could not write to ' + path)) }
|
||||
|
||||
_ := fpath
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,111 @@
|
||||
module os
|
||||
|
||||
#include <android/asset_manager.h>
|
||||
#include <android/asset_manager_jni.h>
|
||||
#include <android/native_activity.h>
|
||||
|
||||
pub enum AssetMode {
|
||||
buffer = C.AASSET_MODE_BUFFER // Caller plans to ask for a read-only buffer with all data.
|
||||
random = C.AASSET_MODE_RANDOM // Read chunks, and seek forward and backward.
|
||||
streaming = C.AASSET_MODE_STREAMING // Read sequentially, with an occasional forward seek.
|
||||
unknown = C.AASSET_MODE_UNKNOWN // No specific information about how data will be accessed.
|
||||
}
|
||||
|
||||
// See https://developer.android.com/ndk/reference/struct/a-native-activity for more info.
|
||||
pub struct C.ANativeActivity {
|
||||
pub:
|
||||
assetManager &AssetManager = unsafe { nil } // Pointer to the Asset Manager instance for the application.
|
||||
clazz voidptr // (jobject) The NativeActivity object handle.
|
||||
env voidptr // (JNIEnv *) JNI context for the main thread of the app.
|
||||
externalDataPath &char // Path to this application's external (removable/mountable) data directory.
|
||||
instance voidptr // This is the native instance of the application.
|
||||
internalDataPath &char // Path to this application's internal data directory.
|
||||
obbPath &char // Available starting with Honeycomb: path to the directory containing the application's OBB files (if any).
|
||||
sdkVersion int // The platform's SDK version code.
|
||||
vm voidptr // (JavaVM *) The global handle on the process's Java VM.
|
||||
}
|
||||
|
||||
// NativeActivity defines the native side of an android.app.NativeActivity.
|
||||
pub type NativeActivity = C.ANativeActivity
|
||||
|
||||
pub struct C.AAssetManager {
|
||||
}
|
||||
|
||||
// AssetManager provides access to an application's raw assets by creating Asset objects.
|
||||
pub type AssetManager = C.AAssetManager
|
||||
|
||||
fn C.AAssetManager_open(&C.AAssetManager, &char, i32) &C.AAsset
|
||||
|
||||
// open opens an Android `Asset`.
|
||||
pub fn (am &AssetManager) open(filename string, mode AssetMode) !&Asset {
|
||||
asset := C.AAssetManager_open(am, filename.str, int(mode))
|
||||
if isnil(asset) {
|
||||
return error('file `${filename}` not found')
|
||||
}
|
||||
return asset
|
||||
}
|
||||
|
||||
pub struct C.AAsset {
|
||||
}
|
||||
|
||||
pub type Asset = C.AAsset
|
||||
|
||||
fn C.AAsset_getBuffer(&C.AAsset) voidptr
|
||||
|
||||
// get_buffer returns a pointer to a buffer holding the entire contents of the asset.
|
||||
pub fn (a &Asset) get_buffer() voidptr {
|
||||
return C.AAsset_getBuffer(a)
|
||||
}
|
||||
|
||||
fn C.AAsset_getLength(&C.AAsset) i32
|
||||
|
||||
// get_length returns the total size of the asset data.
|
||||
pub fn (a &Asset) get_length() int {
|
||||
return C.AAsset_getLength(a)
|
||||
}
|
||||
|
||||
fn C.AAsset_getLength64(&C.AAsset) i64
|
||||
|
||||
// get_length_64 returns the total size of the asset data using a 64-bit number instead of 32-bit as `get_length`.
|
||||
pub fn (a &Asset) get_length_64() i64 {
|
||||
return C.AAsset_getLength64(a)
|
||||
}
|
||||
|
||||
fn C.AAsset_read(&C.AAsset, voidptr, usize) i32
|
||||
|
||||
// read attempts to read 'count' bytes of data from the current offset.
|
||||
// read returns the number of bytes read, zero on EOF, or < 0 on error.
|
||||
pub fn (a &Asset) read(buffer voidptr, count usize) int {
|
||||
return C.AAsset_read(a, buffer, count)
|
||||
}
|
||||
|
||||
fn C.AAsset_close(&C.AAsset)
|
||||
|
||||
// close closes the asset, freeing all associated resources.
|
||||
pub fn (a &Asset) close() {
|
||||
C.AAsset_close(a)
|
||||
}
|
||||
|
||||
// read_apk_asset returns all the data located at `path`.
|
||||
// `path` is expected to be relative to the APK/AAB `assets` directory.
|
||||
pub fn read_apk_asset(path string) ![]u8 {
|
||||
$if apk {
|
||||
act := &NativeActivity(C.sapp_android_get_native_activity())
|
||||
if isnil(act) {
|
||||
return error('could not get reference to Android activity')
|
||||
}
|
||||
asset_manager := act.assetManager
|
||||
asset := asset_manager.open(path, .streaming)!
|
||||
len := asset.get_length()
|
||||
buf := []u8{len: len}
|
||||
for {
|
||||
if asset.read(buf.data, usize(len)) > 0 {
|
||||
break
|
||||
}
|
||||
}
|
||||
asset.close()
|
||||
return buf
|
||||
} $else {
|
||||
return error(@FN + ' can only be used with APK/AAB packaged Android apps')
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
// vtest build: !sanitize-memory-gcc && !sanitize-address-gcc && !sanitize-address-clang
|
||||
// vtest vflags: -autofree
|
||||
import os
|
||||
|
||||
fn test_os_args_no_double_free() {
|
||||
args := os.args
|
||||
assert args.len > 0
|
||||
}
|
||||
|
||||
fn test_os_args_clone() {
|
||||
a1 := os.args
|
||||
a2 := os.args
|
||||
assert a1 == a2
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
// Copyright (c) 2019-2024 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 sys_write = 4
|
||||
pub const sys_open = 5
|
||||
pub const sys_close = 6
|
||||
pub const sys_mkdir = 136
|
||||
pub const sys_creat = 8
|
||||
pub const sys_open_nocancel = 398
|
||||
pub const sys_stat64 = 338
|
||||
@@ -0,0 +1,198 @@
|
||||
module os
|
||||
|
||||
pub fn mkdir(path string, params MkdirParams) ! {
|
||||
$if js_node {
|
||||
if path == '.' {
|
||||
return
|
||||
}
|
||||
#$fs.mkdirSync(path.valueOf())
|
||||
|
||||
return
|
||||
} $else {
|
||||
return error('could not create folder')
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
struct PathKind {
|
||||
is_dir bool
|
||||
is_link bool
|
||||
}
|
||||
|
||||
fn kind_of_existing_path(path string) PathKind {
|
||||
link := false
|
||||
directory := false
|
||||
$if js_node {
|
||||
#link.val = $fs.existsSync(path.str) && $fs.lstatSync(path.str).isSymbolicLink()
|
||||
#directory.val = $fs.existsSync(path,str) && $fs.lstatSync(path.str).isDirectory()
|
||||
} $else {
|
||||
_ = path
|
||||
}
|
||||
return PathKind{
|
||||
is_dir: directory
|
||||
is_link: link
|
||||
}
|
||||
}
|
||||
|
||||
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 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)
|
||||
}
|
||||
}
|
||||
|
||||
@[params]
|
||||
pub struct CopyParams {
|
||||
fail_if_exists bool
|
||||
}
|
||||
|
||||
pub fn cp(src string, dst string, config CopyParams) ! {
|
||||
$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 rename(src string, dst string) ! {
|
||||
$if js_node {
|
||||
err := ''
|
||||
#try {
|
||||
#$fs.renameSync(src.str,dst.str);
|
||||
#return;
|
||||
#} catch (e) {
|
||||
#err.str = 'failed to rename ' + 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
|
||||
}
|
||||
|
||||
pub fn getuid() int {
|
||||
res := 0
|
||||
#if (process.getuid) res.val = process.getuid();
|
||||
|
||||
return res
|
||||
}
|
||||
|
||||
pub fn execvp(cmd string, args []string) ! {
|
||||
panic('os.execvp() is not available on JS backend')
|
||||
}
|
||||
|
||||
pub fn stdin_resume() {
|
||||
#$process.stdin.resume();
|
||||
}
|
||||
|
||||
pub fn is_readable(path string) bool {
|
||||
$if js_node {
|
||||
res := false
|
||||
#try { res.val = $fs.accessSync(path.str,$fs.constants.R_OK); } catch { res.val = false; }
|
||||
|
||||
return res
|
||||
} $else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// get_long_path has no meaning for *nix, but has for windows, where `c:\folder\some~1` for example
|
||||
// can be the equivalent of `c:\folder\some spa ces`. On *nix, it just returns a copy of the input path.
|
||||
fn get_long_path(path string) !string {
|
||||
return path
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
// Copyright (c) 2019-2024 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
|
||||
const prot_write = 2
|
||||
const map_private = 0x02
|
||||
const map_anonymous = 0x20
|
||||
|
||||
pub const sys_write = 1
|
||||
pub const sys_open = 2
|
||||
pub const sys_close = 3
|
||||
pub const sys_mkdir = 83
|
||||
pub const sys_creat = 85
|
||||
@@ -0,0 +1,620 @@
|
||||
module os
|
||||
|
||||
#include <dirent.h>
|
||||
#include <unistd.h>
|
||||
#include <fcntl.h>
|
||||
#include <sys/utsname.h>
|
||||
#include <sys/types.h>
|
||||
#include <sys/statvfs.h>
|
||||
#include <utime.h>
|
||||
#insert "@VEXEROOT/vlib/os/execute_capture_nix.h"
|
||||
|
||||
// short_path is a Windows-only helper that returns the DOS 8.3 short path.
|
||||
// On non-Windows platforms it simply returns the given path unchanged,
|
||||
// so that callers guarded by `$if windows { ... }` type-check on other targets.
|
||||
pub fn short_path(path string) string {
|
||||
return path
|
||||
}
|
||||
|
||||
// path_separator is the platform specific separator string, used between the folders and filenames in a path. It is '/' on POSIX, and '\\' on Windows.
|
||||
pub const path_separator = '/'
|
||||
|
||||
// path_delimiter is the platform specific delimiter string, used between the paths in environment variables like PATH. It is ':' on POSIX, and ';' on Windows.
|
||||
pub const path_delimiter = ':'
|
||||
|
||||
// path_devnull is a platform-specific file path of the null device.
|
||||
// It is '/dev/null' on POSIX, and r'\\.\nul' on Windows.
|
||||
pub const path_devnull = '/dev/null'
|
||||
|
||||
const executable_suffixes = ['']
|
||||
|
||||
const stdin_value = 0
|
||||
const stdout_value = 1
|
||||
const 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
|
||||
pub const s_ifdir = 0x4000 // directory
|
||||
pub const s_ifreg = 0x8000 // regular file
|
||||
pub const s_iflnk = 0xa000 // link
|
||||
pub const s_isuid = 0o4000 // SUID
|
||||
pub const s_isgid = 0o2000 // SGID
|
||||
pub const s_isvtx = 0o1000 // Sticky
|
||||
pub const s_irusr = 0o0400 // Read by owner
|
||||
pub const s_iwusr = 0o0200 // Write by owner
|
||||
pub const s_ixusr = 0o0100 // Execute by owner
|
||||
pub const s_irgrp = 0o0040 // Read by group
|
||||
pub const s_iwgrp = 0o0020 // Write by group
|
||||
pub const s_ixgrp = 0o0010 // Execute by group
|
||||
pub const s_iroth = 0o0004 // Read by others
|
||||
pub const s_iwoth = 0o0002 // Write by others
|
||||
pub const s_ixoth = 0o0001
|
||||
|
||||
fn C.utime(&char, &C.utimbuf) i32
|
||||
|
||||
fn C.uname(name &C.utsname) i32
|
||||
|
||||
fn C.symlink(&char, &char) i32
|
||||
|
||||
fn C.readlink(&char, &char, i32) i32
|
||||
|
||||
fn C.link(&char, &char) i32
|
||||
|
||||
fn C.gethostname(&char, i32) i32
|
||||
|
||||
// Note: not available on Android fn C.getlogin_r(&char, int) int
|
||||
fn C.getlogin() &char
|
||||
|
||||
fn C.getppid() i32
|
||||
|
||||
fn C.getgid() i32
|
||||
|
||||
fn C.getegid() i32
|
||||
|
||||
fn C.v_os_execute_capture_start(cmd &char, child_pid &int, read_fd &int) int
|
||||
|
||||
fn C.v_os_exec_capture_start(argv &&char, child_pid &int, read_fd &int) int
|
||||
|
||||
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(path_separator) {
|
||||
pathwalk := file.split(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}${path_separator}'
|
||||
}
|
||||
} else {
|
||||
matches << fpath
|
||||
}
|
||||
}
|
||||
}
|
||||
return subdirs
|
||||
}
|
||||
|
||||
fn native_glob_pattern(pattern string, mut matches []string) ! {
|
||||
steps := pattern.split(path_separator)
|
||||
cwd := if pattern.starts_with(path_separator) { 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}${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()
|
||||
}
|
||||
}
|
||||
|
||||
// utime changes the access and modification times of the inode specified by path.
|
||||
// It returns POSIX error message, if it can not do so.
|
||||
pub fn utime(path string, actime i64, modtime i64) ! {
|
||||
u := C.utimbuf{actime, modtime}
|
||||
if C.utime(&char(path.str), &u) != 0 {
|
||||
return error_with_code(posix_get_error_msg(C.errno), C.errno)
|
||||
}
|
||||
}
|
||||
|
||||
// uname returns information about the platform on which the program is running.
|
||||
// For example:
|
||||
// os.Uname{
|
||||
// sysname: 'Linux'
|
||||
// nodename: 'nemesis'
|
||||
// release: '5.15.0-57-generic'
|
||||
// version: '#63~20.04.1-Ubuntu SMP Wed Nov 30 13:40:16 UTC 2022'
|
||||
// machine: 'x86_64'
|
||||
// }
|
||||
// where the fields have the following meaning:
|
||||
// sysname is the name of this implementation of the operating system
|
||||
// nodename is the name of this node within an implementation-dependent communications network
|
||||
// release is the current release level of this implementation
|
||||
// version is the current version level of this release
|
||||
// machine is the name of the hardware type, on which the system is running
|
||||
// See also https://pubs.opengroup.org/onlinepubs/7908799/xsh/sysutsname.h.html
|
||||
pub fn uname() Uname {
|
||||
mut u := Uname{}
|
||||
unsafe {
|
||||
d := &C.utsname(malloc_noscan(int(sizeof(C.utsname))))
|
||||
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
|
||||
}
|
||||
|
||||
// hostname returns the hostname (system's DNS name) or POSIX error message if the hostname call fails.
|
||||
pub fn hostname() !string {
|
||||
mut hstnme := ''
|
||||
size := 256
|
||||
buf := unsafe { &char(malloc_noscan(size)) }
|
||||
if C.gethostname(buf, size) == 0 {
|
||||
hstnme = unsafe { cstring_to_vstring(buf) }
|
||||
unsafe { free(buf) }
|
||||
return hstnme
|
||||
}
|
||||
return error(posix_get_error_msg(C.errno))
|
||||
}
|
||||
|
||||
// loginname returns the name of the user logged in on the controlling terminal of the process.
|
||||
// It returns a POSIX error message, if the getlogin call fails.
|
||||
pub fn loginname() !string {
|
||||
x := C.getlogin()
|
||||
if !isnil(x) {
|
||||
return unsafe { cstring_to_vstring(x) }
|
||||
}
|
||||
return error(posix_get_error_msg(C.errno))
|
||||
}
|
||||
|
||||
// ls returns ![]string of the files and dirs in the given `path` ( os.ls uses C.readdir ). Symbolic links are returned to be files. For recursive list see os.walk functions.
|
||||
// See also: `os.walk`, `os.walk_ext`, `os.is_dir`, `os.is_file`
|
||||
// Example:
|
||||
// ```
|
||||
// entries := os.ls(os.home_dir()) or { [] }
|
||||
// for entry in entries {
|
||||
// if os.is_dir(os.join_path(os.home_dir(), entry)) {
|
||||
// println('dir: ${entry}')
|
||||
// } else {
|
||||
// println('file: ${entry}')
|
||||
// }
|
||||
// }
|
||||
// ```
|
||||
@[manualfree]
|
||||
pub fn ls(path string) ![]string {
|
||||
if path == '' {
|
||||
return error('ls() expects a folder, not an empty string')
|
||||
}
|
||||
mut res := []string{cap: 50}
|
||||
dir_ptr := unsafe { C.opendir(&char(path.str)) }
|
||||
if isnil(dir_ptr) {
|
||||
return error_posix(msg: 'ls() couldnt open dir "${path}"')
|
||||
}
|
||||
mut ent := &C.dirent(unsafe { nil })
|
||||
for {
|
||||
ent = C.readdir(dir_ptr)
|
||||
if isnil(ent) {
|
||||
break
|
||||
}
|
||||
unsafe {
|
||||
bptr := &u8(&ent.d_name[0])
|
||||
// vfmt off
|
||||
if bptr[0] == 0 || (bptr[0] == `.` && bptr[1] == 0) || (bptr[0] == `.` && bptr[1] == `.` && bptr[2] == 0) {
|
||||
continue
|
||||
}
|
||||
res << tos_clone(bptr)
|
||||
// vfmt on
|
||||
}
|
||||
}
|
||||
C.closedir(dir_ptr)
|
||||
return res
|
||||
}
|
||||
|
||||
// mkdir creates a new directory with the specified path.
|
||||
pub fn mkdir(path string, params MkdirParams) ! {
|
||||
if path == '.' {
|
||||
return
|
||||
}
|
||||
apath := real_path(path)
|
||||
r := unsafe { C.mkdir(&char(apath.str), params.mode) }
|
||||
if r == -1 {
|
||||
return error(posix_get_error_msg(C.errno))
|
||||
}
|
||||
}
|
||||
|
||||
// execute starts the specified command, waits for it to complete, and returns its output.
|
||||
pub fn execute(cmd string) Result {
|
||||
mut pid := 0
|
||||
mut read_fd := -1
|
||||
v_os_execute_lock()
|
||||
rc := C.v_os_execute_capture_start(&char(cmd.str), &pid, &read_fd)
|
||||
v_os_execute_unlock()
|
||||
if rc != 0 {
|
||||
return Result{
|
||||
exit_code: -1
|
||||
output: 'exec("${cmd}") failed'
|
||||
}
|
||||
}
|
||||
soutput := fd_slurp(read_fd).join('')
|
||||
fd_close(read_fd)
|
||||
mut status := 0
|
||||
for {
|
||||
C.errno = 0
|
||||
if C.waitpid(pid, &status, 0) != -1 {
|
||||
break
|
||||
}
|
||||
if C.errno == C.EINTR {
|
||||
continue
|
||||
}
|
||||
return Result{
|
||||
exit_code: -1
|
||||
output: soutput
|
||||
}
|
||||
}
|
||||
exit_code, _ := posix_wait4_to_exit_status(status)
|
||||
return Result{
|
||||
exit_code: exit_code
|
||||
output: soutput
|
||||
}
|
||||
}
|
||||
|
||||
// exec starts the specified command with arguments, waits for it to complete, and returns its output.
|
||||
pub fn exec(args []string) Result {
|
||||
if args.len == 0 {
|
||||
return Result{
|
||||
exit_code: -1
|
||||
output: 'exec requires at least one argument'
|
||||
}
|
||||
}
|
||||
mut cargs := []&char{cap: args.len + 1}
|
||||
for arg in args {
|
||||
cargs << &char(arg.str)
|
||||
}
|
||||
cargs << &char(unsafe { nil })
|
||||
mut pid := 0
|
||||
mut read_fd := -1
|
||||
v_os_execute_lock()
|
||||
rc := C.v_os_exec_capture_start(cargs.data, &pid, &read_fd)
|
||||
v_os_execute_unlock()
|
||||
if rc != 0 {
|
||||
return Result{
|
||||
exit_code: -1
|
||||
output: 'exec("${args[0]}") failed'
|
||||
}
|
||||
}
|
||||
soutput := fd_slurp(read_fd).join('')
|
||||
fd_close(read_fd)
|
||||
mut status := 0
|
||||
for {
|
||||
C.errno = 0
|
||||
if C.waitpid(pid, &status, 0) != -1 {
|
||||
break
|
||||
}
|
||||
if C.errno == C.EINTR {
|
||||
continue
|
||||
}
|
||||
return Result{
|
||||
exit_code: -1
|
||||
output: soutput
|
||||
}
|
||||
}
|
||||
exit_code, _ := posix_wait4_to_exit_status(status)
|
||||
return Result{
|
||||
exit_code: exit_code
|
||||
output: soutput
|
||||
}
|
||||
}
|
||||
|
||||
// raw_execute does the same as `execute` on Unix platforms.
|
||||
// On Windows raw_execute starts the specified command, waits for it to complete, and returns its output.
|
||||
// It's marked as `unsafe` to help emphasize the problems that may arise by allowing, for example,
|
||||
// user provided escape sequences.
|
||||
@[unsafe]
|
||||
pub fn raw_execute(cmd string) Result {
|
||||
return execute(cmd)
|
||||
}
|
||||
|
||||
// symlink creates a symbolic link named link_name, which points to target.
|
||||
// It returns a POSIX error message, if it can not do so.
|
||||
pub fn symlink(target string, link_name string) ! {
|
||||
res := C.symlink(&char(target.str), &char(link_name.str))
|
||||
if res == 0 {
|
||||
return
|
||||
}
|
||||
return error(posix_get_error_msg(C.errno))
|
||||
}
|
||||
|
||||
// readlink reads the target of a symbolic link.
|
||||
// It returns a POSIX error message if it can not do so.
|
||||
//
|
||||
// Note that the target of a symbolic link can be any string:
|
||||
// it is often used to point to another path, but the target is not guaranteed
|
||||
// to resolve as a path, nor to point to a path that exists.
|
||||
@[manualfree]
|
||||
pub fn readlink(path string) !string {
|
||||
// Use a region of stack to get information into; we'll return new memory of more precise size later.
|
||||
mut buf := [max_path_buffer_size]u8{}
|
||||
// readlink returns the number of bytes written into buf, or -1 for errors.
|
||||
res := C.readlink(&char(path.str), &char(&buf[0]), max_path_buffer_size)
|
||||
if res < 0 {
|
||||
return last_error()
|
||||
}
|
||||
// Common case: we got a complete read into our buffer on the stack.
|
||||
// In this case, copy the data into a new heap-allocated string that's right-sized
|
||||
// (we can't return memory from our stack).
|
||||
if res < max_path_buffer_size {
|
||||
return unsafe { (&buf[0]).vstring_with_len(res).clone() }
|
||||
}
|
||||
// If the number of bytes read wasn't less than as many as we said we'd accept: that means we might not have gotten a complete read.
|
||||
// In this case, we have to start doing heap allocations, increasingly large, and simply check until we get a complete one.
|
||||
// Whenever we do succeed: we'll return a string that refers to a subset of that possibly excessively sized buffer,
|
||||
// because we're already on the heap and returning it is valid; and because allocating a new buffer just
|
||||
// to save some resident memory is usually a poor trade of spending of time just to reclaim a very minor amount of space.
|
||||
mut size := max_path_buffer_size
|
||||
for {
|
||||
size *= 2
|
||||
mut buf2 := unsafe { &char(malloc_noscan(size)) }
|
||||
res2 := C.readlink(&char(path.str), buf2, size)
|
||||
if res2 < 0 {
|
||||
return last_error()
|
||||
}
|
||||
if res2 < size {
|
||||
unsafe {
|
||||
buf2[res2] = 0
|
||||
return cstring_to_vstring(buf2)
|
||||
}
|
||||
}
|
||||
unsafe { free(buf2) } // and then loop around to try again with a larger one.
|
||||
}
|
||||
return error('${@METHOD} unreachable code')
|
||||
}
|
||||
|
||||
// link creates a new link (also known as a hard link) to an existing file.
|
||||
// It returns a POSIX error message, if it can not do so.
|
||||
pub fn link(origin string, target string) ! {
|
||||
res := C.link(&char(origin.str), &char(target.str))
|
||||
if res == 0 {
|
||||
return
|
||||
}
|
||||
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
|
||||
cfile := f.cfile
|
||||
f.cfile = unsafe { nil }
|
||||
C.fflush(cfile)
|
||||
C.fclose(cfile)
|
||||
}
|
||||
|
||||
fn C.mkstemp(stemplate &u8) i32
|
||||
|
||||
// ensure_folder_is_writable checks that `folder` exists, and is writable to the process
|
||||
// by creating an empty file in it, then deleting it.
|
||||
@[manualfree]
|
||||
pub fn ensure_folder_is_writable(folder string) ! {
|
||||
if !exists(folder) {
|
||||
return error_with_code('`${folder}` does not exist', 1)
|
||||
}
|
||||
if !is_dir(folder) {
|
||||
return error_with_code('`${folder}` is not a folder', 2)
|
||||
}
|
||||
tmp_perm_check := join_path_single(folder, 'XXXXXX')
|
||||
defer {
|
||||
unsafe { tmp_perm_check.free() }
|
||||
}
|
||||
unsafe {
|
||||
x := C.mkstemp(&char(tmp_perm_check.str))
|
||||
if -1 == x {
|
||||
return error_with_code('folder `${folder}` is not writable', 3)
|
||||
}
|
||||
C.close(x)
|
||||
}
|
||||
rm(tmp_perm_check)!
|
||||
}
|
||||
|
||||
// getpid returns the process ID (PID) of the calling process.
|
||||
@[inline]
|
||||
pub fn getpid() int {
|
||||
return C.getpid()
|
||||
}
|
||||
|
||||
// getppid returns the process ID of the parent of the calling process.
|
||||
@[inline]
|
||||
pub fn getppid() int {
|
||||
return C.getppid()
|
||||
}
|
||||
|
||||
// getuid returns the real user ID of the calling process.
|
||||
@[inline]
|
||||
pub fn getuid() int {
|
||||
return C.getuid()
|
||||
}
|
||||
|
||||
// geteuid returns the effective user ID of the calling process.
|
||||
@[inline]
|
||||
pub fn geteuid() int {
|
||||
return C.geteuid()
|
||||
}
|
||||
|
||||
// getgid returns the real group ID of the calling process.
|
||||
@[inline]
|
||||
pub fn getgid() int {
|
||||
return C.getgid()
|
||||
}
|
||||
|
||||
// getegid returns the effective group ID of the calling process.
|
||||
@[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 new_mode := u32(0)
|
||||
if s := stat(path_s) {
|
||||
new_mode = s.mode
|
||||
}
|
||||
match enable {
|
||||
true { new_mode |= mode }
|
||||
false { new_mode &= (0o7777 - mode) }
|
||||
}
|
||||
|
||||
C.chmod(&char(path_s.str), int(new_mode))
|
||||
}
|
||||
|
||||
// get_long_path has no meaning for *nix, but has for windows, where `c:\folder\some~1` for example
|
||||
// can be the equivalent of `c:\folder\some spa ces`. On *nix, it just returns a copy of the input path.
|
||||
fn get_long_path(path string) !string {
|
||||
return path
|
||||
}
|
||||
|
||||
fn C.sysconf(name i32) i64
|
||||
|
||||
// page_size returns the page size in bytes.
|
||||
pub fn page_size() int {
|
||||
return int(C.sysconf(C._SC_PAGESIZE))
|
||||
}
|
||||
|
||||
struct C.statvfs {
|
||||
f_bsize usize
|
||||
f_blocks usize
|
||||
f_bfree usize
|
||||
f_bavail usize
|
||||
}
|
||||
|
||||
// disk_usage returns disk usage of `path`.
|
||||
@[manualfree]
|
||||
pub fn disk_usage(path string) !DiskUsage {
|
||||
mpath := if path == '' { '.' } else { path }
|
||||
defer { unsafe { mpath.free() } }
|
||||
mut vfs := C.statvfs{}
|
||||
ret := unsafe { C.statvfs(&char(mpath.str), &vfs) }
|
||||
if ret == -1 {
|
||||
return error('cannot get disk usage of path')
|
||||
}
|
||||
f_bsize := u64(vfs.f_bsize)
|
||||
f_blocks := u64(vfs.f_blocks)
|
||||
f_bavail := u64(vfs.f_bavail)
|
||||
f_bfree := u64(vfs.f_bfree)
|
||||
return DiskUsage{
|
||||
total: f_bsize * f_blocks
|
||||
available: f_bsize * f_bavail
|
||||
used: f_bsize * (f_blocks - f_bfree)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
module os
|
||||
|
||||
// stat returns a platform-agnostic Stat struct, containing metadata about the given file/folder path.
|
||||
// It returns a POSIX error, if it can not do so.
|
||||
// Note: symlinks are followed, and the resulting Stat for their target will be returned.
|
||||
// If this is not desired, call lstat/1 instead.
|
||||
pub fn stat(path string) !Stat {
|
||||
mut s := C.stat{}
|
||||
unsafe {
|
||||
res := C.stat(&char(path.str), &s)
|
||||
if res != 0 {
|
||||
return error_posix()
|
||||
}
|
||||
return Stat{
|
||||
dev: s.st_dev
|
||||
inode: s.st_ino
|
||||
nlink: s.st_nlink
|
||||
mode: s.st_mode
|
||||
uid: s.st_uid
|
||||
gid: s.st_gid
|
||||
rdev: s.st_rdev
|
||||
size: s.st_size
|
||||
atime: s.st_atime
|
||||
mtime: s.st_mtime
|
||||
ctime: s.st_ctime
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// lstat is similar to stat/1 for normal files/folders.
|
||||
// Unlike stat/1, however, it will return the stat info for a symlink, instead of its target.
|
||||
pub fn lstat(path string) !Stat {
|
||||
mut s := C.stat{}
|
||||
unsafe {
|
||||
res := C.lstat(&char(path.str), &s)
|
||||
if res != 0 {
|
||||
return error_posix()
|
||||
}
|
||||
return Stat{
|
||||
dev: s.st_dev
|
||||
inode: s.st_ino
|
||||
nlink: s.st_nlink
|
||||
mode: s.st_mode
|
||||
uid: s.st_uid
|
||||
gid: s.st_gid
|
||||
rdev: s.st_rdev
|
||||
size: s.st_size
|
||||
atime: s.st_atime
|
||||
mtime: s.st_mtime
|
||||
ctime: s.st_ctime
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// get_filetype returns the FileType from the Stat struct.
|
||||
pub fn (st Stat) get_filetype() FileType {
|
||||
match st.mode & u32(C.S_IFMT) {
|
||||
u32(C.S_IFREG) {
|
||||
return .regular
|
||||
}
|
||||
u32(C.S_IFDIR) {
|
||||
return .directory
|
||||
}
|
||||
u32(C.S_IFCHR) {
|
||||
return .character_device
|
||||
}
|
||||
u32(C.S_IFBLK) {
|
||||
return .block_device
|
||||
}
|
||||
u32(C.S_IFIFO) {
|
||||
return .fifo
|
||||
}
|
||||
u32(C.S_IFLNK) {
|
||||
return .symbolic_link
|
||||
}
|
||||
u32(C.S_IFSOCK) {
|
||||
return .socket
|
||||
}
|
||||
else {
|
||||
return .unknown
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// get_mode returns the file type and permissions (readable, writable, executable) in owner/group/others format.
|
||||
pub fn (st Stat) get_mode() FileMode {
|
||||
return FileMode{
|
||||
typ: st.get_filetype()
|
||||
owner: FilePermission{
|
||||
read: (st.mode & u32(C.S_IRUSR)) != 0
|
||||
write: (st.mode & u32(C.S_IWUSR)) != 0
|
||||
execute: (st.mode & u32(C.S_IXUSR)) != 0
|
||||
}
|
||||
group: FilePermission{
|
||||
read: (st.mode & u32(C.S_IRGRP)) != 0
|
||||
write: (st.mode & u32(C.S_IWGRP)) != 0
|
||||
execute: (st.mode & u32(C.S_IXGRP)) != 0
|
||||
}
|
||||
others: FilePermission{
|
||||
read: (st.mode & u32(C.S_IROTH)) != 0
|
||||
write: (st.mode & u32(C.S_IWOTH)) != 0
|
||||
execute: (st.mode & u32(C.S_IXOTH)) != 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// is_dir returns a `bool` indicating whether the given `path` is a directory.
|
||||
pub fn is_dir(path string) bool {
|
||||
attr := stat(path) or { return false }
|
||||
return attr.get_filetype() == .directory
|
||||
}
|
||||
|
||||
// is_link returns a boolean indicating whether `path` is a link.
|
||||
// Warning: `is_link()` is known to cause a TOCTOU vulnerability when used incorrectly
|
||||
// (for more information: https://github.com/vlang/v/blob/master/vlib/os/README.md).
|
||||
pub fn is_link(path string) bool {
|
||||
attr := lstat(path) or { return false }
|
||||
return attr.get_filetype() == .symbolic_link
|
||||
}
|
||||
|
||||
// kind_of_existing_path identifies whether path is a file, directory, or link.
|
||||
fn kind_of_existing_path(path string) PathKind {
|
||||
mut res := PathKind{}
|
||||
attr := lstat(path) or { return res }
|
||||
res.is_file = attr.get_filetype() == .regular
|
||||
res.is_dir = attr.get_filetype() == .directory
|
||||
res.is_link = attr.get_filetype() == .symbolic_link
|
||||
return res
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
import os
|
||||
import rand
|
||||
import time
|
||||
|
||||
fn test_stat() {
|
||||
start_time := time.utc().add(-2 * time.second)
|
||||
|
||||
temp_dir := os.join_path(os.temp_dir(), rand.ulid())
|
||||
os.mkdir(temp_dir)!
|
||||
defer {
|
||||
os.rmdir(temp_dir) or {}
|
||||
}
|
||||
|
||||
test_file := os.join_path(temp_dir, rand.ulid())
|
||||
test_content := rand.ulid()
|
||||
os.write_file(test_file, test_content)!
|
||||
defer {
|
||||
os.rm(test_file) or {}
|
||||
}
|
||||
|
||||
end_time := time.utc()
|
||||
|
||||
mut fstat := os.stat(test_file)!
|
||||
eprintln(@LOCATION)
|
||||
eprintln(' | start_time: ${start_time.unix()}\n | end_time: ${end_time.unix()}\n | fstat.ctime: ${fstat.ctime}\n | fstat.mtime: ${fstat.mtime}')
|
||||
assert fstat.get_filetype() == .regular
|
||||
assert fstat.size == u64(test_content.len)
|
||||
assert fstat.ctime >= start_time.unix()
|
||||
assert fstat.ctime <= end_time.unix()
|
||||
assert fstat.mtime >= start_time.unix()
|
||||
assert fstat.mtime <= end_time.unix()
|
||||
|
||||
$if !windows {
|
||||
os.chmod(test_file, 0o600)!
|
||||
fstat = os.stat(test_file)!
|
||||
|
||||
mut fmode := fstat.get_mode()
|
||||
assert fmode.typ == .regular
|
||||
assert fmode.owner.read && fmode.owner.write && !fmode.owner.execute
|
||||
assert !fmode.group.read && !fmode.group.write && !fmode.group.execute
|
||||
assert !fmode.others.read && !fmode.others.write && !fmode.others.execute
|
||||
|
||||
os.chmod(test_file, 0o421)!
|
||||
fstat = os.stat(test_file)!
|
||||
fmode = fstat.get_mode()
|
||||
assert fmode.owner.read && !fmode.owner.write && !fmode.owner.execute
|
||||
assert !fmode.group.read && fmode.group.write && !fmode.group.execute
|
||||
assert !fmode.others.read && !fmode.others.write && fmode.others.execute
|
||||
|
||||
os.chmod(test_file, 0o600)!
|
||||
}
|
||||
|
||||
// When using the Time struct, allow for up to 1 second difference due to nanoseconds
|
||||
// which are not captured in the timestamp
|
||||
dstat := os.stat(temp_dir)!
|
||||
assert dstat.get_filetype() == .directory
|
||||
assert fstat.dev == dstat.dev, 'File and directory should be created on same device'
|
||||
$if !freebsd && !openbsd {
|
||||
assert fstat.rdev == dstat.rdev, 'File and directory should have same device ID'
|
||||
} $else {
|
||||
// On FreeBSD and OpenBSD, the rdev values are not necessarily the same for non-devices
|
||||
// such as regular files and directories.
|
||||
// assert fstat.rdev != dstat.rdev, 'File and directory should not have same device ID'
|
||||
// However, see also https://discord.com/channels/592103645835821068/592114487759470596/1222322061217632347 :
|
||||
// > They may be different but don't have to be.
|
||||
// > On zfs it seems to be 0.
|
||||
// Once upon a time (Unix v7) it was (major<<8)|minor to indicate the underlying raw device but those days are long gone.
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
module os
|
||||
|
||||
// stat returns metadata for the given file/folder.
|
||||
// It will return a POSIX error message, if it can not do so.
|
||||
// C._wstat64() can be used on 32- and 64-bit Windows per
|
||||
// https://learn.microsoft.com/en-us/cpp/c-runtime-library/reference/stat-functions?view=msvc-170
|
||||
pub fn stat(path string) !Stat {
|
||||
mut s := C.__stat64{}
|
||||
unsafe {
|
||||
res := C._wstat64(path.to_wide(), &s)
|
||||
if res != 0 {
|
||||
return error_posix()
|
||||
}
|
||||
return Stat{
|
||||
dev: s.st_dev
|
||||
inode: s.st_ino
|
||||
nlink: s.st_nlink
|
||||
mode: s.st_mode
|
||||
uid: s.st_uid
|
||||
gid: s.st_gid
|
||||
rdev: s.st_rdev
|
||||
size: s.st_size
|
||||
atime: s.st_atime
|
||||
mtime: s.st_mtime
|
||||
ctime: s.st_ctime
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// lstat is the same as stat() for Windows.
|
||||
@[inline]
|
||||
pub fn lstat(path string) !Stat {
|
||||
return stat(path)
|
||||
}
|
||||
|
||||
// get_filetype returns the FileType from the Stat struct.
|
||||
pub fn (st Stat) get_filetype() FileType {
|
||||
match st.mode & u32(C.S_IFMT) {
|
||||
u32(C.S_IFDIR) {
|
||||
return .directory
|
||||
}
|
||||
else {
|
||||
return .regular
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// get_mode returns the file type and permissions (readable, writable, executable) in owner/group/others format.
|
||||
// Note: they will all be the same for Windows.
|
||||
pub fn (st Stat) get_mode() FileMode {
|
||||
return FileMode{
|
||||
typ: st.get_filetype()
|
||||
owner: FilePermission{
|
||||
read: (st.mode & u32(C.S_IREAD)) != 0
|
||||
write: (st.mode & u32(C.S_IWRITE)) != 0
|
||||
execute: (st.mode & u32(C.S_IEXEC)) != 0
|
||||
}
|
||||
group: FilePermission{
|
||||
read: (st.mode & u32(C.S_IREAD)) != 0
|
||||
write: (st.mode & u32(C.S_IWRITE)) != 0
|
||||
execute: (st.mode & u32(C.S_IEXEC)) != 0
|
||||
}
|
||||
others: FilePermission{
|
||||
read: (st.mode & u32(C.S_IREAD)) != 0
|
||||
write: (st.mode & u32(C.S_IWRITE)) != 0
|
||||
execute: (st.mode & u32(C.S_IEXEC)) != 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// is_dir returns a `bool` indicating whether the given `path` is a directory.
|
||||
pub fn is_dir(path string) bool {
|
||||
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
|
||||
}
|
||||
|
||||
// is_link returns a boolean indicating whether `path` is a link.
|
||||
// Warning: `is_link()` is known to cause a TOCTOU vulnerability when used incorrectly
|
||||
// (for more information: https://github.com/vlang/v/blob/master/vlib/os/README.md)
|
||||
pub fn is_link(path string) bool {
|
||||
path_ := path.replace('/', '\\')
|
||||
attr := C.GetFileAttributesW(path_.to_wide())
|
||||
return int(attr) != int(C.INVALID_FILE_ATTRIBUTES) && (attr & 0x400) != 0
|
||||
}
|
||||
|
||||
// kind_of_existing_path identifies whether path is a file, directory, or link.
|
||||
fn kind_of_existing_path(path string) PathKind {
|
||||
mut res := PathKind{}
|
||||
attr := C.GetFileAttributesW(path.to_wide())
|
||||
if attr != u32(C.INVALID_FILE_ATTRIBUTES) {
|
||||
if (int(attr) & C.FILE_ATTRIBUTE_NORMAL) != 0 {
|
||||
res.is_file = true
|
||||
}
|
||||
if (int(attr) & C.FILE_ATTRIBUTE_DIRECTORY) != 0 {
|
||||
res.is_dir = true
|
||||
}
|
||||
if (int(attr) & 0x400) != 0 {
|
||||
res.is_link = true
|
||||
}
|
||||
}
|
||||
return res
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
module os
|
||||
|
||||
pub struct C.dirent {
|
||||
d_ino u64
|
||||
d_seekoff u64
|
||||
d_reclen u16
|
||||
d_namlen u16
|
||||
d_type u8
|
||||
d_name [256]char
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
module os
|
||||
|
||||
pub type FN_SA_Handler = fn (sig int)
|
||||
|
||||
pub struct C.sigaction {
|
||||
mut:
|
||||
sa_mask int
|
||||
sa_sigaction int
|
||||
sa_flags int
|
||||
sa_handler FN_SA_Handler
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
module os
|
||||
|
||||
// Minimal stat struct as specified in
|
||||
// https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/sys_stat.h.html
|
||||
pub struct C.stat {
|
||||
st_dev u64
|
||||
st_ino u64
|
||||
st_mode u32
|
||||
st_nlink u64
|
||||
st_uid u32
|
||||
st_gid u32
|
||||
st_rdev u64
|
||||
st_size u64
|
||||
st_atime i64
|
||||
st_mtime i64
|
||||
st_ctime i64
|
||||
}
|
||||
|
||||
pub struct C.__stat64 {
|
||||
st_size u64
|
||||
st_mode u32
|
||||
st_mtime i64
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
module os
|
||||
|
||||
pub struct C.stat {
|
||||
st_dev u64 // 8
|
||||
st_ino u64 // 8
|
||||
st_nlink u64 // 8
|
||||
st_mode u32 // 4
|
||||
st_uid u32 // 4
|
||||
st_gid u32 // 4
|
||||
st_rdev u64 // 8
|
||||
st_size u64 // 8
|
||||
st_blksize u64 // 8
|
||||
st_blocks u64 // 8
|
||||
st_atime i64 // 8
|
||||
st_mtime i64 // 8
|
||||
st_ctime i64 // 8
|
||||
}
|
||||
|
||||
pub struct C.__stat64 {
|
||||
st_mode u32
|
||||
st_size u64
|
||||
st_mtime u64
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
module os
|
||||
|
||||
// Minimal stat struct as specified in
|
||||
// https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/sys_stat.h.html
|
||||
pub struct C.stat {
|
||||
st_dev u64
|
||||
st_ino u64
|
||||
st_mode u32
|
||||
st_nlink u64
|
||||
st_uid u32
|
||||
st_gid u32
|
||||
st_rdev u64
|
||||
st_size u64
|
||||
st_atime int
|
||||
st_mtime int
|
||||
st_ctime int
|
||||
}
|
||||
|
||||
pub struct C.__stat64 {
|
||||
st_dev u32 // 4
|
||||
st_ino u16 // 2
|
||||
st_mode u16 // 2
|
||||
st_nlink u16 // 2
|
||||
st_uid u16 // 2
|
||||
st_gid u16 // 2
|
||||
st_rdev u32 // 4
|
||||
st_size u64 // 8
|
||||
st_atime i64 // 8
|
||||
st_mtime i64 // 8
|
||||
st_ctime i64 // 8
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
module os
|
||||
|
||||
pub struct C.utsname {
|
||||
mut:
|
||||
sysname &char
|
||||
nodename &char
|
||||
release &char
|
||||
version &char
|
||||
machine &char
|
||||
}
|
||||
|
||||
pub struct C.utimbuf {
|
||||
actime i64
|
||||
modtime i64
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,855 @@
|
||||
module os
|
||||
|
||||
import strings
|
||||
import encoding.utf8.validate
|
||||
|
||||
#flag windows -l advapi32
|
||||
#include <process.h>
|
||||
#include <sys/utime.h>
|
||||
|
||||
// path_separator is the platform specific separator string, used between the folders, and filenames in a path. It is '/' on POSIX, and '\\' on Windows.
|
||||
pub const path_separator = '\\'
|
||||
|
||||
// path_delimiter is the platform specific delimiter string, used between the paths in environment variables like PATH. It is ':' on POSIX, and ';' on Windows.
|
||||
pub const path_delimiter = ';'
|
||||
|
||||
// path_devnull is a platform-specific file path of the null device.
|
||||
// It is '/dev/null' on POSIX, and r'\\.\nul' on Windows.
|
||||
pub const path_devnull = r'\\.\nul'
|
||||
|
||||
// See https://docs.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-createsymboliclinkw
|
||||
fn C.CreateSymbolicLinkW(&u16, &u16, u32) i32
|
||||
|
||||
// See https://docs.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-createhardlinkw
|
||||
fn C.CreateHardLinkW(&u16, &u16, C.SECURITY_ATTRIBUTES) i32
|
||||
|
||||
// See https://learn.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-getshortpathnamew
|
||||
fn C.GetShortPathNameW(&u16, &u16, u32) u32
|
||||
|
||||
fn C.AddVectoredExceptionHandler(u32, voidptr) voidptr
|
||||
|
||||
fn C._getpid() i32
|
||||
|
||||
const executable_suffixes = ['.exe', '.bat', '.cmd', '']
|
||||
|
||||
// these consts are declared for parity with the nix version, their values are not used, except for -cross
|
||||
const s_ifmt = 0xF000 // type of file
|
||||
const s_ifdir = 0x4000 // directory
|
||||
const s_ifreg = 0x8000 // regular file
|
||||
const s_iflnk = 0xa000 // link
|
||||
const s_isuid = 0o4000 // SUID
|
||||
const s_isgid = 0o2000 // SGID
|
||||
const s_isvtx = 0o1000 // Sticky
|
||||
const s_irusr = 0o0400 // Read by owner
|
||||
const s_iwusr = 0o0200 // Write by owner
|
||||
const s_ixusr = 0o0100 // Execute by owner
|
||||
const s_irgrp = 0o0040 // Read by group
|
||||
const s_iwgrp = 0o0020 // Write by group
|
||||
const s_ixgrp = 0o0010 // Execute by group
|
||||
const s_iroth = 0o0004 // Read by others
|
||||
const s_iwoth = 0o0002 // Write by others
|
||||
const s_ixoth = 0o0001
|
||||
|
||||
// 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 = unsafe { nil }
|
||||
lp_desktop &u16 = unsafe { nil }
|
||||
lp_title &u16 = unsafe { nil }
|
||||
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 &u8 = unsafe { nil }
|
||||
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
|
||||
}
|
||||
|
||||
@[inline]
|
||||
fn wide_ptr_to_string(wstr &u16) string {
|
||||
return unsafe { string_from_wide(wstr) }
|
||||
}
|
||||
|
||||
fn looks_like_utf16le_captured_output(raw string) bool {
|
||||
if raw.len < 2 {
|
||||
return false
|
||||
}
|
||||
// Allow odd-length buffers: Windows text-mode translation can insert
|
||||
// extra bytes (e.g. \r before \n), producing odd-length UTF-16LE output.
|
||||
// In that case, just ignore the trailing byte during detection.
|
||||
if raw[0] == 0xff && raw[1] == 0xfe {
|
||||
return true
|
||||
}
|
||||
sample_len := if raw.len > 128 { 128 } else { raw.len }
|
||||
mut checked_pairs := 0
|
||||
mut zero_high_bytes := 0
|
||||
for i := 1; i < sample_len; i += 2 {
|
||||
checked_pairs++
|
||||
if raw[i] == 0 {
|
||||
zero_high_bytes++
|
||||
}
|
||||
}
|
||||
return checked_pairs > 0 && zero_high_bytes * 4 >= checked_pairs * 3
|
||||
}
|
||||
|
||||
fn looks_like_utf16be_captured_output(raw string) bool {
|
||||
if raw.len < 2 || raw.len % 2 != 0 {
|
||||
return false
|
||||
}
|
||||
if raw[0] == 0xfe && raw[1] == 0xff {
|
||||
return true
|
||||
}
|
||||
sample_len := if raw.len > 128 { 128 } else { raw.len }
|
||||
mut checked_pairs := 0
|
||||
mut zero_low_bytes := 0
|
||||
for i := 0; i < sample_len; i += 2 {
|
||||
checked_pairs++
|
||||
if raw[i] == 0 {
|
||||
zero_low_bytes++
|
||||
}
|
||||
}
|
||||
return checked_pairs > 0 && zero_low_bytes * 4 >= checked_pairs * 3
|
||||
}
|
||||
|
||||
@[manualfree]
|
||||
fn utf16_captured_output_to_string(raw string, little_endian bool) string {
|
||||
mut start := 0
|
||||
if little_endian && raw[0] == 0xff && raw[1] == 0xfe {
|
||||
start = 2
|
||||
} else if !little_endian && raw[0] == 0xfe && raw[1] == 0xff {
|
||||
start = 2
|
||||
}
|
||||
pairs := (raw.len - start) / 2
|
||||
mut wide := []u16{len: pairs + 1, init: u16(0)}
|
||||
for idx := 0; idx < pairs; idx++ {
|
||||
offset := start + idx * 2
|
||||
b0 := u16(raw[offset])
|
||||
b1 := u16(raw[offset + 1])
|
||||
wide[idx] = if little_endian { b0 | (b1 << 8) } else { (b0 << 8) | b1 }
|
||||
}
|
||||
res := unsafe { string_from_wide2(wide.data, pairs) }
|
||||
unsafe { wide.free() }
|
||||
return res
|
||||
}
|
||||
|
||||
@[manualfree]
|
||||
fn decode_windows_captured_output(raw string) string {
|
||||
if raw.len == 0 {
|
||||
return ''
|
||||
}
|
||||
if looks_like_utf16le_captured_output(raw) {
|
||||
return utf16_captured_output_to_string(raw, true)
|
||||
}
|
||||
if looks_like_utf16be_captured_output(raw) {
|
||||
return utf16_captured_output_to_string(raw, false)
|
||||
}
|
||||
if validate.utf8_string(raw) {
|
||||
// Check for embedded null bytes, which suggest this is actually UTF-16
|
||||
// that wasn't detected (e.g. due to text-mode \r\n translation corrupting
|
||||
// the byte alignment). Strip null bytes as a recovery heuristic.
|
||||
mut has_null := false
|
||||
for i in 0 .. raw.len {
|
||||
if raw[i] == 0 {
|
||||
has_null = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !has_null {
|
||||
return raw
|
||||
}
|
||||
mut nulls := 0
|
||||
for i in 0 .. raw.len {
|
||||
if raw[i] == 0 {
|
||||
nulls++
|
||||
}
|
||||
}
|
||||
if nulls * 4 < raw.len {
|
||||
return raw
|
||||
}
|
||||
// Contains many null bytes - likely corrupted UTF-16LE.
|
||||
// Strip null bytes and normalize \r\n to \n (text-mode artifacts).
|
||||
mut cleaned := strings.new_builder(raw.len)
|
||||
for i in 0 .. raw.len {
|
||||
if raw[i] == 0 {
|
||||
continue
|
||||
}
|
||||
if raw[i] == 0x0D && i + 1 < raw.len && raw[i + 1] == 0x0A {
|
||||
continue
|
||||
}
|
||||
cleaned.write_u8(raw[i])
|
||||
}
|
||||
return cleaned.str()
|
||||
}
|
||||
mut wide := raw.to_wide(from_ansi: true)
|
||||
if isnil(wide) {
|
||||
return raw
|
||||
}
|
||||
res := unsafe { string_from_wide(wide) }
|
||||
unsafe { free(wide) }
|
||||
return res
|
||||
}
|
||||
|
||||
pub struct C._utimbuf {
|
||||
actime i64
|
||||
modtime i64
|
||||
}
|
||||
|
||||
fn C._utime(&char, voidptr) i32
|
||||
|
||||
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')
|
||||
}
|
||||
normalized_pattern := pattern.replace('\\', '/')
|
||||
mut match_base := dir(normalized_pattern).replace('\\', '/')
|
||||
if match_base == '.' {
|
||||
match_base = ''
|
||||
} else if match_base != '/' && !match_base.ends_with('/') {
|
||||
match_base += '/'
|
||||
}
|
||||
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 := wide_ptr_to_string(&find_file_data.c_file_name[0])
|
||||
if fname !in ['.', '..'] {
|
||||
mut fp := fname.replace('\\', '/')
|
||||
if match_base != '' {
|
||||
fp = '${match_base}${fp}'
|
||||
}
|
||||
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 := wide_ptr_to_string(&find_file_data.c_file_name[0])
|
||||
if filename in ['.', '..'] {
|
||||
continue
|
||||
}
|
||||
mut fpath := filename.replace('\\', '/')
|
||||
if match_base != '' {
|
||||
fpath = '${match_base}${fpath}'
|
||||
}
|
||||
if find_file_data.dw_file_attributes & u32(C.FILE_ATTRIBUTE_DIRECTORY) > 0 {
|
||||
fpath += '/'
|
||||
}
|
||||
matches << fpath
|
||||
}
|
||||
}
|
||||
|
||||
// short_path returns the Windows DOS 8.3 short path when it is available.
|
||||
// If the path does not exist, or if short names are unavailable, it returns the original path.
|
||||
pub fn short_path(path string) string {
|
||||
if path == '' {
|
||||
return ''
|
||||
}
|
||||
normalized := path.replace('/', '\\')
|
||||
mut short_buf := [max_path_buffer_size]u16{}
|
||||
mut wpath := normalized.to_wide()
|
||||
defer {
|
||||
unsafe { free(voidptr(wpath)) }
|
||||
}
|
||||
short_len := C.GetShortPathNameW(wpath, &short_buf[0], max_path_buffer_size)
|
||||
if short_len > 0 && short_len < u32(max_path_buffer_size) {
|
||||
return unsafe { string_from_wide2(&short_buf[0], int(short_len)) }
|
||||
}
|
||||
parent := dir(normalized)
|
||||
if parent == '' || parent == '.' || parent == normalized || !is_dir(parent) {
|
||||
return path
|
||||
}
|
||||
short_parent := short_path(parent)
|
||||
if short_parent == parent {
|
||||
return path
|
||||
}
|
||||
return join_path_single(short_parent, file_name(normalized))
|
||||
}
|
||||
|
||||
pub fn utime(path string, actime i64, modtime i64) ! {
|
||||
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 {
|
||||
if path == '' {
|
||||
return error('ls() expects a folder, not an empty 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')
|
||||
}
|
||||
// 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))
|
||||
// Handle cases where files cannot be opened. for example:"System Volume Information"
|
||||
if h_find_files == C.INVALID_HANDLE_VALUE {
|
||||
return error('ls(): Could not get a file handle: ' + get_error_msg(int(C.GetLastError())))
|
||||
}
|
||||
first_filename := wide_ptr_to_string(&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 := wide_ptr_to_string(&find_file_data.c_file_name[0])
|
||||
if filename != '.' && filename != '..' {
|
||||
dir_files << filename.clone()
|
||||
}
|
||||
}
|
||||
C.FindClose(h_find_files)
|
||||
return dir_files
|
||||
}
|
||||
|
||||
// mkdir creates a new directory with the specified path.
|
||||
pub fn mkdir(path string, params MkdirParams) ! {
|
||||
if path == '.' {
|
||||
return
|
||||
}
|
||||
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())))
|
||||
}
|
||||
}
|
||||
|
||||
// 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 FormatMessageW
|
||||
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-FormatMessageWa#parameters
|
||||
const format_message_allocate_buffer = 0x00000100
|
||||
const format_message_argument_array = 0x00002000
|
||||
const format_message_from_hmodule = 0x00000800
|
||||
const format_message_from_string = 0x00000400
|
||||
const format_message_from_system = 0x00001000
|
||||
const format_message_ignore_inserts = 0x00000200
|
||||
|
||||
// Ref - winnt.h
|
||||
const sublang_neutral = 0x00
|
||||
const sublang_default = 0x01
|
||||
const lang_neutral = sublang_neutral
|
||||
|
||||
// Ref - https://docs.microsoft.com/en-us/windows/win32/debug/system-error-codes--12000-15999-
|
||||
const max_error_code = 15841
|
||||
|
||||
// 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(unsafe { nil })
|
||||
// Check for code overflow
|
||||
if code > u32(max_error_code) {
|
||||
return buf
|
||||
}
|
||||
C.FormatMessageW(format_message_allocate_buffer | format_message_from_system | format_message_ignore_inserts,
|
||||
0, code, 0, 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 ''
|
||||
}
|
||||
msg := wide_ptr_to_string(&u16(ptr_text))
|
||||
C.LocalFree(ptr_text)
|
||||
return msg
|
||||
}
|
||||
|
||||
// execute starts the specified command, waits for it to complete, and returns its output.
|
||||
// In opposition to `raw_execute` this function rejects `&&`, `||`, and linefeeds when they appear
|
||||
// outside double-quoted strings before delegating to `cmd.exe`.
|
||||
pub fn execute(cmd string) Result {
|
||||
if windows_execute_has_forbidden_shell_operator(cmd) {
|
||||
return Result{
|
||||
exit_code: -1
|
||||
output: '&&, || and \\n are not allowed in shell commands'
|
||||
}
|
||||
}
|
||||
return unsafe { raw_execute(cmd) }
|
||||
}
|
||||
|
||||
// exec starts the specified command with arguments, waits for it to complete, and returns its output.
|
||||
pub fn exec(args []string) Result {
|
||||
if args.len == 0 {
|
||||
return Result{
|
||||
exit_code: -1
|
||||
output: 'exec requires at least one argument'
|
||||
}
|
||||
}
|
||||
mut command_line := requote_arg(args[0])
|
||||
if args.len > 1 {
|
||||
command_line += ' ' + requote_args(args[1..])
|
||||
}
|
||||
mut application_name := ''
|
||||
filename_lc := args[0].to_lower_ascii()
|
||||
if is_abs_path(args[0]) && !filename_lc.ends_with('.bat') && !filename_lc.ends_with('.cmd') {
|
||||
application_name = args[0]
|
||||
}
|
||||
return windows_execute_command_line(command_line, application_name, args.join(' '), false)
|
||||
}
|
||||
|
||||
// windows_execute_has_forbidden_shell_operator rejects only the command chaining operators that `cmd.exe`
|
||||
// treats specially outside double-quoted strings.
|
||||
fn windows_execute_has_forbidden_shell_operator(cmd string) bool {
|
||||
mut in_double_quotes := false
|
||||
for i := 0; i < cmd.len; i++ {
|
||||
ch := cmd[i]
|
||||
if ch == `"` {
|
||||
in_double_quotes = !in_double_quotes
|
||||
continue
|
||||
}
|
||||
if ch == `\n` {
|
||||
return true
|
||||
}
|
||||
if in_double_quotes {
|
||||
continue
|
||||
}
|
||||
if ch == `&` && i + 1 < cmd.len && cmd[i + 1] == `&` {
|
||||
return true
|
||||
}
|
||||
if ch == `|` && i + 1 < cmd.len && cmd[i + 1] == `|` {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// raw_execute starts the specified command, waits for it to complete, and returns its output.
|
||||
// It's marked as `unsafe` to help emphasize the problems that may arise by allowing, for example,
|
||||
// user provided escape sequences.
|
||||
@[unsafe]
|
||||
pub fn raw_execute(cmd string) Result {
|
||||
mut pcmd := cmd
|
||||
if cmd.contains('./') {
|
||||
pcmd = pcmd.replace('./', '.\\')
|
||||
}
|
||||
if cmd.contains('2>') {
|
||||
pcmd = 'cmd /c "${pcmd}"'
|
||||
} else {
|
||||
pcmd = 'cmd /c "${pcmd} 2>&1"'
|
||||
}
|
||||
return windows_execute_command_line(pcmd, '', cmd, true)
|
||||
}
|
||||
|
||||
fn windows_execute_command_line(command_line_text string, application_name string, display_cmd string, expand_environment bool) Result {
|
||||
mut child_stdin := &u32(unsafe { nil })
|
||||
mut child_stdout_read := &u32(unsafe { nil })
|
||||
mut child_stdout_write := &u32(unsafe { nil })
|
||||
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: unsafe { nil }
|
||||
lp_reserved: unsafe { nil }
|
||||
lp_desktop: unsafe { nil }
|
||||
lp_title: unsafe { nil }
|
||||
cb: sizeof(StartupInfo)
|
||||
h_std_input: child_stdin
|
||||
h_std_output: child_stdout_write
|
||||
h_std_error: child_stdout_write
|
||||
dw_flags: u32(C.STARTF_USESTDHANDLES)
|
||||
}
|
||||
|
||||
mut command_line_ptr := command_line_text.to_wide()
|
||||
mut expanded_command_line := [32768]u16{}
|
||||
if expand_environment {
|
||||
C.ExpandEnvironmentStringsW(command_line_ptr, voidptr(&expanded_command_line), 32768)
|
||||
command_line_ptr = unsafe { &expanded_command_line[0] }
|
||||
}
|
||||
mut application_name_ptr := &u16(unsafe { nil })
|
||||
if application_name != '' {
|
||||
application_name_ptr = application_name.to_wide()
|
||||
}
|
||||
create_process_ok := C.CreateProcessW(application_name_ptr, command_line_ptr, 0, 0, C.TRUE,
|
||||
C.CREATE_NO_WINDOW, 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: ${display_cmd}'
|
||||
}
|
||||
}
|
||||
C.CloseHandle(child_stdin)
|
||||
C.CloseHandle(child_stdout_write)
|
||||
buf := [4096]u8{}
|
||||
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 := decode_windows_captured_output(read_data.str())
|
||||
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) ! {
|
||||
// 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
|
||||
}
|
||||
return error('could not symlink')
|
||||
}
|
||||
|
||||
// readlink reads the target of a symbolic link.
|
||||
// TODO: implement this for windows too.
|
||||
pub fn readlink(path string) !string {
|
||||
return error('${@METHOD} not yet supported on windows')
|
||||
}
|
||||
|
||||
pub fn link(origin string, target string) ! {
|
||||
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')
|
||||
}
|
||||
}
|
||||
|
||||
pub fn (mut f File) close() {
|
||||
if !f.is_opened {
|
||||
return
|
||||
}
|
||||
f.is_opened = false
|
||||
cfile := f.cfile
|
||||
f.cfile = unsafe { nil }
|
||||
C.fflush(cfile)
|
||||
C.fclose(cfile)
|
||||
}
|
||||
|
||||
pub struct ExceptionRecord {
|
||||
pub:
|
||||
// status_ constants
|
||||
code u32
|
||||
flags u32
|
||||
record &ExceptionRecord = unsafe { nil }
|
||||
address voidptr
|
||||
param_count u32
|
||||
// params []voidptr
|
||||
}
|
||||
|
||||
pub struct ContextRecord {
|
||||
// TODO
|
||||
}
|
||||
|
||||
pub struct ExceptionPointers {
|
||||
pub:
|
||||
exception_record &ExceptionRecord = unsafe { nil }
|
||||
context_record &ContextRecord = unsafe { nil }
|
||||
}
|
||||
|
||||
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), voidptr(handler))
|
||||
}
|
||||
|
||||
// uname returns information about the platform on which the program is running.
|
||||
// Currently `uname` on windows is not standardized, so it just mimics current practices from other popular software/language implementations:
|
||||
// busybox-v1.35.0 * `busybox uname -a` => "Windows_NT HOSTNAME 10.0 19044 x86_64 MS/Windows"
|
||||
// rust/coreutils-v0.0.17 * `coreutils uname -a` => `Windows_NT HOSTNAME 10.0 19044 x86_64 MS/Windows (Windows 10)`
|
||||
// Python3 => `uname_result(system='Windows', node='HOSTNAME', release='10', version='10.0.19044', machine='AMD64')`
|
||||
// See: [NT Version Info](https://en.wikipedia.org/wiki/Windows_NT) @@ <https://archive.is/GnnvF>
|
||||
// and: [NT Version Info (detailed)](https://en.wikipedia.org/wiki/Comparison_of_Microsoft_Windows_versions#NT_Kernel-based_2)
|
||||
pub fn uname() Uname {
|
||||
nodename := hostname() or { '' }
|
||||
// ToDO: environment variables have low reliability; check for another quick way
|
||||
machine :=
|
||||
getenv('PROCESSOR_ARCHITECTURE') // * note: 'AMD64' == 'x86_64' (not standardized, but 'x86_64' use is more common; but, python == 'AMD64')
|
||||
version_info := execute('cmd /d/c ver').output
|
||||
version_n := (version_info.split(' '))[3].replace(']', '').trim_space()
|
||||
return Uname{
|
||||
sysname: 'Windows_NT' // as of 2022-12, WinOS has only two possible kernels ~ 'Windows_NT' or 'Windows_9x'
|
||||
nodename: nodename
|
||||
machine: machine.trim_space()
|
||||
release: (version_n.split('.'))[0..2].join('.').trim_space() // Major.minor-only == "primary"/release version
|
||||
version: (version_n.split('.'))[2].trim_space()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn hostname() !string {
|
||||
buf := [255]u16{}
|
||||
size := u32(255)
|
||||
res := C.GetComputerNameW(&buf[0], voidptr(&size))
|
||||
if !res {
|
||||
return error(get_error_msg(int(C.GetLastError())))
|
||||
}
|
||||
return unsafe { string_from_wide(&buf[0]) }
|
||||
}
|
||||
|
||||
pub fn loginname() !string {
|
||||
buf := [255]u16{}
|
||||
size := u32(255)
|
||||
res := C.GetUserNameW(&buf[0], voidptr(&size))
|
||||
if !res {
|
||||
return error(get_error_msg(int(C.GetLastError())))
|
||||
}
|
||||
return unsafe { string_from_wide(&buf[0]) }
|
||||
}
|
||||
|
||||
// ensure_folder_is_writable checks that `folder` exists, and is writable to the process, by creating an empty file in it, then deleting it.
|
||||
pub fn ensure_folder_is_writable(folder string) ! {
|
||||
if !exists(folder) {
|
||||
return error_with_code('`${folder}` does not exist', 1)
|
||||
}
|
||||
if !is_dir(folder) {
|
||||
return error_with_code('`folder` is not a folder', 2)
|
||||
}
|
||||
tmp_folder_name := 'tmp_perm_check_pid_' + getpid().str()
|
||||
tmp_perm_check := join_path_single(folder, tmp_folder_name)
|
||||
write_file(tmp_perm_check, 'test') or {
|
||||
return error_with_code('cannot write to folder "${folder}": ${err}', 3)
|
||||
}
|
||||
rm(tmp_perm_check)!
|
||||
}
|
||||
|
||||
@[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
|
||||
}
|
||||
|
||||
fn C.GetLongPathName(short_path &u16, long_path &u16, long_path_bufsize u32) u32
|
||||
|
||||
// get_long_path has no meaning for *nix, but has for windows, where `c:\folder\some~1` for example
|
||||
// can be the equivalent of `c:\folder\some spa ces`. On *nix, it just returns a copy of the input path.
|
||||
fn get_long_path(path string) !string {
|
||||
if !path.contains('~') {
|
||||
return path
|
||||
}
|
||||
input_short_path := path.to_wide()
|
||||
defer {
|
||||
unsafe { free(input_short_path) }
|
||||
}
|
||||
long_path_buf := [4096]u16{}
|
||||
res := C.GetLongPathName(input_short_path, &long_path_buf[0], sizeof(long_path_buf))
|
||||
if res == 0 {
|
||||
return error(get_error_msg(int(C.GetLastError())))
|
||||
}
|
||||
long_path := wide_ptr_to_string(&long_path_buf[0])
|
||||
return long_path
|
||||
}
|
||||
|
||||
// page_size returns the page size in bytes.
|
||||
pub fn page_size() int {
|
||||
sinfo := C.SYSTEM_INFO{}
|
||||
C.GetSystemInfo(&sinfo)
|
||||
return int(sinfo.dwPageSize)
|
||||
}
|
||||
|
||||
// disk_usage returns disk usage of `path`.
|
||||
pub fn disk_usage(path string) !DiskUsage {
|
||||
mut free_bytes_available_to_caller := u64(0)
|
||||
mut total := u64(0)
|
||||
mut available := u64(0)
|
||||
mut ret := false
|
||||
if path == '.' || path == '' {
|
||||
ret = C.GetDiskFreeSpaceExA(&char(unsafe { nil }), &free_bytes_available_to_caller, &total,
|
||||
&available)
|
||||
} else {
|
||||
ret = C.GetDiskFreeSpaceExA(&char(path.str), &free_bytes_available_to_caller, &total,
|
||||
&available)
|
||||
}
|
||||
if ret == false {
|
||||
return error('cannot get disk usage of path')
|
||||
}
|
||||
return DiskUsage{
|
||||
total: total
|
||||
available: available
|
||||
used: total - available
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
module os
|
||||
|
||||
import term.termios
|
||||
|
||||
// input_password prompts the user for a password-like secret.
|
||||
// It disables the terminal echo during user input and resets it back to normal when done.
|
||||
pub fn input_password(prompt string) !string {
|
||||
if is_atty(1) <= 0 || getenv('TERM') == 'dumb' {
|
||||
return error('Could not obtain password discretely.')
|
||||
}
|
||||
|
||||
mut old_state := termios.Termios{}
|
||||
if termios.tcgetattr(0, mut old_state) != 0 {
|
||||
return last_error()
|
||||
}
|
||||
|
||||
mut new_state := old_state
|
||||
new_state.disable_echo()
|
||||
termios.set_state(0, new_state)
|
||||
|
||||
password := input_opt(prompt) or { return error('Failed to read password') }
|
||||
|
||||
termios.set_state(0, old_state)
|
||||
|
||||
println('')
|
||||
return password
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
module os
|
||||
|
||||
#include <windows.h>
|
||||
|
||||
// input_password prompts the user for a password-like secret.
|
||||
// It disables the terminal echo during user input and resets it back to normal when done.
|
||||
pub fn input_password(prompt string) !string {
|
||||
if is_atty(1) <= 0 || getenv('TERM') == 'dumb' {
|
||||
return error('Could not obtain password discretely.')
|
||||
}
|
||||
|
||||
std_handle := C.GetStdHandle(C.STD_INPUT_HANDLE)
|
||||
mut mode := u32(0)
|
||||
|
||||
unsafe { C.GetConsoleMode(std_handle, voidptr(&mode)) }
|
||||
unsafe { C.SetConsoleMode(std_handle, mode & (~u32(C.ENABLE_ECHO_INPUT))) }
|
||||
|
||||
defer {
|
||||
unsafe { C.SetConsoleMode(std_handle, &mode) }
|
||||
println('')
|
||||
}
|
||||
|
||||
password := input_opt(prompt) or { return error('Failed to read password') }
|
||||
|
||||
return password
|
||||
}
|
||||
@@ -0,0 +1,213 @@
|
||||
module os
|
||||
|
||||
fn C._dup(fd i32) i32
|
||||
fn C._dup2(fd1 i32, fd2 i32) i32
|
||||
fn C._pipe(fds &int, size u32, mode i32) i32
|
||||
fn C.dup(fd i32) i32
|
||||
|
||||
const fd_stdout = $if windows { 1 } $else { C.STDOUT_FILENO }
|
||||
const fd_stderr = $if windows { 2 } $else { C.STDERR_FILENO }
|
||||
|
||||
// fd_dup duplicates a file descriptor
|
||||
pub fn fd_dup(fd int) int {
|
||||
return $if windows { C._dup(fd) } $else { C.dup(fd) }
|
||||
}
|
||||
|
||||
// fd_dup2 duplicates file descriptor `fd1` to descriptor number `fd2`
|
||||
// If `fd2` is already open, it is closed first before being reused.
|
||||
// Returns the new file descriptor on success, or -1 on error.
|
||||
pub fn fd_dup2(fd1 int, fd2 int) int {
|
||||
return $if windows { C._dup2(fd1, fd2) } $else { C.dup2(fd1, fd2) }
|
||||
}
|
||||
|
||||
// Pipe represents a bidirectional communication channel
|
||||
@[noinit]
|
||||
pub struct Pipe {
|
||||
pub mut:
|
||||
read_fd int = -1
|
||||
write_fd int = -1
|
||||
}
|
||||
|
||||
// pipe creates a new pipe for inter-process communication
|
||||
pub fn pipe() !Pipe {
|
||||
mut fds := [2]int{}
|
||||
$if windows {
|
||||
if C._pipe(&fds[0], 0, 0) == -1 {
|
||||
return error('Failed to create pipe')
|
||||
}
|
||||
} $else {
|
||||
if C.pipe(&fds[0]) == -1 {
|
||||
return error('Failed to create pipe')
|
||||
}
|
||||
}
|
||||
|
||||
return Pipe{
|
||||
read_fd: fds[0]
|
||||
write_fd: fds[1]
|
||||
}
|
||||
}
|
||||
|
||||
// close closes the pipe and releases associated resources
|
||||
pub fn (mut p Pipe) close() {
|
||||
if p.read_fd != -1 {
|
||||
fd_close(p.read_fd)
|
||||
p.read_fd = -1
|
||||
}
|
||||
if p.write_fd != -1 {
|
||||
fd_close(p.write_fd)
|
||||
p.write_fd = -1
|
||||
}
|
||||
}
|
||||
|
||||
// read reads data from the pipe into the provided buffer
|
||||
pub fn (p &Pipe) read(mut buffer []u8) !int {
|
||||
result := C.read(p.read_fd, buffer.data, buffer.len)
|
||||
if result == -1 {
|
||||
return error('Read failed')
|
||||
}
|
||||
return int(result)
|
||||
}
|
||||
|
||||
// write writes data from the buffer to the pipe
|
||||
pub fn (p &Pipe) write(buffer []u8) !int {
|
||||
result := C.write(p.write_fd, buffer.data, buffer.len)
|
||||
if result == -1 {
|
||||
return error('Write failed')
|
||||
}
|
||||
return int(result)
|
||||
}
|
||||
|
||||
// write_string writes data from the string to the pipe
|
||||
pub fn (p &Pipe) write_string(s string) !int {
|
||||
result := C.write(p.write_fd, voidptr(s.str), s.len)
|
||||
if result == -1 {
|
||||
return error('Write failed')
|
||||
}
|
||||
return int(result)
|
||||
}
|
||||
|
||||
// slurp reads all data from the pipe until EOF
|
||||
pub fn (mut p Pipe) slurp() []string {
|
||||
// Close write end to send EOF signal to the pipe
|
||||
fd_close(p.write_fd)
|
||||
p.write_fd = -1
|
||||
result := fd_slurp(p.read_fd)
|
||||
fd_close(p.read_fd)
|
||||
p.read_fd = -1
|
||||
return result
|
||||
}
|
||||
|
||||
// IOCapture manages redirection of standard output and error streams
|
||||
@[noinit]
|
||||
pub struct IOCapture {
|
||||
pub mut:
|
||||
stdout Pipe
|
||||
stderr Pipe
|
||||
mut:
|
||||
original_stdout_fd int = -1
|
||||
original_stderr_fd int = -1
|
||||
}
|
||||
|
||||
// stdio_capture starts capturing stdout and stderr by redirecting them to pipes
|
||||
// Example:
|
||||
// ```v
|
||||
// import os
|
||||
// unbuffer_stdout()
|
||||
// mut cap := os.stdio_capture()!
|
||||
// println('hello println')
|
||||
// eprintln('hello eprintln')
|
||||
// cap.stop()
|
||||
// sout := cap.stdout.slurp()
|
||||
// serr := cap.stderr.slurp()
|
||||
// cap.close()
|
||||
// assert sout == ['hello println\n']
|
||||
// assert serr == ['hello eprintln\n']
|
||||
// ```
|
||||
// or
|
||||
// ```v
|
||||
// import os
|
||||
// unbuffer_stdout()
|
||||
// mut cap := os.stdio_capture()!
|
||||
// println('hello println')
|
||||
// eprintln('hello eprintln')
|
||||
// eprintln('World eprintln')
|
||||
// println('World println')
|
||||
// sout, serr := cap.finish()
|
||||
// assert sout == ['hello println\nWorld println\n']
|
||||
// assert serr == ['hello eprintln\nWorld eprintln\n']
|
||||
// ```
|
||||
pub fn stdio_capture() !IOCapture {
|
||||
mut c := IOCapture{}
|
||||
mut pipe_stdout := pipe()!
|
||||
mut pipe_stderr := pipe()!
|
||||
|
||||
flush_stdout()
|
||||
flush_stderr()
|
||||
|
||||
// Save original file descriptors
|
||||
c.original_stdout_fd = fd_dup(fd_stdout)
|
||||
c.original_stderr_fd = fd_dup(fd_stderr)
|
||||
|
||||
// Redirect stdout to pipe
|
||||
if fd_dup2(pipe_stdout.write_fd, fd_stdout) == -1 {
|
||||
pipe_stdout.close()
|
||||
pipe_stderr.close()
|
||||
return error('Failed to redirect stdout')
|
||||
}
|
||||
|
||||
// Redirect stderr to pipe
|
||||
if fd_dup2(pipe_stderr.write_fd, fd_stderr) == -1 {
|
||||
fd_dup2(c.original_stdout_fd, fd_stdout) // Restore stdout
|
||||
pipe_stdout.close()
|
||||
pipe_stderr.close()
|
||||
return error('Failed to redirect stderr')
|
||||
}
|
||||
|
||||
// Close original write ends (duplicated by dup2)
|
||||
fd_close(pipe_stdout.write_fd)
|
||||
fd_close(pipe_stderr.write_fd)
|
||||
|
||||
pipe_stdout.write_fd = -1
|
||||
pipe_stderr.write_fd = -1
|
||||
|
||||
// Store pipes for later reading
|
||||
c.stdout = pipe_stdout
|
||||
c.stderr = pipe_stderr
|
||||
return c
|
||||
}
|
||||
|
||||
// stop restores the original stdout and stderr streams
|
||||
// This should be called to resume normal console output
|
||||
pub fn (mut c IOCapture) stop() {
|
||||
flush_stdout()
|
||||
flush_stderr()
|
||||
|
||||
// Restore original stdout
|
||||
if c.original_stdout_fd != -1 {
|
||||
fd_dup2(c.original_stdout_fd, fd_stdout)
|
||||
fd_close(c.original_stdout_fd)
|
||||
c.original_stdout_fd = -1
|
||||
}
|
||||
|
||||
// Restore original stderr
|
||||
if c.original_stderr_fd != -1 {
|
||||
fd_dup2(c.original_stderr_fd, fd_stderr)
|
||||
fd_close(c.original_stderr_fd)
|
||||
c.original_stderr_fd = -1
|
||||
}
|
||||
}
|
||||
|
||||
// close releases all resources associated with the capture
|
||||
pub fn (mut c IOCapture) close() {
|
||||
c.stdout.close()
|
||||
c.stderr.close()
|
||||
}
|
||||
|
||||
// finish stops capturing, reads all captured data, and releases resources
|
||||
pub fn (mut c IOCapture) finish() ([]string, []string) {
|
||||
c.stop()
|
||||
stdout_str := c.stdout.slurp()
|
||||
stderr_str := c.stderr.slurp()
|
||||
c.close()
|
||||
return stdout_str, stderr_str
|
||||
}
|
||||
@@ -0,0 +1,164 @@
|
||||
import os
|
||||
|
||||
fn testsuite_begin() {
|
||||
// Calling unbuffer_stdout() here is important, in order
|
||||
// for `v test pipe_test.v` to work, since the test runner
|
||||
// will start a non interactive process for the test, in which
|
||||
// case libc buffers the output lines by default -> failures.
|
||||
unbuffer_stdout()
|
||||
}
|
||||
|
||||
fn test_pipe_basic() {
|
||||
println('=== Testing basic pipe functionality ===')
|
||||
mut pipe := os.pipe()!
|
||||
defer {
|
||||
pipe.close()
|
||||
}
|
||||
test_data := 'Hello, Pipe!'
|
||||
bytes_written := pipe.write(test_data.bytes()) or {
|
||||
eprintln('Failed to write to pipe: ${err}')
|
||||
return
|
||||
}
|
||||
println('Written ${bytes_written} bytes to pipe')
|
||||
mut buffer := []u8{len: 1024}
|
||||
bytes_read := pipe.read(mut buffer) or {
|
||||
eprintln('Failed to read from pipe: ${err}')
|
||||
return
|
||||
}
|
||||
received_data := buffer[0..bytes_read].bytestr()
|
||||
println('Read ${bytes_read} bytes from pipe: "${received_data}"')
|
||||
assert bytes_written == bytes_read
|
||||
assert test_data == received_data
|
||||
println('✓ Basic pipe test passed')
|
||||
}
|
||||
|
||||
fn test_pipe_large_data() {
|
||||
println('=== Testing pipe with large data ===')
|
||||
mut pipe := os.pipe()!
|
||||
defer {
|
||||
pipe.close()
|
||||
}
|
||||
large_data := 'X'.repeat(1000)
|
||||
bytes_written := pipe.write(large_data.bytes()) or {
|
||||
eprintln('Failed to write large data to pipe: ${err}')
|
||||
return
|
||||
}
|
||||
println('Written ${bytes_written} bytes of large data to pipe')
|
||||
mut buffer := []u8{len: 1024}
|
||||
bytes_read := pipe.read(mut buffer) or {
|
||||
eprintln('Failed to read large data from pipe: ${err}')
|
||||
return
|
||||
}
|
||||
assert bytes_written == bytes_read
|
||||
assert bytes_read == 1000
|
||||
println('✓ Large data pipe test passed')
|
||||
}
|
||||
|
||||
fn test_capture_stdout() {
|
||||
println('=== Testing stdout capture ===')
|
||||
mut cap := os.stdio_capture()!
|
||||
test_output := 'This is stdout test message'
|
||||
println(test_output)
|
||||
println('Another stdout line')
|
||||
cap.stop()
|
||||
stdout := cap.stdout.slurp().join('\n')
|
||||
stderr := cap.stderr.slurp().join('\n')
|
||||
cap.close()
|
||||
println('Captured stdout: "${stdout}"')
|
||||
println('Captured stderr: "${stderr}"')
|
||||
assert stdout.contains(test_output)
|
||||
assert stdout.contains('Another stdout line')
|
||||
assert stderr == ''
|
||||
println('✓ Stdout capture test passed')
|
||||
}
|
||||
|
||||
fn test_capture_stderr() {
|
||||
println('=== Testing stderr capture ===')
|
||||
mut cap := os.stdio_capture()!
|
||||
test_error := 'This is stderr test message'
|
||||
eprintln(test_error)
|
||||
eprintln('Another stderr line')
|
||||
cap.stop()
|
||||
stdout := cap.stdout.slurp().join('\n')
|
||||
stderr := cap.stderr.slurp().join('\n')
|
||||
cap.close()
|
||||
println('Captured stdout: "${stdout}"')
|
||||
println('Captured stderr: "${stderr}"')
|
||||
assert stderr.contains(test_error)
|
||||
assert stderr.contains('Another stderr line')
|
||||
assert stdout == ''
|
||||
println('✓ Stderr capture test passed')
|
||||
}
|
||||
|
||||
fn test_capture_both() {
|
||||
println('=== Testing both stdout and stderr capture ===')
|
||||
mut cap := os.stdio_capture()!
|
||||
stdout_msg := 'Standard output message'
|
||||
stderr_msg := 'Standard error message'
|
||||
println(stdout_msg)
|
||||
eprintln(stderr_msg)
|
||||
println('More stdout')
|
||||
eprintln('More stderr')
|
||||
cap.stop()
|
||||
stdout := cap.stdout.slurp().join('\n')
|
||||
stderr := cap.stderr.slurp().join('\n')
|
||||
cap.close()
|
||||
println('Captured stdout: "${stdout}"')
|
||||
println('Captured stderr: "${stderr}"')
|
||||
assert stdout.contains(stdout_msg)
|
||||
assert stdout.contains('More stdout')
|
||||
assert stderr.contains(stderr_msg)
|
||||
assert stderr.contains('More stderr')
|
||||
assert !stdout.contains(stderr_msg)
|
||||
assert !stderr.contains(stdout_msg)
|
||||
println('✓ Both stdout and stderr capture test passed')
|
||||
}
|
||||
|
||||
fn test_capture_both_finish() {
|
||||
println('=== Testing both stdout and stderr capture (finish)===')
|
||||
mut cap := os.stdio_capture()!
|
||||
stdout_msg := 'Standard output message'
|
||||
stderr_msg := 'Standard error message'
|
||||
println(stdout_msg)
|
||||
eprintln(stderr_msg)
|
||||
println('More stdout')
|
||||
eprintln('More stderr')
|
||||
stdout_list, stderr_list := cap.finish()
|
||||
stdout := stdout_list.join('\n')
|
||||
stderr := stderr_list.join('\n')
|
||||
println('Captured stdout: "${stdout}"')
|
||||
println('Captured stderr: "${stderr}"')
|
||||
assert stdout.contains(stdout_msg)
|
||||
assert stdout.contains('More stdout')
|
||||
assert stderr.contains(stderr_msg)
|
||||
assert stderr.contains('More stderr')
|
||||
assert !stdout.contains(stderr_msg)
|
||||
assert !stderr.contains(stdout_msg)
|
||||
println('✓ Both stdout and stderr capture test passed')
|
||||
}
|
||||
|
||||
fn test_capture_restoration() {
|
||||
println('=== Testing output restoration after capture ===')
|
||||
original_stdout_msg := 'Before capture - stdout'
|
||||
original_stderr_msg := 'Before capture - stderr'
|
||||
println(original_stdout_msg)
|
||||
eprintln(original_stderr_msg)
|
||||
mut cap := os.stdio_capture()!
|
||||
captured_stdout_msg := 'During capture - stdout'
|
||||
captured_stderr_msg := 'During capture - stderr'
|
||||
println(captured_stdout_msg)
|
||||
eprintln(captured_stderr_msg)
|
||||
cap.stop()
|
||||
stdout := cap.stdout.slurp().join('\n')
|
||||
stderr := cap.stderr.slurp().join('\n')
|
||||
cap.close()
|
||||
after_capture_stdout_msg := 'After capture - stdout'
|
||||
after_capture_stderr_msg := 'After capture - stderr'
|
||||
println(after_capture_stdout_msg)
|
||||
eprintln(after_capture_stderr_msg)
|
||||
assert stdout.contains(captured_stdout_msg)
|
||||
assert stderr.contains(captured_stderr_msg)
|
||||
assert !stdout.contains(original_stdout_msg)
|
||||
assert !stderr.contains(original_stderr_msg)
|
||||
println('✓ Output restoration test passed')
|
||||
}
|
||||
@@ -0,0 +1,351 @@
|
||||
module os
|
||||
|
||||
// The kind of the pipe file descriptor, that is used for communicating with the child process
|
||||
pub enum ChildProcessPipeKind {
|
||||
stdin
|
||||
stdout
|
||||
stderr
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// signal_term terminates the process.
|
||||
pub fn (mut p Process) signal_term() {
|
||||
if p.status !in [.running, .stopped] {
|
||||
return
|
||||
}
|
||||
p._signal_term()
|
||||
}
|
||||
|
||||
// signal_pgkill kills the whole process group.
|
||||
pub fn (mut p Process) signal_pgkill() {
|
||||
if p.status !in [.running, .stopped] {
|
||||
return
|
||||
}
|
||||
p._signal_pgkill()
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// wait for a process to finish.
|
||||
// Note: 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.
|
||||
// Note: 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()
|
||||
}
|
||||
|
||||
// close frees the OS resources associated with the process.
|
||||
// It can be called multiple times, but will free the resources just once.
|
||||
// If the process has already finished, this sets the process state to
|
||||
// .closed, which is final.
|
||||
pub fn (mut p Process) close() {
|
||||
if p.status in [.not_started, .closed] {
|
||||
return
|
||||
}
|
||||
$if !windows {
|
||||
for i in 0 .. 3 {
|
||||
if p.stdio_fd[i] != -1 {
|
||||
fd_close(p.stdio_fd[i])
|
||||
p.stdio_fd[i] = -1
|
||||
}
|
||||
}
|
||||
}
|
||||
if p.status !in [.running, .stopped] {
|
||||
p.status = .closed
|
||||
}
|
||||
}
|
||||
|
||||
@[unsafe]
|
||||
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 is still alive.
|
||||
pub fn (mut p Process) is_alive() bool {
|
||||
mut res := false
|
||||
if p.status in [.running, .stopped] {
|
||||
res = p._is_alive()
|
||||
}
|
||||
$if trace_process_is_alive ? {
|
||||
eprintln('${@LOCATION}, pid: ${p.pid}, status: ${p.status}, res: ${res}')
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
//
|
||||
pub fn (mut p Process) set_redirect_stdio() {
|
||||
p.use_stdio_ctl = true
|
||||
$if trace_process_pipes ? {
|
||||
eprintln('${@LOCATION}, pid: ${p.pid}, status: ${p.status}')
|
||||
}
|
||||
}
|
||||
|
||||
// stdin_write will write the string `s`, to the stdin pipe of the child process.
|
||||
pub fn (mut p Process) stdin_write(s string) {
|
||||
p._check_redirection_call(@METHOD)
|
||||
$if trace_process_pipes ? {
|
||||
eprintln('${@LOCATION}, pid: ${p.pid}, status: ${p.status}, s.len: ${s.len}, s: `${s}`')
|
||||
}
|
||||
p._write_to(.stdin, s)
|
||||
}
|
||||
|
||||
// stdout_slurp will read from the stdout pipe.
|
||||
// It will block until it either reads all the data, or until the pipe is closed (end of file).
|
||||
pub fn (mut p Process) stdout_slurp() string {
|
||||
p._check_redirection_call(@METHOD)
|
||||
res := p._slurp_from(.stdout)
|
||||
$if trace_process_pipes ? {
|
||||
eprintln('${@LOCATION}, pid: ${p.pid}, status: ${p.status}, res.len: ${res.len}, res: `${res}`')
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
// stderr_slurp will read from the stderr pipe.
|
||||
// It will block until it either reads all the data, or until the pipe is closed (end of file).
|
||||
pub fn (mut p Process) stderr_slurp() string {
|
||||
p._check_redirection_call(@METHOD)
|
||||
res := p._slurp_from(.stderr)
|
||||
$if trace_process_pipes ? {
|
||||
eprintln('${@LOCATION}, pid: ${p.pid}, status: ${p.status}, res.len: ${res.len}, res: `${res}`')
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
// stdout_read reads a block of data from the child process stdout pipe.
|
||||
// It returns `''` immediately when there is currently no data to be read.
|
||||
pub fn (mut p Process) stdout_read() string {
|
||||
p._check_redirection_call(@METHOD)
|
||||
if !p._is_pending(.stdout) {
|
||||
return ''
|
||||
}
|
||||
res := p._read_from(.stdout)
|
||||
$if trace_process_pipes ? {
|
||||
eprintln('${@LOCATION}, pid: ${p.pid}, status: ${p.status}, res.len: ${res.len}, res: `${res}`')
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
// stderr_read reads a block of data from the child process stderr pipe.
|
||||
// It returns `''` immediately when there is currently no data to be read.
|
||||
pub fn (mut p Process) stderr_read() string {
|
||||
p._check_redirection_call(@METHOD)
|
||||
if !p._is_pending(.stderr) {
|
||||
return ''
|
||||
}
|
||||
res := p._read_from(.stderr)
|
||||
$if trace_process_pipes ? {
|
||||
eprintln('${@LOCATION}, pid: ${p.pid}, status: ${p.status}, res.len: ${res.len}, res: `${res}`')
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
// pipe_read reads a block of data, from the given pipe of the child process.
|
||||
// It will return `none`, if there is no data to be read, *without blocking*.
|
||||
pub fn (mut p Process) pipe_read(pkind ChildProcessPipeKind) ?string {
|
||||
p._check_redirection_call(@METHOD)
|
||||
if !p._is_pending(pkind) {
|
||||
$if trace_process_pipes ? {
|
||||
eprintln('${@LOCATION}, pid: ${p.pid}, status: ${p.status}, no pending data')
|
||||
}
|
||||
return none
|
||||
}
|
||||
res := p._read_from(pkind)
|
||||
if res.len == 0 {
|
||||
return none
|
||||
}
|
||||
$if trace_process_pipes ? {
|
||||
eprintln('${@LOCATION}, pid: ${p.pid}, status: ${p.status}, res.len: ${res.len}, res: `${res}`')
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
// is_pending returns whether there is data to be read from child process's pipe corresponding to `pkind`.
|
||||
// For example `if p.is_pending(.stdout) { dump( p.stdout_read() ) }` will not block indefinitely.
|
||||
pub fn (mut p Process) is_pending(pkind ChildProcessPipeKind) bool {
|
||||
p._check_redirection_call(@METHOD)
|
||||
res := p._is_pending(pkind)
|
||||
$if trace_process_pipes ? {
|
||||
eprintln('${@LOCATION}, pid: ${p.pid}, status: ${p.status}, pkind: ${pkind}, res: ${res}')
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
// _read_from should be called only from .stdout_read/0, .stderr_read/0 and .pipe_read/1.
|
||||
fn (mut p Process) _read_from(pkind ChildProcessPipeKind) string {
|
||||
$if windows {
|
||||
s, _ := p.win_read_string(int(pkind), 4096)
|
||||
return s
|
||||
} $else {
|
||||
s, _ := fd_read(p.stdio_fd[pkind], 4096)
|
||||
return s
|
||||
}
|
||||
}
|
||||
|
||||
// _slurp_from should be called only from stdout_slurp() and stderr_slurp().
|
||||
fn (mut p Process) _slurp_from(pkind ChildProcessPipeKind) string {
|
||||
$if windows {
|
||||
return p.win_slurp(int(pkind))
|
||||
} $else {
|
||||
return fd_slurp(p.stdio_fd[pkind]).join('')
|
||||
}
|
||||
}
|
||||
|
||||
// _write_to should be called only from stdin_write().
|
||||
fn (mut p Process) _write_to(pkind ChildProcessPipeKind, s string) {
|
||||
$if windows {
|
||||
p.win_write_string(int(pkind), s)
|
||||
} $else {
|
||||
fd_write(p.stdio_fd[pkind], s)
|
||||
}
|
||||
}
|
||||
|
||||
// _is_pending should be called only from is_pending().
|
||||
fn (mut p Process) _is_pending(pkind ChildProcessPipeKind) bool {
|
||||
$if windows {
|
||||
return p.win_is_pending(int(pkind))
|
||||
} $else {
|
||||
return fd_is_pending(p.stdio_fd[pkind])
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// _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_term should not be called directly, except by p.signal_term.
|
||||
fn (mut p Process) _signal_term() {
|
||||
$if windows {
|
||||
p.win_term_process()
|
||||
} $else {
|
||||
p.unix_term_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()
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
module os
|
||||
|
||||
$if js_node {
|
||||
#const $child_process = require('child_process')
|
||||
}
|
||||
|
||||
// new_process - create a new process descriptor.
|
||||
// Note: 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
|
||||
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)
|
||||
}
|
||||
|
||||
pub fn (mut p Process) stdin_resume() {
|
||||
#p.val.pid.stdin.resume()
|
||||
}
|
||||
|
||||
// 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 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()')
|
||||
}
|
||||
}
|
||||
|
||||
pub fn (mut p Process) close() {
|
||||
// no-op on JS backend
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
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 mut:
|
||||
filename string // the process's command file path
|
||||
pid int // the PID of the process
|
||||
code int = -1 // the exit code of the process, != -1 *only* when status is .exited *and* the process was not aborted
|
||||
status ProcessState = .not_started // the current status of the process
|
||||
err string // if the process fails, contains the reason why
|
||||
args []string // the arguments that the command takes
|
||||
work_folder string // the initial working folder of the process. When '', reuse the same folder as the parent process.
|
||||
env_is_custom bool // true, when the environment was customized with .set_environment
|
||||
env []string // the environment with which the process was started (list of 'var=val')
|
||||
use_stdio_ctl bool // when true, then you can use p.stdin_write(), p.stdout_slurp() and p.stderr_slurp()
|
||||
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
|
||||
create_no_window bool // sets a value indicating whether to start the process in a new window, The default is false; used only by the windows implementation
|
||||
}
|
||||
|
||||
// new_process - create a new process descriptor.
|
||||
// Note: 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_work_folder - set the initial working folder for the new process.
|
||||
// If you do not set it, it will reuse the current working folder of the parent process.
|
||||
pub fn (mut p Process) set_work_folder(path string) {
|
||||
if p.status != .not_started {
|
||||
return
|
||||
}
|
||||
p.work_folder = real_path(path)
|
||||
return
|
||||
}
|
||||
|
||||
// set_environment - set a custom environment variable mapping for the new process.
|
||||
pub fn (mut p Process) set_environment(envs map[string]string) {
|
||||
if p.status != .not_started {
|
||||
return
|
||||
}
|
||||
p.env_is_custom = true
|
||||
p.env = []string{}
|
||||
for k, v in envs {
|
||||
p.env << '${k}=${v}'
|
||||
}
|
||||
return
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
import os
|
||||
|
||||
const vexe = os.getenv('VEXE')
|
||||
const vroot = os.dir(vexe)
|
||||
|
||||
// strip_tcc_fallback_warning removes the noisy macOS-only `warning: tcc compilation
|
||||
// failed, falling back to cc` line that v emits to stderr and that os.execute folds
|
||||
// into the captured output. The CI path-with-spaces+comma jobs trigger this fallback,
|
||||
// which would otherwise prefix the program output and break starts_with assertions.
|
||||
fn strip_tcc_fallback_warning(output string) string {
|
||||
mut s := output
|
||||
for s.contains('tcc compilation failed') {
|
||||
nl := s.index('\n') or { return s }
|
||||
s = s[nl + 1..]
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
fn test_v_run_simple() {
|
||||
echo_os_args := os.join_path(vroot, 'cmd/tools/test_os_args.v')
|
||||
res123 := os.execute('${os.quoted_path(vexe)} run ${os.quoted_path(echo_os_args)} 1 2 3')
|
||||
println(res123)
|
||||
assert res123.exit_code == 0
|
||||
assert strip_tcc_fallback_warning(res123.output).starts_with("['1', '2', '3']")
|
||||
}
|
||||
|
||||
fn test_v_run_quoted_args_with_spaces() {
|
||||
echo_os_args := os.join_path(vroot, 'cmd/tools/test_os_args.v')
|
||||
res := os.execute('${os.quoted_path(vexe)} run ${os.quoted_path(echo_os_args)} 1 "Learn V" 3')
|
||||
println(res)
|
||||
assert res.exit_code == 0
|
||||
assert strip_tcc_fallback_warning(res.output).starts_with("['1', 'Learn V', '3']")
|
||||
}
|
||||
|
||||
fn test_v_run_quoted_args_with_spaces__use_os_system_to_run() {
|
||||
echo_os_args := os.join_path(vroot, 'cmd/tools/test_os_args.v')
|
||||
res :=
|
||||
os.execute('${os.quoted_path(vexe)} -use-os-system-to-run run ${os.quoted_path(echo_os_args)} 1 "Learn V" 3')
|
||||
println(res)
|
||||
assert res.exit_code == 0
|
||||
assert strip_tcc_fallback_warning(res.output).starts_with("['1', 'Learn V', '3']")
|
||||
}
|
||||
|
||||
fn test_v_run_file_from_path_with_spaces() {
|
||||
echo_os_args := os.join_path(vroot, 'cmd/tools/test_os_args.v')
|
||||
spaced_dir := os.join_path(os.vtmp_dir(), 'v run path with spaces')
|
||||
spaced_echo_os_args := os.join_path(spaced_dir, 'test os args.v')
|
||||
os.rmdir_all(spaced_dir) or {}
|
||||
os.mkdir_all(spaced_dir)!
|
||||
defer {
|
||||
os.rmdir_all(spaced_dir) or {}
|
||||
}
|
||||
os.cp(echo_os_args, spaced_echo_os_args)!
|
||||
res :=
|
||||
os.execute('${os.quoted_path(vexe)} run ${os.quoted_path(spaced_echo_os_args)} 1 "Learn V" 3')
|
||||
println(res)
|
||||
assert res.exit_code == 0
|
||||
assert strip_tcc_fallback_warning(res.output).starts_with("['1', 'Learn V', '3']")
|
||||
}
|
||||
@@ -0,0 +1,185 @@
|
||||
module os
|
||||
|
||||
fn C.setpgid(pid i32, pgid i32) i32
|
||||
|
||||
fn env_value_from_entries(env []string, name string) ?string {
|
||||
prefix := '${name}='
|
||||
for entry in env {
|
||||
if entry.starts_with(prefix) {
|
||||
return entry[prefix.len..]
|
||||
}
|
||||
}
|
||||
return none
|
||||
}
|
||||
|
||||
fn (p &Process) unix_resolve_filename() !string {
|
||||
if is_abs_path(p.filename) {
|
||||
return p.filename
|
||||
}
|
||||
if p.filename.contains(path_separator) {
|
||||
if p.work_folder != '' {
|
||||
return abs_path(p.filename)
|
||||
}
|
||||
return p.filename
|
||||
}
|
||||
path := env_value_from_entries(p.env, 'PATH') or { return error_failed_to_find_executable() }
|
||||
return find_abs_path_of_executable_in_path_env(p.filename, path)
|
||||
}
|
||||
|
||||
fn (mut p Process) unix_spawn_process() int {
|
||||
mut pipeset := [6]int{}
|
||||
if p.use_stdio_ctl {
|
||||
mut dont_care := C.pipe(&pipeset[0]) // pipe read end 0 <- 1 pipe write end
|
||||
dont_care = C.pipe(&pipeset[2]) // pipe read end 2 <- 3 pipe write end
|
||||
dont_care = C.pipe(&pipeset[4]) // pipe read end 4 <- 5 pipe write end
|
||||
_ = dont_care // using `_` directly on each above `pipe` fails to avoid C compiler generate an `-Wunused-result` warning
|
||||
}
|
||||
pid := fork()
|
||||
if pid != 0 {
|
||||
// This is the parent process after the fork.
|
||||
// Note: 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 independent 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 standard 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])
|
||||
}
|
||||
p.filename = p.unix_resolve_filename() or {
|
||||
eprintln(err)
|
||||
exit(1)
|
||||
}
|
||||
if p.work_folder != '' {
|
||||
chdir(p.work_folder) or {}
|
||||
}
|
||||
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_term_process() {
|
||||
C.kill(p.pid, C.SIGTERM)
|
||||
}
|
||||
|
||||
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() {
|
||||
p.impl_check_pid_status(false, 0)
|
||||
}
|
||||
|
||||
fn (mut p Process) unix_is_alive() bool {
|
||||
return p.impl_check_pid_status(true, C.WNOHANG)
|
||||
}
|
||||
|
||||
fn (mut p Process) impl_check_pid_status(exit_early_on_ret0 bool, waitpid_options int) bool {
|
||||
mut cstatus := 0
|
||||
mut ret := -1
|
||||
$if !emscripten ? {
|
||||
ret = C.waitpid(p.pid, &cstatus, waitpid_options)
|
||||
}
|
||||
p.code = ret
|
||||
if ret == -1 {
|
||||
p.err = posix_get_error_msg(C.errno)
|
||||
return false
|
||||
}
|
||||
if exit_early_on_ret0 && ret == 0 {
|
||||
return true
|
||||
}
|
||||
mut pret, is_signaled := posix_wait4_to_exit_status(cstatus)
|
||||
if is_signaled {
|
||||
p.status = .aborted
|
||||
p.err = 'Terminated by signal ${pret:2d} (${sigint_to_signal_name(pret)})'
|
||||
pret += 128
|
||||
} 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_term_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_is_pending(_idx int) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
fn (mut p Process) win_slurp(_idx int) string {
|
||||
return ''
|
||||
}
|
||||
@@ -0,0 +1,448 @@
|
||||
import os
|
||||
import time
|
||||
|
||||
const vexe = os.getenv('VEXE')
|
||||
const vroot = os.dir(vexe)
|
||||
const tfolder = os.join_path(os.vtmp_dir(), 'os_process_tests')
|
||||
const test_os_process = os.join_path(tfolder, 'test_os_process.exe')
|
||||
const test_os_process_source = os.join_path(vroot, 'cmd/tools/test_os_process.v')
|
||||
const echo_process_exe_filename = os.join_path(tfolder, 'echo.exe')
|
||||
const echo_process_source_filename = os.join_path(tfolder, 'echo.v')
|
||||
const delayed_output_exe_filename = os.join_path(tfolder, 'delayed_output.exe')
|
||||
const delayed_output_source_filename = os.join_path(tfolder, 'delayed_output.v')
|
||||
const utf16le_output_exe_filename = os.join_path(tfolder, 'utf16le_output.exe')
|
||||
const utf16le_output_source_filename = os.join_path(tfolder, 'utf16le_output.v')
|
||||
const stdin_exit_exe_filename = os.join_path(tfolder, 'stdin_exit.exe')
|
||||
const stdin_exit_source_filename = os.join_path(tfolder, 'stdin_exit.v')
|
||||
const argv_echo_exe_filename = os.join_path(tfolder, 'argv_echo.exe')
|
||||
const argv_echo_source_filename = os.join_path(tfolder, 'argv_echo.v')
|
||||
const echo_process_source_code = '
|
||||
module main
|
||||
import io
|
||||
import os
|
||||
|
||||
fn main() {
|
||||
unbuffer_stdout()
|
||||
mut reader := io.new_buffered_reader(reader: os.stdin(), cap: 1)
|
||||
for {
|
||||
line := reader.read_line()!
|
||||
println(line)
|
||||
eprintln(line)
|
||||
}
|
||||
}
|
||||
'
|
||||
|
||||
const delayed_output_source_code = '
|
||||
module main
|
||||
import time
|
||||
|
||||
fn main() {
|
||||
unbuffer_stdout()
|
||||
time.sleep(500 * time.millisecond)
|
||||
println("late")
|
||||
time.sleep(300 * time.millisecond)
|
||||
}
|
||||
'
|
||||
|
||||
const utf16le_output_source_code = '
|
||||
module main
|
||||
import os
|
||||
|
||||
fn main() {
|
||||
payload := [u8(`O`), 0, `K`, 0, u8(10), 0]
|
||||
mut out := os.stdout()
|
||||
out.write(payload) or { panic(err) }
|
||||
}
|
||||
'
|
||||
|
||||
const stdin_exit_source_code = '
|
||||
module main
|
||||
import os
|
||||
|
||||
fn main() {
|
||||
_ = os.get_raw_line()
|
||||
exit(7)
|
||||
}
|
||||
'
|
||||
|
||||
const argv_echo_source_code = '
|
||||
module main
|
||||
import os
|
||||
|
||||
fn main() {
|
||||
for arg in os.args[1..] {
|
||||
println(arg)
|
||||
}
|
||||
}
|
||||
'
|
||||
|
||||
const echo_wait_timeout = 5 // seconds
|
||||
|
||||
fn testsuite_begin() {
|
||||
os.rmdir_all(tfolder) or {}
|
||||
os.mkdir_all(tfolder)!
|
||||
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('${os.quoted_path(vexe)} -o ${os.quoted_path(test_os_process)} ${os.quoted_path(test_os_process_source)}')
|
||||
}
|
||||
assert os.exists(test_os_process)
|
||||
|
||||
os.write_file(echo_process_source_filename, echo_process_source_code)!
|
||||
os.system('${os.quoted_path(vexe)} -o ${os.quoted_path(echo_process_exe_filename)} ${os.quoted_path(echo_process_source_filename)}')
|
||||
assert os.exists(echo_process_exe_filename)
|
||||
|
||||
os.write_file(delayed_output_source_filename, delayed_output_source_code)!
|
||||
os.system('${os.quoted_path(vexe)} -o ${os.quoted_path(delayed_output_exe_filename)} ${os.quoted_path(delayed_output_source_filename)}')
|
||||
assert os.exists(delayed_output_exe_filename)
|
||||
|
||||
os.write_file(utf16le_output_source_filename, utf16le_output_source_code)!
|
||||
os.system('${os.quoted_path(vexe)} -o ${os.quoted_path(utf16le_output_exe_filename)} ${os.quoted_path(utf16le_output_source_filename)}')
|
||||
assert os.exists(utf16le_output_exe_filename)
|
||||
|
||||
os.write_file(stdin_exit_source_filename, stdin_exit_source_code)!
|
||||
os.system('${os.quoted_path(vexe)} -o ${os.quoted_path(stdin_exit_exe_filename)} ${os.quoted_path(stdin_exit_source_filename)}')
|
||||
assert os.exists(stdin_exit_exe_filename)
|
||||
|
||||
os.write_file(argv_echo_source_filename, argv_echo_source_code)!
|
||||
os.system('${os.quoted_path(vexe)} -o ${os.quoted_path(argv_echo_exe_filename)} ${os.quoted_path(argv_echo_source_filename)}')
|
||||
assert os.exists(argv_echo_exe_filename)
|
||||
}
|
||||
|
||||
fn testsuite_end() {
|
||||
os.rmdir_all(tfolder) or {}
|
||||
}
|
||||
|
||||
fn test_getpid() {
|
||||
eprintln(@FN)
|
||||
pid := os.getpid()
|
||||
eprintln('current pid: ${pid}')
|
||||
assert pid != 0
|
||||
}
|
||||
|
||||
fn test_set_work_folder() {
|
||||
eprintln(@FN)
|
||||
new_work_folder := os.real_path(os.temp_dir())
|
||||
parent_working_folder := os.getwd()
|
||||
dump(new_work_folder)
|
||||
dump(parent_working_folder)
|
||||
if new_work_folder == parent_working_folder {
|
||||
eprintln('... skipping ${@METHOD} because the working folder is the temporary one')
|
||||
return
|
||||
}
|
||||
mut p := os.new_process(test_os_process)
|
||||
p.set_args(['-show_wd', '-target', 'stdout'])
|
||||
p.set_work_folder(new_work_folder)
|
||||
p.set_redirect_stdio()
|
||||
p.wait()
|
||||
assert p.code == 0
|
||||
output := p.stdout_slurp().trim_space()
|
||||
p.close()
|
||||
$if trace_process_output ? {
|
||||
eprintln('p output: "${output}"')
|
||||
}
|
||||
child_work_folder := output.find_between('stdout, WORK_DIR=', '\n').trim_space()
|
||||
dump(child_work_folder)
|
||||
assert child_work_folder == new_work_folder
|
||||
new_parent_work_folder := os.getwd()
|
||||
dump(new_parent_work_folder)
|
||||
assert new_parent_work_folder == parent_working_folder
|
||||
assert new_parent_work_folder != child_work_folder
|
||||
}
|
||||
|
||||
fn test_set_environment() {
|
||||
eprintln(@FN)
|
||||
mut p := os.new_process(test_os_process)
|
||||
p.set_args(['-show_env', '-target', 'stdout'])
|
||||
p.set_environment({
|
||||
'V_OS_TEST_PORT': '1234567890'
|
||||
})
|
||||
p.set_redirect_stdio()
|
||||
p.wait()
|
||||
assert p.code == 0
|
||||
output := p.stdout_slurp().trim_space()
|
||||
p.close()
|
||||
$if trace_process_output ? {
|
||||
eprintln('p output: "${output}"')
|
||||
}
|
||||
assert output.contains('V_OS_TEST_PORT=1234567890'), output
|
||||
}
|
||||
|
||||
fn test_new_process_uses_exact_executable_path_when_folder_contains_spaces() {
|
||||
$if !windows {
|
||||
return
|
||||
}
|
||||
eprintln(@FN)
|
||||
spaced_dir := os.join_path(tfolder, 'spawn path with spaces')
|
||||
os.rmdir_all(spaced_dir) or {}
|
||||
os.mkdir_all(spaced_dir)!
|
||||
spaced_exe := os.join_path(spaced_dir, 'test os process.exe')
|
||||
os.cp(test_os_process, spaced_exe)!
|
||||
|
||||
stale_source := os.join_path(tfolder, 'spawn.v')
|
||||
stale_exe := os.join_path(tfolder, 'spawn.exe')
|
||||
os.write_file(stale_source, 'fn main() {\n\tprintln("stale-prefix-exe")\n}\n')!
|
||||
assert os.system('${os.quoted_path(vexe)} -o ${os.quoted_path(stale_exe)} ${os.quoted_path(stale_source)}') == 0
|
||||
|
||||
mut p := os.new_process(spaced_exe)
|
||||
p.set_args(['-show_env', '-target', 'stdout'])
|
||||
p.set_environment({
|
||||
'V_OS_TEST_PORT': 'exact_path'
|
||||
})
|
||||
p.set_redirect_stdio()
|
||||
p.wait()
|
||||
assert p.code == 0
|
||||
output := p.stdout_slurp().trim_space()
|
||||
errors := p.stderr_slurp().trim_space()
|
||||
p.close()
|
||||
assert output.contains('V_OS_TEST_PORT=exact_path'), 'stdout:\n${output}\nstderr:\n${errors}'
|
||||
assert !output.contains('stale-prefix-exe'), output
|
||||
}
|
||||
|
||||
fn test_new_process_passes_spaced_path_args_on_windows() {
|
||||
$if !windows {
|
||||
return
|
||||
}
|
||||
eprintln(@FN)
|
||||
file_arg := os.join_path(tfolder, 'path with spaces', 'child file.txt')
|
||||
dir_arg := os.join_path(tfolder, 'path with spaces') + '\\'
|
||||
quoted_arg := 'value with "quotes"'
|
||||
mut p := os.new_process(argv_echo_exe_filename)
|
||||
p.set_args([file_arg, dir_arg, quoted_arg, 'marker'])
|
||||
p.set_redirect_stdio()
|
||||
p.wait()
|
||||
assert p.code == 0
|
||||
output := p.stdout_slurp().trim_space()
|
||||
errors := p.stderr_slurp().trim_space()
|
||||
p.close()
|
||||
lines := output.split_into_lines()
|
||||
assert lines.len == 4, 'stdout:\n${output}\nstderr:\n${errors}'
|
||||
assert lines[0] == file_arg
|
||||
assert lines[1] == dir_arg
|
||||
assert lines[2] == quoted_arg
|
||||
assert lines[3] == 'marker'
|
||||
}
|
||||
|
||||
fn test_new_process_uses_path_for_bare_command_names() {
|
||||
$if windows {
|
||||
return
|
||||
}
|
||||
eprintln(@FN)
|
||||
original_path := os.getenv('PATH')
|
||||
defer {
|
||||
os.setenv('PATH', original_path, true)
|
||||
}
|
||||
path_dir := os.join_path(tfolder, 'path_bin')
|
||||
os.rmdir_all(path_dir) or {}
|
||||
os.mkdir_all(path_dir)!
|
||||
path_exe := os.join_path(path_dir, 'process_from_path.exe')
|
||||
os.cp(test_os_process, path_exe)!
|
||||
os.setenv('PATH', '${path_dir}${os.path_delimiter}${original_path}', true)
|
||||
mut p := os.new_process('process_from_path.exe')
|
||||
p.set_args(['-exitcode', '7'])
|
||||
p.set_work_folder(os.real_path(os.temp_dir()))
|
||||
p.wait()
|
||||
assert p.status == .exited
|
||||
assert p.code == 7
|
||||
p.close()
|
||||
}
|
||||
|
||||
fn test_run() {
|
||||
eprintln(@FN)
|
||||
mut p := os.new_process(test_os_process)
|
||||
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() {
|
||||
eprintln(@FN)
|
||||
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() {
|
||||
eprintln(@FN)
|
||||
mut p := os.new_process(test_os_process)
|
||||
p.set_args(['-timeout_ms', '600', '-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('---------------------------')
|
||||
}
|
||||
assert output.contains('stdout, 1'), output
|
||||
assert output.contains('stdout, 2'), output
|
||||
assert output.contains('stdout, 3'), output
|
||||
assert output.contains('stdout, 4'), output
|
||||
assert errors.contains('stderr, 1'), errors
|
||||
assert errors.contains('stderr, 2'), errors
|
||||
assert errors.contains('stderr, 3'), errors
|
||||
assert errors.contains('stderr, 4'), errors
|
||||
}
|
||||
|
||||
fn echo(mut p os.Process, echo_string string) {
|
||||
// append `\n`, as `echo.exe` use `read_line()`
|
||||
p.stdin_write(echo_string + '\n')
|
||||
mut got_echo_back := false
|
||||
mut echo_back := ''
|
||||
mut timeout := 0
|
||||
for p.is_alive() && timeout < echo_wait_timeout * 1000 / 50 {
|
||||
echo_back = p.stdout_read()
|
||||
if echo_back.len > 0 {
|
||||
got_echo_back = true
|
||||
break
|
||||
}
|
||||
time.sleep(50 * time.millisecond)
|
||||
timeout++
|
||||
}
|
||||
assert got_echo_back
|
||||
assert echo_back.trim_space() == echo_string.trim_space()
|
||||
}
|
||||
|
||||
fn test_stdin_write() {
|
||||
eprintln(@FN)
|
||||
echo_exe := $if windows { echo_process_exe_filename } $else { os.find_abs_path_of_executable('cat') or {
|
||||
'/bin/cat'} }
|
||||
mut p := os.new_process(echo_exe)
|
||||
p.set_redirect_stdio()
|
||||
assert p.status != .exited
|
||||
p.run()
|
||||
echo(mut p, 'hello')
|
||||
echo(mut p, 'world')
|
||||
p.signal_kill()
|
||||
p.close()
|
||||
}
|
||||
|
||||
fn test_close_before_wait_preserves_exit_code() {
|
||||
eprintln(@FN)
|
||||
mut p := os.new_process(stdin_exit_exe_filename)
|
||||
p.set_redirect_stdio()
|
||||
p.run()
|
||||
p.stdin_write('hello\n')
|
||||
p.close()
|
||||
p.wait()
|
||||
assert p.status == .exited
|
||||
assert p.code == 7
|
||||
p.close()
|
||||
}
|
||||
|
||||
fn test_stdout_read_returns_immediately_when_no_data_is_pending() {
|
||||
eprintln(@FN)
|
||||
mut p := os.new_process(delayed_output_exe_filename)
|
||||
p.set_redirect_stdio()
|
||||
p.run()
|
||||
defer {
|
||||
if p.is_alive() {
|
||||
p.signal_kill()
|
||||
}
|
||||
p.close()
|
||||
}
|
||||
mut sw := time.new_stopwatch()
|
||||
output := p.stdout_read()
|
||||
elapsed_ms := sw.elapsed().milliseconds()
|
||||
assert output == ''
|
||||
assert elapsed_ms < 300, 'stdout_read blocked for ${elapsed_ms}ms'
|
||||
p.wait()
|
||||
assert p.stdout_slurp().contains('late')
|
||||
}
|
||||
|
||||
fn test_pipe_read_while_process_is_alive() {
|
||||
eprintln(@FN)
|
||||
mut p := os.new_process(test_os_process)
|
||||
p.set_args(['-timeout_ms', '600', '-period_ms', '50'])
|
||||
p.set_redirect_stdio()
|
||||
p.run()
|
||||
mut stdout_output := ''
|
||||
mut stderr_output := ''
|
||||
mut timeout := 0
|
||||
for p.is_alive() && timeout < echo_wait_timeout * 1000 / 20 {
|
||||
if out := p.pipe_read(.stdout) {
|
||||
stdout_output += out
|
||||
}
|
||||
if err := p.pipe_read(.stderr) {
|
||||
stderr_output += err
|
||||
}
|
||||
if stdout_output.len > 0 && stderr_output.len > 0 {
|
||||
break
|
||||
}
|
||||
time.sleep(20 * time.millisecond)
|
||||
timeout++
|
||||
}
|
||||
p.wait()
|
||||
p.close()
|
||||
assert stdout_output.contains('stdout, start'), stdout_output
|
||||
assert stderr_output.contains('stderr, start'), stderr_output
|
||||
}
|
||||
|
||||
fn test_pipe_read_returns_none_after_eof() {
|
||||
eprintln(@FN)
|
||||
mut p := os.new_process(test_os_process)
|
||||
p.set_args(['-timeout_ms', '120', '-period_ms', '50'])
|
||||
p.set_redirect_stdio()
|
||||
p.wait()
|
||||
assert p.code == 0
|
||||
_ = p.stdout_slurp()
|
||||
_ = p.stderr_slurp()
|
||||
assert !p.is_pending(.stdout)
|
||||
assert !p.is_pending(.stderr)
|
||||
if out := p.pipe_read(.stdout) {
|
||||
assert false, 'expected none after stdout EOF, got `${out}`'
|
||||
}
|
||||
if err := p.pipe_read(.stderr) {
|
||||
assert false, 'expected none after stderr EOF, got `${err}`'
|
||||
}
|
||||
p.close()
|
||||
}
|
||||
|
||||
fn test_slurping_utf16le_output_on_windows() {
|
||||
if os.user_os() != 'windows' {
|
||||
return
|
||||
}
|
||||
mut p := os.new_process(utf16le_output_exe_filename)
|
||||
p.set_redirect_stdio()
|
||||
p.wait()
|
||||
assert p.code == 0
|
||||
output := p.stdout_slurp()
|
||||
errors := p.stderr_slurp()
|
||||
p.close()
|
||||
assert output == 'OK\n', output
|
||||
assert errors == ''
|
||||
}
|
||||
@@ -0,0 +1,438 @@
|
||||
module os
|
||||
|
||||
import strings
|
||||
|
||||
fn C.GenerateConsoleCtrlEvent(event u32, pgid u32) bool
|
||||
fn C.GetModuleHandleA(name &char) HMODULE
|
||||
fn C.GetProcAddress(handle voidptr, procname &u8) voidptr
|
||||
fn C.TerminateProcess(process HANDLE, exit_code u32) bool
|
||||
fn C.PeekNamedPipe(hNamedPipe voidptr, lpBuffer voidptr, nBufferSize i32, lpBytesRead voidptr, lpTotalBytesAvail voidptr,
|
||||
lpBytesLeftThisMessage voidptr) bool
|
||||
|
||||
type FN_NTSuspendResume = fn (voidptr) u64
|
||||
|
||||
fn ntdll_fn(name &char) FN_NTSuspendResume {
|
||||
ntdll := C.GetModuleHandleA(c'NTDLL')
|
||||
if ntdll == 0 {
|
||||
return unsafe { FN_NTSuspendResume(0) }
|
||||
}
|
||||
the_fn := FN_NTSuspendResume(C.GetProcAddress(ntdll, voidptr(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)
|
||||
}
|
||||
|
||||
fn close_valid_handle(p voidptr) {
|
||||
h := &&u32(p)
|
||||
if *h != &u32(unsafe { nil }) {
|
||||
C.CloseHandle(*h)
|
||||
unsafe {
|
||||
*h = &u32(nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct WProcess {
|
||||
pub mut:
|
||||
proc_info ProcessInformation
|
||||
command_line [65536]u8
|
||||
child_stdin_read &u32 = unsafe { nil }
|
||||
child_stdin_write &u32 = unsafe { nil }
|
||||
|
||||
child_stdout_read &u32 = unsafe { nil }
|
||||
child_stdout_write &u32 = unsafe { nil }
|
||||
|
||||
child_stderr_read &u32 = unsafe { nil }
|
||||
child_stderr_write &u32 = unsafe { nil }
|
||||
}
|
||||
|
||||
@[manualfree]
|
||||
fn (mut p Process) win_spawn_process() int {
|
||||
mut to_be_freed := []voidptr{cap: 5}
|
||||
defer {
|
||||
for idx := to_be_freed.len - 1; idx >= 0; idx-- {
|
||||
unsafe { free(to_be_freed[idx]) }
|
||||
}
|
||||
unsafe { to_be_freed.free() }
|
||||
}
|
||||
p.filename =
|
||||
abs_path(p.filename) // expand the path to an absolute one, in case we later change the working folder
|
||||
mut wdata := &WProcess{
|
||||
child_stdin_read: unsafe { nil }
|
||||
child_stdin_write: unsafe { nil }
|
||||
child_stdout_read: unsafe { nil }
|
||||
child_stdout_write: unsafe { nil }
|
||||
child_stderr_read: unsafe { nil }
|
||||
child_stderr_write: unsafe { nil }
|
||||
}
|
||||
p.wdata = voidptr(wdata)
|
||||
mut start_info := StartupInfo{
|
||||
lp_reserved2: unsafe { nil }
|
||||
lp_reserved: unsafe { nil }
|
||||
lp_desktop: unsafe { nil }
|
||||
lp_title: unsafe { nil }
|
||||
cb: sizeof(StartupInfo)
|
||||
}
|
||||
if p.use_stdio_ctl {
|
||||
mut sa := SecurityAttributes{}
|
||||
sa.n_length = sizeof(C.SECURITY_ATTRIBUTES)
|
||||
sa.b_inherit_handle = true
|
||||
|
||||
create_pipe_ok0 := C.CreatePipe(voidptr(&wdata.child_stdin_read),
|
||||
voidptr(&wdata.child_stdin_write), voidptr(&sa), 65536)
|
||||
failed_cfn_report_error(create_pipe_ok0, 'CreatePipe stdin')
|
||||
set_handle_info_ok0 := C.SetHandleInformation(wdata.child_stdin_write,
|
||||
C.HANDLE_FLAG_INHERIT, 0)
|
||||
failed_cfn_report_error(set_handle_info_ok0, 'SetHandleInformation')
|
||||
create_pipe_ok1 := C.CreatePipe(voidptr(&wdata.child_stdout_read),
|
||||
voidptr(&wdata.child_stdout_write), voidptr(&sa), 65536)
|
||||
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), 65536)
|
||||
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_read
|
||||
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)
|
||||
}
|
||||
mut cmd := requote_arg(p.filename)
|
||||
if p.args.len > 0 {
|
||||
cmd += ' ' + requote_args(p.args)
|
||||
}
|
||||
cmd_wide_ptr := cmd.to_wide()
|
||||
to_be_freed << cmd_wide_ptr
|
||||
C.ExpandEnvironmentStringsW(cmd_wide_ptr, voidptr(&wdata.command_line[0]), 32768)
|
||||
|
||||
mut creation_flags := if p.create_no_window {
|
||||
int(C.CREATE_NO_WINDOW)
|
||||
} else {
|
||||
int(C.NORMAL_PRIORITY_CLASS)
|
||||
}
|
||||
if p.use_pgroup {
|
||||
creation_flags |= C.CREATE_NEW_PROCESS_GROUP
|
||||
}
|
||||
|
||||
mut application_name_ptr := &u16(unsafe { nil })
|
||||
filename_lc := p.filename.to_lower_ascii()
|
||||
if is_abs_path(p.filename) && !filename_lc.ends_with('.bat') && !filename_lc.ends_with('.cmd') {
|
||||
// Bind CreateProcessW to the exact executable path, instead of relying only on
|
||||
// command-line parsing. That avoids accidental prefix matches for spaced paths like
|
||||
// `C:\work\testing v\program.exe`, where Windows may otherwise resolve a stale
|
||||
// `C:\work\testing.exe`.
|
||||
application_name_ptr = p.filename.to_wide()
|
||||
to_be_freed << application_name_ptr
|
||||
}
|
||||
|
||||
mut work_folder_ptr := voidptr(unsafe { nil })
|
||||
if p.work_folder != '' {
|
||||
work_folder_ptr = p.work_folder.to_wide()
|
||||
to_be_freed << work_folder_ptr
|
||||
}
|
||||
|
||||
mut env_block := []u16{}
|
||||
if p.env.len > 0 {
|
||||
mut env_ptr := &u16(unsafe { nil })
|
||||
|
||||
for e in p.env {
|
||||
// e should in `ABC=123` format
|
||||
env_ptr = e.to_wide()
|
||||
if isnil(env_ptr) {
|
||||
continue
|
||||
}
|
||||
mut i := 0
|
||||
for {
|
||||
character := unsafe { env_ptr[i] }
|
||||
if character == 0 {
|
||||
break
|
||||
}
|
||||
env_block << character
|
||||
i++
|
||||
}
|
||||
env_block << u16(0)
|
||||
to_be_freed << env_ptr
|
||||
}
|
||||
env_block << u16(0)
|
||||
creation_flags |= C.CREATE_UNICODE_ENVIRONMENT
|
||||
defer(fn) {
|
||||
unsafe { env_block.free() }
|
||||
}
|
||||
}
|
||||
|
||||
create_process_ok := C.CreateProcessW(application_name_ptr, voidptr(&wdata.command_line[0]), 0,
|
||||
0, C.TRUE, creation_flags, if env_block.len > 0 {
|
||||
env_block.data
|
||||
} else {
|
||||
0
|
||||
}, work_folder_ptr, voidptr(&start_info), voidptr(&wdata.proc_info))
|
||||
failed_cfn_report_error(create_process_ok, 'CreateProcess')
|
||||
if p.use_stdio_ctl {
|
||||
close_valid_handle(&wdata.child_stdin_read)
|
||||
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 := unsafe { &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 := unsafe { &WProcess(p.wdata) }
|
||||
the_fn(wdata.proc_info.h_process)
|
||||
}
|
||||
|
||||
fn (mut p Process) win_kill_process() {
|
||||
wdata := unsafe { &WProcess(p.wdata) }
|
||||
C.TerminateProcess(wdata.proc_info.h_process, 3)
|
||||
}
|
||||
|
||||
fn (mut p Process) win_term_process() {
|
||||
p.win_kill_process()
|
||||
}
|
||||
|
||||
fn (mut p Process) win_kill_pgroup() {
|
||||
wdata := unsafe { &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 := unsafe { &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_read)
|
||||
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 := unsafe { &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) {
|
||||
mut wdata := unsafe { &WProcess(p.wdata) }
|
||||
if unsafe { wdata == 0 } || idx != 0 {
|
||||
return
|
||||
}
|
||||
mut rhandle := wdata.child_stdin_write
|
||||
if rhandle == 0 {
|
||||
return
|
||||
}
|
||||
mut bytes_write := u32(0)
|
||||
unsafe {
|
||||
C.WriteFile(rhandle, _s.str, u32(_s.len), &bytes_write, 0)
|
||||
}
|
||||
}
|
||||
|
||||
fn (mut p Process) win_read_string(idx int, _maxbytes int) (string, int) {
|
||||
mut wdata := unsafe { &WProcess(p.wdata) }
|
||||
if unsafe { wdata == 0 } {
|
||||
return '', 0
|
||||
}
|
||||
mut rhandle := &u32(unsafe { nil })
|
||||
if idx == 1 {
|
||||
rhandle = wdata.child_stdout_read
|
||||
}
|
||||
if idx == 2 {
|
||||
rhandle = wdata.child_stderr_read
|
||||
}
|
||||
if rhandle == 0 {
|
||||
return '', 0
|
||||
}
|
||||
mut bytes_avail := int(0)
|
||||
if !C.PeekNamedPipe(rhandle, unsafe { nil }, int(0), unsafe { nil }, voidptr(&bytes_avail),
|
||||
unsafe { nil }) {
|
||||
return '', 0
|
||||
}
|
||||
if bytes_avail == 0 {
|
||||
return '', 0
|
||||
}
|
||||
|
||||
mut bytes_read := int(0)
|
||||
buf := []u8{len: bytes_avail + 300}
|
||||
unsafe {
|
||||
C.ReadFile(rhandle, &buf[0], buf.cap, voidptr(&bytes_read), 0)
|
||||
}
|
||||
return decode_windows_captured_output(buf[..bytes_read].bytestr()), bytes_read
|
||||
}
|
||||
|
||||
fn (mut p Process) win_is_pending(idx int) bool {
|
||||
mut wdata := unsafe { &WProcess(p.wdata) }
|
||||
if unsafe { wdata == 0 } {
|
||||
return false
|
||||
}
|
||||
mut rhandle := &u32(unsafe { nil })
|
||||
if idx == 1 {
|
||||
rhandle = wdata.child_stdout_read
|
||||
}
|
||||
if idx == 2 {
|
||||
rhandle = wdata.child_stderr_read
|
||||
}
|
||||
if rhandle == 0 {
|
||||
return false
|
||||
}
|
||||
mut bytes_avail := int(0)
|
||||
if C.PeekNamedPipe(rhandle, 0, 0, 0, &bytes_avail, 0) {
|
||||
return bytes_avail > 0
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
fn (mut p Process) win_slurp(idx int) string {
|
||||
mut wdata := unsafe { &WProcess(p.wdata) }
|
||||
if unsafe { wdata == 0 } {
|
||||
return ''
|
||||
}
|
||||
mut rhandle := &u32(unsafe { nil })
|
||||
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]u8{}
|
||||
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 := decode_windows_captured_output(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_term_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
|
||||
}
|
||||
|
||||
@[manualfree]
|
||||
fn requote_args(cargs []string) string {
|
||||
mut sb := strings.new_builder(128)
|
||||
defer { unsafe { sb.free() } }
|
||||
for idx, a in cargs {
|
||||
if idx > 0 {
|
||||
sb.write_rune(` `)
|
||||
}
|
||||
sb.write_string(requote_arg(a))
|
||||
}
|
||||
res := sb.str()
|
||||
return res
|
||||
}
|
||||
|
||||
fn requote_arg(arg string) string {
|
||||
if arg.len == 0 {
|
||||
return '""'
|
||||
}
|
||||
// Escape a literal argv entry using the same backslash+quote rules that
|
||||
// Windows uses when reconstructing argc/argv from CreateProcessW.
|
||||
mut sb := strings.new_builder(arg.len + 8)
|
||||
defer { unsafe { sb.free() } }
|
||||
sb.write_u8(`"`)
|
||||
mut pending_backslashes := 0
|
||||
for i := 0; i < arg.len; i++ {
|
||||
ch := arg[i]
|
||||
if ch == `\\` {
|
||||
pending_backslashes++
|
||||
continue
|
||||
}
|
||||
if ch == `"` {
|
||||
for _ in 0 .. pending_backslashes * 2 + 1 {
|
||||
sb.write_u8(`\\`)
|
||||
}
|
||||
sb.write_u8(`"`)
|
||||
pending_backslashes = 0
|
||||
continue
|
||||
}
|
||||
for _ in 0 .. pending_backslashes {
|
||||
sb.write_u8(`\\`)
|
||||
}
|
||||
pending_backslashes = 0
|
||||
sb.write_u8(ch)
|
||||
}
|
||||
for _ in 0 .. pending_backslashes * 2 {
|
||||
sb.write_u8(`\\`)
|
||||
}
|
||||
sb.write_u8(`"`)
|
||||
return sb.str()
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
module os
|
||||
|
||||
#include <signal.h>
|
||||
|
||||
// v_signal_with_handler_cast wraps signal() with a cast to avoid
|
||||
// clang -Werror=incompatible-function-pointer-types between
|
||||
// void (*)(os__Signal) and void (*)(int).
|
||||
#define v_signal_with_handler_cast(sig, handler) signal((sig), (void (*)(int))(handler))
|
||||
|
||||
fn C.v_signal_with_handler_cast(signal i32, 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.v_signal_with_handler_cast(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)
|
||||
}
|
||||
|
||||
// A convenient way to ignore certain system signals when there is no need to process certain system signals,
|
||||
// Masking of system signals under posix systems requires a distinction between main and background threads.
|
||||
// Since there is no good way to easily tell whether the current thread is the main or background thread,
|
||||
// So a global variable is introduced to make the distinction.
|
||||
|
||||
// An empty system signal handler (callback function) used to mask the specified system signal.
|
||||
fn ignore_signal_handler(_signal Signal) {
|
||||
}
|
||||
|
||||
// signal_ignore to mask system signals, e.g.: signal_ignore(.pipe, .urg, ...).
|
||||
pub fn signal_ignore(args ...Signal) {
|
||||
if is_main_thread() {
|
||||
// for main thread.
|
||||
$if !windows {
|
||||
for arg in args {
|
||||
signal_opt(arg, ignore_signal_handler) or {}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// for background threads.
|
||||
signal_ignore_internal(...args)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,133 @@
|
||||
module os
|
||||
|
||||
fn signal_str(signal Signal) string {
|
||||
mut result := signal.str().to_upper()
|
||||
result = 'SIG${result}'
|
||||
return result
|
||||
}
|
||||
|
||||
fn signal_from_str(str JS.String) Signal {
|
||||
s := string(str)
|
||||
return match s {
|
||||
'SIGHUP' {
|
||||
Signal.hup
|
||||
}
|
||||
'SIGINT' {
|
||||
Signal.int
|
||||
}
|
||||
'SIGQUIT' {
|
||||
Signal.quit
|
||||
}
|
||||
'SIGILL' {
|
||||
Signal.ill
|
||||
}
|
||||
'SIGTRAP' {
|
||||
Signal.trap
|
||||
}
|
||||
'SIGABRT' {
|
||||
Signal.abrt
|
||||
}
|
||||
'SIGBUS' {
|
||||
Signal.bus
|
||||
}
|
||||
'SIGFPE' {
|
||||
Signal.fpe
|
||||
}
|
||||
'SIGKILL' {
|
||||
Signal.kill
|
||||
}
|
||||
'SIGUSR1' {
|
||||
Signal.usr1
|
||||
}
|
||||
'SIGSEGV' {
|
||||
Signal.segv
|
||||
}
|
||||
'SIGUSR2' {
|
||||
Signal.usr2
|
||||
}
|
||||
'SIGPIPE' {
|
||||
Signal.pipe
|
||||
}
|
||||
'SIGALRM' {
|
||||
Signal.alrm
|
||||
}
|
||||
'SIGTERM' {
|
||||
Signal.term
|
||||
}
|
||||
'SIGSTKFLT' {
|
||||
Signal.stkflt
|
||||
}
|
||||
'SIGCHLD' {
|
||||
Signal.chld
|
||||
}
|
||||
'SIGCONT' {
|
||||
Signal.cont
|
||||
}
|
||||
'SIGSTOP' {
|
||||
Signal.stop
|
||||
}
|
||||
'SIGTSTP' {
|
||||
Signal.tstp
|
||||
}
|
||||
'SIGTTIN' {
|
||||
Signal.ttin
|
||||
}
|
||||
'SIGTTOU' {
|
||||
Signal.ttou
|
||||
}
|
||||
'SIGURG' {
|
||||
Signal.urg
|
||||
}
|
||||
'SIGXCPU' {
|
||||
Signal.xcpu
|
||||
}
|
||||
'SIGXFSZ' {
|
||||
Signal.xfsz
|
||||
}
|
||||
'SIGVTALRM' {
|
||||
Signal.vtalrm
|
||||
}
|
||||
'SIGPROF' {
|
||||
Signal.prof
|
||||
}
|
||||
'SIGWINCH' {
|
||||
Signal.winch
|
||||
}
|
||||
'SIGPOLL' {
|
||||
Signal.poll
|
||||
}
|
||||
'SIGPWR' {
|
||||
Signal.pwr
|
||||
}
|
||||
'SIGSYS' {
|
||||
Signal.sys
|
||||
}
|
||||
else {
|
||||
panic('unknown signal: ${s}')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// signal will assign `handler` callback to be called when `signum` signal is received.
|
||||
//
|
||||
// # Behaviour on different backends:
|
||||
// - NodeJS: Will use `process.on` and add `handler` to the listeners list for `signum` to happen
|
||||
// - Browser: Will use `window.addEventListener` for handling signal
|
||||
//
|
||||
// TODO: Add signal events implementation for browser backend
|
||||
pub fn signal_opt(signum Signal, handler SignalHandler) !SignalHandler {
|
||||
signame := signal_str(signum)
|
||||
_ := signame
|
||||
$if js_node {
|
||||
#$process.on(signame.str,function (sig) { handler(os__signal_from_str(sig));});
|
||||
|
||||
return handler
|
||||
} $else $if js_browser {
|
||||
#let event = new CustomEvent(signame.str, {detail: signum});
|
||||
#window.addEventListener(signame.str, function (e) { handler(e.detail); });
|
||||
|
||||
return handler
|
||||
} $else {
|
||||
return error('signal handlers are not supported on bare JS')
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
module os
|
||||
|
||||
// os.Signal - enumerate possible POSIX signals and
|
||||
// their integer codes.
|
||||
// Note: 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 // hangup
|
||||
int = 2 // interrupt from keyboard
|
||||
quit = 3 // quit from keyboard
|
||||
ill = 4 // illegal instruction
|
||||
trap = 5 // trace trap
|
||||
abrt = 6 // abort
|
||||
bus = 7 // bus error
|
||||
fpe = 8 // floating point exception
|
||||
kill = 9 // kill signal
|
||||
usr1 = 10 // user-defined signal 1
|
||||
segv = 11 // segmentation violation
|
||||
usr2 = 12 // user-defined signal 2
|
||||
pipe = 13 // write on a pipe with no one to read it
|
||||
alrm = 14 // alarm clock
|
||||
term = 15 // software termination signal
|
||||
stkflt = 16 // stack fault
|
||||
chld = 17 // child process has stopped or exited
|
||||
cont = 18 // continue executing, if stopped
|
||||
stop = 19 // stop executing (cannot be caught or ignored)
|
||||
tstp = 20 // stop executing, can be caught and ignored
|
||||
ttin = 21 // terminal input for background process
|
||||
ttou = 22 // terminal output for background process
|
||||
urg = 23 // urgent condition on socket
|
||||
xcpu = 24 // CPU time limit exceeded
|
||||
xfsz = 25 // file size limit exceeded
|
||||
vtalrm = 26 // virtual time alarm
|
||||
prof = 27 // profiling timer alarm
|
||||
winch = 28 // window size change
|
||||
poll = 29 // pollable event occurred
|
||||
pwr = 30 // power failure
|
||||
sys = 31 // bad system call
|
||||
}
|
||||
|
||||
pub type SignalHandler = fn (Signal)
|
||||
@@ -0,0 +1,30 @@
|
||||
@[has_globals]
|
||||
module os
|
||||
|
||||
#flag -lpthread
|
||||
#include <signal.h>
|
||||
|
||||
fn C.pthread_self() usize
|
||||
|
||||
// g_main_thread_id and is_main_thread can be used to determine if the current thread is the main thread.
|
||||
// if need to get the tid of the main thread, can use the global variable g_main_thread_id
|
||||
// instead of using thread_id() every time.
|
||||
__global g_main_thread_id = u64(C.pthread_self())
|
||||
|
||||
// is_main_thread returns whether the current thread is the main thread.
|
||||
pub fn is_main_thread() bool {
|
||||
return g_main_thread_id == u64(C.pthread_self())
|
||||
}
|
||||
|
||||
fn C.sigaddset(set &u32, signum i32) i32
|
||||
fn C.sigemptyset(set &u32)
|
||||
fn C.sigprocmask(how i32, set &u32, oldset &u32) i32
|
||||
|
||||
fn signal_ignore_internal(args ...Signal) {
|
||||
mask1 := u32(0)
|
||||
C.sigemptyset(&mask1)
|
||||
for arg in args {
|
||||
C.sigaddset(&mask1, int(arg))
|
||||
}
|
||||
C.sigprocmask(C.SIG_BLOCK, &mask1, unsafe { nil })
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
@[has_globals]
|
||||
module os
|
||||
|
||||
#flag -lpthread
|
||||
|
||||
fn C.pthread_self() usize
|
||||
|
||||
// g_main_thread_id and is_main_thread can be used to determine if the current thread is the main thread.
|
||||
// if need to get the tid of the main thread, can use the global variable g_main_thread_id
|
||||
// instead of using thread_id() every time.
|
||||
__global g_main_thread_id = u64(C.pthread_self())
|
||||
|
||||
// is_main_thread returns whether the current thread is the main thread.
|
||||
pub fn is_main_thread() bool {
|
||||
return g_main_thread_id == u64(C.pthread_self())
|
||||
}
|
||||
|
||||
fn signal_ignore_internal(_ ...Signal) {
|
||||
// just do nothing by default, since that is most portable ¯\_(ツ)_/¯,
|
||||
// especially on platforms like OpenBSD etc
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
@[has_globals]
|
||||
module os
|
||||
|
||||
#flag -lpthread
|
||||
#include <signal.h>
|
||||
|
||||
fn C.pthread_self() usize
|
||||
|
||||
// g_main_thread_id and is_main_thread can be used to determine if the current thread is the main thread.
|
||||
// if need to get the tid of the main thread, can use the global variable g_main_thread_id
|
||||
// instead of using thread_id() every time.
|
||||
__global g_main_thread_id = u64(C.pthread_self())
|
||||
|
||||
// is_main_thread returns whether the current thread is the main thread.
|
||||
pub fn is_main_thread() bool {
|
||||
return g_main_thread_id == u64(C.pthread_self())
|
||||
}
|
||||
|
||||
@[typedef]
|
||||
struct C.sigset_t {}
|
||||
|
||||
fn C.sigaddset(set &C.sigset_t, signum i32) i32
|
||||
fn C.sigemptyset(set &C.sigset_t)
|
||||
fn C.sigprocmask(how i32, set &C.sigset_t, oldset &C.sigset_t) i32
|
||||
|
||||
fn signal_ignore_internal(args ...Signal) {
|
||||
$if !android && !macos && !openbsd {
|
||||
mask1 := C.sigset_t{}
|
||||
C.sigemptyset(&mask1)
|
||||
for arg in args {
|
||||
C.sigaddset(&mask1, int(arg))
|
||||
}
|
||||
C.sigprocmask(C.SIG_BLOCK, &mask1, unsafe { nil })
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
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.str() == 'Invalid argument; code: 22'
|
||||
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
|
||||
}
|
||||
|
||||
fn signal_ignore_in_background_thread() {
|
||||
os.signal_ignore(.pipe, .urg)
|
||||
assert true
|
||||
}
|
||||
|
||||
fn test_signal_ignore() {
|
||||
os.signal_ignore(.pipe, .urg)
|
||||
assert true
|
||||
t := spawn signal_ignore_in_background_thread()
|
||||
t.wait()
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
@[has_globals]
|
||||
module os
|
||||
|
||||
fn C.GetCurrentThreadId() u32
|
||||
|
||||
// g_main_thread_id and is_main_thread can be used to determine if the current thread is the main thread.
|
||||
// if need to get the tid of the main thread, can use the global variable g_main_thread_id
|
||||
// instead of using thread_id() every time.
|
||||
__global g_main_thread_id = u64(C.GetCurrentThreadId())
|
||||
|
||||
// is_main_thread returns whether the current thread is the main thread.
|
||||
pub fn is_main_thread() bool {
|
||||
return g_main_thread_id == u64(C.GetCurrentThreadId())
|
||||
}
|
||||
|
||||
// The windows platform does not need to be handled.
|
||||
fn signal_ignore_internal(_args ...Signal) {
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
module os
|
||||
|
||||
pub struct C.timespec {
|
||||
pub mut:
|
||||
tv_sec i64
|
||||
tv_nsec i64
|
||||
}
|
||||
|
||||
fn C.nanosleep(req &C.timespec, rem &C.timespec) i32
|
||||
|
||||
// sleep_ms provides a cross platform way to sleep, without having to `import time` for a time.sleep/1 call.
|
||||
fn sleep_ms(ms i64) {
|
||||
$if windows {
|
||||
C.Sleep(u32(ms))
|
||||
} $else {
|
||||
mut req := C.timespec{ms / 1000, 1_000_000 * (ms % 1000)}
|
||||
rem := C.timespec{}
|
||||
for C.nanosleep(&req, &rem) < 0 {
|
||||
if C.errno == C.EINTR {
|
||||
req = rem
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
module os
|
||||
|
||||
// sleep_ms suspends the execution for a given duration (in milliseconds), without having to `import time` for a single time.sleep/1 call.
|
||||
fn sleep_ms(dur i64) {
|
||||
#let now = new Date().getTime()
|
||||
#let toWait = BigInt(dur)
|
||||
#while (new Date().getTime() < now + Number(toWait)) {}
|
||||
|
||||
_ = dur // unused
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
module util
|
||||
|
||||
import os
|
||||
|
||||
// TODO `select` doesn't work with time.Duration for some reason
|
||||
pub fn execute_with_timeout(cmd string, timeout i64) ?os.Result {
|
||||
ch := chan os.Result{cap: 1}
|
||||
spawn fn [cmd] (c chan os.Result) {
|
||||
res := os.execute(cmd)
|
||||
c <- res
|
||||
}(ch)
|
||||
select {
|
||||
a := <-ch {
|
||||
return a
|
||||
}
|
||||
// timeout {
|
||||
// 1000 * time.millisecond {
|
||||
// timeout * time.millisecond {
|
||||
timeout * 1_000_000 {
|
||||
return none
|
||||
}
|
||||
}
|
||||
return os.Result{}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import os
|
||||
|
||||
fn test_data_dir() {
|
||||
d := os.data_dir()
|
||||
assert d.len > 0
|
||||
}
|
||||
|
||||
fn test_state_dir() {
|
||||
d := os.state_dir()
|
||||
assert d.len > 0
|
||||
}
|
||||
|
||||
fn test_local_bin_dir() {
|
||||
d := os.local_bin_dir()
|
||||
assert d.len > 0
|
||||
}
|
||||
Reference in new issue
Block a user