51

I am trying to create a simple VSCode extension to run a set of commands when I open a folder. Basically these commands will set up our development environment. I have started off creating the boilerplace and ran through the example that VSCode provided but I am not clear how to run system commands. Appreciate any help or point me to some documentation about this topic.

4 Answers 4

44

Your extension environment has access to node.js libraries, so you can just use child_process or any helper libraries to execute commands:

const cp = require('child_process')
cp.exec('pwd', (err, stdout, stderr) => {
    console.log('stdout: ' + stdout);
    console.log('stderr: ' + stderr);
    if (err) {
        console.log('error: ' + err);
    }
});
Sign up to request clarification or add additional context in comments.

7 Comments

This is the way we should do such a thing? There is no VSCode graceful API for that? like what if the user closes VSCode abruptly? This means that maybe my extension external process will run forever?
No, it's a child_process, in which case when the VSCode process terminates, so will any child process.
@MatBee Sadly, that is not how child processes work. See: stackoverflow.com/questions/8533377/…
@MattBierner It becomes a mess that ends up being very non-cross-plattform, as you can read in the documentation you linked yourself, quoting: "On non-Windows platforms, [...] Child processes may continue running after the parent exits regardless of whether they are detached or not."
|
32

One alternative could be to use the Terminal API which is the best option if you have the need for the process to be fully observable and controllable to/by the user.

Biggest downside: The Terminal API does not yet offer a way to introspect the processes that run inside of it. As of 9/2024, the Terminal API has some events that you can hook into to observe what is going on inside the Terminal.

The most reliable way to run an observable process in the terminal, is a two-layer approach, where you start a wrapper process that in turn launches and observes the actual process (taken in via command line args).

Our self-made TerminalWrapper

We tried this ourselves.

  • In our first approach, the wrapper used a socket.io connection that allows for communication with and control by the extension.

  • In our second approach, we simplified and instead created the terminal using bash -c (non-interactive shell), and used a file watcher to get the results instead. Easier this way but after the process is done, the user won't be able to use the Terminal window (because its non-interactive). A lot less error-prone and does not require fulfilling socket.io dependency.

Implementation Details

  1. In our extension, we use a TerminalWrapper which runs the command inside a wrapper process, and waits for a file to contain the results.
  2. The wrapper process is here. It writes the result to a file.
  3. Usage example here:
const cwd = '.';
const command = `node -e "console.log('hi!');"`;

const { code } = await TerminalWrapper.execInTerminal(cwd, command, {}).waitForResult();
if (code) {
  const processExecMsg = `${cwd}$ ${command}`;
  throw new Error(`Process failed with exit code ${code} (${processExecMsg})`);
}

Biggest downside with the second approach is that we now need bash to be present, however (i) we do have a dependency checker that will warn you if you don't and explain how to get it, and (ii) using a unified shell, makes running commands a lot easier, as we now have a pretty strong unified feature set, we know we can rely on, rather than only being able to use common command execution syntax, and (iii) we can even run *.sh files without having to worry.

Introducing: VSCode Terminal API

All of the following imagery and excerpts are just straight up copy-and-paste'd from their official sample repository:

Create a terminal and run a command in it

context.subscriptions.push(vscode.commands.registerCommand('terminalTest.createAndSend', () => {
    const terminal = vscode.window.createTerminal(`Ext Terminal #${NEXT_TERM_ID++}`);
    terminal.sendText("echo 'Sent text immediately after creating'");
}));

Terminal activation event

vscode.window.onDidChangeActiveTerminal(e => {
    console.log(`Active terminal changed, name=${e ? e.name : 'undefined'}`);
});

TerminalQuickPickItem

function selectTerminal(): Thenable<vscode.Terminal | undefined> {
    interface TerminalQuickPickItem extends vscode.QuickPickItem {
        terminal: vscode.Terminal;
    }
    const terminals = <vscode.Terminal[]>(<any>vscode.window).terminals;
    const items: TerminalQuickPickItem[] = terminals.map(t => {
        return {
            label: `name: ${t.name}`,
            terminal: t
        };
    });
    return vscode.window.showQuickPick(items).then(item => {
        return item ? item.terminal : undefined;
    });
}

...and a lot more!...

Terminal API Sample Commands

(<3 for the VSCode team and their hard work.)

2 Comments

is there a way to know if running terminals are busy or not? Thanks
The Terminal API is currently very limited. It does not allow you query anything about its state, other than whether or not it is open or not, its PID, dimensions and other things you see in the screenshot above. You can even fetch the output using the onDidWriteTerminalData event (however that will probably never be part of the stable API as discussed here). Sadly there is no way to know if or what is currently running inside of a terminal, unless you wrap your command in an observer application as I propopsed above.
17

What I did was to create a promise based utility function to run all shell command with child_process

import * as cp from "child_process";

const execShell = (cmd: string) =>
    new Promise<string>((resolve, reject) => {
        cp.exec(cmd, (err, out) => {
            if (err) {
                return reject(err);
            }
            return resolve(out);
        });
    });

To get current directory

const currentDir = await execShell('pwd');

To get current git branch name

const branchName = await execShell('git rev-parse --abbrev-ref HEAD');

1 Comment

Wow, this is really complete. Thanks, it helped a lot.
2

In my case, it was mandatory that I execute my commands within the user terminal. But I wanted to get notified on command success or failure. It is less generic but gives me more control.

import * as vscode from 'vscode';
import * as fs from 'fs';

async function executeAndRead(command: string): Promise<string> {
    ensureTerminalExists();
    const terminal = await selectTerminal();
    if (terminal) {
        terminal.sendText(command + ` > ${outputFile} || pwd > ${triggerFile}`);
        return waitForFileUpdate(outputFile, triggerFile);
    }
    return Promise.reject('Could not select terminal');
}

async function waitForFileUpdate(outputFile: string, triggerFile: string): Promise<string> {
    return new Promise<string>((resolve, reject) => {
        const watcher = fs.watch(triggerFile);
        watcher.on('change', () => {
            watcher.close();
            fs.readFile(outputFile, 'utf8', (err, data) => {
                if (err) {
                    reject(err);
                } else {
                    resolve(data);
                }
            });
        });
        watcher.on('error', reject);
    });
}

The idea is that I'm modifying two files in sequence. The second is mostly a dummy. But I know that first file has finished updating when I get the 2nd file's trigger.

1 Comment

There is race condition in some cases when executing multiple commands in quick succession. In that case, I output an increasing serial number to the trigger file and wait for that serial number to be available before reading data file.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.