Skip to content
This repository was archived by the owner on Apr 3, 2024. It is now read-only.

Commit 5c93c44

Browse files
fix: Address startup failure from file access errors (#379)
1 parent e38854e commit 5c93c44

14 files changed

+942
-836
lines changed

package-lock.json

Lines changed: 660 additions & 722 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
"@types/node": "^8.0.27",
3636
"@types/once": "^1.4.0",
3737
"@types/pify": "^3.0.0",
38+
"@types/proxyquire": "^1.3.28",
3839
"@types/request": "^2.0.0",
3940
"@types/rimraf": "^2.0.2",
4041
"@types/semver": "^5.3.32",
@@ -68,6 +69,7 @@
6869
"findit2": "^2.2.3",
6970
"gcp-metadata": "^0.4.0",
7071
"lodash": "^4.12.0",
72+
"proxyquire": "^1.8.0",
7173
"semver": "^5.1.0",
7274
"source-map": "^0.6.1",
7375
"split": "^1.0.0",

src/agent/debuglet.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,7 @@ class IsReadyImpl implements IsReady {
159159
export interface FindFilesResult {
160160
jsStats: scanner.ScanStats;
161161
mapFiles: string[];
162+
errors: Map<string, Error>;
162163
hash?: string;
163164
}
164165

@@ -277,7 +278,8 @@ export class Debuglet extends EventEmitter {
277278
const fileStats = await scanner.scan(shouldHash, baseDir, /.js$|.js.map$/);
278279
const jsStats = fileStats.selectStats(/.js$/);
279280
const mapFiles = fileStats.selectFiles(/.js.map$/, process.cwd());
280-
return {jsStats, mapFiles, hash: fileStats.hash};
281+
const errors = fileStats.errors();
282+
return {jsStats, mapFiles, errors, hash: fileStats.hash};
281283
}
282284

283285
/**
@@ -306,6 +308,7 @@ export class Debuglet extends EventEmitter {
306308
let findResults: FindFilesResult;
307309
try {
308310
findResults = await Debuglet.findFiles(!id, that.config.workingDirectory);
311+
findResults.errors.forEach(that.logger.warn);
309312
} catch (err) {
310313
that.logger.error('Error scanning the filesystem.', err);
311314
that.emit('initError', err);

src/agent/io/scanner.ts

Lines changed: 108 additions & 113 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ import * as _ from 'lodash';
2525
import * as path from 'path';
2626

2727
// TODO: Make this more precise.
28-
const split: () => {} = require('split');
28+
const split: () => fs.WriteStream = require('split');
2929

3030
export interface FileStats {
3131
// TODO: Verify that this member should actually be optional.
@@ -37,6 +37,7 @@ export interface FileStats {
3737
export interface ScanStats { [filename: string]: FileStats|undefined; }
3838

3939
export interface ScanResults {
40+
errors(): Map<string, Error>;
4041
all(): ScanStats;
4142
selectStats(regex: RegExp): ScanStats;
4243
selectFiles(regex: RegExp, baseDir: string): string[];
@@ -56,7 +57,13 @@ class ScanResultsImpl implements ScanResults {
5657
* attributes respectively
5758
* @param hash A hashcode computed from the contents of all the files.
5859
*/
59-
constructor(private readonly stats: ScanStats, readonly hash?: string) {}
60+
constructor(
61+
private readonly stats: ScanStats, readonly errorMap: Map<string, Error>,
62+
readonly hash?: string) {}
63+
64+
errors(): Map<string, Error> {
65+
return this.errorMap;
66+
}
6067

6168
/**
6269
* Used to get all of the file scan results.
@@ -105,22 +112,10 @@ class ScanResultsImpl implements ScanResults {
105112
}
106113
}
107114

108-
export function scan(
115+
export async function scan(
109116
shouldHash: boolean, baseDir: string, regex: RegExp): Promise<ScanResults> {
110-
return new Promise<ScanResults>((resolve, reject) => {
111-
findFiles(baseDir, regex, (err1: Error|null, fileList?: string[]) => {
112-
if (err1) {
113-
return reject(err1);
114-
}
115-
// TODO: Handle the case where `fileList` is undefined.
116-
computeStats(fileList as string[], shouldHash, (err2, results) => {
117-
if (err2) {
118-
return reject(err2);
119-
}
120-
resolve(results);
121-
});
122-
});
123-
});
117+
const fileList = await findFiles(baseDir, regex);
118+
return await computeStats(fileList, shouldHash);
124119
}
125120

126121
/**
@@ -135,46 +130,40 @@ export function scan(
135130
// TODO: Typescript: Fix the docs associated with this function to match the
136131
// call signature
137132
function computeStats(
138-
fileList: string[], shouldHash: boolean,
139-
callback: (err: Error|null, results?: ScanResults) => void): void {
140-
let pending = fileList.length;
141-
// return a valid, if fake, result when there are no js files to hash.
142-
if (pending === 0) {
143-
callback(null, new ScanResultsImpl({}, 'EMPTY-no-js-files'));
144-
return;
145-
}
133+
fileList: string[], shouldHash: boolean): Promise<ScanResults> {
134+
return new Promise<ScanResults>(async (resolve, reject) => {
135+
// return a valid, if fake, result when there are no js files to hash.
136+
if (fileList.length === 0) {
137+
resolve(new ScanResultsImpl({}, new Map(), 'EMPTY-no-js-files'));
138+
return;
139+
}
146140

147-
// TODO: Address the case where the array contains `undefined`.
148-
const hashes: Array<string|undefined> = [];
149-
const statistics: ScanStats = {};
150-
fileList.forEach((filename) => {
151-
statsForFile(
152-
filename, shouldHash, (err: Error|null, fileStats?: FileStats) => {
153-
if (err) {
154-
callback(err);
155-
return;
156-
}
141+
// TODO: Address the case where the array contains `undefined`.
142+
const hashes: Array<string|undefined> = [];
143+
const statistics: ScanStats = {};
144+
const errors: Map<string, Error> = new Map<string, Error>();
157145

158-
pending--;
159-
if (shouldHash) {
160-
// TODO: Address the case when `fileStats` is `undefined`
161-
hashes.push((fileStats as FileStats).hash);
162-
}
163-
statistics[filename] = fileStats;
146+
for (const filename of fileList) {
147+
try {
148+
const fileStats = await statsForFile(filename, shouldHash);
149+
if (shouldHash) {
150+
hashes.push(fileStats.hash);
151+
}
152+
statistics[filename] = fileStats;
153+
} catch (err) {
154+
errors.set(filename, err);
155+
}
156+
}
164157

165-
if (pending === 0) {
166-
let hash;
167-
if (shouldHash) {
168-
// Sort the hashes to get a deterministic order as the files may
169-
// not be in the same order each time we scan the disk.
170-
const buffer = hashes.sort().join();
171-
const sha1 =
172-
crypto.createHash('sha1').update(buffer).digest('hex');
173-
hash = 'SHA1-' + sha1;
174-
}
175-
callback(null, new ScanResultsImpl(statistics, hash));
176-
}
177-
});
158+
let hash;
159+
if (shouldHash) {
160+
// Sort the hashes to get a deterministic order as the files may
161+
// not be in the same order each time we scan the disk.
162+
const buffer = hashes.sort().join();
163+
const sha1 = crypto.createHash('sha1').update(buffer).digest('hex');
164+
hash = 'SHA1-' + sha1;
165+
}
166+
resolve(new ScanResultsImpl(statistics, errors, hash));
178167
});
179168
}
180169

@@ -187,45 +176,44 @@ function computeStats(
187176
* files to find based on their filename
188177
* @param {!function(?Error, Array<string>)} callback error-back callback
189178
*/
190-
function findFiles(
191-
baseDir: string, regex: RegExp,
192-
callback: (err: Error|null, fileList?: string[]) => void): void {
193-
let errored = false;
179+
function findFiles(baseDir: string, regex: RegExp): Promise<string[]> {
180+
return new Promise<string[]>((resolve, reject) => {
181+
let error: Error|undefined;
194182

195-
if (!baseDir) {
196-
callback(new Error('hasher.findJSFiles requires a baseDir argument'));
197-
return;
198-
}
183+
if (!baseDir) {
184+
reject(new Error('hasher.findJSFiles requires a baseDir argument'));
185+
return;
186+
}
199187

200-
const find = findit(baseDir);
201-
const fileList: string[] = [];
188+
const find = findit(baseDir);
189+
const fileList: string[] = [];
202190

203-
find.on('error', (err: Error) => {
204-
errored = true;
205-
callback(err);
206-
return;
207-
});
191+
find.on('error', (err: Error) => {
192+
error = err;
193+
return;
194+
});
208195

209-
find.on('directory', (dir: string, ignore: fs.Stats, stop: () => void) => {
210-
const base = path.basename(dir);
211-
if (base === '.git' || base === 'node_modules') {
212-
stop(); // do not descend
213-
}
214-
});
196+
find.on('directory', (dir: string, ignore: fs.Stats, stop: () => void) => {
197+
const base = path.basename(dir);
198+
if (base === '.git' || base === 'node_modules') {
199+
stop(); // do not descend
200+
}
201+
});
215202

216-
find.on('file', (file: string) => {
217-
if (regex.test(file)) {
218-
fileList.push(file);
219-
}
220-
});
203+
find.on('file', (file: string) => {
204+
if (regex.test(file)) {
205+
fileList.push(file);
206+
}
207+
});
221208

222-
find.on('end', () => {
223-
if (errored) {
224-
// the end event fires even after an error
225-
// simply return because the on('error') has already called back
226-
return;
227-
}
228-
callback(null, fileList);
209+
find.on('end', () => {
210+
// Note: the `end` event fires even after an error
211+
if (error) {
212+
reject(error);
213+
} else {
214+
resolve(fileList);
215+
}
216+
});
229217
});
230218
}
231219

@@ -237,31 +225,38 @@ function findFiles(
237225
* @private
238226
*/
239227
function statsForFile(
240-
filename: string, shouldHash: boolean,
241-
cb: (err: Error|null, stats?: FileStats) => void): void {
242-
let shasum: crypto.Hash;
243-
if (shouldHash) {
244-
shasum = crypto.createHash('sha1');
245-
}
246-
// TODO: Determine why property 'ReadStream' does not exist on type 'fs'
247-
const s = (fs as {} as {ReadStream: Function}).ReadStream(filename);
248-
let lines = 0;
249-
const byLine = s.pipe(split());
250-
byLine.on('error', (e: Error) => {
251-
cb(e);
252-
});
253-
byLine.on('data', (d: string) => {
254-
if (shouldHash) {
255-
shasum.update(d);
256-
}
257-
lines++;
258-
});
259-
byLine.on('end', () => {
260-
// TODO: Address the case where `d` is `undefined`.
261-
let d: string|undefined;
262-
if (shouldHash) {
263-
d = shasum.digest('hex');
264-
}
265-
cb(null, {hash: d, lines});
228+
filename: string, shouldHash: boolean): Promise<FileStats> {
229+
return new Promise<FileStats>((resolve, reject) => {
230+
const reader = fs.createReadStream(filename);
231+
reader.on('error', (err) => {
232+
reject(err);
233+
});
234+
reader.on('open', () => {
235+
let shasum: crypto.Hash;
236+
if (shouldHash) {
237+
shasum = crypto.createHash('sha1');
238+
}
239+
240+
let lines = 0;
241+
let error: Error|undefined;
242+
const byLine = reader!.pipe(split());
243+
byLine.on('error', (e: Error) => {
244+
error = e;
245+
});
246+
byLine.on('data', (d: string) => {
247+
if (shouldHash) {
248+
shasum.update(d);
249+
}
250+
lines++;
251+
});
252+
byLine.on('end', () => {
253+
if (error) {
254+
reject(error);
255+
} else {
256+
const hash = shouldHash ? shasum.digest('hex') : undefined;
257+
resolve({hash, lines});
258+
}
259+
});
260+
});
266261
});
267262
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
intended-link.js

0 commit comments

Comments
 (0)