Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix crash in bsc project when thread has error #235

Merged
merged 3 commits into from
Mar 4, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 29 additions & 2 deletions src/bsc/BscProjectThreaded.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ export class BscProjectThreaded implements ExtractMethods<BscProject> {
private messageHandler: ThreadMessageHandler<BscProject>;

private activateDeferred = new Deferred();
private errorDeferred = new Deferred();

/**
* Has this project finished activating (either resolved or rejected...)
Expand All @@ -26,12 +27,38 @@ export class BscProjectThreaded implements ExtractMethods<BscProject> {
return this.activateDeferred.isCompleted;
}

/**
* If an error occurs at any time during the worker's lifetime, it will be caught and stored here.
*/
public isErrored() {
return this.errorDeferred.isCompleted;
}

private ready() {
return Promise.race([
//if we've encountered an error, reject immediately. The underlying error should propagate up
this.errorDeferred.promise,
//wait for the activate to finish. This should typically be the only promise that resolves
this.activateDeferred.promise
]);
}

public async activate(options: Parameters<ProgramBuilder['run']>[0]) {
const timeEnd = this.logger.timeStart('log', 'activate');

// start a new worker thread or get an unused existing thread
this.worker = bscProjectWorkerPool.getWorker();

//!!!IMPORTANT!!! this observer must be registered in order to prevent the worker thread from crashing the main thread
this.worker.on('error', (error) => {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was the most important part of the fix. by not observing this, node sees an unhandled exception and crashes.

this.logger.error('Worker encountered an error', error);
this.errorDeferred.reject(error);
//!!!IMPORTANT!!! this is required to prevent node from freaking out about an uncaught promise
this.errorDeferred.promise.catch(e => {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is also important. It's new code that helps track whether our worker has crashed. but by not observing the rejection, that would also bring down the node process. So we observe it, do nothing, and assume someone else will pay attention on the outside.

//do nothing. this is just to prevent node from freaking out about an uncaught promise
});
});

//link the message handler to the worker
this.messageHandler = new ThreadMessageHandler<BscProject>({
name: 'MainThread',
Expand Down Expand Up @@ -62,7 +89,7 @@ export class BscProjectThreaded implements ExtractMethods<BscProject> {
* @param relativePath path to the file relative to rootDir
* @returns
*/
public getScopeFunctionsForFile(options: { relativePath: string }): MaybePromise<Array<ScopeFunction>> {
public async getScopeFunctionsForFile(options: { relativePath: string }): Promise<Array<ScopeFunction>> {
return this.sendStandardRequest('getScopeFunctionsForFile', options);
}

Expand All @@ -73,7 +100,7 @@ export class BscProjectThreaded implements ExtractMethods<BscProject> {
* @returns the response from the request
*/
private async sendStandardRequest<T>(name: MethodNames<BscProject>, ...data: any[]) {
await this.activateDeferred.promise;
await this.ready();
const response = await this.messageHandler.sendRequest<T>(name, {
data: data
});
Expand Down
21 changes: 13 additions & 8 deletions src/debugSession/BrightScriptDebugSession.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1919,15 +1919,20 @@ export class BrightScriptDebugSession extends BaseDebugSession {
}

const frame = this.rokuAdapter.getStackFrameById(args.frameId);
let scopeFunctions = await this.projectManager.getScopeFunctionsForFile(frame.filePath as string);
for (let scopeFunction of scopeFunctions) {
if (!completions.has(`${scopeFunction.completionItemKind}-${scopeFunction.name.toLocaleLowerCase()}`)) {
completions.set(`${scopeFunction.completionItemKind}-${scopeFunction.name.toLocaleLowerCase()}`, {
label: scopeFunction.name,
type: scopeFunction.completionItemKind,
sortText: '000000'
});

try {
let scopeFunctions = await this.projectManager.getScopeFunctionsForFile(frame.filePath as string);
for (let scopeFunction of scopeFunctions) {
if (!completions.has(`${scopeFunction.completionItemKind}-${scopeFunction.name.toLocaleLowerCase()}`)) {
completions.set(`${scopeFunction.completionItemKind}-${scopeFunction.name.toLocaleLowerCase()}`, {
label: scopeFunction.name,
type: scopeFunction.completionItemKind,
sortText: '000000'
});
}
}
} catch (e) {
this.logger.warn('Could not build list of scope functions for file', e);
}
}
}
Expand Down
23 changes: 19 additions & 4 deletions src/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import type { Response } from 'request';
import type * as requestType from 'request';
import { OutputEvent } from '@vscode/debugadapter';
import * as xml2js from 'xml2js';
import { isPromise } from 'util/types';
const request = r as typeof requestType;

class Util {
Expand Down Expand Up @@ -499,12 +500,26 @@ class Util {
* @param disposables a list of functions or disposables
*/
public applyDispose(disposables: DisposableLike[]) {
let promises = [];
let exceptions = [];

disposables ??= [];
for (const disposable of disposables) {
if (typeof disposable === 'function') {
disposable();
} else {
disposable?.dispose?.();
let value: any;
try {
if (typeof disposable === 'function') {
value = disposable();
} else {
value = disposable?.dispose?.();
}
} catch (e) {
exceptions.push(e);
}
//if this value is a promise, add a .catch to it so we don't bring down the app
if (isPromise(value)) {
value.catch(e => {
console.error('Unhandled promise during dispose', e);
});
}
}
//empty the array
Expand Down