Skip to content
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
2 changes: 2 additions & 0 deletions src/json-rpc/csolution-rpc-client.factory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ export const csolutionServiceFactory = makeFactory<
listMissingPacks: () => jest.fn(),
getCsolutionBin: () => jest.fn(),
waitForExit: () => jest.fn(),
suspendPackIdxWatcher: () => jest.fn(),
resumePackIdxWatcher: () => jest.fn(),
selectPack: () => jest.fn(),
getVariables: () => jest.fn(),
});
39 changes: 35 additions & 4 deletions src/json-rpc/csolution-rpc-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ export interface CsolutionService extends RpcInterface {
activate(context: Pick<vscode.ExtensionContext, 'subscriptions'>): Promise<void>;
getCsolutionBin(): string;
waitForExit(): Promise<void>;
suspendPackIdxWatcher(): void;
resumePackIdxWatcher(skipPendingReload?: boolean): void;
}

class CsolutionServiceImpl extends RpcMethods implements CsolutionService {
Expand All @@ -40,10 +42,12 @@ class CsolutionServiceImpl extends RpcMethods implements CsolutionService {
private child: ChildProcess | undefined;
private connection: MessageConnection | undefined;
private idxWatcher: Optional<fs.FSWatcher> = undefined;
private readonly debouncedLoadPacks = debounce(super.loadPacks.bind(this), 1000);
private readonly debouncedLoadPacks = debounce(this.reloadPacks.bind(this), 1000);
private csolutionBin = 'csolution';
private exitPromise: Promise<void> | undefined;
private cachedVersion: GetVersionResult = { success: false };
private packIdxWatcherSuspendDepth = 0;
private pendingPackIdxChange = false;
private readonly mutex: Mutex;

constructor(
Expand All @@ -57,9 +61,9 @@ class CsolutionServiceImpl extends RpcMethods implements CsolutionService {
public async activate(context: vscode.ExtensionContext) {
context.subscriptions.push(
this,
this.commandsProvider.registerCommand(CsolutionServiceImpl.reloadPacksCommandId, this.loadPacks, this),
this.commandsProvider.registerCommand(CsolutionServiceImpl.reloadPacksCommandId, this.reloadPacks, this),
);
this.loadPacks();
this.loadPacks(); // direct load without notification
Comment thread
edriouk marked this conversation as resolved.
}

public async dispose() {
Expand Down Expand Up @@ -87,10 +91,16 @@ class CsolutionServiceImpl extends RpcMethods implements CsolutionService {
}
// ensure version is cached
await this.getVersion();

return super.loadPacks();
}


private async reloadPacks() {
const result = await this.loadPacks();
void this.commandsProvider.executeCommand(manifest.REFRESH_COMMAND_ID);
Comment thread
edriouk marked this conversation as resolved.
return result;
}

public getCsolutionBin(): string {
return this.csolutionBin;
}
Expand All @@ -99,6 +109,23 @@ class CsolutionServiceImpl extends RpcMethods implements CsolutionService {
return this.exitPromise ?? Promise.resolve();
}

public suspendPackIdxWatcher(): void {
this.packIdxWatcherSuspendDepth++;
}

public resumePackIdxWatcher(skipPendingReload: boolean = false): void {
if (this.packIdxWatcherSuspendDepth > 0) {
this.packIdxWatcherSuspendDepth--;
}
if (skipPendingReload) {
this.pendingPackIdxChange = false;
}
if (this.packIdxWatcherSuspendDepth === 0 && this.pendingPackIdxChange) {
this.pendingPackIdxChange = false;
this.debouncedLoadPacks();
}
}

private watchPackIdxFile() {
this.idxWatcher?.close();
const pack_idx = path.join(getCmsisPackRoot(), 'pack.idx');
Expand All @@ -108,6 +135,10 @@ class CsolutionServiceImpl extends RpcMethods implements CsolutionService {
const stat = fs.statSync(pack_idx);
if (stat?.mtimeMs !== mtimeMs) {
mtimeMs = stat.mtimeMs;
if (this.packIdxWatcherSuspendDepth > 0) {
this.pendingPackIdxChange = true;
return;
}
this.debouncedLoadPacks();
}
}
Expand Down
100 changes: 98 additions & 2 deletions src/json-rpc/csolution-rpc-helper.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { CsolutionService } from './csolution-rpc-client';
import { splitBoardId, splitDeviceId, splitPackId } from './csolution-rpc-helper';
import { VcpkgManager } from '../vcpkg/vcpkg-manager';
import { Environment, EnvironmentManager } from '../desktop/env-manager';
import * as manifest from '../manifest';

jest.mock('node:child_process', () => ({
...jest.requireActual('node:child_process'),
Expand All @@ -28,12 +29,16 @@ jest.mock('vscode-jsonrpc/node', () => ({
describe('csolution-rpc-client', () => {
type AnyService = Record<string, any>;

function createService(envManager?: EnvironmentManager): AnyService {
function createService(envManager?: EnvironmentManager, commandsProvider?: AnyService): AnyService {
// `constructor(...)` wrapper shape is repo-specific; treat as constructible in tests.
const mockEnvManager = envManager ?? ({
augmentEnv: jest.fn().mockReturnValue(new Environment({}))
} as unknown as EnvironmentManager);
return new (CsolutionService as unknown as new (em: EnvironmentManager) => AnyService)(mockEnvManager);
const mockCommandsProvider = commandsProvider ?? ({
executeCommand: jest.fn().mockResolvedValue(undefined),
registerCommand: jest.fn(),
});
return new (CsolutionService as unknown as new (em: EnvironmentManager, cp: AnyService) => AnyService)(mockEnvManager, mockCommandsProvider);
}

beforeEach(() => {
Expand Down Expand Up @@ -264,6 +269,31 @@ describe('csolution-rpc-client', () => {
expect(transceiveSpy).toHaveBeenCalledWith('GetVersion', undefined);
expect(transceiveSpy).toHaveBeenCalledWith('LoadPacks', undefined);
expect(sequence.indexOf('GetVersion')).toBeLessThan(sequence.indexOf('LoadPacks'));
expect(service.commandsProvider.executeCommand).not.toHaveBeenCalled();
});

it('does not await refresh command execution in reloadPacks', async () => {
const neverResolves = new Promise<void>(() => undefined);
service = createService(undefined, {
executeCommand: jest.fn().mockReturnValue(neverResolves),
registerCommand: jest.fn(),
});
service.idxWatcher = { close: jest.fn() };
jest.spyOn(service as any, 'transceive').mockImplementation(async (...args: unknown[]) => {
const method = String(args[0]);
if (method === 'GetVersion') {
return { success: true, version: '1.2.3', apiVersion: '0.0.9' };
}
if (method === 'LoadPacks') {
return { success: true };
}
return { success: false };
});

const result = await service.reloadPacks();

expect(result).toEqual({ success: true });
expect(service.commandsProvider.executeCommand).toHaveBeenCalledWith(manifest.REFRESH_COMMAND_ID);
});
});

Expand Down Expand Up @@ -381,6 +411,72 @@ describe('csolution-rpc-client', () => {
expect(prevClose).toHaveBeenCalledTimes(1);
expect(fs.watch).toHaveBeenCalledTimes(1);
});

it('suppresses pack.idx-triggered reload while suspended and runs once after resume', () => {
let watcherCallback: ((eventType: string) => void) | undefined;

(fs.statSync as unknown as jest.Mock)
.mockReturnValueOnce({ mtimeMs: 40 }) // initial
.mockReturnValueOnce({ mtimeMs: 41 }); // changed

(fs.watch as unknown as jest.Mock).mockImplementation((_file: string, listener: any) => {
watcherCallback = listener;
return { close: jest.fn() };
});

service.watchPackIdxFile();
service.suspendPackIdxWatcher();

watcherCallback!('change');
expect(service.debouncedLoadPacks).not.toHaveBeenCalled();

service.resumePackIdxWatcher();
expect(service.debouncedLoadPacks).toHaveBeenCalledTimes(1);
});

it('can resume without running pending reload when skipPendingReload is true', () => {
let watcherCallback: ((eventType: string) => void) | undefined;

(fs.statSync as unknown as jest.Mock)
.mockReturnValueOnce({ mtimeMs: 50 }) // initial
.mockReturnValueOnce({ mtimeMs: 51 }); // changed

(fs.watch as unknown as jest.Mock).mockImplementation((_file: string, listener: any) => {
watcherCallback = listener;
return { close: jest.fn() };
});

service.watchPackIdxFile();
service.suspendPackIdxWatcher();

watcherCallback!('change');
expect(service.debouncedLoadPacks).not.toHaveBeenCalled();

service.resumePackIdxWatcher(true);
expect(service.debouncedLoadPacks).not.toHaveBeenCalled();
});

it('triggers pending reload when resuming with depth already zero', () => {
service.pendingPackIdxChange = true;
service.packIdxWatcherSuspendDepth = 0;

service.resumePackIdxWatcher();

expect(service.debouncedLoadPacks).toHaveBeenCalledTimes(1);
expect(service.pendingPackIdxChange).toBe(false);
expect(service.packIdxWatcherSuspendDepth).toBe(0);
});

it('is a no-op when resuming with depth zero and no pending change', () => {
service.pendingPackIdxChange = false;
service.packIdxWatcherSuspendDepth = 0;

service.resumePackIdxWatcher();

expect(service.debouncedLoadPacks).not.toHaveBeenCalled();
expect(service.pendingPackIdxChange).toBe(false);
expect(service.packIdxWatcherSuspendDepth).toBe(0);
});
});

describe('csolution-rpc-client launch', () => {
Expand Down
2 changes: 2 additions & 0 deletions src/solutions/cmsis-toolbox.factories.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ export const cmsisToolboxManagerFactory = (): MockCmsisToolboxManager => {
runCmsisTool: jest.fn().mockResolvedValue([0, undefined]),
runCsolutionRpc: jest.fn().mockResolvedValue({ success: true }),
isRunning: jest.fn(),
suspendPackReload: jest.fn(),
resumePackReload: jest.fn(),
collectSetupMessages: jest.fn(),
getSetupMessages: jest.fn(),
onRunCmsisTool: onRunCmsisToolEventEmitter.event,
Expand Down
12 changes: 12 additions & 0 deletions src/solutions/cmsis-toolbox.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,10 @@ export interface CmsisToolboxManager {
* @return true if the queue is not empty or mutex is locked
*/
isRunning(): boolean

suspendPackReload(): void

resumePackReload(skipPendingReload?: boolean): void
}

/**
Expand Down Expand Up @@ -126,6 +130,14 @@ export class CmsisToolboxManagerImpl implements CmsisToolboxManager {
return this.toolboxQueue.length > 0 || this.toolboxMutex.isLocked();
}

public suspendPackReload(): void {
this.csolutionService.suspendPackIdxWatcher();
}

public resumePackReload(skipPendingReload: boolean = false): void {
this.csolutionService.resumePackIdxWatcher(skipPendingReload);
}

public async runCmsisTool(
tool: ToolboxTool,
args: string[],
Expand Down
6 changes: 6 additions & 0 deletions src/solutions/solution-converter.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -266,6 +266,12 @@ describe('SolutionConverter', () => {
expect(mockRunCmsisTool).toHaveBeenNthCalledWith(2, 'cpackget',
expect.arrayContaining(['add', 'VendorB::PackB@2.0.0']), expect.any(Function), undefined, undefined, true);

expect(mockCsolutionService.suspendPackIdxWatcher).toHaveBeenCalledTimes(1);
expect(mockCsolutionService.resumePackIdxWatcher).toHaveBeenCalledTimes(1);
expect(mockCsolutionService.loadPacks).toHaveBeenCalledTimes(1);
expect(mockCsolutionService.loadPacks.mock.invocationCallOrder[0])
.toBeLessThan(mockCsolutionService.convertSolution.mock.invocationCallOrder[0]);

expect(completedListener).toHaveBeenCalledTimes(1);
});

Expand Down
19 changes: 16 additions & 3 deletions src/solutions/solution-converter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,7 @@ export class SolutionConverterImpl implements SolutionConverter {

outputChannel.appendLine('\n⚙️ Converting solution...');
let missingPacksResult = undefined;
let downloadedMissingPacks = false;
if (this.isDownloadPacksEnabled()) {
// rpc method: ListMissingPacks
outputChannel.append('Check for missing packs... ');
Expand All @@ -127,15 +128,27 @@ export class SolutionConverterImpl implements SolutionConverter {
) as rpc.ListMissingPacksResult;
if (missingPacksResult.success && missingPacksResult.packs && missingPacksResult.packs.length > 0) {
// download missing packs if any
const downloadPacksOutput = await this.downloadMissingPacks(missingPacksResult.packs);
toolsOutputMessages = toolsOutputMessages.concat(downloadPacksOutput);
missingPacksResult.success = downloadPacksOutput.length === 0;
this.cmsisToolboxManager.suspendPackReload();
try {
const downloadPacksOutput = await this.downloadMissingPacks(missingPacksResult.packs);
toolsOutputMessages = toolsOutputMessages.concat(downloadPacksOutput);
missingPacksResult.success = downloadPacksOutput.length === 0;
downloadedMissingPacks = missingPacksResult.success;
} finally {
// Converter explicitly calls LoadPacks below on successful downloads.
this.cmsisToolboxManager.resumePackReload(true);
}
}
}
if (signal.aborted) {
return;
}

if (downloadedMissingPacks) {
outputChannel.append('Loading packs... ');
await this.cmsisToolboxManager.runCsolutionRpc('LoadPacks', {});
Comment thread
edriouk marked this conversation as resolved.
}
Comment thread
edriouk marked this conversation as resolved.

let detection = false;
let convertResult: rpc.ConvertSolutionResult = { success: false };
let availableCompilers: string[] = [];
Expand Down
Loading