• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

rokucommunity / brighterscript / #14843

31 Oct 2025 01:40PM UTC coverage: 89.023% (+0.09%) from 88.935%
#14843

push

web-flow
Fix crash when bsc plugin in worker loads another version of bsc (#1579)

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

7815 of 9259 branches covered (84.4%)

Branch coverage included in aggregate %.

8 of 9 new or added lines in 1 file covered. (88.89%)

1 existing line in 1 file now uncovered.

10003 of 10756 relevant lines covered (93.0%)

1890.99 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

77.95
/src/lsp/worker/WorkerThreadProject.ts
1
import * as EventEmitter from 'eventemitter3';
1✔
2
import { Worker } from 'worker_threads';
1✔
3
import type { WorkerMessage } from './MessageHandler';
4
import { MessageHandler } from './MessageHandler';
1✔
5
import util from '../../util';
1✔
6
import type { LspDiagnostic, ActivateResponse, ProjectConfig } from '../LspProject';
7
import { type LspProject } from '../LspProject';
8
import { WorkerPool } from './WorkerPool';
1✔
9
import type { Hover, MaybePromise, SemanticToken } from '../../interfaces';
10
import type { DocumentAction, DocumentActionWithStatus } from '../DocumentManager';
11
import { Deferred } from '../../deferred';
1✔
12
import type { FileTranspileResult, SignatureInfoObj } from '../../Program';
13
import type { Position, Range, Location, DocumentSymbol, WorkspaceSymbol, CodeAction, CompletionList } from 'vscode-languageserver-protocol';
14
import type { Logger } from '../../logging';
15
import { createLogger } from '../../logging';
1✔
16
import * as fsExtra from 'fs-extra';
1✔
17
import * as path from 'path';
1✔
18
import { standardizePath as s } from '../../util';
1✔
19

20

21
export const workerPool = new WorkerPool(() => {
1✔
22
    //construct the path to the `./run.ts` (or `./run.js`) script in this same directory
23
    const runScriptPath = s`${__dirname}/run${path.extname(__filename)}`;
1✔
24

25
    // Prepare execArgv for debugging support
26
    const execArgv: string[] = [];
1✔
27

28
    // Add ts-node if we're running TypeScript
29
    if (/\.ts$/i.test(runScriptPath)) {
1!
30
        execArgv.push('--require', 'ts-node/register');
1✔
31
    }
32

33
    // Enable debugging for worker threads if the main process is being debugged
34
    // Check if debugging is enabled via execArgv or environment variables
35
    const isDebugging = process.execArgv.some(arg => arg.startsWith('--inspect')) || process.env.NODE_OPTIONS?.includes('--inspect');
1!
36

37
    if (isDebugging) {
1!
38
        // Node.js will automatically assign a unique port for each worker when using --inspect=0
39
        // This allows VSCode to automatically attach to worker threads
NEW
40
        execArgv.push('--inspect=0');
×
41
    }
42

43
    return new Worker(
1✔
44
        runScriptPath,
45
        {
46
            execArgv: execArgv.length > 0 ? execArgv : undefined
1!
47
        }
48
    );
49
});
50

51
export class WorkerThreadProject implements LspProject {
1✔
52
    public constructor(
53
        options?: {
54
            logger?: Logger;
55
        }
56
    ) {
57
        this.logger = options?.logger ?? createLogger();
4✔
58
    }
59

60
    public async activate(options: ProjectConfig) {
61
        this.activateOptions = options;
4✔
62
        this.bsconfigPath = options.bsconfigPath ? util.standardizePath(options.bsconfigPath) : options.bsconfigPath;
4✔
63
        this.projectDir = options.projectDir ? util.standardizePath(options.projectDir) : options.projectDir;
4✔
64
        this.projectKey = options.projectKey ? util.standardizePath(options.projectKey) : options.bsconfigPath ?? options.projectDir;
4!
65
        this.workspaceFolder = options.workspaceFolder ? util.standardizePath(options.workspaceFolder) : options.workspaceFolder;
4✔
66
        this.projectNumber = options.projectNumber;
4✔
67

68
        // start a new worker thread or get an unused existing thread
69
        this.worker = workerPool.getWorker();
4✔
70
        this.messageHandler = new MessageHandler<LspProject>({
4✔
71
            name: 'MainThread',
72
            port: this.worker,
73
            onRequest: this.processRequest.bind(this),
74
            onUpdate: this.processUpdate.bind(this)
75
        });
76
        this.disposables.push(this.messageHandler);
4✔
77

78
        const activateResponse = await this.messageHandler.sendRequest<ActivateResponse>('activate', { data: [options] });
4✔
79
        this.bsconfigPath = activateResponse.data.bsconfigPath;
4✔
80
        this.rootDir = activateResponse.data.rootDir;
4✔
81
        this.filePatterns = activateResponse.data.filePatterns;
4✔
82
        this.logger.logLevel = activateResponse.data.logLevel;
4✔
83

84
        //load the bsconfig file contents (used for performance optimizations externally)
85
        try {
4✔
86
            this.bsconfigFileContents = (await fsExtra.readFile(this.bsconfigPath)).toString();
4✔
87
        } catch { }
88

89

90
        this.activationDeferred.resolve();
4✔
91
        return activateResponse.data;
4✔
92
    }
93

94
    public logger: Logger;
95

96
    public isStandaloneProject = false;
4✔
97

98
    private activationDeferred = new Deferred();
4✔
99

100
    /**
101
     * Options used to activate this project
102
     */
103
    public activateOptions: ProjectConfig;
104

105
    /**
106
     * The root directory of the project
107
     */
108
    public rootDir: string;
109

110
    /**
111
     * The file patterns from bsconfig.json that were used to find all files for this project
112
     */
113
    public filePatterns: string[];
114

115
    /**
116
     * Path to a bsconfig.json file that will be used for this project
117
     */
118
    public bsconfigPath?: string;
119

120
    /**
121
     * The contents of the bsconfig.json file. This is used to detect when the bsconfig file has not actually been changed (even if the fs says it did).
122
     *
123
     * Only available after `.activate()` has completed.
124
     * @deprecated do not depend on this property. This will certainly be removed in a future release
125
     */
126
    bsconfigFileContents?: string;
127

128
    /**
129
     * The worker thread where the actual project will execute
130
     */
131
    private worker: Worker;
132

133
    /**
134
     * Path to the project. For directory-only projects, this is the path to the dir. For bsconfig.json projects, this is the path to the config.
135
     */
136
    projectKey: string;
137
    /**
138
     * The directory for the root of this project (typically where the bsconfig.json or manifest is located)
139
     */
140
    projectDir: string;
141

142
    /**
143
     * A unique number for this project, generated during this current language server session. Mostly used so we can identify which project is doing logging
144
     */
145
    public projectNumber: number;
146

147
    /**
148
     * The path to the workspace where this project resides. A workspace can have multiple projects (by adding a bsconfig.json to each folder).
149
     * Defaults to `.projectPath` if not set
150
     */
151
    public workspaceFolder: string;
152

153
    /**
154
     * Promise that resolves when the project finishes activating
155
     * @returns a promise that resolves when the project finishes activating
156
     */
157
    public whenActivated() {
158
        return this.activationDeferred.promise;
1✔
159
    }
160

161

162
    /**
163
     * Validate the project. This will trigger a full validation on any scopes that were changed since the last validation,
164
     * and will also eventually emit a new 'diagnostics' event that includes all diagnostics for the project
165
     */
166
    public async validate() {
167
        const response = await this.messageHandler.sendRequest<void>('validate');
×
168
        return response.data;
×
169
    }
170

171
    /**
172
     * Cancel any active validation that's running
173
     */
174
    public async cancelValidate() {
175
        const response = await this.messageHandler.sendRequest<void>('cancelValidate');
×
176
        return response.data;
×
177
    }
178

179
    public async getDiagnostics() {
180
        const response = await this.messageHandler.sendRequest<LspDiagnostic[]>('getDiagnostics');
1✔
181
        return response.data;
1✔
182
    }
183

184
    /**
185
     * Apply a series of file changes to the project. This is safe to call any time. Changes will be queued and flushed at the correct times
186
     * during the program's lifecycle flow
187
     */
188
    public async applyFileChanges(documentActions: DocumentAction[]): Promise<DocumentActionWithStatus[]> {
189
        const response = await this.messageHandler.sendRequest<DocumentActionWithStatus[]>('applyFileChanges', {
×
190
            data: [documentActions]
191
        });
192
        return response.data;
×
193
    }
194

195
    /**
196
     * Send a request with the standard structure
197
     * @param name the name of the request
198
     * @param data the array of data to send
199
     * @returns the response from the request
200
     */
201
    private async sendStandardRequest<T>(name: string, ...data: any[]) {
202
        const response = await this.messageHandler.sendRequest<T>(name as any, {
1✔
203
            data: data
204
        });
205
        return response.data;
×
206
    }
207

208
    /**
209
     * Get the full list of semantic tokens for the given file path
210
     */
211
    public async getSemanticTokens(options: { srcPath: string }) {
212
        return this.sendStandardRequest<SemanticToken[]>('getSemanticTokens', options);
×
213
    }
214

215
    public async transpileFile(options: { srcPath: string }) {
216
        return this.sendStandardRequest<FileTranspileResult>('transpileFile', options);
×
217
    }
218

219
    public async getHover(options: { srcPath: string; position: Position }): Promise<Hover[]> {
220
        return this.sendStandardRequest<Hover[]>('getHover', options);
×
221
    }
222

223
    public async getDefinition(options: { srcPath: string; position: Position }): Promise<Location[]> {
224
        return this.sendStandardRequest<Location[]>('getDefinition', options);
×
225
    }
226

227
    public async getSignatureHelp(options: { srcPath: string; position: Position }): Promise<SignatureInfoObj[]> {
228
        return this.sendStandardRequest<SignatureInfoObj[]>('getSignatureHelp', options);
×
229
    }
230

231
    public async getDocumentSymbol(options: { srcPath: string }): Promise<DocumentSymbol[]> {
232
        return this.sendStandardRequest<DocumentSymbol[]>('getDocumentSymbol', options);
×
233
    }
234

235
    public async getWorkspaceSymbol(): Promise<WorkspaceSymbol[]> {
236
        return this.sendStandardRequest<WorkspaceSymbol[]>('getWorkspaceSymbol');
1✔
237
    }
238

239
    public async getReferences(options: { srcPath: string; position: Position }): Promise<Location[]> {
240
        return this.sendStandardRequest<Location[]>('getReferences', options);
×
241
    }
242

243
    public async getCodeActions(options: { srcPath: string; range: Range }): Promise<CodeAction[]> {
244
        return this.sendStandardRequest<CodeAction[]>('getCodeActions', options);
×
245
    }
246

247
    public async getCompletions(options: { srcPath: string; position: Position }): Promise<CompletionList> {
248
        return this.sendStandardRequest<CompletionList>('getCompletions', options);
×
249
    }
250

251
    /**
252
     * Handles request/response/update messages from the worker thread
253
     */
254
    private messageHandler: MessageHandler<LspProject>;
255

256
    private processRequest(request: WorkerMessage) {
257

258
    }
259

260
    private processUpdate(update: WorkerMessage) {
261
        //for now, all updates are treated like "events"
262
        this.emit(update.name as any, update.data);
12✔
263
    }
264

265
    public on(eventName: 'critical-failure', handler: (data: { message: string }) => void);
266
    public on(eventName: 'diagnostics', handler: (data: { diagnostics: LspDiagnostic[] }) => MaybePromise<void>);
267
    public on(eventName: 'all', handler: (eventName: string, data: any) => MaybePromise<void>);
268
    public on(eventName: string, handler: (...args: any[]) => MaybePromise<void>) {
269
        this.emitter.on(eventName, handler as any);
2✔
270
        return () => {
2✔
271
            this.emitter.removeListener(eventName, handler as any);
×
272
        };
273
    }
274

275
    private emit(eventName: 'critical-failure', data: { message: string });
276
    private emit(eventName: 'diagnostics', data: { diagnostics: LspDiagnostic[] });
277
    private async emit(eventName: string, data?) {
278
        //emit these events on next tick, otherwise they will be processed immediately which could cause issues
279
        await util.sleep(0);
12✔
280
        this.emitter.emit(eventName, data);
12✔
281
        //emit the 'all' event
282
        this.emitter.emit('all', eventName, data);
12✔
283
    }
284
    private emitter = new EventEmitter();
4✔
285

286
    public disposables: LspProject['disposables'] = [];
4✔
287

288
    public dispose() {
289
        for (let disposable of this.disposables ?? []) {
4!
290
            disposable?.dispose?.();
6!
291
        }
292
        this.disposables = [];
4✔
293

294
        //move the worker back to the pool so it can be used again
295
        if (this.worker) {
4!
296
            workerPool.releaseWorker(this.worker);
4✔
297
        }
298
        this.emitter?.removeAllListeners();
4!
299
    }
300
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc