mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-08-02 14:57:31 +03:00
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
This commit is contained in:

committed by
Mitchell Hashimoto

parent
04ef21653f
commit
232df8de8f
@ -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",
|
||||
|
@ -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,
|
||||
|
324
src/Command.zig
324
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);
|
||||
}
|
||||
}
|
||||
|
314
src/Pty.zig
314
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
|
||||
|
@ -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();
|
||||
}
|
||||
|
||||
|
@ -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", .{});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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 = .{
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
|
100
src/windows.zig
Normal file
100
src/windows.zig
Normal file
@ -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);
|
||||
};
|
Reference in New Issue
Block a user