1- import { unzipSync , zipSync } from "fflate" ;
1+ import { unzip , zip } from "fflate" ;
22import * as fs from "node:fs/promises" ;
33import * as path from "node:path" ;
4+ import { promisify } from "node:util" ;
45
5- import { toError } from "../error/errorUtils" ;
66import { type Logger } from "../logging/logger" ;
77import { 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. */
1820async 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