a73x

src/os/spawn.zig

Ref:   Size: 3.5 KiB   History

//! Resolve the executable used when mux starts a daemon process. Resolving
//! the running image ensures the new process runs the current binary rather
//! than another `mux` found through `PATH`.
const std = @import("std");
const builtin = @import("builtin");

/// The kernel's link to the running image, used only when the resolved
/// path is no longer executable — Linux keeps a live link after a rename-
/// over, so the exec runs the image already running. On an OS with no such
/// link the fallback is the resolved path itself, and an exec after a
/// rename-over runs the NEW file at that path rather than the running one.
/// That is the ordinary meaning of the path and not a failure, but it does
/// mean the started daemon can be a different build from the starter.
pub const self_exe: []const u8 = switch (builtin.os.tag) {
    .linux => "/proc/self/exe",
    else => "",
};

/// Return the resolved path of the current executable, falling back to
/// this OS's link to the running image. On an OS with no such link an
/// unresolvable path comes back empty and the exec fails with it: naming
/// `mux` instead would be the PATH walk this module exists to prevent.
pub fn selfExe(buf: *[std.fs.max_path_bytes]u8) []const u8 {
    // Prefer the resolved path because process listings derive `comm` from the
    // filename passed to execve; executing the link would name every daemon
    // `exe`.
    return execOrLink(std.fs.selfExePath(buf) catch return self_exe);
}

/// The resolved path if it can still be exec'd, this OS's link to the
/// running image if it cannot and it has one.
/// Split out so the fallback is assertable without deleting a live binary.
fn execOrLink(resolved: []const u8) []const u8 {
    // After `make install`, the resolved path may end in ` (deleted)` on
    // Linux and no longer be executable even though the readlink succeeded.
    std.posix.access(resolved, std.posix.X_OK) catch
        return if (self_exe.len == 0) resolved else self_exe;
    return resolved;
}

// ---------------------------------------------------------------------------

test "selfExe: the exec'd name is a real file, not the /proc link" {
    var buf: [std.fs.max_path_bytes]u8 = undefined;
    const exe = selfExe(&buf);
    // The resolved basename becomes the process name shown by tools such as
    // `ps`, `pgrep`, and `killall`.
    try std.testing.expect(!std.mem.eql(u8, exe, self_exe));
    try std.posix.access(exe, std.posix.X_OK);
}

test "selfExe: a resolved path that is no longer a file falls back to the link" {
    // What `make install` does to a running wall. Spelled as the suffix the
    // kernel actually appends, because that is the string this must survive.
    var buf: [std.fs.max_path_bytes]u8 = undefined;
    const live = try std.fs.selfExePath(&buf);
    try std.testing.expectEqualStrings(live, execOrLink(live));

    // The suffix is Linux's; an OS with no such link has no fallback path
    // to assert, and `execOrLink` hands back what it was given.
    if (builtin.os.tag == .linux) {
        var gone: [std.fs.max_path_bytes]u8 = undefined;
        const deleted = try std.fmt.bufPrint(&gone, "{s} (deleted)", .{live});
        try std.testing.expectEqualStrings(self_exe, execOrLink(deleted));
    }
}

// Forces semantic analysis of every pub decl under `zig build test`, so an
// unreferenced decl must at least compile (the silent-module-loss hazard,
// decisions.md). Pub decls only: std.meta.declarations sees nothing private.
test {
    std.testing.refAllDeclsRecursive(@This());
}