From 232df8de8ff23ca6f3bd65516224df0f80ddf6ad Mon Sep 17 00:00:00 2001 From: kcbanner Date: Sun, 29 Oct 2023 04:03:06 -0400 Subject: [PATCH 01/16] windows: add support for the glfw backend on Windows Changes: - Add WindowsPty, which uses the ConPTY API to create a pseudo console - Pty now selects between PosixPty and WindowsPty - Windows support in Command, including the ability to launch a process with a pseudo console - Enable Command tests on windows - Add some environment variable abstractions to handle the missing libc APIs on Windows - Windows version of ReadThread --- build.zig.zon | 4 +- src/App.zig | 7 +- src/Command.zig | 324 +++++++++++++++++++++++++++++++++------- src/Pty.zig | 314 ++++++++++++++++++++++++++------------ src/Surface.zig | 4 +- src/config/Config.zig | 42 ++++-- src/main.zig | 5 +- src/os/env.zig | 33 ++++ src/os/locale.zig | 19 +-- src/os/passwd.zig | 4 +- src/os/resourcesdir.zig | 26 ++-- src/termio/Exec.zig | 147 +++++++++++++----- src/windows.zig | 100 +++++++++++++ 13 files changed, 795 insertions(+), 234 deletions(-) create mode 100644 src/windows.zig 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); +}; From 9a5322eaf4c5a3bc7bb0e09baeb4f51350aa2992 Mon Sep 17 00:00:00 2001 From: kcbanner Date: Tue, 31 Oct 2023 19:32:34 -0400 Subject: [PATCH 02/16] - Update libxev dependency - Fixup macos compile error --- build.zig.zon | 4 ++-- src/os/locale.zig | 8 +++++--- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/build.zig.zon b/build.zig.zon index 6c0b41049..c0c0d21d2 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -5,8 +5,8 @@ .dependencies = .{ // Zig libs .libxev = .{ - .url = "https://github.com/kcbanner/libxev/archive/d71ddeb19c6697875c2e0e68a49d04c05bc26199.tar.gz", - .hash = "1220a26265c30f5677ce28daf04afbd7a6ef5a17f5d3b5506f767fdfee7dc4225d2b", + .url = "https://github.com/mitchellh/libxev/archive/1b46c2d6f32754a3029d1863275dd0f877163831.tar.gz", + .hash = "12208dc0796bffa6ea9edb60193108e34f835f35fc152e853717aca2b13ba17f3be2", }, .mach_glfw = .{ .url = "https://github.com/hexops/mach-glfw/archive/16dc95cc7f74ebbbdd848d9a2c3cc4afc5717708.tar.gz", diff --git a/src/os/locale.zig b/src/os/locale.zig index 7b11bf239..2efdd1c69 100644 --- a/src/os/locale.zig +++ b/src/os/locale.zig @@ -21,8 +21,10 @@ pub fn ensureLocale(alloc: std.mem.Allocator) !void { // process. if (comptime builtin.target.isDarwin()) { // Set the lang if it is not set or if its empty. - if (lang.len == 0) { - setLangFromCocoa(); + if (lang) |l| { + if (l.len == 0) { + setLangFromCocoa(); + } } } @@ -98,7 +100,7 @@ fn setLangFromCocoa() void { log.info("detected system locale={s}", .{env_value}); // Set it onto our environment - if (internal_os.setEnv("LANG", env_value.ptr) < 0) { + if (internal_os.setEnv("LANG", env_value) < 0) { log.err("error setting locale env var", .{}); return; } From 7594bbd621508d4b950b65e277682a021fca1d00 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sun, 5 Nov 2023 15:27:46 -0800 Subject: [PATCH 03/16] shuffle some source around --- src/Command.zig | 28 ++++++++++++++++------------ src/Pty.zig | 3 +-- src/os/main.zig | 1 + src/{ => os}/windows.zig | 0 src/termio/Exec.zig | 4 ++-- 5 files changed, 20 insertions(+), 16 deletions(-) rename src/{ => os}/windows.zig (100%) diff --git a/src/Command.zig b/src/Command.zig index de284040e..5c9a6d590 100644 --- a/src/Command.zig +++ b/src/Command.zig @@ -14,16 +14,12 @@ //! * posix_spawn is used for Mac, but doesn't support the necessary //! features for tty setup. //! -//! TODO: -//! -//! * Mac -//! const Command = @This(); const std = @import("std"); const builtin = @import("builtin"); const internal_os = @import("os/main.zig"); -const windows = @import("windows.zig"); +const windows = internal_os.windows; const TempDir = internal_os.TempDir; const mem = std.mem; const os = std.os; @@ -297,15 +293,19 @@ 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) { - - // Block until the process exits. This returns immediately if the process already exited. + if (comptime 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()); + 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()); + if (!has_code) { + return windows.unexpectedError(windows.kernel32.GetLastError()); + } return .{ .Exited = exit_code }; } @@ -444,7 +444,7 @@ fn createNullDelimitedEnvMap(arena: mem.Allocator, env_map: *const EnvMap) ![:nu // 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 { +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 @@ -571,7 +571,11 @@ test "Command: pre exec" { 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); + try windows.SetHandleInformation( + file.handle, + windows.HANDLE_FLAG_INHERIT, + windows.HANDLE_FLAG_INHERIT, + ); } return file; diff --git a/src/Pty.zig b/src/Pty.zig index 90108a4a7..7e8c0bf7c 100644 --- a/src/Pty.zig +++ b/src/Pty.zig @@ -1,6 +1,6 @@ const std = @import("std"); const builtin = @import("builtin"); -const windows = @import("windows.zig"); +const windows = @import("os/main.zig").windows; const fd_t = std.os.fd_t; const c = switch (builtin.os.tag) { @@ -37,7 +37,6 @@ else /// of Linux syscalls. The caller is responsible for detail-oriented handling /// of the returned file handles. pub const PosixPty = struct { - // 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; diff --git a/src/os/main.zig b/src/os/main.zig index 9b9b2d549..b2d073f55 100644 --- a/src/os/main.zig +++ b/src/os/main.zig @@ -13,3 +13,4 @@ pub usingnamespace @import("resourcesdir.zig"); pub const TempDir = @import("TempDir.zig"); pub const passwd = @import("passwd.zig"); pub const xdg = @import("xdg.zig"); +pub const windows = @import("windows.zig"); diff --git a/src/windows.zig b/src/os/windows.zig similarity index 100% rename from src/windows.zig rename to src/os/windows.zig diff --git a/src/termio/Exec.zig b/src/termio/Exec.zig index 1e4e2ed63..2dba63fd6 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").Pty; +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"); @@ -21,9 +21,9 @@ const trace = tracy.trace; const apprt = @import("../apprt.zig"); const fastmem = @import("../fastmem.zig"); const internal_os = @import("../os/main.zig"); +const windows = internal_os.windows; const configpkg = @import("../config.zig"); const shell_integration = @import("shell_integration.zig"); -const windows = @import("../windows.zig"); const log = std.log.scoped(.io_exec); From 8f35d5251e8417556e397ad0f4f1d9aa3f0e10ae Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sun, 5 Nov 2023 15:39:25 -0800 Subject: [PATCH 04/16] os: rename env to be posix-like, do not allocate on posix --- src/main.zig | 6 ++-- src/os/env.zig | 88 +++++++++++++++++++++++++++++------------------ src/os/locale.zig | 20 +++++------ 3 files changed, 68 insertions(+), 46 deletions(-) diff --git a/src/main.zig b/src/main.zig index e7dd6c633..17ec5b0f8 100644 --- a/src/main.zig +++ b/src/main.zig @@ -243,9 +243,9 @@ 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 ((try internal_os.getEnvVarOwned(self.alloc, "GHOSTTY_LOG"))) |v| { - defer self.alloc.free(v); - if (v.len > 0) { + if ((try internal_os.getenv(self.alloc, "GHOSTTY_LOG"))) |v| { + defer v.deinit(self.alloc); + if (v.value.len > 0) { self.logging = .{ .stderr = {} }; } } diff --git a/src/os/env.zig b/src/os/env.zig index a3c34bd79..3f94b2a76 100644 --- a/src/os/env.zig +++ b/src/os/env.zig @@ -25,6 +25,61 @@ pub fn appendEnv( }); } +/// The result of getenv, with a shared deinit to properly handle allocation +/// on Windows. +pub const GetEnvResult = struct { + value: []const u8, + + pub fn deinit(self: GetEnvResult, alloc: Allocator) void { + switch (builtin.os.tag) { + .windows => alloc.free(self.value), + else => {}, + } + } +}; + +/// Gets the value of an environment variable, or null if not found. +/// This will allocate on Windows but not on other platforms. The returned +/// value should have deinit called to do the proper cleanup no matter what +/// platform you are on. +pub fn getenv(alloc: Allocator, key: []const u8) !?GetEnvResult { + return switch (builtin.os.tag) { + // Non-Windows doesn't need to allocate + else => if (std.os.getenv(key)) |v| .{ .value = v } else null, + + // Windows needs to allocate + .windows => if (std.process.getEnvVarOwned(alloc, key)) |v| .{ + .value = v, + } else |err| switch (err) { + error.EnvironmentVariableNotFound => null, + else => err, + }, + }; +} + +pub fn setenv(key: [:0]const u8, value: [:0]const u8) c_int { + return switch (builtin.os.tag) { + .windows => c._putenv_s(key.ptr, value.ptr), + else => c.setenv(key.ptr, value.ptr, 1), + }; +} + +pub fn unsetenv(key: [:0]const u8) c_int { + return switch (builtin.os.tag) { + .windows => c._putenv_s(key.ptr, ""), + else => c.unsetenv(key.ptr), + }; +} + +const c = struct { + // POSIX + extern "c" fn setenv(name: ?[*]const u8, value: ?[*]const u8, overwrite: c_int) c_int; + extern "c" fn unsetenv(name: ?[*]const u8) c_int; + + // Windows + extern "c" fn _putenv_s(varname: ?[*]const u8, value_string: ?[*]const u8) c_int; +}; + test "appendEnv empty" { const testing = std.testing; const alloc = testing.allocator; @@ -46,36 +101,3 @@ 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 2efdd1c69..c05fa9d6b 100644 --- a/src/os/locale.zig +++ b/src/os/locale.zig @@ -12,8 +12,8 @@ pub fn ensureLocale(alloc: std.mem.Allocator) !void { // Get our LANG env var. We use this many times but we also need // the original value later. - const lang = try internal_os.getEnvVarOwned(alloc, "LANG"); - defer if (lang) |v| alloc.free(v); + const lang = try internal_os.getenv(alloc, "LANG"); + defer if (lang) |v| v.deinit(alloc); // 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 @@ -22,7 +22,7 @@ pub fn ensureLocale(alloc: std.mem.Allocator) !void { if (comptime builtin.target.isDarwin()) { // Set the lang if it is not set or if its empty. if (lang) |l| { - if (l.len == 0) { + if (l.value.len == 0) { setLangFromCocoa(); } } @@ -37,13 +37,13 @@ pub fn ensureLocale(alloc: std.mem.Allocator) !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 ((try internal_os.getEnvVarOwned(alloc, "LANG"))) |old_lang| { - defer alloc.free(old_lang); - if (old_lang.len > 0) { + if ((try internal_os.getenv(alloc, "LANG"))) |old_lang| { + defer old_lang.deinit(alloc); + if (old_lang.value.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. - _ = internal_os.setEnv("LANG", ""); - _ = internal_os.unsetEnv("LANG"); + _ = internal_os.setenv("LANG", ""); + _ = internal_os.unsetenv("LANG"); if (setlocale(LC_ALL, "")) |v| { log.info("setlocale after unset lang result={s}", .{v}); @@ -59,7 +59,7 @@ pub fn ensureLocale(alloc: std.mem.Allocator) !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| { - _ = internal_os.setEnv("LANG", "en_US.UTF-8"); + _ = 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", .{}); @@ -100,7 +100,7 @@ fn setLangFromCocoa() void { log.info("detected system locale={s}", .{env_value}); // Set it onto our environment - if (internal_os.setEnv("LANG", env_value) < 0) { + if (internal_os.setenv("LANG", env_value) < 0) { log.err("error setting locale env var", .{}); return; } From 74b840df8e18f9d82932ac538b0a1e485b360eae Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sun, 5 Nov 2023 23:41:45 +0000 Subject: [PATCH 05/16] rename Pty.zig to pty.zig --- src/Surface.zig | 2 +- src/main.zig | 2 +- src/os/passwd.zig | 2 +- src/{Pty.zig => pty.zig} | 0 4 files changed, 3 insertions(+), 3 deletions(-) rename src/{Pty.zig => pty.zig} (100%) diff --git a/src/Surface.zig b/src/Surface.zig index e00e5a8bb..80e8db79c 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").Pty; +const Pty = @import("pty.zig").Pty; const font = @import("font/main.zig"); const Command = @import("Command.zig"); const trace = @import("tracy").trace; diff --git a/src/main.zig b/src/main.zig index 17ec5b0f8..16ad70ef3 100644 --- a/src/main.zig +++ b/src/main.zig @@ -285,7 +285,7 @@ pub const GlobalState = struct { }; test { _ = @import("circ_buf.zig"); - _ = @import("Pty.zig"); + _ = @import("pty.zig"); _ = @import("Command.zig"); _ = @import("font/main.zig"); _ = @import("apprt.zig"); diff --git a/src/os/passwd.zig b/src/os/passwd.zig index eac005e4b..a9bfb0e8a 100644 --- a/src/os/passwd.zig +++ b/src/os/passwd.zig @@ -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").Pty; + const Pty = @import("../pty.zig").Pty; var pty = try Pty.open(.{}); defer pty.deinit(); var cmd: internal_os.FlatpakHostCommand = .{ diff --git a/src/Pty.zig b/src/pty.zig similarity index 100% rename from src/Pty.zig rename to src/pty.zig From ea5ff77e29fcc8af2e61e1b549ecbfff1f4fcf98 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sun, 5 Nov 2023 15:46:05 -0800 Subject: [PATCH 06/16] os: macos lang check should include lang null --- src/os/locale.zig | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/os/locale.zig b/src/os/locale.zig index c05fa9d6b..3c3f35fcf 100644 --- a/src/os/locale.zig +++ b/src/os/locale.zig @@ -21,10 +21,8 @@ pub fn ensureLocale(alloc: std.mem.Allocator) !void { // process. if (comptime builtin.target.isDarwin()) { // Set the lang if it is not set or if its empty. - if (lang) |l| { - if (l.value.len == 0) { - setLangFromCocoa(); - } + if (lang == null or lang.?.value.len == 0) { + setLangFromCocoa(); } } From b39d1d6095702bf80fd11bbad8842d22c409ae58 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sun, 5 Nov 2023 15:46:31 -0800 Subject: [PATCH 07/16] shuffle some code --- src/config/Config.zig | 60 +++++++++++++++++++++++-------------------- src/os/homedir.zig | 1 + 2 files changed, 33 insertions(+), 28 deletions(-) diff --git a/src/config/Config.zig b/src/config/Config.zig index 7f7222aa5..b7db59ae4 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -1134,38 +1134,42 @@ pub fn finalize(self: *Config) !void { } } - 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) { - var buf: [std.fs.MAX_PATH_BYTES]u8 = undefined; - if (try internal_os.home(&buf)) |home| { - self.@"working-directory" = try alloc.dupe(u8, home); + switch (builtin.os.tag) { + .windows => { + if (self.command == null) { + log.warn("no default shell found, will default to using cmd", .{}); + self.command = "cmd.exe"; } - } - } 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 (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); + } } - } + }, - if (self.command == null) { - log.warn("no default shell found, will default to using sh", .{}); - } + 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 (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/os/homedir.zig b/src/os/homedir.zig index 5790c4ce9..809afea30 100644 --- a/src/os/homedir.zig +++ b/src/os/homedir.zig @@ -103,6 +103,7 @@ fn homeWindows(buf: []u8) !?[]u8 { if (homepath.ptr != path_buf.ptr) @panic("codebug"); break :blk homepath.len; }; + return buf[0 .. drive_len + path_len]; } From fbd2c344875b5f77dde0a50bf890854ecc55ec97 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sun, 5 Nov 2023 15:48:43 -0800 Subject: [PATCH 08/16] os: more comments --- src/os/resourcesdir.zig | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/os/resourcesdir.zig b/src/os/resourcesdir.zig index 940321469..9726b6cdf 100644 --- a/src/os/resourcesdir.zig +++ b/src/os/resourcesdir.zig @@ -10,6 +10,8 @@ const Allocator = std.mem.Allocator; /// some point but we can cross that bridge if we ever need to. pub fn resourcesDir(alloc: std.mem.Allocator) !?[]const u8 { // If we have an environment variable set, we always use that. + // Note: we ALWAYS want to allocate here because the result is always + // freed, do not try to use internal_os.getenv or posix getenv. if (std.process.getEnvVarOwned(alloc, "GHOSTTY_RESOURCES_DIR")) |dir| { if (dir.len > 0) return dir; } else |err| switch (err) { From 3dc2bbc9b08615085f8eadf5103bc2ed88fe832a Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sun, 5 Nov 2023 15:54:50 -0800 Subject: [PATCH 09/16] os: add internal_os.pipe for cross-platfor pipe --- src/os/main.zig | 1 + src/os/pipe.zig | 19 +++++++++++++++++++ src/termio/Exec.zig | 12 ++---------- 3 files changed, 22 insertions(+), 10 deletions(-) create mode 100644 src/os/pipe.zig diff --git a/src/os/main.zig b/src/os/main.zig index b2d073f55..7fdcb2d8b 100644 --- a/src/os/main.zig +++ b/src/os/main.zig @@ -9,6 +9,7 @@ pub usingnamespace @import("homedir.zig"); pub usingnamespace @import("locale.zig"); pub usingnamespace @import("macos_version.zig"); pub usingnamespace @import("mouse.zig"); +pub usingnamespace @import("pipe.zig"); pub usingnamespace @import("resourcesdir.zig"); pub const TempDir = @import("TempDir.zig"); pub const passwd = @import("passwd.zig"); diff --git a/src/os/pipe.zig b/src/os/pipe.zig new file mode 100644 index 000000000..67c6b943e --- /dev/null +++ b/src/os/pipe.zig @@ -0,0 +1,19 @@ +const std = @import("std"); +const builtin = @import("builtin"); +const windows = @import("windows.zig"); + +/// pipe() that works on Windows and POSIX. +pub fn pipe() ![2]std.os.fd_t { + switch (builtin.os.tag) { + else => return try std.os.pipe(), + .windows => { + 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()); + } + + return .{ read, write }; + }, + } +} diff --git a/src/termio/Exec.zig b/src/termio/Exec.zig index 2dba63fd6..3ae0f5469 100644 --- a/src/termio/Exec.zig +++ b/src/termio/Exec.zig @@ -194,15 +194,7 @@ 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 = 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(); + const pipe = try internal_os.pipe(); errdefer std.os.close(pipe[0]); errdefer std.os.close(pipe[1]); @@ -1140,7 +1132,6 @@ 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); @@ -1154,6 +1145,7 @@ const ReadThread = struct { switch (err) { // Check for a quit signal .OPERATION_ABORTED => break, + else => { log.err("io reader error err={}", .{err}); unreachable; From 85a5a231f2b6c1c64327c0ef172f933f1a02bc81 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sun, 5 Nov 2023 17:52:46 -0800 Subject: [PATCH 10/16] termio: cleanup --- src/pty.zig | 13 +++--- src/termio/Exec.zig | 101 ++++++++++++++++++++++++++++---------------- 2 files changed, 72 insertions(+), 42 deletions(-) diff --git a/src/pty.zig b/src/pty.zig index 7e8c0bf7c..657e3dcaf 100644 --- a/src/pty.zig +++ b/src/pty.zig @@ -1,7 +1,6 @@ const std = @import("std"); const builtin = @import("builtin"); const windows = @import("os/main.zig").windows; -const fd_t = std.os.fd_t; const c = switch (builtin.os.tag) { .macos => @cImport({ @@ -37,6 +36,8 @@ else /// of Linux syscalls. The caller is responsible for detail-oriented handling /// of the returned file handles. pub const PosixPty = struct { + pub const Fd = std.os.fd_t; + // 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; @@ -44,16 +45,16 @@ pub const PosixPty = struct { const TIOCGWINSZ = if (builtin.os.tag == .macos) 1074295912 else c.TIOCGWINSZ; /// The file descriptors for the master and slave side of the pty. - master: fd_t, - slave: fd_t, + master: Fd, + slave: Fd, /// 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; + var master_fd: Fd = undefined; + var slave_fd: Fd = undefined; if (c.openpty( &master_fd, &slave_fd, @@ -128,6 +129,8 @@ pub const PosixPty = struct { /// Windows PTY creation and management. pub const WindowsPty = struct { + pub const Fd = windows.HANDLE; + // Process-wide counter for pipe names var pipe_name_counter = std.atomic.Atomic(u32).init(1); diff --git a/src/termio/Exec.zig b/src/termio/Exec.zig index 3ae0f5469..d83a2e7d8 100644 --- a/src/termio/Exec.zig +++ b/src/termio/Exec.zig @@ -185,7 +185,7 @@ pub fn threadEnter(self: *Exec, thread: *termio.Thread) !ThreadData { const alloc = self.alloc; // Start our subprocess - try self.subprocess.start(alloc); + const pty_fds = try self.subprocess.start(alloc); errdefer self.subprocess.stop(); const pid = pid: { const command = self.subprocess.command orelse return error.ProcessNotStarted; @@ -203,8 +203,7 @@ pub fn threadEnter(self: *Exec, thread: *termio.Thread) !ThreadData { errdefer alloc.destroy(ev_data_ptr); // Setup our stream so that we can write. - 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); + var stream = xev.Stream.initFd(pty_fds.write); errdefer stream.deinit(); // Wakeup watcher for the writer thread. @@ -264,11 +263,10 @@ 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( .{}, if (builtin.os.tag == .windows) ReadThread.threadMainWindows else ReadThread.threadMainPosix, - .{ read_fd, ev_data_ptr, pipe[0] }, + .{ pty_fds.read, ev_data_ptr, pipe[0] }, ); read_thread.setName("io-reader") catch {}; @@ -278,7 +276,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 {}, + .read_thread_fd = if (builtin.os.tag == .windows) pty_fds.read else {}, }; } @@ -296,7 +294,7 @@ pub fn threadExit(self: *Exec, data: ThreadData) void { _ = 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) { + if (comptime 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()) { @@ -678,9 +676,13 @@ const Subprocess = struct { const alloc = arena.allocator(); // Determine the path to the binary we're executing - 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; + const path = try Command.expandPath( + alloc, + opts.full_config.command orelse switch (builtin.os.tag) { + .windows => "cmd.exe", + else => "sh", + }, + ) orelse return error.CommandNotFound; // On macOS, we launch the program as a login shell. This is a Mac-specific // behavior (see other terminals). Terminals in general should NOT be @@ -850,7 +852,10 @@ const Subprocess = struct { /// Start the subprocess. If the subprocess is already started this /// will crash. - pub fn start(self: *Subprocess, alloc: Allocator) !void { + pub fn start(self: *Subprocess, alloc: Allocator) !struct { + read: Pty.Fd, + write: Pty.Fd, + } { assert(self.pty == null and self.command == null); // Create our pty @@ -903,6 +908,11 @@ 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 .{ + .read = pty.master, + .write = pty.master, + }; } // If we can't access the cwd, then don't set any cwd and inherit. @@ -937,10 +947,23 @@ const Subprocess = struct { .data = &self.pty.?, }; try cmd.start(alloc); - errdefer killCommand(cmd); + errdefer killCommand(&cmd) catch |err| { + log.warn("error killing command during cleanup err={}", .{err}); + }; log.info("started subcommand path={s} pid={?}", .{ self.path, cmd.pid }); self.command = cmd; + return switch (builtin.os.tag) { + .windows => .{ + .read = pty.out_pipe, + .write = pty.in_pipe, + }, + + else => .{ + .read = pty.master, + .write = pty.master, + }, + }; } /// Called to notify that we exited externally so we can unset our @@ -998,35 +1021,39 @@ const Subprocess = struct { /// process. This doesn't wait for the child process to be exited. fn killCommand(command: *Command) !void { if (command.pid) |pid| { - 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); - - // 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; + switch (builtin.os.tag) { + .windows => { + if (windows.kernel32.TerminateProcess(pid, 0) == 0) { + return windows.unexpectedError(windows.kernel32.GetLastError()); } + }, - break :pgid pgid; - }; + else => { + const pgid_: ?c.pid_t = pgid: { + const pgid = c.getpgid(pid); - if (pgid_) |pgid| { - if (c.killpg(pgid, c.SIGHUP) < 0) { - log.warn("error killing process group pgid={}", .{pgid}); - return error.KillFailed; + // 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; + } + + break :pgid pgid; + }; + + if (pgid_) |pgid| { + if (c.killpg(pgid, c.SIGHUP) < 0) { + log.warn("error killing process group pgid={}", .{pgid}); + return error.KillFailed; + } } - } + }, } } } From c9b74393548dd9cbcad5420a2a46f04d3fa77055 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sun, 5 Nov 2023 17:59:08 -0800 Subject: [PATCH 11/16] pty: stylistic changes --- src/pty.zig | 63 ++++++++++++++++++++++++++++++++--------------------- 1 file changed, 38 insertions(+), 25 deletions(-) diff --git a/src/pty.zig b/src/pty.zig index 657e3dcaf..a482dc42b 100644 --- a/src/pty.zig +++ b/src/pty.zig @@ -2,17 +2,6 @@ const std = @import("std"); const builtin = @import("builtin"); const windows = @import("os/main.zig").windows; -const c = switch (builtin.os.tag) { - .macos => @cImport({ - @cInclude("sys/ioctl.h"); // ioctl and constants - @cInclude("util.h"); // openpty() - }), - else => @cImport({ - @cInclude("sys/ioctl.h"); // ioctl and constants - @cInclude("pty.h"); - }), -}; - const log = std.log.scoped(.pty); /// Redeclare this winsize struct so we can just use a Zig struct. This @@ -25,8 +14,6 @@ pub const winsize = extern struct { ws_ypixel: u16 = 600, }; -pub extern "c" fn setsid() std.c.pid_t; - pub const Pty = if (builtin.os.tag == .windows) WindowsPty else @@ -35,7 +22,7 @@ else /// 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 { +const PosixPty = struct { pub const Fd = std.os.fd_t; // https://github.com/ziglang/zig/issues/13277 @@ -43,6 +30,19 @@ pub const PosixPty = struct { 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; + extern "c" fn setsid() std.c.pid_t; + const c = struct { + usingnamespace switch (builtin.os.tag) { + .macos => @cImport({ + @cInclude("sys/ioctl.h"); // ioctl and constants + @cInclude("util.h"); // openpty() + }), + else => @cImport({ + @cInclude("sys/ioctl.h"); // ioctl and constants + @cInclude("pty.h"); + }), + }; + }; /// The file descriptors for the master and slave side of the pty. master: Fd, @@ -128,7 +128,7 @@ pub const PosixPty = struct { }; /// Windows PTY creation and management. -pub const WindowsPty = struct { +const WindowsPty = struct { pub const Fd = windows.HANDLE; // Process-wide counter for pipe names @@ -150,10 +150,16 @@ pub const WindowsPty = struct { const pipe_path = std.fmt.bufPrintZ( &pipe_path_buf, "\\\\.\\pipe\\LOCAL\\ghostty-pty-{d}-{d}", - .{ windows.kernel32.GetCurrentProcessId(), pipe_name_counter.fetchAdd(1, .Monotonic) }, + .{ + 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; + 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]; @@ -165,7 +171,9 @@ pub const WindowsPty = struct { 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_ACCESS_OUTBOUND | + windows.exp.FILE_FLAG_FIRST_PIPE_INSTANCE | + windows.FILE_FLAG_OVERLAPPED, windows.PIPE_TYPE_BYTE, 1, 4096, @@ -173,7 +181,9 @@ pub const WindowsPty = struct { 0, &security_attributes, ); - if (pty.in_pipe == windows.INVALID_HANDLE_VALUE) return windows.unexpectedError(windows.kernel32.GetLastError()); + 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; @@ -186,13 +196,17 @@ pub const WindowsPty = struct { windows.FILE_ATTRIBUTE_NORMAL, null, ); - if (pty.in_pipe_pty == windows.INVALID_HANDLE_VALUE) return windows.unexpectedError(windows.kernel32.GetLastError()); + 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. + // 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. + // 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()); @@ -222,7 +236,6 @@ pub const WindowsPty = struct { 0, &pty.pseudo_console, ); - if (result != windows.S_OK) return error.Unexpected; pty.size = size; @@ -255,8 +268,8 @@ pub const WindowsPty = struct { } }; -const testing = std.testing; test { + const testing = std.testing; var ws: winsize = .{ .ws_row = 50, .ws_col = 80, From 1a846597b1d82930479c91d661f1b3928a22264b Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sun, 5 Nov 2023 18:03:22 -0800 Subject: [PATCH 12/16] command: stylistic changes --- src/Command.zig | 252 +++++++++++++++++++++++++----------------------- 1 file changed, 130 insertions(+), 122 deletions(-) diff --git a/src/Command.zig b/src/Command.zig index 5c9a6d590..d12b67204 100644 --- a/src/Command.zig +++ b/src/Command.zig @@ -112,145 +112,153 @@ pub fn start(self: *Command, alloc: Allocator) !void { defer arena_allocator.deinit(); const arena = arena_allocator.allocator(); - 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; + switch (builtin.os.tag) { + .windows => try self.startWindows(arena), + else => try self.startPosix(arena), + } +} - 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); +fn startPosix(self: *Command, arena: Allocator) !void { + // 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; - // 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. + // 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 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, - ); + // Fork + const pid = try std.os.fork(); + if (pid != 0) { + // Parent, return immediately. + self.pid = @intCast(pid); + return; + } - 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()); + // We are the child. - 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()); + // 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); - 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 }; - }; + // Setup our working directory + if (self.cwd) |cwd| try os.chdir(cwd); - 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, - }; + // If the user requested a pre exec callback, call it now. + if (self.pre_exec) |f| f(self); - var flags: windows.DWORD = windows.exp.CREATE_UNICODE_ENVIRONMENT; - if (attribute_list != null) flags |= windows.exp.EXTENDED_STARTUPINFO_PRESENT; + // Finally, replace our process. + _ = std.os.execveZ(pathZ, argsZ, envp) catch null; +} - var process_information: windows.PROCESS_INFORMATION = undefined; - if (windows.exp.kernel32.CreateProcessW( - application_w.ptr, - if (command_line_w) |w| w.ptr else null, +fn startWindows(self: *Command, arena: Allocator) !void { + 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; + + 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); + + // 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. + + 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, - 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, + 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()); - 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; + 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()); - // 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"); + 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 }; + }; - // Fork - const pid = try std.os.fork(); - if (pid != 0) { - // Parent, return immediately. - self.pid = @intCast(pid); - return; - } + 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, + }; - // We are the child. + var flags: windows.DWORD = windows.exp.CREATE_UNICODE_ENVIRONMENT; + if (attribute_list != null) flags |= windows.exp.EXTENDED_STARTUPINFO_PRESENT; - // 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); + 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()); - // 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; - } + self.pid = process_information.hProcess; } fn setupFd(src: File.Handle, target: i32) !void { From 59a3e22d39e9682a068d726b995cfb2ad3f109cf Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sun, 5 Nov 2023 18:04:30 -0800 Subject: [PATCH 13/16] ci: enable windows cross-compile build --- .github/workflows/test.yml | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index abf4a1633..ed678ef09 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -10,9 +10,8 @@ jobs: target: [ aarch64-linux, x86_64-linux, - # No windows support currently. - # i386-windows, - # x86_64-windows-gnu, + x86-windows-gnu, + x86_64-windows-gnu, # We don't support cross-compiling to macOS because the macOS build # requires xcode due to the swift harness. From a864bfdd1dae83b26b01f7b1b8d436b399d03e52 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sun, 5 Nov 2023 18:09:51 -0800 Subject: [PATCH 14/16] ci: style --- .github/workflows/test.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index ed678ef09..55c421dd7 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -12,7 +12,6 @@ jobs: x86_64-linux, x86-windows-gnu, x86_64-windows-gnu, - # We don't support cross-compiling to macOS because the macOS build # requires xcode due to the swift harness. #aarch64-macos, From 088e8b230a91df52ccd6d958bf6a8faab4efa97d Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sun, 5 Nov 2023 18:12:38 -0800 Subject: [PATCH 15/16] apprt: default runtime for windows is glfw --- src/apprt.zig | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/apprt.zig b/src/apprt.zig index a828e1a4c..39f01276d 100644 --- a/src/apprt.zig +++ b/src/apprt.zig @@ -57,6 +57,9 @@ pub const Runtime = enum { // The Linux default is GTK because it is full featured. if (target.isLinux()) return .gtk; + // Windows we currently only support glfw + if (target.isWindows()) return .glfw; + // Otherwise, we do NONE so we don't create an exe. The GLFW // build is opt-in because it is missing so many features compared // to the other builds that are impossible due to the GLFW interface. From a7fdc1dafa217d7210c8bea968b6a8dfb13c8d01 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sun, 5 Nov 2023 18:17:19 -0800 Subject: [PATCH 16/16] update README with windows notes --- README.md | 23 ++++++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 666e6825f..fb0e9c3d6 100644 --- a/README.md +++ b/README.md @@ -48,7 +48,7 @@ things, but I've been using it full time since April 2022. | ------------------ | ------------------------------------------------------------------------ | -------------------------- | | macOS | [Tip ("Nightly")](https://github.com/mitchellh/ghostty/releases/tag/tip) | MacOS 12+ Universal Binary | | Linux | [Build from Source](#developing-ghostty) | | -| Windows | n/a | Not supported yet | +| Windows | [Build from Source](#developing-ghostty) | [Notes](#windows-notes) | ### Configuration @@ -439,13 +439,30 @@ $ sudo log stream --level debug --predicate 'subsystem=="com.mitchellh.ghostty"' ... ``` +### Windows Notes + +Windows support is still a [work-in-progress](https://github.com/mitchellh/ghostty/issues/437). +The current status is that a bare bones glfw-based build _works_! The experience +with this build is super minimal: there are no native experiences, only a +single window is supported, no tabs, etc. Therefore, the current status is +simply that the core terminal experience works. + +If you want to help with Windows development, please see the +[tracking issue](https://github.com/mitchellh/ghostty/issues/437). We plan +on vastly improving this experience over time. + ### Linting -Ghostty's docs and resources (not including Zig code) are linted using [Prettier](https://prettier.io) with out-of-the-box settings. A Prettier CI check will fail builds with improper formatting. Therefore, if you are modifying anything Prettier will lint, you may want to install it locally and run this from the repo root before you commit: +Ghostty's docs and resources (not including Zig code) are linted using +[Prettier](https://prettier.io) with out-of-the-box settings. A Prettier CI +check will fail builds with improper formatting. Therefore, if you are +modifying anything Prettier will lint, you may want to install it locally and +run this from the repo root before you commit: ``` npm install -g prettier prettier --write . ``` -Or simply install one of the many Prettier extensions out there for your editor of choice. +Or simply install one of the many Prettier extensions out there for your +editor of choice.