Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to write async code (promises?) with vscode api: withProgress

I'm pretty sorry about the title, I've got no idea how to better describe my problem.

I would like to implement the withProgress API of VSCode, to be able to show progressbar while my code is runnig/progressing. Documentation is here: https://code.visualstudio.com/api/extension-capabilities/common-capabilities#progress-api

I've tried to implement it:

vscode.window.withProgress({
    location: vscode.ProgressLocation.Notification,
    title: "I am long running!",
}, (progress, token) => {
    return new Promise(resolve => {
        const output = executeProcess('sleep 5');
        resolve();
    });
});

executeProcess(...) is a wrapper around npm child_process.spawnSync. I need it to be synchronous, since I'd like to read the stdout of it.

So, my problem is that it's currently running the executeProcess and when it finished, then it starts showing the progressbar. How could I write it the way that it would start showing the progressbar first, meanwhile it's running and doing its job in the background?

Is it possible without the needing of restructuring my code to use to use child_process.spawn with a callback?

like image 777
original.roland Avatar asked Nov 08 '19 08:11

original.roland


People also ask

What is Uri VS Code?

@:jsRequire("vscode","Uri") A universal resource identifier representing either a file on disk or another resource, like untitled resources.

How do you use VS Code commands?

The most important key combination to know is Ctrl+Shift+P, which brings up the Command Palette. From here, you have access to all of the functionality of VS Code, including keyboard shortcuts for the most common operations. The Command Palette provides access to many commands.


1 Answers

Yes, I think you have to change your code to use the async pattern, because otherwise you are blocking the UI thread that you want render the progress dialog meanwhile instead.

Here is the difference between using spawn and spawnSync:

vscode.window.withProgress calling spawn vs spawnSync

The example of how to read from the child process output in the async approach is here:

        let childProcess = spawn(someProcessToSpawn)
            .on("close", (code, signal) => {
                console.log(`Closed: ${code} ${signal}`);
                if (childProcess.killed) { console.log('KILLED'); }
                resolve();
                clearInterval(interval);
            })
            .on("error", err => {
                reject(err);
            });

        childProcess.stdout
            .on("data", (chunk: string | Buffer) => {
                // YOUR CODE GOES HERE
                console.log(`stdout: ${chunk}`);
                progressUpdate = chunk.toString('utf8', 0, 50).replace(/[\r\n]/g, '');
            });

If you want to run the whole example, clone the progress-sample, run npm install and replace the extension.ts content with this code:

'use strict';

import { ExtensionContext, window, commands, ProgressLocation, CancellationToken, Progress } from 'vscode';
import { spawn, spawnSync } from 'child_process';

export function activate(context: ExtensionContext) {
    context.subscriptions.push(commands.registerCommand('extension.startTask', async () => {
        let mode = await window.showQuickPick(['sync', 'async'], { placeHolder: 'Pick mode...' });
        window.withProgress({
            location: ProgressLocation.Notification,
            title: "I am long running",
            cancellable: true
        }, async (progress, token) => {
            token.onCancellationRequested(() => {
                console.log("User canceled the long running operation");
            });

            switch (mode) {
                case undefined:
                    return; // canceled by the user
                case 'sync':
                    return spawnSomethingSync(token);
                case 'async':
                default:
                    return spawnSomethingAsync(progress, token);
            }
        });
    }));
}

/**
 * Synchronous approach
 * @param _token cancellation token (unused in the sync approach)
 */
function spawnSomethingSync(_token: CancellationToken): Promise<void> {
    return new Promise(resolve => {
        console.log('Started...');
        let child = spawnSync('cmd', ['/c', 'dir', '/S'], { cwd: 'c:\\', encoding: 'utf8' });
        console.log(`stdout: ${child.stdout.slice(0, 1000)}`); // otherwise it is too big for the console
        resolve();
    });
}

/**
 * Asynchronous approach
 * @param token cancellation token (triggered by the cancel button on the UI)
 */
function spawnSomethingAsync(progress: Progress<{ message?: string; increment?: number }>, token: CancellationToken): Promise<void> {
    return new Promise<void>((resolve, reject) => {
        if (token.isCancellationRequested) {
            return;
        }

        var progressUpdate = 'Starting up...';
        const interval = setInterval(() => progress.report({ message: progressUpdate }), 500);

        let childProcess = spawn('cmd', ['/c', 'dir', '/S'], { cwd: 'c:\\' })
            .on("close", (code, signal) => {
                console.log(`Closed: ${code} ${signal}`);
                if (childProcess.killed) { console.log('KILLED'); }
                resolve();
                clearInterval(interval);
            })
            .on("error", err => {
                reject(err);
            });

        childProcess.stdout
            .on("data", (chunk: string | Buffer) => {
                // YOUR CODE GOES HERE
                console.log(`stdout: ${chunk}`);
                progressUpdate = chunk.toString('utf8', 0, 50).replace(/[\r\n]/g, '');
            });

        token.onCancellationRequested(_ => childProcess.kill());
    });
}

If you are not on Windows, just replace the dir /S and c:\\ with some other appropriate long running command.

The other benefit of using the async approach is that the Cancel button can be easily hooked up to kill the spawned process:

token.onCancellationRequested(_ => childProcess.kill());

... and you also get a chance to show progress by updating the progress object.

like image 51
Jan Dolejsi Avatar answered Oct 01 '22 01:10

Jan Dolejsi