Skip to content

Commit a7f3677

Browse files
JesstLeLe Jsessymaria-rcks
authored andcommitted
fix(server): bound terminal history names for encrypted filesystems (pingdotgg#21654)
Co-authored-by: Le Jsessy <13083864+le-jsessy@user.noreply.gitee.com> Co-authored-by: maria-rcks <maria@kuuro.net>
1 parent 0df59f0 commit a7f3677

2 files changed

Lines changed: 272 additions & 99 deletions

File tree

‎apps/server/src/terminal/Manager.test.ts‎

Lines changed: 175 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1195,7 +1195,7 @@ it.layer(
11951195

11961196
it.effect("keeps reading and deleting history at an encoded path that already fit", () =>
11971197
Effect.gen(function* () {
1198-
const threadId = "x".repeat(120);
1198+
const threadId = "x".repeat(48);
11991199
const { manager, logsDir } = yield* createManager();
12001200
const fs = yield* FileSystem.FileSystem;
12011201
const logPath = yield* historyLogPath(logsDir, threadId);
@@ -1213,6 +1213,180 @@ it.layer(
12131213
}),
12141214
);
12151215

1216+
it.effect.each([
1217+
{
1218+
label: "delegated setup",
1219+
threadId:
1220+
"thread:delegated-task:command%3Amcp%3A00000000-0000-4000-8000-000000000000%3Adelegate-task%3Aexample-r4-workspace-prepare-engine3",
1221+
terminalId: "setup-setup-worktree-2",
1222+
},
1223+
{ label: "long thread", threadId: "t".repeat(400), terminalId: DEFAULT_TERMINAL_ID },
1224+
{ label: "Unicode thread", threadId: "项目😀".repeat(60), terminalId: "setup" },
1225+
{ label: "long terminal", threadId: "short", terminalId: "t".repeat(128) },
1226+
{ label: "142-byte boundary", threadId: "t".repeat(48), terminalId: "s".repeat(48) },
1227+
{ label: "144-byte boundary", threadId: "t".repeat(49), terminalId: "s".repeat(48) },
1228+
])("persists $label history with a 143-byte filename limit", ({ threadId, terminalId }) =>
1229+
Effect.gen(function* () {
1230+
const fs = yield* FileSystem.FileSystem;
1231+
const path = yield* Path.Path;
1232+
const check = (filePath: string) =>
1233+
Buffer.byteLength(path.basename(filePath)) <= 143
1234+
? Effect.void
1235+
: Effect.fail(
1236+
PlatformError.systemError({
1237+
_tag: "Unknown",
1238+
module: "FileSystem",
1239+
method: "access",
1240+
pathOrDescriptor: filePath,
1241+
cause: { code: "ENAMETOOLONG" },
1242+
}),
1243+
);
1244+
const limited = FileSystem.FileSystem.of({
1245+
...fs,
1246+
exists: (filePath) => Effect.andThen(check(filePath), fs.exists(filePath)),
1247+
open: (filePath, options) => Effect.andThen(check(filePath), fs.open(filePath, options)),
1248+
writeFileString: (filePath, data, options) =>
1249+
Effect.andThen(check(filePath), fs.writeFileString(filePath, data, options)),
1250+
remove: (filePath, options) =>
1251+
Effect.andThen(check(filePath), fs.remove(filePath, options)),
1252+
});
1253+
const { manager, ptyAdapter, logsDir } = yield* createManager().pipe(
1254+
Effect.provideService(FileSystem.FileSystem, limited),
1255+
);
1256+
yield* manager.open(openInput({ threadId, terminalId }));
1257+
ptyAdapter.processes[0]!.emitData("setup output\n");
1258+
yield* manager.close({ threadId, terminalId });
1259+
const names = yield* fs.readDirectory(logsDir);
1260+
expect(names).toHaveLength(1);
1261+
expect(Buffer.byteLength(names[0]!)).toBeLessThanOrEqual(143);
1262+
expect((yield* manager.open(openInput({ threadId, terminalId }))).history).toBe(
1263+
"setup output\n",
1264+
);
1265+
const otherThread = `${threadId}-other`;
1266+
expect((yield* manager.open(openInput({ threadId: otherThread, terminalId }))).history).toBe(
1267+
"",
1268+
);
1269+
ptyAdapter.processes[2]!.emitData("other output\n");
1270+
yield* manager.close({ threadId: otherThread, terminalId });
1271+
expect(yield* fs.readDirectory(logsDir)).toHaveLength(2);
1272+
yield* manager.close({ threadId, terminalId, deleteHistory: true });
1273+
expect(yield* fs.readDirectory(logsDir)).toHaveLength(1);
1274+
yield* manager.close({ threadId: otherThread, deleteHistory: true });
1275+
expect(yield* fs.readDirectory(logsDir)).toEqual([]);
1276+
}),
1277+
);
1278+
1279+
it.effect.each([
1280+
{ source: "encoded", terminalId: DEFAULT_TERMINAL_ID },
1281+
{ source: "encoded", terminalId: "setup-setup-worktree-2" },
1282+
{ source: "sanitized", terminalId: DEFAULT_TERMINAL_ID },
1283+
])("migrates oversized $source history for $terminalId", ({ source, terminalId }) =>
1284+
Effect.gen(function* () {
1285+
const fs = yield* FileSystem.FileSystem;
1286+
const path = yield* Path.Path;
1287+
const { manager, logsDir } = yield* createManager();
1288+
const threadId = "m".repeat(100);
1289+
const oldPath =
1290+
source === "encoded"
1291+
? path.join(
1292+
logsDir,
1293+
`terminal_${Base64Url.encode(threadId)}${terminalId === DEFAULT_TERMINAL_ID ? "" : `_${Base64Url.encode(terminalId)}`}.log`,
1294+
)
1295+
: path.join(logsDir, `${threadId}.log`);
1296+
yield* fs.writeFileString(oldPath, "old output\n");
1297+
expect((yield* manager.open(openInput({ threadId, terminalId }))).history).toBe(
1298+
"old output\n",
1299+
);
1300+
const names = yield* fs.readDirectory(logsDir);
1301+
expect(names).toHaveLength(1);
1302+
expect(Buffer.byteLength(names[0]!)).toBeLessThanOrEqual(143);
1303+
expect(yield* fs.exists(oldPath)).toBe(false);
1304+
expect(yield* fs.readFileString(path.join(logsDir, names[0]!))).toBe("old output\n");
1305+
yield* manager.close({ threadId, terminalId, deleteHistory: true });
1306+
expect(yield* fs.readDirectory(logsDir)).toEqual([]);
1307+
}),
1308+
);
1309+
1310+
it.effect.each(["terminal", "thread"] as const)(
1311+
"deletes compact and older encoded history when closing a %s",
1312+
(scope) =>
1313+
Effect.gen(function* () {
1314+
const fs = yield* FileSystem.FileSystem;
1315+
const path = yield* Path.Path;
1316+
const { manager, logsDir } = yield* createManager();
1317+
const threadId = "d".repeat(100);
1318+
const terminalId = "setup-setup-worktree-2";
1319+
yield* manager.open(openInput({ threadId, terminalId }));
1320+
yield* manager.close({ threadId, terminalId });
1321+
const oldPath = path.join(
1322+
logsDir,
1323+
`terminal_${Base64Url.encode(threadId)}_${Base64Url.encode(terminalId)}.log`,
1324+
);
1325+
yield* fs.writeFileString(oldPath, "stale copy\n");
1326+
yield* fs.writeFileString(path.join(logsDir, "unrelated.log"), "keep\n");
1327+
if (scope === "terminal") {
1328+
yield* manager.close({ threadId, terminalId, deleteHistory: true });
1329+
} else {
1330+
yield* manager.close({ threadId, deleteHistory: true });
1331+
}
1332+
expect(yield* fs.readDirectory(logsDir)).toEqual(["unrelated.log"]);
1333+
}),
1334+
);
1335+
1336+
it.effect("prefers bounded history over a stale encoded copy", () =>
1337+
Effect.gen(function* () {
1338+
const { manager, ptyAdapter, logsDir } = yield* createManager();
1339+
const fs = yield* FileSystem.FileSystem;
1340+
const threadId = "p".repeat(100);
1341+
yield* manager.open(openInput({ threadId }));
1342+
ptyAdapter.processes[0]!.emitData("current\n");
1343+
yield* manager.close({ threadId });
1344+
const oldPath = yield* historyLogPath(logsDir, threadId);
1345+
yield* fs.writeFileString(oldPath, "stale\n");
1346+
1347+
expect((yield* manager.open(openInput({ threadId }))).history).toBe("current\n");
1348+
yield* manager.close({ threadId, terminalId: DEFAULT_TERMINAL_ID, deleteHistory: true });
1349+
expect(yield* fs.readDirectory(logsDir)).toEqual([]);
1350+
}),
1351+
);
1352+
1353+
it.effect("retains encoded history if migration cannot write the bounded file", () =>
1354+
Effect.gen(function* () {
1355+
const fs = yield* FileSystem.FileSystem;
1356+
let failNextWrite = true;
1357+
const limited = FileSystem.FileSystem.of({
1358+
...fs,
1359+
writeFileString: (filePath, data, options) => {
1360+
if (!failNextWrite) return fs.writeFileString(filePath, data, options);
1361+
failNextWrite = false;
1362+
return fs.writeFileString(filePath, "partial").pipe(
1363+
Effect.andThen(
1364+
Effect.fail(
1365+
PlatformError.systemError({
1366+
_tag: "Unknown",
1367+
module: "FileSystem",
1368+
method: "writeFileString",
1369+
cause: { code: "ENOSPC" },
1370+
}),
1371+
),
1372+
),
1373+
);
1374+
},
1375+
});
1376+
const { manager, logsDir } = yield* createManager().pipe(
1377+
Effect.provideService(FileSystem.FileSystem, limited),
1378+
);
1379+
const threadId = "f".repeat(100);
1380+
const oldPath = yield* historyLogPath(logsDir, threadId);
1381+
yield* fs.writeFileString(oldPath, "preserve me\n");
1382+
1383+
expect(Exit.isFailure(yield* Effect.exit(manager.open(openInput({ threadId }))))).toBe(true);
1384+
expect(yield* fs.readFileString(oldPath)).toBe("preserve me\n");
1385+
expect(yield* fs.readDirectory(logsDir)).toHaveLength(1);
1386+
expect((yield* manager.open(openInput({ threadId }))).history).toBe("preserve me\n");
1387+
}),
1388+
);
1389+
12161390
it.effect("migrates legacy transcript filenames to terminal-scoped history path on open", () =>
12171391
Effect.gen(function* () {
12181392
const { manager, logsDir } = yield* createManager();

0 commit comments

Comments
 (0)