Initial project import

This commit is contained in:
Agila Forge Importer committed 2026-07-13 15:20:32 +00:00
commit 23f2d29e32
11957 files changed
+1754577

No files matched your search

+76
View File
@@ -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()
+51
View File
@@ -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
}
+11
View File
@@ -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.
+44
View File
@@ -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)
}
+82
View File
@@ -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('-'))
}
+37
View File
@@ -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']
}
+80
View File
@@ -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)
}
}
+32
View File
@@ -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
}
+1
View File
@@ -0,0 +1 @@
module os
+25
View File
@@ -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
+144
View File
@@ -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
+41
View File
@@ -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
}
+9
View File
@@ -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
}
+40
View File
@@ -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
}
+45
View File
@@ -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
}
+45
View File
@@ -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
}
+13
View File
@@ -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
}
+160
View File
@@ -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)
}
}
+123
View File
@@ -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
}
+49
View File
@@ -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
}
+70
View File
@@ -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('한국어') == ''
}
+178
View File
@@ -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
+27
View File
@@ -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())
}
+41
View File
@@ -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']
}
}
+153
View File
@@ -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
}
+693
View File
@@ -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
}
+152
View File
@@ -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
}
+42
View File
@@ -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)) }
}
+87
View File
@@ -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']
}
+146
View File
@@ -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')!
}
+73
View File
@@ -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()
}
+542
View File
@@ -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
+174
View File
@@ -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)
}
+74
View File
@@ -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
}
+92
View File
@@ -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
}
+99
View File
@@ -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
}
+313
View File
@@ -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)))
}
+197
View File
@@ -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('') == ''
}
}
+37
View File
@@ -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)
}
+262
View File
@@ -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()
}
}
+75
View File
@@ -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
}
}
+74
View File
@@ -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
}
}
+54
View File
@@ -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
}
+70
View File
@@ -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'
}
}
+227
View File
@@ -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')
}
+202
View File
@@ -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
}
+12
View File
@@ -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
+36
View File
@@ -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
}
+169
View File
@@ -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 := &notifier
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 := &notifier
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 := &notifier
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])
}
+31
View File
@@ -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}')
}
}
+23
View File
@@ -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
+207
View File
@@ -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
}
+1254
View File
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')
}
}
+14
View File
@@ -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
}
+12
View File
@@ -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
+198
View File
@@ -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
}
+15
View File
@@ -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
+620
View File
@@ -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)
}
}
+129
View File
@@ -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
}
+69
View File
@@ -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.
}
}
+109
View File
@@ -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
}
+23
View File
@@ -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
+855
View File
@@ -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
}
}
+27
View File
@@ -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
}
+26
View File
@@ -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
}
+213
View File
@@ -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
}
+164
View File
@@ -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')
}
+351
View File
@@ -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()
}
+126
View File
@@ -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
}
+79
View File
@@ -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
}
+59
View File
@@ -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']")
}
+185
View File
@@ -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 ''
}
+448
View File
@@ -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 == ''
}
+438
View File
@@ -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()
}
+45
View File
@@ -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)
}
}
+133
View File
@@ -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')
}
}
+43
View File
@@ -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)
+30
View File
@@ -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 })
}
+21
View File
@@ -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
}
+35
View File
@@ -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 })
}
}
+48
View File
@@ -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()
}
+18
View File
@@ -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) {
}
+26
View File
@@ -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
}
}
}
}
+10
View File
@@ -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
}
+24
View File
@@ -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{}
}
+16
View File
@@ -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
}