Skip to content

Commit

Permalink
Implemented issue #81 (allow stopping dapr apps) (#179)
Browse files Browse the repository at this point in the history
* added stop app command and documentation

* updated stop app to use process wrapper

* removed comments and temp bug fix for dapr list error

* added stop app assets

* removed unused imports and fixed eslint errors

* minor changes to var names

* updated stop application to kill instances of dapr and daprd

* windows support + minor refactoring

* updating package-lock

* changed kill proc to SIGTERM

* updated to use which module to find daprd executable

* adding package.json

* updated package-lock

* removed unused imports

* updated error handling

* moved stop logic to dapr cli client

* added open telemetry module
  • Loading branch information
sk593 authored Jul 22, 2021
1 parent 8a607ad commit 7b3970b
Show file tree
Hide file tree
Showing 14 changed files with 115 additions and 17 deletions.
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,12 @@ You can also use the extension to directly publish events to running application

![Publish Message](assets/readme/publishMessage.png)

### Stop Dapr applications

The Dapr extension allows you to directly stop locally-running applications without using the command line.

![Stop Application](assets/readme/stopApp.png)

## Telemetry

### Data Collection
Expand Down
Binary file added assets/readme/stopApp.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions i18n/jpn/package.i18n.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
"vscode-dapr.applications.invoke-post.title": "Invoke (POST) application method",
"vscode-dapr.applications.publish-message.title": "Publish message to application",
"vscode-dapr.applications.publish-all-message.title": "Publish Message to All Applications",
"vscode-dapr.applications.stop-app.title": "Stop Application",
"vscode-dapr.help.readDocumentation.title": "Read Documentation",
"vscode-dapr.help.getStarted.title": "Get Started",
"vscode-dapr.help.reportIssue.title": "Report Issue",
Expand Down
13 changes: 13 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

