Skip to content

Commit 2009205

Browse files
committed
refactor: self-review on VS Code logs appender
- Use async fflate (unzip/zip via util.promisify) in-memory instead of streaming; peak usage is bounded by log rotation on disk - Parallelize stat+read in collectDirFiles - Drop fs.access pre-checks; let readFile errors surface naturally - Switch the cleanup path to fs.rm({ force: true }) wrapped in try/catch so non-ENOENT errors are logged rather than masked - Pass raw errors to logger (matches codebase convention) Tests: - mtime-based "not touched" assertions - New test for rename failure keeping the -vscode.zip sibling - Gate chmod 0o000 test to POSIX non-root (no-op as root / on Windows) - Shared readZip / makeBundle / vsCodeLogKeys helpers - Assert the -vscode.zip sibling is cleaned up on corrupt input
1 parent d5f5c42 commit 2009205

2 files changed

Lines changed: 206 additions & 146 deletions

File tree

src/core/supportBundleLogs.ts

Lines changed: 73 additions & 84 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
1-
import { unzipSync, zipSync } from "fflate";
1+
import { unzip, zip } from "fflate";
22
import * as fs from "node:fs/promises";
33
import * as path from "node:path";
4+
import { promisify } from "node:util";
45

5-
import { toError } from "../error/errorUtils";
66
import { type Logger } from "../logging/logger";
77
import { renameWithRetry } from "../util";
88

@@ -12,101 +12,87 @@ export interface LogSources {
1212
extensionLogDir?: string;
1313
}
1414

15-
const PROXY_LOG_MAX_AGE_MS = 3 * 24 * 60 * 60 * 1000;
15+
const LOG_MAX_AGE_MS = 3 * 24 * 60 * 60 * 1000;
16+
17+
const unzipAsync = promisify(unzip);
18+
const zipAsync = promisify(zip);
1619

