-
Notifications
You must be signed in to change notification settings - Fork 572
/
Copy pathExec.zig
1502 lines (1308 loc) · 55.7 KB
/
Exec.zig
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
//! Exec implements the logic for starting and stopping a subprocess with a
//! pty as well as spinning up the necessary read thread to read from the
//! pty and forward it to the Termio instance.
const Exec = @This();
const std = @import("std");
const builtin = @import("builtin");
const assert = std.debug.assert;
const Allocator = std.mem.Allocator;
const ArenaAllocator = std.heap.ArenaAllocator;
const posix = std.posix;
const xev = @import("xev");
const build_config = @import("../build_config.zig");
const configpkg = @import("../config.zig");
const crash = @import("../crash/main.zig");
const fastmem = @import("../fastmem.zig");
const internal_os = @import("../os/main.zig");
const renderer = @import("../renderer.zig");
const shell_integration = @import("shell_integration.zig");
const terminal = @import("../terminal/main.zig");
const termio = @import("../termio.zig");
const Command = @import("../Command.zig");
const SegmentedPool = @import("../datastruct/main.zig").SegmentedPool;
const ptypkg = @import("../pty.zig");
const Pty = ptypkg.Pty;
const EnvMap = std.process.EnvMap;
const windows = internal_os.windows;
const log = std.log.scoped(.io_exec);
/// The termios poll rate in milliseconds.
const TERMIOS_POLL_MS = 200;
/// The subprocess state for our exec backend.
subprocess: Subprocess,
/// Initialize the exec state. This will NOT start it, this only sets
/// up the internal state necessary to start it later.
pub fn init(
alloc: Allocator,
cfg: Config,
) !Exec {
var subprocess = try Subprocess.init(alloc, cfg);
errdefer subprocess.deinit();
return .{ .subprocess = subprocess };
}
pub fn deinit(self: *Exec) void {
self.subprocess.deinit();
}
/// Call to initialize the terminal state as necessary for this backend.
/// This is called before any termio begins. This should not be called
/// after termio begins because it may put the internal terminal state
/// into a bad state.
pub fn initTerminal(self: *Exec, term: *terminal.Terminal) void {
// If we have an initial pwd requested by the subprocess, then we
// set that on the terminal now. This allows rapidly initializing
// new surfaces to use the proper pwd.
if (self.subprocess.cwd) |cwd| term.setPwd(cwd) catch |err| {
log.warn("error setting initial pwd err={}", .{err});
};
// Setup our initial grid/screen size from the terminal. This
// can't fail because the pty should not exist at this point.
self.resize(.{
.columns = term.cols,
.rows = term.rows,
}, .{
.width = term.width_px,
.height = term.height_px,
}) catch unreachable;
}
pub fn threadEnter(
self: *Exec,
alloc: Allocator,
io: *termio.Termio,
td: *termio.Termio.ThreadData,
) !void {
// Start our subprocess
const pty_fds = self.subprocess.start(alloc) catch |err| {
// If we specifically got this error then we are in the forked
// process and our child failed to execute. In that case
if (err != error.Termio) return err;
// Output an error message about the exec faililng and exit.
// This generally should NOT happen because we always wrap
// our command execution either in login (macOS) or /bin/sh
// (Linux) which are usually guaranteed to exist. Still, we
// want to handle this scenario.
execFailedInChild() catch {};
posix.exit(1);
};
errdefer self.subprocess.stop();
// Get the pid from the subprocess
const pid = pid: {
const command = self.subprocess.command orelse return error.ProcessNotStarted;
break :pid command.pid orelse return error.ProcessNoPid;
};
// Track our process start time for abnormal exits
const process_start = try std.time.Instant.now();
// 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 internal_os.pipe();
errdefer posix.close(pipe[0]);
errdefer posix.close(pipe[1]);
// Setup our stream so that we can write.
var stream = xev.Stream.initFd(pty_fds.write);
errdefer stream.deinit();
// Watcher to detect subprocess exit
var process = try xev.Process.init(pid);
errdefer process.deinit();
// Start our timer to read termios state changes. This is used
// to detect things such as when password input is being done
// so we can render the terminal in a different way.
var termios_timer = try xev.Timer.init();
errdefer termios_timer.deinit();
// Start our read thread
const read_thread = try std.Thread.spawn(
.{},
if (builtin.os.tag == .windows) ReadThread.threadMainWindows else ReadThread.threadMainPosix,
.{ pty_fds.read, io, pipe[0] },
);
read_thread.setName("io-reader") catch {};
// Setup our threadata backend state to be our own
td.backend = .{ .exec = .{
.start = process_start,
.abnormal_runtime_threshold_ms = io.config.abnormal_runtime_threshold_ms,
.wait_after_command = io.config.wait_after_command,
.write_stream = stream,
.process = process,
.read_thread = read_thread,
.read_thread_pipe = pipe[1],
.read_thread_fd = pty_fds.read,
.termios_timer = termios_timer,
} };
// Start our process watcher
process.wait(
td.loop,
&td.backend.exec.process_wait_c,
termio.Termio.ThreadData,
td,
processExit,
);
// Start our termios timer. We don't support this on Windows.
// Fundamentally, we could support this on Windows so we're just
// waiting for someone to implement it.
if (comptime builtin.os.tag != .windows) {
termios_timer.run(
td.loop,
&td.backend.exec.termios_timer_c,
TERMIOS_POLL_MS,
termio.Termio.ThreadData,
td,
termiosTimer,
);
}
}
pub fn threadExit(self: *Exec, td: *termio.Termio.ThreadData) void {
assert(td.backend == .exec);
const exec = &td.backend.exec;
if (exec.exited) self.subprocess.externalExit();
self.subprocess.stop();
// Quit our read thread after exiting the subprocess so that
// we don't get stuck waiting for data to stop flowing if it is
// a particularly noisy process.
_ = posix.write(exec.read_thread_pipe, "x") catch |err|
log.warn("error writing to read thread quit pipe err={}", .{err});
if (comptime builtin.os.tag == .windows) {
// Interrupt the blocking read so the thread can see the quit message
if (windows.kernel32.CancelIoEx(exec.read_thread_fd, null) == 0) {
switch (windows.kernel32.GetLastError()) {
.NOT_FOUND => {},
else => |err| log.warn("error interrupting read thread err={}", .{err}),
}
}
}
exec.read_thread.join();
}
pub fn focusGained(
self: *Exec,
td: *termio.Termio.ThreadData,
focused: bool,
) !void {
_ = self;
assert(td.backend == .exec);
const execdata = &td.backend.exec;
if (!focused) {
// Flag the timer to end on the next iteration. This is
// a lot cheaper than doing full timer cancellation.
execdata.termios_timer_running = false;
} else {
// Always set this to true. There is a race condition if we lose
// focus and regain focus before the termios timer ticks where
// if we don't set this unconditionally the timer will end on
// the next iteration.
execdata.termios_timer_running = true;
// If we're focused, we want to start our termios timer. We
// only do this if it isn't already running. We use the termios
// callback because that'll trigger an immediate state check AND
// start the timer.
if (execdata.termios_timer_c.state() != .active) {
_ = termiosTimer(td, undefined, undefined, {});
}
}
}
pub fn resize(
self: *Exec,
grid_size: renderer.GridSize,
screen_size: renderer.ScreenSize,
) !void {
return try self.subprocess.resize(grid_size, screen_size);
}
/// Called when the child process exited abnormally but before the surface
/// is notified.
pub fn childExitedAbnormally(
self: *Exec,
gpa: Allocator,
t: *terminal.Terminal,
exit_code: u32,
runtime_ms: u64,
) !void {
var arena = ArenaAllocator.init(gpa);
defer arena.deinit();
const alloc = arena.allocator();
// Build up our command for the error message
const command = try std.mem.join(alloc, " ", self.subprocess.args);
const runtime_str = try std.fmt.allocPrint(alloc, "{d} ms", .{runtime_ms});
// No matter what move the cursor back to the column 0.
t.carriageReturn();
// Reset styles
try t.setAttribute(.{ .unset = {} });
// If there is data in the viewport, we want to scroll down
// a little bit and write a horizontal rule before writing
// our message. This lets the use see the error message the
// command may have output.
const viewport_str = try t.plainString(alloc);
if (viewport_str.len > 0) {
try t.linefeed();
for (0..t.cols) |_| try t.print(0x2501);
t.carriageReturn();
try t.linefeed();
try t.linefeed();
}
// Output our error message
try t.setAttribute(.{ .@"8_fg" = .bright_red });
try t.setAttribute(.{ .bold = {} });
try t.printString("Ghostty failed to launch the requested command:");
try t.setAttribute(.{ .unset = {} });
t.carriageReturn();
try t.linefeed();
try t.linefeed();
try t.printString(command);
try t.setAttribute(.{ .unset = {} });
t.carriageReturn();
try t.linefeed();
try t.linefeed();
try t.printString("Runtime: ");
try t.setAttribute(.{ .@"8_fg" = .red });
try t.printString(runtime_str);
try t.setAttribute(.{ .unset = {} });
// We don't print this on macOS because the exit code is always 0
// due to the way we launch the process.
if (comptime !builtin.target.isDarwin()) {
const exit_code_str = try std.fmt.allocPrint(alloc, "{d}", .{exit_code});
t.carriageReturn();
try t.linefeed();
try t.printString("Exit Code: ");
try t.setAttribute(.{ .@"8_fg" = .red });
try t.printString(exit_code_str);
try t.setAttribute(.{ .unset = {} });
}
t.carriageReturn();
try t.linefeed();
try t.linefeed();
try t.printString("Press any key to close the window.");
// Hide the cursor
t.modes.set(.cursor_visible, false);
}
/// This outputs an error message when exec failed and we are the
/// child process. This returns so the caller should probably exit
/// after calling this.
///
/// Note that this usually is only called under very very rare
/// circumstances because we wrap our command execution in login
/// (macOS) or /bin/sh (Linux). So this output can be pretty crude
/// because it should never happen. Notably, this is not the error
/// users see when `command` is invalid.
fn execFailedInChild() !void {
const stderr = std.io.getStdErr().writer();
try stderr.writeAll("exec failed\n");
try stderr.writeAll("press any key to exit\n");
var buf: [1]u8 = undefined;
var reader = std.io.getStdIn().reader();
_ = try reader.read(&buf);
}
fn processExit(
td_: ?*termio.Termio.ThreadData,
_: *xev.Loop,
_: *xev.Completion,
r: xev.Process.WaitError!u32,
) xev.CallbackAction {
const exit_code = r catch unreachable;
const td = td_.?;
assert(td.backend == .exec);
const execdata = &td.backend.exec;
execdata.exited = true;
// Determine how long the process was running for.
const runtime_ms: ?u64 = runtime: {
const process_end = std.time.Instant.now() catch break :runtime null;
const runtime_ns = process_end.since(execdata.start);
const runtime_ms = runtime_ns / std.time.ns_per_ms;
break :runtime runtime_ms;
};
log.debug(
"child process exited status={} runtime={}ms",
.{ exit_code, runtime_ms orelse 0 },
);
// If our runtime was below some threshold then we assume that this
// was an abnormal exit and we show an error message.
if (runtime_ms) |runtime| runtime: {
// On macOS, our exit code detection doesn't work, possibly
// because of our `login` wrapper. More investigation required.
if (comptime !builtin.target.isDarwin()) {
// If our exit code is zero, then the command was successful
// and we don't ever consider it abnormal.
if (exit_code == 0) break :runtime;
}
// Our runtime always has to be under the threshold to be
// considered abnormal. This is because a user can always
// manually do something like `exit 1` in their shell to
// force the exit code to be non-zero. We only want to detect
// abnormal exits that happen so quickly the user can't react.
if (runtime > execdata.abnormal_runtime_threshold_ms) break :runtime;
log.warn("abnormal process exit detected, showing error message", .{});
// Notify our main writer thread which has access to more
// information so it can show a better error message.
td.mailbox.send(.{
.child_exited_abnormally = .{
.exit_code = exit_code,
.runtime_ms = runtime,
},
}, null);
td.mailbox.notify();
return .disarm;
}
// If we're purposely waiting then we just return since the process
// exited flag is set to true. This allows the terminal window to remain
// open.
if (execdata.wait_after_command) {
// We output a message so that the user knows whats going on and
// doesn't think their terminal just froze.
terminal: {
td.renderer_state.mutex.lock();
defer td.renderer_state.mutex.unlock();
const t = td.renderer_state.terminal;
t.carriageReturn();
t.linefeed() catch break :terminal;
t.printString("Process exited. Press any key to close the terminal.") catch
break :terminal;
t.modes.set(.cursor_visible, false);
}
return .disarm;
}
// Notify our surface we want to close
_ = td.surface_mailbox.push(.{
.child_exited = {},
}, .{ .forever = {} });
return .disarm;
}
fn termiosTimer(
td_: ?*termio.Termio.ThreadData,
_: *xev.Loop,
_: *xev.Completion,
r: xev.Timer.RunError!void,
) xev.CallbackAction {
// log.debug("termios timer fired", .{});
// This should never happen because we guard starting our
// timer on windows but we want this assertion to fire if
// we ever do start the timer on windows.
// TODO: support on windows
if (comptime builtin.os.tag == .windows) {
@panic("termios timer not implemented on Windows");
}
_ = r catch |err| switch (err) {
// This is sent when our timer is canceled. That's fine.
error.Canceled => return .disarm,
else => {
log.warn("error in termios timer callback err={}", .{err});
@panic("crash in termios timer callback");
},
};
const td = td_.?;
assert(td.backend == .exec);
const exec = &td.backend.exec;
// This is kind of hacky but we rebuild a Pty struct to get the
// termios data.
const mode: ptypkg.Mode = (Pty{
.master = exec.read_thread_fd,
.slave = undefined,
}).getMode() catch |err| err: {
log.warn("error getting termios mode err={}", .{err});
// If we have an error we return the default mode values
// which are the likely values.
break :err .{};
};
// If the mode changed, then we process it.
if (!std.meta.eql(mode, exec.termios_mode)) mode_change: {
log.debug("termios change mode={}", .{mode});
exec.termios_mode = mode;
// We assume we're in some sort of password input if we're
// in canonical mode and not echoing. This is a heuristic.
const password_input = mode.canonical and !mode.echo;
// If our password input state changed on the terminal then
// we notify the surface.
{
td.renderer_state.mutex.lock();
defer td.renderer_state.mutex.unlock();
const t = td.renderer_state.terminal;
if (t.flags.password_input == password_input) {
break :mode_change;
}
}
// We have to notify the surface that we're in password input.
// We must block on this because the balanced true/false state
// of this is critical to apprt behavior.
_ = td.surface_mailbox.push(.{
.password_input = password_input,
}, .{ .forever = {} });
}
// Repeat the timer
if (exec.termios_timer_running) {
exec.termios_timer.run(
td.loop,
&exec.termios_timer_c,
TERMIOS_POLL_MS,
termio.Termio.ThreadData,
td,
termiosTimer,
);
}
return .disarm;
}
pub fn queueWrite(
self: *Exec,
alloc: Allocator,
td: *termio.Termio.ThreadData,
data: []const u8,
linefeed: bool,
) !void {
_ = self;
const exec = &td.backend.exec;
// If our process is exited then we send our surface a message
// about it but we don't queue any more writes.
if (exec.exited) {
_ = td.surface_mailbox.push(.{
.child_exited = {},
}, .{ .forever = {} });
return;
}
// We go through and chunk the data if necessary to fit into
// our cached buffers that we can queue to the stream.
var i: usize = 0;
while (i < data.len) {
const req = try exec.write_req_pool.getGrow(alloc);
const buf = try exec.write_buf_pool.getGrow(alloc);
const slice = slice: {
// The maximum end index is either the end of our data or
// the end of our buffer, whichever is smaller.
const max = @min(data.len, i + buf.len);
// Fast
if (!linefeed) {
fastmem.copy(u8, buf, data[i..max]);
const len = max - i;
i = max;
break :slice buf[0..len];
}
// Slow, have to replace \r with \r\n
var buf_i: usize = 0;
while (i < data.len and buf_i < buf.len - 1) {
const ch = data[i];
i += 1;
if (ch != '\r') {
buf[buf_i] = ch;
buf_i += 1;
continue;
}
// CRLF
buf[buf_i] = '\r';
buf[buf_i + 1] = '\n';
buf_i += 2;
}
break :slice buf[0..buf_i];
};
//for (slice) |b| log.warn("write: {x}", .{b});
exec.write_stream.queueWrite(
td.loop,
&exec.write_queue,
req,
.{ .slice = slice },
termio.Exec.ThreadData,
exec,
ttyWrite,
);
}
}
fn ttyWrite(
td_: ?*ThreadData,
_: *xev.Loop,
_: *xev.Completion,
_: xev.Stream,
_: xev.WriteBuffer,
r: xev.Stream.WriteError!usize,
) xev.CallbackAction {
const td = td_.?;
td.write_req_pool.put();
td.write_buf_pool.put();
const d = r catch |err| {
log.err("write error: {}", .{err});
return .disarm;
};
_ = d;
//log.info("WROTE: {d}", .{d});
return .disarm;
}
/// The thread local data for the exec implementation.
pub const ThreadData = struct {
// The preallocation size for the write request pool. This should be big
// enough to satisfy most write requests. It must be a power of 2.
const WRITE_REQ_PREALLOC = std.math.pow(usize, 2, 5);
/// Process start time and boolean of whether its already exited.
start: std.time.Instant,
exited: bool = false,
/// The number of milliseconds below which we consider a process
/// exit to be abnormal. This is used to show an error message
/// when the process exits too quickly.
abnormal_runtime_threshold_ms: u32,
/// If true, do not immediately send a child exited message to the
/// surface to close the surface when the command exits. If this is
/// false we'll show a process exited message and wait for user input
/// to close the surface.
wait_after_command: bool,
/// The data stream is the main IO for the pty.
write_stream: xev.Stream,
/// The process watcher
process: xev.Process,
/// This is the pool of available (unused) write requests. If you grab
/// one from the pool, you must put it back when you're done!
write_req_pool: SegmentedPool(xev.Stream.WriteRequest, WRITE_REQ_PREALLOC) = .{},
/// The pool of available buffers for writing to the pty.
write_buf_pool: SegmentedPool([64]u8, WRITE_REQ_PREALLOC) = .{},
/// The write queue for the data stream.
write_queue: xev.Stream.WriteQueue = .{},
/// This is used for both waiting for the process to exit and then
/// subsequently to wait for the data_stream to close.
process_wait_c: xev.Completion = .{},
/// Reader thread state
read_thread: std.Thread,
read_thread_pipe: posix.fd_t,
read_thread_fd: posix.fd_t,
/// The timer to detect termios state changes.
termios_timer: xev.Timer,
termios_timer_c: xev.Completion = .{},
termios_timer_running: bool = true,
/// The last known termios mode. Used for change detection
/// to prevent unnecessary locking of expensive mutexes.
termios_mode: ptypkg.Mode = .{},
pub fn deinit(self: *ThreadData, alloc: Allocator) void {
posix.close(self.read_thread_pipe);
// Clear our write pools. We know we aren't ever going to do
// any more IO since we stop our data stream below so we can just
// drop this.
self.write_req_pool.deinit(alloc);
self.write_buf_pool.deinit(alloc);
// Stop our process watcher
self.process.deinit();
// Stop our write stream
self.write_stream.deinit();
// Stop our termios timer
self.termios_timer.deinit();
}
};
pub const Config = struct {
command: ?[]const u8 = null,
shell_integration: configpkg.Config.ShellIntegration = .detect,
shell_integration_features: configpkg.Config.ShellIntegrationFeatures = .{},
working_directory: ?[]const u8 = null,
resources_dir: ?[]const u8,
term: []const u8,
linux_cgroup: Command.LinuxCgroup = Command.linux_cgroup_default,
};
const Subprocess = struct {
/// If we build with flatpak support then we have to keep track of
/// a potential execution on the host.
const FlatpakHostCommand = if (build_config.flatpak) internal_os.FlatpakHostCommand else void;
const c = @cImport({
@cInclude("errno.h");
@cInclude("signal.h");
@cInclude("unistd.h");
});
arena: std.heap.ArenaAllocator,
cwd: ?[]const u8,
env: EnvMap,
args: [][]const u8,
grid_size: renderer.GridSize,
screen_size: renderer.ScreenSize,
pty: ?Pty = null,
command: ?Command = null,
flatpak_command: ?FlatpakHostCommand = null,
linux_cgroup: Command.LinuxCgroup = Command.linux_cgroup_default,
/// Initialize the subprocess. This will NOT start it, this only sets
/// up the internal state necessary to start it later.
pub fn init(gpa: Allocator, cfg: Config) !Subprocess {
// We have a lot of maybe-allocations that all share the same lifetime
// so use an arena so we don't end up in an accounting nightmare.
var arena = std.heap.ArenaAllocator.init(gpa);
errdefer arena.deinit();
const alloc = arena.allocator();
// Set our env vars. For Flatpak builds running in Flatpak we don't
// inherit our environment because the login shell on the host side
// will get it.
var env = env: {
if (comptime build_config.flatpak) {
if (internal_os.isFlatpak()) {
break :env std.process.EnvMap.init(alloc);
}
}
break :env try std.process.getEnvMap(alloc);
};
errdefer env.deinit();
// If we have a resources dir then set our env var
if (cfg.resources_dir) |dir| {
log.info("found Ghostty resources dir: {s}", .{dir});
try env.put("GHOSTTY_RESOURCES_DIR", dir);
}
// Set our TERM var. This is a bit complicated because we want to use
// the ghostty TERM value but we want to only do that if we have
// ghostty in the TERMINFO database.
//
// For now, we just look up a bundled dir but in the future we should
// also load the terminfo database and look for it.
if (cfg.resources_dir) |base| {
try env.put("TERM", cfg.term);
try env.put("COLORTERM", "truecolor");
// Assume that the resources directory is adjacent to the terminfo
// database
var buf: [std.fs.max_path_bytes]u8 = undefined;
const dir = try std.fmt.bufPrint(&buf, "{s}/terminfo", .{
std.fs.path.dirname(base) orelse unreachable,
});
try env.put("TERMINFO", dir);
} else {
if (comptime builtin.target.isDarwin()) {
log.warn("ghostty terminfo not found, using xterm-256color", .{});
log.warn("the terminfo SHOULD exist on macos, please ensure", .{});
log.warn("you're using a valid app bundle.", .{});
}
try env.put("TERM", "xterm-256color");
try env.put("COLORTERM", "truecolor");
}
// Add our binary to the path if we can find it.
ghostty_path: {
var exe_buf: [std.fs.max_path_bytes]u8 = undefined;
const exe_bin_path = std.fs.selfExePath(&exe_buf) catch |err| {
log.warn("failed to get ghostty exe path err={}", .{err});
break :ghostty_path;
};
const exe_dir = std.fs.path.dirname(exe_bin_path) orelse break :ghostty_path;
log.debug("appending ghostty bin to path dir={s}", .{exe_dir});
// We always set this so that if the shell overwrites the path
// scripts still have a way to find the Ghostty binary when
// running in Ghostty.
try env.put("GHOSTTY_BIN_DIR", exe_dir);
// Append if we have a path. We want to append so that ghostty is
// the last priority in the path. If we don't have a path set
// then we just set it to the directory of the binary.
if (env.get("PATH")) |path| {
// Verify that our path doesn't already contain this entry
var it = std.mem.tokenizeScalar(u8, path, std.fs.path.delimiter);
while (it.next()) |entry| {
if (std.mem.eql(u8, entry, exe_dir)) break :ghostty_path;
}
try env.put(
"PATH",
try internal_os.appendEnv(alloc, path, exe_dir),
);
} else {
try env.put("PATH", exe_dir);
}
}
// On macOS, export additional data directories from our
// application bundle.
if (comptime builtin.target.isDarwin()) darwin: {
const resources_dir = cfg.resources_dir orelse break :darwin;
var buf: [std.fs.max_path_bytes]u8 = undefined;
const xdg_data_dir_key = "XDG_DATA_DIRS";
if (std.fmt.bufPrint(&buf, "{s}/..", .{resources_dir})) |data_dir| {
try env.put(
xdg_data_dir_key,
try internal_os.appendEnv(
alloc,
env.get(xdg_data_dir_key) orelse "/usr/local/share:/usr/share",
data_dir,
),
);
} else |err| {
log.warn("error building {s}; err={}", .{ xdg_data_dir_key, err });
}
const manpath_key = "MANPATH";
if (std.fmt.bufPrint(&buf, "{s}/../man", .{resources_dir})) |man_dir| {
// Always append with colon in front, as it mean that if
// `MANPATH` is empty, then it should be treated as an extra
// path instead of overriding all paths set by OS.
try env.put(
manpath_key,
try internal_os.appendEnvAlways(
alloc,
env.get(manpath_key) orelse "",
man_dir,
),
);
} else |err| {
log.warn("error building {s}; man pages may not be available; err={}", .{ manpath_key, err });
}
}
// Set environment variables used by some programs (such as neovim) to detect
// which terminal emulator and version they're running under.
try env.put("TERM_PROGRAM", "ghostty");
try env.put("TERM_PROGRAM_VERSION", build_config.version_string);
// When embedding in macOS and running via XCode, XCode injects
// a bunch of things that break our shell process. We remove those.
if (comptime builtin.target.isDarwin() and build_config.artifact == .lib) {
if (env.get("__XCODE_BUILT_PRODUCTS_DIR_PATHS") != null) {
env.remove("__XCODE_BUILT_PRODUCTS_DIR_PATHS");
env.remove("__XPC_DYLD_LIBRARY_PATH");
env.remove("DYLD_FRAMEWORK_PATH");
env.remove("DYLD_INSERT_LIBRARIES");
env.remove("DYLD_LIBRARY_PATH");
env.remove("LD_LIBRARY_PATH");
env.remove("SECURITYSESSIONID");
env.remove("XPC_SERVICE_NAME");
}
// Remove this so that running `ghostty` within Ghostty works.
env.remove("GHOSTTY_MAC_APP");
}
// VTE_VERSION is set by gnome-terminal and other VTE-based terminals.
// We don't want our child processes to think we're running under VTE.
env.remove("VTE_VERSION");
// Don't leak these GTK environment variables to child processes.
if (comptime build_config.app_runtime == .gtk) {
env.remove("GDK_DEBUG");
env.remove("GDK_DISABLE");
env.remove("GSK_RENDERER");
}
// Setup our shell integration, if we can.
const integrated_shell: ?shell_integration.Shell, const shell_command: []const u8 = shell: {
const default_shell_command = cfg.command orelse switch (builtin.os.tag) {
.windows => "cmd.exe",
else => "sh",
};
const force: ?shell_integration.Shell = switch (cfg.shell_integration) {
.none => break :shell .{ null, default_shell_command },
.detect => null,
.bash => .bash,
.elvish => .elvish,
.fish => .fish,
.zsh => .zsh,
};
const dir = cfg.resources_dir orelse break :shell .{
null,
default_shell_command,
};
const integration = try shell_integration.setup(
alloc,
dir,
default_shell_command,
&env,
force,
cfg.shell_integration_features,
) orelse break :shell .{ null, default_shell_command };
break :shell .{ integration.shell, integration.command };
};
if (integrated_shell) |shell| {
log.info(
"shell integration automatically injected shell={}",
.{shell},
);
} else if (cfg.shell_integration != .none) {
log.warn("shell could not be detected, no automatic shell integration will be injected", .{});
}
// Build our args list
const args = args: {
const cap = 9; // the most we'll ever use
var args = try std.ArrayList([]const u8).initCapacity(alloc, cap);
defer args.deinit();
// If we're on macOS, we have to use `login(1)` to get all of
// the proper environment variables set, a login shell, and proper
// hushlogin behavior.
if (comptime builtin.target.isDarwin()) darwin: {
const passwd = internal_os.passwd.get(alloc) catch |err| {
log.warn("failed to read passwd, not using a login shell err={}", .{err});
break :darwin;
};
const username = passwd.name orelse {
log.warn("failed to get username, not using a login shell", .{});
break :darwin;
};
const hush = if (passwd.home) |home| hush: {
var dir = std.fs.openDirAbsolute(home, .{}) catch |err| {
log.warn(
"failed to open home dir, not checking for hushlogin err={}",
.{err},
);
break :hush false;
};
defer dir.close();
break :hush if (dir.access(".hushlogin", .{})) true else |_| false;
} else false;
const cmd = try std.fmt.allocPrint(
alloc,
"exec -l {s}",
.{shell_command},
);
// The reason for executing login this way is unclear. This
// comment will attempt to explain but prepare for a truly
// unhinged reality.
//
// The first major issue is that on macOS, a lot of users
// put shell configurations in ~/.bash_profile instead of
// ~/.bashrc (or equivalent for another shell). This file is only
// loaded for a login shell so macOS users expect all their terminals
// to be login shells. No other platform behaves this way and its
// totally braindead but somehow the entire dev community on
// macOS has cargo culted their way to this reality so we have to
// do it...
//
// To get a login shell, you COULD just prepend argv0 with a `-`
// but that doesn't fully work because `getlogin()` C API will
// return the wrong value, SHELL won't be set, and various
// other login behaviors that macOS users expect.
//
// The proper way is to use `login(1)`. But login(1) forces
// the working directory to change to the home directory,
// which we may not want. If we specify "-l" then we can avoid
// this behavior but now the shell isn't a login shell.
//
// There is another issue: `login(1)` only checks for ".hushlogin"
// in the working directory. This means that if we specify "-l"
// then we won't get hushlogin honored if its in the home
// directory (which is standard). To get around this, we
// check for hushlogin ourselves and if present specify the
// "-q" flag to login(1).
//
// So to get all the behaviors we want, we specify "-l" but
// execute "bash" (which is built-in to macOS). We then use
// the bash builtin "exec" to replace the process with a login
// shell ("-l" on exec) with the command we really want.
//
// We use "bash" instead of other shells that ship with macOS
// because as of macOS Sonoma, we found with a microbenchmark
// that bash can `exec` into the desired command ~2x faster
// than zsh.
//
// To figure out a lot of this logic I read the login.c
// source code in the OSS distribution Apple provides for
// macOS.
//
// Awesome.
try args.append("/usr/bin/login");
if (hush) try args.append("-q");
try args.append("-flp");
// We execute bash with "--noprofile --norc" so that it doesn't