21 changes: 18 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
"onCommand:vscode-dapr.applications.invoke-post",
"onCommand:vscode-dapr.applications.publish-all-message",
"onCommand:vscode-dapr.applications.publish-message",
"onCommand:vscode-dapr.applications.stop-app",
"onCommand:vscode-dapr.help.getStarted",
"onCommand:vscode-dapr.help.installDapr",
"onCommand:vscode-dapr.help.readDocumentation",
Expand Down Expand Up @@ -72,6 +73,11 @@
"title": "%vscode-dapr.applications.publish-message.title%",
"category": "Dapr"
},
{
"command": "vscode-dapr.applications.stop-app",
"title": "%vscode-dapr.applications.stop-app.title%",
"category": "Dapr"
},
{
"command": "vscode-dapr.help.getStarted",
"title": "%vscode-dapr.help.getStarted.title%",
Expand Down Expand Up @@ -125,15 +131,23 @@
"view/item/context": [
{
"command": "vscode-dapr.applications.invoke-get",
"when": "view == vscode-dapr.views.applications && viewItem == application"
"when": "view == vscode-dapr.views.applications && viewItem == application",
"group": "invoke"
},
{
"command": "vscode-dapr.applications.invoke-post",
"when": "view == vscode-dapr.views.applications && viewItem == application"
"when": "view == vscode-dapr.views.applications && viewItem == application",
"group": "invoke"
},
{
"command": "vscode-dapr.applications.publish-message",
"when": "view == vscode-dapr.views.applications && viewItem == application"
"when": "view == vscode-dapr.views.applications && viewItem == application",
"group": "invoke"
},
{
"command": "vscode-dapr.applications.stop-app",
"when": "view == vscode-dapr.views.applications && viewItem == application",
"group": "stop"
}
],
"view/title": [
Expand Down Expand Up @@ -472,6 +486,7 @@
"@types/terser-webpack-plugin": "^5.0.2",
"@types/vscode": "^1.57.0",
"@types/webpack": "^4.41.26",
"@types/which": "^2.0.1",
"@typescript-eslint/eslint-plugin": "^4.19.0",
"@typescript-eslint/parser": "^4.19.0",
"del": "^6.0.0",
Expand Down
1 change: 1 addition & 0 deletions package.nls.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
"vscode-dapr.applications.invoke-post.title": "Invoke (POST) Application Method",
"vscode-dapr.applications.publish-message.title": "Publish Message to Application",
"vscode-dapr.applications.publish-all-message.title": "Publish Message to All Applications",
"vscode-dapr.applications.stop-app.title": "Stop Application",

"vscode-dapr.configuration.paths.daprPath.description": "The full path to the dapr binary.",
"vscode-dapr.configuration.paths.daprdPath.description": "The full path to the daprd binary.",
Expand Down
24 changes: 24 additions & 0 deletions src/commands/applications/stopApp.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.

import DaprApplicationNode from "../../views/applications/daprApplicationNode";
import { UserInput } from '../../services/userInput';
import { IActionContext } from 'vscode-azureextensionui';
import { getLocalizationPathForFile } from '../../util/localization';
import * as nls from 'vscode-nls';
import { DaprCliClient } from "../../services/daprCliClient";

const localize = nls.loadMessageBundle(getLocalizationPathForFile(__filename));

export async function stopApp(daprCliClient: DaprCliClient, ui: UserInput, node: DaprApplicationNode | undefined): Promise<void> {
try {
return daprCliClient.stopApp(node?.application);
} catch {
await ui.showWarningMessage(localize('commands.invokeCommon.stopAppError', 'Failed to stop application \'{0}\'', node?.application.appId),
{ modal: true });
}
}

const createStopCommand = (daprCliClient: DaprCliClient, ui: UserInput) => (context: IActionContext, node: DaprApplicationNode | undefined): Promise<void> => stopApp(daprCliClient, ui, node);

export default createStopCommand;
5 changes: 4 additions & 1 deletion src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import LocalScaffolder from './scaffolding/scaffolder';
import NodeEnvironmentProvider from './services/environmentProvider';
import createScaffoldDaprComponentsCommand from './commands/scaffoldDaprComponents';
import VsCodeSettingsProvider from './services/settingsProvider';
import createStopCommand from './commands/applications/stopApp';
import LocalDaprCliClient from './services/daprCliClient';
import createInstallDaprCommand from './commands/help/installDapr';

Expand Down Expand Up @@ -66,11 +67,13 @@ export function activate(context: vscode.ExtensionContext): Promise<void> {
const scaffolder = new LocalScaffolder();
const templatesPath = path.join(context.extensionPath, 'assets', 'templates');
const templateScaffolder = new HandlebarsTemplateScaffolder(templatesPath);
const daprCliClient = new LocalDaprCliClient(() => settingsProvider.daprPath)

telemetryProvider.registerContextCommandWithTelemetry('vscode-dapr.applications.invoke-get', createInvokeGetCommand(daprApplicationProvider, daprClient, ext.outputChannel, ui, context.workspaceState));
telemetryProvider.registerContextCommandWithTelemetry('vscode-dapr.applications.invoke-post', createInvokePostCommand(daprApplicationProvider, daprClient, ext.outputChannel, ui, context.workspaceState));
telemetryProvider.registerCommandWithTelemetry('vscode-dapr.applications.publish-all-message', createPublishAllMessageCommand(daprApplicationProvider, daprClient, ext.outputChannel, ui, context.workspaceState));
telemetryProvider.registerContextCommandWithTelemetry('vscode-dapr.applications.publish-message', createPublishMessageCommand(daprApplicationProvider, daprClient, ext.outputChannel, ui, context.workspaceState));
telemetryProvider.registerContextCommandWithTelemetry('vscode-dapr.applications.stop-app', createStopCommand(daprCliClient, ui));
telemetryProvider.registerContextCommandWithTelemetry('vscode-dapr.help.readDocumentation', createReadDocumentationCommand(ui));
telemetryProvider.registerContextCommandWithTelemetry('vscode-dapr.help.getStarted', createGetStartedCommand(ui));
telemetryProvider.registerContextCommandWithTelemetry('vscode-dapr.help.installDapr', createInstallDaprCommand(ui));
Expand All @@ -83,7 +86,7 @@ export function activate(context: vscode.ExtensionContext): Promise<void> {
const daprInstallationManager = new LocalDaprInstallationManager(
extensionPackage.engines['dapr-cli'],
extensionPackage.engines['dapr-runtime'],
new LocalDaprCliClient(() => settingsProvider.daprPath),
daprCliClient,
ui);

registerDisposable(vscode.tasks.registerTaskProvider('dapr', new DaprCommandTaskProvider(daprInstallationManager, () => settingsProvider.daprPath, telemetryProvider)));
Expand Down
8 changes: 5 additions & 3 deletions src/services/daprApplicationProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export interface DaprApplication {
appId: string;
httpPort: number;
pid: number;
ppid: number | undefined;
}

export interface DaprApplicationProvider {
Expand Down Expand Up @@ -40,15 +41,16 @@ function getHttpPort(cmd: string): number {
}
}

function toApplication(cmd: string | undefined, pid: number): DaprApplication | undefined {
function toApplication(cmd: string | undefined, pid: number, ppid: number | undefined): DaprApplication | undefined {
if (cmd) {
const appId = getAppId(cmd);

if (appId) {
return {
appId,
httpPort: getHttpPort(cmd),
pid
pid,
ppid
};
}
}
Expand Down Expand Up @@ -103,7 +105,7 @@ export default class ProcessBasedDaprApplicationProvider extends vscode.Disposab
const processes = await this.processProvider.listProcesses('daprd', this.settingsProvider.daprdPath);

this.applications = processes
.map(process => toApplication(process.cmd, process.pid))
.map(process => toApplication(process.cmd, process.pid, process.ppid))
.filter((application): application is DaprApplication => application !== undefined);

this.onDidChangeEmitter.fire();
Expand Down
16 changes: 16 additions & 0 deletions src/services/daprCliClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import CommandLineBuilder from "../util/commandLineBuilder";
import { Process } from "../util/process";
import * as nls from 'vscode-nls';
import { getLocalizationPathForFile } from '../util/localization';
import { DaprApplication } from "./daprApplicationProvider";
import * as os from 'os'

const localize = nls.loadMessageBundle(getLocalizationPathForFile(__filename));

Expand All @@ -15,6 +17,7 @@ export interface DaprVersion {

export interface DaprCliClient {
version(): Promise<DaprVersion>;
stopApp(application: DaprApplication | undefined): void;
}

export default class LocalDaprCliClient implements DaprCliClient {
Expand Down Expand Up @@ -44,4 +47,17 @@ export default class LocalDaprCliClient implements DaprCliClient {
runtime: runtimeMatch ? runtimeMatch.groups!['version'] : undefined
}
}

stopApp(application: DaprApplication | undefined): void {
const processId = application?.ppid !== undefined ? application.ppid : application?.pid;
if (os.platform() === 'win32') {
// NOTE: Windows does not support SIGTERM/SIGINT/SIGBREAK, so there can be no graceful process shutdown.
// As a partial mitigation, use `taskkill` to kill the entire process tree.
processId !== undefined ? void Process.exec(`taskkill /pid ${processId} /t /f`) : null;
} else {
processId !== undefined ? process.kill(processId, 'SIGTERM') : null;
}
}


}
8 changes: 5 additions & 3 deletions src/services/daprClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { DaprApplication } from "./daprApplicationProvider";
import { HttpClient, HttpResponse } from './httpClient';
import { getLocalizationPathForFile } from '../util/localization';


const localize = nls.loadMessageBundle(getLocalizationPathForFile(__filename));

export interface DaprClient {
Expand Down Expand Up @@ -61,7 +62,7 @@ export default class HttpDaprClient implements DaprClient {

await this.httpClient.post(url, payload, { json: true }, token);
}

async getMetadata(application: DaprApplication, token?: vscode.CancellationToken | undefined): Promise<DaprMetadata> {
const originalUrl = `http://localhost:${application.httpPort}/v1.0/metadata`;

Expand All @@ -70,7 +71,7 @@ export default class HttpDaprClient implements DaprClient {
return manageResponse(response) as DaprMetadata;
}
}

export interface DaprMetadata {
components: DaprComponentMetadata[];
}
Expand All @@ -79,4 +80,5 @@ export interface DaprMetadata {
name: string;
type: string;
version: string;
}
}

2 changes: 1 addition & 1 deletion src/services/daprInstallationManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -154,4 +154,4 @@ export default class LocalDaprInstallationManager implements DaprInstallationMan
context.suppressReportIssue = true;
}
}
}
}
26 changes: 20 additions & 6 deletions src/services/processProvider.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.

import * as psList from 'ps-list';
import * as os from 'os';
import { Process } from '../util/process';
Expand All @@ -7,19 +10,20 @@ export interface ProcessInfo {
cmd: string;
name: string;
pid: number;
ppid: number | undefined;
}

export interface ProcessProvider {
listProcesses(name: string, daprdPath: string): Promise<ProcessInfo[]>;
}

export class UnixProcessProvider implements ProcessProvider {

async listProcesses(name: string, daprdPath: string): Promise<ProcessInfo[]> {
const processes = await psList();
return processes
const temp = processes
.filter(process => process.name === name || this.hasDaprdPath(process, daprdPath))
.map(process => ({ name: process.name, cmd: process.cmd ?? '', pid: process.pid }));
.map(process => ({ name: process.name, cmd: process.cmd ?? '', pid: process.pid , ppid: this.getDaprPpid(process, daprdPath)}));
return temp;
}

hasDaprdPath(process: psList.ProcessDescriptor, daprdPath: string): boolean | undefined {
Expand All @@ -41,6 +45,14 @@ export class UnixProcessProvider implements ProcessProvider {
}
}
}

getDaprPpid(process: psList.ProcessDescriptor, daprdPath: string): number | undefined {
if(this.hasDaprdPath(process, daprdPath)) {
return process.ppid
}
return undefined;
}

}


Expand All @@ -52,7 +64,7 @@ function getWmicValue(line: string): string {

export class WindowsProcessProvider implements ProcessProvider {
async listProcesses(name: string): Promise<ProcessInfo[]> {
const list = await Process.exec(`wmic process where "name='${name}.exe'" get commandline,name,processid /format:list`);
const list = await Process.exec(`wmic process where "name='${name}.exe'" get commandline,name,parentprocessid,processid /format:list`);

// Lines in the output are delimited by "<CR><CR><LF>".
const lines = list.stdout.split('\r\r\n');
Expand All @@ -68,9 +80,11 @@ export class WindowsProcessProvider implements ProcessProvider {

const cmd = getWmicValue(lines[(i * 5) + 2]);
const name = getWmicValue(lines[(i * 5) + 3]);
const pid = parseInt(getWmicValue(lines[(i * 5) + 4]), 10);
const ppid = parseInt(getWmicValue(lines[(i*5) + 4]), 10);
const pid = parseInt(getWmicValue(lines[(i * 5) + 5]), 10);


processes.push({ cmd, name, pid });
processes.push({ cmd, name, pid, ppid});
}

return processes;
Expand Down
1 change: 1 addition & 0 deletions src/views/applications/daprApplicationTreeDataProvider.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
/* eslint-disable @typescript-eslint/no-unsafe-call */
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.

Expand Down

0 comments on commit 7b3970b

Please sign in to comment.