Skip to content

CLI plugins error handling #109

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

Merged
merged 11 commits into from
May 20, 2025
31 changes: 6 additions & 25 deletions src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ import {
queueTelemetryEvent,
} from './telemetry/client';
import { checkForDockerEngine } from './utils/monitor';
import { spawnDockerCommand } from './utils/spawnDockerCommand';
import { getExtensionSetting } from './utils/settings';

export const BakeBuildCommandId = 'dockerLspClient.bake.build';
export const ScoutImageScanCommandId = 'docker.scout.imageScan';
Expand All @@ -22,15 +24,7 @@ const errorRegExp = new RegExp('(E[A-Z]+)');

function registerCommands(ctx: vscode.ExtensionContext) {
registerCommand(ctx, BakeBuildCommandId, async (commandArgs: any) => {
const result = await new Promise<boolean>((resolve) => {
const process = spawn('docker', ['buildx', 'bake', '--help']);
process.on('error', () => {
resolve(false);
});
process.on('exit', (code) => {
resolve(code === 0);
});
});
const result = await spawnDockerCommand('buildx', ['bake', '--help']);
const args = ['buildx', 'bake'];

if (commandArgs['call'] === 'print') {
Expand All @@ -56,15 +50,7 @@ function registerCommands(ctx: vscode.ExtensionContext) {
});

registerCommand(ctx, ScoutImageScanCommandId, async (args) => {
const result = await new Promise<boolean>((resolve) => {
const process = spawn('docker', ['scout']);
process.on('error', () => {
resolve(false);
});
process.on('exit', (code) => {
resolve(code === 0);
});
});
const result = spawnDockerCommand('scout');
const options: vscode.ShellExecutionOptions = {};
if (
vscode.workspace.workspaceFolders === undefined ||
Expand Down Expand Up @@ -105,8 +91,6 @@ function registerCommand(
}

const activateDockerLSP = async (ctx: vscode.ExtensionContext) => {
registerCommands(ctx);

if (await activateDockerNativeLanguageClient(ctx)) {
getNativeClient()
.start()
Expand Down Expand Up @@ -141,15 +125,12 @@ const activateDockerLSP = async (ctx: vscode.ExtensionContext) => {
export function activate(ctx: vscode.ExtensionContext) {
extensionVersion = String(ctx.extension.packageJSON.version);
recordVersionTelemetry();
registerCommands(ctx);
activateExtension(ctx);
}

async function activateExtension(ctx: vscode.ExtensionContext) {
if (
vscode.workspace
.getConfiguration('docker.extension')
.get('dockerEngineAvailabilityPrompt')
) {
if (getExtensionSetting('dockerEngineAvailabilityPrompt')) {
let notified = false;
for (const document of vscode.workspace.textDocuments) {
if (
Expand Down
163 changes: 28 additions & 135 deletions src/utils/monitor.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
import { access } from 'fs';
import { spawn } from 'child_process';
import * as vscode from 'vscode';
import {
promptOpenDockerDesktop,
promptInstallDesktop,
promptUnauthenticatedDesktop,
} from './prompt';
import { isDockerDesktopInstalled } from './os';
import { getExtensionSetting } from './settings';
import { spawnDockerCommand } from './spawnDockerCommand';

enum DockerEngineStatus {
Unavailable,
Expand All @@ -13,11 +18,7 @@ enum DockerEngineStatus {
* either install or open Docker Desktop.
*/
export async function checkForDockerEngine(): Promise<void> {
if (
!vscode.workspace
.getConfiguration('docker.extension')
.get('dockerEngineAvailabilityPrompt')
) {
if (!getExtensionSetting('dockerEngineAvailabilityPrompt')) {
return;
}

Expand All @@ -38,134 +39,26 @@ export async function checkForDockerEngine(): Promise<void> {
*/
function checkDockerStatus(): Promise<DockerEngineStatus> {
return new Promise<DockerEngineStatus>((resolve) => {
const s = spawn('docker', ['ps']);
let output = '';
s.stderr.on('data', (chunk) => {
output += String(chunk);
});
s.on('error', () => {
// this happens if docker cannot be found on the PATH
return resolve(DockerEngineStatus.Unavailable);
});
s.on('exit', (code) => {
if (code === 0) {
return resolve(DockerEngineStatus.Available);
}
if (
output.includes('Sign-in enforcement is enabled') ||
output.includes(
'request returned Internal Server Error for API route and version',
)
) {
return resolve(DockerEngineStatus.Unauthenticated);
}
return resolve(DockerEngineStatus.Unavailable);
});
});
}

function getDockerDesktopPath(): string {
switch (process.platform) {
case 'win32':
return 'C:\\Program Files\\Docker\\Docker\\Docker Desktop.exe';
case 'darwin':
return '/Applications/Docker.app';
}
return '/opt/docker-desktop/bin/com.docker.backend';
}

async function isDockerDesktopInstalled(): Promise<boolean> {
return new Promise<boolean>((resolve) => {
const s = spawn('docker', ['desktop', 'version']);
s.on('error', () => {
// this happens if docker cannot be found on the PATH
resolve(false);
});
s.on('exit', (code) => {
if (code === 0) {
return resolve(true);
}

access(getDockerDesktopPath(), (err) => {
resolve(err === null);
});
});
});
}

function disableDockerEngineAvailabilityPrompt(): void {
vscode.workspace
.getConfiguration('docker.extension')
.update(
'dockerEngineAvailabilityPrompt',
false,
vscode.ConfigurationTarget.Global,
);
}

/**
* Prompts the user to login to Docker Desktop.
*/
async function promptUnauthenticatedDesktop(): Promise<void> {
const response = await vscode.window.showInformationMessage(
'Docker is not running. To get help with your Dockerfile, sign in to Docker Desktop.',
"Don't show again",
);
if (response === "Don't show again") {
disableDockerEngineAvailabilityPrompt();
}
}

/**
* Prompts the user to open Docker Desktop.
*/
async function promptOpenDockerDesktop(): Promise<void> {
const response = await vscode.window.showInformationMessage(
'Docker is not running. To get help with your Dockerfile, start Docker.',
"Don't show again",
'Open Docker Desktop',
);
if (response === "Don't show again") {
disableDockerEngineAvailabilityPrompt();
} else if (response === 'Open Docker Desktop') {
const dockerDesktopPath = getDockerDesktopPath();
if (process.platform === 'darwin') {
spawn('open', [dockerDesktopPath]).on('exit', (code) => {
if (code !== 0) {
vscode.window.showErrorMessage(
`Failed to open Docker Desktop: open ${dockerDesktopPath}`,
{ modal: true },
);
spawnDockerCommand('ps', [], {
onError: () => resolve(DockerEngineStatus.Unavailable),
onStderr: (chunk) => {
output += String(chunk);
},
onExit: (code) => {
if (code === 0) {
return resolve(DockerEngineStatus.Available);
}
});
} else {
spawn(dockerDesktopPath).on('exit', (code) => {
if (code !== 0) {
vscode.window.showErrorMessage(
`Failed to open Docker Desktop: ${dockerDesktopPath}`,
{ modal: true },
);
if (
output.includes('Sign-in enforcement is enabled') ||
output.includes(
'request returned Internal Server Error for API route and version',
)
) {
return resolve(DockerEngineStatus.Unauthenticated);
}
});
}
}
}

/**
* Prompts the user to install Docker Desktop by navigating to the
* website.
*/
async function promptInstallDesktop(): Promise<void> {
const response = await vscode.window.showInformationMessage(
'Docker is not running. To get help with your Dockerfile, start Docker.',
"Don't show again",
'Install Docker Desktop',
);
if (response === "Don't show again") {
disableDockerEngineAvailabilityPrompt();
} else if (response === 'Install Docker Desktop') {
vscode.env.openExternal(
vscode.Uri.parse('https://docs.docker.com/install/'),
);
}
return resolve(DockerEngineStatus.Unavailable);
},
});
});
}
28 changes: 28 additions & 0 deletions src/utils/os.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { access } from 'fs';
import { spawnDockerCommand } from './spawnDockerCommand';

export function getDockerDesktopPath(): string {
switch (process.platform) {
case 'win32':
return 'C:\\Program Files\\Docker\\Docker\\Docker Desktop.exe';
case 'darwin':
return '/Applications/Docker.app';
}
return '/opt/docker-desktop/bin/com.docker.backend';
}

export async function isDockerDesktopInstalled(): Promise<boolean> {
return new Promise<boolean>((resolve) => {
spawnDockerCommand('docker', ['desktop', 'version'], {
onExit: (code) => {
if (code === 0) {
return resolve(true);
}

access(getDockerDesktopPath(), (err) => {
resolve(err === null);
});
},
});
});
}
83 changes: 83 additions & 0 deletions src/utils/prompt.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import * as vscode from 'vscode';
import { spawn } from 'child_process';
import { disableDockerEngineAvailabilityPrompt } from './settings';
import { getDockerDesktopPath } from './os';

/**
* Prompts the user to login to Docker Desktop.
*/
export async function promptUnauthenticatedDesktop(): Promise<void> {
const response = await vscode.window.showInformationMessage(
'Docker is not running. To get help with your Dockerfile, sign in to Docker Desktop.',
"Don't show again",
);
if (response === "Don't show again") {
disableDockerEngineAvailabilityPrompt();
}
}

/**
* Prompts the user to open Docker Desktop.
*/
export async function promptOpenDockerDesktop(): Promise<void> {
const response = await vscode.window.showInformationMessage(
'Docker is not running. To get help with your Dockerfile, start Docker.',
"Don't show again",
'Open Docker Desktop',
);
if (response === "Don't show again") {
disableDockerEngineAvailabilityPrompt();
} else if (response === 'Open Docker Desktop') {
const dockerDesktopPath = getDockerDesktopPath();
if (process.platform === 'darwin') {
spawn('open', [dockerDesktopPath]).on('exit', (code) => {
if (code !== 0) {
vscode.window.showErrorMessage(
`Failed to open Docker Desktop: open ${dockerDesktopPath}`,
{ modal: true },
);
}
});
} else {
spawn(dockerDesktopPath).on('exit', (code) => {
if (code !== 0) {
vscode.window.showErrorMessage(
`Failed to open Docker Desktop: ${dockerDesktopPath}`,
{ modal: true },
);
}
});
}
}
}

/**
* Prompts the user to install Docker Desktop by navigating to the
* website.
*/
export async function promptInstallDesktop(): Promise<void> {
const response = await vscode.window.showInformationMessage(
'Docker is not running. To get help with your Dockerfile, install Docker.',
"Don't show again",
'Install Docker Desktop',
);
if (response === "Don't show again") {
disableDockerEngineAvailabilityPrompt();
} else if (response === 'Install Docker Desktop') {
vscode.env.openExternal(
vscode.Uri.parse('https://docs.docker.com/install/'),
);
}
}

/**
* Shows a message to the user indicating that Docker Desktop does not know the command
*/
export async function showUnknownCommandMessage(
command: string,
): Promise<void> {
// TODO: Get a proper error message from Allie
await vscode.window.showErrorMessage(
`Docker Desktop does not know the command "${command}".`,
Copy link
Contributor Author

Choose a reason for hiding this comment

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

@aevesdocker could we please get an error message for when an extension tries to run a docker subcommand like docker scout or docker ai but it is not available because the user is running an older version of Docker Desktop from before that command was introduced?

Copy link
Collaborator

Choose a reason for hiding this comment

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

it is not available because the user is running an older version of Docker Desktop from before that command was introduced?

  1. It might be unavailable because the user installed Docker CE instead of installing Docker Desktop (likely case on Linux). We can reuse promptInstallDesktop in that case although the wording would be different of course.
  2. For the Docker Desktop prompts, Varun had mentioned to me we should show them based on the docker.extension.dockerEngineAvailabilityPrompt setting so we will need to add a conditional here similar to checkForDockerEngine. Sorry for not making that clear earlier.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

For 2 are we just happy with the fact that if the user sets docker.extension.dockerEngineAvailabilityPrompt to false they will not get a prompt regardless of if it's docker engine missing or they are running an unavailable command?

Copy link
Collaborator

Choose a reason for hiding this comment

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

For 2 are we just happy with the fact that if the user sets docker.extension.dockerEngineAvailabilityPrompt to false they will not get a prompt regardless of if it's docker engine missing or they are running an unavailable command?

You got it. 👍 I think the idea is "if I have it set to false do not tell me to open or install or update Docker Desktop".

Copy link
Contributor

Choose a reason for hiding this comment

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

so we all good, or still need me?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I think we still need one for the case:

It might be unavailable because the user installed Docker CE instead of installing Docker Desktop (likely case on Linux). We can reuse promptInstallDesktop in that case although the wording would be different of course.

Copy link
Collaborator

Choose a reason for hiding this comment

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

We can discuss this during standup to try and get a grasp on all the possibilities.

Copy link
Collaborator

Choose a reason for hiding this comment

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

@stanislavHamara We have three different cases here:

  1. docker is not found and Node.js throws that ENOENT error
    • use the promptInstallDesktop function to prompt the user to install Docker Desktop if the user wants to be prompted (per getExtensionSetting('dockerEngineAvailabilityPrompt'))
  2. docker scout is not found and we get the non-zero exit code (which suggests docker itself is installed) and isDockerDesktopInstalled returns false, this is likely the Linux use case
    • similar to 1 above, we can prompt the user about this but we need a separate, distinct message from Allie first
  3. the reverse case of 2, isDockerDesktopInstalled returns true
    • as discussed on the call, this might not even be a real problem so we will just ignore this for now

@aevesdocker We need your help with 2 above.

const response = await vscode.window.showInformationMessage(
'Docker is not running. To get help with your Dockerfile, install Docker.',
"Don't show again",
'Install Docker Desktop',
);

To help jog your memory, these are the strings we use for the case where "Docker is not installed you should install Docker Desktop". This time we need a message to convey that Docker Scout could not be found and you can get it if you install Docker Desktop (without mentioning that Docker is not running as we know they have the docker CLI installed in this particular case).

If any of the above is unclear, please let me know. :)

Copy link
Contributor

Choose a reason for hiding this comment

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

So something simple like

Docker Scout is not available. To access Docker Scout's features, install Docker Desktop.
with the same action options as above - Don't show again/Install Docker Desktop

?

Copy link
Collaborator

Choose a reason for hiding this comment

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

@aevesdocker That sounds fine to me. 👍 Thanks!

);
}
Loading
Loading