diff --git a/build.zig.zon b/build.zig.zon index ce9dbe8e1..6c0b41049 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -5,8 +5,8 @@ .dependencies = .{ // Zig libs .libxev = .{ - .url = "https://github.com/mitchellh/libxev/archive/5ecbc871f3bfa80fb7bf0fa853866cb93b99bc18.tar.gz", - .hash = "1220416854e424601ecc9814afb461a5dc9cf95db5917d82f794594a58ffc723b82c", + .url = "https://github.com/kcbanner/libxev/archive/d71ddeb19c6697875c2e0e68a49d04c05bc26199.tar.gz", + .hash = "1220a26265c30f5677ce28daf04afbd7a6ef5a17f5d3b5506f767fdfee7dc4225d2b", }, .mach_glfw = .{ .url = "https://github.com/hexops/mach-glfw/archive/16dc95cc7f74ebbbdd848d9a2c3cc4afc5717708.tar.gz", diff --git a/src/App.zig b/src/App.zig index b9b9501eb..a09860a86 100644 --- a/src/App.zig +++ b/src/App.zig @@ -61,11 +61,8 @@ pub fn create( // Find our resources directory once for the app so every launch // hereafter can use this cached value. - var resources_buf: [std.fs.MAX_PATH_BYTES]u8 = undefined; - const resources_dir = if (try internal_os.resourcesDir(&resources_buf)) |dir| - try alloc.dupe(u8, dir) - else - null; + const resources_dir = try internal_os.resourcesDir(alloc); + errdefer if (resources_dir) |dir| alloc.free(dir); app.* = .{ .alloc = alloc, diff --git a/src/Command.zig b/src/Command.zig index f963a1857..de284040e 100644 --- a/src/Command.zig +++ b/src/Command.zig @@ -16,7 +16,6 @@ //! //! TODO: //! -//! * Windows //! * Mac //! const Command = @This(); @@ -24,6 +23,7 @@ const Command = @This(); const std = @import("std"); const builtin = @import("builtin"); const internal_os = @import("os/main.zig"); +const windows = @import("windows.zig"); const TempDir = internal_os.TempDir; const mem = std.mem; const os = std.os; @@ -64,15 +64,22 @@ stderr: ?File = null, /// exec process takes over, such as signal handlers, setsid, setuid, etc. pre_exec: ?*const PreExecFn = null, +/// If set, then the process will be created attached to this pseudo console. +/// `stdin`, `stdout`, and `stderr` will be ignored if set. +pseudo_console: if (builtin.os.tag == .windows) ?windows.exp.HPCON else void = + if (builtin.os.tag == .windows) null else {}, + /// User data that is sent to the callback. Set with setData and getData /// for a more user-friendly API. data: ?*anyopaque = null, /// Process ID is set after start is called. -pid: ?i32 = null, +pid: ?os.pid_t = null, /// The various methods a process may exit. -pub const Exit = union(enum) { +pub const Exit = if (builtin.os.tag == .windows) union(enum) { + Exited: u32, +} else union(enum) { /// Exited by normal exit call, value is exit status Exited: u8, @@ -109,45 +116,145 @@ pub fn start(self: *Command, alloc: Allocator) !void { defer arena_allocator.deinit(); const arena = arena_allocator.allocator(); - // Null-terminate all our arguments - const pathZ = try arena.dupeZ(u8, self.path); - const argsZ = try arena.allocSentinel(?[*:0]u8, self.args.len, null); - for (self.args, 0..) |arg, i| argsZ[i] = (try arena.dupeZ(u8, arg)).ptr; + if (builtin.os.tag == .windows) { + const application_w = try std.unicode.utf8ToUtf16LeWithNull(arena, self.path); + const cwd_w = if (self.cwd) |cwd| try std.unicode.utf8ToUtf16LeWithNull(arena, cwd) else null; + const command_line_w = if (self.args.len > 0) b: { + const command_line = try windowsCreateCommandLine(arena, self.args); + break :b try std.unicode.utf8ToUtf16LeWithNull(arena, command_line); + } else null; + const env_w = if (self.env) |env_map| try createWindowsEnvBlock(arena, env_map) else null; - // Determine our env vars - const envp = if (self.env) |env_map| - (try createNullDelimitedEnvMap(arena, env_map)).ptr - else if (builtin.link_libc) - std.c.environ - else - @compileError("missing env vars"); + const any_null_fd = self.stdin == null or self.stdout == null or self.stderr == null; + const null_fd = if (any_null_fd) try windows.OpenFile( + &[_]u16{ '\\', 'D', 'e', 'v', 'i', 'c', 'e', '\\', 'N', 'u', 'l', 'l' }, + .{ + .access_mask = windows.GENERIC_READ | windows.SYNCHRONIZE, + .share_access = windows.FILE_SHARE_READ, + .creation = windows.OPEN_EXISTING, + .io_mode = .blocking, + }, + ) else null; + defer if (null_fd) |fd| std.os.close(fd); - if (builtin.os.tag == .windows) - @panic("start not implemented on windows"); + // TODO: In the case of having FDs instead of pty, need to set up attributes such that the + // child process only inherits these handles, then set bInheritsHandles below. - // Fork - const pid = try std.os.fork(); - if (pid != 0) { - // Parent, return immediately. - self.pid = @intCast(pid); - return; + const attribute_list, const stdin, const stdout, const stderr = if (self.pseudo_console) |pseudo_console| b: { + var attribute_list_size: usize = undefined; + _ = windows.exp.kernel32.InitializeProcThreadAttributeList( + null, + 1, + 0, + &attribute_list_size, + ); + + const attribute_list_buf = try arena.alloc(u8, attribute_list_size); + if (windows.exp.kernel32.InitializeProcThreadAttributeList( + attribute_list_buf.ptr, + 1, + 0, + &attribute_list_size, + ) == 0) return windows.unexpectedError(windows.kernel32.GetLastError()); + + if (windows.exp.kernel32.UpdateProcThreadAttribute( + attribute_list_buf.ptr, + 0, + windows.exp.PROC_THREAD_ATTRIBUTE_PSEUDOCONSOLE, + pseudo_console, + @sizeOf(windows.exp.HPCON), + null, + null, + ) == 0) return windows.unexpectedError(windows.kernel32.GetLastError()); + + break :b .{ attribute_list_buf.ptr, null, null, null }; + } else b: { + const stdin = if (self.stdin) |f| f.handle else null_fd.?; + const stdout = if (self.stdout) |f| f.handle else null_fd.?; + const stderr = if (self.stderr) |f| f.handle else null_fd.?; + break :b .{ null, stdin, stdout, stderr }; + }; + + var startup_info_ex = windows.exp.STARTUPINFOEX{ + .StartupInfo = .{ + .cb = if (attribute_list != null) @sizeOf(windows.exp.STARTUPINFOEX) else @sizeOf(windows.STARTUPINFOW), + .hStdError = stderr, + .hStdOutput = stdout, + .hStdInput = stdin, + .dwFlags = windows.STARTF_USESTDHANDLES, + .lpReserved = null, + .lpDesktop = null, + .lpTitle = null, + .dwX = 0, + .dwY = 0, + .dwXSize = 0, + .dwYSize = 0, + .dwXCountChars = 0, + .dwYCountChars = 0, + .dwFillAttribute = 0, + .wShowWindow = 0, + .cbReserved2 = 0, + .lpReserved2 = null, + }, + .lpAttributeList = attribute_list, + }; + + var flags: windows.DWORD = windows.exp.CREATE_UNICODE_ENVIRONMENT; + if (attribute_list != null) flags |= windows.exp.EXTENDED_STARTUPINFO_PRESENT; + + var process_information: windows.PROCESS_INFORMATION = undefined; + if (windows.exp.kernel32.CreateProcessW( + application_w.ptr, + if (command_line_w) |w| w.ptr else null, + null, + null, + windows.TRUE, + flags, + if (env_w) |w| w.ptr else null, + if (cwd_w) |w| w.ptr else null, + @ptrCast(&startup_info_ex.StartupInfo), + &process_information, + ) == 0) return windows.unexpectedError(windows.kernel32.GetLastError()); + + self.pid = process_information.hProcess; + } else { + // Null-terminate all our arguments + const pathZ = try arena.dupeZ(u8, self.path); + const argsZ = try arena.allocSentinel(?[*:0]u8, self.args.len, null); + for (self.args, 0..) |arg, i| argsZ[i] = (try arena.dupeZ(u8, arg)).ptr; + + // Determine our env vars + const envp = if (self.env) |env_map| + (try createNullDelimitedEnvMap(arena, env_map)).ptr + else if (builtin.link_libc) + std.c.environ + else + @compileError("missing env vars"); + + // Fork + const pid = try std.os.fork(); + if (pid != 0) { + // Parent, return immediately. + self.pid = @intCast(pid); + return; + } + + // We are the child. + + // Setup our file descriptors for std streams. + if (self.stdin) |f| try setupFd(f.handle, os.STDIN_FILENO); + if (self.stdout) |f| try setupFd(f.handle, os.STDOUT_FILENO); + if (self.stderr) |f| try setupFd(f.handle, os.STDERR_FILENO); + + // Setup our working directory + if (self.cwd) |cwd| try os.chdir(cwd); + + // If the user requested a pre exec callback, call it now. + if (self.pre_exec) |f| f(self); + + // Finally, replace our process. + _ = std.os.execveZ(pathZ, argsZ, envp) catch null; } - - // We are the child. - - // Setup our file descriptors for std streams. - if (self.stdin) |f| try setupFd(f.handle, os.STDIN_FILENO); - if (self.stdout) |f| try setupFd(f.handle, os.STDOUT_FILENO); - if (self.stderr) |f| try setupFd(f.handle, os.STDERR_FILENO); - - // Setup our working directory - if (self.cwd) |cwd| try os.chdir(cwd); - - // If the user requested a pre exec callback, call it now. - if (self.pre_exec) |f| f(self); - - // Finally, replace our process. - _ = std.os.execveZ(pathZ, argsZ, envp) catch null; } fn setupFd(src: File.Handle, target: i32) !void { @@ -190,8 +297,18 @@ fn setupFd(src: File.Handle, target: i32) !void { /// Wait for the command to exit and return information about how it exited. pub fn wait(self: Command, block: bool) !Exit { - if (builtin.os.tag == .windows) - @panic("wait not implemented on windows"); + if (builtin.os.tag == .windows) { + + // Block until the process exits. This returns immediately if the process already exited. + const result = windows.kernel32.WaitForSingleObject(self.pid.?, windows.INFINITE); + if (result == windows.WAIT_FAILED) return windows.unexpectedError(windows.kernel32.GetLastError()); + + var exit_code: windows.DWORD = undefined; + var has_code = windows.kernel32.GetExitCodeProcess(self.pid.?, &exit_code) != 0; + if (!has_code) return windows.unexpectedError(windows.kernel32.GetLastError()); + + return .{ .Exited = exit_code }; + } const res = if (block) std.os.waitpid(self.pid.?, 0) else res: { // We specify NOHANG because its not our fault if the process we launch @@ -325,6 +442,79 @@ fn createNullDelimitedEnvMap(arena: mem.Allocator, env_map: *const EnvMap) ![:nu return envp_buf; } +// Copied from Zig. This is a publicly exported function but there is no +// way to get it from the std package. +pub fn createWindowsEnvBlock(allocator: mem.Allocator, env_map: *const EnvMap) ![]u16 { + // count bytes needed + const max_chars_needed = x: { + var max_chars_needed: usize = 4; // 4 for the final 4 null bytes + var it = env_map.iterator(); + while (it.next()) |pair| { + // +1 for '=' + // +1 for null byte + max_chars_needed += pair.key_ptr.len + pair.value_ptr.len + 2; + } + break :x max_chars_needed; + }; + const result = try allocator.alloc(u16, max_chars_needed); + errdefer allocator.free(result); + + var it = env_map.iterator(); + var i: usize = 0; + while (it.next()) |pair| { + i += try std.unicode.utf8ToUtf16Le(result[i..], pair.key_ptr.*); + result[i] = '='; + i += 1; + i += try std.unicode.utf8ToUtf16Le(result[i..], pair.value_ptr.*); + result[i] = 0; + i += 1; + } + result[i] = 0; + i += 1; + result[i] = 0; + i += 1; + result[i] = 0; + i += 1; + result[i] = 0; + i += 1; + return try allocator.realloc(result, i); +} + +/// Copied from Zig. This function could be made public in child_process.zig instead. +fn windowsCreateCommandLine(allocator: mem.Allocator, argv: []const []const u8) ![:0]u8 { + var buf = std.ArrayList(u8).init(allocator); + defer buf.deinit(); + + for (argv, 0..) |arg, arg_i| { + if (arg_i != 0) try buf.append(' '); + if (mem.indexOfAny(u8, arg, " \t\n\"") == null) { + try buf.appendSlice(arg); + continue; + } + try buf.append('"'); + var backslash_count: usize = 0; + for (arg) |byte| { + switch (byte) { + '\\' => backslash_count += 1, + '"' => { + try buf.appendNTimes('\\', backslash_count * 2 + 1); + try buf.append('"'); + backslash_count = 0; + }, + else => { + try buf.appendNTimes('\\', backslash_count); + try buf.append(byte); + backslash_count = 0; + }, + } + } + try buf.appendNTimes('\\', backslash_count * 2); + try buf.append('"'); + } + + return buf.toOwnedSliceSentinel(0); +} + test "createNullDelimitedEnvMap" { const allocator = testing.allocator; var envmap = EnvMap.init(allocator); @@ -378,14 +568,26 @@ test "Command: pre exec" { try testing.expect(exit.Exited == 42); } +fn createTestStdout(dir: std.fs.Dir) !File { + const file = try dir.createFile("stdout.txt", .{ .read = true }); + if (builtin.os.tag == .windows) { + try windows.SetHandleInformation(file.handle, windows.HANDLE_FLAG_INHERIT, windows.HANDLE_FLAG_INHERIT); + } + + return file; +} + test "Command: redirect stdout to file" { - if (builtin.os.tag == .windows) return error.SkipZigTest; var td = try TempDir.init(); defer td.deinit(); - var stdout = try td.dir.createFile("stdout.txt", .{ .read = true }); + var stdout = try createTestStdout(td.dir); defer stdout.close(); - var cmd: Command = .{ + var cmd: Command = if (builtin.os.tag == .windows) .{ + .path = "C:\\Windows\\System32\\whoami.exe", + .args = &.{"C:\\Windows\\System32\\whoami.exe"}, + .stdout = stdout, + } else .{ .path = "/usr/bin/env", .args = &.{ "/usr/bin/env", "-v" }, .stdout = stdout, @@ -395,7 +597,7 @@ test "Command: redirect stdout to file" { try testing.expect(cmd.pid != null); const exit = try cmd.wait(true); try testing.expect(exit == .Exited); - try testing.expect(exit.Exited == 0); + try testing.expectEqual(@as(u32, 0), @as(u32, exit.Exited)); // Read our stdout try stdout.seekTo(0); @@ -405,17 +607,21 @@ test "Command: redirect stdout to file" { } test "Command: custom env vars" { - if (builtin.os.tag == .windows) return error.SkipZigTest; var td = try TempDir.init(); defer td.deinit(); - var stdout = try td.dir.createFile("stdout.txt", .{ .read = true }); + var stdout = try createTestStdout(td.dir); defer stdout.close(); var env = EnvMap.init(testing.allocator); defer env.deinit(); try env.put("VALUE", "hello"); - var cmd: Command = .{ + var cmd: Command = if (builtin.os.tag == .windows) .{ + .path = "C:\\Windows\\System32\\cmd.exe", + .args = &.{ "C:\\Windows\\System32\\cmd.exe", "/C", "echo %VALUE%" }, + .stdout = stdout, + .env = &env, + } else .{ .path = "/usr/bin/env", .args = &.{ "/usr/bin/env", "sh", "-c", "echo $VALUE" }, .stdout = stdout, @@ -432,17 +638,26 @@ test "Command: custom env vars" { try stdout.seekTo(0); const contents = try stdout.readToEndAlloc(testing.allocator, 4096); defer testing.allocator.free(contents); - try testing.expectEqualStrings("hello\n", contents); + + if (builtin.os.tag == .windows) { + try testing.expectEqualStrings("hello\r\n", contents); + } else { + try testing.expectEqualStrings("hello\n", contents); + } } test "Command: custom working directory" { - if (builtin.os.tag == .windows) return error.SkipZigTest; var td = try TempDir.init(); defer td.deinit(); - var stdout = try td.dir.createFile("stdout.txt", .{ .read = true }); + var stdout = try createTestStdout(td.dir); defer stdout.close(); - var cmd: Command = .{ + var cmd: Command = if (builtin.os.tag == .windows) .{ + .path = "C:\\Windows\\System32\\cmd.exe", + .args = &.{ "C:\\Windows\\System32\\cmd.exe", "/C", "cd" }, + .stdout = stdout, + .cwd = "C:\\Windows\\System32", + } else .{ .path = "/usr/bin/env", .args = &.{ "/usr/bin/env", "sh", "-c", "pwd" }, .stdout = stdout, @@ -459,5 +674,10 @@ test "Command: custom working directory" { try stdout.seekTo(0); const contents = try stdout.readToEndAlloc(testing.allocator, 4096); defer testing.allocator.free(contents); - try testing.expectEqualStrings("/usr/bin\n", contents); + + if (builtin.os.tag == .windows) { + try testing.expectEqualStrings("C:\\Windows\\System32\r\n", contents); + } else { + try testing.expectEqualStrings("/usr/bin\n", contents); + } } diff --git a/src/Pty.zig b/src/Pty.zig index c1e8efd18..90108a4a7 100644 --- a/src/Pty.zig +++ b/src/Pty.zig @@ -1,11 +1,6 @@ -//! Linux PTY creation and management. This is just a thin layer on top -//! of Linux syscalls. The caller is responsible for detail-oriented handling -//! of the returned file handles. -const Pty = @This(); - const std = @import("std"); const builtin = @import("builtin"); -const testing = std.testing; +const windows = @import("windows.zig"); const fd_t = std.os.fd_t; const c = switch (builtin.os.tag) { @@ -13,7 +8,6 @@ const c = switch (builtin.os.tag) { @cInclude("sys/ioctl.h"); // ioctl and constants @cInclude("util.h"); // openpty() }), - .windows => {}, else => @cImport({ @cInclude("sys/ioctl.h"); // ioctl and constants @cInclude("pty.h"); @@ -22,16 +16,10 @@ const c = switch (builtin.os.tag) { const log = std.log.scoped(.pty); -// https://github.com/ziglang/zig/issues/13277 -// Once above is fixed, use `c.TIOCSCTTY` -const TIOCSCTTY = if (builtin.os.tag == .macos) 536900705 else c.TIOCSCTTY; -const TIOCSWINSZ = if (builtin.os.tag == .macos) 2148037735 else c.TIOCSWINSZ; -const TIOCGWINSZ = if (builtin.os.tag == .macos) 1074295912 else c.TIOCGWINSZ; - /// Redeclare this winsize struct so we can just use a Zig struct. This /// layout should be correct on all tested platforms. The defaults on this /// are some reasonable screen size but you should probably not use them. -const winsize = extern struct { +pub const winsize = extern struct { ws_row: u16 = 100, ws_col: u16 = 80, ws_xpixel: u16 = 800, @@ -40,95 +28,233 @@ const winsize = extern struct { pub extern "c" fn setsid() std.c.pid_t; -/// The file descriptors for the master and slave side of the pty. -master: fd_t, -slave: fd_t, +pub const Pty = if (builtin.os.tag == .windows) + WindowsPty +else + PosixPty; -/// Open a new PTY with the given initial size. -pub fn open(size: winsize) !Pty { - if (builtin.os.tag == .windows) return error.NotImplementedOnWindows; +/// Linux PTY creation and management. This is just a thin layer on top +/// of Linux syscalls. The caller is responsible for detail-oriented handling +/// of the returned file handles. +pub const PosixPty = struct { - // Need to copy so that it becomes non-const. - var sizeCopy = size; + // https://github.com/ziglang/zig/issues/13277 + // Once above is fixed, use `c.TIOCSCTTY` + const TIOCSCTTY = if (builtin.os.tag == .macos) 536900705 else c.TIOCSCTTY; + const TIOCSWINSZ = if (builtin.os.tag == .macos) 2148037735 else c.TIOCSWINSZ; + const TIOCGWINSZ = if (builtin.os.tag == .macos) 1074295912 else c.TIOCGWINSZ; - var master_fd: fd_t = undefined; - var slave_fd: fd_t = undefined; - if (c.openpty( - &master_fd, - &slave_fd, - null, - null, - @ptrCast(&sizeCopy), - ) < 0) - return error.OpenptyFailed; - errdefer { - _ = std.os.system.close(master_fd); - _ = std.os.system.close(slave_fd); + /// The file descriptors for the master and slave side of the pty. + master: fd_t, + slave: fd_t, + + /// Open a new PTY with the given initial size. + pub fn open(size: winsize) !Pty { + // Need to copy so that it becomes non-const. + var sizeCopy = size; + + var master_fd: fd_t = undefined; + var slave_fd: fd_t = undefined; + if (c.openpty( + &master_fd, + &slave_fd, + null, + null, + @ptrCast(&sizeCopy), + ) < 0) + return error.OpenptyFailed; + errdefer { + _ = std.os.system.close(master_fd); + _ = std.os.system.close(slave_fd); + } + + // Enable UTF-8 mode. I think this is on by default on Linux but it + // is NOT on by default on macOS so we ensure that it is always set. + var attrs: c.termios = undefined; + if (c.tcgetattr(master_fd, &attrs) != 0) + return error.OpenptyFailed; + attrs.c_iflag |= c.IUTF8; + if (c.tcsetattr(master_fd, c.TCSANOW, &attrs) != 0) + return error.OpenptyFailed; + + return Pty{ + .master = master_fd, + .slave = slave_fd, + }; } - // Enable UTF-8 mode. I think this is on by default on Linux but it - // is NOT on by default on macOS so we ensure that it is always set. - var attrs: c.termios = undefined; - if (c.tcgetattr(master_fd, &attrs) != 0) - return error.OpenptyFailed; - attrs.c_iflag |= c.IUTF8; - if (c.tcsetattr(master_fd, c.TCSANOW, &attrs) != 0) - return error.OpenptyFailed; - - return Pty{ - .master = master_fd, - .slave = slave_fd, - }; -} - -pub fn deinit(self: *Pty) void { - _ = std.os.system.close(self.master); - _ = std.os.system.close(self.slave); - self.* = undefined; -} - -/// Return the size of the pty. -pub fn getSize(self: Pty) !winsize { - if (builtin.os.tag == .windows) return error.NotImplementedOnWindows; - - var ws: winsize = undefined; - if (c.ioctl(self.master, TIOCGWINSZ, @intFromPtr(&ws)) < 0) - return error.IoctlFailed; - - return ws; -} - -/// Set the size of the pty. -pub fn setSize(self: Pty, size: winsize) !void { - if (builtin.os.tag == .windows) return error.NotImplementedOnWindows; - if (c.ioctl(self.master, TIOCSWINSZ, @intFromPtr(&size)) < 0) - return error.IoctlFailed; -} - -/// This should be called prior to exec in the forked child process -/// in order to setup the tty properly. -pub fn childPreExec(self: Pty) !void { - // Create a new process group - if (setsid() < 0) return error.ProcessGroupFailed; - - // Set controlling terminal - switch (std.os.system.getErrno(c.ioctl(self.slave, TIOCSCTTY, @as(c_ulong, 0)))) { - .SUCCESS => {}, - else => |err| { - log.err("error setting controlling terminal errno={}", .{err}); - return error.SetControllingTerminalFailed; - }, + pub fn deinit(self: *Pty) void { + _ = std.os.system.close(self.master); + _ = std.os.system.close(self.slave); + self.* = undefined; } - // Can close master/slave pair now - std.os.close(self.slave); - std.os.close(self.master); + /// Return the size of the pty. + pub fn getSize(self: Pty) !winsize { + var ws: winsize = undefined; + if (c.ioctl(self.master, TIOCGWINSZ, @intFromPtr(&ws)) < 0) + return error.IoctlFailed; - // TODO: reset signals -} + return ws; + } + /// Set the size of the pty. + pub fn setSize(self: *Pty, size: winsize) !void { + if (c.ioctl(self.master, TIOCSWINSZ, @intFromPtr(&size)) < 0) + return error.IoctlFailed; + } + + /// This should be called prior to exec in the forked child process + /// in order to setup the tty properly. + pub fn childPreExec(self: Pty) !void { + // Create a new process group + if (setsid() < 0) return error.ProcessGroupFailed; + + // Set controlling terminal + switch (std.os.system.getErrno(c.ioctl(self.slave, TIOCSCTTY, @as(c_ulong, 0)))) { + .SUCCESS => {}, + else => |err| { + log.err("error setting controlling terminal errno={}", .{err}); + return error.SetControllingTerminalFailed; + }, + } + + // Can close master/slave pair now + std.os.close(self.slave); + std.os.close(self.master); + + // TODO: reset signals + } +}; + +/// Windows PTY creation and management. +pub const WindowsPty = struct { + // Process-wide counter for pipe names + var pipe_name_counter = std.atomic.Atomic(u32).init(1); + + out_pipe: windows.HANDLE, + in_pipe: windows.HANDLE, + out_pipe_pty: windows.HANDLE, + in_pipe_pty: windows.HANDLE, + pseudo_console: windows.exp.HPCON, + size: winsize, + + /// Open a new PTY with the given initial size. + pub fn open(size: winsize) !Pty { + var pty: Pty = undefined; + + var pipe_path_buf: [128]u8 = undefined; + var pipe_path_buf_w: [128]u16 = undefined; + const pipe_path = std.fmt.bufPrintZ( + &pipe_path_buf, + "\\\\.\\pipe\\LOCAL\\ghostty-pty-{d}-{d}", + .{ windows.kernel32.GetCurrentProcessId(), pipe_name_counter.fetchAdd(1, .Monotonic) }, + ) catch unreachable; + + const pipe_path_w_len = std.unicode.utf8ToUtf16Le(&pipe_path_buf_w, pipe_path) catch unreachable; + pipe_path_buf_w[pipe_path_w_len] = 0; + const pipe_path_w = pipe_path_buf_w[0..pipe_path_w_len :0]; + + const security_attributes = windows.SECURITY_ATTRIBUTES{ + .nLength = @sizeOf(windows.SECURITY_ATTRIBUTES), + .bInheritHandle = windows.FALSE, + .lpSecurityDescriptor = null, + }; + + pty.in_pipe = windows.kernel32.CreateNamedPipeW( + pipe_path_w.ptr, + windows.PIPE_ACCESS_OUTBOUND | windows.exp.FILE_FLAG_FIRST_PIPE_INSTANCE | windows.FILE_FLAG_OVERLAPPED, + windows.PIPE_TYPE_BYTE, + 1, + 4096, + 4096, + 0, + &security_attributes, + ); + if (pty.in_pipe == windows.INVALID_HANDLE_VALUE) return windows.unexpectedError(windows.kernel32.GetLastError()); + errdefer _ = windows.kernel32.CloseHandle(pty.in_pipe); + + var security_attributes_read = security_attributes; + pty.in_pipe_pty = windows.kernel32.CreateFileW( + pipe_path_w.ptr, + windows.GENERIC_READ, + 0, + &security_attributes_read, + windows.OPEN_EXISTING, + windows.FILE_ATTRIBUTE_NORMAL, + null, + ); + if (pty.in_pipe_pty == windows.INVALID_HANDLE_VALUE) return windows.unexpectedError(windows.kernel32.GetLastError()); + errdefer _ = windows.kernel32.CloseHandle(pty.in_pipe_pty); + + // The in_pipe needs to be created as a named pipe, since anonymous pipes created with CreatePipe do not + // support overlapped operations, and the IOCP backend of libxev only uses overlapped operations on files. + // + // It would be ideal to use CreatePipe here, so that our pipe isn't visible to any other processes. + + // if (windows.exp.kernel32.CreatePipe(&pty.in_pipe_pty, &pty.in_pipe, null, 0) == 0) { + // return windows.unexpectedError(windows.kernel32.GetLastError()); + // } + // errdefer { + // _ = windows.kernel32.CloseHandle(pty.in_pipe_pty); + // _ = windows.kernel32.CloseHandle(pty.in_pipe); + // } + + if (windows.exp.kernel32.CreatePipe(&pty.out_pipe, &pty.out_pipe_pty, null, 0) == 0) { + return windows.unexpectedError(windows.kernel32.GetLastError()); + } + errdefer { + _ = windows.kernel32.CloseHandle(pty.out_pipe); + _ = windows.kernel32.CloseHandle(pty.out_pipe_pty); + } + + try windows.SetHandleInformation(pty.in_pipe, windows.HANDLE_FLAG_INHERIT, 0); + try windows.SetHandleInformation(pty.in_pipe_pty, windows.HANDLE_FLAG_INHERIT, 0); + try windows.SetHandleInformation(pty.out_pipe, windows.HANDLE_FLAG_INHERIT, 0); + try windows.SetHandleInformation(pty.out_pipe_pty, windows.HANDLE_FLAG_INHERIT, 0); + + const result = windows.exp.kernel32.CreatePseudoConsole( + .{ .X = @intCast(size.ws_col), .Y = @intCast(size.ws_row) }, + pty.in_pipe_pty, + pty.out_pipe_pty, + 0, + &pty.pseudo_console, + ); + + if (result != windows.S_OK) return error.Unexpected; + + pty.size = size; + return pty; + } + + pub fn deinit(self: *Pty) void { + _ = windows.kernel32.CloseHandle(self.in_pipe_pty); + _ = windows.kernel32.CloseHandle(self.in_pipe); + _ = windows.kernel32.CloseHandle(self.out_pipe_pty); + _ = windows.kernel32.CloseHandle(self.out_pipe); + _ = windows.exp.kernel32.ClosePseudoConsole(self.pseudo_console); + self.* = undefined; + } + + /// Return the size of the pty. + pub fn getSize(self: Pty) !winsize { + return self.size; + } + + /// Set the size of the pty. + pub fn setSize(self: *Pty, size: winsize) !void { + const result = windows.exp.kernel32.ResizePseudoConsole( + self.pseudo_console, + .{ .X = @intCast(size.ws_col), .Y = @intCast(size.ws_row) }, + ); + + if (result != windows.S_OK) return error.ResizeFailed; + self.size = size; + } +}; + +const testing = std.testing; test { - if (builtin.os.tag == .windows) return error.SkipZigTest; var ws: winsize = .{ .ws_row = 50, .ws_col = 80, @@ -136,7 +262,7 @@ test { .ws_ypixel = 1, }; - var pty = try open(ws); + var pty = try Pty.open(ws); defer pty.deinit(); // Initialize size should match what we gave it diff --git a/src/Surface.zig b/src/Surface.zig index 7032ec8ae..e00e5a8bb 100644 --- a/src/Surface.zig +++ b/src/Surface.zig @@ -24,7 +24,7 @@ const renderer = @import("renderer.zig"); const termio = @import("termio.zig"); const objc = @import("objc"); const imgui = @import("imgui"); -const Pty = @import("Pty.zig"); +const Pty = @import("Pty.zig").Pty; const font = @import("font/main.zig"); const Command = @import("Command.zig"); const trace = @import("tracy").trace; @@ -941,7 +941,7 @@ pub fn setFontSize(self: *Surface, size: font.face.DesiredSize) void { /// This queues a render operation with the renderer thread. The render /// isn't guaranteed to happen immediately but it will happen as soon as /// practical. -fn queueRender(self: *const Surface) !void { +fn queueRender(self: *Surface) !void { try self.renderer_thread.wakeup.notify(); } diff --git a/src/config/Config.zig b/src/config/Config.zig index a091191d4..7f7222aa5 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -1134,24 +1134,38 @@ pub fn finalize(self: *Config) !void { } } - // We need the passwd entry for the remainder - const pw = try internal_os.passwd.get(alloc); - if (self.command == null) { - if (pw.shell) |sh| { - log.info("default shell src=passwd value={s}", .{sh}); - self.command = sh; + if (builtin.target.os.tag == .windows) { + if (self.command == null) { + log.warn("no default shell found, will default to using cmd", .{}); + self.command = "cmd.exe"; } - } - if (wd_home) { - if (pw.home) |home| { - log.info("default working directory src=passwd value={s}", .{home}); - self.@"working-directory" = home; + if (wd_home) { + var buf: [std.fs.MAX_PATH_BYTES]u8 = undefined; + if (try internal_os.home(&buf)) |home| { + self.@"working-directory" = try alloc.dupe(u8, home); + } + } + } else { + // We need the passwd entry for the remainder + const pw = try internal_os.passwd.get(alloc); + if (self.command == null) { + if (pw.shell) |sh| { + log.info("default shell src=passwd value={s}", .{sh}); + self.command = sh; + } } - } - if (self.command == null) { - log.warn("no default shell found, will default to using sh", .{}); + if (wd_home) { + if (pw.home) |home| { + log.info("default working directory src=passwd value={s}", .{home}); + self.@"working-directory" = home; + } + } + + if (self.command == null) { + log.warn("no default shell found, will default to using sh", .{}); + } } } } diff --git a/src/main.zig b/src/main.zig index af669de92..e7dd6c633 100644 --- a/src/main.zig +++ b/src/main.zig @@ -243,7 +243,8 @@ pub const GlobalState = struct { // maybe once for logging) so for now this is an easy way to do // this. Env vars are useful for logging too because they are // easy to set. - if (std.os.getenv("GHOSTTY_LOG")) |v| { + if ((try internal_os.getEnvVarOwned(self.alloc, "GHOSTTY_LOG"))) |v| { + defer self.alloc.free(v); if (v.len > 0) { self.logging = .{ .stderr = {} }; } @@ -265,7 +266,7 @@ pub const GlobalState = struct { // We need to make sure the process locale is set properly. Locale // affects a lot of behaviors in a shell. - internal_os.ensureLocale(); + try internal_os.ensureLocale(self.alloc); } /// Cleans up the global state. This doesn't _need_ to be called but diff --git a/src/os/env.zig b/src/os/env.zig index d6c2970c5..a3c34bd79 100644 --- a/src/os/env.zig +++ b/src/os/env.zig @@ -46,3 +46,36 @@ test "appendEnv existing" { try testing.expectEqualStrings(result, "a:b:foo"); } } + +extern "c" fn setenv(name: ?[*]const u8, value: ?[*]const u8, overwrite: c_int) c_int; +extern "c" fn unsetenv(name: ?[*]const u8) c_int; +extern "c" fn _putenv_s(varname: ?[*]const u8, value_string: ?[*]const u8) c_int; + +pub fn setEnv(key: [:0]const u8, value: [:0]const u8) c_int { + if (builtin.os.tag == .windows) { + return _putenv_s(key.ptr, value.ptr); + } else { + return setenv(key.ptr, value.ptr, 1); + } +} + +pub fn unsetEnv(key: [:0]const u8) c_int { + if (builtin.os.tag == .windows) { + return _putenv_s(key.ptr, ""); + } else { + return unsetenv(key.ptr); + } +} + +/// Returns the value of an environment variable, or null if not found. +/// The returned value is always allocated so it must be freed. +pub fn getEnvVarOwned(alloc: std.mem.Allocator, key: []const u8) !?[]u8 { + if (std.process.getEnvVarOwned(alloc, key)) |v| { + return v; + } else |err| switch (err) { + error.EnvironmentVariableNotFound => {}, + else => return err, + } + + return null; +} diff --git a/src/os/locale.zig b/src/os/locale.zig index a55f91622..7b11bf239 100644 --- a/src/os/locale.zig +++ b/src/os/locale.zig @@ -2,16 +2,18 @@ const std = @import("std"); const builtin = @import("builtin"); const assert = std.debug.assert; const objc = @import("objc"); +const internal_os = @import("main.zig"); const log = std.log.scoped(.os); /// Ensure that the locale is set. -pub fn ensureLocale() void { +pub fn ensureLocale(alloc: std.mem.Allocator) !void { assert(builtin.link_libc); // Get our LANG env var. We use this many times but we also need // the original value later. - const lang = std.os.getenv("LANG") orelse ""; + const lang = try internal_os.getEnvVarOwned(alloc, "LANG"); + defer if (lang) |v| alloc.free(v); // On macOS, pre-populate the LANG env var with system preferences. // When launching the .app, LANG is not set so we must query it from the @@ -33,12 +35,13 @@ pub fn ensureLocale() void { // setlocale failed. This is probably because the LANG env var is // invalid. Try to set it without the LANG var set to use the system // default. - if (std.os.getenv("LANG")) |old_lang| { + if ((try internal_os.getEnvVarOwned(alloc, "LANG"))) |old_lang| { + defer alloc.free(old_lang); if (old_lang.len > 0) { // We don't need to do both of these things but we do them // both to be sure that lang is either empty or unset completely. - _ = setenv("LANG", "", 1); - _ = unsetenv("LANG"); + _ = internal_os.setEnv("LANG", ""); + _ = internal_os.unsetEnv("LANG"); if (setlocale(LC_ALL, "")) |v| { log.info("setlocale after unset lang result={s}", .{v}); @@ -54,7 +57,7 @@ pub fn ensureLocale() void { // Failure again... fallback to en_US.UTF-8 log.warn("setlocale failed with LANG and system default. Falling back to en_US.UTF-8", .{}); if (setlocale(LC_ALL, "en_US.UTF-8")) |v| { - _ = setenv("LANG", "en_US.UTF-8", 1); + _ = internal_os.setEnv("LANG", "en_US.UTF-8"); log.info("setlocale default result={s}", .{v}); return; } else log.err("setlocale failed even with the fallback, uncertain results", .{}); @@ -95,7 +98,7 @@ fn setLangFromCocoa() void { log.info("detected system locale={s}", .{env_value}); // Set it onto our environment - if (setenv("LANG", env_value.ptr, 1) < 0) { + if (internal_os.setEnv("LANG", env_value.ptr) < 0) { log.err("error setting locale env var", .{}); return; } @@ -104,8 +107,6 @@ fn setLangFromCocoa() void { const LC_ALL: c_int = 6; // from locale.h const LC_ALL_MASK: c_int = 0x7fffffff; // from locale.h const locale_t = ?*anyopaque; -extern "c" fn setenv(name: ?[*]const u8, value: ?[*]const u8, overwrite: c_int) c_int; -extern "c" fn unsetenv(name: ?[*]const u8) c_int; extern "c" fn setlocale(category: c_int, locale: ?[*]const u8) ?[*:0]u8; extern "c" fn newlocale(category: c_int, locale: ?[*]const u8, base: locale_t) locale_t; extern "c" fn freelocale(v: locale_t) void; diff --git a/src/os/passwd.zig b/src/os/passwd.zig index b3e3dcc12..eac005e4b 100644 --- a/src/os/passwd.zig +++ b/src/os/passwd.zig @@ -30,7 +30,7 @@ pub const Entry = struct { /// Get the passwd entry for the currently executing user. pub fn get(alloc: Allocator) !Entry { - if (builtin.os.tag == .windows) @panic("todo: windows"); + if (builtin.os.tag == .windows) @compileError("passwd is not available on windows"); var buf: [1024]u8 = undefined; var pw: c.struct_passwd = undefined; @@ -63,7 +63,7 @@ pub fn get(alloc: Allocator) !Entry { // Note: we wrap our getent call in a /bin/sh login shell because // some operating systems (NixOS tested) don't set the PATH for various // utilities properly until we get a login shell. - const Pty = @import("../Pty.zig"); + const Pty = @import("../Pty.zig").Pty; var pty = try Pty.open(.{}); defer pty.deinit(); var cmd: internal_os.FlatpakHostCommand = .{ diff --git a/src/os/resourcesdir.zig b/src/os/resourcesdir.zig index 1496f53f7..940321469 100644 --- a/src/os/resourcesdir.zig +++ b/src/os/resourcesdir.zig @@ -4,20 +4,17 @@ const Allocator = std.mem.Allocator; /// Gets the directory to the bundled resources directory, if it /// exists (not all platforms or packages have it). The output is -/// written to the given buffer if we need to allocate. Note that -/// the output is not ALWAYS written to the buffer and may refer to -/// static memory. +/// owned by the caller. /// /// This is highly Ghostty-specific and can likely be generalized at /// some point but we can cross that bridge if we ever need to. -/// -/// This returns error.OutOfMemory is buffer is not big enough. -pub fn resourcesDir(buf: []u8) !?[]const u8 { +pub fn resourcesDir(alloc: std.mem.Allocator) !?[]const u8 { // If we have an environment variable set, we always use that. - if (std.os.getenv("GHOSTTY_RESOURCES_DIR")) |dir| { - if (dir.len > 0) { - return dir; - } + if (std.process.getEnvVarOwned(alloc, "GHOSTTY_RESOURCES_DIR")) |dir| { + if (dir.len > 0) return dir; + } else |err| switch (err) { + error.EnvironmentVariableNotFound => {}, + else => return err, } // This is the sentinel value we look for in the path to know @@ -30,21 +27,22 @@ pub fn resourcesDir(buf: []u8) !?[]const u8 { // We have an exe path! Climb the tree looking for the terminfo // bundle as we expect it. + var dir_buf: [std.fs.MAX_PATH_BYTES]u8 = undefined; while (std.fs.path.dirname(exe)) |dir| { exe = dir; // On MacOS, we look for the app bundle path. if (comptime builtin.target.isDarwin()) { - if (try maybeDir(buf, dir, "Contents/Resources", sentinel)) |v| { - return v; + if (try maybeDir(&dir_buf, dir, "Contents/Resources", sentinel)) |v| { + return try alloc.dupe(u8, v); } } // On all platforms, we look for a /usr/share style path. This // is valid even on Mac since there is nothing that requires // Ghostty to be in an app bundle. - if (try maybeDir(buf, dir, "share", sentinel)) |v| { - return v; + if (try maybeDir(&dir_buf, dir, "share", sentinel)) |v| { + return try alloc.dupe(u8, v); } } diff --git a/src/termio/Exec.zig b/src/termio/Exec.zig index 55b0e0dff..1e4e2ed63 100644 --- a/src/termio/Exec.zig +++ b/src/termio/Exec.zig @@ -10,7 +10,7 @@ const ArenaAllocator = std.heap.ArenaAllocator; const EnvMap = std.process.EnvMap; const termio = @import("../termio.zig"); const Command = @import("../Command.zig"); -const Pty = @import("../Pty.zig"); +const Pty = @import("../Pty.zig").Pty; const SegmentedPool = @import("../segmented_pool.zig").SegmentedPool; const terminal = @import("../terminal/main.zig"); const terminfo = @import("../terminfo/main.zig"); @@ -23,6 +23,7 @@ const fastmem = @import("../fastmem.zig"); const internal_os = @import("../os/main.zig"); const configpkg = @import("../config.zig"); const shell_integration = @import("shell_integration.zig"); +const windows = @import("../windows.zig"); const log = std.log.scoped(.io_exec); @@ -184,7 +185,7 @@ pub fn threadEnter(self: *Exec, thread: *termio.Thread) !ThreadData { const alloc = self.alloc; // Start our subprocess - const master_fd = try self.subprocess.start(alloc); + try self.subprocess.start(alloc); errdefer self.subprocess.stop(); const pid = pid: { const command = self.subprocess.command orelse return error.ProcessNotStarted; @@ -193,7 +194,15 @@ pub fn threadEnter(self: *Exec, thread: *termio.Thread) !ThreadData { // Create our pipe that we'll use to kill our read thread. // pipe[0] is the read end, pipe[1] is the write end. - const pipe = try std.os.pipe(); + const pipe = if (builtin.os.tag == .windows) pipe: { + var read: windows.HANDLE = undefined; + var write: windows.HANDLE = undefined; + if (windows.exp.kernel32.CreatePipe(&read, &write, null, 0) == 0) { + return windows.unexpectedError(windows.kernel32.GetLastError()); + } + + break :pipe .{ read, write }; + } else try std.os.pipe(); errdefer std.os.close(pipe[0]); errdefer std.os.close(pipe[1]); @@ -202,7 +211,8 @@ pub fn threadEnter(self: *Exec, thread: *termio.Thread) !ThreadData { errdefer alloc.destroy(ev_data_ptr); // Setup our stream so that we can write. - var stream = xev.Stream.initFd(master_fd); + const write_fd = if (builtin.os.tag == .windows) self.subprocess.pty.?.in_pipe else self.subprocess.pty.?.master; + var stream = xev.Stream.initFd(write_fd); errdefer stream.deinit(); // Wakeup watcher for the writer thread. @@ -262,10 +272,11 @@ pub fn threadEnter(self: *Exec, thread: *termio.Thread) !ThreadData { ); // Start our reader thread + const read_fd = if (builtin.os.tag == .windows) self.subprocess.pty.?.out_pipe else self.subprocess.pty.?.master; const read_thread = try std.Thread.spawn( .{}, - ReadThread.threadMain, - .{ master_fd, ev_data_ptr, pipe[0] }, + if (builtin.os.tag == .windows) ReadThread.threadMainWindows else ReadThread.threadMainPosix, + .{ read_fd, ev_data_ptr, pipe[0] }, ); read_thread.setName("io-reader") catch {}; @@ -275,6 +286,7 @@ pub fn threadEnter(self: *Exec, thread: *termio.Thread) !ThreadData { .ev = ev_data_ptr, .read_thread = read_thread, .read_thread_pipe = pipe[1], + .read_thread_fd = if (builtin.os.tag == .windows) read_fd else {}, }; } @@ -291,6 +303,17 @@ pub fn threadExit(self: *Exec, data: ThreadData) void { // a particularly noisy process. _ = std.os.write(data.read_thread_pipe, "x") catch |err| log.warn("error writing to read thread quit pipe err={}", .{err}); + + if (builtin.os.tag == .windows) { + // Interrupt the blocking read so the thread can see the quit message + if (windows.kernel32.CancelIoEx(data.read_thread_fd, null) == 0) { + switch (windows.kernel32.GetLastError()) { + .NOT_FOUND => {}, + else => |err| log.warn("error interrupting read thread err={}", .{err}), + } + } + } + data.read_thread.join(); } @@ -476,7 +499,7 @@ pub inline fn queueWrite(self: *Exec, data: []const u8, linefeed: bool) !void { break :slice buf[0..buf_i]; }; - // for (slice) |b| log.warn("write: {x}", .{b}); + //for (slice) |b| log.warn("write: {x}", .{b}); ev.data_stream.queueWrite( ev.loop, @@ -500,6 +523,7 @@ const ThreadData = struct { /// Our read thread read_thread: std.Thread, read_thread_pipe: std.os.fd_t, + read_thread_fd: if (builtin.os.tag == .windows) std.os.fd_t else void, pub fn deinit(self: *ThreadData) void { std.os.close(self.read_thread_pipe); @@ -662,7 +686,8 @@ const Subprocess = struct { const alloc = arena.allocator(); // Determine the path to the binary we're executing - const path = (try Command.expandPath(alloc, opts.full_config.command orelse "sh")) orelse + const default_command = if (builtin.os.tag == .windows) "cmd.exe" else "sh"; + const path = (try Command.expandPath(alloc, opts.full_config.command orelse default_command)) orelse return error.CommandNotFound; // On macOS, we launch the program as a login shell. This is a Mac-specific @@ -833,7 +858,7 @@ const Subprocess = struct { /// Start the subprocess. If the subprocess is already started this /// will crash. - pub fn start(self: *Subprocess, alloc: Allocator) !std.os.fd_t { + pub fn start(self: *Subprocess, alloc: Allocator) !void { assert(self.pty == null and self.command == null); // Create our pty @@ -886,8 +911,6 @@ const Subprocess = struct { // wait right now but that is fine too. This lets us read the // parent and detect EOF. _ = std.os.close(pty.slave); - - return pty.master; } // If we can't access the cwd, then don't set any cwd and inherit. @@ -908,10 +931,11 @@ const Subprocess = struct { .args = self.args, .env = &self.env, .cwd = cwd, - .stdin = .{ .handle = pty.slave }, - .stdout = .{ .handle = pty.slave }, - .stderr = .{ .handle = pty.slave }, - .pre_exec = (struct { + .stdin = if (builtin.os.tag == .windows) null else .{ .handle = pty.slave }, + .stdout = if (builtin.os.tag == .windows) null else .{ .handle = pty.slave }, + .stderr = if (builtin.os.tag == .windows) null else .{ .handle = pty.slave }, + .pseudo_console = if (builtin.os.tag == .windows) pty.pseudo_console else {}, + .pre_exec = if (builtin.os.tag == .windows) null else (struct { fn callback(cmd: *Command) void { const p = cmd.getData(Pty) orelse unreachable; p.childPreExec() catch |err| @@ -925,7 +949,6 @@ const Subprocess = struct { log.info("started subcommand path={s} pid={?}", .{ self.path, cmd.pid }); self.command = cmd; - return pty.master; } /// Called to notify that we exited externally so we can unset our @@ -969,7 +992,7 @@ const Subprocess = struct { self.grid_size = grid_size; self.screen_size = screen_size; - if (self.pty) |pty| { + if (self.pty) |*pty| { try pty.setSize(.{ .ws_row = @intCast(grid_size.rows), .ws_col = @intCast(grid_size.columns), @@ -983,28 +1006,34 @@ const Subprocess = struct { /// process. This doesn't wait for the child process to be exited. fn killCommand(command: *Command) !void { if (command.pid) |pid| { - const pgid_: ?c.pid_t = pgid: { - const pgid = c.getpgid(pid); - - // Don't know why it would be zero but its not a valid pid - if (pgid == 0) break :pgid null; - - // If the pid doesn't exist then... okay. - if (pgid == c.ESRCH) break :pgid null; - - // If we have an error... - if (pgid < 0) { - log.warn("error getting pgid for kill", .{}); - break :pgid null; + if (builtin.os.tag == .windows) { + if (windows.kernel32.TerminateProcess(pid, 0) == 0) { + return windows.unexpectedError(windows.kernel32.GetLastError()); } + } else { + const pgid_: ?c.pid_t = pgid: { + const pgid = c.getpgid(pid); - break :pgid pgid; - }; + // Don't know why it would be zero but its not a valid pid + if (pgid == 0) break :pgid null; - if (pgid_) |pgid| { - if (c.killpg(pgid, c.SIGHUP) < 0) { - log.warn("error killing process group pgid={}", .{pgid}); - return error.KillFailed; + // If the pid doesn't exist then... okay. + if (pgid == c.ESRCH) break :pgid null; + + // If we have an error... + if (pgid < 0) { + log.warn("error getting pgid for kill", .{}); + break :pgid null; + } + + break :pgid pgid; + }; + + if (pgid_) |pgid| { + if (c.killpg(pgid, c.SIGHUP) < 0) { + log.warn("error killing process group pgid={}", .{pgid}); + return error.KillFailed; + } } } } @@ -1034,8 +1063,7 @@ const Subprocess = struct { /// fds and this is still much faster and lower overhead than any async /// mechanism. const ReadThread = struct { - /// The main entrypoint for the thread. - fn threadMain(fd: std.os.fd_t, ev: *EventData, quit: std.os.fd_t) void { + fn threadMainPosix(fd: std.os.fd_t, ev: *EventData, quit: std.os.fd_t) void { // Always close our end of the pipe when we exit. defer std.os.close(quit); @@ -1112,6 +1140,44 @@ const ReadThread = struct { } } + /// The main entrypoint for the thread. + fn threadMainWindows(fd: std.os.fd_t, ev: *EventData, quit: std.os.fd_t) void { + // Always close our end of the pipe when we exit. + defer std.os.close(quit); + + var buf: [1024]u8 = undefined; + while (true) { + while (true) { + var n: windows.DWORD = 0; + if (windows.kernel32.ReadFile(fd, &buf, buf.len, &n, null) == 0) { + const err = windows.kernel32.GetLastError(); + switch (err) { + // Check for a quit signal + .OPERATION_ABORTED => break, + else => { + log.err("io reader error err={}", .{err}); + unreachable; + }, + } + } + + @call(.always_inline, process, .{ ev, buf[0..n] }); + } + + var quit_bytes: windows.DWORD = 0; + if (windows.exp.kernel32.PeekNamedPipe(quit, null, 0, null, &quit_bytes, null) == 0) { + const err = windows.kernel32.GetLastError(); + log.err("quit pipe reader error err={}", .{err}); + unreachable; + } + + if (quit_bytes > 0) { + log.info("read thread got quit signal", .{}); + return; + } + } + } + fn process( ev: *EventData, buf: []const u8, @@ -1913,6 +1979,11 @@ const StreamHandler = struct { } pub fn reportPwd(self: *StreamHandler, url: []const u8) !void { + if (builtin.os.tag == .windows) { + log.warn("reportPwd unimplemented on windows", .{}); + return; + } + const uri = std.Uri.parse(url) catch |e| { log.warn("invalid url in OSC 7: {}", .{e}); return; diff --git a/src/windows.zig b/src/windows.zig new file mode 100644 index 000000000..063122738 --- /dev/null +++ b/src/windows.zig @@ -0,0 +1,100 @@ +const std = @import("std"); +const windows = std.os.windows; + +pub usingnamespace std.os.windows; + +pub const exp = struct { + pub const HPCON = windows.LPVOID; + + pub const CREATE_UNICODE_ENVIRONMENT = 0x00000400; + pub const EXTENDED_STARTUPINFO_PRESENT = 0x00080000; + pub const LPPROC_THREAD_ATTRIBUTE_LIST = ?*anyopaque; + pub const FILE_FLAG_FIRST_PIPE_INSTANCE = 0x00080000; + + pub const STATUS_PENDING = 0x00000103; + pub const STILL_ACTIVE = STATUS_PENDING; + + pub const STARTUPINFOEX = extern struct { + StartupInfo: windows.STARTUPINFOW, + lpAttributeList: LPPROC_THREAD_ATTRIBUTE_LIST, + }; + + pub const kernel32 = struct { + pub extern "kernel32" fn CreatePipe( + hReadPipe: *windows.HANDLE, + hWritePipe: *windows.HANDLE, + lpPipeAttributes: ?*const windows.SECURITY_ATTRIBUTES, + nSize: windows.DWORD, + ) callconv(windows.WINAPI) windows.BOOL; + pub extern "kernel32" fn CreatePseudoConsole( + size: windows.COORD, + hInput: windows.HANDLE, + hOutput: windows.HANDLE, + dwFlags: windows.DWORD, + phPC: *HPCON, + ) callconv(windows.WINAPI) windows.HRESULT; + pub extern "kernel32" fn ResizePseudoConsole(hPC: HPCON, size: windows.COORD) callconv(windows.WINAPI) windows.HRESULT; + pub extern "kernel32" fn ClosePseudoConsole(hPC: HPCON) callconv(windows.WINAPI) void; + pub extern "kernel32" fn InitializeProcThreadAttributeList( + lpAttributeList: LPPROC_THREAD_ATTRIBUTE_LIST, + dwAttributeCount: windows.DWORD, + dwFlags: windows.DWORD, + lpSize: *windows.SIZE_T, + ) callconv(windows.WINAPI) windows.BOOL; + pub extern "kernel32" fn UpdateProcThreadAttribute( + lpAttributeList: LPPROC_THREAD_ATTRIBUTE_LIST, + dwFlags: windows.DWORD, + Attribute: windows.DWORD_PTR, + lpValue: windows.PVOID, + cbSize: windows.SIZE_T, + lpPreviousValue: ?windows.PVOID, + lpReturnSize: ?*windows.SIZE_T, + ) callconv(windows.WINAPI) windows.BOOL; + pub extern "kernel32" fn PeekNamedPipe( + hNamedPipe: windows.HANDLE, + lpBuffer: ?windows.LPVOID, + nBufferSize: windows.DWORD, + lpBytesRead: ?*windows.DWORD, + lpTotalBytesAvail: ?*windows.DWORD, + lpBytesLeftThisMessage: ?*windows.DWORD, + ) callconv(windows.WINAPI) windows.BOOL; + // Duplicated here because lpCommandLine is not marked optional in zig std + pub extern "kernel32" fn CreateProcessW( + lpApplicationName: ?windows.LPWSTR, + lpCommandLine: ?windows.LPWSTR, + lpProcessAttributes: ?*windows.SECURITY_ATTRIBUTES, + lpThreadAttributes: ?*windows.SECURITY_ATTRIBUTES, + bInheritHandles: windows.BOOL, + dwCreationFlags: windows.DWORD, + lpEnvironment: ?*anyopaque, + lpCurrentDirectory: ?windows.LPWSTR, + lpStartupInfo: *windows.STARTUPINFOW, + lpProcessInformation: *windows.PROCESS_INFORMATION, + ) callconv(windows.WINAPI) windows.BOOL; + }; + + pub const PROC_THREAD_ATTRIBUTE_NUMBER = 0x0000FFFF; + pub const PROC_THREAD_ATTRIBUTE_THREAD = 0x00010000; + pub const PROC_THREAD_ATTRIBUTE_INPUT = 0x00020000; + pub const PROC_THREAD_ATTRIBUTE_ADDITIVE = 0x00040000; + + pub const ProcThreadAttributeNumber = enum(windows.DWORD) { + ProcThreadAttributePseudoConsole = 22, + _, + }; + + /// Corresponds to the ProcThreadAttributeValue define in WinBase.h + pub fn ProcThreadAttributeValue( + comptime attribute: ProcThreadAttributeNumber, + comptime thread: bool, + comptime input: bool, + comptime additive: bool, + ) windows.DWORD { + return (@intFromEnum(attribute) & PROC_THREAD_ATTRIBUTE_NUMBER) | + (if (thread) PROC_THREAD_ATTRIBUTE_THREAD else 0) | + (if (input) PROC_THREAD_ATTRIBUTE_INPUT else 0) | + (if (additive) PROC_THREAD_ATTRIBUTE_ADDITIVE else 0); + } + + pub const PROC_THREAD_ATTRIBUTE_PSEUDOCONSOLE = ProcThreadAttributeValue(.ProcThreadAttributePseudoConsole, false, true, false); +};