mirror of
https://codeberg.org/ziglang/zig.git
synced 2025-12-06 05:44:20 +00:00
9425 lines
364 KiB
Zig
9425 lines
364 KiB
Zig
const Threaded = @This();
|
||
|
||
const builtin = @import("builtin");
|
||
const native_os = builtin.os.tag;
|
||
const is_windows = native_os == .windows;
|
||
const windows = std.os.windows;
|
||
const ws2_32 = std.os.windows.ws2_32;
|
||
const is_debug = builtin.mode == .Debug;
|
||
|
||
const std = @import("../std.zig");
|
||
const Io = std.Io;
|
||
const net = std.Io.net;
|
||
const HostName = std.Io.net.HostName;
|
||
const IpAddress = std.Io.net.IpAddress;
|
||
const Allocator = std.mem.Allocator;
|
||
const Alignment = std.mem.Alignment;
|
||
const assert = std.debug.assert;
|
||
const posix = std.posix;
|
||
|
||
/// Thread-safe.
|
||
allocator: Allocator,
|
||
mutex: std.Thread.Mutex = .{},
|
||
cond: std.Thread.Condition = .{},
|
||
run_queue: std.SinglyLinkedList = .{},
|
||
join_requested: bool = false,
|
||
stack_size: usize,
|
||
/// All threads are spawned detached; this is how we wait until they all exit.
|
||
wait_group: std.Thread.WaitGroup = .{},
|
||
/// Maximum thread pool size (excluding main thread) when dispatching async
|
||
/// tasks. Until this limit, calls to `Io.async` when all threads are busy will
|
||
/// cause a new thread to be spawned and permanently added to the pool. After
|
||
/// this limit, calls to `Io.async` when all threads are busy run the task
|
||
/// immediately.
|
||
///
|
||
/// Defaults to a number equal to logical CPU cores.
|
||
///
|
||
/// Protected by `mutex` once the I/O instance is already in use. See
|
||
/// `setAsyncLimit`.
|
||
async_limit: Io.Limit,
|
||
/// Maximum thread pool size (excluding main thread) for dispatching concurrent
|
||
/// tasks. Until this limit, calls to `Io.concurrent` will increase the thread
|
||
/// pool size.
|
||
///
|
||
/// concurrent tasks. After this number, calls to `Io.concurrent` return
|
||
/// `error.ConcurrencyUnavailable`.
|
||
concurrent_limit: Io.Limit = .unlimited,
|
||
/// Error from calling `std.Thread.getCpuCount` in `init`.
|
||
cpu_count_error: ?std.Thread.CpuCountError,
|
||
/// Number of threads that are unavailable to take tasks. To calculate
|
||
/// available count, subtract this from either `async_limit` or
|
||
/// `concurrent_limit`.
|
||
busy_count: usize = 0,
|
||
main_thread: Thread,
|
||
pid: Pid = .unknown,
|
||
/// When a cancel request is made, blocking syscalls can be unblocked by
|
||
/// issuing a signal. However, if the signal arrives after the check and before
|
||
/// the syscall instruction, it is missed.
|
||
///
|
||
/// This option solves the race condition by retrying the signal delivery
|
||
/// until it is acknowledged, with an exponential backoff.
|
||
///
|
||
/// Unfortunately, trying again until the cancellation request is acknowledged
|
||
/// has been observed to be relatively slow, and usually strong cancellation
|
||
/// guarantees are not needed, so this defaults to off.
|
||
robust_cancel: RobustCancel = .disabled,
|
||
|
||
wsa: if (is_windows) Wsa else struct {} = .{},
|
||
|
||
have_signal_handler: bool,
|
||
old_sig_io: if (have_sig_io) posix.Sigaction else void,
|
||
old_sig_pipe: if (have_sig_pipe) posix.Sigaction else void,
|
||
|
||
pub const RobustCancel = if (std.Thread.use_pthreads or native_os == .linux) enum {
|
||
enabled,
|
||
disabled,
|
||
} else enum {
|
||
disabled,
|
||
};
|
||
|
||
pub const Pid = if (native_os == .linux) enum(posix.pid_t) {
|
||
unknown = 0,
|
||
_,
|
||
} else enum(u0) { unknown = 0 };
|
||
|
||
const Thread = struct {
|
||
/// The value that needs to be passed to pthread_kill or tgkill in order to
|
||
/// send a signal.
|
||
signal_id: SignaleeId,
|
||
current_closure: ?*Closure = null,
|
||
|
||
const SignaleeId = if (std.Thread.use_pthreads) std.c.pthread_t else std.Thread.Id;
|
||
|
||
threadlocal var current: ?*Thread = null;
|
||
|
||
fn getCurrent(t: *Threaded) *Thread {
|
||
return current orelse return &t.main_thread;
|
||
}
|
||
|
||
fn checkCancel(thread: *Thread) error{Canceled}!void {
|
||
const closure = thread.current_closure orelse return;
|
||
switch (@cmpxchgStrong(
|
||
CancelStatus,
|
||
&closure.cancel_status,
|
||
.requested,
|
||
.acknowledged,
|
||
.acq_rel,
|
||
.acquire,
|
||
) orelse return error.Canceled) {
|
||
.requested => unreachable,
|
||
.acknowledged => unreachable,
|
||
.none, _ => {},
|
||
}
|
||
}
|
||
|
||
fn beginSyscall(thread: *Thread) error{Canceled}!void {
|
||
const closure = thread.current_closure orelse return;
|
||
|
||
switch (@cmpxchgStrong(
|
||
CancelStatus,
|
||
&closure.cancel_status,
|
||
.none,
|
||
.fromSignaleeId(thread.signal_id),
|
||
.acq_rel,
|
||
.acquire,
|
||
) orelse return) {
|
||
.none => unreachable,
|
||
.requested => {
|
||
@atomicStore(CancelStatus, &closure.cancel_status, .acknowledged, .release);
|
||
return error.Canceled;
|
||
},
|
||
.acknowledged => return,
|
||
_ => unreachable,
|
||
}
|
||
}
|
||
|
||
fn endSyscall(thread: *Thread) void {
|
||
const closure = thread.current_closure orelse return;
|
||
_ = @cmpxchgStrong(
|
||
CancelStatus,
|
||
&closure.cancel_status,
|
||
.fromSignaleeId(thread.signal_id),
|
||
.none,
|
||
.acq_rel,
|
||
.acquire,
|
||
) orelse return;
|
||
}
|
||
|
||
fn endSyscallCanceled(thread: *Thread) Io.Cancelable {
|
||
if (thread.current_closure) |closure| {
|
||
@atomicStore(CancelStatus, &closure.cancel_status, .acknowledged, .release);
|
||
}
|
||
return error.Canceled;
|
||
}
|
||
|
||
fn currentSignalId() SignaleeId {
|
||
return if (std.Thread.use_pthreads) std.c.pthread_self() else std.Thread.getCurrentId();
|
||
}
|
||
};
|
||
|
||
const max_iovecs_len = 8;
|
||
const splat_buffer_size = 64;
|
||
|
||
comptime {
|
||
if (@TypeOf(posix.IOV_MAX) != void) assert(max_iovecs_len <= posix.IOV_MAX);
|
||
}
|
||
|
||
const CancelStatus = enum(usize) {
|
||
/// Cancellation has neither been requested, nor checked. The async
|
||
/// operation will check status before entering a blocking syscall.
|
||
/// This is also the status used for uninteruptible tasks.
|
||
none = 0,
|
||
/// Cancellation has been requested and the status will be checked before
|
||
/// entering a blocking syscall.
|
||
requested = std.math.maxInt(usize) - 1,
|
||
/// Cancellation has been acknowledged and is in progress. Signals should
|
||
/// not be sent.
|
||
acknowledged = std.math.maxInt(usize),
|
||
/// Stores a `Thread.SignaleeId` and indicates that sending a signal to this thread
|
||
/// is needed in order to cancel. This state is set before going into
|
||
/// a blocking operation that needs to get unblocked via signal.
|
||
_,
|
||
|
||
const Unpacked = union(enum) {
|
||
none,
|
||
requested,
|
||
acknowledged,
|
||
signal_id: Thread.SignaleeId,
|
||
};
|
||
|
||
fn unpack(cs: CancelStatus) Unpacked {
|
||
return switch (cs) {
|
||
.none => .none,
|
||
.requested => .requested,
|
||
.acknowledged => .acknowledged,
|
||
_ => |signal_id| .{
|
||
.signal_id = if (std.Thread.use_pthreads)
|
||
@ptrFromInt(@intFromEnum(signal_id))
|
||
else
|
||
@truncate(@intFromEnum(signal_id)),
|
||
},
|
||
};
|
||
}
|
||
|
||
fn fromSignaleeId(signal_id: Thread.SignaleeId) CancelStatus {
|
||
return if (std.Thread.use_pthreads)
|
||
@enumFromInt(@intFromPtr(signal_id))
|
||
else
|
||
@enumFromInt(signal_id);
|
||
}
|
||
};
|
||
|
||
const Closure = struct {
|
||
start: Start,
|
||
node: std.SinglyLinkedList.Node = .{},
|
||
cancel_status: CancelStatus,
|
||
|
||
const Start = *const fn (*Closure, *Threaded) void;
|
||
|
||
fn requestCancel(closure: *Closure, t: *Threaded) void {
|
||
var signal_id = switch (@atomicRmw(CancelStatus, &closure.cancel_status, .Xchg, .requested, .monotonic).unpack()) {
|
||
.none, .acknowledged, .requested => return,
|
||
.signal_id => |signal_id| signal_id,
|
||
};
|
||
// The task will enter a blocking syscall before checking for cancellation again.
|
||
// We can send a signal to interrupt the syscall, but if it arrives before
|
||
// the syscall instruction, it will be missed. Therefore, this code tries
|
||
// again until the cancellation request is acknowledged.
|
||
|
||
// 1 << 10 ns is about 1 microsecond, approximately syscall overhead.
|
||
// 1 << 20 ns is about 1 millisecond.
|
||
// 1 << 30 ns is about 1 second.
|
||
//
|
||
// On a heavily loaded Linux 6.17.5, I observed a maximum of 20
|
||
// attempts not acknowledged before the timeout (including exponential
|
||
// backoff) was sufficient, despite the heavy load.
|
||
const max_attempts = 22;
|
||
|
||
for (0..max_attempts) |attempt_index| {
|
||
if (std.Thread.use_pthreads) {
|
||
if (std.c.pthread_kill(signal_id, .IO) != 0) return;
|
||
} else if (native_os == .linux) {
|
||
const pid: posix.pid_t = p: {
|
||
const cached_pid = @atomicLoad(Pid, &t.pid, .monotonic);
|
||
if (cached_pid != .unknown) break :p @intFromEnum(cached_pid);
|
||
const pid = std.os.linux.getpid();
|
||
@atomicStore(Pid, &t.pid, @enumFromInt(pid), .monotonic);
|
||
break :p pid;
|
||
};
|
||
if (std.os.linux.tgkill(pid, @bitCast(signal_id), .IO) != 0) return;
|
||
} else {
|
||
return;
|
||
}
|
||
|
||
if (t.robust_cancel != .enabled) return;
|
||
|
||
var timespec: posix.timespec = .{
|
||
.sec = 0,
|
||
.nsec = @as(isize, 1) << @intCast(attempt_index),
|
||
};
|
||
if (native_os == .linux) {
|
||
_ = std.os.linux.clock_nanosleep(posix.CLOCK.MONOTONIC, .{ .ABSTIME = false }, ×pec, ×pec);
|
||
} else {
|
||
_ = posix.system.nanosleep(×pec, ×pec);
|
||
}
|
||
|
||
switch (@atomicRmw(CancelStatus, &closure.cancel_status, .Xchg, .requested, .monotonic).unpack()) {
|
||
.requested => continue, // Retry needed in case other thread hasn't yet entered the syscall.
|
||
.none, .acknowledged => return,
|
||
.signal_id => |new_signal_id| signal_id = new_signal_id,
|
||
}
|
||
}
|
||
}
|
||
};
|
||
|
||
/// Related:
|
||
/// * `init_single_threaded`
|
||
pub fn init(
|
||
/// Must be threadsafe. Only used for the following functions:
|
||
/// * `Io.VTable.async`
|
||
/// * `Io.VTable.concurrent`
|
||
/// * `Io.VTable.groupAsync`
|
||
/// * `Io.VTable.groupConcurrent`
|
||
/// If these functions are avoided, then `Allocator.failing` may be passed
|
||
/// here.
|
||
gpa: Allocator,
|
||
) Threaded {
|
||
if (builtin.single_threaded) return .init_single_threaded;
|
||
|
||
const cpu_count = std.Thread.getCpuCount();
|
||
|
||
var t: Threaded = .{
|
||
.allocator = gpa,
|
||
.stack_size = std.Thread.SpawnConfig.default_stack_size,
|
||
.async_limit = if (cpu_count) |n| .limited(n - 1) else |_| .nothing,
|
||
.cpu_count_error = if (cpu_count) |_| null else |e| e,
|
||
.old_sig_io = undefined,
|
||
.old_sig_pipe = undefined,
|
||
.have_signal_handler = false,
|
||
.main_thread = .{
|
||
.signal_id = Thread.currentSignalId(),
|
||
},
|
||
};
|
||
|
||
if (posix.Sigaction != void) {
|
||
// This causes sending `posix.SIG.IO` to thread to interrupt blocking
|
||
// syscalls, returning `posix.E.INTR`.
|
||
const act: posix.Sigaction = .{
|
||
.handler = .{ .handler = doNothingSignalHandler },
|
||
.mask = posix.sigemptyset(),
|
||
.flags = 0,
|
||
};
|
||
if (have_sig_io) posix.sigaction(.IO, &act, &t.old_sig_io);
|
||
if (have_sig_pipe) posix.sigaction(.PIPE, &act, &t.old_sig_pipe);
|
||
t.have_signal_handler = true;
|
||
}
|
||
|
||
return t;
|
||
}
|
||
|
||
/// Statically initialize such that calls to `Io.VTable.concurrent` will fail
|
||
/// with `error.ConcurrencyUnavailable`.
|
||
///
|
||
/// When initialized this way:
|
||
/// * cancel requests have no effect.
|
||
/// * `deinit` is safe, but unnecessary to call.
|
||
pub const init_single_threaded: Threaded = .{
|
||
.allocator = .failing,
|
||
.stack_size = std.Thread.SpawnConfig.default_stack_size,
|
||
.async_limit = .nothing,
|
||
.cpu_count_error = null,
|
||
.concurrent_limit = .nothing,
|
||
.old_sig_io = undefined,
|
||
.old_sig_pipe = undefined,
|
||
.have_signal_handler = false,
|
||
.main_thread = .{ .signal_id = undefined },
|
||
};
|
||
|
||
pub fn setAsyncLimit(t: *Threaded, new_limit: Io.Limit) void {
|
||
t.mutex.lock();
|
||
defer t.mutex.unlock();
|
||
t.async_limit = new_limit;
|
||
}
|
||
|
||
pub fn deinit(t: *Threaded) void {
|
||
t.join();
|
||
if (is_windows and t.wsa.status == .initialized) {
|
||
if (ws2_32.WSACleanup() != 0) recoverableOsBugDetected();
|
||
}
|
||
if (posix.Sigaction != void and t.have_signal_handler) {
|
||
if (have_sig_io) posix.sigaction(.IO, &t.old_sig_io, null);
|
||
if (have_sig_pipe) posix.sigaction(.PIPE, &t.old_sig_pipe, null);
|
||
}
|
||
t.* = undefined;
|
||
}
|
||
|
||
fn join(t: *Threaded) void {
|
||
if (builtin.single_threaded) return;
|
||
{
|
||
t.mutex.lock();
|
||
defer t.mutex.unlock();
|
||
t.join_requested = true;
|
||
}
|
||
t.cond.broadcast();
|
||
t.wait_group.wait();
|
||
}
|
||
|
||
fn worker(t: *Threaded) void {
|
||
var thread: Thread = .{
|
||
.signal_id = Thread.currentSignalId(),
|
||
};
|
||
Thread.current = &thread;
|
||
|
||
defer t.wait_group.finish();
|
||
|
||
t.mutex.lock();
|
||
defer t.mutex.unlock();
|
||
|
||
while (true) {
|
||
while (t.run_queue.popFirst()) |closure_node| {
|
||
t.mutex.unlock();
|
||
const closure: *Closure = @fieldParentPtr("node", closure_node);
|
||
closure.start(closure, t);
|
||
t.mutex.lock();
|
||
t.busy_count -= 1;
|
||
}
|
||
if (t.join_requested) break;
|
||
t.cond.wait(&t.mutex);
|
||
}
|
||
}
|
||
|
||
pub fn io(t: *Threaded) Io {
|
||
return .{
|
||
.userdata = t,
|
||
.vtable = &.{
|
||
.async = async,
|
||
.concurrent = concurrent,
|
||
.await = await,
|
||
.cancel = cancel,
|
||
.select = select,
|
||
|
||
.groupAsync = groupAsync,
|
||
.groupConcurrent = groupConcurrent,
|
||
.groupWait = groupWait,
|
||
.groupCancel = groupCancel,
|
||
|
||
.mutexLock = mutexLock,
|
||
.mutexLockUncancelable = mutexLockUncancelable,
|
||
.mutexUnlock = mutexUnlock,
|
||
|
||
.conditionWait = conditionWait,
|
||
.conditionWaitUncancelable = conditionWaitUncancelable,
|
||
.conditionWake = conditionWake,
|
||
|
||
.dirMake = dirMake,
|
||
.dirMakePath = dirMakePath,
|
||
.dirMakeOpenPath = dirMakeOpenPath,
|
||
.dirStat = dirStat,
|
||
.dirStatPath = dirStatPath,
|
||
.dirAccess = dirAccess,
|
||
.dirCreateFile = dirCreateFile,
|
||
.dirOpenFile = dirOpenFile,
|
||
.dirOpenDir = dirOpenDir,
|
||
.dirClose = dirClose,
|
||
.dirRealPath = dirRealPath,
|
||
.dirDeleteFile = dirDeleteFile,
|
||
.dirDeleteDir = dirDeleteDir,
|
||
.dirRename = dirRename,
|
||
.dirSymLink = dirSymLink,
|
||
.dirReadLink = dirReadLink,
|
||
.dirSetOwner = dirSetOwner,
|
||
.dirSetPermissions = dirSetPermissions,
|
||
.dirSetTimestamps = dirSetTimestamps,
|
||
.dirSetTimestampsNow = dirSetTimestampsNow,
|
||
|
||
.fileStat = fileStat,
|
||
.fileClose = fileClose,
|
||
.fileWriteStreaming = fileWriteStreaming,
|
||
.fileWritePositional = fileWritePositional,
|
||
.fileReadStreaming = fileReadStreaming,
|
||
.fileReadPositional = fileReadPositional,
|
||
.fileSeekBy = fileSeekBy,
|
||
.fileSeekTo = fileSeekTo,
|
||
.openSelfExe = openSelfExe,
|
||
.fileSync = fileSync,
|
||
.fileIsTty = fileIsTty,
|
||
.fileEnableAnsiEscapeCodes = fileEnableAnsiEscapeCodes,
|
||
.fileSupportsAnsiEscapeCodes = fileSupportsAnsiEscapeCodes,
|
||
.fileSetLength = fileSetLength,
|
||
.fileSetOwner = fileSetOwner,
|
||
.fileSetPermissions = fileSetPermissions,
|
||
.fileSetTimestamps = fileSetTimestamps,
|
||
.fileSetTimestampsNow = fileSetTimestampsNow,
|
||
.fileLock = fileLock,
|
||
.fileTryLock = fileTryLock,
|
||
.fileUnlock = fileUnlock,
|
||
.fileDowngradeLock = fileDowngradeLock,
|
||
|
||
.now = now,
|
||
.sleep = sleep,
|
||
|
||
.netListenIp = switch (native_os) {
|
||
.windows => netListenIpWindows,
|
||
else => netListenIpPosix,
|
||
},
|
||
.netListenUnix = switch (native_os) {
|
||
.windows => netListenUnixWindows,
|
||
else => netListenUnixPosix,
|
||
},
|
||
.netAccept = switch (native_os) {
|
||
.windows => netAcceptWindows,
|
||
else => netAcceptPosix,
|
||
},
|
||
.netBindIp = switch (native_os) {
|
||
.windows => netBindIpWindows,
|
||
else => netBindIpPosix,
|
||
},
|
||
.netConnectIp = switch (native_os) {
|
||
.windows => netConnectIpWindows,
|
||
else => netConnectIpPosix,
|
||
},
|
||
.netConnectUnix = switch (native_os) {
|
||
.windows => netConnectUnixWindows,
|
||
else => netConnectUnixPosix,
|
||
},
|
||
.netClose = netClose,
|
||
.netRead = switch (native_os) {
|
||
.windows => netReadWindows,
|
||
else => netReadPosix,
|
||
},
|
||
.netWrite = switch (native_os) {
|
||
.windows => netWriteWindows,
|
||
else => netWritePosix,
|
||
},
|
||
.netSend = switch (native_os) {
|
||
.windows => netSendWindows,
|
||
else => netSendPosix,
|
||
},
|
||
.netReceive = switch (native_os) {
|
||
.windows => netReceiveWindows,
|
||
else => netReceivePosix,
|
||
},
|
||
.netInterfaceNameResolve = netInterfaceNameResolve,
|
||
.netInterfaceName = netInterfaceName,
|
||
.netLookup = netLookup,
|
||
},
|
||
};
|
||
}
|
||
|
||
/// Same as `io` but disables all networking functionality, which has
|
||
/// an additional dependency on Windows (ws2_32).
|
||
pub fn ioBasic(t: *Threaded) Io {
|
||
return .{
|
||
.userdata = t,
|
||
.vtable = &.{
|
||
.async = async,
|
||
.concurrent = concurrent,
|
||
.await = await,
|
||
.cancel = cancel,
|
||
.select = select,
|
||
|
||
.groupAsync = groupAsync,
|
||
.groupConcurrent = groupConcurrent,
|
||
.groupWait = groupWait,
|
||
.groupCancel = groupCancel,
|
||
|
||
.mutexLock = mutexLock,
|
||
.mutexLockUncancelable = mutexLockUncancelable,
|
||
.mutexUnlock = mutexUnlock,
|
||
|
||
.conditionWait = conditionWait,
|
||
.conditionWaitUncancelable = conditionWaitUncancelable,
|
||
.conditionWake = conditionWake,
|
||
|
||
.dirMake = dirMake,
|
||
.dirMakePath = dirMakePath,
|
||
.dirMakeOpenPath = dirMakeOpenPath,
|
||
.dirStat = dirStat,
|
||
.dirStatPath = dirStatPath,
|
||
.dirAccess = dirAccess,
|
||
.dirCreateFile = dirCreateFile,
|
||
.dirOpenFile = dirOpenFile,
|
||
.dirOpenDir = dirOpenDir,
|
||
.dirClose = dirClose,
|
||
.dirRealPath = dirRealPath,
|
||
.dirDeleteFile = dirDeleteFile,
|
||
.dirDeleteDir = dirDeleteDir,
|
||
.dirRename = dirRename,
|
||
.dirSymLink = dirSymLink,
|
||
.dirReadLink = dirReadLink,
|
||
.dirSetOwner = dirSetOwner,
|
||
.dirSetPermissions = dirSetPermissions,
|
||
.dirSetTimestamps = dirSetTimestamps,
|
||
.dirSetTimestampsNow = dirSetTimestampsNow,
|
||
|
||
.fileStat = fileStat,
|
||
.fileClose = fileClose,
|
||
.fileWriteStreaming = fileWriteStreaming,
|
||
.fileWritePositional = fileWritePositional,
|
||
.fileReadStreaming = fileReadStreaming,
|
||
.fileReadPositional = fileReadPositional,
|
||
.fileSeekBy = fileSeekBy,
|
||
.fileSeekTo = fileSeekTo,
|
||
.openSelfExe = openSelfExe,
|
||
.fileSync = fileSync,
|
||
.fileIsTty = fileIsTty,
|
||
.fileEnableAnsiEscapeCodes = fileEnableAnsiEscapeCodes,
|
||
.fileSupportsAnsiEscapeCodes = fileSupportsAnsiEscapeCodes,
|
||
.fileSetLength = fileSetLength,
|
||
.fileSetOwner = fileSetOwner,
|
||
.fileSetPermissions = fileSetPermissions,
|
||
.fileSetTimestamps = fileSetTimestamps,
|
||
.fileSetTimestampsNow = fileSetTimestampsNow,
|
||
.fileLock = fileLock,
|
||
.fileTryLock = fileTryLock,
|
||
.fileUnlock = fileUnlock,
|
||
.fileDowngradeLock = fileDowngradeLock,
|
||
|
||
.now = now,
|
||
.sleep = sleep,
|
||
|
||
.netListenIp = netListenIpUnavailable,
|
||
.netListenUnix = netListenUnixUnavailable,
|
||
.netAccept = netAcceptUnavailable,
|
||
.netBindIp = netBindIpUnavailable,
|
||
.netConnectIp = netConnectIpUnavailable,
|
||
.netConnectUnix = netConnectUnixUnavailable,
|
||
.netClose = netCloseUnavailable,
|
||
.netRead = netReadUnavailable,
|
||
.netWrite = netWriteUnavailable,
|
||
.netSend = netSendUnavailable,
|
||
.netReceive = netReceiveUnavailable,
|
||
.netInterfaceNameResolve = netInterfaceNameResolveUnavailable,
|
||
.netInterfaceName = netInterfaceNameUnavailable,
|
||
.netLookup = netLookupUnavailable,
|
||
},
|
||
};
|
||
}
|
||
|
||
pub const socket_flags_unsupported = native_os.isDarwin() or native_os == .haiku;
|
||
const have_accept4 = !socket_flags_unsupported;
|
||
const have_flock_open_flags = @hasField(posix.O, "EXLOCK");
|
||
const have_networking = native_os != .wasi;
|
||
const have_flock = @TypeOf(posix.system.flock) != void;
|
||
const have_sendmmsg = native_os == .linux;
|
||
const have_futex = switch (builtin.cpu.arch) {
|
||
.wasm32, .wasm64 => builtin.cpu.has(.wasm, .atomics),
|
||
else => true,
|
||
};
|
||
const have_preadv = switch (native_os) {
|
||
.windows, .haiku => false,
|
||
else => true,
|
||
};
|
||
const have_sig_io = posix.SIG != void and @hasField(posix.SIG, "IO");
|
||
const have_sig_pipe = posix.SIG != void and @hasField(posix.SIG, "PIPE");
|
||
|
||
const openat_sym = if (posix.lfs64_abi) posix.system.openat64 else posix.system.openat;
|
||
const fstat_sym = if (posix.lfs64_abi) posix.system.fstat64 else posix.system.fstat;
|
||
const fstatat_sym = if (posix.lfs64_abi) posix.system.fstatat64 else posix.system.fstatat;
|
||
const lseek_sym = if (posix.lfs64_abi) posix.system.lseek64 else posix.system.lseek;
|
||
const preadv_sym = if (posix.lfs64_abi) posix.system.preadv64 else posix.system.preadv;
|
||
const ftruncate_sym = if (posix.lfs64_abi) posix.system.ftruncate64 else posix.system.ftruncate;
|
||
|
||
/// Trailing data:
|
||
/// 1. context
|
||
/// 2. result
|
||
const AsyncClosure = struct {
|
||
closure: Closure,
|
||
func: *const fn (context: *anyopaque, result: *anyopaque) void,
|
||
reset_event: ResetEvent,
|
||
select_condition: ?*ResetEvent,
|
||
context_alignment: Alignment,
|
||
result_offset: usize,
|
||
alloc_len: usize,
|
||
|
||
const done_reset_event: *ResetEvent = @ptrFromInt(@alignOf(ResetEvent));
|
||
|
||
fn start(closure: *Closure, t: *Threaded) void {
|
||
const ac: *AsyncClosure = @alignCast(@fieldParentPtr("closure", closure));
|
||
const current_thread = Thread.getCurrent(t);
|
||
current_thread.current_closure = closure;
|
||
ac.func(ac.contextPointer(), ac.resultPointer());
|
||
current_thread.current_closure = null;
|
||
|
||
if (@atomicRmw(?*ResetEvent, &ac.select_condition, .Xchg, done_reset_event, .release)) |select_reset| {
|
||
assert(select_reset != done_reset_event);
|
||
select_reset.set();
|
||
}
|
||
ac.reset_event.set();
|
||
}
|
||
|
||
fn resultPointer(ac: *AsyncClosure) [*]u8 {
|
||
const base: [*]u8 = @ptrCast(ac);
|
||
return base + ac.result_offset;
|
||
}
|
||
|
||
fn contextPointer(ac: *AsyncClosure) [*]u8 {
|
||
const base: [*]u8 = @ptrCast(ac);
|
||
const context_offset = ac.context_alignment.forward(@intFromPtr(ac) + @sizeOf(AsyncClosure)) - @intFromPtr(ac);
|
||
return base + context_offset;
|
||
}
|
||
|
||
fn init(
|
||
gpa: Allocator,
|
||
result_len: usize,
|
||
result_alignment: Alignment,
|
||
context: []const u8,
|
||
context_alignment: Alignment,
|
||
func: *const fn (context: *const anyopaque, result: *anyopaque) void,
|
||
) Allocator.Error!*AsyncClosure {
|
||
const max_context_misalignment = context_alignment.toByteUnits() -| @alignOf(AsyncClosure);
|
||
const worst_case_context_offset = context_alignment.forward(@sizeOf(AsyncClosure) + max_context_misalignment);
|
||
const worst_case_result_offset = result_alignment.forward(worst_case_context_offset + context.len);
|
||
const alloc_len = worst_case_result_offset + result_len;
|
||
|
||
const ac: *AsyncClosure = @ptrCast(@alignCast(try gpa.alignedAlloc(u8, .of(AsyncClosure), alloc_len)));
|
||
errdefer comptime unreachable;
|
||
|
||
const actual_context_addr = context_alignment.forward(@intFromPtr(ac) + @sizeOf(AsyncClosure));
|
||
const actual_result_addr = result_alignment.forward(actual_context_addr + context.len);
|
||
const actual_result_offset = actual_result_addr - @intFromPtr(ac);
|
||
ac.* = .{
|
||
.closure = .{
|
||
.cancel_status = .none,
|
||
.start = start,
|
||
},
|
||
.func = func,
|
||
.context_alignment = context_alignment,
|
||
.result_offset = actual_result_offset,
|
||
.alloc_len = alloc_len,
|
||
.reset_event = .unset,
|
||
.select_condition = null,
|
||
};
|
||
@memcpy(ac.contextPointer()[0..context.len], context);
|
||
return ac;
|
||
}
|
||
|
||
fn waitAndDeinit(ac: *AsyncClosure, t: *Threaded, result: []u8) void {
|
||
ac.reset_event.wait(t) catch |err| switch (err) {
|
||
error.Canceled => {
|
||
ac.closure.requestCancel(t);
|
||
ac.reset_event.waitUncancelable();
|
||
},
|
||
};
|
||
@memcpy(result, ac.resultPointer()[0..result.len]);
|
||
ac.deinit(t.allocator);
|
||
}
|
||
|
||
fn deinit(ac: *AsyncClosure, gpa: Allocator) void {
|
||
const base: [*]align(@alignOf(AsyncClosure)) u8 = @ptrCast(ac);
|
||
gpa.free(base[0..ac.alloc_len]);
|
||
}
|
||
};
|
||
|
||
fn async(
|
||
userdata: ?*anyopaque,
|
||
result: []u8,
|
||
result_alignment: Alignment,
|
||
context: []const u8,
|
||
context_alignment: Alignment,
|
||
start: *const fn (context: *const anyopaque, result: *anyopaque) void,
|
||
) ?*Io.AnyFuture {
|
||
const t: *Threaded = @ptrCast(@alignCast(userdata));
|
||
if (builtin.single_threaded) {
|
||
start(context.ptr, result.ptr);
|
||
return null;
|
||
}
|
||
const gpa = t.allocator;
|
||
const ac = AsyncClosure.init(gpa, result.len, result_alignment, context, context_alignment, start) catch {
|
||
start(context.ptr, result.ptr);
|
||
return null;
|
||
};
|
||
|
||
t.mutex.lock();
|
||
|
||
const busy_count = t.busy_count;
|
||
|
||
if (busy_count >= @intFromEnum(t.async_limit)) {
|
||
t.mutex.unlock();
|
||
ac.deinit(gpa);
|
||
start(context.ptr, result.ptr);
|
||
return null;
|
||
}
|
||
|
||
t.busy_count = busy_count + 1;
|
||
|
||
const pool_size = t.wait_group.value();
|
||
if (pool_size - busy_count == 0) {
|
||
t.wait_group.start();
|
||
const thread = std.Thread.spawn(.{ .stack_size = t.stack_size }, worker, .{t}) catch {
|
||
t.wait_group.finish();
|
||
t.busy_count = busy_count;
|
||
t.mutex.unlock();
|
||
ac.deinit(gpa);
|
||
start(context.ptr, result.ptr);
|
||
return null;
|
||
};
|
||
thread.detach();
|
||
}
|
||
|
||
t.run_queue.prepend(&ac.closure.node);
|
||
t.mutex.unlock();
|
||
t.cond.signal();
|
||
return @ptrCast(ac);
|
||
}
|
||
|
||
fn concurrent(
|
||
userdata: ?*anyopaque,
|
||
result_len: usize,
|
||
result_alignment: Alignment,
|
||
context: []const u8,
|
||
context_alignment: Alignment,
|
||
start: *const fn (context: *const anyopaque, result: *anyopaque) void,
|
||
) Io.ConcurrentError!*Io.AnyFuture {
|
||
if (builtin.single_threaded) return error.ConcurrencyUnavailable;
|
||
|
||
const t: *Threaded = @ptrCast(@alignCast(userdata));
|
||
|
||
const gpa = t.allocator;
|
||
const ac = AsyncClosure.init(gpa, result_len, result_alignment, context, context_alignment, start) catch
|
||
return error.ConcurrencyUnavailable;
|
||
errdefer ac.deinit(gpa);
|
||
|
||
t.mutex.lock();
|
||
defer t.mutex.unlock();
|
||
|
||
const busy_count = t.busy_count;
|
||
|
||
if (busy_count >= @intFromEnum(t.concurrent_limit))
|
||
return error.ConcurrencyUnavailable;
|
||
|
||
t.busy_count = busy_count + 1;
|
||
errdefer t.busy_count = busy_count;
|
||
|
||
const pool_size = t.wait_group.value();
|
||
if (pool_size - busy_count == 0) {
|
||
t.wait_group.start();
|
||
errdefer t.wait_group.finish();
|
||
|
||
const thread = std.Thread.spawn(.{ .stack_size = t.stack_size }, worker, .{t}) catch
|
||
return error.ConcurrencyUnavailable;
|
||
thread.detach();
|
||
}
|
||
|
||
t.run_queue.prepend(&ac.closure.node);
|
||
t.cond.signal();
|
||
return @ptrCast(ac);
|
||
}
|
||
|
||
const GroupClosure = struct {
|
||
closure: Closure,
|
||
group: *Io.Group,
|
||
/// Points to sibling `GroupClosure`. Used for walking the group to cancel all.
|
||
node: std.SinglyLinkedList.Node,
|
||
func: *const fn (*Io.Group, context: *anyopaque) void,
|
||
context_alignment: Alignment,
|
||
alloc_len: usize,
|
||
|
||
fn start(closure: *Closure, t: *Threaded) void {
|
||
const gc: *GroupClosure = @alignCast(@fieldParentPtr("closure", closure));
|
||
const current_thread = Thread.getCurrent(t);
|
||
const group = gc.group;
|
||
const group_state: *std.atomic.Value(usize) = @ptrCast(&group.state);
|
||
const reset_event: *ResetEvent = @ptrCast(&group.context);
|
||
current_thread.current_closure = closure;
|
||
gc.func(group, gc.contextPointer());
|
||
current_thread.current_closure = null;
|
||
|
||
const prev_state = group_state.fetchSub(sync_one_pending, .acq_rel);
|
||
assert((prev_state / sync_one_pending) > 0);
|
||
if (prev_state == (sync_one_pending | sync_is_waiting)) reset_event.set();
|
||
}
|
||
|
||
fn contextPointer(gc: *GroupClosure) [*]u8 {
|
||
const base: [*]u8 = @ptrCast(gc);
|
||
const context_offset = gc.context_alignment.forward(@intFromPtr(gc) + @sizeOf(GroupClosure)) - @intFromPtr(gc);
|
||
return base + context_offset;
|
||
}
|
||
|
||
/// Does not initialize the `node` field.
|
||
fn init(
|
||
gpa: Allocator,
|
||
group: *Io.Group,
|
||
context: []const u8,
|
||
context_alignment: Alignment,
|
||
func: *const fn (*Io.Group, context: *const anyopaque) void,
|
||
) Allocator.Error!*GroupClosure {
|
||
const max_context_misalignment = context_alignment.toByteUnits() -| @alignOf(GroupClosure);
|
||
const worst_case_context_offset = context_alignment.forward(@sizeOf(GroupClosure) + max_context_misalignment);
|
||
const alloc_len = worst_case_context_offset + context.len;
|
||
|
||
const gc: *GroupClosure = @ptrCast(@alignCast(try gpa.alignedAlloc(u8, .of(GroupClosure), alloc_len)));
|
||
errdefer comptime unreachable;
|
||
|
||
gc.* = .{
|
||
.closure = .{
|
||
.cancel_status = .none,
|
||
.start = start,
|
||
},
|
||
.group = group,
|
||
.node = undefined,
|
||
.func = func,
|
||
.context_alignment = context_alignment,
|
||
.alloc_len = alloc_len,
|
||
};
|
||
@memcpy(gc.contextPointer()[0..context.len], context);
|
||
return gc;
|
||
}
|
||
|
||
fn deinit(gc: *GroupClosure, gpa: Allocator) void {
|
||
const base: [*]align(@alignOf(GroupClosure)) u8 = @ptrCast(gc);
|
||
gpa.free(base[0..gc.alloc_len]);
|
||
}
|
||
|
||
const sync_is_waiting: usize = 1 << 0;
|
||
const sync_one_pending: usize = 1 << 1;
|
||
};
|
||
|
||
fn groupAsync(
|
||
userdata: ?*anyopaque,
|
||
group: *Io.Group,
|
||
context: []const u8,
|
||
context_alignment: Alignment,
|
||
start: *const fn (*Io.Group, context: *const anyopaque) void,
|
||
) void {
|
||
const t: *Threaded = @ptrCast(@alignCast(userdata));
|
||
if (builtin.single_threaded) return start(group, context.ptr);
|
||
|
||
const gpa = t.allocator;
|
||
const gc = GroupClosure.init(gpa, group, context, context_alignment, start) catch
|
||
return start(group, context.ptr);
|
||
|
||
t.mutex.lock();
|
||
|
||
const busy_count = t.busy_count;
|
||
|
||
if (busy_count >= @intFromEnum(t.async_limit)) {
|
||
t.mutex.unlock();
|
||
gc.deinit(gpa);
|
||
return start(group, context.ptr);
|
||
}
|
||
|
||
t.busy_count = busy_count + 1;
|
||
|
||
const pool_size = t.wait_group.value();
|
||
if (pool_size - busy_count == 0) {
|
||
t.wait_group.start();
|
||
const thread = std.Thread.spawn(.{ .stack_size = t.stack_size }, worker, .{t}) catch {
|
||
t.wait_group.finish();
|
||
t.busy_count = busy_count;
|
||
t.mutex.unlock();
|
||
gc.deinit(gpa);
|
||
return start(group, context.ptr);
|
||
};
|
||
thread.detach();
|
||
}
|
||
|
||
// Append to the group linked list inside the mutex to make `Io.Group.async` thread-safe.
|
||
gc.node = .{ .next = @ptrCast(@alignCast(group.token)) };
|
||
group.token = &gc.node;
|
||
|
||
t.run_queue.prepend(&gc.closure.node);
|
||
|
||
// This needs to be done before unlocking the mutex to avoid a race with
|
||
// the associated task finishing.
|
||
const group_state: *std.atomic.Value(usize) = @ptrCast(&group.state);
|
||
const prev_state = group_state.fetchAdd(GroupClosure.sync_one_pending, .monotonic);
|
||
assert((prev_state / GroupClosure.sync_one_pending) < (std.math.maxInt(usize) / GroupClosure.sync_one_pending));
|
||
|
||
t.mutex.unlock();
|
||
t.cond.signal();
|
||
}
|
||
|
||
fn groupConcurrent(
|
||
userdata: ?*anyopaque,
|
||
group: *Io.Group,
|
||
context: []const u8,
|
||
context_alignment: Alignment,
|
||
start: *const fn (*Io.Group, context: *const anyopaque) void,
|
||
) Io.ConcurrentError!void {
|
||
if (builtin.single_threaded) return error.ConcurrencyUnavailable;
|
||
|
||
const t: *Threaded = @ptrCast(@alignCast(userdata));
|
||
|
||
const gpa = t.allocator;
|
||
const gc = GroupClosure.init(gpa, group, context, context_alignment, start) catch
|
||
return error.ConcurrencyUnavailable;
|
||
|
||
t.mutex.lock();
|
||
defer t.mutex.unlock();
|
||
|
||
const busy_count = t.busy_count;
|
||
|
||
if (busy_count >= @intFromEnum(t.concurrent_limit))
|
||
return error.ConcurrencyUnavailable;
|
||
|
||
t.busy_count = busy_count + 1;
|
||
errdefer t.busy_count = busy_count;
|
||
|
||
const pool_size = t.wait_group.value();
|
||
if (pool_size - busy_count == 0) {
|
||
t.wait_group.start();
|
||
errdefer t.wait_group.finish();
|
||
|
||
const thread = std.Thread.spawn(.{ .stack_size = t.stack_size }, worker, .{t}) catch
|
||
return error.ConcurrencyUnavailable;
|
||
thread.detach();
|
||
}
|
||
|
||
// Append to the group linked list inside the mutex to make `Io.Group.concurrent` thread-safe.
|
||
gc.node = .{ .next = @ptrCast(@alignCast(group.token)) };
|
||
group.token = &gc.node;
|
||
|
||
t.run_queue.prepend(&gc.closure.node);
|
||
|
||
// This needs to be done before unlocking the mutex to avoid a race with
|
||
// the associated task finishing.
|
||
const group_state: *std.atomic.Value(usize) = @ptrCast(&group.state);
|
||
const prev_state = group_state.fetchAdd(GroupClosure.sync_one_pending, .monotonic);
|
||
assert((prev_state / GroupClosure.sync_one_pending) < (std.math.maxInt(usize) / GroupClosure.sync_one_pending));
|
||
|
||
t.cond.signal();
|
||
}
|
||
|
||
fn groupWait(userdata: ?*anyopaque, group: *Io.Group, token: *anyopaque) void {
|
||
const t: *Threaded = @ptrCast(@alignCast(userdata));
|
||
const gpa = t.allocator;
|
||
|
||
if (builtin.single_threaded) return;
|
||
|
||
const group_state: *std.atomic.Value(usize) = @ptrCast(&group.state);
|
||
const reset_event: *ResetEvent = @ptrCast(&group.context);
|
||
const prev_state = group_state.fetchAdd(GroupClosure.sync_is_waiting, .acquire);
|
||
assert(prev_state & GroupClosure.sync_is_waiting == 0);
|
||
if ((prev_state / GroupClosure.sync_one_pending) > 0) reset_event.wait(t) catch |err| switch (err) {
|
||
error.Canceled => {
|
||
var node: *std.SinglyLinkedList.Node = @ptrCast(@alignCast(token));
|
||
while (true) {
|
||
const gc: *GroupClosure = @fieldParentPtr("node", node);
|
||
gc.closure.requestCancel(t);
|
||
node = node.next orelse break;
|
||
}
|
||
reset_event.waitUncancelable();
|
||
},
|
||
};
|
||
|
||
var node: *std.SinglyLinkedList.Node = @ptrCast(@alignCast(token));
|
||
while (true) {
|
||
const gc: *GroupClosure = @fieldParentPtr("node", node);
|
||
const node_next = node.next;
|
||
gc.deinit(gpa);
|
||
node = node_next orelse break;
|
||
}
|
||
}
|
||
|
||
fn groupCancel(userdata: ?*anyopaque, group: *Io.Group, token: *anyopaque) void {
|
||
const t: *Threaded = @ptrCast(@alignCast(userdata));
|
||
const gpa = t.allocator;
|
||
|
||
if (builtin.single_threaded) return;
|
||
|
||
{
|
||
var node: *std.SinglyLinkedList.Node = @ptrCast(@alignCast(token));
|
||
while (true) {
|
||
const gc: *GroupClosure = @fieldParentPtr("node", node);
|
||
gc.closure.requestCancel(t);
|
||
node = node.next orelse break;
|
||
}
|
||
}
|
||
|
||
const group_state: *std.atomic.Value(usize) = @ptrCast(&group.state);
|
||
const reset_event: *ResetEvent = @ptrCast(&group.context);
|
||
const prev_state = group_state.fetchAdd(GroupClosure.sync_is_waiting, .acquire);
|
||
assert(prev_state & GroupClosure.sync_is_waiting == 0);
|
||
if ((prev_state / GroupClosure.sync_one_pending) > 0) reset_event.waitUncancelable();
|
||
|
||
{
|
||
var node: *std.SinglyLinkedList.Node = @ptrCast(@alignCast(token));
|
||
while (true) {
|
||
const gc: *GroupClosure = @fieldParentPtr("node", node);
|
||
const node_next = node.next;
|
||
gc.deinit(gpa);
|
||
node = node_next orelse break;
|
||
}
|
||
}
|
||
}
|
||
|
||
fn await(
|
||
userdata: ?*anyopaque,
|
||
any_future: *Io.AnyFuture,
|
||
result: []u8,
|
||
result_alignment: Alignment,
|
||
) void {
|
||
_ = result_alignment;
|
||
const t: *Threaded = @ptrCast(@alignCast(userdata));
|
||
const closure: *AsyncClosure = @ptrCast(@alignCast(any_future));
|
||
closure.waitAndDeinit(t, result);
|
||
}
|
||
|
||
fn cancel(
|
||
userdata: ?*anyopaque,
|
||
any_future: *Io.AnyFuture,
|
||
result: []u8,
|
||
result_alignment: Alignment,
|
||
) void {
|
||
_ = result_alignment;
|
||
const t: *Threaded = @ptrCast(@alignCast(userdata));
|
||
const ac: *AsyncClosure = @ptrCast(@alignCast(any_future));
|
||
ac.closure.requestCancel(t);
|
||
ac.waitAndDeinit(t, result);
|
||
}
|
||
|
||
fn mutexLock(userdata: ?*anyopaque, prev_state: Io.Mutex.State, mutex: *Io.Mutex) Io.Cancelable!void {
|
||
if (builtin.single_threaded) unreachable; // Interface should have prevented this.
|
||
if (native_os == .netbsd) @panic("TODO");
|
||
const t: *Threaded = @ptrCast(@alignCast(userdata));
|
||
const current_thread = Thread.getCurrent(t);
|
||
if (prev_state == .contended) {
|
||
try futexWait(current_thread, @ptrCast(&mutex.state), @intFromEnum(Io.Mutex.State.contended));
|
||
}
|
||
while (@atomicRmw(Io.Mutex.State, &mutex.state, .Xchg, .contended, .acquire) != .unlocked) {
|
||
try futexWait(current_thread, @ptrCast(&mutex.state), @intFromEnum(Io.Mutex.State.contended));
|
||
}
|
||
}
|
||
|
||
fn mutexLockUncancelable(userdata: ?*anyopaque, prev_state: Io.Mutex.State, mutex: *Io.Mutex) void {
|
||
if (builtin.single_threaded) unreachable; // Interface should have prevented this.
|
||
if (native_os == .netbsd) @panic("TODO");
|
||
_ = userdata;
|
||
if (prev_state == .contended) {
|
||
futexWaitUncancelable(@ptrCast(&mutex.state), @intFromEnum(Io.Mutex.State.contended));
|
||
}
|
||
while (@atomicRmw(Io.Mutex.State, &mutex.state, .Xchg, .contended, .acquire) != .unlocked) {
|
||
futexWaitUncancelable(@ptrCast(&mutex.state), @intFromEnum(Io.Mutex.State.contended));
|
||
}
|
||
}
|
||
|
||
fn mutexUnlock(userdata: ?*anyopaque, prev_state: Io.Mutex.State, mutex: *Io.Mutex) void {
|
||
if (builtin.single_threaded) unreachable; // Interface should have prevented this.
|
||
if (native_os == .netbsd) @panic("TODO");
|
||
_ = userdata;
|
||
_ = prev_state;
|
||
if (@atomicRmw(Io.Mutex.State, &mutex.state, .Xchg, .unlocked, .release) == .contended) {
|
||
futexWake(@ptrCast(&mutex.state), 1);
|
||
}
|
||
}
|
||
|
||
fn conditionWaitUncancelable(userdata: ?*anyopaque, cond: *Io.Condition, mutex: *Io.Mutex) void {
|
||
if (builtin.single_threaded) unreachable; // Deadlock.
|
||
if (native_os == .netbsd) @panic("TODO");
|
||
const t: *Threaded = @ptrCast(@alignCast(userdata));
|
||
const t_io = ioBasic(t);
|
||
comptime assert(@TypeOf(cond.state) == u64);
|
||
const ints: *[2]std.atomic.Value(u32) = @ptrCast(&cond.state);
|
||
const cond_state = &ints[0];
|
||
const cond_epoch = &ints[1];
|
||
const one_waiter = 1;
|
||
const waiter_mask = 0xffff;
|
||
const one_signal = 1 << 16;
|
||
const signal_mask = 0xffff << 16;
|
||
var epoch = cond_epoch.load(.acquire);
|
||
var state = cond_state.fetchAdd(one_waiter, .monotonic);
|
||
assert(state & waiter_mask != waiter_mask);
|
||
state += one_waiter;
|
||
|
||
mutex.unlock(t_io);
|
||
defer mutex.lockUncancelable(t_io);
|
||
|
||
while (true) {
|
||
futexWaitUncancelable(cond_epoch, epoch);
|
||
epoch = cond_epoch.load(.acquire);
|
||
state = cond_state.load(.monotonic);
|
||
while (state & signal_mask != 0) {
|
||
const new_state = state - one_waiter - one_signal;
|
||
state = cond_state.cmpxchgWeak(state, new_state, .acquire, .monotonic) orelse return;
|
||
}
|
||
}
|
||
}
|
||
|
||
fn conditionWait(userdata: ?*anyopaque, cond: *Io.Condition, mutex: *Io.Mutex) Io.Cancelable!void {
|
||
if (builtin.single_threaded) unreachable; // Deadlock.
|
||
if (native_os == .netbsd) @panic("TODO");
|
||
const t: *Threaded = @ptrCast(@alignCast(userdata));
|
||
const current_thread = Thread.getCurrent(t);
|
||
const t_io = ioBasic(t);
|
||
comptime assert(@TypeOf(cond.state) == u64);
|
||
const ints: *[2]std.atomic.Value(u32) = @ptrCast(&cond.state);
|
||
const cond_state = &ints[0];
|
||
const cond_epoch = &ints[1];
|
||
const one_waiter = 1;
|
||
const waiter_mask = 0xffff;
|
||
const one_signal = 1 << 16;
|
||
const signal_mask = 0xffff << 16;
|
||
// Observe the epoch, then check the state again to see if we should wake up.
|
||
// The epoch must be observed before we check the state or we could potentially miss a wake() and deadlock:
|
||
//
|
||
// - T1: s = LOAD(&state)
|
||
// - T2: UPDATE(&s, signal)
|
||
// - T2: UPDATE(&epoch, 1) + FUTEX_WAKE(&epoch)
|
||
// - T1: e = LOAD(&epoch) (was reordered after the state load)
|
||
// - T1: s & signals == 0 -> FUTEX_WAIT(&epoch, e) (missed the state update + the epoch change)
|
||
//
|
||
// Acquire barrier to ensure the epoch load happens before the state load.
|
||
var epoch = cond_epoch.load(.acquire);
|
||
var state = cond_state.fetchAdd(one_waiter, .monotonic);
|
||
assert(state & waiter_mask != waiter_mask);
|
||
state += one_waiter;
|
||
|
||
mutex.unlock(t_io);
|
||
defer mutex.lockUncancelable(t_io);
|
||
|
||
while (true) {
|
||
try futexWait(current_thread, cond_epoch, epoch);
|
||
|
||
epoch = cond_epoch.load(.acquire);
|
||
state = cond_state.load(.monotonic);
|
||
|
||
// Try to wake up by consuming a signal and decremented the waiter we
|
||
// added previously. Acquire barrier ensures code before the wake()
|
||
// which added the signal happens before we decrement it and return.
|
||
while (state & signal_mask != 0) {
|
||
const new_state = state - one_waiter - one_signal;
|
||
state = cond_state.cmpxchgWeak(state, new_state, .acquire, .monotonic) orelse return;
|
||
}
|
||
}
|
||
}
|
||
|
||
fn conditionWake(userdata: ?*anyopaque, cond: *Io.Condition, wake: Io.Condition.Wake) void {
|
||
if (builtin.single_threaded) unreachable; // Nothing to wake up.
|
||
const t: *Threaded = @ptrCast(@alignCast(userdata));
|
||
_ = t;
|
||
comptime assert(@TypeOf(cond.state) == u64);
|
||
const ints: *[2]std.atomic.Value(u32) = @ptrCast(&cond.state);
|
||
const cond_state = &ints[0];
|
||
const cond_epoch = &ints[1];
|
||
const one_waiter = 1;
|
||
const waiter_mask = 0xffff;
|
||
const one_signal = 1 << 16;
|
||
const signal_mask = 0xffff << 16;
|
||
var state = cond_state.load(.monotonic);
|
||
while (true) {
|
||
const waiters = (state & waiter_mask) / one_waiter;
|
||
const signals = (state & signal_mask) / one_signal;
|
||
|
||
// Reserves which waiters to wake up by incrementing the signals count.
|
||
// Therefore, the signals count is always less than or equal to the
|
||
// waiters count. We don't need to Futex.wake if there's nothing to
|
||
// wake up or if other wake() threads have reserved to wake up the
|
||
// current waiters.
|
||
const wakeable = waiters - signals;
|
||
if (wakeable == 0) {
|
||
return;
|
||
}
|
||
|
||
const to_wake = switch (wake) {
|
||
.one => 1,
|
||
.all => wakeable,
|
||
};
|
||
|
||
// Reserve the amount of waiters to wake by incrementing the signals
|
||
// count. Release barrier ensures code before the wake() happens before
|
||
// the signal it posted and consumed by the wait() threads.
|
||
const new_state = state + (one_signal * to_wake);
|
||
state = cond_state.cmpxchgWeak(state, new_state, .release, .monotonic) orelse {
|
||
// Wake up the waiting threads we reserved above by changing the epoch value.
|
||
//
|
||
// A waiting thread could miss a wake up if *exactly* ((1<<32)-1)
|
||
// wake()s happen between it observing the epoch and sleeping on
|
||
// it. This is very unlikely due to how many precise amount of
|
||
// Futex.wake() calls that would be between the waiting thread's
|
||
// potential preemption.
|
||
//
|
||
// Release barrier ensures the signal being added to the state
|
||
// happens before the epoch is changed. If not, the waiting thread
|
||
// could potentially deadlock from missing both the state and epoch
|
||
// change:
|
||
//
|
||
// - T2: UPDATE(&epoch, 1) (reordered before the state change)
|
||
// - T1: e = LOAD(&epoch)
|
||
// - T1: s = LOAD(&state)
|
||
// - T2: UPDATE(&state, signal) + FUTEX_WAKE(&epoch)
|
||
// - T1: s & signals == 0 -> FUTEX_WAIT(&epoch, e) (missed both epoch change and state change)
|
||
_ = cond_epoch.fetchAdd(1, .release);
|
||
if (native_os == .netbsd) @panic("TODO");
|
||
futexWake(cond_epoch, to_wake);
|
||
return;
|
||
};
|
||
}
|
||
}
|
||
|
||
const dirMake = switch (native_os) {
|
||
.windows => dirMakeWindows,
|
||
.wasi => dirMakeWasi,
|
||
else => dirMakePosix,
|
||
};
|
||
|
||
fn dirMakePosix(userdata: ?*anyopaque, dir: Io.Dir, sub_path: []const u8, mode: Io.Dir.Mode) Io.Dir.MakeError!void {
|
||
const t: *Threaded = @ptrCast(@alignCast(userdata));
|
||
const current_thread = Thread.getCurrent(t);
|
||
|
||
var path_buffer: [posix.PATH_MAX]u8 = undefined;
|
||
const sub_path_posix = try pathToPosix(sub_path, &path_buffer);
|
||
|
||
try current_thread.beginSyscall();
|
||
while (true) {
|
||
switch (posix.errno(posix.system.mkdirat(dir.handle, sub_path_posix, mode))) {
|
||
.SUCCESS => {
|
||
current_thread.endSyscall();
|
||
return;
|
||
},
|
||
.INTR => {
|
||
try current_thread.checkCancel();
|
||
continue;
|
||
},
|
||
.CANCELED => return current_thread.endSyscallCanceled(),
|
||
else => |e| {
|
||
current_thread.endSyscall();
|
||
switch (e) {
|
||
.ACCES => return error.AccessDenied,
|
||
.BADF => |err| return errnoBug(err), // File descriptor used after closed.
|
||
.PERM => return error.PermissionDenied,
|
||
.DQUOT => return error.DiskQuota,
|
||
.EXIST => return error.PathAlreadyExists,
|
||
.FAULT => |err| return errnoBug(err),
|
||
.LOOP => return error.SymLinkLoop,
|
||
.MLINK => return error.LinkQuotaExceeded,
|
||
.NAMETOOLONG => return error.NameTooLong,
|
||
.NOENT => return error.FileNotFound,
|
||
.NOMEM => return error.SystemResources,
|
||
.NOSPC => return error.NoSpaceLeft,
|
||
.NOTDIR => return error.NotDir,
|
||
.ROFS => return error.ReadOnlyFileSystem,
|
||
// dragonfly: when dir_fd is unlinked from filesystem
|
||
.NOTCONN => return error.FileNotFound,
|
||
.ILSEQ => return error.BadPathName,
|
||
else => |err| return posix.unexpectedErrno(err),
|
||
}
|
||
},
|
||
}
|
||
}
|
||
}
|
||
|
||
fn dirMakeWasi(userdata: ?*anyopaque, dir: Io.Dir, sub_path: []const u8, mode: Io.Dir.Mode) Io.Dir.MakeError!void {
|
||
if (builtin.link_libc) return dirMakePosix(userdata, dir, sub_path, mode);
|
||
const t: *Threaded = @ptrCast(@alignCast(userdata));
|
||
const current_thread = Thread.getCurrent(t);
|
||
try current_thread.beginSyscall();
|
||
while (true) {
|
||
switch (std.os.wasi.path_create_directory(dir.handle, sub_path.ptr, sub_path.len)) {
|
||
.SUCCESS => {
|
||
current_thread.endSyscall();
|
||
return;
|
||
},
|
||
.INTR => {
|
||
try current_thread.checkCancel();
|
||
continue;
|
||
},
|
||
.CANCELED => return current_thread.endSyscallCanceled(),
|
||
else => |e| {
|
||
current_thread.endSyscall();
|
||
switch (e) {
|
||
.ACCES => return error.AccessDenied,
|
||
.BADF => |err| return errnoBug(err), // File descriptor used after closed.
|
||
.PERM => return error.PermissionDenied,
|
||
.DQUOT => return error.DiskQuota,
|
||
.EXIST => return error.PathAlreadyExists,
|
||
.FAULT => |err| return errnoBug(err),
|
||
.LOOP => return error.SymLinkLoop,
|
||
.MLINK => return error.LinkQuotaExceeded,
|
||
.NAMETOOLONG => return error.NameTooLong,
|
||
.NOENT => return error.FileNotFound,
|
||
.NOMEM => return error.SystemResources,
|
||
.NOSPC => return error.NoSpaceLeft,
|
||
.NOTDIR => return error.NotDir,
|
||
.ROFS => return error.ReadOnlyFileSystem,
|
||
.NOTCAPABLE => return error.AccessDenied,
|
||
.ILSEQ => return error.BadPathName,
|
||
else => |err| return posix.unexpectedErrno(err),
|
||
}
|
||
},
|
||
}
|
||
}
|
||
}
|
||
|
||
fn dirMakeWindows(userdata: ?*anyopaque, dir: Io.Dir, sub_path: []const u8, mode: Io.Dir.Mode) Io.Dir.MakeError!void {
|
||
const t: *Threaded = @ptrCast(@alignCast(userdata));
|
||
const current_thread = Thread.getCurrent(t);
|
||
try current_thread.checkCancel();
|
||
|
||
const sub_path_w = try windows.sliceToPrefixedFileW(dir.handle, sub_path);
|
||
_ = mode;
|
||
const sub_dir_handle = windows.OpenFile(sub_path_w.span(), .{
|
||
.dir = dir.handle,
|
||
.access_mask = windows.GENERIC_READ | windows.SYNCHRONIZE,
|
||
.creation = windows.FILE_CREATE,
|
||
.filter = .dir_only,
|
||
}) catch |err| switch (err) {
|
||
error.IsDir => return error.Unexpected,
|
||
error.PipeBusy => return error.Unexpected,
|
||
error.NoDevice => return error.Unexpected,
|
||
error.WouldBlock => return error.Unexpected,
|
||
error.AntivirusInterference => return error.Unexpected,
|
||
else => |e| return e,
|
||
};
|
||
windows.CloseHandle(sub_dir_handle);
|
||
}
|
||
|
||
fn dirMakePath(
|
||
userdata: ?*anyopaque,
|
||
dir: Io.Dir,
|
||
sub_path: []const u8,
|
||
mode: Io.Dir.Mode,
|
||
) Io.Dir.MakePathError!Io.Dir.MakePathStatus {
|
||
const t: *Threaded = @ptrCast(@alignCast(userdata));
|
||
|
||
var it = std.fs.path.componentIterator(sub_path);
|
||
var status: Io.Dir.MakePathStatus = .existed;
|
||
var component = it.last() orelse return error.BadPathName;
|
||
while (true) {
|
||
if (dirMake(t, dir, component.path, mode)) |_| {
|
||
status = .created;
|
||
} else |err| switch (err) {
|
||
error.PathAlreadyExists => {
|
||
// stat the file and return an error if it's not a directory
|
||
// this is important because otherwise a dangling symlink
|
||
// could cause an infinite loop
|
||
check_dir: {
|
||
// workaround for windows, see https://github.com/ziglang/zig/issues/16738
|
||
const fstat = dirStatPath(t, dir, component.path, .{}) catch |stat_err| switch (stat_err) {
|
||
error.IsDir => break :check_dir,
|
||
else => |e| return e,
|
||
};
|
||
if (fstat.kind != .directory) return error.NotDir;
|
||
}
|
||
},
|
||
error.FileNotFound => |e| {
|
||
component = it.previous() orelse return e;
|
||
continue;
|
||
},
|
||
else => |e| return e,
|
||
}
|
||
component = it.next() orelse return status;
|
||
}
|
||
}
|
||
|
||
const dirMakeOpenPath = switch (native_os) {
|
||
.windows => dirMakeOpenPathWindows,
|
||
.wasi => dirMakeOpenPathWasi,
|
||
else => dirMakeOpenPathPosix,
|
||
};
|
||
|
||
fn dirMakeOpenPathPosix(
|
||
userdata: ?*anyopaque,
|
||
dir: Io.Dir,
|
||
sub_path: []const u8,
|
||
options: Io.Dir.OpenOptions,
|
||
) Io.Dir.MakeOpenPathError!Io.Dir {
|
||
const t: *Threaded = @ptrCast(@alignCast(userdata));
|
||
const t_io = ioBasic(t);
|
||
return dirOpenDirPosix(t, dir, sub_path, options) catch |err| switch (err) {
|
||
error.FileNotFound => {
|
||
try dir.makePath(t_io, sub_path);
|
||
return dirOpenDirPosix(t, dir, sub_path, options);
|
||
},
|
||
else => |e| return e,
|
||
};
|
||
}
|
||
|
||
fn dirMakeOpenPathWindows(
|
||
userdata: ?*anyopaque,
|
||
dir: Io.Dir,
|
||
sub_path: []const u8,
|
||
options: Io.Dir.OpenOptions,
|
||
) Io.Dir.MakeOpenPathError!Io.Dir {
|
||
const t: *Threaded = @ptrCast(@alignCast(userdata));
|
||
const current_thread = Thread.getCurrent(t);
|
||
const w = windows;
|
||
const access_mask = w.STANDARD_RIGHTS_READ | w.FILE_READ_ATTRIBUTES | w.FILE_READ_EA |
|
||
w.SYNCHRONIZE | w.FILE_TRAVERSE |
|
||
(if (options.iterate) w.FILE_LIST_DIRECTORY else @as(u32, 0));
|
||
|
||
var it = std.fs.path.componentIterator(sub_path);
|
||
// If there are no components in the path, then create a dummy component with the full path.
|
||
var component: std.fs.path.NativeComponentIterator.Component = it.last() orelse .{
|
||
.name = "",
|
||
.path = sub_path,
|
||
};
|
||
|
||
while (true) {
|
||
try current_thread.checkCancel();
|
||
|
||
const sub_path_w_array = try w.sliceToPrefixedFileW(dir.handle, component.path);
|
||
const sub_path_w = sub_path_w_array.span();
|
||
const is_last = it.peekNext() == null;
|
||
const create_disposition: u32 = if (is_last) w.FILE_OPEN_IF else w.FILE_CREATE;
|
||
|
||
var result: Io.Dir = .{ .handle = undefined };
|
||
|
||
const path_len_bytes: u16 = @intCast(sub_path_w.len * 2);
|
||
var nt_name: w.UNICODE_STRING = .{
|
||
.Length = path_len_bytes,
|
||
.MaximumLength = path_len_bytes,
|
||
.Buffer = @constCast(sub_path_w.ptr),
|
||
};
|
||
var attr: w.OBJECT_ATTRIBUTES = .{
|
||
.Length = @sizeOf(w.OBJECT_ATTRIBUTES),
|
||
.RootDirectory = if (std.fs.path.isAbsoluteWindowsWtf16(sub_path_w)) null else dir.handle,
|
||
.Attributes = 0, // Note we do not use OBJ_CASE_INSENSITIVE here.
|
||
.ObjectName = &nt_name,
|
||
.SecurityDescriptor = null,
|
||
.SecurityQualityOfService = null,
|
||
};
|
||
const open_reparse_point: w.DWORD = if (!options.follow_symlinks) w.FILE_OPEN_REPARSE_POINT else 0x0;
|
||
var io_status_block: w.IO_STATUS_BLOCK = undefined;
|
||
const rc = w.ntdll.NtCreateFile(
|
||
&result.handle,
|
||
access_mask,
|
||
&attr,
|
||
&io_status_block,
|
||
null,
|
||
w.FILE_ATTRIBUTE_NORMAL,
|
||
w.FILE_SHARE_READ | w.FILE_SHARE_WRITE | w.FILE_SHARE_DELETE,
|
||
create_disposition,
|
||
w.FILE_DIRECTORY_FILE | w.FILE_SYNCHRONOUS_IO_NONALERT | w.FILE_OPEN_FOR_BACKUP_INTENT | open_reparse_point,
|
||
null,
|
||
0,
|
||
);
|
||
|
||
switch (rc) {
|
||
.SUCCESS => {
|
||
component = it.next() orelse return result;
|
||
w.CloseHandle(result.handle);
|
||
continue;
|
||
},
|
||
.OBJECT_NAME_INVALID => return error.BadPathName,
|
||
.OBJECT_NAME_COLLISION => {
|
||
assert(!is_last);
|
||
// stat the file and return an error if it's not a directory
|
||
// this is important because otherwise a dangling symlink
|
||
// could cause an infinite loop
|
||
check_dir: {
|
||
// workaround for windows, see https://github.com/ziglang/zig/issues/16738
|
||
const fstat = dirStatPathWindows(t, dir, component.path, .{
|
||
.follow_symlinks = options.follow_symlinks,
|
||
}) catch |stat_err| switch (stat_err) {
|
||
error.IsDir => break :check_dir,
|
||
else => |e| return e,
|
||
};
|
||
if (fstat.kind != .directory) return error.NotDir;
|
||
}
|
||
|
||
component = it.next().?;
|
||
continue;
|
||
},
|
||
|
||
.OBJECT_NAME_NOT_FOUND,
|
||
.OBJECT_PATH_NOT_FOUND,
|
||
=> {
|
||
component = it.previous() orelse return error.FileNotFound;
|
||
continue;
|
||
},
|
||
|
||
.NOT_A_DIRECTORY => return error.NotDir,
|
||
// This can happen if the directory has 'List folder contents' permission set to 'Deny'
|
||
// and the directory is trying to be opened for iteration.
|
||
.ACCESS_DENIED => return error.AccessDenied,
|
||
.INVALID_PARAMETER => |err| return w.statusBug(err),
|
||
else => return w.unexpectedStatus(rc),
|
||
}
|
||
}
|
||
}
|
||
|
||
fn dirMakeOpenPathWasi(
|
||
userdata: ?*anyopaque,
|
||
dir: Io.Dir,
|
||
sub_path: []const u8,
|
||
options: Io.Dir.OpenOptions,
|
||
) Io.Dir.MakeOpenPathError!Io.Dir {
|
||
const t: *Threaded = @ptrCast(@alignCast(userdata));
|
||
const t_io = ioBasic(t);
|
||
return dirOpenDirWasi(t, dir, sub_path, options) catch |err| switch (err) {
|
||
error.FileNotFound => {
|
||
try dir.makePath(t_io, sub_path);
|
||
return dirOpenDirWasi(t, dir, sub_path, options);
|
||
},
|
||
else => |e| return e,
|
||
};
|
||
}
|
||
|
||
fn dirStat(userdata: ?*anyopaque, dir: Io.Dir) Io.Dir.StatError!Io.Dir.Stat {
|
||
const t: *Threaded = @ptrCast(@alignCast(userdata));
|
||
const file: Io.File = .{ .handle = dir.handle };
|
||
return fileStat(t, file);
|
||
}
|
||
|
||
const dirStatPath = switch (native_os) {
|
||
.linux => dirStatPathLinux,
|
||
.windows => dirStatPathWindows,
|
||
.wasi => dirStatPathWasi,
|
||
else => dirStatPathPosix,
|
||
};
|
||
|
||
fn dirStatPathLinux(
|
||
userdata: ?*anyopaque,
|
||
dir: Io.Dir,
|
||
sub_path: []const u8,
|
||
options: Io.Dir.StatPathOptions,
|
||
) Io.Dir.StatPathError!Io.File.Stat {
|
||
const t: *Threaded = @ptrCast(@alignCast(userdata));
|
||
const current_thread = Thread.getCurrent(t);
|
||
const linux = std.os.linux;
|
||
|
||
var path_buffer: [posix.PATH_MAX]u8 = undefined;
|
||
const sub_path_posix = try pathToPosix(sub_path, &path_buffer);
|
||
|
||
const flags: u32 = linux.AT.NO_AUTOMOUNT |
|
||
@as(u32, if (!options.follow_symlinks) linux.AT.SYMLINK_NOFOLLOW else 0);
|
||
|
||
try current_thread.beginSyscall();
|
||
while (true) {
|
||
var statx = std.mem.zeroes(linux.Statx);
|
||
const rc = linux.statx(
|
||
dir.handle,
|
||
sub_path_posix,
|
||
flags,
|
||
linux.STATX_INO | linux.STATX_SIZE | linux.STATX_TYPE | linux.STATX_MODE | linux.STATX_ATIME | linux.STATX_MTIME | linux.STATX_CTIME,
|
||
&statx,
|
||
);
|
||
switch (linux.errno(rc)) {
|
||
.SUCCESS => {
|
||
current_thread.endSyscall();
|
||
return statFromLinux(&statx);
|
||
},
|
||
.INTR => {
|
||
try current_thread.checkCancel();
|
||
continue;
|
||
},
|
||
.CANCELED => return current_thread.endSyscallCanceled(),
|
||
else => |e| {
|
||
current_thread.endSyscall();
|
||
switch (e) {
|
||
.ACCES => return error.AccessDenied,
|
||
.BADF => |err| return errnoBug(err), // File descriptor used after closed.
|
||
.FAULT => |err| return errnoBug(err),
|
||
.INVAL => |err| return errnoBug(err),
|
||
.LOOP => return error.SymLinkLoop,
|
||
.NAMETOOLONG => |err| return errnoBug(err), // Handled by pathToPosix() above.
|
||
.NOENT => return error.FileNotFound,
|
||
.NOTDIR => return error.NotDir,
|
||
.NOMEM => return error.SystemResources,
|
||
else => |err| return posix.unexpectedErrno(err),
|
||
}
|
||
},
|
||
}
|
||
}
|
||
}
|
||
|
||
fn dirStatPathPosix(
|
||
userdata: ?*anyopaque,
|
||
dir: Io.Dir,
|
||
sub_path: []const u8,
|
||
options: Io.Dir.StatPathOptions,
|
||
) Io.Dir.StatPathError!Io.File.Stat {
|
||
const t: *Threaded = @ptrCast(@alignCast(userdata));
|
||
const current_thread = Thread.getCurrent(t);
|
||
|
||
var path_buffer: [posix.PATH_MAX]u8 = undefined;
|
||
const sub_path_posix = try pathToPosix(sub_path, &path_buffer);
|
||
|
||
const flags: u32 = if (!options.follow_symlinks) posix.AT.SYMLINK_NOFOLLOW else 0;
|
||
|
||
try current_thread.beginSyscall();
|
||
while (true) {
|
||
var stat = std.mem.zeroes(posix.Stat);
|
||
switch (posix.errno(fstatat_sym(dir.handle, sub_path_posix, &stat, flags))) {
|
||
.SUCCESS => {
|
||
current_thread.endSyscall();
|
||
return statFromPosix(&stat);
|
||
},
|
||
.INTR => {
|
||
try current_thread.checkCancel();
|
||
continue;
|
||
},
|
||
.CANCELED => return current_thread.endSyscallCanceled(),
|
||
else => |e| {
|
||
current_thread.endSyscall();
|
||
switch (e) {
|
||
.INVAL => |err| return errnoBug(err),
|
||
.BADF => |err| return errnoBug(err), // File descriptor used after closed.
|
||
.NOMEM => return error.SystemResources,
|
||
.ACCES => return error.AccessDenied,
|
||
.PERM => return error.PermissionDenied,
|
||
.FAULT => |err| return errnoBug(err),
|
||
.NAMETOOLONG => return error.NameTooLong,
|
||
.LOOP => return error.SymLinkLoop,
|
||
.NOENT => return error.FileNotFound,
|
||
.NOTDIR => return error.FileNotFound,
|
||
.ILSEQ => return error.BadPathName,
|
||
else => |err| return posix.unexpectedErrno(err),
|
||
}
|
||
},
|
||
}
|
||
}
|
||
}
|
||
|
||
fn dirStatPathWindows(
|
||
userdata: ?*anyopaque,
|
||
dir: Io.Dir,
|
||
sub_path: []const u8,
|
||
options: Io.Dir.StatPathOptions,
|
||
) Io.Dir.StatPathError!Io.File.Stat {
|
||
const t: *Threaded = @ptrCast(@alignCast(userdata));
|
||
const file = try dirOpenFileWindows(t, dir, sub_path, .{
|
||
.follow_symlinks = options.follow_symlinks,
|
||
});
|
||
defer windows.CloseHandle(file.handle);
|
||
return fileStatWindows(t, file);
|
||
}
|
||
|
||
fn dirStatPathWasi(
|
||
userdata: ?*anyopaque,
|
||
dir: Io.Dir,
|
||
sub_path: []const u8,
|
||
options: Io.Dir.StatPathOptions,
|
||
) Io.Dir.StatPathError!Io.File.Stat {
|
||
if (builtin.link_libc) return dirStatPathPosix(userdata, dir, sub_path, options);
|
||
const t: *Threaded = @ptrCast(@alignCast(userdata));
|
||
const current_thread = Thread.getCurrent(t);
|
||
const wasi = std.os.wasi;
|
||
const flags: wasi.lookupflags_t = .{
|
||
.SYMLINK_FOLLOW = options.follow_symlinks,
|
||
};
|
||
var stat: wasi.filestat_t = undefined;
|
||
try current_thread.beginSyscall();
|
||
while (true) {
|
||
switch (wasi.path_filestat_get(dir.handle, flags, sub_path.ptr, sub_path.len, &stat)) {
|
||
.SUCCESS => {
|
||
current_thread.endSyscall();
|
||
return statFromWasi(&stat);
|
||
},
|
||
.INTR => {
|
||
try current_thread.checkCancel();
|
||
continue;
|
||
},
|
||
.CANCELED => return current_thread.endSyscallCanceled(),
|
||
else => |e| {
|
||
current_thread.endSyscall();
|
||
switch (e) {
|
||
.INVAL => |err| return errnoBug(err),
|
||
.BADF => |err| return errnoBug(err), // File descriptor used after closed.
|
||
.NOMEM => return error.SystemResources,
|
||
.ACCES => return error.AccessDenied,
|
||
.FAULT => |err| return errnoBug(err),
|
||
.NAMETOOLONG => return error.NameTooLong,
|
||
.NOENT => return error.FileNotFound,
|
||
.NOTDIR => return error.FileNotFound,
|
||
.NOTCAPABLE => return error.AccessDenied,
|
||
.ILSEQ => return error.BadPathName,
|
||
else => |err| return posix.unexpectedErrno(err),
|
||
}
|
||
},
|
||
}
|
||
}
|
||
}
|
||
|
||
const fileStat = switch (native_os) {
|
||
.linux => fileStatLinux,
|
||
.windows => fileStatWindows,
|
||
.wasi => fileStatWasi,
|
||
else => fileStatPosix,
|
||
};
|
||
|
||
fn fileStatPosix(userdata: ?*anyopaque, file: Io.File) Io.File.StatError!Io.File.Stat {
|
||
const t: *Threaded = @ptrCast(@alignCast(userdata));
|
||
const current_thread = Thread.getCurrent(t);
|
||
|
||
if (posix.Stat == void) return error.Streaming;
|
||
|
||
try current_thread.beginSyscall();
|
||
while (true) {
|
||
var stat = std.mem.zeroes(posix.Stat);
|
||
switch (posix.errno(fstat_sym(file.handle, &stat))) {
|
||
.SUCCESS => {
|
||
current_thread.endSyscall();
|
||
return statFromPosix(&stat);
|
||
},
|
||
.INTR => {
|
||
try current_thread.checkCancel();
|
||
continue;
|
||
},
|
||
.CANCELED => return current_thread.endSyscallCanceled(),
|
||
else => |e| {
|
||
current_thread.endSyscall();
|
||
switch (e) {
|
||
.INVAL => |err| return errnoBug(err),
|
||
.BADF => |err| return errnoBug(err), // File descriptor used after closed.
|
||
.NOMEM => return error.SystemResources,
|
||
.ACCES => return error.AccessDenied,
|
||
else => |err| return posix.unexpectedErrno(err),
|
||
}
|
||
},
|
||
}
|
||
}
|
||
}
|
||
|
||
fn fileStatLinux(userdata: ?*anyopaque, file: Io.File) Io.File.StatError!Io.File.Stat {
|
||
const t: *Threaded = @ptrCast(@alignCast(userdata));
|
||
const current_thread = Thread.getCurrent(t);
|
||
const linux = std.os.linux;
|
||
|
||
try current_thread.beginSyscall();
|
||
while (true) {
|
||
var statx = std.mem.zeroes(linux.Statx);
|
||
const rc = linux.statx(
|
||
file.handle,
|
||
"",
|
||
linux.AT.EMPTY_PATH,
|
||
linux.STATX_INO | linux.STATX_SIZE | linux.STATX_TYPE | linux.STATX_MODE | linux.STATX_ATIME | linux.STATX_MTIME | linux.STATX_CTIME,
|
||
&statx,
|
||
);
|
||
switch (linux.errno(rc)) {
|
||
.SUCCESS => {
|
||
current_thread.endSyscall();
|
||
return statFromLinux(&statx);
|
||
},
|
||
.INTR => {
|
||
try current_thread.checkCancel();
|
||
continue;
|
||
},
|
||
.CANCELED => return current_thread.endSyscallCanceled(),
|
||
else => |e| {
|
||
current_thread.endSyscall();
|
||
switch (e) {
|
||
.ACCES => |err| return errnoBug(err),
|
||
.BADF => |err| return errnoBug(err), // File descriptor used after closed.
|
||
.FAULT => |err| return errnoBug(err),
|
||
.INVAL => |err| return errnoBug(err),
|
||
.LOOP => |err| return errnoBug(err),
|
||
.NAMETOOLONG => |err| return errnoBug(err),
|
||
.NOENT => |err| return errnoBug(err),
|
||
.NOMEM => return error.SystemResources,
|
||
.NOTDIR => |err| return errnoBug(err),
|
||
else => |err| return posix.unexpectedErrno(err),
|
||
}
|
||
},
|
||
}
|
||
}
|
||
}
|
||
|
||
fn fileStatWindows(userdata: ?*anyopaque, file: Io.File) Io.File.StatError!Io.File.Stat {
|
||
const t: *Threaded = @ptrCast(@alignCast(userdata));
|
||
const current_thread = Thread.getCurrent(t);
|
||
try current_thread.checkCancel();
|
||
|
||
var io_status_block: windows.IO_STATUS_BLOCK = undefined;
|
||
var info: windows.FILE_ALL_INFORMATION = undefined;
|
||
const rc = windows.ntdll.NtQueryInformationFile(file.handle, &io_status_block, &info, @sizeOf(windows.FILE_ALL_INFORMATION), .FileAllInformation);
|
||
switch (rc) {
|
||
.SUCCESS => {},
|
||
// Buffer overflow here indicates that there is more information available than was able to be stored in the buffer
|
||
// size provided. This is treated as success because the type of variable-length information that this would be relevant for
|
||
// (name, volume name, etc) we don't care about.
|
||
.BUFFER_OVERFLOW => {},
|
||
.INVALID_PARAMETER => |err| return windows.statusBug(err),
|
||
.ACCESS_DENIED => return error.AccessDenied,
|
||
else => return windows.unexpectedStatus(rc),
|
||
}
|
||
return .{
|
||
.inode = info.InternalInformation.IndexNumber,
|
||
.size = @as(u64, @bitCast(info.StandardInformation.EndOfFile)),
|
||
.mode = 0,
|
||
.kind = if (info.BasicInformation.FileAttributes & windows.FILE_ATTRIBUTE_REPARSE_POINT != 0) reparse_point: {
|
||
var tag_info: windows.FILE_ATTRIBUTE_TAG_INFO = undefined;
|
||
const tag_rc = windows.ntdll.NtQueryInformationFile(file.handle, &io_status_block, &tag_info, @sizeOf(windows.FILE_ATTRIBUTE_TAG_INFO), .FileAttributeTagInformation);
|
||
switch (tag_rc) {
|
||
.SUCCESS => {},
|
||
// INFO_LENGTH_MISMATCH and ACCESS_DENIED are the only documented possible errors
|
||
// https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-fscc/d295752f-ce89-4b98-8553-266d37c84f0e
|
||
.INFO_LENGTH_MISMATCH => |err| return windows.statusBug(err),
|
||
.ACCESS_DENIED => return error.AccessDenied,
|
||
else => return windows.unexpectedStatus(rc),
|
||
}
|
||
if (tag_info.ReparseTag & windows.reparse_tag_name_surrogate_bit != 0) {
|
||
break :reparse_point .sym_link;
|
||
}
|
||
// Unknown reparse point
|
||
break :reparse_point .unknown;
|
||
} else if (info.BasicInformation.FileAttributes & windows.FILE_ATTRIBUTE_DIRECTORY != 0)
|
||
.directory
|
||
else
|
||
.file,
|
||
.atime = windows.fromSysTime(info.BasicInformation.LastAccessTime),
|
||
.mtime = windows.fromSysTime(info.BasicInformation.LastWriteTime),
|
||
.ctime = windows.fromSysTime(info.BasicInformation.ChangeTime),
|
||
};
|
||
}
|
||
|
||
fn fileStatWasi(userdata: ?*anyopaque, file: Io.File) Io.File.StatError!Io.File.Stat {
|
||
if (builtin.link_libc) return fileStatPosix(userdata, file);
|
||
|
||
const t: *Threaded = @ptrCast(@alignCast(userdata));
|
||
const current_thread = Thread.getCurrent(t);
|
||
|
||
try current_thread.beginSyscall();
|
||
while (true) {
|
||
var stat: std.os.wasi.filestat_t = undefined;
|
||
switch (std.os.wasi.fd_filestat_get(file.handle, &stat)) {
|
||
.SUCCESS => {
|
||
current_thread.endSyscall();
|
||
return statFromWasi(&stat);
|
||
},
|
||
.INTR => {
|
||
try current_thread.checkCancel();
|
||
continue;
|
||
},
|
||
.CANCELED => return current_thread.endSyscallCanceled(),
|
||
else => |e| {
|
||
current_thread.endSyscall();
|
||
switch (e) {
|
||
.INVAL => |err| return errnoBug(err),
|
||
.BADF => |err| return errnoBug(err), // File descriptor used after closed.
|
||
.NOMEM => return error.SystemResources,
|
||
.ACCES => return error.AccessDenied,
|
||
.NOTCAPABLE => return error.AccessDenied,
|
||
else => |err| return posix.unexpectedErrno(err),
|
||
}
|
||
},
|
||
}
|
||
}
|
||
}
|
||
|
||
const dirAccess = switch (native_os) {
|
||
.windows => dirAccessWindows,
|
||
.wasi => dirAccessWasi,
|
||
else => dirAccessPosix,
|
||
};
|
||
|
||
fn dirAccessPosix(
|
||
userdata: ?*anyopaque,
|
||
dir: Io.Dir,
|
||
sub_path: []const u8,
|
||
options: Io.Dir.AccessOptions,
|
||
) Io.Dir.AccessError!void {
|
||
const t: *Threaded = @ptrCast(@alignCast(userdata));
|
||
const current_thread = Thread.getCurrent(t);
|
||
|
||
var path_buffer: [posix.PATH_MAX]u8 = undefined;
|
||
const sub_path_posix = try pathToPosix(sub_path, &path_buffer);
|
||
|
||
const flags: u32 = @as(u32, if (!options.follow_symlinks) posix.AT.SYMLINK_NOFOLLOW else 0);
|
||
|
||
const mode: u32 =
|
||
@as(u32, if (options.read) posix.R_OK else 0) |
|
||
@as(u32, if (options.write) posix.W_OK else 0) |
|
||
@as(u32, if (options.execute) posix.X_OK else 0);
|
||
|
||
try current_thread.beginSyscall();
|
||
while (true) {
|
||
switch (posix.errno(posix.system.faccessat(dir.handle, sub_path_posix, mode, flags))) {
|
||
.SUCCESS => {
|
||
current_thread.endSyscall();
|
||
return;
|
||
},
|
||
.INTR => {
|
||
try current_thread.checkCancel();
|
||
continue;
|
||
},
|
||
.CANCELED => return current_thread.endSyscallCanceled(),
|
||
else => |e| {
|
||
current_thread.endSyscall();
|
||
switch (e) {
|
||
.ACCES => return error.AccessDenied,
|
||
.PERM => return error.PermissionDenied,
|
||
.ROFS => return error.ReadOnlyFileSystem,
|
||
.LOOP => return error.SymLinkLoop,
|
||
.TXTBSY => return error.FileBusy,
|
||
.NOTDIR => return error.FileNotFound,
|
||
.NOENT => return error.FileNotFound,
|
||
.NAMETOOLONG => return error.NameTooLong,
|
||
.INVAL => |err| return errnoBug(err),
|
||
.FAULT => |err| return errnoBug(err),
|
||
.IO => return error.InputOutput,
|
||
.NOMEM => return error.SystemResources,
|
||
.ILSEQ => return error.BadPathName,
|
||
else => |err| return posix.unexpectedErrno(err),
|
||
}
|
||
},
|
||
}
|
||
}
|
||
}
|
||
|
||
fn dirAccessWasi(
|
||
userdata: ?*anyopaque,
|
||
dir: Io.Dir,
|
||
sub_path: []const u8,
|
||
options: Io.Dir.AccessOptions,
|
||
) Io.Dir.AccessError!void {
|
||
if (builtin.link_libc) return dirAccessPosix(userdata, dir, sub_path, options);
|
||
const t: *Threaded = @ptrCast(@alignCast(userdata));
|
||
const current_thread = Thread.getCurrent(t);
|
||
const wasi = std.os.wasi;
|
||
const flags: wasi.lookupflags_t = .{
|
||
.SYMLINK_FOLLOW = options.follow_symlinks,
|
||
};
|
||
var stat: wasi.filestat_t = undefined;
|
||
|
||
try current_thread.beginSyscall();
|
||
while (true) {
|
||
switch (wasi.path_filestat_get(dir.handle, flags, sub_path.ptr, sub_path.len, &stat)) {
|
||
.SUCCESS => {
|
||
current_thread.endSyscall();
|
||
break;
|
||
},
|
||
.INTR => {
|
||
try current_thread.checkCancel();
|
||
continue;
|
||
},
|
||
.CANCELED => return current_thread.endSyscallCanceled(),
|
||
else => |e| {
|
||
current_thread.endSyscall();
|
||
switch (e) {
|
||
.INVAL => |err| return errnoBug(err),
|
||
.BADF => |err| return errnoBug(err), // File descriptor used after closed.
|
||
.NOMEM => return error.SystemResources,
|
||
.ACCES => return error.AccessDenied,
|
||
.FAULT => |err| return errnoBug(err),
|
||
.NAMETOOLONG => return error.NameTooLong,
|
||
.NOENT => return error.FileNotFound,
|
||
.NOTDIR => return error.FileNotFound,
|
||
.NOTCAPABLE => return error.AccessDenied,
|
||
.ILSEQ => return error.BadPathName,
|
||
else => |err| return posix.unexpectedErrno(err),
|
||
}
|
||
},
|
||
}
|
||
}
|
||
|
||
if (!options.read and !options.write and !options.execute)
|
||
return;
|
||
|
||
var directory: wasi.fdstat_t = undefined;
|
||
if (wasi.fd_fdstat_get(dir.handle, &directory) != .SUCCESS)
|
||
return error.AccessDenied;
|
||
|
||
var rights: wasi.rights_t = .{};
|
||
if (options.read) {
|
||
if (stat.filetype == .DIRECTORY) {
|
||
rights.FD_READDIR = true;
|
||
} else {
|
||
rights.FD_READ = true;
|
||
}
|
||
}
|
||
if (options.write)
|
||
rights.FD_WRITE = true;
|
||
|
||
// No validation for execution.
|
||
|
||
// https://github.com/ziglang/zig/issues/18882
|
||
const rights_int: u64 = @bitCast(rights);
|
||
const inheriting_int: u64 = @bitCast(directory.fs_rights_inheriting);
|
||
if ((rights_int & inheriting_int) != rights_int)
|
||
return error.AccessDenied;
|
||
}
|
||
|
||
fn dirAccessWindows(
|
||
userdata: ?*anyopaque,
|
||
dir: Io.Dir,
|
||
sub_path: []const u8,
|
||
options: Io.Dir.AccessOptions,
|
||
) Io.Dir.AccessError!void {
|
||
const t: *Threaded = @ptrCast(@alignCast(userdata));
|
||
const current_thread = Thread.getCurrent(t);
|
||
try current_thread.checkCancel();
|
||
|
||
_ = options; // TODO
|
||
|
||
const sub_path_w_array = try windows.sliceToPrefixedFileW(dir.handle, sub_path);
|
||
const sub_path_w = sub_path_w_array.span();
|
||
|
||
if (sub_path_w[0] == '.' and sub_path_w[1] == 0) return;
|
||
if (sub_path_w[0] == '.' and sub_path_w[1] == '.' and sub_path_w[2] == 0) return;
|
||
|
||
const path_len_bytes = std.math.cast(u16, std.mem.sliceTo(sub_path_w, 0).len * 2) orelse
|
||
return error.NameTooLong;
|
||
var nt_name: windows.UNICODE_STRING = .{
|
||
.Length = path_len_bytes,
|
||
.MaximumLength = path_len_bytes,
|
||
.Buffer = @constCast(sub_path_w.ptr),
|
||
};
|
||
var attr = windows.OBJECT_ATTRIBUTES{
|
||
.Length = @sizeOf(windows.OBJECT_ATTRIBUTES),
|
||
.RootDirectory = if (std.fs.path.isAbsoluteWindowsWtf16(sub_path_w)) null else dir.handle,
|
||
.Attributes = 0, // Note we do not use OBJ_CASE_INSENSITIVE here.
|
||
.ObjectName = &nt_name,
|
||
.SecurityDescriptor = null,
|
||
.SecurityQualityOfService = null,
|
||
};
|
||
var basic_info: windows.FILE_BASIC_INFORMATION = undefined;
|
||
switch (windows.ntdll.NtQueryAttributesFile(&attr, &basic_info)) {
|
||
.SUCCESS => return,
|
||
.OBJECT_NAME_NOT_FOUND => return error.FileNotFound,
|
||
.OBJECT_PATH_NOT_FOUND => return error.FileNotFound,
|
||
.OBJECT_NAME_INVALID => |err| return windows.statusBug(err),
|
||
.INVALID_PARAMETER => |err| return windows.statusBug(err),
|
||
.ACCESS_DENIED => return error.AccessDenied,
|
||
.OBJECT_PATH_SYNTAX_BAD => |err| return windows.statusBug(err),
|
||
else => |rc| return windows.unexpectedStatus(rc),
|
||
}
|
||
}
|
||
|
||
const dirCreateFile = switch (native_os) {
|
||
.windows => dirCreateFileWindows,
|
||
.wasi => dirCreateFileWasi,
|
||
else => dirCreateFilePosix,
|
||
};
|
||
|
||
fn dirCreateFilePosix(
|
||
userdata: ?*anyopaque,
|
||
dir: Io.Dir,
|
||
sub_path: []const u8,
|
||
flags: Io.File.CreateFlags,
|
||
) Io.File.OpenError!Io.File {
|
||
const t: *Threaded = @ptrCast(@alignCast(userdata));
|
||
const current_thread = Thread.getCurrent(t);
|
||
|
||
var path_buffer: [posix.PATH_MAX]u8 = undefined;
|
||
const sub_path_posix = try pathToPosix(sub_path, &path_buffer);
|
||
|
||
var os_flags: posix.O = .{
|
||
.ACCMODE = if (flags.read) .RDWR else .WRONLY,
|
||
.CREAT = true,
|
||
.TRUNC = flags.truncate,
|
||
.EXCL = flags.exclusive,
|
||
};
|
||
if (@hasField(posix.O, "LARGEFILE")) os_flags.LARGEFILE = true;
|
||
if (@hasField(posix.O, "CLOEXEC")) os_flags.CLOEXEC = true;
|
||
|
||
// Use the O locking flags if the os supports them to acquire the lock
|
||
// atomically. Note that the NONBLOCK flag is removed after the openat()
|
||
// call is successful.
|
||
if (have_flock_open_flags) switch (flags.lock) {
|
||
.none => {},
|
||
.shared => {
|
||
os_flags.SHLOCK = true;
|
||
os_flags.NONBLOCK = flags.lock_nonblocking;
|
||
},
|
||
.exclusive => {
|
||
os_flags.EXLOCK = true;
|
||
os_flags.NONBLOCK = flags.lock_nonblocking;
|
||
},
|
||
};
|
||
|
||
try current_thread.beginSyscall();
|
||
const fd: posix.fd_t = while (true) {
|
||
const rc = openat_sym(dir.handle, sub_path_posix, os_flags, flags.mode);
|
||
switch (posix.errno(rc)) {
|
||
.SUCCESS => {
|
||
current_thread.endSyscall();
|
||
break @intCast(rc);
|
||
},
|
||
.INTR => {
|
||
try current_thread.checkCancel();
|
||
continue;
|
||
},
|
||
.CANCELED => return current_thread.endSyscallCanceled(),
|
||
else => |e| {
|
||
current_thread.endSyscall();
|
||
switch (e) {
|
||
.FAULT => |err| return errnoBug(err),
|
||
.INVAL => return error.BadPathName,
|
||
.BADF => |err| return errnoBug(err), // File descriptor used after closed.
|
||
.ACCES => return error.AccessDenied,
|
||
.FBIG => return error.FileTooBig,
|
||
.OVERFLOW => return error.FileTooBig,
|
||
.ISDIR => return error.IsDir,
|
||
.LOOP => return error.SymLinkLoop,
|
||
.MFILE => return error.ProcessFdQuotaExceeded,
|
||
.NAMETOOLONG => return error.NameTooLong,
|
||
.NFILE => return error.SystemFdQuotaExceeded,
|
||
.NODEV => return error.NoDevice,
|
||
.NOENT => return error.FileNotFound,
|
||
.SRCH => return error.ProcessNotFound,
|
||
.NOMEM => return error.SystemResources,
|
||
.NOSPC => return error.NoSpaceLeft,
|
||
.NOTDIR => return error.NotDir,
|
||
.PERM => return error.PermissionDenied,
|
||
.EXIST => return error.PathAlreadyExists,
|
||
.BUSY => return error.DeviceBusy,
|
||
.OPNOTSUPP => return error.FileLocksUnsupported,
|
||
.AGAIN => return error.WouldBlock,
|
||
.TXTBSY => return error.FileBusy,
|
||
.NXIO => return error.NoDevice,
|
||
.ILSEQ => return error.BadPathName,
|
||
else => |err| return posix.unexpectedErrno(err),
|
||
}
|
||
},
|
||
}
|
||
};
|
||
errdefer posix.close(fd);
|
||
|
||
if (have_flock and !have_flock_open_flags and flags.lock != .none) {
|
||
const lock_nonblocking: i32 = if (flags.lock_nonblocking) posix.LOCK.NB else 0;
|
||
const lock_flags = switch (flags.lock) {
|
||
.none => unreachable,
|
||
.shared => posix.LOCK.SH | lock_nonblocking,
|
||
.exclusive => posix.LOCK.EX | lock_nonblocking,
|
||
};
|
||
|
||
try current_thread.beginSyscall();
|
||
while (true) {
|
||
switch (posix.errno(posix.system.flock(fd, lock_flags))) {
|
||
.SUCCESS => {
|
||
current_thread.endSyscall();
|
||
break;
|
||
},
|
||
.INTR => {
|
||
try current_thread.checkCancel();
|
||
continue;
|
||
},
|
||
.CANCELED => return current_thread.endSyscallCanceled(),
|
||
else => |e| {
|
||
current_thread.endSyscall();
|
||
switch (e) {
|
||
.BADF => |err| return errnoBug(err), // File descriptor used after closed.
|
||
.INVAL => |err| return errnoBug(err), // invalid parameters
|
||
.NOLCK => return error.SystemResources,
|
||
.AGAIN => return error.WouldBlock,
|
||
.OPNOTSUPP => return error.FileLocksUnsupported,
|
||
else => |err| return posix.unexpectedErrno(err),
|
||
}
|
||
},
|
||
}
|
||
}
|
||
}
|
||
|
||
if (have_flock_open_flags and flags.lock_nonblocking) {
|
||
try current_thread.beginSyscall();
|
||
var fl_flags: usize = while (true) {
|
||
const rc = posix.system.fcntl(fd, posix.F.GETFL, @as(usize, 0));
|
||
switch (posix.errno(rc)) {
|
||
.SUCCESS => {
|
||
current_thread.endSyscall();
|
||
break @intCast(rc);
|
||
},
|
||
.INTR => {
|
||
try current_thread.checkCancel();
|
||
continue;
|
||
},
|
||
else => |err| {
|
||
current_thread.endSyscall();
|
||
return posix.unexpectedErrno(err);
|
||
},
|
||
}
|
||
};
|
||
|
||
fl_flags |= @as(usize, 1 << @bitOffsetOf(posix.O, "NONBLOCK"));
|
||
|
||
try current_thread.beginSyscall();
|
||
while (true) {
|
||
switch (posix.errno(posix.system.fcntl(fd, posix.F.SETFL, fl_flags))) {
|
||
.SUCCESS => {
|
||
current_thread.endSyscall();
|
||
break;
|
||
},
|
||
.INTR => {
|
||
try current_thread.checkCancel();
|
||
continue;
|
||
},
|
||
else => |err| {
|
||
current_thread.endSyscall();
|
||
return posix.unexpectedErrno(err);
|
||
},
|
||
}
|
||
}
|
||
}
|
||
|
||
return .{ .handle = fd };
|
||
}
|
||
|
||
fn dirCreateFileWindows(
|
||
userdata: ?*anyopaque,
|
||
dir: Io.Dir,
|
||
sub_path: []const u8,
|
||
flags: Io.File.CreateFlags,
|
||
) Io.File.OpenError!Io.File {
|
||
const w = windows;
|
||
const t: *Threaded = @ptrCast(@alignCast(userdata));
|
||
const current_thread = Thread.getCurrent(t);
|
||
try current_thread.checkCancel();
|
||
|
||
const sub_path_w_array = try w.sliceToPrefixedFileW(dir.handle, sub_path);
|
||
const sub_path_w = sub_path_w_array.span();
|
||
|
||
const read_flag = if (flags.read) @as(u32, w.GENERIC_READ) else 0;
|
||
const handle = try w.OpenFile(sub_path_w, .{
|
||
.dir = dir.handle,
|
||
.access_mask = w.SYNCHRONIZE | w.GENERIC_WRITE | read_flag,
|
||
.creation = if (flags.exclusive)
|
||
@as(u32, w.FILE_CREATE)
|
||
else if (flags.truncate)
|
||
@as(u32, w.FILE_OVERWRITE_IF)
|
||
else
|
||
@as(u32, w.FILE_OPEN_IF),
|
||
});
|
||
errdefer w.CloseHandle(handle);
|
||
var io_status_block: w.IO_STATUS_BLOCK = undefined;
|
||
const range_off: w.LARGE_INTEGER = 0;
|
||
const range_len: w.LARGE_INTEGER = 1;
|
||
const exclusive = switch (flags.lock) {
|
||
.none => return .{ .handle = handle },
|
||
.shared => false,
|
||
.exclusive => true,
|
||
};
|
||
try w.LockFile(
|
||
handle,
|
||
null,
|
||
null,
|
||
null,
|
||
&io_status_block,
|
||
&range_off,
|
||
&range_len,
|
||
null,
|
||
@intFromBool(flags.lock_nonblocking),
|
||
@intFromBool(exclusive),
|
||
);
|
||
return .{ .handle = handle };
|
||
}
|
||
|
||
fn dirCreateFileWasi(
|
||
userdata: ?*anyopaque,
|
||
dir: Io.Dir,
|
||
sub_path: []const u8,
|
||
flags: Io.File.CreateFlags,
|
||
) Io.File.OpenError!Io.File {
|
||
const t: *Threaded = @ptrCast(@alignCast(userdata));
|
||
const current_thread = Thread.getCurrent(t);
|
||
const wasi = std.os.wasi;
|
||
const lookup_flags: wasi.lookupflags_t = .{};
|
||
const oflags: wasi.oflags_t = .{
|
||
.CREAT = true,
|
||
.TRUNC = flags.truncate,
|
||
.EXCL = flags.exclusive,
|
||
};
|
||
const fdflags: wasi.fdflags_t = .{};
|
||
const base: wasi.rights_t = .{
|
||
.FD_READ = flags.read,
|
||
.FD_WRITE = true,
|
||
.FD_DATASYNC = true,
|
||
.FD_SEEK = true,
|
||
.FD_TELL = true,
|
||
.FD_FDSTAT_SET_FLAGS = true,
|
||
.FD_SYNC = true,
|
||
.FD_ALLOCATE = true,
|
||
.FD_ADVISE = true,
|
||
.FD_FILESTAT_SET_TIMES = true,
|
||
.FD_FILESTAT_SET_SIZE = true,
|
||
.FD_FILESTAT_GET = true,
|
||
// POLL_FD_READWRITE only grants extra rights if the corresponding FD_READ and/or
|
||
// FD_WRITE is also set.
|
||
.POLL_FD_READWRITE = true,
|
||
};
|
||
const inheriting: wasi.rights_t = .{};
|
||
var fd: posix.fd_t = undefined;
|
||
try current_thread.beginSyscall();
|
||
while (true) {
|
||
switch (wasi.path_open(dir.handle, lookup_flags, sub_path.ptr, sub_path.len, oflags, base, inheriting, fdflags, &fd)) {
|
||
.SUCCESS => {
|
||
current_thread.endSyscall();
|
||
return .{ .handle = fd };
|
||
},
|
||
.INTR => {
|
||
try current_thread.checkCancel();
|
||
continue;
|
||
},
|
||
.CANCELED => return current_thread.endSyscallCanceled(),
|
||
else => |e| {
|
||
current_thread.endSyscall();
|
||
switch (e) {
|
||
.FAULT => |err| return errnoBug(err),
|
||
.INVAL => return error.BadPathName,
|
||
.BADF => |err| return errnoBug(err), // File descriptor used after closed.
|
||
.ACCES => return error.AccessDenied,
|
||
.FBIG => return error.FileTooBig,
|
||
.OVERFLOW => return error.FileTooBig,
|
||
.ISDIR => return error.IsDir,
|
||
.LOOP => return error.SymLinkLoop,
|
||
.MFILE => return error.ProcessFdQuotaExceeded,
|
||
.NAMETOOLONG => return error.NameTooLong,
|
||
.NFILE => return error.SystemFdQuotaExceeded,
|
||
.NODEV => return error.NoDevice,
|
||
.NOENT => return error.FileNotFound,
|
||
.NOMEM => return error.SystemResources,
|
||
.NOSPC => return error.NoSpaceLeft,
|
||
.NOTDIR => return error.NotDir,
|
||
.PERM => return error.PermissionDenied,
|
||
.EXIST => return error.PathAlreadyExists,
|
||
.BUSY => return error.DeviceBusy,
|
||
.NOTCAPABLE => return error.AccessDenied,
|
||
.ILSEQ => return error.BadPathName,
|
||
else => |err| return posix.unexpectedErrno(err),
|
||
}
|
||
},
|
||
}
|
||
}
|
||
}
|
||
|
||
const dirOpenFile = switch (native_os) {
|
||
.windows => dirOpenFileWindows,
|
||
.wasi => dirOpenFileWasi,
|
||
else => dirOpenFilePosix,
|
||
};
|
||
|
||
fn dirOpenFilePosix(
|
||
userdata: ?*anyopaque,
|
||
dir: Io.Dir,
|
||
sub_path: []const u8,
|
||
flags: Io.File.OpenFlags,
|
||
) Io.File.OpenError!Io.File {
|
||
const t: *Threaded = @ptrCast(@alignCast(userdata));
|
||
const current_thread = Thread.getCurrent(t);
|
||
|
||
var path_buffer: [posix.PATH_MAX]u8 = undefined;
|
||
const sub_path_posix = try pathToPosix(sub_path, &path_buffer);
|
||
|
||
var os_flags: posix.O = switch (native_os) {
|
||
.wasi => .{
|
||
.read = flags.mode != .write_only,
|
||
.write = flags.mode != .read_only,
|
||
},
|
||
else => .{
|
||
.ACCMODE = switch (flags.mode) {
|
||
.read_only => .RDONLY,
|
||
.write_only => .WRONLY,
|
||
.read_write => .RDWR,
|
||
},
|
||
},
|
||
};
|
||
if (@hasField(posix.O, "CLOEXEC")) os_flags.CLOEXEC = true;
|
||
if (@hasField(posix.O, "LARGEFILE")) os_flags.LARGEFILE = true;
|
||
if (@hasField(posix.O, "NOCTTY")) os_flags.NOCTTY = !flags.allow_ctty;
|
||
|
||
// Use the O locking flags if the os supports them to acquire the lock
|
||
// atomically. Note that the NONBLOCK flag is removed after the openat()
|
||
// call is successful.
|
||
if (have_flock_open_flags) switch (flags.lock) {
|
||
.none => {},
|
||
.shared => {
|
||
os_flags.SHLOCK = true;
|
||
os_flags.NONBLOCK = flags.lock_nonblocking;
|
||
},
|
||
.exclusive => {
|
||
os_flags.EXLOCK = true;
|
||
os_flags.NONBLOCK = flags.lock_nonblocking;
|
||
},
|
||
};
|
||
|
||
try current_thread.beginSyscall();
|
||
const fd: posix.fd_t = while (true) {
|
||
const rc = openat_sym(dir.handle, sub_path_posix, os_flags, @as(posix.mode_t, 0));
|
||
switch (posix.errno(rc)) {
|
||
.SUCCESS => {
|
||
current_thread.endSyscall();
|
||
break @intCast(rc);
|
||
},
|
||
.INTR => {
|
||
try current_thread.checkCancel();
|
||
continue;
|
||
},
|
||
.CANCELED => return current_thread.endSyscallCanceled(),
|
||
else => |e| {
|
||
current_thread.endSyscall();
|
||
switch (e) {
|
||
.FAULT => |err| return errnoBug(err),
|
||
.INVAL => return error.BadPathName,
|
||
.BADF => |err| return errnoBug(err), // File descriptor used after closed.
|
||
.ACCES => return error.AccessDenied,
|
||
.FBIG => return error.FileTooBig,
|
||
.OVERFLOW => return error.FileTooBig,
|
||
.ISDIR => return error.IsDir,
|
||
.LOOP => return error.SymLinkLoop,
|
||
.MFILE => return error.ProcessFdQuotaExceeded,
|
||
.NAMETOOLONG => return error.NameTooLong,
|
||
.NFILE => return error.SystemFdQuotaExceeded,
|
||
.NODEV => return error.NoDevice,
|
||
.NOENT => return error.FileNotFound,
|
||
.SRCH => return error.ProcessNotFound,
|
||
.NOMEM => return error.SystemResources,
|
||
.NOSPC => return error.NoSpaceLeft,
|
||
.NOTDIR => return error.NotDir,
|
||
.PERM => return error.PermissionDenied,
|
||
.EXIST => return error.PathAlreadyExists,
|
||
.BUSY => return error.DeviceBusy,
|
||
.OPNOTSUPP => return error.FileLocksUnsupported,
|
||
.AGAIN => return error.WouldBlock,
|
||
.TXTBSY => return error.FileBusy,
|
||
.NXIO => return error.NoDevice,
|
||
.ILSEQ => return error.BadPathName,
|
||
else => |err| return posix.unexpectedErrno(err),
|
||
}
|
||
},
|
||
}
|
||
};
|
||
errdefer posix.close(fd);
|
||
|
||
if (have_flock and !have_flock_open_flags and flags.lock != .none) {
|
||
const lock_nonblocking: i32 = if (flags.lock_nonblocking) posix.LOCK.NB else 0;
|
||
const lock_flags = switch (flags.lock) {
|
||
.none => unreachable,
|
||
.shared => posix.LOCK.SH | lock_nonblocking,
|
||
.exclusive => posix.LOCK.EX | lock_nonblocking,
|
||
};
|
||
try current_thread.beginSyscall();
|
||
while (true) {
|
||
switch (posix.errno(posix.system.flock(fd, lock_flags))) {
|
||
.SUCCESS => {
|
||
current_thread.endSyscall();
|
||
break;
|
||
},
|
||
.INTR => {
|
||
try current_thread.checkCancel();
|
||
continue;
|
||
},
|
||
.CANCELED => return current_thread.endSyscallCanceled(),
|
||
else => |e| {
|
||
current_thread.endSyscall();
|
||
switch (e) {
|
||
.BADF => |err| return errnoBug(err), // File descriptor used after closed.
|
||
.INVAL => |err| return errnoBug(err), // invalid parameters
|
||
.NOLCK => return error.SystemResources,
|
||
.AGAIN => return error.WouldBlock,
|
||
.OPNOTSUPP => return error.FileLocksUnsupported,
|
||
else => |err| return posix.unexpectedErrno(err),
|
||
}
|
||
},
|
||
}
|
||
}
|
||
}
|
||
|
||
if (have_flock_open_flags and flags.lock_nonblocking) {
|
||
try current_thread.beginSyscall();
|
||
var fl_flags: usize = while (true) {
|
||
const rc = posix.system.fcntl(fd, posix.F.GETFL, @as(usize, 0));
|
||
switch (posix.errno(rc)) {
|
||
.SUCCESS => {
|
||
current_thread.endSyscall();
|
||
break @intCast(rc);
|
||
},
|
||
.INTR => {
|
||
try current_thread.checkCancel();
|
||
continue;
|
||
},
|
||
.CANCELED => return current_thread.endSyscallCanceled(),
|
||
else => |err| {
|
||
current_thread.endSyscall();
|
||
return posix.unexpectedErrno(err);
|
||
},
|
||
}
|
||
};
|
||
|
||
fl_flags |= @as(usize, 1 << @bitOffsetOf(posix.O, "NONBLOCK"));
|
||
|
||
try current_thread.beginSyscall();
|
||
while (true) {
|
||
switch (posix.errno(posix.system.fcntl(fd, posix.F.SETFL, fl_flags))) {
|
||
.SUCCESS => {
|
||
current_thread.endSyscall();
|
||
break;
|
||
},
|
||
.INTR => {
|
||
try current_thread.checkCancel();
|
||
continue;
|
||
},
|
||
.CANCELED => return current_thread.endSyscallCanceled(),
|
||
else => |err| {
|
||
current_thread.endSyscall();
|
||
return posix.unexpectedErrno(err);
|
||
},
|
||
}
|
||
}
|
||
}
|
||
|
||
return .{ .handle = fd };
|
||
}
|
||
|
||
fn dirOpenFileWindows(
|
||
userdata: ?*anyopaque,
|
||
dir: Io.Dir,
|
||
sub_path: []const u8,
|
||
flags: Io.File.OpenFlags,
|
||
) Io.File.OpenError!Io.File {
|
||
const t: *Threaded = @ptrCast(@alignCast(userdata));
|
||
const sub_path_w_array = try windows.sliceToPrefixedFileW(dir.handle, sub_path);
|
||
const sub_path_w = sub_path_w_array.span();
|
||
const dir_handle = if (std.fs.path.isAbsoluteWindowsWtf16(sub_path_w)) null else dir.handle;
|
||
return dirOpenFileWtf16(t, dir_handle, sub_path_w, flags);
|
||
}
|
||
|
||
pub fn dirOpenFileWtf16(
|
||
t: *Threaded,
|
||
dir_handle: ?windows.HANDLE,
|
||
sub_path_w: [:0]const u16,
|
||
flags: Io.File.OpenFlags,
|
||
) Io.File.OpenError!Io.File {
|
||
if (std.mem.eql(u16, sub_path_w, &.{'.'})) return error.IsDir;
|
||
if (std.mem.eql(u16, sub_path_w, &.{ '.', '.' })) return error.IsDir;
|
||
const path_len_bytes = std.math.cast(u16, sub_path_w.len * 2) orelse return error.NameTooLong;
|
||
const current_thread = Thread.getCurrent(t);
|
||
const w = windows;
|
||
|
||
var nt_name: w.UNICODE_STRING = .{
|
||
.Length = path_len_bytes,
|
||
.MaximumLength = path_len_bytes,
|
||
.Buffer = @constCast(sub_path_w.ptr),
|
||
};
|
||
var attr: w.OBJECT_ATTRIBUTES = .{
|
||
.Length = @sizeOf(w.OBJECT_ATTRIBUTES),
|
||
.RootDirectory = dir_handle,
|
||
.Attributes = 0,
|
||
.ObjectName = &nt_name,
|
||
.SecurityDescriptor = null,
|
||
.SecurityQualityOfService = null,
|
||
};
|
||
var io_status_block: w.IO_STATUS_BLOCK = undefined;
|
||
const blocking_flag: w.ULONG = w.FILE_SYNCHRONOUS_IO_NONALERT;
|
||
const file_or_dir_flag: w.ULONG = w.FILE_NON_DIRECTORY_FILE;
|
||
// If we're not following symlinks, we need to ensure we don't pass in any
|
||
// synchronization flags such as FILE_SYNCHRONOUS_IO_NONALERT.
|
||
const create_file_flags: w.ULONG = file_or_dir_flag |
|
||
if (flags.follow_symlinks) blocking_flag else w.FILE_OPEN_REPARSE_POINT;
|
||
|
||
// There are multiple kernel bugs being worked around with retries.
|
||
const max_attempts = 13;
|
||
var attempt: u5 = 0;
|
||
|
||
const handle = while (true) {
|
||
try current_thread.checkCancel();
|
||
|
||
var result: w.HANDLE = undefined;
|
||
const rc = w.ntdll.NtCreateFile(
|
||
&result,
|
||
w.SYNCHRONIZE |
|
||
(if (flags.isRead()) @as(u32, w.GENERIC_READ) else 0) |
|
||
(if (flags.isWrite()) @as(u32, w.GENERIC_WRITE) else 0),
|
||
&attr,
|
||
&io_status_block,
|
||
null,
|
||
w.FILE_ATTRIBUTE_NORMAL,
|
||
w.FILE_SHARE_WRITE | w.FILE_SHARE_READ | w.FILE_SHARE_DELETE,
|
||
w.FILE_OPEN,
|
||
create_file_flags,
|
||
null,
|
||
0,
|
||
);
|
||
switch (rc) {
|
||
.SUCCESS => break result,
|
||
.OBJECT_NAME_INVALID => return error.BadPathName,
|
||
.OBJECT_NAME_NOT_FOUND => return error.FileNotFound,
|
||
.OBJECT_PATH_NOT_FOUND => return error.FileNotFound,
|
||
.BAD_NETWORK_PATH => return error.NetworkNotFound, // \\server was not found
|
||
.BAD_NETWORK_NAME => return error.NetworkNotFound, // \\server was found but \\server\share wasn't
|
||
.NO_MEDIA_IN_DEVICE => return error.NoDevice,
|
||
.INVALID_PARAMETER => |err| return w.statusBug(err),
|
||
.SHARING_VIOLATION => {
|
||
// This occurs if the file attempting to be opened is a running
|
||
// executable. However, there's a kernel bug: the error may be
|
||
// incorrectly returned for an indeterminate amount of time
|
||
// after an executable file is closed. Here we work around the
|
||
// kernel bug with retry attempts.
|
||
if (max_attempts - attempt == 0) return error.SharingViolation;
|
||
_ = w.kernel32.SleepEx((@as(u32, 1) << attempt) >> 1, w.TRUE);
|
||
attempt += 1;
|
||
continue;
|
||
},
|
||
.ACCESS_DENIED => return error.AccessDenied,
|
||
.PIPE_BUSY => return error.PipeBusy,
|
||
.PIPE_NOT_AVAILABLE => return error.NoDevice,
|
||
.OBJECT_PATH_SYNTAX_BAD => |err| return w.statusBug(err),
|
||
.OBJECT_NAME_COLLISION => return error.PathAlreadyExists,
|
||
.FILE_IS_A_DIRECTORY => return error.IsDir,
|
||
.NOT_A_DIRECTORY => return error.NotDir,
|
||
.USER_MAPPED_FILE => return error.AccessDenied,
|
||
.INVALID_HANDLE => |err| return w.statusBug(err),
|
||
.DELETE_PENDING => {
|
||
// This error means that there *was* a file in this location on
|
||
// the file system, but it was deleted. However, the OS is not
|
||
// finished with the deletion operation, and so this CreateFile
|
||
// call has failed. Here, we simulate the kernel bug being
|
||
// fixed by sleeping and retrying until the error goes away.
|
||
if (max_attempts - attempt == 0) return error.SharingViolation;
|
||
_ = w.kernel32.SleepEx((@as(u32, 1) << attempt) >> 1, w.TRUE);
|
||
attempt += 1;
|
||
continue;
|
||
},
|
||
.VIRUS_INFECTED, .VIRUS_DELETED => return error.AntivirusInterference,
|
||
else => return w.unexpectedStatus(rc),
|
||
}
|
||
};
|
||
errdefer w.CloseHandle(handle);
|
||
|
||
const range_off: w.LARGE_INTEGER = 0;
|
||
const range_len: w.LARGE_INTEGER = 1;
|
||
const exclusive = switch (flags.lock) {
|
||
.none => return .{ .handle = handle },
|
||
.shared => false,
|
||
.exclusive => true,
|
||
};
|
||
try w.LockFile(
|
||
handle,
|
||
null,
|
||
null,
|
||
null,
|
||
&io_status_block,
|
||
&range_off,
|
||
&range_len,
|
||
null,
|
||
@intFromBool(flags.lock_nonblocking),
|
||
@intFromBool(exclusive),
|
||
);
|
||
return .{ .handle = handle };
|
||
}
|
||
|
||
fn dirOpenFileWasi(
|
||
userdata: ?*anyopaque,
|
||
dir: Io.Dir,
|
||
sub_path: []const u8,
|
||
flags: Io.File.OpenFlags,
|
||
) Io.File.OpenError!Io.File {
|
||
if (builtin.link_libc) return dirOpenFilePosix(userdata, dir, sub_path, flags);
|
||
const t: *Threaded = @ptrCast(@alignCast(userdata));
|
||
const current_thread = Thread.getCurrent(t);
|
||
const wasi = std.os.wasi;
|
||
var base: std.os.wasi.rights_t = .{};
|
||
// POLL_FD_READWRITE only grants extra rights if the corresponding FD_READ and/or FD_WRITE
|
||
// is also set.
|
||
if (flags.isRead()) {
|
||
base.FD_READ = true;
|
||
base.FD_TELL = true;
|
||
base.FD_SEEK = true;
|
||
base.FD_FILESTAT_GET = true;
|
||
base.POLL_FD_READWRITE = true;
|
||
}
|
||
if (flags.isWrite()) {
|
||
base.FD_WRITE = true;
|
||
base.FD_TELL = true;
|
||
base.FD_SEEK = true;
|
||
base.FD_DATASYNC = true;
|
||
base.FD_FDSTAT_SET_FLAGS = true;
|
||
base.FD_SYNC = true;
|
||
base.FD_ALLOCATE = true;
|
||
base.FD_ADVISE = true;
|
||
base.FD_FILESTAT_SET_TIMES = true;
|
||
base.FD_FILESTAT_SET_SIZE = true;
|
||
base.POLL_FD_READWRITE = true;
|
||
}
|
||
const lookup_flags: wasi.lookupflags_t = .{};
|
||
const oflags: wasi.oflags_t = .{};
|
||
const inheriting: wasi.rights_t = .{};
|
||
const fdflags: wasi.fdflags_t = .{};
|
||
var fd: posix.fd_t = undefined;
|
||
try current_thread.beginSyscall();
|
||
while (true) {
|
||
switch (wasi.path_open(dir.handle, lookup_flags, sub_path.ptr, sub_path.len, oflags, base, inheriting, fdflags, &fd)) {
|
||
.SUCCESS => {
|
||
errdefer posix.close(fd);
|
||
current_thread.endSyscall();
|
||
return .{ .handle = fd };
|
||
},
|
||
.INTR => {
|
||
try current_thread.checkCancel();
|
||
continue;
|
||
},
|
||
.CANCELED => return current_thread.endSyscallCanceled(),
|
||
else => |e| {
|
||
current_thread.endSyscall();
|
||
switch (e) {
|
||
.FAULT => |err| return errnoBug(err),
|
||
.BADF => |err| return errnoBug(err), // File descriptor used after closed.
|
||
.ACCES => return error.AccessDenied,
|
||
.FBIG => return error.FileTooBig,
|
||
.OVERFLOW => return error.FileTooBig,
|
||
.ISDIR => return error.IsDir,
|
||
.LOOP => return error.SymLinkLoop,
|
||
.MFILE => return error.ProcessFdQuotaExceeded,
|
||
.NFILE => return error.SystemFdQuotaExceeded,
|
||
.NODEV => return error.NoDevice,
|
||
.NOENT => return error.FileNotFound,
|
||
.NOMEM => return error.SystemResources,
|
||
.NOTDIR => return error.NotDir,
|
||
.PERM => return error.PermissionDenied,
|
||
.BUSY => return error.DeviceBusy,
|
||
.NOTCAPABLE => return error.AccessDenied,
|
||
.NAMETOOLONG => return error.NameTooLong,
|
||
.INVAL => return error.BadPathName,
|
||
.ILSEQ => return error.BadPathName,
|
||
else => |err| return posix.unexpectedErrno(err),
|
||
}
|
||
},
|
||
}
|
||
}
|
||
}
|
||
|
||
const dirOpenDir = switch (native_os) {
|
||
.wasi => dirOpenDirWasi,
|
||
.haiku => dirOpenDirHaiku,
|
||
else => dirOpenDirPosix,
|
||
};
|
||
|
||
/// This function is also used for WASI when libc is linked.
|
||
fn dirOpenDirPosix(
|
||
userdata: ?*anyopaque,
|
||
dir: Io.Dir,
|
||
sub_path: []const u8,
|
||
options: Io.Dir.OpenOptions,
|
||
) Io.Dir.OpenError!Io.Dir {
|
||
const t: *Threaded = @ptrCast(@alignCast(userdata));
|
||
|
||
if (is_windows) {
|
||
const sub_path_w = try windows.sliceToPrefixedFileW(dir.handle, sub_path);
|
||
return dirOpenDirWindows(t, dir, sub_path_w.span(), options);
|
||
}
|
||
|
||
const current_thread = Thread.getCurrent(t);
|
||
|
||
var path_buffer: [posix.PATH_MAX]u8 = undefined;
|
||
const sub_path_posix = try pathToPosix(sub_path, &path_buffer);
|
||
|
||
var flags: posix.O = switch (native_os) {
|
||
.wasi => .{
|
||
.read = true,
|
||
.NOFOLLOW = !options.follow_symlinks,
|
||
.DIRECTORY = true,
|
||
},
|
||
else => .{
|
||
.ACCMODE = .RDONLY,
|
||
.NOFOLLOW = !options.follow_symlinks,
|
||
.DIRECTORY = true,
|
||
.CLOEXEC = true,
|
||
},
|
||
};
|
||
|
||
if (@hasField(posix.O, "PATH") and !options.iterate)
|
||
flags.PATH = true;
|
||
|
||
try current_thread.beginSyscall();
|
||
while (true) {
|
||
const rc = openat_sym(dir.handle, sub_path_posix, flags, @as(usize, 0));
|
||
switch (posix.errno(rc)) {
|
||
.SUCCESS => {
|
||
current_thread.endSyscall();
|
||
return .{ .handle = @intCast(rc) };
|
||
},
|
||
.INTR => {
|
||
try current_thread.checkCancel();
|
||
continue;
|
||
},
|
||
.CANCELED => return current_thread.endSyscallCanceled(),
|
||
else => |e| {
|
||
current_thread.endSyscall();
|
||
switch (e) {
|
||
.FAULT => |err| return errnoBug(err),
|
||
.INVAL => return error.BadPathName,
|
||
.BADF => |err| return errnoBug(err), // File descriptor used after closed.
|
||
.ACCES => return error.AccessDenied,
|
||
.LOOP => return error.SymLinkLoop,
|
||
.MFILE => return error.ProcessFdQuotaExceeded,
|
||
.NAMETOOLONG => return error.NameTooLong,
|
||
.NFILE => return error.SystemFdQuotaExceeded,
|
||
.NODEV => return error.NoDevice,
|
||
.NOENT => return error.FileNotFound,
|
||
.NOMEM => return error.SystemResources,
|
||
.NOTDIR => return error.NotDir,
|
||
.PERM => return error.PermissionDenied,
|
||
.BUSY => return error.DeviceBusy,
|
||
.NXIO => return error.NoDevice,
|
||
.ILSEQ => return error.BadPathName,
|
||
else => |err| return posix.unexpectedErrno(err),
|
||
}
|
||
},
|
||
}
|
||
}
|
||
}
|
||
|
||
fn dirOpenDirHaiku(
|
||
userdata: ?*anyopaque,
|
||
dir: Io.Dir,
|
||
sub_path: []const u8,
|
||
options: Io.Dir.OpenOptions,
|
||
) Io.Dir.OpenError!Io.Dir {
|
||
const t: *Threaded = @ptrCast(@alignCast(userdata));
|
||
const current_thread = Thread.getCurrent(t);
|
||
|
||
var path_buffer: [posix.PATH_MAX]u8 = undefined;
|
||
const sub_path_posix = try pathToPosix(sub_path, &path_buffer);
|
||
|
||
_ = options;
|
||
|
||
try current_thread.beginSyscall();
|
||
while (true) {
|
||
const rc = posix.system._kern_open_dir(dir.handle, sub_path_posix);
|
||
if (rc >= 0) {
|
||
current_thread.endSyscall();
|
||
return .{ .handle = rc };
|
||
}
|
||
switch (@as(posix.E, @enumFromInt(rc))) {
|
||
.INTR => {
|
||
try current_thread.checkCancel();
|
||
continue;
|
||
},
|
||
.CANCELED => return current_thread.endSyscallCanceled(),
|
||
else => |e| {
|
||
current_thread.endSyscall();
|
||
switch (e) {
|
||
.FAULT => |err| return errnoBug(err),
|
||
.INVAL => |err| return errnoBug(err),
|
||
.BADF => |err| return errnoBug(err), // File descriptor used after closed.
|
||
.ACCES => return error.AccessDenied,
|
||
.LOOP => return error.SymLinkLoop,
|
||
.MFILE => return error.ProcessFdQuotaExceeded,
|
||
.NAMETOOLONG => return error.NameTooLong,
|
||
.NFILE => return error.SystemFdQuotaExceeded,
|
||
.NODEV => return error.NoDevice,
|
||
.NOENT => return error.FileNotFound,
|
||
.NOMEM => return error.SystemResources,
|
||
.NOTDIR => return error.NotDir,
|
||
.PERM => return error.PermissionDenied,
|
||
.BUSY => return error.DeviceBusy,
|
||
else => |err| return posix.unexpectedErrno(err),
|
||
}
|
||
},
|
||
}
|
||
}
|
||
}
|
||
|
||
pub fn dirOpenDirWindows(
|
||
t: *Io.Threaded,
|
||
dir: Io.Dir,
|
||
sub_path_w: [:0]const u16,
|
||
options: Io.Dir.OpenOptions,
|
||
) Io.Dir.OpenError!Io.Dir {
|
||
const current_thread = Thread.getCurrent(t);
|
||
const w = windows;
|
||
// TODO remove some of these flags if options.access_sub_paths is false
|
||
const base_flags = w.STANDARD_RIGHTS_READ | w.FILE_READ_ATTRIBUTES | w.FILE_READ_EA |
|
||
w.SYNCHRONIZE | w.FILE_TRAVERSE;
|
||
const access_mask: u32 = if (options.iterate) base_flags | w.FILE_LIST_DIRECTORY else base_flags;
|
||
|
||
const path_len_bytes: u16 = @intCast(sub_path_w.len * 2);
|
||
var nt_name: w.UNICODE_STRING = .{
|
||
.Length = path_len_bytes,
|
||
.MaximumLength = path_len_bytes,
|
||
.Buffer = @constCast(sub_path_w.ptr),
|
||
};
|
||
var attr: w.OBJECT_ATTRIBUTES = .{
|
||
.Length = @sizeOf(w.OBJECT_ATTRIBUTES),
|
||
.RootDirectory = if (std.fs.path.isAbsoluteWindowsWtf16(sub_path_w)) null else dir.handle,
|
||
.Attributes = 0, // Note we do not use OBJ_CASE_INSENSITIVE here.
|
||
.ObjectName = &nt_name,
|
||
.SecurityDescriptor = null,
|
||
.SecurityQualityOfService = null,
|
||
};
|
||
const open_reparse_point: w.DWORD = if (!options.follow_symlinks) w.FILE_OPEN_REPARSE_POINT else 0x0;
|
||
var io_status_block: w.IO_STATUS_BLOCK = undefined;
|
||
var result: Io.Dir = .{ .handle = undefined };
|
||
try current_thread.checkCancel();
|
||
const rc = w.ntdll.NtCreateFile(
|
||
&result.handle,
|
||
access_mask,
|
||
&attr,
|
||
&io_status_block,
|
||
null,
|
||
w.FILE_ATTRIBUTE_NORMAL,
|
||
w.FILE_SHARE_READ | w.FILE_SHARE_WRITE | w.FILE_SHARE_DELETE,
|
||
w.FILE_OPEN,
|
||
w.FILE_DIRECTORY_FILE | w.FILE_SYNCHRONOUS_IO_NONALERT | w.FILE_OPEN_FOR_BACKUP_INTENT | open_reparse_point,
|
||
null,
|
||
0,
|
||
);
|
||
|
||
switch (rc) {
|
||
.SUCCESS => return result,
|
||
.OBJECT_NAME_INVALID => return error.BadPathName,
|
||
.OBJECT_NAME_NOT_FOUND => return error.FileNotFound,
|
||
.OBJECT_NAME_COLLISION => |err| return w.statusBug(err),
|
||
.OBJECT_PATH_NOT_FOUND => return error.FileNotFound,
|
||
.NOT_A_DIRECTORY => return error.NotDir,
|
||
// This can happen if the directory has 'List folder contents' permission set to 'Deny'
|
||
// and the directory is trying to be opened for iteration.
|
||
.ACCESS_DENIED => return error.AccessDenied,
|
||
.INVALID_PARAMETER => |err| return w.statusBug(err),
|
||
else => return w.unexpectedStatus(rc),
|
||
}
|
||
}
|
||
|
||
const MakeOpenDirAccessMaskWOptions = struct {
|
||
no_follow: bool,
|
||
create_disposition: u32,
|
||
};
|
||
|
||
fn dirClose(userdata: ?*anyopaque, dir: Io.Dir) void {
|
||
const t: *Threaded = @ptrCast(@alignCast(userdata));
|
||
_ = t;
|
||
posix.close(dir.handle);
|
||
}
|
||
|
||
const dirRealPath = switch (native_os) {
|
||
.windows => dirRealPathWindows,
|
||
else => dirRealPathPosix,
|
||
};
|
||
|
||
fn dirRealPathWindows(userdata: ?*anyopaque, dir: Io.Dir, sub_path: []const u8, out_buffer: []u8) Io.Dir.RealPathError!usize {
|
||
const t: *Threaded = @ptrCast(@alignCast(userdata));
|
||
const w = windows;
|
||
const current_thread = Thread.getCurrent(t);
|
||
|
||
try current_thread.checkCancel();
|
||
|
||
var path_name_w = try w.sliceToPrefixedFileW(dir.handle, sub_path);
|
||
|
||
const access_mask = w.GENERIC_READ | w.SYNCHRONIZE;
|
||
const share_access = w.FILE_SHARE_READ | w.FILE_SHARE_WRITE | w.FILE_SHARE_DELETE;
|
||
const creation = w.FILE_OPEN;
|
||
const h_file = blk: {
|
||
const res = w.OpenFile(path_name_w.span(), .{
|
||
.dir = dir.handle,
|
||
.access_mask = access_mask,
|
||
.share_access = share_access,
|
||
.creation = creation,
|
||
.filter = .any,
|
||
}) catch |err| switch (err) {
|
||
error.WouldBlock => unreachable,
|
||
else => |e| return e,
|
||
};
|
||
break :blk res;
|
||
};
|
||
defer w.CloseHandle(h_file);
|
||
|
||
const wide_slice = w.GetFinalPathNameByHandle(h_file, .{}, out_buffer);
|
||
|
||
const len = std.unicode.calcWtf8Len(wide_slice);
|
||
if (len > out_buffer.len)
|
||
return error.NameTooLong;
|
||
|
||
return std.unicode.wtf16LeToWtf8(out_buffer, wide_slice);
|
||
}
|
||
|
||
fn dirRealPathPosix(userdata: ?*anyopaque, dir: Io.Dir, sub_path: []const u8, out_buffer: []u8) Io.Dir.RealPathError!usize {
|
||
if (native_os == .wasi) @compileError("unsupported operating system");
|
||
const max_path_bytes = std.fs.max_path_bytes;
|
||
|
||
const t: *Threaded = @ptrCast(@alignCast(userdata));
|
||
const current_thread = Thread.getCurrent(t);
|
||
|
||
var path_buffer: [posix.PATH_MAX]u8 = undefined;
|
||
const sub_path_posix = try pathToPosix(sub_path, &path_buffer);
|
||
|
||
var flags: posix.O = .{};
|
||
if (@hasField(posix.O, "NONBLOCK")) flags.NONBLOCK = true;
|
||
if (@hasField(posix.O, "CLOEXEC")) flags.CLOEXEC = true;
|
||
if (@hasField(posix.O, "PATH")) flags.PATH = true;
|
||
|
||
try current_thread.beginSyscall();
|
||
const fd: posix.fd_t = while (true) {
|
||
const rc = openat_sym(dir.handle, sub_path_posix, flags, 0);
|
||
switch (posix.errno(rc)) {
|
||
.SUCCESS => {
|
||
current_thread.endSyscall();
|
||
break @intCast(rc);
|
||
},
|
||
.INTR => {
|
||
try current_thread.checkCancel();
|
||
continue;
|
||
},
|
||
.CANCELED => return current_thread.endSyscallCanceled(),
|
||
else => |e| {
|
||
current_thread.endSyscall();
|
||
switch (e) {
|
||
.FAULT => |err| return errnoBug(err),
|
||
.INVAL => return error.BadPathName,
|
||
.BADF => |err| return errnoBug(err), // File descriptor used after closed.
|
||
.ACCES => return error.AccessDenied,
|
||
.FBIG => return error.FileTooBig,
|
||
.OVERFLOW => return error.FileTooBig,
|
||
.ISDIR => return error.IsDir,
|
||
.LOOP => return error.SymLinkLoop,
|
||
.MFILE => return error.ProcessFdQuotaExceeded,
|
||
.NAMETOOLONG => return error.NameTooLong,
|
||
.NFILE => return error.SystemFdQuotaExceeded,
|
||
.NODEV => return error.NoDevice,
|
||
.NOENT => return error.FileNotFound,
|
||
.SRCH => return error.ProcessNotFound,
|
||
.NOMEM => return error.SystemResources,
|
||
.NOSPC => return error.NoSpaceLeft,
|
||
.NOTDIR => return error.NotDir,
|
||
.PERM => return error.PermissionDenied,
|
||
.EXIST => return error.PathAlreadyExists,
|
||
.BUSY => return error.DeviceBusy,
|
||
.NXIO => return error.NoDevice,
|
||
.ILSEQ => return error.BadPathName,
|
||
else => |err| return posix.unexpectedErrno(err),
|
||
}
|
||
},
|
||
}
|
||
};
|
||
errdefer posix.close(fd);
|
||
|
||
switch (native_os) {
|
||
.driverkit, .ios, .maccatalyst, .macos, .tvos, .visionos, .watchos => {
|
||
// On macOS, we can use F.GETPATH fcntl command to query the OS for
|
||
// the path to the file descriptor.
|
||
@memset(out_buffer, 0);
|
||
try current_thread.beginSyscall();
|
||
while (true) {
|
||
switch (posix.errno(posix.system.fcntl(fd, posix.F.GETPATH, out_buffer))) {
|
||
.SUCCESS => {
|
||
current_thread.endSyscall();
|
||
break;
|
||
},
|
||
.INTR => {
|
||
try current_thread.checkCancel();
|
||
continue;
|
||
},
|
||
.CANCELED => return current_thread.endSyscallCanceled(),
|
||
else => |e| {
|
||
current_thread.endSyscall();
|
||
switch (e) {
|
||
.BADF => return error.FileNotFound,
|
||
.NOSPC => return error.NameTooLong,
|
||
.NOENT => return error.FileNotFound,
|
||
// TODO man pages for fcntl on macOS don't really tell you what
|
||
// errno values to expect when command is F.GETPATH...
|
||
else => |err| return posix.unexpectedErrno(err),
|
||
}
|
||
},
|
||
}
|
||
}
|
||
return std.mem.indexOfScalar(u8, &out_buffer, 0) orelse out_buffer.len;
|
||
},
|
||
.linux, .serenity, .illumos => {
|
||
var procfs_buf: ["/proc/self/path/-2147483648\x00".len]u8 = undefined;
|
||
const template = if (native_os == .illumos) "/proc/self/path/{d}" else "/proc/self/fd/{d}";
|
||
const proc_path = std.fmt.bufPrintSentinel(&procfs_buf, template, .{fd}, 0) catch unreachable;
|
||
try current_thread.beginSyscall();
|
||
while (true) {
|
||
const rc = posix.system.readlink(proc_path, out_buffer.ptr, out_buffer.len);
|
||
switch (posix.errno(rc)) {
|
||
.SUCCESS => {
|
||
current_thread.endSyscall();
|
||
const len: usize = @bitCast(rc);
|
||
return len;
|
||
},
|
||
.INTR => {
|
||
try current_thread.checkCancel();
|
||
continue;
|
||
},
|
||
.CANCELED => return current_thread.endSyscallCanceled(),
|
||
else => |e| {
|
||
current_thread.endSyscall();
|
||
switch (e) {
|
||
.ACCES => return error.AccessDenied,
|
||
.FAULT => |err| return errnoBug(err),
|
||
.INVAL => return error.NotLink,
|
||
.IO => return error.FileSystem,
|
||
.LOOP => return error.SymLinkLoop,
|
||
.NAMETOOLONG => return error.NameTooLong,
|
||
.NOENT => return error.FileNotFound,
|
||
.NOMEM => return error.SystemResources,
|
||
.NOTDIR => return error.NotDir,
|
||
.ILSEQ => |err| return errnoBug(err),
|
||
else => |err| return posix.unexpectedErrno(err),
|
||
}
|
||
},
|
||
}
|
||
}
|
||
},
|
||
.freebsd => {
|
||
var kfile: std.c.kinfo_file = undefined;
|
||
kfile.structsize = std.c.KINFO_FILE_SIZE;
|
||
try current_thread.beginSyscall();
|
||
while (true) {
|
||
switch (posix.errno(std.c.fcntl(fd, std.c.F.KINFO, @intFromPtr(&kfile)))) {
|
||
.SUCCESS => {
|
||
current_thread.endSyscall();
|
||
break;
|
||
},
|
||
.INTR => {
|
||
try current_thread.checkCancel();
|
||
continue;
|
||
},
|
||
.CANCELED => return current_thread.endSyscallCanceled(),
|
||
else => |e| {
|
||
current_thread.endSyscall();
|
||
switch (e) {
|
||
.BADF => return error.FileNotFound,
|
||
else => |err| return posix.unexpectedErrno(err),
|
||
}
|
||
},
|
||
}
|
||
}
|
||
const len = std.mem.indexOfScalar(u8, &kfile.path, 0) orelse kfile.path.len;
|
||
if (len == 0) return error.NameTooLong;
|
||
return len;
|
||
},
|
||
.netbsd, .dragonfly => {
|
||
@memset(out_buffer[0..max_path_bytes], 0);
|
||
try current_thread.beginSyscall();
|
||
while (true) {
|
||
switch (posix.errno(std.c.fcntl(fd, posix.F.GETPATH, out_buffer))) {
|
||
.SUCCESS => {
|
||
current_thread.endSyscall();
|
||
break;
|
||
},
|
||
.INTR => {
|
||
try current_thread.checkCancel();
|
||
continue;
|
||
},
|
||
.CANCELED => return current_thread.endSyscallCanceled(),
|
||
else => |e| {
|
||
current_thread.endSyscall();
|
||
switch (e) {
|
||
.ACCES => return error.AccessDenied,
|
||
.BADF => return error.FileNotFound,
|
||
.NOENT => return error.FileNotFound,
|
||
.NOMEM => return error.SystemResources,
|
||
.RANGE => return error.NameTooLong,
|
||
else => |err| return posix.unexpectedErrno(err),
|
||
}
|
||
},
|
||
}
|
||
}
|
||
return std.mem.indexOfScalar(u8, &out_buffer, 0) orelse out_buffer.len;
|
||
},
|
||
else => @compileError("unsupported OS"),
|
||
}
|
||
comptime unreachable;
|
||
}
|
||
|
||
const dirDeleteFile = switch (native_os) {
|
||
.windows => dirDeleteFileWindows,
|
||
.wasi => dirDeleteFileWasi,
|
||
else => dirDeleteFilePosix,
|
||
};
|
||
|
||
fn dirDeleteFileWindows(userdata: ?*anyopaque, dir: Io.Dir, sub_path: []const u8) Io.Dir.DeleteFileError!void {
|
||
return dirDeleteWindows(userdata, dir, sub_path, false);
|
||
}
|
||
|
||
fn dirDeleteFileWasi(userdata: ?*anyopaque, dir: Io.Dir, sub_path: []const u8) Io.Dir.DeleteFileError!void {
|
||
if (builtin.link_libc) return dirDeleteFilePosix(userdata, dir, sub_path);
|
||
const t: *Threaded = @ptrCast(@alignCast(userdata));
|
||
const current_thread = Thread.getCurrent(t);
|
||
try current_thread.beginSyscall();
|
||
while (true) {
|
||
const res = std.os.wasi.path_unlink_file(dir.handle, sub_path.ptr, sub_path.len);
|
||
switch (res) {
|
||
.SUCCESS => {
|
||
current_thread.endSyscall();
|
||
return;
|
||
},
|
||
.INTR => {
|
||
try current_thread.checkCancel();
|
||
continue;
|
||
},
|
||
.CANCELED => return current_thread.endSyscallCanceled(),
|
||
else => |e| {
|
||
current_thread.endSyscall();
|
||
switch (e) {
|
||
.ACCES => return error.AccessDenied,
|
||
.PERM => return error.PermissionDenied,
|
||
.BUSY => return error.FileBusy,
|
||
.FAULT => |err| return errnoBug(err),
|
||
.IO => return error.FileSystem,
|
||
.ISDIR => return error.IsDir,
|
||
.LOOP => return error.SymLinkLoop,
|
||
.NAMETOOLONG => return error.NameTooLong,
|
||
.NOENT => return error.FileNotFound,
|
||
.NOTDIR => return error.NotDir,
|
||
.NOMEM => return error.SystemResources,
|
||
.ROFS => return error.ReadOnlyFileSystem,
|
||
.NOTEMPTY => return error.DirNotEmpty,
|
||
.NOTCAPABLE => return error.AccessDenied,
|
||
.ILSEQ => return error.BadPathName,
|
||
.INVAL => |err| return errnoBug(err), // invalid flags, or pathname has . as last component
|
||
.BADF => |err| return errnoBug(err), // File descriptor used after closed.
|
||
else => |err| return posix.unexpectedErrno(err),
|
||
}
|
||
},
|
||
}
|
||
}
|
||
}
|
||
|
||
fn dirDeleteFilePosix(userdata: ?*anyopaque, dir: Io.Dir, sub_path: []const u8) Io.Dir.DeleteFileError!void {
|
||
const t: *Threaded = @ptrCast(@alignCast(userdata));
|
||
const current_thread = Thread.getCurrent(t);
|
||
|
||
var path_buffer: [posix.PATH_MAX]u8 = undefined;
|
||
const sub_path_posix = try pathToPosix(sub_path, &path_buffer);
|
||
|
||
try current_thread.beginSyscall();
|
||
while (true) {
|
||
switch (posix.errno(posix.system.unlinkat(dir.handle, sub_path_posix, 0))) {
|
||
.SUCCESS => {
|
||
current_thread.endSyscall();
|
||
return;
|
||
},
|
||
.INTR => {
|
||
try current_thread.checkCancel();
|
||
continue;
|
||
},
|
||
.CANCELED => return current_thread.endSyscallCanceled(),
|
||
// Some systems return permission errors when trying to delete a
|
||
// directory, so we need to handle that case specifically and
|
||
// translate the error.
|
||
.PERM => switch (native_os) {
|
||
.driverkit, .ios, .maccatalyst, .macos, .tvos, .visionos, .watchos, .freebsd, .netbsd, .dragonfly, .openbsd, .illumos => {
|
||
|
||
// Don't follow symlinks to match unlinkat (which acts on symlinks rather than follows them).
|
||
var st = std.mem.zeroes(posix.Stat);
|
||
while (true) {
|
||
try current_thread.checkCancel();
|
||
switch (posix.errno(fstatat_sym(dir.handle, sub_path_posix, &st, posix.AT.SYMLINK_NOFOLLOW))) {
|
||
.SUCCESS => {
|
||
current_thread.endSyscall();
|
||
break;
|
||
},
|
||
.INTR => continue,
|
||
.CANCELED => return current_thread.endSyscallCanceled(),
|
||
else => {
|
||
current_thread.endSyscall();
|
||
return error.PermissionDenied;
|
||
},
|
||
}
|
||
}
|
||
const is_dir = st.mode & posix.S.IFMT == posix.S.IFDIR;
|
||
if (is_dir)
|
||
return error.IsDir
|
||
else
|
||
return error.PermissionDenied;
|
||
},
|
||
else => {
|
||
current_thread.endSyscall();
|
||
return error.PermissionDenied;
|
||
},
|
||
},
|
||
else => |e| {
|
||
current_thread.endSyscall();
|
||
switch (e) {
|
||
.ACCES => return error.AccessDenied,
|
||
.BUSY => return error.FileBusy,
|
||
.FAULT => |err| return errnoBug(err),
|
||
.IO => return error.FileSystem,
|
||
.ISDIR => return error.IsDir,
|
||
.LOOP => return error.SymLinkLoop,
|
||
.NAMETOOLONG => return error.NameTooLong,
|
||
.NOENT => return error.FileNotFound,
|
||
.NOTDIR => return error.NotDir,
|
||
.NOMEM => return error.SystemResources,
|
||
.ROFS => return error.ReadOnlyFileSystem,
|
||
.EXIST => |err| return errnoBug(err),
|
||
.NOTEMPTY => |err| return errnoBug(err), // Not passing AT.REMOVEDIR
|
||
.ILSEQ => return error.BadPathName,
|
||
.INVAL => |err| return errnoBug(err), // invalid flags, or pathname has . as last component
|
||
.BADF => |err| return errnoBug(err), // File descriptor used after closed.
|
||
else => |err| return posix.unexpectedErrno(err),
|
||
}
|
||
},
|
||
}
|
||
}
|
||
}
|
||
|
||
const dirDeleteDir = switch (native_os) {
|
||
.windows => dirDeleteDirWindows,
|
||
.wasi => dirDeleteDirWasi,
|
||
else => dirDeleteDirPosix,
|
||
};
|
||
|
||
fn dirDeleteDirWindows(userdata: ?*anyopaque, dir: Io.Dir, sub_path: []const u8) Io.Dir.DeleteDirError!void {
|
||
return dirDeleteWindows(userdata, dir, sub_path, true);
|
||
}
|
||
|
||
fn dirDeleteWindows(userdata: ?*anyopaque, dir: Io.Dir, sub_path: []const u8, remove_dir: bool) Io.Dir.DeleteFileError!void {
|
||
const t: *Threaded = @ptrCast(@alignCast(userdata));
|
||
const current_thread = Thread.getCurrent(t);
|
||
const w = windows;
|
||
|
||
try current_thread.checkCancel();
|
||
|
||
const sub_path_w = try w.sliceToPrefixedFileW(dir.handle, sub_path);
|
||
|
||
const path_len_bytes = @as(u16, @intCast(sub_path_w.len * 2));
|
||
var nt_name: w.UNICODE_STRING = .{
|
||
.Length = path_len_bytes,
|
||
.MaximumLength = path_len_bytes,
|
||
// The Windows API makes this mutable, but it will not mutate here.
|
||
.Buffer = @constCast(sub_path_w.ptr),
|
||
};
|
||
|
||
if (sub_path_w[0] == '.' and sub_path_w[1] == 0) {
|
||
// Windows does not recognize this, but it does work with empty string.
|
||
nt_name.Length = 0;
|
||
}
|
||
if (sub_path_w[0] == '.' and sub_path_w[1] == '.' and sub_path_w[2] == 0) {
|
||
// Can't remove the parent directory with an open handle.
|
||
return error.FileBusy;
|
||
}
|
||
|
||
const create_options_flags: w.ULONG = if (remove_dir)
|
||
w.FILE_DIRECTORY_FILE | w.FILE_OPEN_REPARSE_POINT
|
||
else
|
||
w.FILE_NON_DIRECTORY_FILE | w.FILE_OPEN_REPARSE_POINT;
|
||
|
||
var attr: w.OBJECT_ATTRIBUTES = .{
|
||
.Length = @sizeOf(w.OBJECT_ATTRIBUTES),
|
||
.RootDirectory = if (std.fs.path.isAbsoluteWindowsWtf16(sub_path_w)) null else dir.handle,
|
||
.Attributes = w.OBJ_CASE_INSENSITIVE,
|
||
.ObjectName = &nt_name,
|
||
.SecurityDescriptor = null,
|
||
.SecurityQualityOfService = null,
|
||
};
|
||
var io_status_block: w.IO_STATUS_BLOCK = undefined;
|
||
var tmp_handle: w.HANDLE = undefined;
|
||
var rc = w.ntdll.NtCreateFile(
|
||
&tmp_handle,
|
||
w.SYNCHRONIZE | w.DELETE,
|
||
&attr,
|
||
&io_status_block,
|
||
null,
|
||
0,
|
||
w.FILE_SHARE_READ | w.FILE_SHARE_WRITE | w.FILE_SHARE_DELETE,
|
||
w.FILE_OPEN,
|
||
create_options_flags,
|
||
null,
|
||
0,
|
||
);
|
||
switch (rc) {
|
||
.SUCCESS => {},
|
||
.OBJECT_NAME_INVALID => |err| return w.statusBug(err),
|
||
.OBJECT_NAME_NOT_FOUND => return error.FileNotFound,
|
||
.OBJECT_PATH_NOT_FOUND => return error.FileNotFound,
|
||
.BAD_NETWORK_PATH => return error.NetworkNotFound, // \\server was not found
|
||
.BAD_NETWORK_NAME => return error.NetworkNotFound, // \\server was found but \\server\share wasn't
|
||
.INVALID_PARAMETER => |err| return w.statusBug(err),
|
||
.FILE_IS_A_DIRECTORY => return error.IsDir,
|
||
.NOT_A_DIRECTORY => return error.NotDir,
|
||
.SHARING_VIOLATION => return error.FileBusy,
|
||
.ACCESS_DENIED => return error.AccessDenied,
|
||
.DELETE_PENDING => return,
|
||
else => return w.unexpectedStatus(rc),
|
||
}
|
||
defer w.CloseHandle(tmp_handle);
|
||
|
||
// FileDispositionInformationEx has varying levels of support:
|
||
// - FILE_DISPOSITION_INFORMATION_EX requires >= win10_rs1
|
||
// (INVALID_INFO_CLASS is returned if not supported)
|
||
// - Requires the NTFS filesystem
|
||
// (on filesystems like FAT32, INVALID_PARAMETER is returned)
|
||
// - FILE_DISPOSITION_POSIX_SEMANTICS requires >= win10_rs1
|
||
// - FILE_DISPOSITION_IGNORE_READONLY_ATTRIBUTE requires >= win10_rs5
|
||
// (NOT_SUPPORTED is returned if a flag is unsupported)
|
||
//
|
||
// The strategy here is just to try using FileDispositionInformationEx and fall back to
|
||
// FileDispositionInformation if the return value lets us know that some aspect of it is not supported.
|
||
const need_fallback = need_fallback: {
|
||
try current_thread.checkCancel();
|
||
|
||
// Deletion with posix semantics if the filesystem supports it.
|
||
var info: w.FILE_DISPOSITION_INFORMATION_EX = .{
|
||
.Flags = w.FILE_DISPOSITION_DELETE |
|
||
w.FILE_DISPOSITION_POSIX_SEMANTICS |
|
||
w.FILE_DISPOSITION_IGNORE_READONLY_ATTRIBUTE,
|
||
};
|
||
|
||
rc = w.ntdll.NtSetInformationFile(
|
||
tmp_handle,
|
||
&io_status_block,
|
||
&info,
|
||
@sizeOf(w.FILE_DISPOSITION_INFORMATION_EX),
|
||
.FileDispositionInformationEx,
|
||
);
|
||
switch (rc) {
|
||
.SUCCESS => return,
|
||
// The filesystem does not support FileDispositionInformationEx
|
||
.INVALID_PARAMETER,
|
||
// The operating system does not support FileDispositionInformationEx
|
||
.INVALID_INFO_CLASS,
|
||
// The operating system does not support one of the flags
|
||
.NOT_SUPPORTED,
|
||
=> break :need_fallback true,
|
||
// For all other statuses, fall down to the switch below to handle them.
|
||
else => break :need_fallback false,
|
||
}
|
||
};
|
||
|
||
if (need_fallback) {
|
||
try current_thread.checkCancel();
|
||
|
||
// Deletion with file pending semantics, which requires waiting or moving
|
||
// files to get them removed (from here).
|
||
var file_dispo: w.FILE_DISPOSITION_INFORMATION = .{
|
||
.DeleteFile = w.TRUE,
|
||
};
|
||
|
||
rc = w.ntdll.NtSetInformationFile(
|
||
tmp_handle,
|
||
&io_status_block,
|
||
&file_dispo,
|
||
@sizeOf(w.FILE_DISPOSITION_INFORMATION),
|
||
.FileDispositionInformation,
|
||
);
|
||
}
|
||
switch (rc) {
|
||
.SUCCESS => {},
|
||
.DIRECTORY_NOT_EMPTY => return error.DirNotEmpty,
|
||
.INVALID_PARAMETER => |err| return w.statusBug(err),
|
||
.CANNOT_DELETE => return error.AccessDenied,
|
||
.MEDIA_WRITE_PROTECTED => return error.AccessDenied,
|
||
.ACCESS_DENIED => return error.AccessDenied,
|
||
else => return w.unexpectedStatus(rc),
|
||
}
|
||
}
|
||
|
||
fn dirDeleteDirWasi(userdata: ?*anyopaque, dir: Io.Dir, sub_path: []const u8) Io.Dir.DeleteDirError!void {
|
||
if (builtin.link_libc) return dirDeleteDirPosix(userdata, dir, sub_path);
|
||
|
||
const t: *Threaded = @ptrCast(@alignCast(userdata));
|
||
const current_thread = Thread.getCurrent(t);
|
||
|
||
try current_thread.beginSyscall();
|
||
while (true) {
|
||
const res = std.os.wasi.path_remove_directory(dir.handle, sub_path.ptr, sub_path.len);
|
||
switch (res) {
|
||
.SUCCESS => {
|
||
current_thread.endSyscall();
|
||
return;
|
||
},
|
||
.INTR => {
|
||
try current_thread.checkCancel();
|
||
continue;
|
||
},
|
||
.CANCELED => return current_thread.endSyscallCanceled(),
|
||
else => |e| {
|
||
current_thread.endSyscall();
|
||
switch (e) {
|
||
.ACCES => return error.AccessDenied,
|
||
.PERM => return error.PermissionDenied,
|
||
.BUSY => return error.FileBusy,
|
||
.FAULT => |err| return errnoBug(err),
|
||
.IO => return error.FileSystem,
|
||
.ISDIR => return error.IsDir,
|
||
.LOOP => return error.SymLinkLoop,
|
||
.NAMETOOLONG => return error.NameTooLong,
|
||
.NOENT => return error.FileNotFound,
|
||
.NOTDIR => return error.NotDir,
|
||
.NOMEM => return error.SystemResources,
|
||
.ROFS => return error.ReadOnlyFileSystem,
|
||
.NOTEMPTY => return error.DirNotEmpty,
|
||
.NOTCAPABLE => return error.AccessDenied,
|
||
.ILSEQ => return error.BadPathName,
|
||
.INVAL => |err| return errnoBug(err), // invalid flags, or pathname has . as last component
|
||
.BADF => |err| return errnoBug(err), // File descriptor used after closed.
|
||
else => |err| return posix.unexpectedErrno(err),
|
||
}
|
||
},
|
||
}
|
||
}
|
||
}
|
||
|
||
fn dirDeleteDirPosix(userdata: ?*anyopaque, dir: Io.Dir, sub_path: []const u8) Io.Dir.DeleteDirError!void {
|
||
const t: *Threaded = @ptrCast(@alignCast(userdata));
|
||
const current_thread = Thread.getCurrent(t);
|
||
|
||
var path_buffer: [posix.PATH_MAX]u8 = undefined;
|
||
const sub_path_posix = try pathToPosix(sub_path, &path_buffer);
|
||
|
||
try current_thread.beginSyscall();
|
||
while (true) {
|
||
switch (posix.errno(posix.system.unlinkat(dir.handle, sub_path_posix, posix.AT.REMOVEDIR))) {
|
||
.SUCCESS => {
|
||
current_thread.endSyscall();
|
||
return;
|
||
},
|
||
.INTR => {
|
||
try current_thread.checkCancel();
|
||
continue;
|
||
},
|
||
.CANCELED => return current_thread.endSyscallCanceled(),
|
||
else => |e| {
|
||
current_thread.endSyscall();
|
||
switch (e) {
|
||
.ACCES => return error.AccessDenied,
|
||
.PERM => return error.PermissionDenied,
|
||
.BUSY => return error.FileBusy,
|
||
.FAULT => |err| return errnoBug(err),
|
||
.IO => return error.FileSystem,
|
||
.ISDIR => |err| return errnoBug(err),
|
||
.LOOP => return error.SymLinkLoop,
|
||
.NAMETOOLONG => return error.NameTooLong,
|
||
.NOENT => return error.FileNotFound,
|
||
.NOTDIR => return error.NotDir,
|
||
.NOMEM => return error.SystemResources,
|
||
.ROFS => return error.ReadOnlyFileSystem,
|
||
.EXIST => |err| return errnoBug(err),
|
||
.NOTEMPTY => |err| return errnoBug(err), // Not passing AT.REMOVEDIR
|
||
.ILSEQ => return error.BadPathName,
|
||
.INVAL => |err| return errnoBug(err), // invalid flags, or pathname has . as last component
|
||
.BADF => |err| return errnoBug(err), // File descriptor used after closed.
|
||
else => |err| return posix.unexpectedErrno(err),
|
||
}
|
||
},
|
||
}
|
||
}
|
||
}
|
||
|
||
const dirRename = switch (native_os) {
|
||
.windows => dirRenameWindows,
|
||
.wasi => dirRenameWasi,
|
||
else => dirRenamePosix,
|
||
};
|
||
|
||
fn dirRenameWindows(
|
||
userdata: ?*anyopaque,
|
||
old_dir: Io.Dir,
|
||
old_sub_path: []const u8,
|
||
new_dir: Io.Dir,
|
||
new_sub_path: []const u8,
|
||
) Io.Dir.RenameError!void {
|
||
const w = windows;
|
||
const t: *Threaded = @ptrCast(@alignCast(userdata));
|
||
const current_thread = Thread.getCurrent(t);
|
||
|
||
const old_path_w = try windows.sliceToPrefixedFileW(old_dir.handle, old_sub_path);
|
||
const new_path_w = try windows.sliceToPrefixedFileW(new_dir.handle, new_sub_path);
|
||
const replace_if_exists = true;
|
||
|
||
try current_thread.checkCancel();
|
||
|
||
const src_fd = w.OpenFile(old_path_w, .{
|
||
.dir = old_dir.handle,
|
||
.access_mask = w.SYNCHRONIZE | w.GENERIC_WRITE | w.DELETE,
|
||
.creation = w.FILE_OPEN,
|
||
.filter = .any, // This function is supposed to rename both files and directories.
|
||
.follow_symlinks = false,
|
||
}) catch |err| switch (err) {
|
||
error.WouldBlock => unreachable, // Not possible without `.share_access_nonblocking = true`.
|
||
else => |e| return e,
|
||
};
|
||
defer w.CloseHandle(src_fd);
|
||
|
||
var rc: w.NTSTATUS = undefined;
|
||
// FileRenameInformationEx has varying levels of support:
|
||
// - FILE_RENAME_INFORMATION_EX requires >= win10_rs1
|
||
// (INVALID_INFO_CLASS is returned if not supported)
|
||
// - Requires the NTFS filesystem
|
||
// (on filesystems like FAT32, INVALID_PARAMETER is returned)
|
||
// - FILE_RENAME_POSIX_SEMANTICS requires >= win10_rs1
|
||
// - FILE_RENAME_IGNORE_READONLY_ATTRIBUTE requires >= win10_rs5
|
||
// (NOT_SUPPORTED is returned if a flag is unsupported)
|
||
//
|
||
// The strategy here is just to try using FileRenameInformationEx and fall back to
|
||
// FileRenameInformation if the return value lets us know that some aspect of it is not supported.
|
||
const need_fallback = need_fallback: {
|
||
const struct_buf_len = @sizeOf(w.FILE_RENAME_INFORMATION_EX) + (w.PATH_MAX_WIDE * 2);
|
||
var rename_info_buf: [struct_buf_len]u8 align(@alignOf(w.FILE_RENAME_INFORMATION_EX)) = undefined;
|
||
const struct_len = @sizeOf(w.FILE_RENAME_INFORMATION_EX) + new_path_w.len * 2;
|
||
if (struct_len > struct_buf_len) return error.NameTooLong;
|
||
|
||
const rename_info: *w.FILE_RENAME_INFORMATION_EX = @ptrCast(&rename_info_buf);
|
||
var io_status_block: w.IO_STATUS_BLOCK = undefined;
|
||
|
||
var flags: w.ULONG = w.FILE_RENAME_POSIX_SEMANTICS | w.FILE_RENAME_IGNORE_READONLY_ATTRIBUTE;
|
||
if (replace_if_exists) flags |= w.FILE_RENAME_REPLACE_IF_EXISTS;
|
||
rename_info.* = .{
|
||
.Flags = flags,
|
||
.RootDirectory = if (std.fs.path.isAbsoluteWindowsWtf16(new_path_w)) null else new_dir.handle,
|
||
.FileNameLength = @intCast(new_path_w.len * 2), // already checked error.NameTooLong
|
||
.FileName = undefined,
|
||
};
|
||
@memcpy((&rename_info.FileName).ptr, new_path_w);
|
||
rc = w.ntdll.NtSetInformationFile(
|
||
src_fd,
|
||
&io_status_block,
|
||
rename_info,
|
||
@intCast(struct_len), // already checked for error.NameTooLong
|
||
.FileRenameInformationEx,
|
||
);
|
||
switch (rc) {
|
||
.SUCCESS => return,
|
||
// The filesystem does not support FileDispositionInformationEx
|
||
.INVALID_PARAMETER,
|
||
// The operating system does not support FileDispositionInformationEx
|
||
.INVALID_INFO_CLASS,
|
||
// The operating system does not support one of the flags
|
||
.NOT_SUPPORTED,
|
||
=> break :need_fallback true,
|
||
// For all other statuses, fall down to the switch below to handle them.
|
||
else => break :need_fallback false,
|
||
}
|
||
};
|
||
|
||
if (need_fallback) {
|
||
const struct_buf_len = @sizeOf(w.FILE_RENAME_INFORMATION) + (w.PATH_MAX_WIDE * 2);
|
||
var rename_info_buf: [struct_buf_len]u8 align(@alignOf(w.FILE_RENAME_INFORMATION)) = undefined;
|
||
const struct_len = @sizeOf(w.FILE_RENAME_INFORMATION) + new_path_w.len * 2;
|
||
if (struct_len > struct_buf_len) return error.NameTooLong;
|
||
|
||
const rename_info: *w.FILE_RENAME_INFORMATION = @ptrCast(&rename_info_buf);
|
||
var io_status_block: w.IO_STATUS_BLOCK = undefined;
|
||
|
||
rename_info.* = .{
|
||
.Flags = @intFromBool(replace_if_exists),
|
||
.RootDirectory = if (std.fs.path.isAbsoluteWindowsWtf16(new_path_w)) null else new_dir.handle,
|
||
.FileNameLength = @intCast(new_path_w.len * 2), // already checked error.NameTooLong
|
||
.FileName = undefined,
|
||
};
|
||
@memcpy((&rename_info.FileName).ptr, new_path_w);
|
||
|
||
rc = w.ntdll.NtSetInformationFile(
|
||
src_fd,
|
||
&io_status_block,
|
||
rename_info,
|
||
@intCast(struct_len), // already checked for error.NameTooLong
|
||
.FileRenameInformation,
|
||
);
|
||
}
|
||
|
||
switch (rc) {
|
||
.SUCCESS => {},
|
||
.INVALID_HANDLE => |err| return w.statusBug(err),
|
||
.INVALID_PARAMETER => |err| return w.statusBug(err),
|
||
.OBJECT_PATH_SYNTAX_BAD => |err| return w.statusBug(err),
|
||
.ACCESS_DENIED => return error.AccessDenied,
|
||
.OBJECT_NAME_NOT_FOUND => return error.FileNotFound,
|
||
.OBJECT_PATH_NOT_FOUND => return error.FileNotFound,
|
||
.NOT_SAME_DEVICE => return error.RenameAcrossMountPoints,
|
||
.OBJECT_NAME_COLLISION => return error.PathAlreadyExists,
|
||
.DIRECTORY_NOT_EMPTY => return error.PathAlreadyExists,
|
||
.FILE_IS_A_DIRECTORY => return error.IsDir,
|
||
.NOT_A_DIRECTORY => return error.NotDir,
|
||
else => return w.unexpectedStatus(rc),
|
||
}
|
||
}
|
||
|
||
fn dirRenameWasi(
|
||
userdata: ?*anyopaque,
|
||
old_dir: Io.Dir,
|
||
old_sub_path: []const u8,
|
||
new_dir: Io.Dir,
|
||
new_sub_path: []const u8,
|
||
) Io.Dir.RenameError!void {
|
||
if (builtin.link_libc) return dirRenamePosix(userdata, old_dir, old_sub_path, new_dir, new_sub_path);
|
||
|
||
const t: *Threaded = @ptrCast(@alignCast(userdata));
|
||
const current_thread = Thread.getCurrent(t);
|
||
|
||
try current_thread.beginSyscall();
|
||
while (true) {
|
||
switch (std.os.wasi.path_rename(old_dir.handle, old_sub_path.ptr, old_sub_path.len, new_dir.handle, new_sub_path.ptr, new_sub_path.len)) {
|
||
.SUCCESS => return current_thread.endSyscall(),
|
||
.CANCELED => return current_thread.endSyscallCanceled(),
|
||
.INTR => {
|
||
try current_thread.checkCancel();
|
||
continue;
|
||
},
|
||
else => |e| {
|
||
current_thread.endSyscall();
|
||
switch (e) {
|
||
.ACCES => return error.AccessDenied,
|
||
.PERM => return error.PermissionDenied,
|
||
.BUSY => return error.FileBusy,
|
||
.DQUOT => return error.DiskQuota,
|
||
.FAULT => |err| return errnoBug(err),
|
||
.INVAL => |err| return errnoBug(err),
|
||
.ISDIR => return error.IsDir,
|
||
.LOOP => return error.SymLinkLoop,
|
||
.MLINK => return error.LinkQuotaExceeded,
|
||
.NAMETOOLONG => return error.NameTooLong,
|
||
.NOENT => return error.FileNotFound,
|
||
.NOTDIR => return error.NotDir,
|
||
.NOMEM => return error.SystemResources,
|
||
.NOSPC => return error.NoSpaceLeft,
|
||
.EXIST => return error.PathAlreadyExists,
|
||
.NOTEMPTY => return error.PathAlreadyExists,
|
||
.ROFS => return error.ReadOnlyFileSystem,
|
||
.XDEV => return error.RenameAcrossMountPoints,
|
||
.NOTCAPABLE => return error.AccessDenied,
|
||
.ILSEQ => return error.BadPathName,
|
||
else => |err| return posix.unexpectedErrno(err),
|
||
}
|
||
},
|
||
}
|
||
}
|
||
}
|
||
|
||
fn dirRenamePosix(
|
||
userdata: ?*anyopaque,
|
||
old_dir: Io.Dir,
|
||
old_sub_path: []const u8,
|
||
new_dir: Io.Dir,
|
||
new_sub_path: []const u8,
|
||
) Io.Dir.RenameError!void {
|
||
const t: *Threaded = @ptrCast(@alignCast(userdata));
|
||
const current_thread = Thread.getCurrent(t);
|
||
|
||
var old_path_buffer: [posix.PATH_MAX]u8 = undefined;
|
||
var new_path_buffer: [posix.PATH_MAX]u8 = undefined;
|
||
|
||
const old_sub_path_posix = try pathToPosix(old_sub_path, &old_path_buffer);
|
||
const new_sub_path_posix = try pathToPosix(new_sub_path, &new_path_buffer);
|
||
|
||
try current_thread.beginSyscall();
|
||
while (true) {
|
||
switch (posix.errno(posix.system.renameat(old_dir.handle, old_sub_path_posix, new_dir.handle, new_sub_path_posix))) {
|
||
.SUCCESS => return current_thread.endSyscall(),
|
||
.CANCELED => return current_thread.endSyscallCanceled(),
|
||
.INTR => {
|
||
try current_thread.checkCancel();
|
||
continue;
|
||
},
|
||
else => |e| {
|
||
current_thread.endSyscall();
|
||
switch (e) {
|
||
.ACCES => return error.AccessDenied,
|
||
.PERM => return error.PermissionDenied,
|
||
.BUSY => return error.FileBusy,
|
||
.DQUOT => return error.DiskQuota,
|
||
.FAULT => |err| return errnoBug(err),
|
||
.INVAL => |err| return errnoBug(err),
|
||
.ISDIR => return error.IsDir,
|
||
.LOOP => return error.SymLinkLoop,
|
||
.MLINK => return error.LinkQuotaExceeded,
|
||
.NAMETOOLONG => return error.NameTooLong,
|
||
.NOENT => return error.FileNotFound,
|
||
.NOTDIR => return error.NotDir,
|
||
.NOMEM => return error.SystemResources,
|
||
.NOSPC => return error.NoSpaceLeft,
|
||
.EXIST => return error.PathAlreadyExists,
|
||
.NOTEMPTY => return error.PathAlreadyExists,
|
||
.ROFS => return error.ReadOnlyFileSystem,
|
||
.XDEV => return error.RenameAcrossMountPoints,
|
||
.ILSEQ => return error.BadPathName,
|
||
else => |err| return posix.unexpectedErrno(err),
|
||
}
|
||
},
|
||
}
|
||
}
|
||
}
|
||
|
||
const dirSymLink = switch (native_os) {
|
||
.windows => dirSymLinkWindows,
|
||
.wasi => dirSymLinkWasi,
|
||
else => dirSymLinkPosix,
|
||
};
|
||
|
||
fn dirSymLinkWindows(
|
||
userdata: ?*anyopaque,
|
||
dir: Io.Dir,
|
||
target_path: []const u8,
|
||
sym_link_path: []const u8,
|
||
flags: Io.Dir.SymLinkFlags,
|
||
) Io.Dir.SymLinkError!void {
|
||
const t: *Threaded = @ptrCast(@alignCast(userdata));
|
||
const current_thread = Thread.getCurrent(t);
|
||
const w = windows;
|
||
|
||
try current_thread.checkCancel();
|
||
|
||
// Target path does not use sliceToPrefixedFileW because certain paths
|
||
// are handled differently when creating a symlink than they would be
|
||
// when converting to an NT namespaced path. CreateSymbolicLink in
|
||
// symLinkW will handle the necessary conversion.
|
||
var target_path_w: w.PathSpace = undefined;
|
||
target_path_w.len = try w.wtf8ToWtf16Le(&target_path_w.data, target_path);
|
||
target_path_w.data[target_path_w.len] = 0;
|
||
// However, we need to canonicalize any path separators to `\`, since if
|
||
// the target path is relative, then it must use `\` as the path separator.
|
||
std.mem.replaceScalar(
|
||
u16,
|
||
target_path_w.data[0..target_path_w.len],
|
||
std.mem.nativeToLittle(u16, '/'),
|
||
std.mem.nativeToLittle(u16, '\\'),
|
||
);
|
||
|
||
const sym_link_path_w = try w.sliceToPrefixedFileW(dir.handle, sym_link_path);
|
||
|
||
const SYMLINK_DATA = extern struct {
|
||
ReparseTag: w.ULONG,
|
||
ReparseDataLength: w.USHORT,
|
||
Reserved: w.USHORT,
|
||
SubstituteNameOffset: w.USHORT,
|
||
SubstituteNameLength: w.USHORT,
|
||
PrintNameOffset: w.USHORT,
|
||
PrintNameLength: w.USHORT,
|
||
Flags: w.ULONG,
|
||
};
|
||
|
||
const symlink_handle = w.OpenFile(sym_link_path_w.span(), .{
|
||
.access_mask = w.SYNCHRONIZE | w.GENERIC_READ | w.GENERIC_WRITE,
|
||
.dir = dir,
|
||
.creation = w.FILE_CREATE,
|
||
.filter = if (flags.is_directory) .dir_only else .file_only,
|
||
}) catch |err| switch (err) {
|
||
error.IsDir => return error.PathAlreadyExists,
|
||
error.NotDir => return error.Unexpected,
|
||
error.WouldBlock => return error.Unexpected,
|
||
error.PipeBusy => return error.Unexpected,
|
||
error.NoDevice => return error.Unexpected,
|
||
error.AntivirusInterference => return error.Unexpected,
|
||
else => |e| return e,
|
||
};
|
||
defer w.CloseHandle(symlink_handle);
|
||
|
||
// Relevant portions of the documentation:
|
||
// > Relative links are specified using the following conventions:
|
||
// > - Root relative—for example, "\Windows\System32" resolves to "current drive:\Windows\System32".
|
||
// > - Current working directory–relative—for example, if the current working directory is
|
||
// > C:\Windows\System32, "C:File.txt" resolves to "C:\Windows\System32\File.txt".
|
||
// > Note: If you specify a current working directory–relative link, it is created as an absolute
|
||
// > link, due to the way the current working directory is processed based on the user and the thread.
|
||
// https://learn.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-createsymboliclinkw
|
||
var is_target_absolute = false;
|
||
const final_target_path = target_path: {
|
||
if (w.hasCommonNtPrefix(u16, target_path)) {
|
||
// Already an NT path, no need to do anything to it
|
||
break :target_path target_path;
|
||
} else {
|
||
switch (w.getWin32PathType(u16, target_path)) {
|
||
// Rooted paths need to avoid getting put through wToPrefixedFileW
|
||
// (and they are treated as relative in this context)
|
||
// Note: It seems that rooted paths in symbolic links are relative to
|
||
// the drive that the symbolic exists on, not to the CWD's drive.
|
||
// So, if the symlink is on C:\ and the CWD is on D:\,
|
||
// it will still resolve the path relative to the root of
|
||
// the C:\ drive.
|
||
.rooted => break :target_path target_path,
|
||
// Keep relative paths relative, but anything else needs to get NT-prefixed.
|
||
else => if (!std.fs.path.isAbsoluteWindowsWtf16(target_path))
|
||
break :target_path target_path,
|
||
}
|
||
}
|
||
var prefixed_target_path = try w.wToPrefixedFileW(dir, target_path);
|
||
// We do this after prefixing to ensure that drive-relative paths are treated as absolute
|
||
is_target_absolute = std.fs.path.isAbsoluteWindowsWtf16(prefixed_target_path.span());
|
||
break :target_path prefixed_target_path.span();
|
||
};
|
||
|
||
// prepare reparse data buffer
|
||
var buffer: [w.MAXIMUM_REPARSE_DATA_BUFFER_SIZE]u8 = undefined;
|
||
const buf_len = @sizeOf(SYMLINK_DATA) + final_target_path.len * 4;
|
||
const header_len = @sizeOf(w.ULONG) + @sizeOf(w.USHORT) * 2;
|
||
const target_is_absolute = std.fs.path.isAbsoluteWindowsWtf16(final_target_path);
|
||
const symlink_data = SYMLINK_DATA{
|
||
.ReparseTag = w.IO_REPARSE_TAG_SYMLINK,
|
||
.ReparseDataLength = @intCast(buf_len - header_len),
|
||
.Reserved = 0,
|
||
.SubstituteNameOffset = @intCast(final_target_path.len * 2),
|
||
.SubstituteNameLength = @intCast(final_target_path.len * 2),
|
||
.PrintNameOffset = 0,
|
||
.PrintNameLength = @intCast(final_target_path.len * 2),
|
||
.Flags = if (!target_is_absolute) w.SYMLINK_FLAG_RELATIVE else 0,
|
||
};
|
||
|
||
@memcpy(buffer[0..@sizeOf(SYMLINK_DATA)], std.mem.asBytes(&symlink_data));
|
||
@memcpy(buffer[@sizeOf(SYMLINK_DATA)..][0 .. final_target_path.len * 2], @as([*]const u8, @ptrCast(final_target_path)));
|
||
const paths_start = @sizeOf(SYMLINK_DATA) + final_target_path.len * 2;
|
||
@memcpy(buffer[paths_start..][0 .. final_target_path.len * 2], @as([*]const u8, @ptrCast(final_target_path)));
|
||
_ = try w.DeviceIoControl(symlink_handle, w.FSCTL_SET_REPARSE_POINT, buffer[0..buf_len], null);
|
||
}
|
||
|
||
fn dirSymLinkWasi(
|
||
userdata: ?*anyopaque,
|
||
dir: Io.Dir,
|
||
target_path: []const u8,
|
||
sym_link_path: []const u8,
|
||
flags: Io.Dir.SymLinkFlags,
|
||
) Io.Dir.SymLinkError!void {
|
||
if (builtin.link_libc) return dirSymLinkPosix(dir, target_path, sym_link_path, flags);
|
||
|
||
const t: *Threaded = @ptrCast(@alignCast(userdata));
|
||
const current_thread = Thread.getCurrent(t);
|
||
|
||
try current_thread.beginSyscall();
|
||
while (true) {
|
||
switch (std.os.wasi.path_symlink(target_path.ptr, target_path.len, dir.handle, sym_link_path.ptr, sym_link_path.len)) {
|
||
.SUCCESS => return current_thread.endSyscall(),
|
||
.CANCELED => return current_thread.endSyscallCanceled(),
|
||
.INTR => {
|
||
try current_thread.checkCancel();
|
||
continue;
|
||
},
|
||
else => |e| {
|
||
current_thread.endSyscall();
|
||
switch (e) {
|
||
.FAULT => |err| return errnoBug(err),
|
||
.INVAL => |err| return errnoBug(err),
|
||
.BADF => |err| return errnoBug(err),
|
||
.ACCES => return error.AccessDenied,
|
||
.PERM => return error.PermissionDenied,
|
||
.DQUOT => return error.DiskQuota,
|
||
.EXIST => return error.PathAlreadyExists,
|
||
.IO => return error.FileSystem,
|
||
.LOOP => return error.SymLinkLoop,
|
||
.NAMETOOLONG => return error.NameTooLong,
|
||
.NOENT => return error.FileNotFound,
|
||
.NOTDIR => return error.NotDir,
|
||
.NOMEM => return error.SystemResources,
|
||
.NOSPC => return error.NoSpaceLeft,
|
||
.ROFS => return error.ReadOnlyFileSystem,
|
||
.NOTCAPABLE => return error.AccessDenied,
|
||
.ILSEQ => return error.BadPathName,
|
||
else => |err| return posix.unexpectedErrno(err),
|
||
}
|
||
},
|
||
}
|
||
}
|
||
}
|
||
|
||
fn dirSymLinkPosix(
|
||
userdata: ?*anyopaque,
|
||
dir: Io.Dir,
|
||
target_path: []const u8,
|
||
sym_link_path: []const u8,
|
||
flags: Io.Dir.SymLinkFlags,
|
||
) Io.Dir.SymLinkError!void {
|
||
_ = flags;
|
||
const t: *Threaded = @ptrCast(@alignCast(userdata));
|
||
const current_thread = Thread.getCurrent(t);
|
||
|
||
var target_path_buffer: [posix.PATH_MAX]u8 = undefined;
|
||
var sym_link_path_buffer: [posix.PATH_MAX]u8 = undefined;
|
||
|
||
const target_path_posix = try pathToPosix(target_path, &target_path_buffer);
|
||
const sym_link_path_posix = try pathToPosix(sym_link_path, &sym_link_path_buffer);
|
||
|
||
try current_thread.beginSyscall();
|
||
while (true) {
|
||
switch (posix.errno(posix.system.symlinkat(target_path_posix, dir.handle, sym_link_path_posix))) {
|
||
.SUCCESS => return current_thread.endSyscall(),
|
||
.CANCELED => return current_thread.endSyscallCanceled(),
|
||
.INTR => {
|
||
try current_thread.checkCancel();
|
||
continue;
|
||
},
|
||
else => |e| {
|
||
current_thread.endSyscall();
|
||
switch (e) {
|
||
.FAULT => |err| return errnoBug(err),
|
||
.INVAL => |err| return errnoBug(err),
|
||
.ACCES => return error.AccessDenied,
|
||
.PERM => return error.PermissionDenied,
|
||
.DQUOT => return error.DiskQuota,
|
||
.EXIST => return error.PathAlreadyExists,
|
||
.IO => return error.FileSystem,
|
||
.LOOP => return error.SymLinkLoop,
|
||
.NAMETOOLONG => return error.NameTooLong,
|
||
.NOENT => return error.FileNotFound,
|
||
.NOTDIR => return error.NotDir,
|
||
.NOMEM => return error.SystemResources,
|
||
.NOSPC => return error.NoSpaceLeft,
|
||
.ROFS => return error.ReadOnlyFileSystem,
|
||
.ILSEQ => return error.BadPathName,
|
||
else => |err| return posix.unexpectedErrno(err),
|
||
}
|
||
},
|
||
}
|
||
}
|
||
}
|
||
|
||
const dirReadLink = switch (native_os) {
|
||
.windows => dirReadLinkWindows,
|
||
.wasi => dirReadLinkWasi,
|
||
else => dirReadLinkPosix,
|
||
};
|
||
|
||
fn dirReadLinkWindows(userdata: ?*anyopaque, dir: Io.Dir, sub_path: []const u8, buffer: []u8) Io.Dir.ReadLinkError!usize {
|
||
const t: *Threaded = @ptrCast(@alignCast(userdata));
|
||
const current_thread = Thread.getCurrent(t);
|
||
const w = windows;
|
||
|
||
try current_thread.checkCancel();
|
||
|
||
var sub_path_w = try windows.sliceToPrefixedFileW(dir.handle, sub_path);
|
||
|
||
const result_handle = w.OpenFile(sub_path_w.span(), .{
|
||
.access_mask = w.FILE_READ_ATTRIBUTES | w.SYNCHRONIZE,
|
||
.dir = dir,
|
||
.creation = w.FILE_OPEN,
|
||
.follow_symlinks = false,
|
||
.filter = .any,
|
||
}) catch |err| switch (err) {
|
||
error.IsDir, error.NotDir => return error.Unexpected, // filter = .any
|
||
error.PathAlreadyExists => return error.Unexpected, // FILE_OPEN
|
||
error.WouldBlock => return error.Unexpected,
|
||
error.NoDevice => return error.FileNotFound,
|
||
error.PipeBusy => return error.AccessDenied,
|
||
else => |e| return e,
|
||
};
|
||
defer w.CloseHandle(result_handle);
|
||
|
||
var reparse_buf: [w.MAXIMUM_REPARSE_DATA_BUFFER_SIZE]u8 align(@alignOf(w.REPARSE_DATA_BUFFER)) = undefined;
|
||
_ = w.DeviceIoControl(result_handle, w.FSCTL_GET_REPARSE_POINT, null, reparse_buf[0..]) catch |err| switch (err) {
|
||
error.AccessDenied => return error.Unexpected,
|
||
error.UnrecognizedVolume => return error.Unexpected,
|
||
else => |e| return e,
|
||
};
|
||
|
||
const reparse_struct: *const w.REPARSE_DATA_BUFFER = @ptrCast(@alignCast(&reparse_buf[0]));
|
||
const wide_result = switch (reparse_struct.ReparseTag) {
|
||
w.IO_REPARSE_TAG_SYMLINK => r: {
|
||
const buf: *const w.SYMBOLIC_LINK_REPARSE_BUFFER = @ptrCast(@alignCast(&reparse_struct.DataBuffer[0]));
|
||
const offset = buf.SubstituteNameOffset >> 1;
|
||
const len = buf.SubstituteNameLength >> 1;
|
||
const path_buf: [*]const u16 = &buf.PathBuffer;
|
||
const is_relative = buf.Flags & w.SYMLINK_FLAG_RELATIVE != 0;
|
||
break :r try w.parseReadLinkPath(path_buf[offset..][0..len], is_relative, buffer);
|
||
},
|
||
w.IO_REPARSE_TAG_MOUNT_POINT => r: {
|
||
const buf: *const w.MOUNT_POINT_REPARSE_BUFFER = @ptrCast(@alignCast(&reparse_struct.DataBuffer[0]));
|
||
const offset = buf.SubstituteNameOffset >> 1;
|
||
const len = buf.SubstituteNameLength >> 1;
|
||
const path_buf: [*]const u16 = &buf.PathBuffer;
|
||
break :r try w.parseReadLinkPath(path_buf[offset..][0..len], false, buffer);
|
||
},
|
||
else => return error.UnsupportedReparsePointType,
|
||
};
|
||
|
||
const len = std.unicode.calcWtf8Len(wide_result);
|
||
if (len > buffer.len) return error.NameTooLong;
|
||
|
||
return std.unicode.wtf16LeToWtf8(buffer, wide_result);
|
||
}
|
||
|
||
fn dirReadLinkWasi(userdata: ?*anyopaque, dir: Io.Dir, sub_path: []const u8, buffer: []u8) Io.Dir.ReadLinkError!usize {
|
||
if (builtin.link_libc) return dirReadLinkPosix(userdata, dir, sub_path, buffer);
|
||
|
||
const t: *Threaded = @ptrCast(@alignCast(userdata));
|
||
const current_thread = Thread.getCurrent(t);
|
||
|
||
var n: usize = undefined;
|
||
try current_thread.beginSyscall();
|
||
while (true) {
|
||
switch (std.os.wasi.path_readlink(dir.handle, sub_path.ptr, sub_path.len, buffer.ptr, buffer.len, &n)) {
|
||
.SUCCESS => {
|
||
current_thread.endSyscall();
|
||
return buffer[0..n];
|
||
},
|
||
.CANCELED => return current_thread.endSyscallCanceled(),
|
||
.INTR => {
|
||
try current_thread.checkCancel();
|
||
continue;
|
||
},
|
||
else => |e| {
|
||
current_thread.endSyscall();
|
||
switch (e) {
|
||
.ACCES => return error.AccessDenied,
|
||
.FAULT => |err| return errnoBug(err),
|
||
.INVAL => return error.NotLink,
|
||
.IO => return error.FileSystem,
|
||
.LOOP => return error.SymLinkLoop,
|
||
.NAMETOOLONG => return error.NameTooLong,
|
||
.NOENT => return error.FileNotFound,
|
||
.NOMEM => return error.SystemResources,
|
||
.NOTDIR => return error.NotDir,
|
||
.NOTCAPABLE => return error.AccessDenied,
|
||
.ILSEQ => return error.BadPathName,
|
||
else => |err| return posix.unexpectedErrno(err),
|
||
}
|
||
},
|
||
}
|
||
}
|
||
}
|
||
|
||
fn dirReadLinkPosix(userdata: ?*anyopaque, dir: Io.Dir, sub_path: []const u8, buffer: []u8) Io.Dir.ReadLinkError!usize {
|
||
const t: *Threaded = @ptrCast(@alignCast(userdata));
|
||
const current_thread = Thread.getCurrent(t);
|
||
|
||
var sub_path_buffer: [posix.PATH_MAX]u8 = undefined;
|
||
const sub_path_posix = try pathToPosix(sub_path, &sub_path_buffer);
|
||
|
||
try current_thread.beginSyscall();
|
||
while (true) {
|
||
const rc = posix.system.readlinkat(dir.handle, sub_path_posix, buffer.ptr, buffer.len);
|
||
switch (posix.errno(rc)) {
|
||
.SUCCESS => {
|
||
current_thread.endSyscall();
|
||
const len: usize = @bitCast(rc);
|
||
return len;
|
||
},
|
||
.CANCELED => return current_thread.endSyscallCanceled(),
|
||
.INTR => {
|
||
try current_thread.checkCancel();
|
||
continue;
|
||
},
|
||
else => |e| {
|
||
current_thread.endSyscall();
|
||
switch (e) {
|
||
.ACCES => return error.AccessDenied,
|
||
.FAULT => |err| return errnoBug(err),
|
||
.INVAL => return error.NotLink,
|
||
.IO => return error.FileSystem,
|
||
.LOOP => return error.SymLinkLoop,
|
||
.NAMETOOLONG => return error.NameTooLong,
|
||
.NOENT => return error.FileNotFound,
|
||
.NOMEM => return error.SystemResources,
|
||
.NOTDIR => return error.NotDir,
|
||
.ILSEQ => return error.BadPathName,
|
||
else => |err| return posix.unexpectedErrno(err),
|
||
}
|
||
},
|
||
}
|
||
}
|
||
}
|
||
|
||
const dirSetPermissions = switch (native_os) {
|
||
.windows => dirSetPermissionsWindows,
|
||
else => dirSetPermissionsPosix,
|
||
};
|
||
|
||
fn dirSetPermissionsWindows(userdata: ?*anyopaque, dir: Io.Dir, permissions: Io.Dir.Permissions) Io.Dir.SetPermissionsError!void {
|
||
// TODO I think we can actually set permissions on a dir on windows?
|
||
_ = userdata;
|
||
_ = dir;
|
||
_ = permissions;
|
||
return error.Unexpected;
|
||
}
|
||
|
||
fn dirSetPermissionsPosix(userdata: ?*anyopaque, dir: Io.Dir, permissions: Io.Dir.Permissions) Io.Dir.SetPermissionsError!void {
|
||
const t: *Threaded = @ptrCast(@alignCast(userdata));
|
||
const current_thread = Thread.getCurrent(t);
|
||
return setPermissionsPosix(current_thread, dir.handle, permissions.toMode());
|
||
}
|
||
|
||
const dirSetOwner = switch (native_os) {
|
||
.windows => dirSetOwnerUnsupported,
|
||
else => dirSetOwnerPosix,
|
||
};
|
||
|
||
fn dirSetOwnerUnsupported(userdata: ?*anyopaque, dir: Io.Dir, owner: ?Io.File.Uid, group: ?Io.File.Gid) Io.Dir.SetOwnerError!void {
|
||
_ = userdata;
|
||
_ = dir;
|
||
_ = owner;
|
||
_ = group;
|
||
return error.Unexpected;
|
||
}
|
||
|
||
fn dirSetOwnerPosix(userdata: ?*anyopaque, dir: Io.Dir, owner: ?Io.File.Uid, group: ?Io.File.Gid) Io.Dir.SetOwnerError!void {
|
||
const t: *Threaded = @ptrCast(@alignCast(userdata));
|
||
const current_thread = Thread.getCurrent(t);
|
||
const uid = owner orelse ~@as(posix.uid_t, 0);
|
||
const gid = group orelse ~@as(posix.gid_t, 0);
|
||
return setOwnerPosix(current_thread, dir.handle, uid, gid);
|
||
}
|
||
|
||
fn setOwnerPosix(current_thread: *Thread, fd: posix.fd_t, uid: posix.uid_t, gid: posix.gid_t) Io.File.SetOwnerError!void {
|
||
try current_thread.beginSyscall();
|
||
while (true) {
|
||
switch (posix.errno(posix.system.fchown(fd, uid, gid))) {
|
||
.SUCCESS => return current_thread.endSyscall(),
|
||
.CANCELED => return current_thread.endSyscallCanceled(),
|
||
.INTR => {
|
||
try current_thread.checkCancel();
|
||
continue;
|
||
},
|
||
else => |e| {
|
||
current_thread.endSyscall();
|
||
switch (e) {
|
||
.BADF => |err| return errnoBug(err), // likely fd refers to directory opened without `Io.Dir.OpenOptions.iterate`
|
||
.FAULT => |err| return errnoBug(err),
|
||
.INVAL => |err| return errnoBug(err),
|
||
.ACCES => return error.AccessDenied,
|
||
.IO => return error.InputOutput,
|
||
.LOOP => return error.SymLinkLoop,
|
||
.NOENT => return error.FileNotFound,
|
||
.NOMEM => return error.SystemResources,
|
||
.NOTDIR => return error.FileNotFound,
|
||
.PERM => return error.PermissionDenied,
|
||
.ROFS => return error.ReadOnlyFileSystem,
|
||
else => |err| return posix.unexpectedErrno(err),
|
||
}
|
||
},
|
||
}
|
||
}
|
||
}
|
||
|
||
const fileSync = switch (native_os) {
|
||
.windows => fileSyncWindows,
|
||
else => fileSyncPosix,
|
||
};
|
||
|
||
fn fileSyncWindows(userdata: ?*anyopaque, file: Io.File) Io.File.SyncError!void {
|
||
const t: *Threaded = @ptrCast(@alignCast(userdata));
|
||
const current_thread = Thread.getCurrent(t);
|
||
|
||
try current_thread.checkCancel();
|
||
|
||
if (windows.kernel32.FlushFileBuffers(file.handle) != 0)
|
||
return;
|
||
|
||
switch (windows.GetLastError()) {
|
||
.SUCCESS => return,
|
||
.INVALID_HANDLE => unreachable,
|
||
.ACCESS_DENIED => return error.AccessDenied, // a sync was performed but the system couldn't update the access time
|
||
.UNEXP_NET_ERR => return error.InputOutput,
|
||
else => |err| return windows.unexpectedError(err),
|
||
}
|
||
}
|
||
|
||
fn fileSyncPosix(userdata: ?*anyopaque, file: Io.File) Io.File.SyncError!void {
|
||
const t: *Threaded = @ptrCast(@alignCast(userdata));
|
||
const current_thread = Thread.getCurrent(t);
|
||
try current_thread.beginSyscall();
|
||
while (true) {
|
||
switch (posix.system.fsync(file.handle)) {
|
||
.SUCCESS => return current_thread.endSyscall(),
|
||
.CANCELED => return current_thread.endSyscallCanceled(),
|
||
.INTR => {
|
||
try current_thread.checkCancel();
|
||
continue;
|
||
},
|
||
else => |e| {
|
||
current_thread.endSyscall();
|
||
switch (e) {
|
||
.BADF => |err| return errnoBug(err),
|
||
.INVAL => |err| return errnoBug(err),
|
||
.ROFS => |err| return errnoBug(err),
|
||
.IO => return error.InputOutput,
|
||
.NOSPC => return error.NoSpaceLeft,
|
||
.DQUOT => return error.DiskQuota,
|
||
else => |err| return posix.unexpectedErrno(err),
|
||
}
|
||
},
|
||
}
|
||
}
|
||
}
|
||
|
||
fn fileIsTty(userdata: ?*anyopaque, file: Io.File) Io.Cancelable!bool {
|
||
const t: *Threaded = @ptrCast(@alignCast(userdata));
|
||
const current_thread = Thread.getCurrent(t);
|
||
return isTty(current_thread, file);
|
||
}
|
||
|
||
fn isTty(current_thread: *Thread, file: Io.File) Io.Cancelable!bool {
|
||
if (is_windows) {
|
||
if (try isCygwinPty(current_thread, file)) return true;
|
||
try current_thread.checkCancel();
|
||
var out: windows.DWORD = undefined;
|
||
return windows.kernel32.GetConsoleMode(file.handle, &out) != 0;
|
||
}
|
||
|
||
if (builtin.link_libc) {
|
||
try current_thread.beginSyscall();
|
||
while (true) {
|
||
const rc = posix.system.isatty(file.handle);
|
||
switch (posix.errno(rc - 1)) {
|
||
.SUCCESS => {
|
||
current_thread.endSyscall();
|
||
return true;
|
||
},
|
||
.CANCELED => return current_thread.endSyscallCanceled(),
|
||
.INTR => {
|
||
try current_thread.checkCancel();
|
||
continue;
|
||
},
|
||
else => {
|
||
current_thread.endSyscall();
|
||
return false;
|
||
},
|
||
}
|
||
}
|
||
}
|
||
|
||
if (native_os == .wasi) {
|
||
var statbuf: std.os.wasi.fdstat_t = undefined;
|
||
const err = std.os.wasi.fd_fdstat_get(file.handle, &statbuf);
|
||
if (err != .SUCCESS)
|
||
return false;
|
||
|
||
// A tty is a character device that we can't seek or tell on.
|
||
if (statbuf.fs_filetype != .CHARACTER_DEVICE)
|
||
return false;
|
||
if (statbuf.fs_rights_base.FD_SEEK or statbuf.fs_rights_base.FD_TELL)
|
||
return false;
|
||
|
||
return true;
|
||
}
|
||
|
||
if (native_os == .linux) {
|
||
const linux = std.os.linux;
|
||
try current_thread.beginSyscall();
|
||
while (true) {
|
||
var wsz: linux.winsize = undefined;
|
||
const fd: usize = @bitCast(@as(isize, file.handle));
|
||
const rc = linux.syscall3(.ioctl, fd, linux.T.IOCGWINSZ, @intFromPtr(&wsz));
|
||
switch (linux.errno(rc)) {
|
||
.SUCCESS => {
|
||
current_thread.endSyscall();
|
||
return true;
|
||
},
|
||
.CANCELED => return current_thread.endSyscallCanceled(),
|
||
.INTR => {
|
||
try current_thread.checkCancel();
|
||
continue;
|
||
},
|
||
else => {
|
||
current_thread.endSyscall();
|
||
return false;
|
||
},
|
||
}
|
||
}
|
||
}
|
||
|
||
@compileError("unimplemented");
|
||
}
|
||
|
||
fn fileEnableAnsiEscapeCodes(userdata: ?*anyopaque, file: Io.File) Io.File.EnableAnsiEscapeCodesError!void {
|
||
const t: *Threaded = @ptrCast(@alignCast(userdata));
|
||
const current_thread = Thread.getCurrent(t);
|
||
|
||
if (is_windows) {
|
||
try current_thread.checkCancel();
|
||
|
||
// For Windows Terminal, VT Sequences processing is enabled by default.
|
||
var original_console_mode: windows.DWORD = 0;
|
||
if (windows.kernel32.GetConsoleMode(file.handle, &original_console_mode) != 0) {
|
||
if (original_console_mode & windows.ENABLE_VIRTUAL_TERMINAL_PROCESSING != 0) return;
|
||
|
||
// For Windows Console, VT Sequences processing support was added in Windows 10 build 14361, but disabled by default.
|
||
// https://devblogs.microsoft.com/commandline/tmux-support-arrives-for-bash-on-ubuntu-on-windows/
|
||
//
|
||
// Note: In Microsoft's example for enabling virtual terminal processing, it
|
||
// shows attempting to enable `DISABLE_NEWLINE_AUTO_RETURN` as well:
|
||
// https://learn.microsoft.com/en-us/windows/console/console-virtual-terminal-sequences#example-of-enabling-virtual-terminal-processing
|
||
// This is avoided because in the old Windows Console, that flag causes \n (as opposed to \r\n)
|
||
// to behave unexpectedly (the cursor moves down 1 row but remains on the same column).
|
||
// Additionally, the default console mode in Windows Terminal does not have
|
||
// `DISABLE_NEWLINE_AUTO_RETURN` set, so by only enabling `ENABLE_VIRTUAL_TERMINAL_PROCESSING`
|
||
// we end up matching the mode of Windows Terminal.
|
||
const requested_console_modes = windows.ENABLE_VIRTUAL_TERMINAL_PROCESSING;
|
||
const console_mode = original_console_mode | requested_console_modes;
|
||
try current_thread.checkCancel();
|
||
if (windows.kernel32.SetConsoleMode(file.handle, console_mode) != 0) return;
|
||
}
|
||
if (try isCygwinPty(current_thread, file)) return;
|
||
} else {
|
||
if (try supportsAnsiEscapeCodes(current_thread, file)) return;
|
||
}
|
||
return error.NotTerminalDevice;
|
||
}
|
||
|
||
fn fileSupportsAnsiEscapeCodes(userdata: ?*anyopaque, file: Io.File) Io.Cancelable!bool {
|
||
const t: *Threaded = @ptrCast(@alignCast(userdata));
|
||
const current_thread = Thread.getCurrent(t);
|
||
return supportsAnsiEscapeCodes(current_thread, file);
|
||
}
|
||
|
||
fn supportsAnsiEscapeCodes(current_thread: *Thread, file: Io.File) Io.Cancelable!bool {
|
||
if (is_windows) {
|
||
try current_thread.checkCancel();
|
||
var console_mode: windows.DWORD = 0;
|
||
if (windows.kernel32.GetConsoleMode(file.handle, &console_mode) != 0) {
|
||
if (console_mode & windows.ENABLE_VIRTUAL_TERMINAL_PROCESSING != 0) return true;
|
||
}
|
||
return isCygwinPty(current_thread, file);
|
||
}
|
||
|
||
if (native_os == .wasi) {
|
||
// WASI sanitizes stdout when fd is a tty so ANSI escape codes will not
|
||
// be interpreted as actual cursor commands, and stderr is always
|
||
// sanitized.
|
||
return false;
|
||
}
|
||
|
||
if (try isTty(current_thread, file)) return true;
|
||
|
||
return false;
|
||
}
|
||
|
||
fn isCygwinPty(current_thread: *Thread, file: Io.File) Io.Cancelable!bool {
|
||
if (!is_windows) return false;
|
||
|
||
const handle = file.handle;
|
||
|
||
// If this is a MSYS2/cygwin pty, then it will be a named pipe with a name in one of these formats:
|
||
// msys-[...]-ptyN-[...]
|
||
// cygwin-[...]-ptyN-[...]
|
||
//
|
||
// Example: msys-1888ae32e00d56aa-pty0-to-master
|
||
|
||
// First, just check that the handle is a named pipe.
|
||
// This allows us to avoid the more costly NtQueryInformationFile call
|
||
// for handles that aren't named pipes.
|
||
{
|
||
try current_thread.checkCancel();
|
||
var io_status: windows.IO_STATUS_BLOCK = undefined;
|
||
var device_info: windows.FILE_FS_DEVICE_INFORMATION = undefined;
|
||
const rc = windows.ntdll.NtQueryVolumeInformationFile(handle, &io_status, &device_info, @sizeOf(windows.FILE_FS_DEVICE_INFORMATION), .FileFsDeviceInformation);
|
||
switch (rc) {
|
||
.SUCCESS => {},
|
||
else => return false,
|
||
}
|
||
if (device_info.DeviceType != windows.FILE_DEVICE_NAMED_PIPE) return false;
|
||
}
|
||
|
||
const name_bytes_offset = @offsetOf(windows.FILE_NAME_INFO, "FileName");
|
||
// `NAME_MAX` UTF-16 code units (2 bytes each)
|
||
// This buffer may not be long enough to handle *all* possible paths
|
||
// (PATH_MAX_WIDE would be necessary for that), but because we only care
|
||
// about certain paths and we know they must be within a reasonable length,
|
||
// we can use this smaller buffer and just return false on any error from
|
||
// NtQueryInformationFile.
|
||
const num_name_bytes = windows.MAX_PATH * 2;
|
||
var name_info_bytes align(@alignOf(windows.FILE_NAME_INFO)) = [_]u8{0} ** (name_bytes_offset + num_name_bytes);
|
||
|
||
var io_status_block: windows.IO_STATUS_BLOCK = undefined;
|
||
try current_thread.checkCancel();
|
||
const rc = windows.ntdll.NtQueryInformationFile(handle, &io_status_block, &name_info_bytes, @intCast(name_info_bytes.len), .FileNameInformation);
|
||
switch (rc) {
|
||
.SUCCESS => {},
|
||
.INVALID_PARAMETER => unreachable,
|
||
else => return false,
|
||
}
|
||
|
||
const name_info: *const windows.FILE_NAME_INFO = @ptrCast(&name_info_bytes);
|
||
const name_bytes = name_info_bytes[name_bytes_offset .. name_bytes_offset + name_info.FileNameLength];
|
||
const name_wide = std.mem.bytesAsSlice(u16, name_bytes);
|
||
// The name we get from NtQueryInformationFile will be prefixed with a '\', e.g. \msys-1888ae32e00d56aa-pty0-to-master
|
||
return (std.mem.startsWith(u16, name_wide, &[_]u16{ '\\', 'm', 's', 'y', 's', '-' }) or
|
||
std.mem.startsWith(u16, name_wide, &[_]u16{ '\\', 'c', 'y', 'g', 'w', 'i', 'n', '-' })) and
|
||
std.mem.indexOf(u16, name_wide, &[_]u16{ '-', 'p', 't', 'y' }) != null;
|
||
}
|
||
|
||
fn fileSetLength(userdata: ?*anyopaque, file: Io.File, length: u64) Io.File.SetLengthError!void {
|
||
const t: *Threaded = @ptrCast(@alignCast(userdata));
|
||
const current_thread = Thread.getCurrent(t);
|
||
|
||
const signed_len: i64 = @bitCast(length);
|
||
if (signed_len < 0) return error.FileTooBig; // Avoid ambiguous EINVAL errors.
|
||
|
||
if (is_windows) {
|
||
try current_thread.checkCancel();
|
||
|
||
var io_status_block: windows.IO_STATUS_BLOCK = undefined;
|
||
var eof_info: windows.FILE_END_OF_FILE_INFORMATION = .{
|
||
.EndOfFile = signed_len,
|
||
};
|
||
|
||
const status = windows.ntdll.NtSetInformationFile(
|
||
file.handle,
|
||
&io_status_block,
|
||
&eof_info,
|
||
@sizeOf(windows.FILE_END_OF_FILE_INFORMATION),
|
||
.FileEndOfFileInformation,
|
||
);
|
||
switch (status) {
|
||
.SUCCESS => return,
|
||
.INVALID_HANDLE => |err| return windows.statusBug(err), // Handle not open for writing.
|
||
.ACCESS_DENIED => return error.AccessDenied,
|
||
.USER_MAPPED_FILE => return error.AccessDenied,
|
||
.INVALID_PARAMETER => return error.FileTooBig,
|
||
else => return windows.unexpectedStatus(status),
|
||
}
|
||
}
|
||
|
||
if (native_os == .wasi and !builtin.link_libc) {
|
||
try current_thread.beginSyscall();
|
||
while (true) {
|
||
switch (std.os.wasi.fd_filestat_set_size(file.handle, length)) {
|
||
.SUCCESS => return current_thread.endSyscall(),
|
||
.CANCELED => return current_thread.endSyscallCanceled(),
|
||
.INTR => {
|
||
try current_thread.checkCancel();
|
||
continue;
|
||
},
|
||
else => |e| {
|
||
current_thread.endSyscall();
|
||
switch (e) {
|
||
.FBIG => return error.FileTooBig,
|
||
.IO => return error.InputOutput,
|
||
.PERM => return error.PermissionDenied,
|
||
.TXTBSY => return error.FileBusy,
|
||
.BADF => |err| return errnoBug(err), // Handle not open for writing
|
||
.INVAL => return error.NonResizable,
|
||
.NOTCAPABLE => return error.AccessDenied,
|
||
else => |err| return posix.unexpectedErrno(err),
|
||
}
|
||
},
|
||
}
|
||
}
|
||
}
|
||
|
||
try current_thread.beginSyscall();
|
||
while (true) {
|
||
switch (posix.errno(ftruncate_sym(file.handle, signed_len))) {
|
||
.SUCCESS => return current_thread.endSyscall(),
|
||
.CANCELED => return current_thread.endSyscallCanceled(),
|
||
.INTR => {
|
||
try current_thread.checkCancel();
|
||
continue;
|
||
},
|
||
else => |e| {
|
||
current_thread.endSyscall();
|
||
switch (e) {
|
||
.FBIG => return error.FileTooBig,
|
||
.IO => return error.InputOutput,
|
||
.PERM => return error.PermissionDenied,
|
||
.TXTBSY => return error.FileBusy,
|
||
.BADF => |err| return errnoBug(err), // Handle not open for writing.
|
||
.INVAL => return error.NonResizable, // This is returned for /dev/null for example.
|
||
else => |err| return posix.unexpectedErrno(err),
|
||
}
|
||
},
|
||
}
|
||
}
|
||
}
|
||
|
||
fn fileSetOwner(userdata: ?*anyopaque, file: Io.File, owner: ?Io.File.Uid, group: ?Io.File.Gid) Io.File.SetOwnerError!void {
|
||
switch (native_os) {
|
||
.windows, .wasi => return error.Unexpected,
|
||
else => {},
|
||
}
|
||
const t: *Threaded = @ptrCast(@alignCast(userdata));
|
||
const current_thread = Thread.getCurrent(t);
|
||
const uid = owner orelse ~@as(posix.uid_t, 0);
|
||
const gid = group orelse ~@as(posix.gid_t, 0);
|
||
return setOwnerPosix(current_thread, file.handle, uid, gid);
|
||
}
|
||
|
||
fn fileSetPermissions(userdata: ?*anyopaque, file: Io.File, permissions: Io.File.Permissions) Io.File.SetPermissionsError!void {
|
||
const t: *Threaded = @ptrCast(@alignCast(userdata));
|
||
const current_thread = Thread.getCurrent(t);
|
||
switch (native_os) {
|
||
.windows => {
|
||
try current_thread.checkCancel();
|
||
var io_status_block: windows.IO_STATUS_BLOCK = undefined;
|
||
var info: windows.FILE_BASIC_INFORMATION = .{
|
||
.CreationTime = 0,
|
||
.LastAccessTime = 0,
|
||
.LastWriteTime = 0,
|
||
.ChangeTime = 0,
|
||
.FileAttributes = permissions.inner.attributes,
|
||
};
|
||
const status = windows.ntdll.NtSetInformationFile(
|
||
file.handle,
|
||
&io_status_block,
|
||
&info,
|
||
@sizeOf(windows.FILE_BASIC_INFORMATION),
|
||
.FileBasicInformation,
|
||
);
|
||
switch (status) {
|
||
.SUCCESS => return,
|
||
.INVALID_HANDLE => |err| return windows.statusBug(err),
|
||
.ACCESS_DENIED => return error.AccessDenied,
|
||
else => return windows.unexpectedStatus(status),
|
||
}
|
||
},
|
||
.wasi => return error.Unexpected, // Unsupported OS.
|
||
else => return setPermissionsPosix(current_thread, file.handle, permissions.toMode()),
|
||
}
|
||
}
|
||
|
||
fn setPermissionsPosix(current_thread: *Thread, fd: posix.fd_t, mode: posix.mode_t) Io.File.SetPermissionsError!void {
|
||
try current_thread.beginSyscall();
|
||
while (true) {
|
||
switch (posix.errno(posix.system.fchmod(fd, mode))) {
|
||
.SUCCESS => return current_thread.endSyscall(),
|
||
.CANCELED => return current_thread.endSyscallCanceled(),
|
||
.INTR => {
|
||
try current_thread.checkCancel();
|
||
continue;
|
||
},
|
||
else => |e| {
|
||
current_thread.endSyscall();
|
||
switch (e) {
|
||
.BADF => |err| return errnoBug(err),
|
||
.FAULT => |err| return errnoBug(err),
|
||
.INVAL => |err| return errnoBug(err),
|
||
.ACCES => return error.AccessDenied,
|
||
.IO => return error.InputOutput,
|
||
.LOOP => return error.SymLinkLoop,
|
||
.NOENT => return error.FileNotFound,
|
||
.NOMEM => return error.SystemResources,
|
||
.NOTDIR => return error.FileNotFound,
|
||
.PERM => return error.PermissionDenied,
|
||
.ROFS => return error.ReadOnlyFileSystem,
|
||
else => |err| return posix.unexpectedErrno(err),
|
||
}
|
||
},
|
||
}
|
||
}
|
||
}
|
||
|
||
fn dirSetTimestamps(
|
||
userdata: ?*anyopaque,
|
||
dir: Io.Dir,
|
||
sub_path: []const u8,
|
||
last_accessed: Io.Timestamp,
|
||
last_modified: Io.Timestamp,
|
||
options: Io.File.SetTimestampsOptions,
|
||
) Io.File.SetTimestampsError!void {
|
||
const t: *Threaded = @ptrCast(@alignCast(userdata));
|
||
const current_thread = Thread.getCurrent(t);
|
||
|
||
if (is_windows) {
|
||
@panic("TODO");
|
||
}
|
||
|
||
if (native_os == .wasi and !builtin.link_libc) {
|
||
@panic("TODO");
|
||
}
|
||
|
||
const times: [2]posix.timespec = .{
|
||
timestampToPosix(last_accessed),
|
||
timestampToPosix(last_modified),
|
||
};
|
||
|
||
const flags: u32 = if (!options.follow_symlinks) posix.AT.SYMLINK_NOFOLLOW else 0;
|
||
|
||
var path_buffer: [posix.PATH_MAX]u8 = undefined;
|
||
const sub_path_posix = try pathToPosix(sub_path, &path_buffer);
|
||
|
||
try current_thread.beginSyscall();
|
||
while (true) {
|
||
switch (posix.errno(posix.system.utimensat(dir.handle, sub_path_posix, ×, flags))) {
|
||
.SUCCESS => return current_thread.endSyscall(),
|
||
.CANCELED => return current_thread.endSyscallCanceled(),
|
||
.INTR => {
|
||
try current_thread.checkCancel();
|
||
continue;
|
||
},
|
||
else => |e| {
|
||
current_thread.endSyscall();
|
||
switch (e) {
|
||
.ACCES => return error.AccessDenied,
|
||
.PERM => return error.PermissionDenied,
|
||
.BADF => |err| return errnoBug(err), // always a race condition
|
||
.FAULT => |err| return errnoBug(err),
|
||
.INVAL => |err| return errnoBug(err),
|
||
.ROFS => return error.ReadOnlyFileSystem,
|
||
else => |err| return posix.unexpectedErrno(err),
|
||
}
|
||
},
|
||
}
|
||
}
|
||
}
|
||
|
||
fn dirSetTimestampsNow(
|
||
userdata: ?*anyopaque,
|
||
dir: Io.Dir,
|
||
sub_path: []const u8,
|
||
options: Io.File.SetTimestampsOptions,
|
||
) Io.File.SetTimestampsError!void {
|
||
const t: *Threaded = @ptrCast(@alignCast(userdata));
|
||
const current_thread = Thread.getCurrent(t);
|
||
|
||
if (is_windows) {
|
||
@panic("TODO");
|
||
}
|
||
|
||
if (native_os == .wasi and !builtin.link_libc) {
|
||
@panic("TODO");
|
||
}
|
||
|
||
const flags: u32 = if (!options.follow_symlinks) posix.AT.SYMLINK_NOFOLLOW else 0;
|
||
|
||
var path_buffer: [posix.PATH_MAX]u8 = undefined;
|
||
const sub_path_posix = try pathToPosix(sub_path, &path_buffer);
|
||
|
||
try current_thread.beginSyscall();
|
||
while (true) {
|
||
switch (posix.errno(posix.system.utimensat(dir.handle, sub_path_posix, null, flags))) {
|
||
.SUCCESS => return current_thread.endSyscall(),
|
||
.CANCELED => return current_thread.endSyscallCanceled(),
|
||
.INTR => {
|
||
try current_thread.checkCancel();
|
||
continue;
|
||
},
|
||
else => |e| {
|
||
current_thread.endSyscall();
|
||
switch (e) {
|
||
.ACCES => return error.AccessDenied,
|
||
.PERM => return error.PermissionDenied,
|
||
.BADF => |err| return errnoBug(err), // always a race condition
|
||
.FAULT => |err| return errnoBug(err),
|
||
.INVAL => |err| return errnoBug(err),
|
||
.ROFS => return error.ReadOnlyFileSystem,
|
||
else => |err| return posix.unexpectedErrno(err),
|
||
}
|
||
},
|
||
}
|
||
}
|
||
}
|
||
|
||
fn fileSetTimestamps(
|
||
userdata: ?*anyopaque,
|
||
file: Io.File,
|
||
last_accessed: Io.Timestamp,
|
||
last_modified: Io.Timestamp,
|
||
) Io.File.SetTimestampsError!void {
|
||
const t: *Threaded = @ptrCast(@alignCast(userdata));
|
||
const current_thread = Thread.getCurrent(t);
|
||
|
||
if (is_windows) {
|
||
try current_thread.checkCancel();
|
||
|
||
const atime_ft = windows.nanoSecondsToFileTime(last_accessed.toNanoseconds());
|
||
const mtime_ft = windows.nanoSecondsToFileTime(last_modified.toNanoseconds());
|
||
|
||
// https://github.com/ziglang/zig/issues/1840
|
||
const rc = windows.kernel32.SetFileTime(file.handle, null, &atime_ft, &mtime_ft);
|
||
if (rc == 0) {
|
||
switch (windows.GetLastError()) {
|
||
else => |err| return windows.unexpectedError(err),
|
||
}
|
||
}
|
||
return;
|
||
}
|
||
|
||
const times: [2]posix.timespec = .{
|
||
timestampToPosix(last_accessed),
|
||
timestampToPosix(last_modified),
|
||
};
|
||
|
||
if (native_os == .wasi and !builtin.link_libc) {
|
||
const atim = times[0].toTimestamp();
|
||
const mtim = times[1].toTimestamp();
|
||
try current_thread.beginSyscall();
|
||
while (true) {
|
||
switch (std.os.wasi.fd_filestat_set_times(file.handle, atim, mtim, .{
|
||
.ATIM = true,
|
||
.MTIM = true,
|
||
})) {
|
||
.SUCCESS => return current_thread.endSyscall(),
|
||
.CANCELED => return current_thread.endSyscallCanceled(),
|
||
.INTR => {
|
||
try current_thread.checkCancel();
|
||
continue;
|
||
},
|
||
else => |e| {
|
||
current_thread.endSyscall();
|
||
switch (e) {
|
||
.ACCES => return error.AccessDenied,
|
||
.PERM => return error.PermissionDenied,
|
||
.BADF => |err| return errnoBug(err), // File descriptor use-after-free.
|
||
.FAULT => |err| return errnoBug(err),
|
||
.INVAL => |err| return errnoBug(err),
|
||
.ROFS => return error.ReadOnlyFileSystem,
|
||
else => |err| return posix.unexpectedErrno(err),
|
||
}
|
||
},
|
||
}
|
||
}
|
||
}
|
||
|
||
try current_thread.beginSyscall();
|
||
while (true) {
|
||
switch (posix.errno(posix.system.futimens(file.handle, ×))) {
|
||
.SUCCESS => return current_thread.endSyscall(),
|
||
.CANCELED => return current_thread.endSyscallCanceled(),
|
||
.INTR => {
|
||
try current_thread.checkCancel();
|
||
continue;
|
||
},
|
||
else => |e| {
|
||
current_thread.endSyscall();
|
||
switch (e) {
|
||
.ACCES => return error.AccessDenied,
|
||
.PERM => return error.PermissionDenied,
|
||
.BADF => |err| return errnoBug(err), // always a race condition
|
||
.FAULT => |err| return errnoBug(err),
|
||
.INVAL => |err| return errnoBug(err),
|
||
.ROFS => return error.ReadOnlyFileSystem,
|
||
else => |err| return posix.unexpectedErrno(err),
|
||
}
|
||
},
|
||
}
|
||
}
|
||
}
|
||
|
||
fn fileSetTimestampsNow(userdata: ?*anyopaque, file: Io.File) Io.File.SetTimestampsError!void {
|
||
const t: *Threaded = @ptrCast(@alignCast(userdata));
|
||
const current_thread = Thread.getCurrent(t);
|
||
|
||
if (is_windows) {
|
||
@panic("TODO");
|
||
}
|
||
|
||
if (native_os == .wasi and !builtin.link_libc) {
|
||
try current_thread.beginSyscall();
|
||
while (true) {
|
||
switch (std.os.wasi.fd_filestat_set_times(file.handle, 0, 0, .{
|
||
.ATIM_NOW = true,
|
||
.MTIM_NOW = true,
|
||
})) {
|
||
.SUCCESS => return current_thread.endSyscall(),
|
||
.CANCELED => return current_thread.endSyscallCanceled(),
|
||
.INTR => {
|
||
try current_thread.checkCancel();
|
||
continue;
|
||
},
|
||
else => |e| {
|
||
current_thread.endSyscall();
|
||
switch (e) {
|
||
.ACCES => return error.AccessDenied,
|
||
.PERM => return error.PermissionDenied,
|
||
.BADF => |err| return errnoBug(err), // always a race condition
|
||
.FAULT => |err| return errnoBug(err),
|
||
.INVAL => |err| return errnoBug(err),
|
||
.ROFS => return error.ReadOnlyFileSystem,
|
||
else => |err| return posix.unexpectedErrno(err),
|
||
}
|
||
},
|
||
}
|
||
}
|
||
}
|
||
|
||
try current_thread.beginSyscall();
|
||
while (true) {
|
||
switch (posix.errno(posix.system.futimens(file.handle, null))) {
|
||
.SUCCESS => return current_thread.endSyscall(),
|
||
.CANCELED => return current_thread.endSyscallCanceled(),
|
||
.INTR => {
|
||
try current_thread.checkCancel();
|
||
continue;
|
||
},
|
||
else => |e| {
|
||
current_thread.endSyscall();
|
||
switch (e) {
|
||
.ACCES => return error.AccessDenied,
|
||
.PERM => return error.PermissionDenied,
|
||
.BADF => |err| return errnoBug(err), // always a race condition
|
||
.FAULT => |err| return errnoBug(err),
|
||
.INVAL => |err| return errnoBug(err),
|
||
.ROFS => return error.ReadOnlyFileSystem,
|
||
else => |err| return posix.unexpectedErrno(err),
|
||
}
|
||
},
|
||
}
|
||
}
|
||
}
|
||
|
||
const windows_lock_range_off: windows.LARGE_INTEGER = 0;
|
||
const windows_lock_range_len: windows.LARGE_INTEGER = 1;
|
||
|
||
fn fileLock(userdata: ?*anyopaque, file: Io.File, lock: Io.File.Lock) Io.File.LockError!void {
|
||
const t: *Threaded = @ptrCast(@alignCast(userdata));
|
||
const current_thread = Thread.getCurrent(t);
|
||
|
||
if (is_windows) {
|
||
const exclusive = switch (lock) {
|
||
.none => return,
|
||
.shared => false,
|
||
.exclusive => true,
|
||
};
|
||
try current_thread.checkCancel();
|
||
var io_status_block: windows.IO_STATUS_BLOCK = undefined;
|
||
const status = windows.ntdll.NtLockFile(
|
||
file.handle,
|
||
null,
|
||
null,
|
||
null,
|
||
&io_status_block,
|
||
&windows_lock_range_off,
|
||
&windows_lock_range_len,
|
||
null,
|
||
windows.FALSE,
|
||
@intFromBool(exclusive),
|
||
);
|
||
switch (status) {
|
||
.SUCCESS => return,
|
||
.INSUFFICIENT_RESOURCES => return error.SystemResources,
|
||
.LOCK_NOT_GRANTED => |err| return windows.statusBug(err), // passed FailImmediately=false
|
||
.ACCESS_VIOLATION => |err| return windows.statusBug(err), // bad io_status_block pointer
|
||
else => return windows.unexpectedStatus(status),
|
||
}
|
||
}
|
||
|
||
const operation = switch (lock) {
|
||
.none => posix.LOCK.UN,
|
||
.shared => posix.LOCK.SH,
|
||
.exclusive => posix.LOCK.EX,
|
||
};
|
||
try current_thread.beginSyscall();
|
||
while (true) {
|
||
switch (posix.errno(posix.system.flock(file.handle, operation))) {
|
||
.SUCCESS => return current_thread.endSyscall(),
|
||
.CANCELED => return current_thread.endSyscallCanceled(),
|
||
.INTR => {
|
||
try current_thread.checkCancel();
|
||
continue;
|
||
},
|
||
else => |e| {
|
||
current_thread.endSyscall();
|
||
switch (e) {
|
||
.BADF => |err| return errnoBug(err),
|
||
.INVAL => |err| return errnoBug(err), // invalid parameters
|
||
.NOLCK => return error.SystemResources,
|
||
.AGAIN => |err| return errnoBug(err),
|
||
.OPNOTSUPP => return error.FileLocksUnsupported,
|
||
else => |err| return posix.unexpectedErrno(err),
|
||
}
|
||
},
|
||
}
|
||
}
|
||
}
|
||
|
||
fn fileTryLock(userdata: ?*anyopaque, file: Io.File, lock: Io.File.Lock) Io.File.LockError!bool {
|
||
const t: *Threaded = @ptrCast(@alignCast(userdata));
|
||
const current_thread = Thread.getCurrent(t);
|
||
|
||
if (is_windows) {
|
||
const exclusive = switch (lock) {
|
||
.none => return,
|
||
.shared => false,
|
||
.exclusive => true,
|
||
};
|
||
try current_thread.checkCancel();
|
||
var io_status_block: windows.IO_STATUS_BLOCK = undefined;
|
||
const status = windows.ntdll.NtLockFile(
|
||
file.handle,
|
||
null,
|
||
null,
|
||
null,
|
||
&io_status_block,
|
||
&windows_lock_range_off,
|
||
&windows_lock_range_len,
|
||
null,
|
||
windows.TRUE,
|
||
@intFromBool(exclusive),
|
||
);
|
||
switch (status) {
|
||
.SUCCESS => return true,
|
||
.INSUFFICIENT_RESOURCES => return error.SystemResources,
|
||
.LOCK_NOT_GRANTED => return false,
|
||
.ACCESS_VIOLATION => |err| return windows.statusBug(err), // bad io_status_block pointer
|
||
else => return windows.unexpectedStatus(status),
|
||
}
|
||
}
|
||
|
||
const operation = switch (lock) {
|
||
.none => posix.LOCK.UN,
|
||
.shared => posix.LOCK.SH | posix.LOCK.NB,
|
||
.exclusive => posix.LOCK.EX | posix.LOCK.NB,
|
||
};
|
||
try current_thread.beginSyscall();
|
||
while (true) {
|
||
switch (posix.errno(posix.system.flock(file.handle, operation))) {
|
||
.SUCCESS => {
|
||
current_thread.endSyscall();
|
||
return true;
|
||
},
|
||
.CANCELED => return current_thread.endSyscallCanceled(),
|
||
.INTR => {
|
||
try current_thread.checkCancel();
|
||
continue;
|
||
},
|
||
.AGAIN => {
|
||
current_thread.endSyscall();
|
||
return false;
|
||
},
|
||
else => |e| {
|
||
current_thread.endSyscall();
|
||
switch (e) {
|
||
.BADF => |err| return errnoBug(err),
|
||
.INVAL => |err| return errnoBug(err), // invalid parameters
|
||
.NOLCK => return error.SystemResources,
|
||
.OPNOTSUPP => return error.FileLocksUnsupported,
|
||
else => |err| return posix.unexpectedErrno(err),
|
||
}
|
||
},
|
||
}
|
||
}
|
||
}
|
||
|
||
fn fileUnlock(userdata: ?*anyopaque, file: Io.File) void {
|
||
const t: *Threaded = @ptrCast(@alignCast(userdata));
|
||
const current_thread = Thread.getCurrent(t);
|
||
|
||
if (is_windows) {
|
||
try current_thread.checkCancel();
|
||
var io_status_block: windows.IO_STATUS_BLOCK = undefined;
|
||
const status = windows.ntdll.NtUnlockFile(
|
||
file.handle,
|
||
&io_status_block,
|
||
&windows_lock_range_off,
|
||
&windows_lock_range_len,
|
||
null,
|
||
);
|
||
if (is_debug) switch (status) {
|
||
.SUCCESS => {},
|
||
.RANGE_NOT_LOCKED => unreachable, // Function asserts unlocked.
|
||
.ACCESS_VIOLATION => unreachable, // bad io_status_block pointer
|
||
else => unreachable, // Resource deallocation must succeed.
|
||
};
|
||
return;
|
||
}
|
||
|
||
try current_thread.beginSyscall();
|
||
while (true) {
|
||
switch (posix.errno(posix.system.flock(file.handle, posix.LOCK.UN))) {
|
||
.SUCCESS => return current_thread.endSyscall(),
|
||
.CANCELED => return current_thread.endSyscallCanceled(),
|
||
.INTR => {
|
||
try current_thread.checkCancel();
|
||
continue;
|
||
},
|
||
else => |e| {
|
||
current_thread.endSyscall();
|
||
if (is_debug) switch (e) {
|
||
.AGAIN => unreachable, // unlocking can't block
|
||
.BADF => unreachable, // File descriptor used after closed.
|
||
.INVAL => unreachable, // invalid parameters
|
||
.NOLCK => unreachable, // Resource deallocation.
|
||
.OPNOTSUPP => unreachable, // We already got the lock.
|
||
else => unreachable, // Resource deallocation must succeed.
|
||
};
|
||
return;
|
||
},
|
||
}
|
||
}
|
||
}
|
||
|
||
fn fileDowngradeLock(userdata: ?*anyopaque, file: Io.File) Io.File.DowngradeLockError!void {
|
||
const t: *Threaded = @ptrCast(@alignCast(userdata));
|
||
const current_thread = Thread.getCurrent(t);
|
||
|
||
if (is_windows) {
|
||
try current_thread.checkCancel();
|
||
// On Windows it works like a semaphore + exclusivity flag. To
|
||
// implement this function, we first obtain another lock in shared
|
||
// mode. This changes the exclusivity flag, but increments the
|
||
// semaphore to 2. So we follow up with an NtUnlockFile which
|
||
// decrements the semaphore but does not modify the exclusivity flag.
|
||
var io_status_block: windows.IO_STATUS_BLOCK = undefined;
|
||
switch (windows.ntdll.NtLockFile(
|
||
file.handle,
|
||
null,
|
||
null,
|
||
null,
|
||
&io_status_block,
|
||
&windows_lock_range_off,
|
||
&windows_lock_range_len,
|
||
null,
|
||
windows.TRUE,
|
||
windows.FALSE,
|
||
)) {
|
||
.SUCCESS => {},
|
||
.INSUFFICIENT_RESOURCES => |err| return windows.statusBug(err),
|
||
.LOCK_NOT_GRANTED => |err| return windows.statusBug(err), // File was not locked in exclusive mode.
|
||
.ACCESS_VIOLATION => |err| return windows.statusBug(err), // bad io_status_block pointer
|
||
else => |status| return windows.unexpectedStatus(status),
|
||
}
|
||
const status = windows.ntdll.NtUnlockFile(
|
||
file.handle,
|
||
&io_status_block,
|
||
&windows_lock_range_off,
|
||
&windows_lock_range_len,
|
||
null,
|
||
);
|
||
if (is_debug) switch (status) {
|
||
.SUCCESS => {},
|
||
.RANGE_NOT_LOCKED => unreachable, // File was not locked.
|
||
.ACCESS_VIOLATION => unreachable, // bad io_status_block pointer
|
||
else => unreachable, // Resource deallocation must succeed.
|
||
};
|
||
return;
|
||
}
|
||
|
||
const operation = posix.LOCK.SH | posix.LOCK.NB;
|
||
|
||
try current_thread.beginSyscall();
|
||
while (true) {
|
||
switch (posix.errno(posix.system.flock(file.handle, operation))) {
|
||
.SUCCESS => {
|
||
current_thread.endSyscall();
|
||
return true;
|
||
},
|
||
.CANCELED => return current_thread.endSyscallCanceled(),
|
||
.INTR => {
|
||
try current_thread.checkCancel();
|
||
continue;
|
||
},
|
||
else => |e| {
|
||
current_thread.endSyscall();
|
||
switch (e) {
|
||
.AGAIN => |err| return errnoBug(err), // File was not locked in exclusive mode.
|
||
.BADF => |err| return errnoBug(err),
|
||
.INVAL => |err| return errnoBug(err), // invalid parameters
|
||
.NOLCK => |err| return errnoBug(err), // Lock already obtained.
|
||
.OPNOTSUPP => |err| return errnoBug(err), // Lock already obtained.
|
||
else => |err| return posix.unexpectedErrno(err),
|
||
}
|
||
},
|
||
}
|
||
}
|
||
}
|
||
|
||
fn dirOpenDirWasi(
|
||
userdata: ?*anyopaque,
|
||
dir: Io.Dir,
|
||
sub_path: []const u8,
|
||
options: Io.Dir.OpenOptions,
|
||
) Io.Dir.OpenError!Io.Dir {
|
||
if (builtin.link_libc) return dirOpenDirPosix(userdata, dir, sub_path, options);
|
||
const t: *Threaded = @ptrCast(@alignCast(userdata));
|
||
const current_thread = Thread.getCurrent(t);
|
||
const wasi = std.os.wasi;
|
||
|
||
var base: std.os.wasi.rights_t = .{
|
||
.FD_FILESTAT_GET = true,
|
||
.FD_FDSTAT_SET_FLAGS = true,
|
||
.FD_FILESTAT_SET_TIMES = true,
|
||
};
|
||
if (options.access_sub_paths) {
|
||
base.FD_READDIR = true;
|
||
base.PATH_CREATE_DIRECTORY = true;
|
||
base.PATH_CREATE_FILE = true;
|
||
base.PATH_LINK_SOURCE = true;
|
||
base.PATH_LINK_TARGET = true;
|
||
base.PATH_OPEN = true;
|
||
base.PATH_READLINK = true;
|
||
base.PATH_RENAME_SOURCE = true;
|
||
base.PATH_RENAME_TARGET = true;
|
||
base.PATH_FILESTAT_GET = true;
|
||
base.PATH_FILESTAT_SET_SIZE = true;
|
||
base.PATH_FILESTAT_SET_TIMES = true;
|
||
base.PATH_SYMLINK = true;
|
||
base.PATH_REMOVE_DIRECTORY = true;
|
||
base.PATH_UNLINK_FILE = true;
|
||
}
|
||
|
||
const lookup_flags: wasi.lookupflags_t = .{ .SYMLINK_FOLLOW = options.follow_symlinks };
|
||
const oflags: wasi.oflags_t = .{ .DIRECTORY = true };
|
||
const fdflags: wasi.fdflags_t = .{};
|
||
var fd: posix.fd_t = undefined;
|
||
try current_thread.beginSyscall();
|
||
while (true) {
|
||
switch (wasi.path_open(dir.handle, lookup_flags, sub_path.ptr, sub_path.len, oflags, base, base, fdflags, &fd)) {
|
||
.SUCCESS => {
|
||
current_thread.endSyscall();
|
||
return .{ .handle = fd };
|
||
},
|
||
.INTR => {
|
||
try current_thread.checkCancel();
|
||
continue;
|
||
},
|
||
.CANCELED => return current_thread.endSyscallCanceled(),
|
||
else => |e| {
|
||
current_thread.endSyscall();
|
||
switch (e) {
|
||
.FAULT => |err| return errnoBug(err),
|
||
.INVAL => return error.BadPathName,
|
||
.BADF => |err| return errnoBug(err), // File descriptor used after closed.
|
||
.ACCES => return error.AccessDenied,
|
||
.LOOP => return error.SymLinkLoop,
|
||
.MFILE => return error.ProcessFdQuotaExceeded,
|
||
.NAMETOOLONG => return error.NameTooLong,
|
||
.NFILE => return error.SystemFdQuotaExceeded,
|
||
.NODEV => return error.NoDevice,
|
||
.NOENT => return error.FileNotFound,
|
||
.NOMEM => return error.SystemResources,
|
||
.NOTDIR => return error.NotDir,
|
||
.PERM => return error.PermissionDenied,
|
||
.BUSY => return error.DeviceBusy,
|
||
.NOTCAPABLE => return error.AccessDenied,
|
||
.ILSEQ => return error.BadPathName,
|
||
else => |err| return posix.unexpectedErrno(err),
|
||
}
|
||
},
|
||
}
|
||
}
|
||
}
|
||
|
||
fn fileClose(userdata: ?*anyopaque, file: Io.File) void {
|
||
const t: *Threaded = @ptrCast(@alignCast(userdata));
|
||
_ = t;
|
||
posix.close(file.handle);
|
||
}
|
||
|
||
const fileReadStreaming = switch (native_os) {
|
||
.windows => fileReadStreamingWindows,
|
||
else => fileReadStreamingPosix,
|
||
};
|
||
|
||
fn fileReadStreamingPosix(userdata: ?*anyopaque, file: Io.File, data: [][]u8) Io.File.Reader.Error!usize {
|
||
const t: *Threaded = @ptrCast(@alignCast(userdata));
|
||
const current_thread = Thread.getCurrent(t);
|
||
|
||
var iovecs_buffer: [max_iovecs_len]posix.iovec = undefined;
|
||
var i: usize = 0;
|
||
for (data) |buf| {
|
||
if (iovecs_buffer.len - i == 0) break;
|
||
if (buf.len != 0) {
|
||
iovecs_buffer[i] = .{ .base = buf.ptr, .len = buf.len };
|
||
i += 1;
|
||
}
|
||
}
|
||
const dest = iovecs_buffer[0..i];
|
||
assert(dest[0].len > 0);
|
||
|
||
if (native_os == .wasi and !builtin.link_libc) {
|
||
try current_thread.beginSyscall();
|
||
while (true) {
|
||
var nread: usize = undefined;
|
||
switch (std.os.wasi.fd_read(file.handle, dest.ptr, dest.len, &nread)) {
|
||
.SUCCESS => {
|
||
current_thread.endSyscall();
|
||
return nread;
|
||
},
|
||
.INTR => {
|
||
try current_thread.checkCancel();
|
||
continue;
|
||
},
|
||
.CANCELED => return current_thread.endSyscallCanceled(),
|
||
else => |e| {
|
||
current_thread.endSyscall();
|
||
switch (e) {
|
||
.INVAL => |err| return errnoBug(err),
|
||
.FAULT => |err| return errnoBug(err),
|
||
.BADF => return error.NotOpenForReading, // File operation on directory.
|
||
.IO => return error.InputOutput,
|
||
.ISDIR => return error.IsDir,
|
||
.NOBUFS => return error.SystemResources,
|
||
.NOMEM => return error.SystemResources,
|
||
.NOTCONN => return error.SocketUnconnected,
|
||
.CONNRESET => return error.ConnectionResetByPeer,
|
||
.TIMEDOUT => return error.Timeout,
|
||
.NOTCAPABLE => return error.AccessDenied,
|
||
else => |err| return posix.unexpectedErrno(err),
|
||
}
|
||
},
|
||
}
|
||
}
|
||
}
|
||
|
||
try current_thread.beginSyscall();
|
||
while (true) {
|
||
const rc = posix.system.readv(file.handle, dest.ptr, @intCast(dest.len));
|
||
switch (posix.errno(rc)) {
|
||
.SUCCESS => {
|
||
current_thread.endSyscall();
|
||
return @intCast(rc);
|
||
},
|
||
.INTR => {
|
||
try current_thread.checkCancel();
|
||
continue;
|
||
},
|
||
.CANCELED => return current_thread.endSyscallCanceled(),
|
||
else => |e| {
|
||
current_thread.endSyscall();
|
||
switch (e) {
|
||
.INVAL => |err| return errnoBug(err),
|
||
.FAULT => |err| return errnoBug(err),
|
||
.SRCH => return error.ProcessNotFound,
|
||
.AGAIN => return error.WouldBlock,
|
||
.BADF => |err| {
|
||
if (native_os == .wasi) return error.NotOpenForReading; // File operation on directory.
|
||
return errnoBug(err); // File descriptor used after closed.
|
||
},
|
||
.IO => return error.InputOutput,
|
||
.ISDIR => return error.IsDir,
|
||
.NOBUFS => return error.SystemResources,
|
||
.NOMEM => return error.SystemResources,
|
||
.NOTCONN => return error.SocketUnconnected,
|
||
.CONNRESET => return error.ConnectionResetByPeer,
|
||
.TIMEDOUT => return error.Timeout,
|
||
else => |err| return posix.unexpectedErrno(err),
|
||
}
|
||
},
|
||
}
|
||
}
|
||
}
|
||
|
||
fn fileReadStreamingWindows(userdata: ?*anyopaque, file: Io.File, data: [][]u8) Io.File.Reader.Error!usize {
|
||
const t: *Threaded = @ptrCast(@alignCast(userdata));
|
||
const current_thread = Thread.getCurrent(t);
|
||
|
||
const DWORD = windows.DWORD;
|
||
var index: usize = 0;
|
||
while (data[index].len == 0) index += 1;
|
||
const buffer = data[index];
|
||
const want_read_count: DWORD = @min(std.math.maxInt(DWORD), buffer.len);
|
||
|
||
while (true) {
|
||
try current_thread.checkCancel();
|
||
var n: DWORD = undefined;
|
||
if (windows.kernel32.ReadFile(file.handle, buffer.ptr, want_read_count, &n, null) != 0)
|
||
return n;
|
||
switch (windows.GetLastError()) {
|
||
.IO_PENDING => |err| return windows.errorBug(err),
|
||
.OPERATION_ABORTED => continue,
|
||
.BROKEN_PIPE => return 0,
|
||
.HANDLE_EOF => return 0,
|
||
.NETNAME_DELETED => return error.ConnectionResetByPeer,
|
||
.LOCK_VIOLATION => return error.LockViolation,
|
||
.ACCESS_DENIED => return error.AccessDenied,
|
||
.INVALID_HANDLE => return error.NotOpenForReading,
|
||
else => |err| return windows.unexpectedError(err),
|
||
}
|
||
}
|
||
}
|
||
|
||
fn fileReadPositionalPosix(userdata: ?*anyopaque, file: Io.File, data: [][]u8, offset: u64) Io.File.ReadPositionalError!usize {
|
||
const t: *Threaded = @ptrCast(@alignCast(userdata));
|
||
const current_thread = Thread.getCurrent(t);
|
||
|
||
if (!have_preadv) @compileError("TODO");
|
||
|
||
var iovecs_buffer: [max_iovecs_len]posix.iovec = undefined;
|
||
var i: usize = 0;
|
||
for (data) |buf| {
|
||
if (iovecs_buffer.len - i == 0) break;
|
||
if (buf.len != 0) {
|
||
iovecs_buffer[i] = .{ .base = buf.ptr, .len = buf.len };
|
||
i += 1;
|
||
}
|
||
}
|
||
const dest = iovecs_buffer[0..i];
|
||
assert(dest[0].len > 0);
|
||
|
||
if (native_os == .wasi and !builtin.link_libc) {
|
||
try current_thread.beginSyscall();
|
||
while (true) {
|
||
var nread: usize = undefined;
|
||
switch (std.os.wasi.fd_pread(file.handle, dest.ptr, dest.len, offset, &nread)) {
|
||
.SUCCESS => {
|
||
current_thread.endSyscall();
|
||
return nread;
|
||
},
|
||
.INTR => {
|
||
try current_thread.checkCancel();
|
||
continue;
|
||
},
|
||
.CANCELED => return current_thread.endSyscallCanceled(),
|
||
else => |e| {
|
||
current_thread.endSyscall();
|
||
switch (e) {
|
||
.INVAL => |err| return errnoBug(err),
|
||
.FAULT => |err| return errnoBug(err),
|
||
.AGAIN => |err| return errnoBug(err),
|
||
.BADF => return error.NotOpenForReading, // File operation on directory.
|
||
.IO => return error.InputOutput,
|
||
.ISDIR => return error.IsDir,
|
||
.NOBUFS => return error.SystemResources,
|
||
.NOMEM => return error.SystemResources,
|
||
.NOTCONN => return error.SocketUnconnected,
|
||
.CONNRESET => return error.ConnectionResetByPeer,
|
||
.TIMEDOUT => return error.Timeout,
|
||
.NXIO => return error.Unseekable,
|
||
.SPIPE => return error.Unseekable,
|
||
.OVERFLOW => return error.Unseekable,
|
||
.NOTCAPABLE => return error.AccessDenied,
|
||
else => |err| return posix.unexpectedErrno(err),
|
||
}
|
||
},
|
||
}
|
||
}
|
||
}
|
||
|
||
try current_thread.beginSyscall();
|
||
while (true) {
|
||
const rc = preadv_sym(file.handle, dest.ptr, @intCast(dest.len), @bitCast(offset));
|
||
switch (posix.errno(rc)) {
|
||
.SUCCESS => {
|
||
current_thread.endSyscall();
|
||
return @bitCast(rc);
|
||
},
|
||
.INTR => {
|
||
try current_thread.checkCancel();
|
||
continue;
|
||
},
|
||
.CANCELED => return current_thread.endSyscallCanceled(),
|
||
else => |e| {
|
||
current_thread.endSyscall();
|
||
switch (e) {
|
||
.INVAL => |err| return errnoBug(err),
|
||
.FAULT => |err| return errnoBug(err),
|
||
.SRCH => return error.ProcessNotFound,
|
||
.AGAIN => return error.WouldBlock,
|
||
.BADF => |err| {
|
||
if (native_os == .wasi) return error.NotOpenForReading; // File operation on directory.
|
||
return errnoBug(err); // File descriptor used after closed.
|
||
},
|
||
.IO => return error.InputOutput,
|
||
.ISDIR => return error.IsDir,
|
||
.NOBUFS => return error.SystemResources,
|
||
.NOMEM => return error.SystemResources,
|
||
.NOTCONN => return error.SocketUnconnected,
|
||
.CONNRESET => return error.ConnectionResetByPeer,
|
||
.TIMEDOUT => return error.Timeout,
|
||
.NXIO => return error.Unseekable,
|
||
.SPIPE => return error.Unseekable,
|
||
.OVERFLOW => return error.Unseekable,
|
||
else => |err| return posix.unexpectedErrno(err),
|
||
}
|
||
},
|
||
}
|
||
}
|
||
}
|
||
|
||
const fileReadPositional = switch (native_os) {
|
||
.windows => fileReadPositionalWindows,
|
||
else => fileReadPositionalPosix,
|
||
};
|
||
|
||
fn fileReadPositionalWindows(userdata: ?*anyopaque, file: Io.File, data: [][]u8, offset: u64) Io.File.ReadPositionalError!usize {
|
||
const t: *Threaded = @ptrCast(@alignCast(userdata));
|
||
const current_thread = Thread.getCurrent(t);
|
||
|
||
const DWORD = windows.DWORD;
|
||
|
||
var index: usize = 0;
|
||
while (data[index].len == 0) index += 1;
|
||
const buffer = data[index];
|
||
const want_read_count: DWORD = @min(std.math.maxInt(DWORD), buffer.len);
|
||
|
||
var overlapped: windows.OVERLAPPED = .{
|
||
.Internal = 0,
|
||
.InternalHigh = 0,
|
||
.DUMMYUNIONNAME = .{
|
||
.DUMMYSTRUCTNAME = .{
|
||
.Offset = @truncate(offset),
|
||
.OffsetHigh = @truncate(offset >> 32),
|
||
},
|
||
},
|
||
.hEvent = null,
|
||
};
|
||
|
||
while (true) {
|
||
try current_thread.checkCancel();
|
||
var n: DWORD = undefined;
|
||
if (windows.kernel32.ReadFile(file.handle, buffer.ptr, want_read_count, &n, &overlapped) != 0)
|
||
return n;
|
||
switch (windows.GetLastError()) {
|
||
.IO_PENDING => |err| return windows.errorBug(err),
|
||
.OPERATION_ABORTED => continue,
|
||
.BROKEN_PIPE => return 0,
|
||
.HANDLE_EOF => return 0,
|
||
.NETNAME_DELETED => return error.ConnectionResetByPeer,
|
||
.LOCK_VIOLATION => return error.LockViolation,
|
||
.ACCESS_DENIED => return error.AccessDenied,
|
||
.INVALID_HANDLE => return error.NotOpenForReading,
|
||
else => |err| return windows.unexpectedError(err),
|
||
}
|
||
}
|
||
}
|
||
|
||
fn fileSeekBy(userdata: ?*anyopaque, file: Io.File, offset: i64) Io.File.SeekError!void {
|
||
const t: *Threaded = @ptrCast(@alignCast(userdata));
|
||
const current_thread = Thread.getCurrent(t);
|
||
const fd = file.handle;
|
||
|
||
if (native_os == .linux and !builtin.link_libc and @sizeOf(usize) == 4) {
|
||
var result: u64 = undefined;
|
||
try current_thread.beginSyscall();
|
||
while (true) {
|
||
switch (posix.errno(posix.system.llseek(fd, offset, &result, posix.SEEK.CUR))) {
|
||
.SUCCESS => {
|
||
current_thread.endSyscall();
|
||
return;
|
||
},
|
||
.INTR => {
|
||
try current_thread.checkCancel();
|
||
continue;
|
||
},
|
||
.CANCELED => return current_thread.endSyscallCanceled(),
|
||
else => |e| {
|
||
current_thread.endSyscall();
|
||
switch (e) {
|
||
.BADF => |err| return errnoBug(err), // File descriptor used after closed.
|
||
.INVAL => return error.Unseekable,
|
||
.OVERFLOW => return error.Unseekable,
|
||
.SPIPE => return error.Unseekable,
|
||
.NXIO => return error.Unseekable,
|
||
else => |err| return posix.unexpectedErrno(err),
|
||
}
|
||
},
|
||
}
|
||
}
|
||
}
|
||
|
||
if (native_os == .windows) {
|
||
try current_thread.checkCancel();
|
||
return windows.SetFilePointerEx_CURRENT(fd, offset);
|
||
}
|
||
|
||
if (native_os == .wasi and !builtin.link_libc) {
|
||
var new_offset: std.os.wasi.filesize_t = undefined;
|
||
try current_thread.beginSyscall();
|
||
while (true) {
|
||
switch (std.os.wasi.fd_seek(fd, offset, .CUR, &new_offset)) {
|
||
.SUCCESS => {
|
||
current_thread.endSyscall();
|
||
return;
|
||
},
|
||
.INTR => {
|
||
try current_thread.checkCancel();
|
||
continue;
|
||
},
|
||
.CANCELED => return current_thread.endSyscallCanceled(),
|
||
else => |e| {
|
||
current_thread.endSyscall();
|
||
switch (e) {
|
||
.BADF => |err| return errnoBug(err), // File descriptor used after closed.
|
||
.INVAL => return error.Unseekable,
|
||
.OVERFLOW => return error.Unseekable,
|
||
.SPIPE => return error.Unseekable,
|
||
.NXIO => return error.Unseekable,
|
||
.NOTCAPABLE => return error.AccessDenied,
|
||
else => |err| return posix.unexpectedErrno(err),
|
||
}
|
||
},
|
||
}
|
||
}
|
||
}
|
||
|
||
if (posix.SEEK == void) return error.Unseekable;
|
||
|
||
try current_thread.beginSyscall();
|
||
while (true) {
|
||
switch (posix.errno(lseek_sym(fd, offset, posix.SEEK.CUR))) {
|
||
.SUCCESS => {
|
||
current_thread.endSyscall();
|
||
return;
|
||
},
|
||
.INTR => {
|
||
try current_thread.checkCancel();
|
||
continue;
|
||
},
|
||
.CANCELED => return current_thread.endSyscallCanceled(),
|
||
else => |e| {
|
||
current_thread.endSyscall();
|
||
switch (e) {
|
||
.BADF => |err| return errnoBug(err), // File descriptor used after closed.
|
||
.INVAL => return error.Unseekable,
|
||
.OVERFLOW => return error.Unseekable,
|
||
.SPIPE => return error.Unseekable,
|
||
.NXIO => return error.Unseekable,
|
||
else => |err| return posix.unexpectedErrno(err),
|
||
}
|
||
},
|
||
}
|
||
}
|
||
}
|
||
|
||
fn fileSeekTo(userdata: ?*anyopaque, file: Io.File, offset: u64) Io.File.SeekError!void {
|
||
const t: *Threaded = @ptrCast(@alignCast(userdata));
|
||
const current_thread = Thread.getCurrent(t);
|
||
const fd = file.handle;
|
||
|
||
if (native_os == .linux and !builtin.link_libc and @sizeOf(usize) == 4) {
|
||
try current_thread.beginSyscall();
|
||
while (true) {
|
||
var result: u64 = undefined;
|
||
switch (posix.errno(posix.system.llseek(fd, offset, &result, posix.SEEK.SET))) {
|
||
.SUCCESS => {
|
||
current_thread.endSyscall();
|
||
return;
|
||
},
|
||
.INTR => {
|
||
try current_thread.checkCancel();
|
||
continue;
|
||
},
|
||
.CANCELED => return current_thread.endSyscallCanceled(),
|
||
else => |e| {
|
||
current_thread.endSyscall();
|
||
switch (e) {
|
||
.BADF => |err| return errnoBug(err), // File descriptor used after closed.
|
||
.INVAL => return error.Unseekable,
|
||
.OVERFLOW => return error.Unseekable,
|
||
.SPIPE => return error.Unseekable,
|
||
.NXIO => return error.Unseekable,
|
||
else => |err| return posix.unexpectedErrno(err),
|
||
}
|
||
},
|
||
}
|
||
}
|
||
}
|
||
|
||
if (native_os == .windows) {
|
||
try current_thread.checkCancel();
|
||
return windows.SetFilePointerEx_BEGIN(fd, offset);
|
||
}
|
||
|
||
if (native_os == .wasi and !builtin.link_libc) while (true) {
|
||
var new_offset: std.os.wasi.filesize_t = undefined;
|
||
try current_thread.beginSyscall();
|
||
switch (std.os.wasi.fd_seek(fd, @bitCast(offset), .SET, &new_offset)) {
|
||
.SUCCESS => {
|
||
current_thread.endSyscall();
|
||
return;
|
||
},
|
||
.INTR => {
|
||
try current_thread.checkCancel();
|
||
continue;
|
||
},
|
||
.CANCELED => return current_thread.endSyscallCanceled(),
|
||
else => |e| {
|
||
current_thread.endSyscall();
|
||
switch (e) {
|
||
.BADF => |err| return errnoBug(err), // File descriptor used after closed.
|
||
.INVAL => return error.Unseekable,
|
||
.OVERFLOW => return error.Unseekable,
|
||
.SPIPE => return error.Unseekable,
|
||
.NXIO => return error.Unseekable,
|
||
.NOTCAPABLE => return error.AccessDenied,
|
||
else => |err| return posix.unexpectedErrno(err),
|
||
}
|
||
},
|
||
}
|
||
};
|
||
|
||
if (posix.SEEK == void) return error.Unseekable;
|
||
|
||
try current_thread.beginSyscall();
|
||
while (true) {
|
||
switch (posix.errno(lseek_sym(fd, @bitCast(offset), posix.SEEK.SET))) {
|
||
.SUCCESS => {
|
||
current_thread.endSyscall();
|
||
return;
|
||
},
|
||
.INTR => {
|
||
try current_thread.checkCancel();
|
||
continue;
|
||
},
|
||
.CANCELED => return current_thread.endSyscallCanceled(),
|
||
else => |e| {
|
||
current_thread.endSyscall();
|
||
switch (e) {
|
||
.BADF => |err| return errnoBug(err), // File descriptor used after closed.
|
||
.INVAL => return error.Unseekable,
|
||
.OVERFLOW => return error.Unseekable,
|
||
.SPIPE => return error.Unseekable,
|
||
.NXIO => return error.Unseekable,
|
||
else => |err| return posix.unexpectedErrno(err),
|
||
}
|
||
},
|
||
}
|
||
}
|
||
}
|
||
|
||
fn openSelfExe(userdata: ?*anyopaque, flags: Io.File.OpenFlags) Io.File.OpenSelfExeError!Io.File {
|
||
const t: *Threaded = @ptrCast(@alignCast(userdata));
|
||
switch (native_os) {
|
||
.linux, .serenity => return dirOpenFilePosix(t, .{ .handle = posix.AT.FDCWD }, "/proc/self/exe", flags),
|
||
.windows => {
|
||
// If ImagePathName is a symlink, then it will contain the path of the symlink,
|
||
// not the path that the symlink points to. However, because we are opening
|
||
// the file, we can let the openFileW call follow the symlink for us.
|
||
const image_path_unicode_string = &windows.peb().ProcessParameters.ImagePathName;
|
||
const image_path_name = image_path_unicode_string.Buffer.?[0 .. image_path_unicode_string.Length / 2 :0];
|
||
const prefixed_path_w = try windows.wToPrefixedFileW(null, image_path_name);
|
||
return dirOpenFileWtf16(t, null, prefixed_path_w.span(), flags);
|
||
},
|
||
else => @panic("TODO implement openSelfExe"),
|
||
}
|
||
}
|
||
|
||
fn fileWritePositional(
|
||
userdata: ?*anyopaque,
|
||
file: Io.File,
|
||
buffer: [][]const u8,
|
||
offset: u64,
|
||
) Io.File.WritePositionalError!usize {
|
||
const t: *Threaded = @ptrCast(@alignCast(userdata));
|
||
_ = t;
|
||
while (true) {
|
||
_ = file;
|
||
_ = buffer;
|
||
_ = offset;
|
||
@panic("TODO implement fileWritePositional");
|
||
}
|
||
}
|
||
|
||
fn fileWriteStreaming(userdata: ?*anyopaque, file: Io.File, buffer: [][]const u8) Io.File.WriteStreamingError!usize {
|
||
const t: *Threaded = @ptrCast(@alignCast(userdata));
|
||
_ = t;
|
||
while (true) {
|
||
_ = file;
|
||
_ = buffer;
|
||
@panic("TODO implement fileWriteStreaming");
|
||
}
|
||
}
|
||
|
||
fn nowPosix(userdata: ?*anyopaque, clock: Io.Clock) Io.Clock.Error!Io.Timestamp {
|
||
const t: *Threaded = @ptrCast(@alignCast(userdata));
|
||
_ = t;
|
||
const clock_id: posix.clockid_t = clockToPosix(clock);
|
||
var tp: posix.timespec = undefined;
|
||
switch (posix.errno(posix.system.clock_gettime(clock_id, &tp))) {
|
||
.SUCCESS => return timestampFromPosix(&tp),
|
||
.INVAL => return error.UnsupportedClock,
|
||
else => |err| return posix.unexpectedErrno(err),
|
||
}
|
||
}
|
||
|
||
const now = switch (native_os) {
|
||
.windows => nowWindows,
|
||
.wasi => nowWasi,
|
||
else => nowPosix,
|
||
};
|
||
|
||
fn nowWindows(userdata: ?*anyopaque, clock: Io.Clock) Io.Clock.Error!Io.Timestamp {
|
||
const t: *Threaded = @ptrCast(@alignCast(userdata));
|
||
_ = t;
|
||
switch (clock) {
|
||
.real => {
|
||
// RtlGetSystemTimePrecise() has a granularity of 100 nanoseconds
|
||
// and uses the NTFS/Windows epoch, which is 1601-01-01.
|
||
const epoch_ns = std.time.epoch.windows * std.time.ns_per_s;
|
||
return .{ .nanoseconds = @as(i96, windows.ntdll.RtlGetSystemTimePrecise()) * 100 + epoch_ns };
|
||
},
|
||
.awake, .boot => {
|
||
// QPC on windows doesn't fail on >= XP/2000 and includes time suspended.
|
||
const qpc = windows.QueryPerformanceCounter();
|
||
// We don't need to cache QPF as it's internally just a memory read to KUSER_SHARED_DATA
|
||
// (a read-only page of info updated and mapped by the kernel to all processes):
|
||
// https://docs.microsoft.com/en-us/windows-hardware/drivers/ddi/ntddk/ns-ntddk-kuser_shared_data
|
||
// https://www.geoffchappell.com/studies/windows/km/ntoskrnl/inc/api/ntexapi_x/kuser_shared_data/index.htm
|
||
const qpf = windows.QueryPerformanceFrequency();
|
||
|
||
// 10Mhz (1 qpc tick every 100ns) is a common enough QPF value that we can optimize on it.
|
||
// https://github.com/microsoft/STL/blob/785143a0c73f030238ef618890fd4d6ae2b3a3a0/stl/inc/chrono#L694-L701
|
||
const common_qpf = 10_000_000;
|
||
if (qpf == common_qpf) return .{ .nanoseconds = qpc * (std.time.ns_per_s / common_qpf) };
|
||
|
||
// Convert to ns using fixed point.
|
||
const scale = @as(u64, std.time.ns_per_s << 32) / @as(u32, @intCast(qpf));
|
||
const result = (@as(u96, qpc) * scale) >> 32;
|
||
return .{ .nanoseconds = @intCast(result) };
|
||
},
|
||
.cpu_process,
|
||
.cpu_thread,
|
||
=> return error.UnsupportedClock,
|
||
}
|
||
}
|
||
|
||
fn nowWasi(userdata: ?*anyopaque, clock: Io.Clock) Io.Clock.Error!Io.Timestamp {
|
||
const t: *Threaded = @ptrCast(@alignCast(userdata));
|
||
_ = t;
|
||
var ns: std.os.wasi.timestamp_t = undefined;
|
||
const err = std.os.wasi.clock_time_get(clockToWasi(clock), 1, &ns);
|
||
if (err != .SUCCESS) return error.Unexpected;
|
||
return .fromNanoseconds(ns);
|
||
}
|
||
|
||
const sleep = switch (native_os) {
|
||
.windows => sleepWindows,
|
||
.wasi => sleepWasi,
|
||
.linux => sleepLinux,
|
||
else => sleepPosix,
|
||
};
|
||
|
||
fn sleepLinux(userdata: ?*anyopaque, timeout: Io.Timeout) Io.SleepError!void {
|
||
const t: *Threaded = @ptrCast(@alignCast(userdata));
|
||
const current_thread = Thread.getCurrent(t);
|
||
const clock_id: posix.clockid_t = clockToPosix(switch (timeout) {
|
||
.none => .awake,
|
||
.duration => |d| d.clock,
|
||
.deadline => |d| d.clock,
|
||
});
|
||
const deadline_nanoseconds: i96 = switch (timeout) {
|
||
.none => std.math.maxInt(i96),
|
||
.duration => |duration| duration.raw.nanoseconds,
|
||
.deadline => |deadline| deadline.raw.nanoseconds,
|
||
};
|
||
var timespec: posix.timespec = timestampToPosix(deadline_nanoseconds);
|
||
try current_thread.beginSyscall();
|
||
while (true) {
|
||
switch (std.os.linux.errno(std.os.linux.clock_nanosleep(clock_id, .{ .ABSTIME = switch (timeout) {
|
||
.none, .duration => false,
|
||
.deadline => true,
|
||
} }, ×pec, ×pec))) {
|
||
.SUCCESS => {
|
||
current_thread.endSyscall();
|
||
return;
|
||
},
|
||
.INTR => {
|
||
try current_thread.checkCancel();
|
||
continue;
|
||
},
|
||
.CANCELED => return current_thread.endSyscallCanceled(),
|
||
else => |e| {
|
||
current_thread.endSyscall();
|
||
switch (e) {
|
||
.INVAL => return error.UnsupportedClock,
|
||
else => |err| return posix.unexpectedErrno(err),
|
||
}
|
||
},
|
||
}
|
||
}
|
||
}
|
||
|
||
fn sleepWindows(userdata: ?*anyopaque, timeout: Io.Timeout) Io.SleepError!void {
|
||
const t: *Threaded = @ptrCast(@alignCast(userdata));
|
||
const current_thread = Thread.getCurrent(t);
|
||
const t_io = ioBasic(t);
|
||
try current_thread.checkCancel();
|
||
const ms = ms: {
|
||
const d = (try timeout.toDurationFromNow(t_io)) orelse
|
||
break :ms std.math.maxInt(windows.DWORD);
|
||
break :ms std.math.lossyCast(windows.DWORD, d.raw.toMilliseconds());
|
||
};
|
||
// TODO: alertable true with checkCancel in a loop plus deadline
|
||
_ = windows.kernel32.SleepEx(ms, windows.FALSE);
|
||
}
|
||
|
||
fn sleepWasi(userdata: ?*anyopaque, timeout: Io.Timeout) Io.SleepError!void {
|
||
const t: *Threaded = @ptrCast(@alignCast(userdata));
|
||
const current_thread = Thread.getCurrent(t);
|
||
const t_io = ioBasic(t);
|
||
const w = std.os.wasi;
|
||
|
||
const clock: w.subscription_clock_t = if (try timeout.toDurationFromNow(t_io)) |d| .{
|
||
.id = clockToWasi(d.clock),
|
||
.timeout = std.math.lossyCast(u64, d.raw.nanoseconds),
|
||
.precision = 0,
|
||
.flags = 0,
|
||
} else .{
|
||
.id = .MONOTONIC,
|
||
.timeout = std.math.maxInt(u64),
|
||
.precision = 0,
|
||
.flags = 0,
|
||
};
|
||
const in: w.subscription_t = .{
|
||
.userdata = 0,
|
||
.u = .{
|
||
.tag = .CLOCK,
|
||
.u = .{ .clock = clock },
|
||
},
|
||
};
|
||
var event: w.event_t = undefined;
|
||
var nevents: usize = undefined;
|
||
try current_thread.beginSyscall();
|
||
_ = w.poll_oneoff(&in, &event, 1, &nevents);
|
||
current_thread.endSyscall();
|
||
}
|
||
|
||
fn sleepPosix(userdata: ?*anyopaque, timeout: Io.Timeout) Io.SleepError!void {
|
||
const t: *Threaded = @ptrCast(@alignCast(userdata));
|
||
const current_thread = Thread.getCurrent(t);
|
||
const t_io = ioBasic(t);
|
||
const sec_type = @typeInfo(posix.timespec).@"struct".fields[0].type;
|
||
const nsec_type = @typeInfo(posix.timespec).@"struct".fields[1].type;
|
||
|
||
var timespec: posix.timespec = t: {
|
||
const d = (try timeout.toDurationFromNow(t_io)) orelse break :t .{
|
||
.sec = std.math.maxInt(sec_type),
|
||
.nsec = std.math.maxInt(nsec_type),
|
||
};
|
||
break :t timestampToPosix(d.raw.toNanoseconds());
|
||
};
|
||
try current_thread.beginSyscall();
|
||
while (true) {
|
||
switch (posix.errno(posix.system.nanosleep(×pec, ×pec))) {
|
||
.INTR => {
|
||
try current_thread.checkCancel();
|
||
continue;
|
||
},
|
||
.CANCELED => return current_thread.endSyscallCanceled(),
|
||
// This prong handles success as well as unexpected errors.
|
||
else => return current_thread.endSyscall(),
|
||
}
|
||
}
|
||
}
|
||
|
||
fn select(userdata: ?*anyopaque, futures: []const *Io.AnyFuture) Io.Cancelable!usize {
|
||
const t: *Threaded = @ptrCast(@alignCast(userdata));
|
||
|
||
var reset_event: ResetEvent = .unset;
|
||
|
||
for (futures, 0..) |future, i| {
|
||
const closure: *AsyncClosure = @ptrCast(@alignCast(future));
|
||
if (@atomicRmw(?*ResetEvent, &closure.select_condition, .Xchg, &reset_event, .seq_cst) == AsyncClosure.done_reset_event) {
|
||
for (futures[0..i]) |cleanup_future| {
|
||
const cleanup_closure: *AsyncClosure = @ptrCast(@alignCast(cleanup_future));
|
||
if (@atomicRmw(?*ResetEvent, &cleanup_closure.select_condition, .Xchg, null, .seq_cst) == AsyncClosure.done_reset_event) {
|
||
cleanup_closure.reset_event.waitUncancelable(); // Ensure no reference to our stack-allocated reset_event.
|
||
}
|
||
}
|
||
return i;
|
||
}
|
||
}
|
||
|
||
try reset_event.wait(t);
|
||
|
||
var result: ?usize = null;
|
||
for (futures, 0..) |future, i| {
|
||
const closure: *AsyncClosure = @ptrCast(@alignCast(future));
|
||
if (@atomicRmw(?*ResetEvent, &closure.select_condition, .Xchg, null, .seq_cst) == AsyncClosure.done_reset_event) {
|
||
closure.reset_event.waitUncancelable(); // Ensure no reference to our stack-allocated reset_event.
|
||
if (result == null) result = i; // In case multiple are ready, return first.
|
||
}
|
||
}
|
||
return result.?;
|
||
}
|
||
|
||
fn netListenIpPosix(
|
||
userdata: ?*anyopaque,
|
||
address: IpAddress,
|
||
options: IpAddress.ListenOptions,
|
||
) IpAddress.ListenError!net.Server {
|
||
if (!have_networking) return error.NetworkDown;
|
||
const t: *Threaded = @ptrCast(@alignCast(userdata));
|
||
const current_thread = Thread.getCurrent(t);
|
||
const family = posixAddressFamily(&address);
|
||
const socket_fd = try openSocketPosix(current_thread, family, .{
|
||
.mode = options.mode,
|
||
.protocol = options.protocol,
|
||
});
|
||
errdefer posix.close(socket_fd);
|
||
|
||
if (options.reuse_address) {
|
||
try setSocketOption(current_thread, socket_fd, posix.SOL.SOCKET, posix.SO.REUSEADDR, 1);
|
||
if (@hasDecl(posix.SO, "REUSEPORT"))
|
||
try setSocketOption(current_thread, socket_fd, posix.SOL.SOCKET, posix.SO.REUSEPORT, 1);
|
||
}
|
||
|
||
var storage: PosixAddress = undefined;
|
||
var addr_len = addressToPosix(&address, &storage);
|
||
try posixBind(current_thread, socket_fd, &storage.any, addr_len);
|
||
|
||
try current_thread.beginSyscall();
|
||
while (true) {
|
||
switch (posix.errno(posix.system.listen(socket_fd, options.kernel_backlog))) {
|
||
.SUCCESS => {
|
||
current_thread.endSyscall();
|
||
break;
|
||
},
|
||
.INTR => {
|
||
try current_thread.checkCancel();
|
||
continue;
|
||
},
|
||
.CANCELED => return current_thread.endSyscallCanceled(),
|
||
else => |e| {
|
||
current_thread.endSyscall();
|
||
switch (e) {
|
||
.ADDRINUSE => return error.AddressInUse,
|
||
.BADF => |err| return errnoBug(err), // File descriptor used after closed.
|
||
else => |err| return posix.unexpectedErrno(err),
|
||
}
|
||
},
|
||
}
|
||
}
|
||
|
||
try posixGetSockName(current_thread, socket_fd, &storage.any, &addr_len);
|
||
return .{
|
||
.socket = .{
|
||
.handle = socket_fd,
|
||
.address = addressFromPosix(&storage),
|
||
},
|
||
};
|
||
}
|
||
|
||
fn netListenIpWindows(
|
||
userdata: ?*anyopaque,
|
||
address: IpAddress,
|
||
options: IpAddress.ListenOptions,
|
||
) IpAddress.ListenError!net.Server {
|
||
if (!have_networking) return error.NetworkDown;
|
||
const t: *Threaded = @ptrCast(@alignCast(userdata));
|
||
const current_thread = Thread.getCurrent(t);
|
||
const family = posixAddressFamily(&address);
|
||
const socket_handle = try openSocketWsa(t, current_thread, family, .{
|
||
.mode = options.mode,
|
||
.protocol = options.protocol,
|
||
});
|
||
errdefer closeSocketWindows(socket_handle);
|
||
|
||
if (options.reuse_address)
|
||
try setSocketOptionWsa(t, socket_handle, posix.SOL.SOCKET, posix.SO.REUSEADDR, 1);
|
||
|
||
var storage: WsaAddress = undefined;
|
||
var addr_len = addressToWsa(&address, &storage);
|
||
|
||
try current_thread.beginSyscall();
|
||
while (true) {
|
||
const rc = ws2_32.bind(socket_handle, &storage.any, addr_len);
|
||
if (rc != ws2_32.SOCKET_ERROR) {
|
||
current_thread.endSyscall();
|
||
break;
|
||
}
|
||
switch (ws2_32.WSAGetLastError()) {
|
||
.EINTR => {
|
||
try current_thread.checkCancel();
|
||
continue;
|
||
},
|
||
.NOTINITIALISED => {
|
||
try initializeWsa(t);
|
||
try current_thread.checkCancel();
|
||
continue;
|
||
},
|
||
else => |e| {
|
||
current_thread.endSyscall();
|
||
switch (e) {
|
||
.ECANCELLED, .E_CANCELLED, .OPERATION_ABORTED => return error.Canceled,
|
||
.EADDRINUSE => return error.AddressInUse,
|
||
.EADDRNOTAVAIL => return error.AddressUnavailable,
|
||
.ENOTSOCK => |err| return wsaErrorBug(err),
|
||
.EFAULT => |err| return wsaErrorBug(err),
|
||
.EINVAL => |err| return wsaErrorBug(err),
|
||
.ENOBUFS => return error.SystemResources,
|
||
.ENETDOWN => return error.NetworkDown,
|
||
else => |err| return windows.unexpectedWSAError(err),
|
||
}
|
||
},
|
||
}
|
||
}
|
||
|
||
try current_thread.beginSyscall();
|
||
while (true) {
|
||
const rc = ws2_32.listen(socket_handle, options.kernel_backlog);
|
||
if (rc != ws2_32.SOCKET_ERROR) {
|
||
current_thread.endSyscall();
|
||
break;
|
||
}
|
||
switch (ws2_32.WSAGetLastError()) {
|
||
.EINTR => {
|
||
try current_thread.checkCancel();
|
||
continue;
|
||
},
|
||
.NOTINITIALISED => {
|
||
try initializeWsa(t);
|
||
try current_thread.checkCancel();
|
||
continue;
|
||
},
|
||
else => |e| {
|
||
current_thread.endSyscall();
|
||
switch (e) {
|
||
.ECANCELLED, .E_CANCELLED, .OPERATION_ABORTED => return error.Canceled,
|
||
.ENETDOWN => return error.NetworkDown,
|
||
.EADDRINUSE => return error.AddressInUse,
|
||
.EISCONN => |err| return wsaErrorBug(err),
|
||
.EINVAL => |err| return wsaErrorBug(err),
|
||
.EMFILE, .ENOBUFS => return error.SystemResources,
|
||
.ENOTSOCK => |err| return wsaErrorBug(err),
|
||
.EOPNOTSUPP => |err| return wsaErrorBug(err),
|
||
.EINPROGRESS => |err| return wsaErrorBug(err),
|
||
else => |err| return windows.unexpectedWSAError(err),
|
||
}
|
||
},
|
||
}
|
||
}
|
||
|
||
try wsaGetSockName(t, current_thread, socket_handle, &storage.any, &addr_len);
|
||
|
||
return .{
|
||
.socket = .{
|
||
.handle = socket_handle,
|
||
.address = addressFromWsa(&storage),
|
||
},
|
||
};
|
||
}
|
||
|
||
fn netListenIpUnavailable(
|
||
userdata: ?*anyopaque,
|
||
address: IpAddress,
|
||
options: IpAddress.ListenOptions,
|
||
) IpAddress.ListenError!net.Server {
|
||
_ = userdata;
|
||
_ = address;
|
||
_ = options;
|
||
return error.NetworkDown;
|
||
}
|
||
|
||
fn netListenUnixPosix(
|
||
userdata: ?*anyopaque,
|
||
address: *const net.UnixAddress,
|
||
options: net.UnixAddress.ListenOptions,
|
||
) net.UnixAddress.ListenError!net.Socket.Handle {
|
||
if (!net.has_unix_sockets) return error.AddressFamilyUnsupported;
|
||
const t: *Threaded = @ptrCast(@alignCast(userdata));
|
||
const current_thread = Thread.getCurrent(t);
|
||
const socket_fd = openSocketPosix(current_thread, posix.AF.UNIX, .{ .mode = .stream }) catch |err| switch (err) {
|
||
error.ProtocolUnsupportedBySystem => return error.AddressFamilyUnsupported,
|
||
error.ProtocolUnsupportedByAddressFamily => return error.AddressFamilyUnsupported,
|
||
error.SocketModeUnsupported => return error.AddressFamilyUnsupported,
|
||
error.OptionUnsupported => return error.Unexpected,
|
||
else => |e| return e,
|
||
};
|
||
errdefer posix.close(socket_fd);
|
||
|
||
var storage: UnixAddress = undefined;
|
||
const addr_len = addressUnixToPosix(address, &storage);
|
||
try posixBindUnix(current_thread, socket_fd, &storage.any, addr_len);
|
||
|
||
try current_thread.beginSyscall();
|
||
while (true) {
|
||
switch (posix.errno(posix.system.listen(socket_fd, options.kernel_backlog))) {
|
||
.SUCCESS => {
|
||
current_thread.endSyscall();
|
||
break;
|
||
},
|
||
.INTR => {
|
||
try current_thread.checkCancel();
|
||
continue;
|
||
},
|
||
.CANCELED => return current_thread.endSyscallCanceled(),
|
||
else => |e| {
|
||
current_thread.endSyscall();
|
||
switch (e) {
|
||
.ADDRINUSE => return error.AddressInUse,
|
||
.BADF => |err| return errnoBug(err), // File descriptor used after closed.
|
||
else => |err| return posix.unexpectedErrno(err),
|
||
}
|
||
},
|
||
}
|
||
}
|
||
|
||
return socket_fd;
|
||
}
|
||
|
||
fn netListenUnixWindows(
|
||
userdata: ?*anyopaque,
|
||
address: *const net.UnixAddress,
|
||
options: net.UnixAddress.ListenOptions,
|
||
) net.UnixAddress.ListenError!net.Socket.Handle {
|
||
if (!net.has_unix_sockets) return error.AddressFamilyUnsupported;
|
||
const t: *Threaded = @ptrCast(@alignCast(userdata));
|
||
const current_thread = Thread.getCurrent(t);
|
||
|
||
const socket_handle = openSocketWsa(t, current_thread, posix.AF.UNIX, .{ .mode = .stream }) catch |err| switch (err) {
|
||
error.ProtocolUnsupportedByAddressFamily => return error.AddressFamilyUnsupported,
|
||
else => |e| return e,
|
||
};
|
||
errdefer closeSocketWindows(socket_handle);
|
||
|
||
var storage: WsaAddress = undefined;
|
||
const addr_len = addressUnixToWsa(address, &storage);
|
||
|
||
try current_thread.beginSyscall();
|
||
while (true) {
|
||
const rc = ws2_32.bind(socket_handle, &storage.any, addr_len);
|
||
if (rc != ws2_32.SOCKET_ERROR) break;
|
||
switch (ws2_32.WSAGetLastError()) {
|
||
.EINTR => {
|
||
try current_thread.checkCancel();
|
||
continue;
|
||
},
|
||
.NOTINITIALISED => {
|
||
try initializeWsa(t);
|
||
try current_thread.checkCancel();
|
||
continue;
|
||
},
|
||
else => |e| {
|
||
current_thread.endSyscall();
|
||
switch (e) {
|
||
.ECANCELLED, .E_CANCELLED, .OPERATION_ABORTED => return error.Canceled,
|
||
.EADDRINUSE => return error.AddressInUse,
|
||
.EADDRNOTAVAIL => return error.AddressUnavailable,
|
||
.ENOTSOCK => |err| return wsaErrorBug(err),
|
||
.EFAULT => |err| return wsaErrorBug(err),
|
||
.EINVAL => |err| return wsaErrorBug(err),
|
||
.ENOBUFS => return error.SystemResources,
|
||
.ENETDOWN => return error.NetworkDown,
|
||
else => |err| return windows.unexpectedWSAError(err),
|
||
}
|
||
},
|
||
}
|
||
}
|
||
|
||
while (true) {
|
||
try current_thread.checkCancel();
|
||
const rc = ws2_32.listen(socket_handle, options.kernel_backlog);
|
||
if (rc != ws2_32.SOCKET_ERROR) {
|
||
current_thread.endSyscall();
|
||
return socket_handle;
|
||
}
|
||
switch (ws2_32.WSAGetLastError()) {
|
||
.EINTR => continue,
|
||
.NOTINITIALISED => {
|
||
try initializeWsa(t);
|
||
continue;
|
||
},
|
||
else => |e| {
|
||
current_thread.endSyscall();
|
||
switch (e) {
|
||
.ECANCELLED, .E_CANCELLED, .OPERATION_ABORTED => return error.Canceled,
|
||
.ENETDOWN => return error.NetworkDown,
|
||
.EADDRINUSE => return error.AddressInUse,
|
||
.EISCONN => |err| return wsaErrorBug(err),
|
||
.EINVAL => |err| return wsaErrorBug(err),
|
||
.EMFILE, .ENOBUFS => return error.SystemResources,
|
||
.ENOTSOCK => |err| return wsaErrorBug(err),
|
||
.EOPNOTSUPP => |err| return wsaErrorBug(err),
|
||
.EINPROGRESS => |err| return wsaErrorBug(err),
|
||
else => |err| return windows.unexpectedWSAError(err),
|
||
}
|
||
},
|
||
}
|
||
}
|
||
}
|
||
|
||
fn netListenUnixUnavailable(
|
||
userdata: ?*anyopaque,
|
||
address: *const net.UnixAddress,
|
||
options: net.UnixAddress.ListenOptions,
|
||
) net.UnixAddress.ListenError!net.Socket.Handle {
|
||
_ = userdata;
|
||
_ = address;
|
||
_ = options;
|
||
return error.AddressFamilyUnsupported;
|
||
}
|
||
|
||
fn posixBindUnix(
|
||
current_thread: *Thread,
|
||
fd: posix.socket_t,
|
||
addr: *const posix.sockaddr,
|
||
addr_len: posix.socklen_t,
|
||
) !void {
|
||
try current_thread.beginSyscall();
|
||
while (true) {
|
||
switch (posix.errno(posix.system.bind(fd, addr, addr_len))) {
|
||
.SUCCESS => {
|
||
current_thread.endSyscall();
|
||
break;
|
||
},
|
||
.INTR => {
|
||
try current_thread.checkCancel();
|
||
continue;
|
||
},
|
||
.CANCELED => return current_thread.endSyscallCanceled(),
|
||
else => |e| {
|
||
current_thread.endSyscall();
|
||
switch (e) {
|
||
.ACCES => return error.AccessDenied,
|
||
.ADDRINUSE => return error.AddressInUse,
|
||
.AFNOSUPPORT => return error.AddressFamilyUnsupported,
|
||
.ADDRNOTAVAIL => return error.AddressUnavailable,
|
||
.NOMEM => return error.SystemResources,
|
||
|
||
.LOOP => return error.SymLinkLoop,
|
||
.NOENT => return error.FileNotFound,
|
||
.NOTDIR => return error.NotDir,
|
||
.ROFS => return error.ReadOnlyFileSystem,
|
||
.PERM => return error.PermissionDenied,
|
||
|
||
.BADF => |err| return errnoBug(err), // File descriptor used after closed.
|
||
.INVAL => |err| return errnoBug(err), // invalid parameters
|
||
.NOTSOCK => |err| return errnoBug(err), // invalid `sockfd`
|
||
.FAULT => |err| return errnoBug(err), // invalid `addr` pointer
|
||
.NAMETOOLONG => |err| return errnoBug(err),
|
||
else => |err| return posix.unexpectedErrno(err),
|
||
}
|
||
},
|
||
}
|
||
}
|
||
}
|
||
|
||
fn posixBind(
|
||
current_thread: *Thread,
|
||
socket_fd: posix.socket_t,
|
||
addr: *const posix.sockaddr,
|
||
addr_len: posix.socklen_t,
|
||
) !void {
|
||
try current_thread.beginSyscall();
|
||
while (true) {
|
||
switch (posix.errno(posix.system.bind(socket_fd, addr, addr_len))) {
|
||
.SUCCESS => {
|
||
current_thread.endSyscall();
|
||
break;
|
||
},
|
||
.INTR => {
|
||
try current_thread.checkCancel();
|
||
continue;
|
||
},
|
||
.CANCELED => return current_thread.endSyscallCanceled(),
|
||
else => |e| {
|
||
current_thread.endSyscall();
|
||
switch (e) {
|
||
.ADDRINUSE => return error.AddressInUse,
|
||
.BADF => |err| return errnoBug(err), // File descriptor used after closed.
|
||
.INVAL => |err| return errnoBug(err), // invalid parameters
|
||
.NOTSOCK => |err| return errnoBug(err), // invalid `sockfd`
|
||
.AFNOSUPPORT => return error.AddressFamilyUnsupported,
|
||
.ADDRNOTAVAIL => return error.AddressUnavailable,
|
||
.FAULT => |err| return errnoBug(err), // invalid `addr` pointer
|
||
.NOMEM => return error.SystemResources,
|
||
else => |err| return posix.unexpectedErrno(err),
|
||
}
|
||
},
|
||
}
|
||
}
|
||
}
|
||
|
||
fn posixConnect(
|
||
current_thread: *Thread,
|
||
socket_fd: posix.socket_t,
|
||
addr: *const posix.sockaddr,
|
||
addr_len: posix.socklen_t,
|
||
) !void {
|
||
try current_thread.beginSyscall();
|
||
while (true) {
|
||
switch (posix.errno(posix.system.connect(socket_fd, addr, addr_len))) {
|
||
.SUCCESS => {
|
||
current_thread.endSyscall();
|
||
return;
|
||
},
|
||
.INTR => {
|
||
try current_thread.checkCancel();
|
||
continue;
|
||
},
|
||
.CANCELED => return current_thread.endSyscallCanceled(),
|
||
else => |e| {
|
||
current_thread.endSyscall();
|
||
switch (e) {
|
||
.ADDRNOTAVAIL => return error.AddressUnavailable,
|
||
.AFNOSUPPORT => return error.AddressFamilyUnsupported,
|
||
.AGAIN, .INPROGRESS => return error.WouldBlock,
|
||
.ALREADY => return error.ConnectionPending,
|
||
.BADF => |err| return errnoBug(err), // File descriptor used after closed.
|
||
.CONNREFUSED => return error.ConnectionRefused,
|
||
.CONNRESET => return error.ConnectionResetByPeer,
|
||
.FAULT => |err| return errnoBug(err),
|
||
.ISCONN => |err| return errnoBug(err),
|
||
.HOSTUNREACH => return error.HostUnreachable,
|
||
.NETUNREACH => return error.NetworkUnreachable,
|
||
.NOTSOCK => |err| return errnoBug(err),
|
||
.PROTOTYPE => |err| return errnoBug(err),
|
||
.TIMEDOUT => return error.Timeout,
|
||
.CONNABORTED => |err| return errnoBug(err),
|
||
.ACCES => return error.AccessDenied,
|
||
.PERM => |err| return errnoBug(err),
|
||
.NOENT => |err| return errnoBug(err),
|
||
.NETDOWN => return error.NetworkDown,
|
||
else => |err| return posix.unexpectedErrno(err),
|
||
}
|
||
},
|
||
}
|
||
}
|
||
}
|
||
|
||
fn posixConnectUnix(
|
||
current_thread: *Thread,
|
||
fd: posix.socket_t,
|
||
addr: *const posix.sockaddr,
|
||
addr_len: posix.socklen_t,
|
||
) !void {
|
||
try current_thread.beginSyscall();
|
||
while (true) {
|
||
switch (posix.errno(posix.system.connect(fd, addr, addr_len))) {
|
||
.SUCCESS => {
|
||
current_thread.endSyscall();
|
||
return;
|
||
},
|
||
.INTR => {
|
||
try current_thread.checkCancel();
|
||
continue;
|
||
},
|
||
.CANCELED => return current_thread.endSyscallCanceled(),
|
||
else => |e| {
|
||
current_thread.endSyscall();
|
||
switch (e) {
|
||
.AFNOSUPPORT => return error.AddressFamilyUnsupported,
|
||
.AGAIN => return error.WouldBlock,
|
||
.INPROGRESS => return error.WouldBlock,
|
||
.ACCES => return error.AccessDenied,
|
||
|
||
.LOOP => return error.SymLinkLoop,
|
||
.NOENT => return error.FileNotFound,
|
||
.NOTDIR => return error.NotDir,
|
||
.ROFS => return error.ReadOnlyFileSystem,
|
||
.PERM => return error.PermissionDenied,
|
||
|
||
.BADF => |err| return errnoBug(err), // File descriptor used after closed.
|
||
.CONNABORTED => |err| return errnoBug(err),
|
||
.FAULT => |err| return errnoBug(err),
|
||
.ISCONN => |err| return errnoBug(err),
|
||
.NOTSOCK => |err| return errnoBug(err),
|
||
.PROTOTYPE => |err| return errnoBug(err),
|
||
else => |err| return posix.unexpectedErrno(err),
|
||
}
|
||
},
|
||
}
|
||
}
|
||
}
|
||
|
||
fn posixGetSockName(
|
||
current_thread: *Thread,
|
||
socket_fd: posix.fd_t,
|
||
addr: *posix.sockaddr,
|
||
addr_len: *posix.socklen_t,
|
||
) !void {
|
||
try current_thread.beginSyscall();
|
||
while (true) {
|
||
switch (posix.errno(posix.system.getsockname(socket_fd, addr, addr_len))) {
|
||
.SUCCESS => {
|
||
current_thread.endSyscall();
|
||
break;
|
||
},
|
||
.INTR => {
|
||
try current_thread.checkCancel();
|
||
continue;
|
||
},
|
||
.CANCELED => return current_thread.endSyscallCanceled(),
|
||
else => |e| {
|
||
current_thread.endSyscall();
|
||
switch (e) {
|
||
.BADF => |err| return errnoBug(err), // File descriptor used after closed.
|
||
.FAULT => |err| return errnoBug(err),
|
||
.INVAL => |err| return errnoBug(err), // invalid parameters
|
||
.NOTSOCK => |err| return errnoBug(err), // always a race condition
|
||
.NOBUFS => return error.SystemResources,
|
||
else => |err| return posix.unexpectedErrno(err),
|
||
}
|
||
},
|
||
}
|
||
}
|
||
}
|
||
|
||
fn wsaGetSockName(
|
||
t: *Threaded,
|
||
current_thread: *Thread,
|
||
handle: ws2_32.SOCKET,
|
||
addr: *ws2_32.sockaddr,
|
||
addr_len: *i32,
|
||
) !void {
|
||
try current_thread.beginSyscall();
|
||
while (true) {
|
||
const rc = ws2_32.getsockname(handle, addr, addr_len);
|
||
if (rc != ws2_32.SOCKET_ERROR) {
|
||
current_thread.endSyscall();
|
||
return;
|
||
}
|
||
switch (ws2_32.WSAGetLastError()) {
|
||
.EINTR => {
|
||
try current_thread.checkCancel();
|
||
continue;
|
||
},
|
||
.NOTINITIALISED => {
|
||
try initializeWsa(t);
|
||
try current_thread.checkCancel();
|
||
continue;
|
||
},
|
||
else => |e| {
|
||
current_thread.endSyscall();
|
||
switch (e) {
|
||
.ECANCELLED, .E_CANCELLED, .OPERATION_ABORTED => return error.Canceled,
|
||
.ENETDOWN => return error.NetworkDown,
|
||
.EFAULT => |err| return wsaErrorBug(err),
|
||
.ENOTSOCK => |err| return wsaErrorBug(err),
|
||
.EINVAL => |err| return wsaErrorBug(err),
|
||
else => |err| return windows.unexpectedWSAError(err),
|
||
}
|
||
},
|
||
}
|
||
}
|
||
}
|
||
|
||
fn setSocketOption(current_thread: *Thread, fd: posix.fd_t, level: i32, opt_name: u32, option: u32) !void {
|
||
const o: []const u8 = @ptrCast(&option);
|
||
try current_thread.beginSyscall();
|
||
while (true) {
|
||
switch (posix.errno(posix.system.setsockopt(fd, level, opt_name, o.ptr, @intCast(o.len)))) {
|
||
.SUCCESS => {
|
||
current_thread.endSyscall();
|
||
return;
|
||
},
|
||
.INTR => {
|
||
try current_thread.checkCancel();
|
||
continue;
|
||
},
|
||
.CANCELED => return current_thread.endSyscallCanceled(),
|
||
else => |e| {
|
||
current_thread.endSyscall();
|
||
switch (e) {
|
||
.BADF => |err| return errnoBug(err), // File descriptor used after closed.
|
||
.NOTSOCK => |err| return errnoBug(err),
|
||
.INVAL => |err| return errnoBug(err),
|
||
.FAULT => |err| return errnoBug(err),
|
||
else => |err| return posix.unexpectedErrno(err),
|
||
}
|
||
},
|
||
}
|
||
}
|
||
}
|
||
|
||
fn setSocketOptionWsa(t: *Threaded, socket: Io.net.Socket.Handle, level: i32, opt_name: u32, option: u32) !void {
|
||
const o: []const u8 = @ptrCast(&option);
|
||
const rc = ws2_32.setsockopt(socket, level, @bitCast(opt_name), o.ptr, @intCast(o.len));
|
||
while (true) {
|
||
if (rc != ws2_32.SOCKET_ERROR) return;
|
||
switch (ws2_32.WSAGetLastError()) {
|
||
.EINTR => continue,
|
||
.ECANCELLED, .E_CANCELLED, .OPERATION_ABORTED => return error.Canceled,
|
||
.NOTINITIALISED => {
|
||
try initializeWsa(t);
|
||
continue;
|
||
},
|
||
.ENETDOWN => return error.NetworkDown,
|
||
.EFAULT => |err| return wsaErrorBug(err),
|
||
.ENOTSOCK => |err| return wsaErrorBug(err),
|
||
.EINVAL => |err| return wsaErrorBug(err),
|
||
else => |err| return windows.unexpectedWSAError(err),
|
||
}
|
||
}
|
||
}
|
||
|
||
fn netConnectIpPosix(
|
||
userdata: ?*anyopaque,
|
||
address: *const IpAddress,
|
||
options: IpAddress.ConnectOptions,
|
||
) IpAddress.ConnectError!net.Stream {
|
||
if (!have_networking) return error.NetworkDown;
|
||
if (options.timeout != .none) @panic("TODO implement netConnectIpPosix with timeout");
|
||
const t: *Threaded = @ptrCast(@alignCast(userdata));
|
||
const current_thread = Thread.getCurrent(t);
|
||
const family = posixAddressFamily(address);
|
||
const socket_fd = try openSocketPosix(current_thread, family, .{
|
||
.mode = options.mode,
|
||
.protocol = options.protocol,
|
||
});
|
||
errdefer posix.close(socket_fd);
|
||
var storage: PosixAddress = undefined;
|
||
var addr_len = addressToPosix(address, &storage);
|
||
try posixConnect(current_thread, socket_fd, &storage.any, addr_len);
|
||
try posixGetSockName(current_thread, socket_fd, &storage.any, &addr_len);
|
||
return .{ .socket = .{
|
||
.handle = socket_fd,
|
||
.address = addressFromPosix(&storage),
|
||
} };
|
||
}
|
||
|
||
fn netConnectIpWindows(
|
||
userdata: ?*anyopaque,
|
||
address: *const IpAddress,
|
||
options: IpAddress.ConnectOptions,
|
||
) IpAddress.ConnectError!net.Stream {
|
||
if (!have_networking) return error.NetworkDown;
|
||
if (options.timeout != .none) @panic("TODO implement netConnectIpWindows with timeout");
|
||
const t: *Threaded = @ptrCast(@alignCast(userdata));
|
||
const current_thread = Thread.getCurrent(t);
|
||
const family = posixAddressFamily(address);
|
||
const socket_handle = try openSocketWsa(t, current_thread, family, .{
|
||
.mode = options.mode,
|
||
.protocol = options.protocol,
|
||
});
|
||
errdefer closeSocketWindows(socket_handle);
|
||
|
||
var storage: WsaAddress = undefined;
|
||
var addr_len = addressToWsa(address, &storage);
|
||
|
||
try current_thread.beginSyscall();
|
||
while (true) {
|
||
const rc = ws2_32.connect(socket_handle, &storage.any, addr_len);
|
||
if (rc != ws2_32.SOCKET_ERROR) {
|
||
current_thread.endSyscall();
|
||
break;
|
||
}
|
||
switch (ws2_32.WSAGetLastError()) {
|
||
.EINTR => {
|
||
try current_thread.checkCancel();
|
||
continue;
|
||
},
|
||
.NOTINITIALISED => {
|
||
try initializeWsa(t);
|
||
try current_thread.checkCancel();
|
||
continue;
|
||
},
|
||
else => |e| {
|
||
current_thread.endSyscall();
|
||
switch (e) {
|
||
.ECANCELLED, .E_CANCELLED, .OPERATION_ABORTED => return error.Canceled,
|
||
.EADDRNOTAVAIL => return error.AddressUnavailable,
|
||
.ECONNREFUSED => return error.ConnectionRefused,
|
||
.ECONNRESET => return error.ConnectionResetByPeer,
|
||
.ETIMEDOUT => return error.Timeout,
|
||
.EHOSTUNREACH => return error.HostUnreachable,
|
||
.ENETUNREACH => return error.NetworkUnreachable,
|
||
.EFAULT => |err| return wsaErrorBug(err),
|
||
.EINVAL => |err| return wsaErrorBug(err),
|
||
.EISCONN => |err| return wsaErrorBug(err),
|
||
.ENOTSOCK => |err| return wsaErrorBug(err),
|
||
.EWOULDBLOCK => return error.WouldBlock,
|
||
.EACCES => return error.AccessDenied,
|
||
.ENOBUFS => return error.SystemResources,
|
||
.EAFNOSUPPORT => return error.AddressFamilyUnsupported,
|
||
else => |err| return windows.unexpectedWSAError(err),
|
||
}
|
||
},
|
||
}
|
||
}
|
||
|
||
try wsaGetSockName(t, current_thread, socket_handle, &storage.any, &addr_len);
|
||
|
||
return .{ .socket = .{
|
||
.handle = socket_handle,
|
||
.address = addressFromWsa(&storage),
|
||
} };
|
||
}
|
||
|
||
fn netConnectIpUnavailable(
|
||
userdata: ?*anyopaque,
|
||
address: *const IpAddress,
|
||
options: IpAddress.ConnectOptions,
|
||
) IpAddress.ConnectError!net.Stream {
|
||
_ = userdata;
|
||
_ = address;
|
||
_ = options;
|
||
return error.NetworkDown;
|
||
}
|
||
|
||
fn netConnectUnixPosix(
|
||
userdata: ?*anyopaque,
|
||
address: *const net.UnixAddress,
|
||
) net.UnixAddress.ConnectError!net.Socket.Handle {
|
||
if (!net.has_unix_sockets) return error.AddressFamilyUnsupported;
|
||
const t: *Threaded = @ptrCast(@alignCast(userdata));
|
||
const current_thread = Thread.getCurrent(t);
|
||
const socket_fd = openSocketPosix(current_thread, posix.AF.UNIX, .{ .mode = .stream }) catch |err| switch (err) {
|
||
error.OptionUnsupported => return error.Unexpected,
|
||
else => |e| return e,
|
||
};
|
||
errdefer posix.close(socket_fd);
|
||
var storage: UnixAddress = undefined;
|
||
const addr_len = addressUnixToPosix(address, &storage);
|
||
try posixConnectUnix(current_thread, socket_fd, &storage.any, addr_len);
|
||
return socket_fd;
|
||
}
|
||
|
||
fn netConnectUnixWindows(
|
||
userdata: ?*anyopaque,
|
||
address: *const net.UnixAddress,
|
||
) net.UnixAddress.ConnectError!net.Socket.Handle {
|
||
if (!net.has_unix_sockets) return error.AddressFamilyUnsupported;
|
||
const t: *Threaded = @ptrCast(@alignCast(userdata));
|
||
const current_thread = Thread.getCurrent(t);
|
||
|
||
const socket_handle = try openSocketWsa(t, current_thread, posix.AF.UNIX, .{ .mode = .stream });
|
||
errdefer closeSocketWindows(socket_handle);
|
||
var storage: WsaAddress = undefined;
|
||
const addr_len = addressUnixToWsa(address, &storage);
|
||
|
||
while (true) {
|
||
const rc = ws2_32.connect(socket_handle, &storage.any, addr_len);
|
||
if (rc != ws2_32.SOCKET_ERROR) break;
|
||
switch (ws2_32.WSAGetLastError()) {
|
||
.EINTR => continue,
|
||
.ECANCELLED, .E_CANCELLED, .OPERATION_ABORTED => return error.Canceled,
|
||
.NOTINITIALISED => {
|
||
try initializeWsa(t);
|
||
continue;
|
||
},
|
||
|
||
.ECONNREFUSED => return error.FileNotFound,
|
||
.EFAULT => |err| return wsaErrorBug(err),
|
||
.EINVAL => |err| return wsaErrorBug(err),
|
||
.EISCONN => |err| return wsaErrorBug(err),
|
||
.ENOTSOCK => |err| return wsaErrorBug(err),
|
||
.EWOULDBLOCK => return error.WouldBlock,
|
||
.EACCES => return error.AccessDenied,
|
||
.ENOBUFS => return error.SystemResources,
|
||
.EAFNOSUPPORT => return error.AddressFamilyUnsupported,
|
||
else => |err| return windows.unexpectedWSAError(err),
|
||
}
|
||
}
|
||
|
||
return socket_handle;
|
||
}
|
||
|
||
fn netConnectUnixUnavailable(
|
||
userdata: ?*anyopaque,
|
||
address: *const net.UnixAddress,
|
||
) net.UnixAddress.ConnectError!net.Socket.Handle {
|
||
_ = userdata;
|
||
_ = address;
|
||
return error.AddressFamilyUnsupported;
|
||
}
|
||
|
||
fn netBindIpPosix(
|
||
userdata: ?*anyopaque,
|
||
address: *const IpAddress,
|
||
options: IpAddress.BindOptions,
|
||
) IpAddress.BindError!net.Socket {
|
||
if (!have_networking) return error.NetworkDown;
|
||
const t: *Threaded = @ptrCast(@alignCast(userdata));
|
||
const current_thread = Thread.getCurrent(t);
|
||
const family = posixAddressFamily(address);
|
||
const socket_fd = try openSocketPosix(current_thread, family, options);
|
||
errdefer posix.close(socket_fd);
|
||
var storage: PosixAddress = undefined;
|
||
var addr_len = addressToPosix(address, &storage);
|
||
try posixBind(current_thread, socket_fd, &storage.any, addr_len);
|
||
try posixGetSockName(current_thread, socket_fd, &storage.any, &addr_len);
|
||
return .{
|
||
.handle = socket_fd,
|
||
.address = addressFromPosix(&storage),
|
||
};
|
||
}
|
||
|
||
fn netBindIpWindows(
|
||
userdata: ?*anyopaque,
|
||
address: *const IpAddress,
|
||
options: IpAddress.BindOptions,
|
||
) IpAddress.BindError!net.Socket {
|
||
if (!have_networking) return error.NetworkDown;
|
||
const t: *Threaded = @ptrCast(@alignCast(userdata));
|
||
const current_thread = Thread.getCurrent(t);
|
||
const family = posixAddressFamily(address);
|
||
const socket_handle = try openSocketWsa(t, current_thread, family, .{
|
||
.mode = options.mode,
|
||
.protocol = options.protocol,
|
||
});
|
||
errdefer closeSocketWindows(socket_handle);
|
||
|
||
var storage: WsaAddress = undefined;
|
||
var addr_len = addressToWsa(address, &storage);
|
||
|
||
try current_thread.beginSyscall();
|
||
while (true) {
|
||
const rc = ws2_32.bind(socket_handle, &storage.any, addr_len);
|
||
if (rc != ws2_32.SOCKET_ERROR) {
|
||
current_thread.endSyscall();
|
||
break;
|
||
}
|
||
switch (ws2_32.WSAGetLastError()) {
|
||
.EINTR => {
|
||
try current_thread.checkCancel();
|
||
continue;
|
||
},
|
||
.NOTINITIALISED => {
|
||
try initializeWsa(t);
|
||
try current_thread.checkCancel();
|
||
continue;
|
||
},
|
||
else => |e| {
|
||
current_thread.endSyscall();
|
||
switch (e) {
|
||
.ECANCELLED, .E_CANCELLED, .OPERATION_ABORTED => return error.Canceled,
|
||
.EADDRINUSE => return error.AddressInUse,
|
||
.EADDRNOTAVAIL => return error.AddressUnavailable,
|
||
.ENOTSOCK => |err| return wsaErrorBug(err),
|
||
.EFAULT => |err| return wsaErrorBug(err),
|
||
.EINVAL => |err| return wsaErrorBug(err),
|
||
.ENOBUFS => return error.SystemResources,
|
||
.ENETDOWN => return error.NetworkDown,
|
||
else => |err| return windows.unexpectedWSAError(err),
|
||
}
|
||
},
|
||
}
|
||
}
|
||
|
||
try wsaGetSockName(t, current_thread, socket_handle, &storage.any, &addr_len);
|
||
|
||
return .{
|
||
.handle = socket_handle,
|
||
.address = addressFromWsa(&storage),
|
||
};
|
||
}
|
||
|
||
fn netBindIpUnavailable(
|
||
userdata: ?*anyopaque,
|
||
address: *const IpAddress,
|
||
options: IpAddress.BindOptions,
|
||
) IpAddress.BindError!net.Socket {
|
||
_ = userdata;
|
||
_ = address;
|
||
_ = options;
|
||
return error.NetworkDown;
|
||
}
|
||
|
||
fn openSocketPosix(
|
||
current_thread: *Thread,
|
||
family: posix.sa_family_t,
|
||
options: IpAddress.BindOptions,
|
||
) error{
|
||
AddressFamilyUnsupported,
|
||
ProtocolUnsupportedBySystem,
|
||
ProcessFdQuotaExceeded,
|
||
SystemFdQuotaExceeded,
|
||
SystemResources,
|
||
ProtocolUnsupportedByAddressFamily,
|
||
SocketModeUnsupported,
|
||
OptionUnsupported,
|
||
Unexpected,
|
||
Canceled,
|
||
}!posix.socket_t {
|
||
const mode = posixSocketMode(options.mode);
|
||
const protocol = posixProtocol(options.protocol);
|
||
try current_thread.beginSyscall();
|
||
const socket_fd = while (true) {
|
||
const flags: u32 = mode | if (socket_flags_unsupported) 0 else posix.SOCK.CLOEXEC;
|
||
const socket_rc = posix.system.socket(family, flags, protocol);
|
||
switch (posix.errno(socket_rc)) {
|
||
.SUCCESS => {
|
||
const fd: posix.fd_t = @intCast(socket_rc);
|
||
errdefer posix.close(fd);
|
||
if (socket_flags_unsupported) while (true) {
|
||
try current_thread.checkCancel();
|
||
switch (posix.errno(posix.system.fcntl(fd, posix.F.SETFD, @as(usize, posix.FD_CLOEXEC)))) {
|
||
.SUCCESS => break,
|
||
.INTR => continue,
|
||
.CANCELED => return current_thread.endSyscallCanceled(),
|
||
else => |err| {
|
||
current_thread.endSyscall();
|
||
return posix.unexpectedErrno(err);
|
||
},
|
||
}
|
||
};
|
||
current_thread.endSyscall();
|
||
break fd;
|
||
},
|
||
.INTR => {
|
||
try current_thread.checkCancel();
|
||
continue;
|
||
},
|
||
.CANCELED => return current_thread.endSyscallCanceled(),
|
||
else => |e| {
|
||
current_thread.endSyscall();
|
||
switch (e) {
|
||
.AFNOSUPPORT => return error.AddressFamilyUnsupported,
|
||
.INVAL => return error.ProtocolUnsupportedBySystem,
|
||
.MFILE => return error.ProcessFdQuotaExceeded,
|
||
.NFILE => return error.SystemFdQuotaExceeded,
|
||
.NOBUFS => return error.SystemResources,
|
||
.NOMEM => return error.SystemResources,
|
||
.PROTONOSUPPORT => return error.ProtocolUnsupportedByAddressFamily,
|
||
.PROTOTYPE => return error.SocketModeUnsupported,
|
||
else => |err| return posix.unexpectedErrno(err),
|
||
}
|
||
},
|
||
}
|
||
};
|
||
errdefer posix.close(socket_fd);
|
||
|
||
if (options.ip6_only) {
|
||
if (posix.IPV6 == void) return error.OptionUnsupported;
|
||
try setSocketOption(current_thread, socket_fd, posix.IPPROTO.IPV6, posix.IPV6.V6ONLY, 0);
|
||
}
|
||
|
||
return socket_fd;
|
||
}
|
||
|
||
fn openSocketWsa(
|
||
t: *Threaded,
|
||
current_thread: *Thread,
|
||
family: posix.sa_family_t,
|
||
options: IpAddress.BindOptions,
|
||
) !ws2_32.SOCKET {
|
||
const mode = posixSocketMode(options.mode);
|
||
const protocol = posixProtocol(options.protocol);
|
||
const flags: u32 = ws2_32.WSA_FLAG_OVERLAPPED | ws2_32.WSA_FLAG_NO_HANDLE_INHERIT;
|
||
try current_thread.beginSyscall();
|
||
while (true) {
|
||
const rc = ws2_32.WSASocketW(family, @bitCast(mode), @bitCast(protocol), null, 0, flags);
|
||
if (rc != ws2_32.INVALID_SOCKET) {
|
||
current_thread.endSyscall();
|
||
return rc;
|
||
}
|
||
switch (ws2_32.WSAGetLastError()) {
|
||
.EINTR => {
|
||
try current_thread.checkCancel();
|
||
continue;
|
||
},
|
||
.NOTINITIALISED => {
|
||
try initializeWsa(t);
|
||
try current_thread.checkCancel();
|
||
continue;
|
||
},
|
||
else => |e| {
|
||
current_thread.endSyscall();
|
||
switch (e) {
|
||
.ECANCELLED, .E_CANCELLED, .OPERATION_ABORTED => return error.Canceled,
|
||
.EAFNOSUPPORT => return error.AddressFamilyUnsupported,
|
||
.EMFILE => return error.ProcessFdQuotaExceeded,
|
||
.ENOBUFS => return error.SystemResources,
|
||
.EPROTONOSUPPORT => return error.ProtocolUnsupportedByAddressFamily,
|
||
else => |err| return windows.unexpectedWSAError(err),
|
||
}
|
||
},
|
||
}
|
||
}
|
||
}
|
||
|
||
fn netAcceptPosix(userdata: ?*anyopaque, listen_fd: net.Socket.Handle) net.Server.AcceptError!net.Stream {
|
||
if (!have_networking) return error.NetworkDown;
|
||
const t: *Threaded = @ptrCast(@alignCast(userdata));
|
||
const current_thread = Thread.getCurrent(t);
|
||
var storage: PosixAddress = undefined;
|
||
var addr_len: posix.socklen_t = @sizeOf(PosixAddress);
|
||
try current_thread.beginSyscall();
|
||
const fd = while (true) {
|
||
const rc = if (have_accept4)
|
||
posix.system.accept4(listen_fd, &storage.any, &addr_len, posix.SOCK.CLOEXEC)
|
||
else
|
||
posix.system.accept(listen_fd, &storage.any, &addr_len);
|
||
switch (posix.errno(rc)) {
|
||
.SUCCESS => {
|
||
const fd: posix.fd_t = @intCast(rc);
|
||
errdefer posix.close(fd);
|
||
if (!have_accept4) while (true) {
|
||
try current_thread.checkCancel();
|
||
switch (posix.errno(posix.system.fcntl(fd, posix.F.SETFD, @as(usize, posix.FD_CLOEXEC)))) {
|
||
.SUCCESS => break,
|
||
.INTR => continue,
|
||
else => |err| {
|
||
current_thread.endSyscall();
|
||
return posix.unexpectedErrno(err);
|
||
},
|
||
}
|
||
};
|
||
current_thread.endSyscall();
|
||
break fd;
|
||
},
|
||
.INTR => {
|
||
try current_thread.checkCancel();
|
||
continue;
|
||
},
|
||
.CANCELED => return current_thread.endSyscallCanceled(),
|
||
else => |e| {
|
||
current_thread.endSyscall();
|
||
switch (e) {
|
||
.AGAIN => |err| return errnoBug(err),
|
||
.BADF => |err| return errnoBug(err), // File descriptor used after closed.
|
||
.CONNABORTED => return error.ConnectionAborted,
|
||
.FAULT => |err| return errnoBug(err),
|
||
.INVAL => return error.SocketNotListening,
|
||
.NOTSOCK => |err| return errnoBug(err),
|
||
.MFILE => return error.ProcessFdQuotaExceeded,
|
||
.NFILE => return error.SystemFdQuotaExceeded,
|
||
.NOBUFS => return error.SystemResources,
|
||
.NOMEM => return error.SystemResources,
|
||
.OPNOTSUPP => |err| return errnoBug(err),
|
||
.PROTO => return error.ProtocolFailure,
|
||
.PERM => return error.BlockedByFirewall,
|
||
else => |err| return posix.unexpectedErrno(err),
|
||
}
|
||
},
|
||
}
|
||
};
|
||
return .{ .socket = .{
|
||
.handle = fd,
|
||
.address = addressFromPosix(&storage),
|
||
} };
|
||
}
|
||
|
||
fn netAcceptWindows(userdata: ?*anyopaque, listen_handle: net.Socket.Handle) net.Server.AcceptError!net.Stream {
|
||
if (!have_networking) return error.NetworkDown;
|
||
const t: *Threaded = @ptrCast(@alignCast(userdata));
|
||
const current_thread = Thread.getCurrent(t);
|
||
var storage: WsaAddress = undefined;
|
||
var addr_len: i32 = @sizeOf(WsaAddress);
|
||
try current_thread.beginSyscall();
|
||
while (true) {
|
||
const rc = ws2_32.accept(listen_handle, &storage.any, &addr_len);
|
||
if (rc != ws2_32.INVALID_SOCKET) {
|
||
current_thread.endSyscall();
|
||
return .{ .socket = .{
|
||
.handle = rc,
|
||
.address = addressFromWsa(&storage),
|
||
} };
|
||
}
|
||
switch (ws2_32.WSAGetLastError()) {
|
||
.EINTR => {
|
||
try current_thread.checkCancel();
|
||
continue;
|
||
},
|
||
.NOTINITIALISED => {
|
||
try initializeWsa(t);
|
||
try current_thread.checkCancel();
|
||
continue;
|
||
},
|
||
else => |e| {
|
||
current_thread.endSyscall();
|
||
switch (e) {
|
||
.ECANCELLED, .E_CANCELLED, .OPERATION_ABORTED => return error.Canceled,
|
||
.ECONNRESET => return error.ConnectionAborted,
|
||
.EFAULT => |err| return wsaErrorBug(err),
|
||
.ENOTSOCK => |err| return wsaErrorBug(err),
|
||
.EINVAL => |err| return wsaErrorBug(err),
|
||
.EMFILE => return error.ProcessFdQuotaExceeded,
|
||
.ENETDOWN => return error.NetworkDown,
|
||
.ENOBUFS => return error.SystemResources,
|
||
.EOPNOTSUPP => |err| return wsaErrorBug(err),
|
||
else => |err| return windows.unexpectedWSAError(err),
|
||
}
|
||
},
|
||
}
|
||
}
|
||
}
|
||
|
||
fn netAcceptUnavailable(userdata: ?*anyopaque, listen_handle: net.Socket.Handle) net.Server.AcceptError!net.Stream {
|
||
_ = userdata;
|
||
_ = listen_handle;
|
||
return error.NetworkDown;
|
||
}
|
||
|
||
fn netReadPosix(userdata: ?*anyopaque, fd: net.Socket.Handle, data: [][]u8) net.Stream.Reader.Error!usize {
|
||
if (!have_networking) return error.NetworkDown;
|
||
const t: *Threaded = @ptrCast(@alignCast(userdata));
|
||
const current_thread = Thread.getCurrent(t);
|
||
|
||
var iovecs_buffer: [max_iovecs_len]posix.iovec = undefined;
|
||
var i: usize = 0;
|
||
for (data) |buf| {
|
||
if (iovecs_buffer.len - i == 0) break;
|
||
if (buf.len != 0) {
|
||
iovecs_buffer[i] = .{ .base = buf.ptr, .len = buf.len };
|
||
i += 1;
|
||
}
|
||
}
|
||
const dest = iovecs_buffer[0..i];
|
||
assert(dest[0].len > 0);
|
||
|
||
if (native_os == .wasi and !builtin.link_libc) {
|
||
try current_thread.beginSyscall();
|
||
while (true) {
|
||
var n: usize = undefined;
|
||
switch (std.os.wasi.fd_read(fd, dest.ptr, dest.len, &n)) {
|
||
.SUCCESS => {
|
||
current_thread.endSyscall();
|
||
return n;
|
||
},
|
||
.INTR => {
|
||
try current_thread.checkCancel();
|
||
continue;
|
||
},
|
||
.CANCELED => return current_thread.endSyscallCanceled(),
|
||
else => |e| {
|
||
current_thread.endSyscall();
|
||
switch (e) {
|
||
.INVAL => |err| return errnoBug(err),
|
||
.FAULT => |err| return errnoBug(err),
|
||
.AGAIN => |err| return errnoBug(err),
|
||
.BADF => |err| return errnoBug(err), // File descriptor used after closed.
|
||
.NOBUFS => return error.SystemResources,
|
||
.NOMEM => return error.SystemResources,
|
||
.NOTCONN => return error.SocketUnconnected,
|
||
.CONNRESET => return error.ConnectionResetByPeer,
|
||
.TIMEDOUT => return error.Timeout,
|
||
.NOTCAPABLE => return error.AccessDenied,
|
||
else => |err| return posix.unexpectedErrno(err),
|
||
}
|
||
},
|
||
}
|
||
}
|
||
}
|
||
|
||
try current_thread.beginSyscall();
|
||
while (true) {
|
||
const rc = posix.system.readv(fd, dest.ptr, @intCast(dest.len));
|
||
switch (posix.errno(rc)) {
|
||
.SUCCESS => {
|
||
current_thread.endSyscall();
|
||
return @intCast(rc);
|
||
},
|
||
.INTR => {
|
||
try current_thread.checkCancel();
|
||
continue;
|
||
},
|
||
.CANCELED => return current_thread.endSyscallCanceled(),
|
||
else => |e| {
|
||
current_thread.endSyscall();
|
||
switch (e) {
|
||
.INVAL => |err| return errnoBug(err),
|
||
.FAULT => |err| return errnoBug(err),
|
||
.AGAIN => |err| return errnoBug(err),
|
||
.BADF => |err| return errnoBug(err), // File descriptor used after closed.
|
||
.NOBUFS => return error.SystemResources,
|
||
.NOMEM => return error.SystemResources,
|
||
.NOTCONN => return error.SocketUnconnected,
|
||
.CONNRESET => return error.ConnectionResetByPeer,
|
||
.TIMEDOUT => return error.Timeout,
|
||
.PIPE => return error.SocketUnconnected,
|
||
.NETDOWN => return error.NetworkDown,
|
||
else => |err| return posix.unexpectedErrno(err),
|
||
}
|
||
},
|
||
}
|
||
}
|
||
}
|
||
|
||
fn netReadWindows(userdata: ?*anyopaque, handle: net.Socket.Handle, data: [][]u8) net.Stream.Reader.Error!usize {
|
||
if (!have_networking) return error.NetworkDown;
|
||
const t: *Threaded = @ptrCast(@alignCast(userdata));
|
||
const current_thread = Thread.getCurrent(t);
|
||
|
||
const bufs = b: {
|
||
var iovec_buffer: [max_iovecs_len]ws2_32.WSABUF = undefined;
|
||
var i: usize = 0;
|
||
var n: usize = 0;
|
||
for (data) |buf| {
|
||
if (iovec_buffer.len - i == 0) break;
|
||
if (buf.len == 0) continue;
|
||
if (std.math.cast(u32, buf.len)) |len| {
|
||
iovec_buffer[i] = .{ .buf = buf.ptr, .len = len };
|
||
i += 1;
|
||
n += len;
|
||
continue;
|
||
}
|
||
iovec_buffer[i] = .{ .buf = buf.ptr, .len = std.math.maxInt(u32) };
|
||
i += 1;
|
||
n += std.math.maxInt(u32);
|
||
break;
|
||
}
|
||
|
||
const bufs = iovec_buffer[0..i];
|
||
assert(bufs[0].len != 0);
|
||
|
||
break :b bufs;
|
||
};
|
||
|
||
while (true) {
|
||
try current_thread.checkCancel();
|
||
|
||
var flags: u32 = 0;
|
||
var overlapped: windows.OVERLAPPED = std.mem.zeroes(windows.OVERLAPPED);
|
||
var n: u32 = undefined;
|
||
const rc = ws2_32.WSARecv(handle, bufs.ptr, @intCast(bufs.len), &n, &flags, &overlapped, null);
|
||
if (rc != ws2_32.SOCKET_ERROR) return n;
|
||
const wsa_error: ws2_32.WinsockError = switch (ws2_32.WSAGetLastError()) {
|
||
.IO_PENDING => e: {
|
||
var result_flags: u32 = undefined;
|
||
const overlapped_rc = ws2_32.WSAGetOverlappedResult(
|
||
handle,
|
||
&overlapped,
|
||
&n,
|
||
windows.TRUE,
|
||
&result_flags,
|
||
);
|
||
if (overlapped_rc == windows.FALSE) {
|
||
break :e ws2_32.WSAGetLastError();
|
||
} else {
|
||
return n;
|
||
}
|
||
},
|
||
else => |err| err,
|
||
};
|
||
switch (wsa_error) {
|
||
.EINTR => continue,
|
||
.ECANCELLED, .E_CANCELLED, .OPERATION_ABORTED => return error.Canceled,
|
||
.NOTINITIALISED => {
|
||
try initializeWsa(t);
|
||
continue;
|
||
},
|
||
|
||
.ECONNRESET => return error.ConnectionResetByPeer,
|
||
.EFAULT => unreachable, // a pointer is not completely contained in user address space.
|
||
.EINVAL => |err| return wsaErrorBug(err),
|
||
.EMSGSIZE => |err| return wsaErrorBug(err),
|
||
.ENETDOWN => return error.NetworkDown,
|
||
.ENETRESET => return error.ConnectionResetByPeer,
|
||
.ENOTCONN => return error.SocketUnconnected,
|
||
else => |err| return windows.unexpectedWSAError(err),
|
||
}
|
||
}
|
||
}
|
||
|
||
fn netReadUnavailable(userdata: ?*anyopaque, fd: net.Socket.Handle, data: [][]u8) net.Stream.Reader.Error!usize {
|
||
_ = userdata;
|
||
_ = fd;
|
||
_ = data;
|
||
return error.NetworkDown;
|
||
}
|
||
|
||
fn netSendPosix(
|
||
userdata: ?*anyopaque,
|
||
handle: net.Socket.Handle,
|
||
messages: []net.OutgoingMessage,
|
||
flags: net.SendFlags,
|
||
) struct { ?net.Socket.SendError, usize } {
|
||
if (!have_networking) return .{ error.NetworkDown, 0 };
|
||
const t: *Threaded = @ptrCast(@alignCast(userdata));
|
||
const current_thread = Thread.getCurrent(t);
|
||
|
||
const posix_flags: u32 =
|
||
@as(u32, if (@hasDecl(posix.MSG, "CONFIRM") and flags.confirm) posix.MSG.CONFIRM else 0) |
|
||
@as(u32, if (@hasDecl(posix.MSG, "DONTROUTE") and flags.dont_route) posix.MSG.DONTROUTE else 0) |
|
||
@as(u32, if (@hasDecl(posix.MSG, "EOR") and flags.eor) posix.MSG.EOR else 0) |
|
||
@as(u32, if (@hasDecl(posix.MSG, "OOB") and flags.oob) posix.MSG.OOB else 0) |
|
||
@as(u32, if (@hasDecl(posix.MSG, "FASTOPEN") and flags.fastopen) posix.MSG.FASTOPEN else 0) |
|
||
posix.MSG.NOSIGNAL;
|
||
|
||
var i: usize = 0;
|
||
while (messages.len - i != 0) {
|
||
if (have_sendmmsg) {
|
||
i += netSendMany(current_thread, handle, messages[i..], posix_flags) catch |err| return .{ err, i };
|
||
continue;
|
||
}
|
||
netSendOne(t, current_thread, handle, &messages[i], posix_flags) catch |err| return .{ err, i };
|
||
i += 1;
|
||
}
|
||
return .{ null, i };
|
||
}
|
||
|
||
fn netSendWindows(
|
||
userdata: ?*anyopaque,
|
||
handle: net.Socket.Handle,
|
||
messages: []net.OutgoingMessage,
|
||
flags: net.SendFlags,
|
||
) struct { ?net.Socket.SendError, usize } {
|
||
if (!have_networking) return .{ error.NetworkDown, 0 };
|
||
const t: *Threaded = @ptrCast(@alignCast(userdata));
|
||
_ = t;
|
||
_ = handle;
|
||
_ = messages;
|
||
_ = flags;
|
||
@panic("TODO netSendWindows");
|
||
}
|
||
|
||
fn netSendUnavailable(
|
||
userdata: ?*anyopaque,
|
||
handle: net.Socket.Handle,
|
||
messages: []net.OutgoingMessage,
|
||
flags: net.SendFlags,
|
||
) struct { ?net.Socket.SendError, usize } {
|
||
_ = userdata;
|
||
_ = handle;
|
||
_ = messages;
|
||
_ = flags;
|
||
return .{ error.NetworkDown, 0 };
|
||
}
|
||
|
||
fn netSendOne(
|
||
t: *Threaded,
|
||
current_thread: *Thread,
|
||
handle: net.Socket.Handle,
|
||
message: *net.OutgoingMessage,
|
||
flags: u32,
|
||
) net.Socket.SendError!void {
|
||
var addr: PosixAddress = undefined;
|
||
var iovec: posix.iovec_const = .{ .base = @constCast(message.data_ptr), .len = message.data_len };
|
||
const msg: posix.msghdr_const = .{
|
||
.name = &addr.any,
|
||
.namelen = addressToPosix(message.address, &addr),
|
||
.iov = (&iovec)[0..1],
|
||
.iovlen = 1,
|
||
// OS returns EINVAL if this pointer is invalid even if controllen is zero.
|
||
.control = if (message.control.len == 0) null else @constCast(message.control.ptr),
|
||
.controllen = @intCast(message.control.len),
|
||
.flags = 0,
|
||
};
|
||
try current_thread.beginSyscall();
|
||
while (true) {
|
||
const rc = posix.system.sendmsg(handle, &msg, flags);
|
||
if (is_windows) {
|
||
if (rc != ws2_32.SOCKET_ERROR) {
|
||
current_thread.endSyscall();
|
||
message.data_len = @intCast(rc);
|
||
return;
|
||
}
|
||
switch (ws2_32.WSAGetLastError()) {
|
||
.EINTR => {
|
||
try current_thread.checkCancel();
|
||
continue;
|
||
},
|
||
.NOTINITIALISED => {
|
||
try initializeWsa(t);
|
||
try current_thread.checkCancel();
|
||
continue;
|
||
},
|
||
else => |e| {
|
||
current_thread.endSyscall();
|
||
switch (e) {
|
||
.ECANCELLED, .E_CANCELLED, .OPERATION_ABORTED => return error.Canceled,
|
||
.EACCES => return error.AccessDenied,
|
||
.EADDRNOTAVAIL => return error.AddressUnavailable,
|
||
.ECONNRESET => return error.ConnectionResetByPeer,
|
||
.EMSGSIZE => return error.MessageOversize,
|
||
.ENOBUFS => return error.SystemResources,
|
||
.ENOTSOCK => return error.FileDescriptorNotASocket,
|
||
.EAFNOSUPPORT => return error.AddressFamilyUnsupported,
|
||
.EDESTADDRREQ => unreachable, // A destination address is required.
|
||
.EFAULT => unreachable, // The lpBuffers, lpTo, lpOverlapped, lpNumberOfBytesSent, or lpCompletionRoutine parameters are not part of the user address space, or the lpTo parameter is too small.
|
||
.EHOSTUNREACH => return error.NetworkUnreachable,
|
||
.EINVAL => unreachable,
|
||
.ENETDOWN => return error.NetworkDown,
|
||
.ENETRESET => return error.ConnectionResetByPeer,
|
||
.ENETUNREACH => return error.NetworkUnreachable,
|
||
.ENOTCONN => return error.SocketUnconnected,
|
||
.ESHUTDOWN => |err| return wsaErrorBug(err),
|
||
else => |err| return windows.unexpectedWSAError(err),
|
||
}
|
||
},
|
||
}
|
||
}
|
||
switch (posix.errno(rc)) {
|
||
.SUCCESS => {
|
||
current_thread.endSyscall();
|
||
message.data_len = @intCast(rc);
|
||
return;
|
||
},
|
||
.INTR => {
|
||
try current_thread.checkCancel();
|
||
continue;
|
||
},
|
||
.CANCELED => return current_thread.endSyscallCanceled(),
|
||
else => |e| {
|
||
current_thread.endSyscall();
|
||
switch (e) {
|
||
.ACCES => return error.AccessDenied,
|
||
.ALREADY => return error.FastOpenAlreadyInProgress,
|
||
.BADF => |err| return errnoBug(err), // File descriptor used after closed.
|
||
.CONNRESET => return error.ConnectionResetByPeer,
|
||
.DESTADDRREQ => |err| return errnoBug(err),
|
||
.FAULT => |err| return errnoBug(err),
|
||
.INVAL => |err| return errnoBug(err),
|
||
.ISCONN => |err| return errnoBug(err),
|
||
.MSGSIZE => return error.MessageOversize,
|
||
.NOBUFS => return error.SystemResources,
|
||
.NOMEM => return error.SystemResources,
|
||
.NOTSOCK => |err| return errnoBug(err),
|
||
.OPNOTSUPP => |err| return errnoBug(err),
|
||
.PIPE => return error.SocketUnconnected,
|
||
.AFNOSUPPORT => return error.AddressFamilyUnsupported,
|
||
.HOSTUNREACH => return error.HostUnreachable,
|
||
.NETUNREACH => return error.NetworkUnreachable,
|
||
.NOTCONN => return error.SocketUnconnected,
|
||
.NETDOWN => return error.NetworkDown,
|
||
else => |err| return posix.unexpectedErrno(err),
|
||
}
|
||
},
|
||
}
|
||
}
|
||
}
|
||
|
||
fn netSendMany(
|
||
current_thread: *Thread,
|
||
handle: net.Socket.Handle,
|
||
messages: []net.OutgoingMessage,
|
||
flags: u32,
|
||
) net.Socket.SendError!usize {
|
||
var msg_buffer: [64]posix.system.mmsghdr = undefined;
|
||
var addr_buffer: [msg_buffer.len]PosixAddress = undefined;
|
||
var iovecs_buffer: [msg_buffer.len]posix.iovec = undefined;
|
||
const min_len: usize = @min(messages.len, msg_buffer.len);
|
||
const clamped_messages = messages[0..min_len];
|
||
const clamped_msgs = (&msg_buffer)[0..min_len];
|
||
const clamped_addrs = (&addr_buffer)[0..min_len];
|
||
const clamped_iovecs = (&iovecs_buffer)[0..min_len];
|
||
|
||
for (clamped_messages, clamped_msgs, clamped_addrs, clamped_iovecs) |*message, *msg, *addr, *iovec| {
|
||
iovec.* = .{ .base = @constCast(message.data_ptr), .len = message.data_len };
|
||
msg.* = .{
|
||
.hdr = .{
|
||
.name = &addr.any,
|
||
.namelen = addressToPosix(message.address, addr),
|
||
.iov = iovec[0..1],
|
||
.iovlen = 1,
|
||
.control = @constCast(message.control.ptr),
|
||
.controllen = message.control.len,
|
||
.flags = 0,
|
||
},
|
||
.len = undefined, // Populated by calling sendmmsg below.
|
||
};
|
||
}
|
||
|
||
try current_thread.beginSyscall();
|
||
while (true) {
|
||
const rc = posix.system.sendmmsg(handle, clamped_msgs.ptr, @intCast(clamped_msgs.len), flags);
|
||
switch (posix.errno(rc)) {
|
||
.SUCCESS => {
|
||
current_thread.endSyscall();
|
||
const n: usize = @intCast(rc);
|
||
for (clamped_messages[0..n], clamped_msgs[0..n]) |*message, *msg| {
|
||
message.data_len = msg.len;
|
||
}
|
||
return n;
|
||
},
|
||
.INTR => {
|
||
try current_thread.checkCancel();
|
||
continue;
|
||
},
|
||
.CANCELED => return current_thread.endSyscallCanceled(),
|
||
else => |e| {
|
||
current_thread.endSyscall();
|
||
switch (e) {
|
||
.AGAIN => |err| return errnoBug(err),
|
||
.ALREADY => return error.FastOpenAlreadyInProgress,
|
||
.BADF => |err| return errnoBug(err), // File descriptor used after closed.
|
||
.CONNRESET => return error.ConnectionResetByPeer,
|
||
.DESTADDRREQ => |err| return errnoBug(err), // The socket is not connection-mode, and no peer address is set.
|
||
.FAULT => |err| return errnoBug(err), // An invalid user space address was specified for an argument.
|
||
.INVAL => |err| return errnoBug(err), // Invalid argument passed.
|
||
.ISCONN => |err| return errnoBug(err), // connection-mode socket was connected already but a recipient was specified
|
||
.MSGSIZE => return error.MessageOversize,
|
||
.NOBUFS => return error.SystemResources,
|
||
.NOMEM => return error.SystemResources,
|
||
.NOTSOCK => |err| return errnoBug(err), // The file descriptor sockfd does not refer to a socket.
|
||
.OPNOTSUPP => |err| return errnoBug(err), // Some bit in the flags argument is inappropriate for the socket type.
|
||
.PIPE => return error.SocketUnconnected,
|
||
.AFNOSUPPORT => return error.AddressFamilyUnsupported,
|
||
.HOSTUNREACH => return error.HostUnreachable,
|
||
.NETUNREACH => return error.NetworkUnreachable,
|
||
.NOTCONN => return error.SocketUnconnected,
|
||
.NETDOWN => return error.NetworkDown,
|
||
else => |err| return posix.unexpectedErrno(err),
|
||
}
|
||
},
|
||
}
|
||
}
|
||
}
|
||
|
||
fn netReceivePosix(
|
||
userdata: ?*anyopaque,
|
||
handle: net.Socket.Handle,
|
||
message_buffer: []net.IncomingMessage,
|
||
data_buffer: []u8,
|
||
flags: net.ReceiveFlags,
|
||
timeout: Io.Timeout,
|
||
) struct { ?net.Socket.ReceiveTimeoutError, usize } {
|
||
if (!have_networking) return .{ error.NetworkDown, 0 };
|
||
const t: *Threaded = @ptrCast(@alignCast(userdata));
|
||
const current_thread = Thread.getCurrent(t);
|
||
const t_io = io(t);
|
||
|
||
// recvmmsg is useless, here's why:
|
||
// * [timeout bug](https://bugzilla.kernel.org/show_bug.cgi?id=75371)
|
||
// * it wants iovecs for each message but we have a better API: one data
|
||
// buffer to handle all the messages. The better API cannot be lowered to
|
||
// the split vectors though because reducing the buffer size might make
|
||
// some messages unreceivable.
|
||
|
||
// So the strategy instead is to use non-blocking recvmsg calls, calling
|
||
// poll() with timeout if the first one returns EAGAIN.
|
||
const posix_flags: u32 =
|
||
@as(u32, if (flags.oob) posix.MSG.OOB else 0) |
|
||
@as(u32, if (flags.peek) posix.MSG.PEEK else 0) |
|
||
@as(u32, if (flags.trunc) posix.MSG.TRUNC else 0) |
|
||
posix.MSG.DONTWAIT | posix.MSG.NOSIGNAL;
|
||
|
||
var poll_fds: [1]posix.pollfd = .{
|
||
.{
|
||
.fd = handle,
|
||
.events = posix.POLL.IN,
|
||
.revents = undefined,
|
||
},
|
||
};
|
||
var message_i: usize = 0;
|
||
var data_i: usize = 0;
|
||
|
||
const deadline = timeout.toDeadline(t_io) catch |err| return .{ err, message_i };
|
||
|
||
recv: while (true) {
|
||
if (message_buffer.len - message_i == 0) return .{ null, message_i };
|
||
const message = &message_buffer[message_i];
|
||
const remaining_data_buffer = data_buffer[data_i..];
|
||
var storage: PosixAddress = undefined;
|
||
var iov: posix.iovec = .{ .base = remaining_data_buffer.ptr, .len = remaining_data_buffer.len };
|
||
var msg: posix.msghdr = .{
|
||
.name = &storage.any,
|
||
.namelen = @sizeOf(PosixAddress),
|
||
.iov = (&iov)[0..1],
|
||
.iovlen = 1,
|
||
.control = message.control.ptr,
|
||
.controllen = @intCast(message.control.len),
|
||
.flags = undefined,
|
||
};
|
||
|
||
current_thread.beginSyscall() catch |err| return .{ err, message_i };
|
||
const recv_rc = posix.system.recvmsg(handle, &msg, posix_flags);
|
||
current_thread.endSyscall();
|
||
switch (posix.errno(recv_rc)) {
|
||
.SUCCESS => {
|
||
const data = remaining_data_buffer[0..@intCast(recv_rc)];
|
||
data_i += data.len;
|
||
message.* = .{
|
||
.from = addressFromPosix(&storage),
|
||
.data = data,
|
||
.control = if (msg.control) |ptr| @as([*]u8, @ptrCast(ptr))[0..msg.controllen] else message.control,
|
||
.flags = .{
|
||
.eor = (msg.flags & posix.MSG.EOR) != 0,
|
||
.trunc = (msg.flags & posix.MSG.TRUNC) != 0,
|
||
.ctrunc = (msg.flags & posix.MSG.CTRUNC) != 0,
|
||
.oob = (msg.flags & posix.MSG.OOB) != 0,
|
||
.errqueue = if (@hasDecl(posix.MSG, "ERRQUEUE")) (msg.flags & posix.MSG.ERRQUEUE) != 0 else false,
|
||
},
|
||
};
|
||
message_i += 1;
|
||
continue;
|
||
},
|
||
.AGAIN => while (true) {
|
||
if (message_i != 0) return .{ null, message_i };
|
||
|
||
const max_poll_ms = std.math.maxInt(u31);
|
||
const timeout_ms: u31 = if (deadline) |d| t: {
|
||
const duration = d.durationFromNow(t_io) catch |err| return .{ err, message_i };
|
||
if (duration.raw.nanoseconds <= 0) return .{ error.Timeout, message_i };
|
||
break :t @intCast(@min(max_poll_ms, duration.raw.toMilliseconds()));
|
||
} else max_poll_ms;
|
||
|
||
current_thread.beginSyscall() catch |err| return .{ err, message_i };
|
||
const poll_rc = posix.system.poll(&poll_fds, poll_fds.len, timeout_ms);
|
||
current_thread.endSyscall();
|
||
|
||
switch (posix.errno(poll_rc)) {
|
||
.SUCCESS => {
|
||
if (poll_rc == 0) {
|
||
// Although spurious timeouts are OK, when no deadline
|
||
// is passed we must not return `error.Timeout`.
|
||
if (deadline == null) continue;
|
||
return .{ error.Timeout, message_i };
|
||
}
|
||
continue :recv;
|
||
},
|
||
.INTR => continue,
|
||
.CANCELED => return .{ current_thread.endSyscallCanceled(), message_i },
|
||
|
||
.FAULT => |err| return .{ errnoBug(err), message_i },
|
||
.INVAL => |err| return .{ errnoBug(err), message_i },
|
||
.NOMEM => return .{ error.SystemResources, message_i },
|
||
else => |err| return .{ posix.unexpectedErrno(err), message_i },
|
||
}
|
||
},
|
||
.INTR => continue,
|
||
.CANCELED => return .{ current_thread.endSyscallCanceled(), message_i },
|
||
|
||
.BADF => |err| return .{ errnoBug(err), message_i },
|
||
.NFILE => return .{ error.SystemFdQuotaExceeded, message_i },
|
||
.MFILE => return .{ error.ProcessFdQuotaExceeded, message_i },
|
||
.FAULT => |err| return .{ errnoBug(err), message_i },
|
||
.INVAL => |err| return .{ errnoBug(err), message_i },
|
||
.NOBUFS => return .{ error.SystemResources, message_i },
|
||
.NOMEM => return .{ error.SystemResources, message_i },
|
||
.NOTCONN => return .{ error.SocketUnconnected, message_i },
|
||
.NOTSOCK => |err| return .{ errnoBug(err), message_i },
|
||
.MSGSIZE => return .{ error.MessageOversize, message_i },
|
||
.PIPE => return .{ error.SocketUnconnected, message_i },
|
||
.OPNOTSUPP => |err| return .{ errnoBug(err), message_i },
|
||
.CONNRESET => return .{ error.ConnectionResetByPeer, message_i },
|
||
.NETDOWN => return .{ error.NetworkDown, message_i },
|
||
else => |err| return .{ posix.unexpectedErrno(err), message_i },
|
||
}
|
||
}
|
||
}
|
||
|
||
fn netReceiveWindows(
|
||
userdata: ?*anyopaque,
|
||
handle: net.Socket.Handle,
|
||
message_buffer: []net.IncomingMessage,
|
||
data_buffer: []u8,
|
||
flags: net.ReceiveFlags,
|
||
timeout: Io.Timeout,
|
||
) struct { ?net.Socket.ReceiveTimeoutError, usize } {
|
||
if (!have_networking) return .{ error.NetworkDown, 0 };
|
||
const t: *Threaded = @ptrCast(@alignCast(userdata));
|
||
_ = t;
|
||
_ = handle;
|
||
_ = message_buffer;
|
||
_ = data_buffer;
|
||
_ = flags;
|
||
_ = timeout;
|
||
@panic("TODO implement netReceiveWindows");
|
||
}
|
||
|
||
fn netReceiveUnavailable(
|
||
userdata: ?*anyopaque,
|
||
handle: net.Socket.Handle,
|
||
message_buffer: []net.IncomingMessage,
|
||
data_buffer: []u8,
|
||
flags: net.ReceiveFlags,
|
||
timeout: Io.Timeout,
|
||
) struct { ?net.Socket.ReceiveTimeoutError, usize } {
|
||
_ = userdata;
|
||
_ = handle;
|
||
_ = message_buffer;
|
||
_ = data_buffer;
|
||
_ = flags;
|
||
_ = timeout;
|
||
return .{ error.NetworkDown, 0 };
|
||
}
|
||
|
||
fn netWritePosix(
|
||
userdata: ?*anyopaque,
|
||
fd: net.Socket.Handle,
|
||
header: []const u8,
|
||
data: []const []const u8,
|
||
splat: usize,
|
||
) net.Stream.Writer.Error!usize {
|
||
if (!have_networking) return error.NetworkDown;
|
||
const t: *Threaded = @ptrCast(@alignCast(userdata));
|
||
const current_thread = Thread.getCurrent(t);
|
||
|
||
var iovecs: [max_iovecs_len]posix.iovec_const = undefined;
|
||
var msg: posix.msghdr_const = .{
|
||
.name = null,
|
||
.namelen = 0,
|
||
.iov = &iovecs,
|
||
.iovlen = 0,
|
||
.control = null,
|
||
.controllen = 0,
|
||
.flags = 0,
|
||
};
|
||
addBuf(&iovecs, &msg.iovlen, header);
|
||
for (data[0 .. data.len - 1]) |bytes| addBuf(&iovecs, &msg.iovlen, bytes);
|
||
const pattern = data[data.len - 1];
|
||
if (iovecs.len - msg.iovlen != 0) switch (splat) {
|
||
0 => {},
|
||
1 => addBuf(&iovecs, &msg.iovlen, pattern),
|
||
else => switch (pattern.len) {
|
||
0 => {},
|
||
1 => {
|
||
var backup_buffer: [splat_buffer_size]u8 = undefined;
|
||
const splat_buffer = &backup_buffer;
|
||
const memset_len = @min(splat_buffer.len, splat);
|
||
const buf = splat_buffer[0..memset_len];
|
||
@memset(buf, pattern[0]);
|
||
addBuf(&iovecs, &msg.iovlen, buf);
|
||
var remaining_splat = splat - buf.len;
|
||
while (remaining_splat > splat_buffer.len and iovecs.len - msg.iovlen != 0) {
|
||
assert(buf.len == splat_buffer.len);
|
||
addBuf(&iovecs, &msg.iovlen, splat_buffer);
|
||
remaining_splat -= splat_buffer.len;
|
||
}
|
||
addBuf(&iovecs, &msg.iovlen, splat_buffer[0..remaining_splat]);
|
||
},
|
||
else => for (0..@min(splat, iovecs.len - msg.iovlen)) |_| {
|
||
addBuf(&iovecs, &msg.iovlen, pattern);
|
||
},
|
||
},
|
||
};
|
||
const flags = posix.MSG.NOSIGNAL;
|
||
try current_thread.beginSyscall();
|
||
while (true) {
|
||
const rc = posix.system.sendmsg(fd, &msg, flags);
|
||
switch (posix.errno(rc)) {
|
||
.SUCCESS => {
|
||
current_thread.endSyscall();
|
||
return @intCast(rc);
|
||
},
|
||
.INTR => {
|
||
try current_thread.checkCancel();
|
||
continue;
|
||
},
|
||
.CANCELED => return current_thread.endSyscallCanceled(),
|
||
else => |e| {
|
||
current_thread.endSyscall();
|
||
switch (e) {
|
||
.ACCES => |err| return errnoBug(err),
|
||
.AGAIN => |err| return errnoBug(err),
|
||
.ALREADY => return error.FastOpenAlreadyInProgress,
|
||
.BADF => |err| return errnoBug(err), // File descriptor used after closed.
|
||
.CONNRESET => return error.ConnectionResetByPeer,
|
||
.DESTADDRREQ => |err| return errnoBug(err), // The socket is not connection-mode, and no peer address is set.
|
||
.FAULT => |err| return errnoBug(err), // An invalid user space address was specified for an argument.
|
||
.INVAL => |err| return errnoBug(err), // Invalid argument passed.
|
||
.ISCONN => |err| return errnoBug(err), // connection-mode socket was connected already but a recipient was specified
|
||
.MSGSIZE => |err| return errnoBug(err),
|
||
.NOBUFS => return error.SystemResources,
|
||
.NOMEM => return error.SystemResources,
|
||
.NOTSOCK => |err| return errnoBug(err), // The file descriptor sockfd does not refer to a socket.
|
||
.OPNOTSUPP => |err| return errnoBug(err), // Some bit in the flags argument is inappropriate for the socket type.
|
||
.PIPE => return error.SocketUnconnected,
|
||
.AFNOSUPPORT => return error.AddressFamilyUnsupported,
|
||
.HOSTUNREACH => return error.HostUnreachable,
|
||
.NETUNREACH => return error.NetworkUnreachable,
|
||
.NOTCONN => return error.SocketUnconnected,
|
||
.NETDOWN => return error.NetworkDown,
|
||
else => |err| return posix.unexpectedErrno(err),
|
||
}
|
||
},
|
||
}
|
||
}
|
||
}
|
||
|
||
fn netWriteWindows(
|
||
userdata: ?*anyopaque,
|
||
handle: net.Socket.Handle,
|
||
header: []const u8,
|
||
data: []const []const u8,
|
||
splat: usize,
|
||
) net.Stream.Writer.Error!usize {
|
||
const t: *Threaded = @ptrCast(@alignCast(userdata));
|
||
const current_thread = Thread.getCurrent(t);
|
||
comptime assert(native_os == .windows);
|
||
|
||
var iovecs: [max_iovecs_len]ws2_32.WSABUF = undefined;
|
||
var len: u32 = 0;
|
||
addWsaBuf(&iovecs, &len, header);
|
||
for (data[0 .. data.len - 1]) |bytes| addWsaBuf(&iovecs, &len, bytes);
|
||
const pattern = data[data.len - 1];
|
||
if (iovecs.len - len != 0) switch (splat) {
|
||
0 => {},
|
||
1 => addWsaBuf(&iovecs, &len, pattern),
|
||
else => switch (pattern.len) {
|
||
0 => {},
|
||
1 => {
|
||
var backup_buffer: [64]u8 = undefined;
|
||
const splat_buffer = &backup_buffer;
|
||
const memset_len = @min(splat_buffer.len, splat);
|
||
const buf = splat_buffer[0..memset_len];
|
||
@memset(buf, pattern[0]);
|
||
addWsaBuf(&iovecs, &len, buf);
|
||
var remaining_splat = splat - buf.len;
|
||
while (remaining_splat > splat_buffer.len and len < iovecs.len) {
|
||
addWsaBuf(&iovecs, &len, splat_buffer);
|
||
remaining_splat -= splat_buffer.len;
|
||
}
|
||
addWsaBuf(&iovecs, &len, splat_buffer[0..remaining_splat]);
|
||
},
|
||
else => for (0..@min(splat, iovecs.len - len)) |_| {
|
||
addWsaBuf(&iovecs, &len, pattern);
|
||
},
|
||
},
|
||
};
|
||
|
||
while (true) {
|
||
try current_thread.checkCancel();
|
||
|
||
var n: u32 = undefined;
|
||
var overlapped: windows.OVERLAPPED = std.mem.zeroes(windows.OVERLAPPED);
|
||
const rc = ws2_32.WSASend(handle, &iovecs, len, &n, 0, &overlapped, null);
|
||
if (rc != ws2_32.SOCKET_ERROR) return n;
|
||
const wsa_error: ws2_32.WinsockError = switch (ws2_32.WSAGetLastError()) {
|
||
.IO_PENDING => e: {
|
||
var result_flags: u32 = undefined;
|
||
const overlapped_rc = ws2_32.WSAGetOverlappedResult(
|
||
handle,
|
||
&overlapped,
|
||
&n,
|
||
windows.TRUE,
|
||
&result_flags,
|
||
);
|
||
if (overlapped_rc == windows.FALSE) {
|
||
break :e ws2_32.WSAGetLastError();
|
||
} else {
|
||
return n;
|
||
}
|
||
},
|
||
else => |err| err,
|
||
};
|
||
switch (wsa_error) {
|
||
.EINTR => continue,
|
||
.ECANCELLED, .E_CANCELLED, .OPERATION_ABORTED => return current_thread.endSyscallCanceled(),
|
||
.NOTINITIALISED => {
|
||
try initializeWsa(t);
|
||
continue;
|
||
},
|
||
|
||
.ECONNABORTED => return error.ConnectionResetByPeer,
|
||
.ECONNRESET => return error.ConnectionResetByPeer,
|
||
.EINVAL => return error.SocketUnconnected,
|
||
.ENETDOWN => return error.NetworkDown,
|
||
.ENETRESET => return error.ConnectionResetByPeer,
|
||
.ENOBUFS => return error.SystemResources,
|
||
.ENOTCONN => return error.SocketUnconnected,
|
||
.ENOTSOCK => |err| return wsaErrorBug(err),
|
||
.EOPNOTSUPP => |err| return wsaErrorBug(err),
|
||
.ESHUTDOWN => |err| return wsaErrorBug(err),
|
||
else => |err| return windows.unexpectedWSAError(err),
|
||
}
|
||
}
|
||
}
|
||
|
||
fn addWsaBuf(v: []ws2_32.WSABUF, i: *u32, bytes: []const u8) void {
|
||
const cap = std.math.maxInt(u32);
|
||
var remaining = bytes;
|
||
while (remaining.len > cap) {
|
||
if (v.len - i.* == 0) return;
|
||
v[i.*] = .{ .buf = @constCast(remaining.ptr), .len = cap };
|
||
i.* += 1;
|
||
remaining = remaining[cap..];
|
||
} else {
|
||
@branchHint(.likely);
|
||
if (v.len - i.* == 0) return;
|
||
v[i.*] = .{ .buf = @constCast(remaining.ptr), .len = @intCast(remaining.len) };
|
||
i.* += 1;
|
||
}
|
||
}
|
||
|
||
fn netWriteUnavailable(
|
||
userdata: ?*anyopaque,
|
||
handle: net.Socket.Handle,
|
||
header: []const u8,
|
||
data: []const []const u8,
|
||
splat: usize,
|
||
) net.Stream.Writer.Error!usize {
|
||
_ = userdata;
|
||
_ = handle;
|
||
_ = header;
|
||
_ = data;
|
||
_ = splat;
|
||
return error.NetworkDown;
|
||
}
|
||
|
||
fn addBuf(v: []posix.iovec_const, i: *@FieldType(posix.msghdr_const, "iovlen"), bytes: []const u8) void {
|
||
// OS checks ptr addr before length so zero length vectors must be omitted.
|
||
if (bytes.len == 0) return;
|
||
if (v.len - i.* == 0) return;
|
||
v[i.*] = .{ .base = bytes.ptr, .len = bytes.len };
|
||
i.* += 1;
|
||
}
|
||
|
||
fn netClose(userdata: ?*anyopaque, handle: net.Socket.Handle) void {
|
||
const t: *Threaded = @ptrCast(@alignCast(userdata));
|
||
_ = t;
|
||
switch (native_os) {
|
||
.windows => closeSocketWindows(handle),
|
||
else => posix.close(handle),
|
||
}
|
||
}
|
||
|
||
fn netCloseUnavailable(userdata: ?*anyopaque, handle: net.Socket.Handle) void {
|
||
_ = userdata;
|
||
_ = handle;
|
||
unreachable; // How you gonna close something that was impossible to open?
|
||
}
|
||
|
||
fn netInterfaceNameResolve(
|
||
userdata: ?*anyopaque,
|
||
name: *const net.Interface.Name,
|
||
) net.Interface.Name.ResolveError!net.Interface {
|
||
if (!have_networking) return error.InterfaceNotFound;
|
||
const t: *Threaded = @ptrCast(@alignCast(userdata));
|
||
const current_thread = Thread.getCurrent(t);
|
||
|
||
if (native_os == .linux) {
|
||
const sock_fd = openSocketPosix(current_thread, posix.AF.UNIX, .{ .mode = .dgram }) catch |err| switch (err) {
|
||
error.ProcessFdQuotaExceeded => return error.SystemResources,
|
||
error.SystemFdQuotaExceeded => return error.SystemResources,
|
||
error.AddressFamilyUnsupported => return error.Unexpected,
|
||
error.ProtocolUnsupportedBySystem => return error.Unexpected,
|
||
error.ProtocolUnsupportedByAddressFamily => return error.Unexpected,
|
||
error.SocketModeUnsupported => return error.Unexpected,
|
||
error.OptionUnsupported => return error.Unexpected,
|
||
else => |e| return e,
|
||
};
|
||
defer posix.close(sock_fd);
|
||
|
||
var ifr: posix.ifreq = .{
|
||
.ifrn = .{ .name = @bitCast(name.bytes) },
|
||
.ifru = undefined,
|
||
};
|
||
|
||
try current_thread.beginSyscall();
|
||
while (true) {
|
||
switch (posix.errno(posix.system.ioctl(sock_fd, posix.SIOCGIFINDEX, @intFromPtr(&ifr)))) {
|
||
.SUCCESS => {
|
||
current_thread.endSyscall();
|
||
return .{ .index = @bitCast(ifr.ifru.ivalue) };
|
||
},
|
||
.INTR => {
|
||
try current_thread.checkCancel();
|
||
continue;
|
||
},
|
||
.CANCELED => return current_thread.endSyscallCanceled(),
|
||
else => |e| {
|
||
current_thread.endSyscall();
|
||
switch (e) {
|
||
.INVAL => |err| return errnoBug(err), // Bad parameters.
|
||
.NOTTY => |err| return errnoBug(err),
|
||
.NXIO => |err| return errnoBug(err),
|
||
.BADF => |err| return errnoBug(err), // File descriptor used after closed.
|
||
.FAULT => |err| return errnoBug(err), // Bad pointer parameter.
|
||
.IO => |err| return errnoBug(err), // sock_fd is not a file descriptor
|
||
.NODEV => return error.InterfaceNotFound,
|
||
else => |err| return posix.unexpectedErrno(err),
|
||
}
|
||
},
|
||
}
|
||
}
|
||
}
|
||
|
||
if (native_os == .windows) {
|
||
try current_thread.checkCancel();
|
||
@panic("TODO implement netInterfaceNameResolve for Windows");
|
||
}
|
||
|
||
if (builtin.link_libc) {
|
||
try current_thread.checkCancel();
|
||
const index = std.c.if_nametoindex(&name.bytes);
|
||
if (index == 0) return error.InterfaceNotFound;
|
||
return .{ .index = @bitCast(index) };
|
||
}
|
||
|
||
@panic("unimplemented");
|
||
}
|
||
|
||
fn netInterfaceNameResolveUnavailable(
|
||
userdata: ?*anyopaque,
|
||
name: *const net.Interface.Name,
|
||
) net.Interface.Name.ResolveError!net.Interface {
|
||
_ = userdata;
|
||
_ = name;
|
||
return error.InterfaceNotFound;
|
||
}
|
||
|
||
fn netInterfaceName(userdata: ?*anyopaque, interface: net.Interface) net.Interface.NameError!net.Interface.Name {
|
||
const t: *Threaded = @ptrCast(@alignCast(userdata));
|
||
const current_thread = Thread.getCurrent(t);
|
||
try current_thread.checkCancel();
|
||
|
||
if (native_os == .linux) {
|
||
_ = interface;
|
||
@panic("TODO implement netInterfaceName for linux");
|
||
}
|
||
|
||
if (native_os == .windows) {
|
||
@panic("TODO implement netInterfaceName for windows");
|
||
}
|
||
|
||
if (builtin.link_libc) {
|
||
@panic("TODO implement netInterfaceName for libc");
|
||
}
|
||
|
||
@panic("unimplemented");
|
||
}
|
||
|
||
fn netInterfaceNameUnavailable(userdata: ?*anyopaque, interface: net.Interface) net.Interface.NameError!net.Interface.Name {
|
||
_ = userdata;
|
||
_ = interface;
|
||
return error.Unexpected;
|
||
}
|
||
|
||
fn netLookup(
|
||
userdata: ?*anyopaque,
|
||
host_name: HostName,
|
||
resolved: *Io.Queue(HostName.LookupResult),
|
||
options: HostName.LookupOptions,
|
||
) void {
|
||
const t: *Threaded = @ptrCast(@alignCast(userdata));
|
||
const current_thread = Thread.getCurrent(t);
|
||
const t_io = io(t);
|
||
resolved.putOneUncancelable(t_io, .{ .end = netLookupFallible(t, current_thread, host_name, resolved, options) });
|
||
}
|
||
|
||
fn netLookupUnavailable(
|
||
userdata: ?*anyopaque,
|
||
host_name: HostName,
|
||
resolved: *Io.Queue(HostName.LookupResult),
|
||
options: HostName.LookupOptions,
|
||
) void {
|
||
_ = host_name;
|
||
_ = options;
|
||
const t: *Threaded = @ptrCast(@alignCast(userdata));
|
||
const t_io = ioBasic(t);
|
||
resolved.putOneUncancelable(t_io, .{ .end = error.NetworkDown });
|
||
}
|
||
|
||
fn netLookupFallible(
|
||
t: *Threaded,
|
||
current_thread: *Thread,
|
||
host_name: HostName,
|
||
resolved: *Io.Queue(HostName.LookupResult),
|
||
options: HostName.LookupOptions,
|
||
) !void {
|
||
if (!have_networking) return error.NetworkDown;
|
||
const t_io = io(t);
|
||
const name = host_name.bytes;
|
||
assert(name.len <= HostName.max_len);
|
||
|
||
if (is_windows) {
|
||
var name_buffer: [HostName.max_len + 1]u16 = undefined;
|
||
const name_len = std.unicode.wtf8ToWtf16Le(&name_buffer, host_name.bytes) catch
|
||
unreachable; // HostName is prevalidated.
|
||
name_buffer[name_len] = 0;
|
||
const name_w = name_buffer[0..name_len :0];
|
||
|
||
var port_buffer: [8]u8 = undefined;
|
||
var port_buffer_wide: [8]u16 = undefined;
|
||
const port = std.fmt.bufPrint(&port_buffer, "{d}", .{options.port}) catch
|
||
unreachable; // `port_buffer` is big enough for decimal u16.
|
||
for (port, port_buffer_wide[0..port.len]) |byte, *wide|
|
||
wide.* = std.mem.nativeToLittle(u16, byte);
|
||
port_buffer_wide[port.len] = 0;
|
||
const port_w = port_buffer_wide[0..port.len :0];
|
||
|
||
const hints: ws2_32.ADDRINFOEXW = .{
|
||
.flags = .{ .NUMERICSERV = true },
|
||
.family = if (options.family) |f| switch (f) {
|
||
.ip4 => posix.AF.INET,
|
||
.ip6 => posix.AF.INET6,
|
||
} else posix.AF.UNSPEC,
|
||
.socktype = posix.SOCK.STREAM,
|
||
.protocol = posix.IPPROTO.TCP,
|
||
.canonname = null,
|
||
.addr = null,
|
||
.addrlen = 0,
|
||
.blob = null,
|
||
.bloblen = 0,
|
||
.provider = null,
|
||
.next = null,
|
||
};
|
||
const cancel_handle: ?*windows.HANDLE = null;
|
||
var res: *ws2_32.ADDRINFOEXW = undefined;
|
||
const timeout: ?*ws2_32.timeval = null;
|
||
while (true) {
|
||
try current_thread.checkCancel(); // TODO make requestCancel call GetAddrInfoExCancel
|
||
// TODO make this append to the queue eagerly rather than blocking until
|
||
// the whole thing finishes
|
||
const rc: ws2_32.WinsockError = @enumFromInt(ws2_32.GetAddrInfoExW(name_w, port_w, .DNS, null, &hints, &res, timeout, null, null, cancel_handle));
|
||
switch (rc) {
|
||
@as(ws2_32.WinsockError, @enumFromInt(0)) => break,
|
||
.EINTR => continue,
|
||
.ECANCELLED, .E_CANCELLED, .OPERATION_ABORTED => return error.Canceled,
|
||
.NOTINITIALISED => {
|
||
try initializeWsa(t);
|
||
continue;
|
||
},
|
||
.TRY_AGAIN => return error.NameServerFailure,
|
||
.EINVAL => |err| return wsaErrorBug(err),
|
||
.NO_RECOVERY => return error.NameServerFailure,
|
||
.EAFNOSUPPORT => return error.AddressFamilyUnsupported,
|
||
.NOT_ENOUGH_MEMORY => return error.SystemResources,
|
||
.HOST_NOT_FOUND => return error.UnknownHostName,
|
||
.TYPE_NOT_FOUND => return error.ProtocolUnsupportedByAddressFamily,
|
||
.ESOCKTNOSUPPORT => return error.ProtocolUnsupportedBySystem,
|
||
else => |err| return windows.unexpectedWSAError(err),
|
||
}
|
||
}
|
||
defer ws2_32.FreeAddrInfoExW(res);
|
||
|
||
var it: ?*ws2_32.ADDRINFOEXW = res;
|
||
var canon_name: ?[*:0]const u16 = null;
|
||
while (it) |info| : (it = info.next) {
|
||
const addr = info.addr orelse continue;
|
||
const storage: WsaAddress = .{ .any = addr.* };
|
||
try resolved.putOne(t_io, .{ .address = addressFromWsa(&storage) });
|
||
|
||
if (info.canonname) |n| {
|
||
if (canon_name == null) {
|
||
canon_name = n;
|
||
}
|
||
}
|
||
}
|
||
if (canon_name) |n| {
|
||
const len = std.unicode.wtf16LeToWtf8(options.canonical_name_buffer, std.mem.sliceTo(n, 0));
|
||
try resolved.putOne(t_io, .{ .canonical_name = .{
|
||
.bytes = options.canonical_name_buffer[0..len],
|
||
} });
|
||
}
|
||
return;
|
||
}
|
||
|
||
// On Linux, glibc provides getaddrinfo_a which is capable of supporting our semantics.
|
||
// However, musl's POSIX-compliant getaddrinfo is not, so we bypass it.
|
||
|
||
if (builtin.target.isGnuLibC()) {
|
||
// TODO use getaddrinfo_a / gai_cancel
|
||
}
|
||
|
||
if (native_os == .linux) {
|
||
if (options.family != .ip4) {
|
||
if (IpAddress.parseIp6(name, options.port)) |addr| {
|
||
try resolved.putAll(t_io, &.{
|
||
.{ .address = addr },
|
||
.{ .canonical_name = copyCanon(options.canonical_name_buffer, name) },
|
||
});
|
||
return;
|
||
} else |_| {}
|
||
}
|
||
|
||
if (options.family != .ip6) {
|
||
if (IpAddress.parseIp4(name, options.port)) |addr| {
|
||
try resolved.putAll(t_io, &.{
|
||
.{ .address = addr },
|
||
.{ .canonical_name = copyCanon(options.canonical_name_buffer, name) },
|
||
});
|
||
return;
|
||
} else |_| {}
|
||
}
|
||
|
||
lookupHosts(t, host_name, resolved, options) catch |err| switch (err) {
|
||
error.UnknownHostName => {},
|
||
else => |e| return e,
|
||
};
|
||
|
||
// RFC 6761 Section 6.3.3
|
||
// Name resolution APIs and libraries SHOULD recognize
|
||
// localhost names as special and SHOULD always return the IP
|
||
// loopback address for address queries and negative responses
|
||
// for all other query types.
|
||
|
||
// Check for equal to "localhost(.)" or ends in ".localhost(.)"
|
||
const localhost = if (name[name.len - 1] == '.') "localhost." else "localhost";
|
||
if (std.mem.endsWith(u8, name, localhost) and
|
||
(name.len == localhost.len or name[name.len - localhost.len] == '.'))
|
||
{
|
||
var results_buffer: [3]HostName.LookupResult = undefined;
|
||
var results_index: usize = 0;
|
||
if (options.family != .ip4) {
|
||
results_buffer[results_index] = .{ .address = .{ .ip6 = .loopback(options.port) } };
|
||
results_index += 1;
|
||
}
|
||
if (options.family != .ip6) {
|
||
results_buffer[results_index] = .{ .address = .{ .ip4 = .loopback(options.port) } };
|
||
results_index += 1;
|
||
}
|
||
const canon_name = "localhost";
|
||
const canon_name_dest = options.canonical_name_buffer[0..canon_name.len];
|
||
canon_name_dest.* = canon_name.*;
|
||
results_buffer[results_index] = .{ .canonical_name = .{ .bytes = canon_name_dest } };
|
||
results_index += 1;
|
||
try resolved.putAll(t_io, results_buffer[0..results_index]);
|
||
return;
|
||
}
|
||
|
||
return lookupDnsSearch(t, host_name, resolved, options);
|
||
}
|
||
|
||
if (native_os == .openbsd) {
|
||
// TODO use getaddrinfo_async / asr_abort
|
||
}
|
||
|
||
if (native_os == .freebsd) {
|
||
// TODO use dnsres_getaddrinfo
|
||
}
|
||
|
||
if (native_os.isDarwin()) {
|
||
// TODO use CFHostStartInfoResolution / CFHostCancelInfoResolution
|
||
}
|
||
|
||
if (builtin.link_libc) {
|
||
// This operating system lacks a way to resolve asynchronously. We are
|
||
// stuck with getaddrinfo.
|
||
var name_buffer: [HostName.max_len + 1]u8 = undefined;
|
||
@memcpy(name_buffer[0..host_name.bytes.len], host_name.bytes);
|
||
name_buffer[host_name.bytes.len] = 0;
|
||
const name_c = name_buffer[0..host_name.bytes.len :0];
|
||
|
||
var port_buffer: [8]u8 = undefined;
|
||
const port_c = std.fmt.bufPrintZ(&port_buffer, "{d}", .{options.port}) catch unreachable;
|
||
|
||
const hints: posix.addrinfo = .{
|
||
.flags = .{ .NUMERICSERV = true },
|
||
.family = posix.AF.UNSPEC,
|
||
.socktype = posix.SOCK.STREAM,
|
||
.protocol = posix.IPPROTO.TCP,
|
||
.canonname = null,
|
||
.addr = null,
|
||
.addrlen = 0,
|
||
.next = null,
|
||
};
|
||
var res: ?*posix.addrinfo = null;
|
||
try current_thread.beginSyscall();
|
||
while (true) {
|
||
switch (posix.system.getaddrinfo(name_c.ptr, port_c.ptr, &hints, &res)) {
|
||
@as(posix.system.EAI, @enumFromInt(0)) => {
|
||
current_thread.endSyscall();
|
||
break;
|
||
},
|
||
.SYSTEM => switch (posix.errno(-1)) {
|
||
.INTR => {
|
||
try current_thread.checkCancel();
|
||
continue;
|
||
},
|
||
.CANCELED => return current_thread.endSyscallCanceled(),
|
||
else => |e| {
|
||
current_thread.endSyscall();
|
||
return posix.unexpectedErrno(e);
|
||
},
|
||
},
|
||
else => |e| {
|
||
current_thread.endSyscall();
|
||
switch (e) {
|
||
.ADDRFAMILY => return error.AddressFamilyUnsupported,
|
||
.AGAIN => return error.NameServerFailure,
|
||
.FAIL => return error.NameServerFailure,
|
||
.FAMILY => return error.AddressFamilyUnsupported,
|
||
.MEMORY => return error.SystemResources,
|
||
.NODATA => return error.UnknownHostName,
|
||
.NONAME => return error.UnknownHostName,
|
||
else => return error.Unexpected,
|
||
}
|
||
},
|
||
}
|
||
}
|
||
defer if (res) |some| posix.system.freeaddrinfo(some);
|
||
|
||
var it = res;
|
||
var canon_name: ?[*:0]const u8 = null;
|
||
while (it) |info| : (it = info.next) {
|
||
const addr = info.addr orelse continue;
|
||
const storage: PosixAddress = .{ .any = addr.* };
|
||
try resolved.putOne(t_io, .{ .address = addressFromPosix(&storage) });
|
||
|
||
if (info.canonname) |n| {
|
||
if (canon_name == null) {
|
||
canon_name = n;
|
||
}
|
||
}
|
||
}
|
||
if (canon_name) |n| {
|
||
try resolved.putOne(t_io, .{
|
||
.canonical_name = copyCanon(options.canonical_name_buffer, std.mem.sliceTo(n, 0)),
|
||
});
|
||
}
|
||
return;
|
||
}
|
||
|
||
return error.OptionUnsupported;
|
||
}
|
||
|
||
pub const PosixAddress = extern union {
|
||
any: posix.sockaddr,
|
||
in: posix.sockaddr.in,
|
||
in6: posix.sockaddr.in6,
|
||
};
|
||
|
||
const UnixAddress = extern union {
|
||
any: posix.sockaddr,
|
||
un: posix.sockaddr.un,
|
||
};
|
||
|
||
const WsaAddress = extern union {
|
||
any: ws2_32.sockaddr,
|
||
in: ws2_32.sockaddr.in,
|
||
in6: ws2_32.sockaddr.in6,
|
||
un: ws2_32.sockaddr.un,
|
||
};
|
||
|
||
pub fn posixAddressFamily(a: *const IpAddress) posix.sa_family_t {
|
||
return switch (a.*) {
|
||
.ip4 => posix.AF.INET,
|
||
.ip6 => posix.AF.INET6,
|
||
};
|
||
}
|
||
|
||
pub fn addressFromPosix(posix_address: *const PosixAddress) IpAddress {
|
||
return switch (posix_address.any.family) {
|
||
posix.AF.INET => .{ .ip4 = address4FromPosix(&posix_address.in) },
|
||
posix.AF.INET6 => .{ .ip6 = address6FromPosix(&posix_address.in6) },
|
||
else => .{ .ip4 = .loopback(0) },
|
||
};
|
||
}
|
||
|
||
fn addressFromWsa(wsa_address: *const WsaAddress) IpAddress {
|
||
return switch (wsa_address.any.family) {
|
||
posix.AF.INET => .{ .ip4 = address4FromWsa(&wsa_address.in) },
|
||
posix.AF.INET6 => .{ .ip6 = address6FromWsa(&wsa_address.in6) },
|
||
else => .{ .ip4 = .loopback(0) },
|
||
};
|
||
}
|
||
|
||
pub fn addressToPosix(a: *const IpAddress, storage: *PosixAddress) posix.socklen_t {
|
||
return switch (a.*) {
|
||
.ip4 => |ip4| {
|
||
storage.in = address4ToPosix(ip4);
|
||
return @sizeOf(posix.sockaddr.in);
|
||
},
|
||
.ip6 => |*ip6| {
|
||
storage.in6 = address6ToPosix(ip6);
|
||
return @sizeOf(posix.sockaddr.in6);
|
||
},
|
||
};
|
||
}
|
||
|
||
fn addressToWsa(a: *const IpAddress, storage: *WsaAddress) i32 {
|
||
return switch (a.*) {
|
||
.ip4 => |ip4| {
|
||
storage.in = address4ToPosix(ip4);
|
||
return @sizeOf(posix.sockaddr.in);
|
||
},
|
||
.ip6 => |*ip6| {
|
||
storage.in6 = address6ToPosix(ip6);
|
||
return @sizeOf(posix.sockaddr.in6);
|
||
},
|
||
};
|
||
}
|
||
|
||
fn addressUnixToPosix(a: *const net.UnixAddress, storage: *UnixAddress) posix.socklen_t {
|
||
@memcpy(storage.un.path[0..a.path.len], a.path);
|
||
storage.un.family = posix.AF.UNIX;
|
||
storage.un.path[a.path.len] = 0;
|
||
return @sizeOf(posix.sockaddr.un);
|
||
}
|
||
|
||
fn addressUnixToWsa(a: *const net.UnixAddress, storage: *WsaAddress) i32 {
|
||
@memcpy(storage.un.path[0..a.path.len], a.path);
|
||
storage.un.family = posix.AF.UNIX;
|
||
storage.un.path[a.path.len] = 0;
|
||
return @sizeOf(posix.sockaddr.un);
|
||
}
|
||
|
||
fn address4FromPosix(in: *const posix.sockaddr.in) net.Ip4Address {
|
||
return .{
|
||
.port = std.mem.bigToNative(u16, in.port),
|
||
.bytes = @bitCast(in.addr),
|
||
};
|
||
}
|
||
|
||
fn address6FromPosix(in6: *const posix.sockaddr.in6) net.Ip6Address {
|
||
return .{
|
||
.port = std.mem.bigToNative(u16, in6.port),
|
||
.bytes = in6.addr,
|
||
.flow = in6.flowinfo,
|
||
.interface = .{ .index = in6.scope_id },
|
||
};
|
||
}
|
||
|
||
fn address4FromWsa(in: *const ws2_32.sockaddr.in) net.Ip4Address {
|
||
return .{
|
||
.port = std.mem.bigToNative(u16, in.port),
|
||
.bytes = @bitCast(in.addr),
|
||
};
|
||
}
|
||
|
||
fn address6FromWsa(in6: *const ws2_32.sockaddr.in6) net.Ip6Address {
|
||
return .{
|
||
.port = std.mem.bigToNative(u16, in6.port),
|
||
.bytes = in6.addr,
|
||
.flow = in6.flowinfo,
|
||
.interface = .{ .index = in6.scope_id },
|
||
};
|
||
}
|
||
|
||
fn address4ToPosix(a: net.Ip4Address) posix.sockaddr.in {
|
||
return .{
|
||
.port = std.mem.nativeToBig(u16, a.port),
|
||
.addr = @bitCast(a.bytes),
|
||
};
|
||
}
|
||
|
||
fn address6ToPosix(a: *const net.Ip6Address) posix.sockaddr.in6 {
|
||
return .{
|
||
.port = std.mem.nativeToBig(u16, a.port),
|
||
.flowinfo = a.flow,
|
||
.addr = a.bytes,
|
||
.scope_id = a.interface.index,
|
||
};
|
||
}
|
||
|
||
pub fn errnoBug(err: posix.E) Io.UnexpectedError {
|
||
if (is_debug) std.debug.panic("programmer bug caused syscall error: {t}", .{err});
|
||
return error.Unexpected;
|
||
}
|
||
|
||
fn wsaErrorBug(err: ws2_32.WinsockError) Io.UnexpectedError {
|
||
if (is_debug) std.debug.panic("programmer bug caused syscall error: {t}", .{err});
|
||
return error.Unexpected;
|
||
}
|
||
|
||
pub fn posixSocketMode(mode: net.Socket.Mode) u32 {
|
||
return switch (mode) {
|
||
.stream => posix.SOCK.STREAM,
|
||
.dgram => posix.SOCK.DGRAM,
|
||
.seqpacket => posix.SOCK.SEQPACKET,
|
||
.raw => posix.SOCK.RAW,
|
||
.rdm => posix.SOCK.RDM,
|
||
};
|
||
}
|
||
|
||
pub fn posixProtocol(protocol: ?net.Protocol) u32 {
|
||
return @intFromEnum(protocol orelse return 0);
|
||
}
|
||
|
||
fn recoverableOsBugDetected() void {
|
||
if (is_debug) unreachable;
|
||
}
|
||
|
||
fn clockToPosix(clock: Io.Clock) posix.clockid_t {
|
||
return switch (clock) {
|
||
.real => posix.CLOCK.REALTIME,
|
||
.awake => switch (native_os) {
|
||
.driverkit, .ios, .maccatalyst, .macos, .tvos, .visionos, .watchos => posix.CLOCK.UPTIME_RAW,
|
||
else => posix.CLOCK.MONOTONIC,
|
||
},
|
||
.boot => switch (native_os) {
|
||
.driverkit, .ios, .maccatalyst, .macos, .tvos, .visionos, .watchos => posix.CLOCK.MONOTONIC_RAW,
|
||
// On freebsd derivatives, use MONOTONIC_FAST as currently there's
|
||
// no precision tradeoff.
|
||
.freebsd, .dragonfly => posix.CLOCK.MONOTONIC_FAST,
|
||
// On linux, use BOOTTIME instead of MONOTONIC as it ticks while
|
||
// suspended.
|
||
.linux => posix.CLOCK.BOOTTIME,
|
||
// On other posix systems, MONOTONIC is generally the fastest and
|
||
// ticks while suspended.
|
||
else => posix.CLOCK.MONOTONIC,
|
||
},
|
||
.cpu_process => posix.CLOCK.PROCESS_CPUTIME_ID,
|
||
.cpu_thread => posix.CLOCK.THREAD_CPUTIME_ID,
|
||
};
|
||
}
|
||
|
||
fn clockToWasi(clock: Io.Clock) std.os.wasi.clockid_t {
|
||
return switch (clock) {
|
||
.real => .REALTIME,
|
||
.awake => .MONOTONIC,
|
||
.boot => .MONOTONIC,
|
||
.cpu_process => .PROCESS_CPUTIME_ID,
|
||
.cpu_thread => .THREAD_CPUTIME_ID,
|
||
};
|
||
}
|
||
|
||
fn statFromLinux(stx: *const std.os.linux.Statx) Io.File.Stat {
|
||
const atime = stx.atime;
|
||
const mtime = stx.mtime;
|
||
const ctime = stx.ctime;
|
||
return .{
|
||
.inode = stx.ino,
|
||
.size = stx.size,
|
||
.mode = stx.mode,
|
||
.kind = switch (stx.mode & std.os.linux.S.IFMT) {
|
||
std.os.linux.S.IFDIR => .directory,
|
||
std.os.linux.S.IFCHR => .character_device,
|
||
std.os.linux.S.IFBLK => .block_device,
|
||
std.os.linux.S.IFREG => .file,
|
||
std.os.linux.S.IFIFO => .named_pipe,
|
||
std.os.linux.S.IFLNK => .sym_link,
|
||
std.os.linux.S.IFSOCK => .unix_domain_socket,
|
||
else => .unknown,
|
||
},
|
||
.atime = .{ .nanoseconds = @intCast(@as(i128, atime.sec) * std.time.ns_per_s + atime.nsec) },
|
||
.mtime = .{ .nanoseconds = @intCast(@as(i128, mtime.sec) * std.time.ns_per_s + mtime.nsec) },
|
||
.ctime = .{ .nanoseconds = @intCast(@as(i128, ctime.sec) * std.time.ns_per_s + ctime.nsec) },
|
||
};
|
||
}
|
||
|
||
fn statFromPosix(st: *const posix.Stat) Io.File.Stat {
|
||
const atime = st.atime();
|
||
const mtime = st.mtime();
|
||
const ctime = st.ctime();
|
||
return .{
|
||
.inode = st.ino,
|
||
.size = @bitCast(st.size),
|
||
.mode = st.mode,
|
||
.kind = k: {
|
||
const m = st.mode & posix.S.IFMT;
|
||
switch (m) {
|
||
posix.S.IFBLK => break :k .block_device,
|
||
posix.S.IFCHR => break :k .character_device,
|
||
posix.S.IFDIR => break :k .directory,
|
||
posix.S.IFIFO => break :k .named_pipe,
|
||
posix.S.IFLNK => break :k .sym_link,
|
||
posix.S.IFREG => break :k .file,
|
||
posix.S.IFSOCK => break :k .unix_domain_socket,
|
||
else => {},
|
||
}
|
||
if (native_os == .illumos) switch (m) {
|
||
posix.S.IFDOOR => break :k .door,
|
||
posix.S.IFPORT => break :k .event_port,
|
||
else => {},
|
||
};
|
||
|
||
break :k .unknown;
|
||
},
|
||
.atime = timestampFromPosix(&atime),
|
||
.mtime = timestampFromPosix(&mtime),
|
||
.ctime = timestampFromPosix(&ctime),
|
||
};
|
||
}
|
||
|
||
fn statFromWasi(st: *const std.os.wasi.filestat_t) Io.File.Stat {
|
||
return .{
|
||
.inode = st.ino,
|
||
.size = @bitCast(st.size),
|
||
.mode = 0,
|
||
.kind = switch (st.filetype) {
|
||
.BLOCK_DEVICE => .block_device,
|
||
.CHARACTER_DEVICE => .character_device,
|
||
.DIRECTORY => .directory,
|
||
.SYMBOLIC_LINK => .sym_link,
|
||
.REGULAR_FILE => .file,
|
||
.SOCKET_STREAM, .SOCKET_DGRAM => .unix_domain_socket,
|
||
else => .unknown,
|
||
},
|
||
.atime = .fromNanoseconds(st.atim),
|
||
.mtime = .fromNanoseconds(st.mtim),
|
||
.ctime = .fromNanoseconds(st.ctim),
|
||
};
|
||
}
|
||
|
||
fn timestampFromPosix(timespec: *const posix.timespec) Io.Timestamp {
|
||
return .{ .nanoseconds = @intCast(@as(i128, timespec.sec) * std.time.ns_per_s + timespec.nsec) };
|
||
}
|
||
|
||
fn timestampToPosix(nanoseconds: i96) posix.timespec {
|
||
return .{
|
||
.sec = @intCast(@divFloor(nanoseconds, std.time.ns_per_s)),
|
||
.nsec = @intCast(@mod(nanoseconds, std.time.ns_per_s)),
|
||
};
|
||
}
|
||
|
||
fn pathToPosix(file_path: []const u8, buffer: *[posix.PATH_MAX]u8) Io.Dir.PathNameError![:0]u8 {
|
||
if (std.mem.containsAtLeastScalar2(u8, file_path, 0, 1)) return error.BadPathName;
|
||
// >= rather than > to make room for the null byte
|
||
if (file_path.len >= buffer.len) return error.NameTooLong;
|
||
@memcpy(buffer[0..file_path.len], file_path);
|
||
buffer[file_path.len] = 0;
|
||
return buffer[0..file_path.len :0];
|
||
}
|
||
|
||
fn lookupDnsSearch(
|
||
t: *Threaded,
|
||
host_name: HostName,
|
||
resolved: *Io.Queue(HostName.LookupResult),
|
||
options: HostName.LookupOptions,
|
||
) HostName.LookupError!void {
|
||
const t_io = io(t);
|
||
const rc = HostName.ResolvConf.init(t_io) catch return error.ResolvConfParseFailed;
|
||
|
||
// Count dots, suppress search when >=ndots or name ends in
|
||
// a dot, which is an explicit request for global scope.
|
||
const dots = std.mem.countScalar(u8, host_name.bytes, '.');
|
||
const search_len = if (dots >= rc.ndots or std.mem.endsWith(u8, host_name.bytes, ".")) 0 else rc.search_len;
|
||
const search = rc.search_buffer[0..search_len];
|
||
|
||
var canon_name = host_name.bytes;
|
||
|
||
// Strip final dot for canon, fail if multiple trailing dots.
|
||
if (std.mem.endsWith(u8, canon_name, ".")) canon_name.len -= 1;
|
||
if (std.mem.endsWith(u8, canon_name, ".")) return error.UnknownHostName;
|
||
|
||
// Name with search domain appended is set up in `canon_name`. This
|
||
// both provides the desired default canonical name (if the requested
|
||
// name is not a CNAME record) and serves as a buffer for passing the
|
||
// full requested name to `lookupDns`.
|
||
@memcpy(options.canonical_name_buffer[0..canon_name.len], canon_name);
|
||
options.canonical_name_buffer[canon_name.len] = '.';
|
||
var it = std.mem.tokenizeAny(u8, search, " \t");
|
||
while (it.next()) |token| {
|
||
@memcpy(options.canonical_name_buffer[canon_name.len + 1 ..][0..token.len], token);
|
||
const lookup_canon_name = options.canonical_name_buffer[0 .. canon_name.len + 1 + token.len];
|
||
if (lookupDns(t, lookup_canon_name, &rc, resolved, options)) |result| {
|
||
return result;
|
||
} else |err| switch (err) {
|
||
error.UnknownHostName => continue,
|
||
else => |e| return e,
|
||
}
|
||
}
|
||
|
||
const lookup_canon_name = options.canonical_name_buffer[0..canon_name.len];
|
||
return lookupDns(t, lookup_canon_name, &rc, resolved, options);
|
||
}
|
||
|
||
fn lookupDns(
|
||
t: *Threaded,
|
||
lookup_canon_name: []const u8,
|
||
rc: *const HostName.ResolvConf,
|
||
resolved: *Io.Queue(HostName.LookupResult),
|
||
options: HostName.LookupOptions,
|
||
) HostName.LookupError!void {
|
||
const t_io = io(t);
|
||
const family_records: [2]struct { af: IpAddress.Family, rr: HostName.DnsRecord } = .{
|
||
.{ .af = .ip6, .rr = .A },
|
||
.{ .af = .ip4, .rr = .AAAA },
|
||
};
|
||
var query_buffers: [2][280]u8 = undefined;
|
||
var answer_buffer: [2 * 512]u8 = undefined;
|
||
var queries_buffer: [2][]const u8 = undefined;
|
||
var answers_buffer: [2][]const u8 = undefined;
|
||
var nq: usize = 0;
|
||
var answer_buffer_i: usize = 0;
|
||
|
||
for (family_records) |fr| {
|
||
if (options.family != fr.af) {
|
||
const entropy = std.crypto.random.array(u8, 2);
|
||
const len = writeResolutionQuery(&query_buffers[nq], 0, lookup_canon_name, 1, fr.rr, entropy);
|
||
queries_buffer[nq] = query_buffers[nq][0..len];
|
||
nq += 1;
|
||
}
|
||
}
|
||
|
||
var ip4_mapped_buffer: [HostName.ResolvConf.max_nameservers]IpAddress = undefined;
|
||
const ip4_mapped = ip4_mapped_buffer[0..rc.nameservers_len];
|
||
var any_ip6 = false;
|
||
for (rc.nameservers(), ip4_mapped) |*ns, *m| {
|
||
m.* = .{ .ip6 = .fromAny(ns.*) };
|
||
any_ip6 = any_ip6 or ns.* == .ip6;
|
||
}
|
||
var socket = s: {
|
||
if (any_ip6) ip6: {
|
||
const ip6_addr: IpAddress = .{ .ip6 = .unspecified(0) };
|
||
const socket = ip6_addr.bind(t_io, .{ .ip6_only = true, .mode = .dgram }) catch |err| switch (err) {
|
||
error.AddressFamilyUnsupported => break :ip6,
|
||
else => |e| return e,
|
||
};
|
||
break :s socket;
|
||
}
|
||
any_ip6 = false;
|
||
const ip4_addr: IpAddress = .{ .ip4 = .unspecified(0) };
|
||
const socket = try ip4_addr.bind(t_io, .{ .mode = .dgram });
|
||
break :s socket;
|
||
};
|
||
defer socket.close(t_io);
|
||
|
||
const mapped_nameservers = if (any_ip6) ip4_mapped else rc.nameservers();
|
||
const queries = queries_buffer[0..nq];
|
||
const answers = answers_buffer[0..queries.len];
|
||
var answers_remaining = answers.len;
|
||
for (answers) |*answer| answer.len = 0;
|
||
|
||
// boot clock is chosen because time the computer is suspended should count
|
||
// against time spent waiting for external messages to arrive.
|
||
const clock: Io.Clock = .boot;
|
||
var now_ts = try clock.now(t_io);
|
||
const final_ts = now_ts.addDuration(.fromSeconds(rc.timeout_seconds));
|
||
const attempt_duration: Io.Duration = .{
|
||
.nanoseconds = (std.time.ns_per_s / rc.attempts) * @as(i96, rc.timeout_seconds),
|
||
};
|
||
|
||
send: while (now_ts.nanoseconds < final_ts.nanoseconds) : (now_ts = try clock.now(t_io)) {
|
||
const max_messages = queries_buffer.len * HostName.ResolvConf.max_nameservers;
|
||
{
|
||
var message_buffer: [max_messages]Io.net.OutgoingMessage = undefined;
|
||
var message_i: usize = 0;
|
||
for (queries, answers) |query, *answer| {
|
||
if (answer.len != 0) continue;
|
||
for (mapped_nameservers) |*ns| {
|
||
message_buffer[message_i] = .{
|
||
.address = ns,
|
||
.data_ptr = query.ptr,
|
||
.data_len = query.len,
|
||
};
|
||
message_i += 1;
|
||
}
|
||
}
|
||
_ = netSendPosix(t, socket.handle, message_buffer[0..message_i], .{});
|
||
}
|
||
|
||
const timeout: Io.Timeout = .{ .deadline = .{
|
||
.raw = now_ts.addDuration(attempt_duration),
|
||
.clock = clock,
|
||
} };
|
||
|
||
while (true) {
|
||
var message_buffer: [max_messages]Io.net.IncomingMessage = @splat(.init);
|
||
const buf = answer_buffer[answer_buffer_i..];
|
||
const recv_err, const recv_n = socket.receiveManyTimeout(t_io, &message_buffer, buf, .{}, timeout);
|
||
for (message_buffer[0..recv_n]) |*received_message| {
|
||
const reply = received_message.data;
|
||
// Ignore non-identifiable packets.
|
||
if (reply.len < 4) continue;
|
||
|
||
// Ignore replies from addresses we didn't send to.
|
||
const ns = for (mapped_nameservers) |*ns| {
|
||
if (received_message.from.eql(ns)) break ns;
|
||
} else {
|
||
continue;
|
||
};
|
||
|
||
// Find which query this answer goes with, if any.
|
||
const query, const answer = for (queries, answers) |query, *answer| {
|
||
if (reply[0] == query[0] and reply[1] == query[1]) break .{ query, answer };
|
||
} else {
|
||
continue;
|
||
};
|
||
if (answer.len != 0) continue;
|
||
|
||
// Only accept positive or negative responses; retry immediately on
|
||
// server failure, and ignore all other codes such as refusal.
|
||
switch (reply[3] & 15) {
|
||
0, 3 => {
|
||
answer.* = reply;
|
||
answer_buffer_i += reply.len;
|
||
answers_remaining -= 1;
|
||
if (answer_buffer.len - answer_buffer_i == 0) break :send;
|
||
if (answers_remaining == 0) break :send;
|
||
},
|
||
2 => {
|
||
var retry_message: Io.net.OutgoingMessage = .{
|
||
.address = ns,
|
||
.data_ptr = query.ptr,
|
||
.data_len = query.len,
|
||
};
|
||
_ = netSendPosix(t, socket.handle, (&retry_message)[0..1], .{});
|
||
continue;
|
||
},
|
||
else => continue,
|
||
}
|
||
}
|
||
if (recv_err) |err| switch (err) {
|
||
error.Canceled => return error.Canceled,
|
||
error.Timeout => continue :send,
|
||
else => continue,
|
||
};
|
||
}
|
||
} else {
|
||
return error.NameServerFailure;
|
||
}
|
||
|
||
var addresses_len: usize = 0;
|
||
var canonical_name: ?HostName = null;
|
||
|
||
for (answers) |answer| {
|
||
var it = HostName.DnsResponse.init(answer) catch {
|
||
// Here we could potentially add diagnostics to the results queue.
|
||
continue;
|
||
};
|
||
while (it.next() catch {
|
||
// Here we could potentially add diagnostics to the results queue.
|
||
continue;
|
||
}) |record| switch (record.rr) {
|
||
.A => {
|
||
const data = record.packet[record.data_off..][0..record.data_len];
|
||
if (data.len != 4) return error.InvalidDnsARecord;
|
||
try resolved.putOne(t_io, .{ .address = .{ .ip4 = .{
|
||
.bytes = data[0..4].*,
|
||
.port = options.port,
|
||
} } });
|
||
addresses_len += 1;
|
||
},
|
||
.AAAA => {
|
||
const data = record.packet[record.data_off..][0..record.data_len];
|
||
if (data.len != 16) return error.InvalidDnsAAAARecord;
|
||
try resolved.putOne(t_io, .{ .address = .{ .ip6 = .{
|
||
.bytes = data[0..16].*,
|
||
.port = options.port,
|
||
} } });
|
||
addresses_len += 1;
|
||
},
|
||
.CNAME => {
|
||
_, canonical_name = HostName.expand(record.packet, record.data_off, options.canonical_name_buffer) catch
|
||
return error.InvalidDnsCnameRecord;
|
||
},
|
||
_ => continue,
|
||
};
|
||
}
|
||
|
||
try resolved.putOne(t_io, .{ .canonical_name = canonical_name orelse .{ .bytes = lookup_canon_name } });
|
||
if (addresses_len == 0) return error.NameServerFailure;
|
||
}
|
||
|
||
fn lookupHosts(
|
||
t: *Threaded,
|
||
host_name: HostName,
|
||
resolved: *Io.Queue(HostName.LookupResult),
|
||
options: HostName.LookupOptions,
|
||
) !void {
|
||
const t_io = io(t);
|
||
const file = Io.File.openAbsolute(t_io, "/etc/hosts", .{}) catch |err| switch (err) {
|
||
error.FileNotFound,
|
||
error.NotDir,
|
||
error.AccessDenied,
|
||
=> return error.UnknownHostName,
|
||
|
||
error.Canceled => |e| return e,
|
||
|
||
else => {
|
||
// Here we could add more detailed diagnostics to the results queue.
|
||
return error.DetectingNetworkConfigurationFailed;
|
||
},
|
||
};
|
||
defer file.close(t_io);
|
||
|
||
var line_buf: [512]u8 = undefined;
|
||
var file_reader = file.reader(t_io, &line_buf);
|
||
return lookupHostsReader(t, host_name, resolved, options, &file_reader.interface) catch |err| switch (err) {
|
||
error.ReadFailed => switch (file_reader.err.?) {
|
||
error.Canceled => |e| return e,
|
||
else => {
|
||
// Here we could add more detailed diagnostics to the results queue.
|
||
return error.DetectingNetworkConfigurationFailed;
|
||
},
|
||
},
|
||
error.Canceled => |e| return e,
|
||
error.UnknownHostName => |e| return e,
|
||
};
|
||
}
|
||
|
||
fn lookupHostsReader(
|
||
t: *Threaded,
|
||
host_name: HostName,
|
||
resolved: *Io.Queue(HostName.LookupResult),
|
||
options: HostName.LookupOptions,
|
||
reader: *Io.Reader,
|
||
) error{ ReadFailed, Canceled, UnknownHostName }!void {
|
||
const t_io = io(t);
|
||
var addresses_len: usize = 0;
|
||
var canonical_name: ?HostName = null;
|
||
while (true) {
|
||
const line = reader.takeDelimiterExclusive('\n') catch |err| switch (err) {
|
||
error.StreamTooLong => {
|
||
// Skip lines that are too long.
|
||
_ = reader.discardDelimiterInclusive('\n') catch |e| switch (e) {
|
||
error.EndOfStream => break,
|
||
error.ReadFailed => return error.ReadFailed,
|
||
};
|
||
continue;
|
||
},
|
||
error.ReadFailed => return error.ReadFailed,
|
||
error.EndOfStream => break,
|
||
};
|
||
reader.toss(1);
|
||
var split_it = std.mem.splitScalar(u8, line, '#');
|
||
const no_comment_line = split_it.first();
|
||
|
||
var line_it = std.mem.tokenizeAny(u8, no_comment_line, " \t");
|
||
const ip_text = line_it.next() orelse continue;
|
||
var first_name_text: ?[]const u8 = null;
|
||
while (line_it.next()) |name_text| {
|
||
if (std.mem.eql(u8, name_text, host_name.bytes)) {
|
||
if (first_name_text == null) first_name_text = name_text;
|
||
break;
|
||
}
|
||
} else continue;
|
||
|
||
if (canonical_name == null) {
|
||
if (HostName.init(first_name_text.?)) |name_text| {
|
||
if (name_text.bytes.len <= options.canonical_name_buffer.len) {
|
||
const canonical_name_dest = options.canonical_name_buffer[0..name_text.bytes.len];
|
||
@memcpy(canonical_name_dest, name_text.bytes);
|
||
canonical_name = .{ .bytes = canonical_name_dest };
|
||
}
|
||
} else |_| {}
|
||
}
|
||
|
||
if (options.family != .ip6) {
|
||
if (IpAddress.parseIp4(ip_text, options.port)) |addr| {
|
||
try resolved.putOne(t_io, .{ .address = addr });
|
||
addresses_len += 1;
|
||
} else |_| {}
|
||
}
|
||
if (options.family != .ip4) {
|
||
if (IpAddress.parseIp6(ip_text, options.port)) |addr| {
|
||
try resolved.putOne(t_io, .{ .address = addr });
|
||
addresses_len += 1;
|
||
} else |_| {}
|
||
}
|
||
}
|
||
|
||
if (canonical_name) |canon_name| try resolved.putOne(t_io, .{ .canonical_name = canon_name });
|
||
if (addresses_len == 0) return error.UnknownHostName;
|
||
}
|
||
|
||
/// Writes DNS resolution query packet data to `w`; at most 280 bytes.
|
||
fn writeResolutionQuery(q: *[280]u8, op: u4, dname: []const u8, class: u8, ty: HostName.DnsRecord, entropy: [2]u8) usize {
|
||
// This implementation is ported from musl libc.
|
||
// A more idiomatic "ziggy" implementation would be welcome.
|
||
var name = dname;
|
||
if (std.mem.endsWith(u8, name, ".")) name.len -= 1;
|
||
assert(name.len <= 253);
|
||
const n = 17 + name.len + @intFromBool(name.len != 0);
|
||
|
||
// Construct query template - ID will be filled later
|
||
q[0..2].* = entropy;
|
||
@memset(q[2..n], 0);
|
||
q[2] = @as(u8, op) * 8 + 1;
|
||
q[5] = 1;
|
||
@memcpy(q[13..][0..name.len], name);
|
||
var i: usize = 13;
|
||
var j: usize = undefined;
|
||
while (q[i] != 0) : (i = j + 1) {
|
||
j = i;
|
||
while (q[j] != 0 and q[j] != '.') : (j += 1) {}
|
||
// TODO determine the circumstances for this and whether or
|
||
// not this should be an error.
|
||
if (j - i - 1 > 62) unreachable;
|
||
q[i - 1] = @intCast(j - i);
|
||
}
|
||
q[i + 1] = @intFromEnum(ty);
|
||
q[i + 3] = class;
|
||
return n;
|
||
}
|
||
|
||
fn copyCanon(canonical_name_buffer: *[HostName.max_len]u8, name: []const u8) HostName {
|
||
const dest = canonical_name_buffer[0..name.len];
|
||
@memcpy(dest, name);
|
||
return .{ .bytes = dest };
|
||
}
|
||
|
||
/// Darwin XNU 7195.50.7.100.1 introduced __ulock_wait2 and migrated code paths (notably pthread_cond_t) towards it:
|
||
/// https://github.com/apple/darwin-xnu/commit/d4061fb0260b3ed486147341b72468f836ed6c8f#diff-08f993cc40af475663274687b7c326cc6c3031e0db3ac8de7b24624610616be6
|
||
///
|
||
/// This XNU version appears to correspond to 11.0.1:
|
||
/// https://kernelshaman.blogspot.com/2021/01/building-xnu-for-macos-big-sur-1101.html
|
||
///
|
||
/// ulock_wait() uses 32-bit micro-second timeouts where 0 = INFINITE or no-timeout
|
||
/// ulock_wait2() uses 64-bit nano-second timeouts (with the same convention)
|
||
const darwin_supports_ulock_wait2 = builtin.os.version_range.semver.min.major >= 11;
|
||
|
||
fn futexWait(current_thread: *Thread, ptr: *const std.atomic.Value(u32), expect: u32) Io.Cancelable!void {
|
||
@branchHint(.cold);
|
||
|
||
if (builtin.cpu.arch.isWasm()) {
|
||
comptime assert(builtin.cpu.has(.wasm, .atomics));
|
||
try current_thread.checkCancel();
|
||
const timeout: i64 = -1;
|
||
const signed_expect: i32 = @bitCast(expect);
|
||
const result = asm volatile (
|
||
\\local.get %[ptr]
|
||
\\local.get %[expected]
|
||
\\local.get %[timeout]
|
||
\\memory.atomic.wait32 0
|
||
\\local.set %[ret]
|
||
: [ret] "=r" (-> u32),
|
||
: [ptr] "r" (&ptr.raw),
|
||
[expected] "r" (signed_expect),
|
||
[timeout] "r" (timeout),
|
||
);
|
||
switch (result) {
|
||
0 => {}, // ok
|
||
1 => {}, // expected != loaded
|
||
2 => assert(!is_debug), // timeout
|
||
else => assert(!is_debug),
|
||
}
|
||
} else switch (native_os) {
|
||
.linux => {
|
||
const linux = std.os.linux;
|
||
try current_thread.beginSyscall();
|
||
const rc = linux.futex_4arg(ptr, .{ .cmd = .WAIT, .private = true }, expect, null);
|
||
current_thread.endSyscall();
|
||
switch (linux.errno(rc)) {
|
||
.SUCCESS => {}, // notified by `wake()`
|
||
.INTR => {}, // caller's responsibility to retry
|
||
.AGAIN => {}, // ptr.* != expect
|
||
.INVAL => {}, // possibly timeout overflow
|
||
.TIMEDOUT => recoverableOsBugDetected(),
|
||
.FAULT => recoverableOsBugDetected(), // ptr was invalid
|
||
else => recoverableOsBugDetected(),
|
||
}
|
||
},
|
||
.driverkit, .ios, .maccatalyst, .macos, .tvos, .visionos, .watchos => {
|
||
const c = std.c;
|
||
const flags: c.UL = .{
|
||
.op = .COMPARE_AND_WAIT,
|
||
.NO_ERRNO = true,
|
||
};
|
||
try current_thread.beginSyscall();
|
||
const status = if (darwin_supports_ulock_wait2)
|
||
c.__ulock_wait2(flags, ptr, expect, 0, 0)
|
||
else
|
||
c.__ulock_wait(flags, ptr, expect, 0);
|
||
current_thread.endSyscall();
|
||
|
||
if (status >= 0) return;
|
||
|
||
if (is_debug) switch (@as(c.E, @enumFromInt(-status))) {
|
||
.INTR => {}, // spurious wake
|
||
// Address of the futex was paged out. This is unlikely, but possible in theory, and
|
||
// pthread/libdispatch on darwin bother to handle it. In this case we'll return
|
||
// without waiting, but the caller should retry anyway.
|
||
.FAULT => {},
|
||
.TIMEDOUT => unreachable,
|
||
else => unreachable,
|
||
};
|
||
},
|
||
.windows => {
|
||
try current_thread.checkCancel();
|
||
switch (windows.ntdll.RtlWaitOnAddress(ptr, &expect, @sizeOf(@TypeOf(expect)), null)) {
|
||
.SUCCESS => {},
|
||
.CANCELLED => return error.Canceled,
|
||
else => recoverableOsBugDetected(),
|
||
}
|
||
},
|
||
.freebsd => {
|
||
const flags = @intFromEnum(std.c.UMTX_OP.WAIT_UINT_PRIVATE);
|
||
try current_thread.beginSyscall();
|
||
const rc = std.c._umtx_op(@intFromPtr(&ptr.raw), flags, @as(c_ulong, expect), 0, 0);
|
||
current_thread.endSyscall();
|
||
if (is_debug) switch (posix.errno(rc)) {
|
||
.SUCCESS => {},
|
||
.FAULT => unreachable, // one of the args points to invalid memory
|
||
.INVAL => unreachable, // arguments should be correct
|
||
.TIMEDOUT => unreachable, // no timeout provided
|
||
.INTR => {}, // spurious wake
|
||
else => unreachable,
|
||
};
|
||
},
|
||
else => @compileError("unimplemented: futexWait"),
|
||
}
|
||
}
|
||
|
||
pub fn futexWaitUncancelable(ptr: *const std.atomic.Value(u32), expect: u32) void {
|
||
@branchHint(.cold);
|
||
|
||
if (builtin.cpu.arch.isWasm()) {
|
||
comptime assert(builtin.cpu.has(.wasm, .atomics));
|
||
const timeout: i64 = -1;
|
||
const signed_expect: i32 = @bitCast(expect);
|
||
const result = asm volatile (
|
||
\\local.get %[ptr]
|
||
\\local.get %[expected]
|
||
\\local.get %[timeout]
|
||
\\memory.atomic.wait32 0
|
||
\\local.set %[ret]
|
||
: [ret] "=r" (-> u32),
|
||
: [ptr] "r" (&ptr.raw),
|
||
[expected] "r" (signed_expect),
|
||
[timeout] "r" (timeout),
|
||
);
|
||
switch (result) {
|
||
0 => {}, // ok
|
||
1 => {}, // expected != loaded
|
||
2 => recoverableOsBugDetected(), // timeout
|
||
else => recoverableOsBugDetected(),
|
||
}
|
||
} else switch (native_os) {
|
||
.linux => {
|
||
const linux = std.os.linux;
|
||
const rc = linux.futex_4arg(ptr, .{ .cmd = .WAIT, .private = true }, expect, null);
|
||
switch (linux.errno(rc)) {
|
||
.SUCCESS => {}, // notified by `wake()`
|
||
.INTR => {}, // caller's responsibility to repeat
|
||
.AGAIN => {}, // ptr.* != expect
|
||
.INVAL => {}, // possibly timeout overflow
|
||
.TIMEDOUT => recoverableOsBugDetected(),
|
||
.FAULT => recoverableOsBugDetected(), // ptr was invalid
|
||
else => recoverableOsBugDetected(),
|
||
}
|
||
},
|
||
.driverkit, .ios, .maccatalyst, .macos, .tvos, .visionos, .watchos => {
|
||
const c = std.c;
|
||
const flags: c.UL = .{
|
||
.op = .COMPARE_AND_WAIT,
|
||
.NO_ERRNO = true,
|
||
};
|
||
const status = if (darwin_supports_ulock_wait2)
|
||
c.__ulock_wait2(flags, ptr, expect, 0, 0)
|
||
else
|
||
c.__ulock_wait(flags, ptr, expect, 0);
|
||
|
||
if (status >= 0) return;
|
||
|
||
switch (@as(c.E, @enumFromInt(-status))) {
|
||
// Wait was interrupted by the OS or other spurious signalling.
|
||
.INTR => {},
|
||
// Address of the futex was paged out. This is unlikely, but possible in theory, and
|
||
// pthread/libdispatch on darwin bother to handle it. In this case we'll return
|
||
// without waiting, but the caller should retry anyway.
|
||
.FAULT => {},
|
||
.TIMEDOUT => recoverableOsBugDetected(),
|
||
else => recoverableOsBugDetected(),
|
||
}
|
||
},
|
||
.windows => {
|
||
switch (windows.ntdll.RtlWaitOnAddress(ptr, &expect, @sizeOf(@TypeOf(expect)), null)) {
|
||
.SUCCESS, .CANCELLED => {},
|
||
else => recoverableOsBugDetected(),
|
||
}
|
||
},
|
||
.freebsd => {
|
||
const flags = @intFromEnum(std.c.UMTX_OP.WAIT_UINT_PRIVATE);
|
||
const rc = std.c._umtx_op(@intFromPtr(&ptr.raw), flags, @as(c_ulong, expect), 0, 0);
|
||
switch (posix.errno(rc)) {
|
||
.SUCCESS => {},
|
||
.INTR => {}, // spurious wake
|
||
.FAULT => recoverableOsBugDetected(), // one of the args points to invalid memory
|
||
.INVAL => recoverableOsBugDetected(), // arguments should be correct
|
||
.TIMEDOUT => recoverableOsBugDetected(), // no timeout provided
|
||
else => recoverableOsBugDetected(),
|
||
}
|
||
},
|
||
else => @compileError("unimplemented: futexWaitUncancelable"),
|
||
}
|
||
}
|
||
|
||
pub fn futexWake(ptr: *const std.atomic.Value(u32), max_waiters: u32) void {
|
||
@branchHint(.cold);
|
||
|
||
if (builtin.cpu.arch.isWasm()) {
|
||
comptime assert(builtin.cpu.has(.wasm, .atomics));
|
||
assert(max_waiters != 0);
|
||
const woken_count = asm volatile (
|
||
\\local.get %[ptr]
|
||
\\local.get %[waiters]
|
||
\\memory.atomic.notify 0
|
||
\\local.set %[ret]
|
||
: [ret] "=r" (-> u32),
|
||
: [ptr] "r" (&ptr.raw),
|
||
[waiters] "r" (max_waiters),
|
||
);
|
||
_ = woken_count; // can be 0 when linker flag 'shared-memory' is not enabled
|
||
} else switch (native_os) {
|
||
.linux => {
|
||
const linux = std.os.linux;
|
||
switch (linux.errno(linux.futex_3arg(
|
||
&ptr.raw,
|
||
.{ .cmd = .WAKE, .private = true },
|
||
@min(max_waiters, std.math.maxInt(i32)),
|
||
))) {
|
||
.SUCCESS => return, // successful wake up
|
||
.INVAL => return, // invalid futex_wait() on ptr done elsewhere
|
||
.FAULT => return, // pointer became invalid while doing the wake
|
||
else => return recoverableOsBugDetected(), // deadlock due to operating system bug
|
||
}
|
||
},
|
||
.driverkit, .ios, .maccatalyst, .macos, .tvos, .visionos, .watchos => {
|
||
const c = std.c;
|
||
const flags: c.UL = .{
|
||
.op = .COMPARE_AND_WAIT,
|
||
.NO_ERRNO = true,
|
||
.WAKE_ALL = max_waiters > 1,
|
||
};
|
||
while (true) {
|
||
const status = c.__ulock_wake(flags, ptr, 0);
|
||
if (status >= 0) return;
|
||
switch (@as(c.E, @enumFromInt(-status))) {
|
||
.INTR, .CANCELED => continue, // spurious wake()
|
||
.FAULT => unreachable, // __ulock_wake doesn't generate EFAULT according to darwin pthread_cond_t
|
||
.NOENT => return, // nothing was woken up
|
||
.ALREADY => unreachable, // only for UL.Op.WAKE_THREAD
|
||
else => unreachable, // deadlock due to operating system bug
|
||
}
|
||
}
|
||
},
|
||
.windows => {
|
||
assert(max_waiters != 0);
|
||
switch (max_waiters) {
|
||
1 => windows.ntdll.RtlWakeAddressSingle(ptr),
|
||
else => windows.ntdll.RtlWakeAddressAll(ptr),
|
||
}
|
||
},
|
||
.freebsd => {
|
||
const rc = std.c._umtx_op(
|
||
@intFromPtr(&ptr.raw),
|
||
@intFromEnum(std.c.UMTX_OP.WAKE_PRIVATE),
|
||
@as(c_ulong, max_waiters),
|
||
0, // there is no timeout struct
|
||
0, // there is no timeout struct pointer
|
||
);
|
||
switch (posix.errno(rc)) {
|
||
.SUCCESS => {},
|
||
.FAULT => {}, // it's ok if the ptr doesn't point to valid memory
|
||
.INVAL => unreachable, // arguments should be correct
|
||
else => unreachable, // deadlock due to operating system bug
|
||
}
|
||
},
|
||
else => @compileError("unimplemented: futexWake"),
|
||
}
|
||
}
|
||
|
||
/// A thread-safe logical boolean value which can be `set` and `unset`.
|
||
///
|
||
/// It can also block threads until the value is set with cancelation via timed
|
||
/// waits. Statically initializable; four bytes on all targets.
|
||
pub const ResetEvent = switch (native_os) {
|
||
.illumos, .netbsd => ResetEventPosix,
|
||
else => ResetEventFutex,
|
||
};
|
||
|
||
/// A `ResetEvent` implementation based on futexes.
|
||
const ResetEventFutex = enum(u32) {
|
||
unset = 0,
|
||
waiting = 1,
|
||
is_set = 2,
|
||
|
||
/// Returns whether the logical boolean is `set`.
|
||
///
|
||
/// Once `reset` is called, this returns false until the next `set`.
|
||
///
|
||
/// The memory accesses before the `set` can be said to happen before
|
||
/// `isSet` returns true.
|
||
pub fn isSet(ref: *const ResetEventFutex) bool {
|
||
if (builtin.single_threaded) return switch (ref.*) {
|
||
.unset => false,
|
||
.waiting => unreachable,
|
||
.is_set => true,
|
||
};
|
||
// Acquire barrier ensures memory accesses before `set` happen before
|
||
// returning true.
|
||
return @atomicLoad(ResetEventFutex, ref, .acquire) == .is_set;
|
||
}
|
||
|
||
/// Blocks the calling thread until `set` is called.
|
||
///
|
||
/// This is effectively a more efficient version of `while (!isSet()) {}`.
|
||
///
|
||
/// The memory accesses before the `set` can be said to happen before `wait` returns.
|
||
pub fn wait(ref: *ResetEventFutex, t: *Threaded) Io.Cancelable!void {
|
||
if (builtin.single_threaded) switch (ref.*) {
|
||
.unset => unreachable, // Deadlock, no other threads to wake us up.
|
||
.waiting => unreachable, // Invalid state.
|
||
.is_set => return,
|
||
};
|
||
// Try to set the state from `unset` to `waiting` to indicate to the
|
||
// `set` thread that others are blocked on the ResetEventFutex. Avoid using
|
||
// any strict barriers until we know the ResetEventFutex is set.
|
||
var state = @atomicLoad(ResetEventFutex, ref, .acquire);
|
||
if (state == .is_set) {
|
||
@branchHint(.likely);
|
||
return;
|
||
}
|
||
if (state == .unset) {
|
||
state = @cmpxchgStrong(ResetEventFutex, ref, state, .waiting, .acquire, .acquire) orelse .waiting;
|
||
}
|
||
const current_thread = Thread.getCurrent(t);
|
||
while (state == .waiting) {
|
||
try futexWait(current_thread, @ptrCast(ref), @intFromEnum(ResetEventFutex.waiting));
|
||
state = @atomicLoad(ResetEventFutex, ref, .acquire);
|
||
}
|
||
assert(state == .is_set);
|
||
}
|
||
|
||
/// Same as `wait` except uninterruptible.
|
||
pub fn waitUncancelable(ref: *ResetEventFutex) void {
|
||
if (builtin.single_threaded) switch (ref.*) {
|
||
.unset => unreachable, // Deadlock, no other threads to wake us up.
|
||
.waiting => unreachable, // Invalid state.
|
||
.is_set => return,
|
||
};
|
||
// Try to set the state from `unset` to `waiting` to indicate to the
|
||
// `set` thread that others are blocked on the ResetEventFutex. Avoid using
|
||
// any strict barriers until we know the ResetEventFutex is set.
|
||
var state = @atomicLoad(ResetEventFutex, ref, .acquire);
|
||
if (state == .is_set) {
|
||
@branchHint(.likely);
|
||
return;
|
||
}
|
||
if (state == .unset) {
|
||
state = @cmpxchgStrong(ResetEventFutex, ref, state, .waiting, .acquire, .acquire) orelse .waiting;
|
||
}
|
||
while (state == .waiting) {
|
||
futexWaitUncancelable(@ptrCast(ref), @intFromEnum(ResetEventFutex.waiting));
|
||
state = @atomicLoad(ResetEventFutex, ref, .acquire);
|
||
}
|
||
assert(state == .is_set);
|
||
}
|
||
|
||
/// Marks the logical boolean as `set` and unblocks any threads in `wait`
|
||
/// or `timedWait` to observe the new state.
|
||
///
|
||
/// The logical boolean stays `set` until `reset` is called, making future
|
||
/// `set` calls do nothing semantically.
|
||
///
|
||
/// The memory accesses before `set` can be said to happen before `isSet`
|
||
/// returns true or `wait`/`timedWait` return successfully.
|
||
pub fn set(ref: *ResetEventFutex) void {
|
||
if (builtin.single_threaded) {
|
||
ref.* = .is_set;
|
||
return;
|
||
}
|
||
if (@atomicRmw(ResetEventFutex, ref, .Xchg, .is_set, .release) == .waiting) {
|
||
futexWake(@ptrCast(ref), std.math.maxInt(u32));
|
||
}
|
||
}
|
||
|
||
/// Unmarks the ResetEventFutex as if `set` was never called.
|
||
///
|
||
/// Assumes no threads are blocked in `wait` or `timedWait`. Concurrent
|
||
/// calls to `set`, `isSet` and `reset` are allowed.
|
||
pub fn reset(ref: *ResetEventFutex) void {
|
||
if (builtin.single_threaded) {
|
||
ref.* = .unset;
|
||
return;
|
||
}
|
||
@atomicStore(ResetEventFutex, ref, .unset, .monotonic);
|
||
}
|
||
};
|
||
|
||
/// A `ResetEvent` implementation based on pthreads API.
|
||
const ResetEventPosix = struct {
|
||
cond: std.c.pthread_cond_t,
|
||
mutex: std.c.pthread_mutex_t,
|
||
state: ResetEventFutex,
|
||
|
||
pub const unset: ResetEventPosix = .{
|
||
.cond = std.c.PTHREAD_COND_INITIALIZER,
|
||
.mutex = std.c.PTHREAD_MUTEX_INITIALIZER,
|
||
.state = .unset,
|
||
};
|
||
|
||
pub fn isSet(rep: *const ResetEventPosix) bool {
|
||
if (builtin.single_threaded) return switch (rep.state) {
|
||
.unset => false,
|
||
.waiting => unreachable,
|
||
.is_set => true,
|
||
};
|
||
return @atomicLoad(ResetEventFutex, &rep.state, .acquire) == .is_set;
|
||
}
|
||
|
||
pub fn wait(rep: *ResetEventPosix, t: *Threaded) Io.Cancelable!void {
|
||
if (builtin.single_threaded) switch (rep.*) {
|
||
.unset => unreachable, // Deadlock, no other threads to wake us up.
|
||
.waiting => unreachable, // Invalid state.
|
||
.is_set => return,
|
||
};
|
||
const current_thread = Thread.getCurrent(t);
|
||
assert(std.c.pthread_mutex_lock(&rep.mutex) == .SUCCESS);
|
||
defer assert(std.c.pthread_mutex_unlock(&rep.mutex) == .SUCCESS);
|
||
sw: switch (rep.state) {
|
||
.unset => {
|
||
rep.state = .waiting;
|
||
continue :sw .waiting;
|
||
},
|
||
.waiting => {
|
||
try current_thread.beginSyscall();
|
||
assert(std.c.pthread_cond_wait(&rep.cond, &rep.mutex) == .SUCCESS);
|
||
current_thread.endSyscall();
|
||
continue :sw rep.state;
|
||
},
|
||
.is_set => return,
|
||
}
|
||
}
|
||
|
||
pub fn waitUncancelable(rep: *ResetEventPosix) void {
|
||
if (builtin.single_threaded) switch (rep.*) {
|
||
.unset => unreachable, // Deadlock, no other threads to wake us up.
|
||
.waiting => unreachable, // Invalid state.
|
||
.is_set => return,
|
||
};
|
||
assert(std.c.pthread_mutex_lock(&rep.mutex) == .SUCCESS);
|
||
defer assert(std.c.pthread_mutex_unlock(&rep.mutex) == .SUCCESS);
|
||
sw: switch (rep.state) {
|
||
.unset => {
|
||
rep.state = .waiting;
|
||
continue :sw .waiting;
|
||
},
|
||
.waiting => {
|
||
assert(std.c.pthread_cond_wait(&rep.cond, &rep.mutex) == .SUCCESS);
|
||
continue :sw rep.state;
|
||
},
|
||
.is_set => return,
|
||
}
|
||
}
|
||
|
||
pub fn set(rep: *ResetEventPosix) void {
|
||
if (builtin.single_threaded) {
|
||
rep.* = .is_set;
|
||
return;
|
||
}
|
||
if (@atomicRmw(ResetEventFutex, &rep.state, .Xchg, .is_set, .release) == .waiting) {
|
||
assert(std.c.pthread_cond_broadcast(&rep.cond) == .SUCCESS);
|
||
}
|
||
}
|
||
|
||
pub fn reset(rep: *ResetEventPosix) void {
|
||
if (builtin.single_threaded) {
|
||
rep.* = .unset;
|
||
return;
|
||
}
|
||
@atomicStore(ResetEventFutex, &rep.state, .unset, .monotonic);
|
||
}
|
||
};
|
||
|
||
fn closeSocketWindows(s: ws2_32.SOCKET) void {
|
||
const rc = ws2_32.closesocket(s);
|
||
if (is_debug) switch (rc) {
|
||
0 => {},
|
||
ws2_32.SOCKET_ERROR => switch (ws2_32.WSAGetLastError()) {
|
||
else => recoverableOsBugDetected(),
|
||
},
|
||
else => recoverableOsBugDetected(),
|
||
};
|
||
}
|
||
|
||
const Wsa = struct {
|
||
status: Status = .uninitialized,
|
||
mutex: Io.Mutex = .init,
|
||
init_error: ?Wsa.InitError = null,
|
||
|
||
const Status = enum { uninitialized, initialized, failure };
|
||
|
||
const InitError = error{
|
||
ProcessFdQuotaExceeded,
|
||
NetworkDown,
|
||
VersionUnsupported,
|
||
BlockingOperationInProgress,
|
||
} || Io.UnexpectedError;
|
||
};
|
||
|
||
fn initializeWsa(t: *Threaded) error{ NetworkDown, Canceled }!void {
|
||
const t_io = io(t);
|
||
const wsa = &t.wsa;
|
||
try wsa.mutex.lock(t_io);
|
||
defer wsa.mutex.unlock(t_io);
|
||
switch (wsa.status) {
|
||
.uninitialized => {
|
||
var wsa_data: ws2_32.WSADATA = undefined;
|
||
const minor_version = 2;
|
||
const major_version = 2;
|
||
switch (ws2_32.WSAStartup((@as(windows.WORD, minor_version) << 8) | major_version, &wsa_data)) {
|
||
0 => {
|
||
wsa.status = .initialized;
|
||
return;
|
||
},
|
||
else => |err_int| {
|
||
wsa.status = .failure;
|
||
wsa.init_error = switch (@as(ws2_32.WinsockError, @enumFromInt(@as(u16, @intCast(err_int))))) {
|
||
.SYSNOTREADY => error.NetworkDown,
|
||
.VERNOTSUPPORTED => error.VersionUnsupported,
|
||
.EINPROGRESS => error.BlockingOperationInProgress,
|
||
.EPROCLIM => error.ProcessFdQuotaExceeded,
|
||
else => |err| windows.unexpectedWSAError(err),
|
||
};
|
||
},
|
||
}
|
||
},
|
||
.initialized => return,
|
||
.failure => {},
|
||
}
|
||
return error.NetworkDown;
|
||
}
|
||
|
||
fn doNothingSignalHandler(_: posix.SIG) callconv(.c) void {}
|
||
|
||
test {
|
||
_ = @import("Threaded/test.zig");
|
||
}
|