mirror of
https://github.com/ghostty-org/ghostty.git
synced 2026-09-14 18:01:58 +00:00
Sentry initialization already ran on a separate thread, but the cache and state directory resolution happened on the main thread before spawning it. On macOS the cache dir resolution calls NSFileManager URLForDirectory:inDomain:appropriateForURL:create:error: which takes multiple milliseconds and was the single largest cost in global.init. All directory resolution now happens on the init thread. before: 2967us-4018us after: 30us-70us (env map snapshot + thread spawn) global.init total drops from ~3.4-5.0ms to ~0.4-1.0ms.
447 lines
16 KiB
Zig
447 lines
16 KiB
Zig
const std = @import("std");
|
|
const builtin = @import("builtin");
|
|
const build_config = @import("build_config.zig");
|
|
const build_options = @import("build_options");
|
|
const cli = @import("cli.zig");
|
|
const internal_os = @import("os/main.zig");
|
|
const fontconfig = @import("fontconfig");
|
|
const glslang = @import("glslang");
|
|
const harfbuzz = @import("harfbuzz");
|
|
const oni = @import("oniguruma");
|
|
const crash = @import("crash/main.zig");
|
|
const renderer = @import("renderer.zig");
|
|
const apprt = @import("apprt.zig");
|
|
const assert = @import("quirks.zig").inlineAssert;
|
|
const allocTmpDir = @import("os/file.zig").allocTmpDir;
|
|
const freeTmpDir = @import("os/file.zig").freeTmpDir;
|
|
|
|
// This file should only be imported for certain platforms.
|
|
comptime {
|
|
switch (@import("terminal_options").artifact) {
|
|
.ghostty => {},
|
|
// This file is not allowed to be included in libghostty-vt
|
|
.lib => @compileError("global state cannot be used in libghostty-vt"),
|
|
}
|
|
}
|
|
|
|
/// We export the xev backend we want to use so that the rest of
|
|
/// Ghostty can import this once and have access to the proper
|
|
/// backend.
|
|
pub const xev = @import("xev").Dynamic;
|
|
|
|
/// Global process state. This is initialized in main() for exe artifacts and
|
|
/// by ghostty_init() for lib artifacts. Most other methods in this file will
|
|
/// retrieve items stored in this state.
|
|
var state: ?GlobalState = null;
|
|
|
|
pub const InitOpts = union(enum) {
|
|
main: std.process.Init.Minimal,
|
|
|
|
/// Same as `main` but for auxiliary tool binaries (e.g. ghostty-bench
|
|
/// and ghostty-gen) that have their own CLI action namespace. This
|
|
/// skips detection of ghostty app CLI actions, since tool actions
|
|
/// (e.g. `+terminal-stream`) are not valid app actions and would
|
|
/// otherwise cause init to fail with InvalidAction.
|
|
tool: std.process.Init.Minimal,
|
|
|
|
c: struct {
|
|
argc: usize,
|
|
argv: [*][*:0]u8,
|
|
environ: std.process.Environ,
|
|
},
|
|
};
|
|
|
|
/// Initialize the global state.
|
|
pub fn init(opts: InitOpts) !void {
|
|
// Initialize ourself to nothing so we don't have any extra state.
|
|
// IMPORTANT: this MUST be initialized before any log output because
|
|
// the log function uses the global state.
|
|
state = .{
|
|
.io_impl = undefined,
|
|
.gpa = null,
|
|
.alloc = undefined,
|
|
.environ = switch (opts) {
|
|
.main, .tool => |m| m.environ,
|
|
.c => |c| c.environ,
|
|
},
|
|
.args = switch (opts) {
|
|
.main, .tool => |m| m.args,
|
|
// TODO: Using the C API from Windows is unsupported at this time.
|
|
//
|
|
// When do we plan on supporting Windows, it's recommended to
|
|
// ensure that the C API can take a UNICODE_STRING (aka []16, a
|
|
// WTF-16 string) so that it can just be passed into
|
|
// std.process.Args.Vector directly.
|
|
.c => |c| .{ .vector = if (builtin.os.tag == .windows)
|
|
return error.UnsupportedOSForCApi
|
|
else
|
|
c.argv[0..c.argc] },
|
|
},
|
|
.tmp_dir_path = null,
|
|
.action = null,
|
|
.logging = .{},
|
|
.rlimits = .{},
|
|
.resources_dir = .{},
|
|
};
|
|
const self = &state.?;
|
|
errdefer deinit();
|
|
|
|
self.gpa = gpa: {
|
|
// Use the libc allocator if it is available because it is WAY
|
|
// faster than GPA. We only do this in release modes so that we
|
|
// can get easy memory leak detection in debug modes.
|
|
if (builtin.link_libc) {
|
|
if (switch (builtin.mode) {
|
|
.ReleaseSafe, .ReleaseFast => true,
|
|
|
|
// We also use it if we can detect we're running under
|
|
// Valgrind since Valgrind only instruments the C allocator
|
|
else => std.valgrind.runningOnValgrind() > 0,
|
|
}) break :gpa null;
|
|
}
|
|
|
|
break :gpa .init;
|
|
};
|
|
|
|
self.alloc = if (self.gpa) |*value|
|
|
value.allocator()
|
|
else if (builtin.link_libc)
|
|
std.heap.c_allocator
|
|
else
|
|
unreachable;
|
|
|
|
// Set up our main I/O implementation (fully threaded w/allocator). Note
|
|
// that we cannot use any implementation supplied from main at this point,
|
|
// because there are some later initialization steps that depend on us
|
|
// mutating the environment, and thus it needs to be re-synced farther
|
|
// down. For that, we need a stable implementation that allows us to do so.
|
|
self.io_impl = .init(self.alloc, .{
|
|
.argv0 = .init(self.args),
|
|
.environ = self.environ,
|
|
});
|
|
|
|
// Discover and save the temporary directory path
|
|
self.tmp_dir_path = try allocTmpDir(self.alloc, self.environ);
|
|
|
|
// We first try to parse any action that we may be executing.
|
|
// Tool binaries (ghostty-bench, ghostty-gen) have their own action
|
|
// namespace and detect their own actions, so we skip detection here.
|
|
self.action = switch (opts) {
|
|
.main, .c => try cli.action.detectArgs(
|
|
cli.ghostty.Action,
|
|
self.alloc,
|
|
self.args,
|
|
),
|
|
.tool => null,
|
|
};
|
|
|
|
// If we have an action executing, we disable logging by default
|
|
// since we write to stderr we don't want logs messing up our
|
|
// output.
|
|
if (self.action != null) self.logging.stderr = false;
|
|
|
|
// I don't love the env var name but I don't have it in my heart
|
|
// to parse CLI args 3 times (once for actions, once for config,
|
|
// 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.
|
|
logging: {
|
|
const v = self.environ.getAlloc(self.alloc, "GHOSTTY_LOG") catch |err| switch (err) {
|
|
error.EnvironmentVariableMissing => break :logging,
|
|
else => return err,
|
|
};
|
|
defer self.alloc.free(v);
|
|
self.logging = cli.args.parsePackedStruct(GlobalState.Logging, v) catch .{};
|
|
}
|
|
|
|
// Setup our signal handlers before logging
|
|
GlobalState.initSignals();
|
|
|
|
// Setup our Xev backend if we're dynamic
|
|
if (comptime xev.dynamic) xev.detect() catch |err| {
|
|
std.log.warn("failed to detect xev backend, falling back to " ++
|
|
"most compatible backend err={}", .{err});
|
|
};
|
|
|
|
// Output some debug information right away
|
|
std.log.info("ghostty version={s}", .{build_config.version_string});
|
|
std.log.info("ghostty build optimize={s}", .{build_config.mode_string});
|
|
std.log.info("runtime={}", .{build_config.app_runtime});
|
|
std.log.info("font_backend={}", .{build_config.font_backend});
|
|
if (comptime build_config.font_backend.hasHarfbuzz()) {
|
|
std.log.info("dependency harfbuzz={s}", .{harfbuzz.versionString()});
|
|
}
|
|
if (comptime build_config.font_backend.hasFontconfig()) {
|
|
std.log.info("dependency fontconfig={d}", .{fontconfig.version()});
|
|
}
|
|
std.log.info("renderer={}", .{renderer.Renderer});
|
|
std.log.info("libxev default backend={t}", .{xev.backend});
|
|
|
|
// As early as possible, initialize our resource limits.
|
|
self.rlimits = .init();
|
|
|
|
if (build_options.sentry) {
|
|
// Initialize our crash reporting. The environ map snapshot is
|
|
// owned by crash.init (it is freed by the init thread).
|
|
const environ_map = try self.environ.createMap(self.alloc);
|
|
crash.init(self.alloc, environ_map) catch |err| {
|
|
std.log.warn(
|
|
"sentry init failed, no crash capture available err={}",
|
|
.{err},
|
|
);
|
|
};
|
|
}
|
|
|
|
// const sentrylib = @import("sentry");
|
|
// if (sentrylib.captureEvent(sentrylib.Value.initMessageEvent(
|
|
// .info,
|
|
// null,
|
|
// "hello, world",
|
|
// ))) |uuid| {
|
|
// std.log.warn("uuid={s}", .{uuid.string()});
|
|
// } else std.log.warn("failed to capture event", .{});
|
|
|
|
// We need to make sure the process locale is set properly. Locale
|
|
// affects a lot of behaviors in a shell.
|
|
//
|
|
// We need to re-sync the environment after this completes.
|
|
try internal_os.ensureLocale();
|
|
syncEnviron();
|
|
|
|
// Initialize glslang for shader compilation
|
|
try glslang.init();
|
|
|
|
// Initialize oniguruma for regex
|
|
try oni.init(&.{oni.Encoding.utf8});
|
|
|
|
// Find our resources directory once for the app so every launch
|
|
// hereafter can use this cached value.
|
|
self.resources_dir = try apprt.runtime.resourcesDir(self.alloc);
|
|
errdefer self.resources_dir.deinit(self.alloc);
|
|
|
|
// Setup i18n
|
|
if (self.resources_dir.app()) |v| internal_os.i18n.init(v) catch |err| {
|
|
std.log.warn("failed to init i18n, translations will not be available err={}", .{err});
|
|
};
|
|
}
|
|
|
|
/// Cleans up the global state. This doesn't _need_ to be called but
|
|
/// doing so in dev modes will check for memory leaks.
|
|
///
|
|
/// Asserts that the state exists.
|
|
pub fn deinit() void {
|
|
const self = &state.?;
|
|
|
|
self.resources_dir.deinit(self.alloc);
|
|
|
|
// Flush our crash logs
|
|
crash.deinit();
|
|
|
|
// Release our tmp_dir_path if needed
|
|
if (self.tmp_dir_path) |td| freeTmpDir(self.alloc, td);
|
|
|
|
// Release our I/O instance
|
|
self.io_impl.deinit();
|
|
|
|
if (self.gpa) |*value| {
|
|
// We want to ensure that we deinit the GPA because this is
|
|
// the point at which it will output if there were safety violations.
|
|
_ = value.deinit();
|
|
}
|
|
}
|
|
|
|
/// Helper to return either the state's I/O instance, or one from testing.
|
|
///
|
|
/// Asserts that the global state is initialized when not running as as test.
|
|
pub fn io() std.Io {
|
|
if (builtin.is_test) return std.testing.io;
|
|
|
|
return state.?.io();
|
|
}
|
|
|
|
/// Helper to return either the state's I/O instance, or one from testing.
|
|
///
|
|
/// Asserts that the global state is initialized when not running as as test.
|
|
pub fn alloc() std.mem.Allocator {
|
|
if (builtin.is_test) return std.testing.allocator;
|
|
|
|
return state.?.alloc;
|
|
}
|
|
|
|
/// Helper to return either the state's environment, or one from testing.
|
|
///
|
|
/// Asserts that the global state is initialized when not running as a test.
|
|
pub fn environ() std.process.Environ {
|
|
if (builtin.is_test) return std.testing.environ;
|
|
|
|
return state.?.environ;
|
|
}
|
|
|
|
/// Helper to create an environment map off of the state's environment, or one
|
|
/// from testing. The map is created off of the state allocator.
|
|
///
|
|
/// Asserts that the global state is initialized when not running as a test.
|
|
pub fn environMap() !std.process.Environ.Map {
|
|
if (builtin.is_test) return std.testing.environ.createMap(std.testing.allocator);
|
|
|
|
return state.?.environ.createMap(state.?.alloc);
|
|
}
|
|
|
|
/// Re-synchronizes the global Environ (both the higher-level and I/O versions)
|
|
/// from the process. No-op on Windows, asserts libc and an initialized global
|
|
/// state on everything else.
|
|
///
|
|
/// It is not valid to run this within any code that needs to be run through
|
|
/// tests. For any of these, re-factor the code to take an environment map
|
|
/// instead, where you can modify the environment as needed.
|
|
///
|
|
/// NOTE: Be cognizant of where you are calling this! While the only real
|
|
/// difference between the POSIX environment and higher-level Zig `PosixBlock`
|
|
/// struct is that the latter has a length versus the former's many-item
|
|
/// pointer state, there is no concurrency control on this function (or for
|
|
/// that matter, `environ` or `environMap`. Direct modification of the system
|
|
/// environment is becoming more discouraged in the standard library as well,
|
|
/// and this should be kept in mind when resorting to lower-level `setenv` or
|
|
/// `unsetenv` - as a rule, beyond initialization, favor
|
|
/// `std.process.Environ.Map` whenever possible.
|
|
pub fn syncEnviron() void {
|
|
switch (builtin.os.tag) {
|
|
.windows => {},
|
|
else => {
|
|
assert(builtin.link_libc);
|
|
assert(!builtin.is_test);
|
|
const new_environ: std.process.Environ = .{ .block = .{ .slice = std.c.environ[0..env_len: {
|
|
var len: usize = 0;
|
|
while (std.c.environ[len]) |_| : (len += 1) {}
|
|
break :env_len len;
|
|
} :null] } };
|
|
state.?.environ = new_environ;
|
|
state.?.io_impl.environ = .{ .process_environ = new_environ };
|
|
},
|
|
}
|
|
}
|
|
|
|
/// Helper to return either the state's args, or one from testing.
|
|
///
|
|
/// Asserts that the global state is initialized when not running as a test.
|
|
pub fn args() std.process.Args {
|
|
if (builtin.is_test) return .{ .vector = &.{} };
|
|
|
|
return state.?.args;
|
|
}
|
|
|
|
/// Returns the temporary directory discovered from the global environment
|
|
/// (saves allocation of the temporary directory where you can get at global
|
|
/// state).
|
|
///
|
|
/// Asserts that the global state is initialized.
|
|
pub fn tmpDirPath() []const u8 {
|
|
return state.?.tmp_dir_path.?;
|
|
}
|
|
|
|
/// Returns the global state resources_dir, or an empty one when testing.
|
|
///
|
|
/// Asserts that the global state is initialized when not running as a test.
|
|
pub fn resourcesDir() internal_os.ResourcesDir {
|
|
if (builtin.is_test) return .{};
|
|
|
|
return state.?.resources_dir;
|
|
}
|
|
|
|
/// Returns the global state rlimits, or an empty one when testing.
|
|
///
|
|
/// Asserts that the global state is initialized when not running as a test.
|
|
pub fn rlimits() ResourceLimits {
|
|
if (builtin.is_test) return .{};
|
|
|
|
return state.?.rlimits;
|
|
}
|
|
|
|
/// Returns the global state logging configuration.
|
|
///
|
|
/// Asserts that the global state is initialized.
|
|
pub fn logging() GlobalState.Logging {
|
|
return state.?.logging;
|
|
}
|
|
|
|
/// Returns the global state action.
|
|
///
|
|
/// Asserts that the global state is initialized.
|
|
pub fn action() ?cli.ghostty.Action {
|
|
return state.?.action;
|
|
}
|
|
|
|
/// This represents the global process state. There should only
|
|
/// be one of these at any given moment. This is extracted into a dedicated
|
|
/// struct because it is reused by main and the static C lib.
|
|
pub const GlobalState = struct {
|
|
const GPA = std.heap.DebugAllocator(.{});
|
|
|
|
io_impl: std.Io.Threaded,
|
|
gpa: ?GPA,
|
|
alloc: std.mem.Allocator,
|
|
environ: std.process.Environ,
|
|
args: std.process.Args,
|
|
tmp_dir_path: ?[]const u8,
|
|
action: ?cli.ghostty.Action,
|
|
logging: Logging,
|
|
rlimits: ResourceLimits = .{},
|
|
|
|
/// The app resources directory, equivalent to zig-out/share when we build
|
|
/// from source. This is null if we can't detect it.
|
|
resources_dir: internal_os.ResourcesDir,
|
|
|
|
/// Where logging should go
|
|
pub const Logging = packed struct {
|
|
/// Whether to log to stderr. For lib mode we always disable stderr
|
|
/// logging by default. Otherwise it's enabled by default.
|
|
stderr: bool = build_config.app_runtime != .none,
|
|
/// Whether to log to macOS's unified logging. Enabled by default
|
|
/// on macOS.
|
|
macos: bool = builtin.os.tag.isDarwin(),
|
|
};
|
|
|
|
/// Asserts that `self.io_impl` has been initialized.
|
|
pub fn io(self: *GlobalState) std.Io {
|
|
return self.io_impl.io();
|
|
}
|
|
|
|
fn initSignals() void {
|
|
// Only posix systems.
|
|
if (comptime builtin.os.tag == .windows) return;
|
|
|
|
const p = std.posix;
|
|
|
|
var sa: p.Sigaction = .{
|
|
.handler = .{ .handler = p.SIG.IGN },
|
|
.mask = p.sigemptyset(),
|
|
.flags = 0,
|
|
};
|
|
|
|
// We ignore SIGPIPE because it is a common signal we may get
|
|
// due to how we implement termio. When a terminal is closed we
|
|
// often write to a broken pipe to exit the read thread. This should
|
|
// be fixed one day but for now this helps make this a bit more
|
|
// robust.
|
|
p.sigaction(p.SIG.PIPE, &sa, null);
|
|
}
|
|
};
|
|
|
|
/// Maintains the Unix resource limits that we set for our process. This
|
|
/// can be used to restore the limits to their original values.
|
|
pub const ResourceLimits = struct {
|
|
nofile: ?internal_os.rlimit = null,
|
|
|
|
pub fn init() ResourceLimits {
|
|
return .{
|
|
// Maximize the number of file descriptors we can have open
|
|
// because we can consume a lot of them if we make many terminals.
|
|
.nofile = internal_os.fixMaxFiles(),
|
|
};
|
|
}
|
|
|
|
pub fn restore(self: *const ResourceLimits) void {
|
|
if (self.nofile) |lim| internal_os.restoreMaxFiles(lim);
|
|
}
|
|
};
|