17-
/** Collect regular files from a directory into zip-ready entries. */
1820
async function collectDirFiles(
1921
dirPath: string,
20-
zipPrefix: string,
2122
logger: Logger,
22-
maxAgeMs?: number,
23-
): Promise<Record<string, Uint8Array>> {
24-
const files: Record<string, Uint8Array> = {};
25-
const now = Date.now();
23+
): Promise<Map<string, Uint8Array>> {
24+
const results = new Map<string, Uint8Array>();
2625

2726
let entries: string[];
2827
try {
2928
entries = await fs.readdir(dirPath);
3029
} catch (error) {
31-
logger.warn(
32-
`Could not read log directory ${dirPath}: ${toError(error).message}`,
33-
);
34-
return files;
30+
logger.warn(`Could not read log directory ${dirPath}`, error);
31+
return results;
3532
}
3633

37-
for (const entry of entries) {
38-
const filePath = path.join(dirPath, entry);
39-
try {
40-
const stat = await fs.stat(filePath);
41-
if (!stat.isFile()) {
42-
continue;
34+
const cutoff = Date.now() - LOG_MAX_AGE_MS;
35+
36+
await Promise.all(
37+
entries.map(async (entry) => {
38+
const filePath = path.join(dirPath, entry);
39+
try {
40+
const stat = await fs.stat(filePath);
41+
if (!stat.isFile() || stat.mtimeMs < cutoff) {
42+
return;
43+
}
44+
results.set(entry, await fs.readFile(filePath));
45+
} catch (error) {
46+
logger.warn(`Could not read log file ${filePath}`, error);
4347
}
44-
if (maxAgeMs !== undefined && now - stat.mtimeMs > maxAgeMs) {
45-
continue;
46-
}
47-
const content = await fs.readFile(filePath);
48-
files[`${zipPrefix}/${entry}`] = new Uint8Array(content);
49-
} catch (error) {
50-
logger.warn(
51-
`Could not read log file ${filePath}: ${toError(error).message}`,
52-
);
53-
}
54-
}
48+
}),
49+
);
5550

56-
return files;
51+
return results;
5752
}
5853

5954
/**
6055
* Gather log files from each source independently so a failure in one
6156
* does not affect the others.
6257
*/
63-
export async function collectLogFiles(
58+
async function collectLogFiles(
6459
sources: LogSources,
6560
logger: Logger,
66-
): Promise<Record<string, Uint8Array>> {
67-
const files: Record<string, Uint8Array> = {};
61+
): Promise<Map<string, Uint8Array>> {
62+
const files = new Map<string, Uint8Array>();
6863

6964
if (sources.remoteSshLogPath) {
7065
try {
71-
const content = await fs.readFile(sources.remoteSshLogPath);
72-
const name = path.basename(sources.remoteSshLogPath);
73-
files[`vscode-logs/remote-ssh/${name}`] = new Uint8Array(content);
66+
files.set(
67+
`vscode-logs/remote-ssh/${path.basename(sources.remoteSshLogPath)}`,
68+
await fs.readFile(sources.remoteSshLogPath),
69+
);
7470
} catch (error) {
75-
logger.warn(`Could not read Remote SSH log: ${toError(error).message}`);
71+
logger.warn("Could not read Remote SSH log", error);
7672
}
7773
}
7874

7975
if (sources.proxyLogDir) {
80-
Object.assign(
81-
files,
82-
await collectDirFiles(
83-
sources.proxyLogDir,
84-
"vscode-logs/proxy",
85-
logger,
86-
PROXY_LOG_MAX_AGE_MS,
87-
),
88-
);
76+
for (const [name, data] of await collectDirFiles(
77+
sources.proxyLogDir,
78+
logger,
79+
)) {
80+
files.set(`vscode-logs/proxy/${name}`, data);
81+
}
8982
}
9083

9184
if (sources.extensionLogDir) {
92-
Object.assign(
93-
files,
94-
await collectDirFiles(
95-
sources.extensionLogDir,
96-
"vscode-logs/extension",
97-
logger,
98-
),
99-
);
85+
for (const [name, data] of await collectDirFiles(
86+
sources.extensionLogDir,
87+
logger,
88+
)) {
89+
files.set(`vscode-logs/extension/${name}`, data);
90+
}
10091
}
10192

10293
return files;
10394
}
10495

105-
function vscodeBundlePath(zipPath: string): string {
106-
const { dir, name, ext } = path.parse(zipPath);
107-
return path.join(dir, `${name}-vscode${ext}`);
108-
}
109-
11096
/**
11197
* Best-effort: append VS Code logs to a support bundle zip.
11298
* Uses atomic rename to avoid corrupting the original bundle on failure.
@@ -117,44 +103,47 @@ export async function appendVsCodeLogs(
117103
logger: Logger,
118104
): Promise<void> {
119105
const logFiles = await collectLogFiles(sources, logger);
120-
const count = Object.keys(logFiles).length;
121-
if (count === 0) {
106+
if (logFiles.size === 0) {
122107
logger.info("No VS Code logs found to add to support bundle");
123108
return;
124109
}
125110

126-
logger.info(`Adding ${count} VS Code log file(s) to support bundle`);
111+
logger.info(`Adding ${logFiles.size} VS Code log file(s) to support bundle`);
127112

128-
let updatedData: Uint8Array;
129-
try {
130-
const existingData = new Uint8Array(await fs.readFile(zipPath));
131-
const entries = unzipSync(existingData);
132-
Object.assign(entries, logFiles);
133-
updatedData = zipSync(entries);
134-
} catch (error) {
135-
logger.error(
136-
`Failed to add VS Code logs to support bundle: ${toError(error).message}`,
137-
);
138-
return;
139-
}
113+
// Write to a named temporary path first so a failure at the rename step
114+
// leaves the user with a properly named file containing VS Code logs.
115+
const parsed = path.parse(zipPath);
116+
const vscodeBundlePath = path.join(
117+
parsed.dir,
118+
`${parsed.name}-vscode${parsed.ext}`,
119+
);
140120

141-
// Write to a named temporary path first so a failure mid-write leaves
142-
// the user with a properly named file containing VS Code logs.
143-
const tmpPath = vscodeBundlePath(zipPath);
144121
try {
145-
await fs.writeFile(tmpPath, updatedData);
122+
const entries = await unzipAsync(await fs.readFile(zipPath));
123+
for (const [name, data] of logFiles) {
124+
entries[name] = data;
125+
}
126+
const updated = await zipAsync(entries);
127+
await fs.writeFile(vscodeBundlePath, updated);
146128
} catch (error) {
147-
logger.error(
148-
`Failed to write updated support bundle: ${toError(error).message}`,
149-
);
129+
logger.error("Failed to add VS Code logs to support bundle", error);
130+
try {
131+
await fs.rm(vscodeBundlePath, { force: true });
132+
} catch (cleanupError) {
133+
logger.warn(
134+
`Could not clean up partial bundle at ${vscodeBundlePath}`,
135+
cleanupError,
136+
);
137+
}
150138
return;
151139
}
152140

153141
try {
154-
await renameWithRetry(fs.rename, tmpPath, zipPath);
142+
await renameWithRetry(fs.rename, vscodeBundlePath, zipPath);
155143
} catch (error) {
156144
logger.warn(
157-
`Could not replace original bundle, VS Code logs saved separately: ${toError(error).message}`,
145+
`Could not replace original bundle; VS Code logs saved separately at ${vscodeBundlePath}`,
146+
error,
158147
);
159148
}
160149
}

0 commit comments

Comments
 (0)