mirror of
https://github.com/ghostty-org/ghostty.git
synced 2026-08-24 16:11:43 +00:00
cli: version SSH terminfo cache entries
Derive a version from the encoded Ghostty terminfo and require callers to pass it explicitly when reading or writing the SSH cache. Cache entries created for older or different payloads no longer suppress a required installation.
This commit is contained in:
@@ -58,6 +58,7 @@ pub fn add(
|
||||
self: DiskCache,
|
||||
alloc: Allocator,
|
||||
key: []const u8,
|
||||
version: []const u8,
|
||||
timestamp: i64,
|
||||
) !void {
|
||||
if (!isValidCacheKey(key)) return error.InvalidCacheKey;
|
||||
@@ -111,16 +112,19 @@ pub fn add(
|
||||
// `deinitEntries` defer to walk.
|
||||
if (entries.getPtr(key)) |existing| {
|
||||
existing.timestamp = timestamp;
|
||||
const version_copy = try alloc.dupe(u8, version);
|
||||
alloc.free(existing.terminfo_version);
|
||||
existing.terminfo_version = version_copy;
|
||||
} else {
|
||||
const key_copy = try alloc.dupe(u8, key);
|
||||
errdefer alloc.free(key_copy);
|
||||
const terminfo_copy = try alloc.dupe(u8, "xterm-ghostty");
|
||||
errdefer alloc.free(terminfo_copy);
|
||||
const version_copy = try alloc.dupe(u8, version);
|
||||
errdefer alloc.free(version_copy);
|
||||
|
||||
try entries.put(key_copy, .{
|
||||
.hostname = key_copy,
|
||||
.timestamp = timestamp,
|
||||
.terminfo_version = terminfo_copy,
|
||||
.terminfo_version = version_copy,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -225,12 +229,13 @@ pub fn prune(
|
||||
return expired.items.len;
|
||||
}
|
||||
|
||||
/// Check if a key exists in the cache.
|
||||
/// Returns false if the cache file doesn't exist.
|
||||
/// Check if a key with `version` exists in the cache.
|
||||
/// Returns false if the cache file doesn't exist or the version doesn't match.
|
||||
pub fn contains(
|
||||
self: DiskCache,
|
||||
alloc: Allocator,
|
||||
key: []const u8,
|
||||
version: []const u8,
|
||||
) !bool {
|
||||
if (!isValidCacheKey(key)) return error.InvalidCacheKey;
|
||||
|
||||
@@ -249,7 +254,8 @@ pub fn contains(
|
||||
var entries = try readEntries(alloc, file);
|
||||
defer deinitEntries(alloc, &entries);
|
||||
|
||||
return entries.contains(key);
|
||||
const entry = entries.get(key) orelse return false;
|
||||
return std.mem.eql(u8, entry.terminfo_version, version);
|
||||
}
|
||||
|
||||
fn fixupPermissions(file: std.Io.File) !void {
|
||||
@@ -506,20 +512,25 @@ test "disk cache operations" {
|
||||
// Setup our cache. Adding the same key twice exercises both the new
|
||||
// and existing-entry paths.
|
||||
const cache: DiskCache = .{ .path = path };
|
||||
try cache.add(alloc, "example.com", std.Io.Timestamp.now(testing.io, .real).toSeconds());
|
||||
try cache.add(alloc, "example.com", std.Io.Timestamp.now(testing.io, .real).toSeconds());
|
||||
try testing.expect(try cache.contains(alloc, "example.com"));
|
||||
try cache.add(alloc, "example.com", "v1", std.Io.Timestamp.now(testing.io, .real).toSeconds());
|
||||
try testing.expect(!try cache.contains(alloc, "example.com", "v2"));
|
||||
try cache.add(alloc, "example.com", "v2", std.Io.Timestamp.now(testing.io, .real).toSeconds());
|
||||
try testing.expect(try cache.contains(alloc, "example.com", "v2"));
|
||||
|
||||
// List
|
||||
var entries = try cache.list(alloc);
|
||||
deinitEntries(alloc, &entries);
|
||||
defer deinitEntries(alloc, &entries);
|
||||
try testing.expectEqualStrings(
|
||||
"v2",
|
||||
entries.get("example.com").?.terminfo_version,
|
||||
);
|
||||
|
||||
// Remove reports that it removed the entry, and a second remove of the
|
||||
// same key reports nothing to remove.
|
||||
try testing.expect(try cache.remove(alloc, "example.com"));
|
||||
try testing.expect(!try cache.remove(alloc, "example.com"));
|
||||
try testing.expect(!(try cache.contains(alloc, "example.com")));
|
||||
try cache.add(alloc, "example.com", std.Io.Timestamp.now(testing.io, .real).toSeconds());
|
||||
try testing.expect(!(try cache.contains(alloc, "example.com", "v2")));
|
||||
try cache.add(alloc, "example.com", "v2", std.Io.Timestamp.now(testing.io, .real).toSeconds());
|
||||
}
|
||||
|
||||
test "disk cache cleans up temp files" {
|
||||
@@ -535,8 +546,8 @@ test "disk cache cleans up temp files" {
|
||||
defer alloc.free(cache_path);
|
||||
|
||||
const cache: DiskCache = .{ .path = cache_path };
|
||||
try cache.add(alloc, "example.com", std.Io.Timestamp.now(testing.io, .real).toSeconds());
|
||||
try cache.add(alloc, "example.org", std.Io.Timestamp.now(testing.io, .real).toSeconds());
|
||||
try cache.add(alloc, "example.com", "v1", std.Io.Timestamp.now(testing.io, .real).toSeconds());
|
||||
try cache.add(alloc, "example.org", "v1", std.Io.Timestamp.now(testing.io, .real).toSeconds());
|
||||
|
||||
// Verify only the cache file exists and no temp files left behind
|
||||
var count: usize = 0;
|
||||
@@ -566,20 +577,20 @@ test "disk cache prune" {
|
||||
const day = std.time.s_per_day;
|
||||
const hour = std.time.s_per_hour;
|
||||
const now = std.Io.Timestamp.now(testing.io, .real).toSeconds();
|
||||
try cache.add(alloc, "recent.com", now - hour);
|
||||
try cache.add(alloc, "old.com", now - 100 * day);
|
||||
try cache.add(alloc, "recent.com", "v1", now - hour);
|
||||
try cache.add(alloc, "old.com", "v1", now - 100 * day);
|
||||
|
||||
// Prune entries older than 90 days: only old.com goes.
|
||||
try testing.expectEqual(@as(usize, 1), try cache.prune(alloc, 90 * day));
|
||||
try testing.expect(try cache.contains(alloc, "recent.com"));
|
||||
try testing.expect(!try cache.contains(alloc, "old.com"));
|
||||
try testing.expect(try cache.contains(alloc, "recent.com", "v1"));
|
||||
try testing.expect(!try cache.contains(alloc, "old.com", "v1"));
|
||||
|
||||
// Pruning again removes nothing.
|
||||
try testing.expectEqual(@as(usize, 0), try cache.prune(alloc, 90 * day));
|
||||
|
||||
// Sub-day granularity: a 30-minute max age prunes the hour-old entry.
|
||||
try testing.expectEqual(@as(usize, 1), try cache.prune(alloc, 30 * std.time.s_per_min));
|
||||
try testing.expect(!try cache.contains(alloc, "recent.com"));
|
||||
try testing.expect(!try cache.contains(alloc, "recent.com", "v1"));
|
||||
}
|
||||
|
||||
test "disk cache prune missing file" {
|
||||
@@ -707,7 +718,7 @@ test "disk cache add survives allocation failure" {
|
||||
);
|
||||
const alloc = failing.allocator();
|
||||
|
||||
if (cache.add(alloc, "user@example.com", 100)) |_| {
|
||||
if (cache.add(alloc, "user@example.com", "v1", 100)) |_| {
|
||||
if (!failing.has_induced_failure) break;
|
||||
} else |err| {
|
||||
try testing.expectEqual(error.OutOfMemory, err);
|
||||
|
||||
@@ -7,7 +7,7 @@ const diagnostics = @import("diagnostics.zig");
|
||||
const Action = @import("ghostty.zig").Action;
|
||||
const DiskCache = @import("ssh_cache.zig").DiskCache;
|
||||
const internal_os = @import("../os/main.zig");
|
||||
const ghostty_terminfo = @import("../terminfo/main.zig").ghostty;
|
||||
const terminfopkg = @import("../terminfo/main.zig");
|
||||
const global = @import("../global.zig");
|
||||
|
||||
const log = std.log.scoped(.ssh);
|
||||
@@ -252,7 +252,11 @@ fn runInner(
|
||||
} else null;
|
||||
|
||||
if (cache) |c| {
|
||||
const cached = c.contains(alloc, dest) catch |err| cached: {
|
||||
const cached = c.contains(
|
||||
alloc,
|
||||
dest,
|
||||
terminfopkg.version,
|
||||
) catch |err| cached: {
|
||||
if (DiskCache.isFailure(err)) warnPrint(
|
||||
stderr,
|
||||
"unable to read the cache '{s}': {t}",
|
||||
@@ -313,10 +317,12 @@ fn runInner(
|
||||
|
||||
// Attempt to cache (if needed) on a successful ssh execution.
|
||||
if (exit_code == 0) if (session.to_cache) |entry| {
|
||||
if (entry.cache.add(alloc, entry.dest, std.Io.Timestamp.now(
|
||||
global.io(),
|
||||
.real,
|
||||
).toSeconds())) |_| {
|
||||
if (entry.cache.add(
|
||||
alloc,
|
||||
entry.dest,
|
||||
terminfopkg.version,
|
||||
std.Io.Timestamp.now(global.io(), .real).toSeconds(),
|
||||
)) |_| {
|
||||
verbosePrint(opts, stderr, "cache: wrote {s}", .{entry.dest});
|
||||
} else |err| {
|
||||
if (DiskCache.isFailure(err)) {
|
||||
@@ -473,7 +479,7 @@ fn installRemoteTerminfo(
|
||||
) !void {
|
||||
var buf: std.Io.Writer.Allocating = .init(alloc);
|
||||
defer buf.deinit();
|
||||
try ghostty_terminfo.encode(&buf.writer);
|
||||
try terminfopkg.ghostty.encode(&buf.writer);
|
||||
const terminfo = buf.written();
|
||||
|
||||
// ControlPath is in TMPDIR with a short, random basename. ssh uses
|
||||
|
||||
@@ -5,6 +5,7 @@ const args = @import("args.zig");
|
||||
const global = @import("../global.zig");
|
||||
const Action = @import("ghostty.zig").Action;
|
||||
const Duration = @import("../config.zig").Config.Duration;
|
||||
const terminfopkg = @import("../terminfo/main.zig");
|
||||
pub const Entry = @import("ssh-cache/Entry.zig");
|
||||
pub const DiskCache = @import("ssh-cache/DiskCache.zig");
|
||||
|
||||
@@ -198,6 +199,7 @@ pub fn runInner(
|
||||
cache.add(
|
||||
alloc,
|
||||
dest,
|
||||
terminfopkg.version,
|
||||
std.Io.Timestamp.now(global.io(), .real).toSeconds(),
|
||||
) catch |err| switch (err) {
|
||||
error.InvalidCacheKey => {
|
||||
|
||||
@@ -392,6 +392,15 @@ pub const ghostty: Source = .{
|
||||
},
|
||||
};
|
||||
|
||||
/// A content-derived version of the encoded terminfo source.
|
||||
pub const version = version: {
|
||||
@setEvalBranchQuota(100_000);
|
||||
var hashing: std.Io.Writer.Hashing(std.hash.Wyhash) =
|
||||
.initHasher(.init(0), &.{});
|
||||
ghostty.encode(&hashing.writer) catch unreachable;
|
||||
break :version std.fmt.comptimePrint("{x}", .{hashing.hasher.final()});
|
||||
};
|
||||
|
||||
test "encode" {
|
||||
// Encode
|
||||
var buf: [1024 * 16]u8 = undefined;
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
//! extract this into a more full-featured library on its own.
|
||||
|
||||
pub const ghostty = @import("ghostty.zig").ghostty;
|
||||
pub const version = @import("ghostty.zig").version;
|
||||
pub const Source = @import("Source.zig");
|
||||
|
||||
test {
|
||||
|
||||
Reference in New Issue
Block a user