diff --git a/extension/src/Extension.ts b/extension/src/Extension.ts index fb7da5324..b3c3c817b 100644 --- a/extension/src/Extension.ts +++ b/extension/src/Extension.ts @@ -111,11 +111,7 @@ export class Extension { treeDataProvider: this.gradleTasksTreeDataProvider, showCollapseAll: true, }); - this.gradleDaemonsTreeDataProvider = new GradleDaemonsTreeDataProvider( - this.context, - this.rootProjectsStore, - this.client - ); + this.gradleDaemonsTreeDataProvider = new GradleDaemonsTreeDataProvider(this.context, this.rootProjectsStore); this.gradleDaemonsTreeView = vscode.window.createTreeView(GRADLE_DAEMONS_VIEW, { treeDataProvider: this.gradleDaemonsTreeDataProvider, showCollapseAll: false, diff --git a/extension/src/commands/Commands.ts b/extension/src/commands/Commands.ts index d35fb755b..419d76262 100644 --- a/extension/src/commands/Commands.ts +++ b/extension/src/commands/Commands.ts @@ -148,8 +148,8 @@ export class Commands { COMMAND_REFRESH_DAEMON_STATUS, new RefreshDaemonStatusCommand(this.gradleDaemonsTreeDataProvider) ); - this.registerCommand(COMMAND_STOP_DAEMONS, new StopDaemonsCommand(this.client, this.rootProjectsStore)); - this.registerCommand(COMMAND_STOP_DAEMON, new StopDaemonCommand(this.client)); + this.registerCommand(COMMAND_STOP_DAEMONS, new StopDaemonsCommand(this.rootProjectsStore)); + this.registerCommand(COMMAND_STOP_DAEMON, new StopDaemonCommand()); this.registerCommand(COMMAND_EXPLORER_TREE, new ExplorerTreeCommand(this.gradleTasksTreeDataProvider)); this.registerCommand(COMMAND_EXPLORER_FLAT, new ExplorerFlatCommand(this.gradleTasksTreeDataProvider)); this.registerCommand(COMMAND_OPEN_SETTINGS, new OpenSettingsCommand()); diff --git a/extension/src/commands/StopDaemonCommand.ts b/extension/src/commands/StopDaemonCommand.ts index 3ce6a2d7b..fdce8581e 100644 --- a/extension/src/commands/StopDaemonCommand.ts +++ b/extension/src/commands/StopDaemonCommand.ts @@ -2,12 +2,14 @@ import { GradleDaemonTreeItem } from "../views"; import { confirmModal } from "../util/input"; import { logger } from "../logger"; import { Command } from "./Command"; -import { GradleClient } from "../client"; +import { execAsync } from "../util/execAsync"; +import * as vscode from "vscode"; +import { COMMAND_REFRESH_DAEMON_STATUS } from "./RefreshDaemonStatusCommand"; export const COMMAND_STOP_DAEMON = "gradle.stopDaemon"; export class StopDaemonCommand extends Command { - constructor(private client: GradleClient) { + constructor() { super(); } async run(treeItem: GradleDaemonTreeItem): Promise { @@ -15,9 +17,21 @@ export class StopDaemonCommand extends Command { return; } const pid = treeItem.pid; - const stopDaemonReply = await this.client.stopDaemon(pid); - if (stopDaemonReply) { - logger.info(stopDaemonReply.getMessage()); + try { + await this.stopDaemon(pid); + logger.info(`Successfully stopped daemon with PID ${pid}.`); + } catch (error) { + logger.error(`Failed to stop daemon with PID ${pid}: ${error.message}.`); } } + + async stopDaemon(pid: string): Promise { + if (!pid) { + throw new Error("PID is required to stop the daemon."); + } + + const command = process.platform === "win32" ? `taskkill /PID ${pid} /F` : `kill ${pid}`; + await execAsync(command); + await vscode.commands.executeCommand(COMMAND_REFRESH_DAEMON_STATUS); + } } diff --git a/extension/src/commands/StopDaemonsCommand.ts b/extension/src/commands/StopDaemonsCommand.ts index ec1a72680..bb9106f83 100644 --- a/extension/src/commands/StopDaemonsCommand.ts +++ b/extension/src/commands/StopDaemonsCommand.ts @@ -1,14 +1,19 @@ import * as vscode from "vscode"; import { confirmModal } from "../util/input"; -import { StopDaemonsReply } from "../proto/gradle_pb"; import { logger } from "../logger"; import { Command } from "./Command"; import { RootProjectsStore } from "../stores"; -import { GradleClient } from "../client"; +import { getGradleConfig } from "../util/config"; +import { GradleStatus } from "../views/gradleDaemons/services/GradleStatus"; +import { GradleConnectionType } from "../views/gradleDaemons/models/GradleConnectionType"; +import { GradleWrapper } from "../views/gradleDaemons/services/GradleWrapper"; +import { GradleLocalInstallation } from "../views/gradleDaemons/services/GradleLocalInstallation"; +import { COMMAND_REFRESH_DAEMON_STATUS } from "./RefreshDaemonStatusCommand"; + export const COMMAND_STOP_DAEMONS = "gradle.stopDaemons"; export class StopDaemonsCommand extends Command { - constructor(private client: GradleClient, private rootProjectsStore: RootProjectsStore) { + constructor(private rootProjectsStore: RootProjectsStore) { super(); } async run(): Promise { @@ -20,14 +25,29 @@ export class StopDaemonsCommand extends Command { return; } const gradleRootFolders = await this.rootProjectsStore.getProjectRootsWithUniqueVersions(); - const promises: Promise[] = gradleRootFolders.map((rootProject) => - this.client.stopDaemons(rootProject.getProjectUri().fsPath) - ); - const replies = await Promise.all(promises); - replies.forEach((reply) => { - if (reply) { - logger.info(reply.getMessage()); - } - }); + try { + const promises: Promise[] = gradleRootFolders.map((rootProject) => + this.stopDaemons(rootProject.getProjectUri().fsPath) + ); + await Promise.all(promises); + logger.info(`Successfully stopped all daemons.`); + await vscode.commands.executeCommand(COMMAND_REFRESH_DAEMON_STATUS); + } catch (error) { + logger.error(`Failed to stop daemons: ${error.message}.`); + } + } + + async stopDaemons(projectFolder: string): Promise { + const gradleConfig = getGradleConfig(); + const connectType = await GradleStatus.getConnectionType(gradleConfig); + if (connectType === GradleConnectionType.WRAPPER) { + const gradleExecution = new GradleWrapper(projectFolder); + await gradleExecution.exec(["--stop"]); + } else if (connectType === GradleConnectionType.LOCALINSTALLATION) { + const gradleExecution = new GradleLocalInstallation(gradleConfig.getGradleHome()); + await gradleExecution.exec(["--stop"]); + } else { + logger.info("No daemons to stop."); + } } } diff --git a/extension/src/test/unit/gradleDaemons.test.ts b/extension/src/test/unit/gradleDaemons.test.ts index 470d621ce..c61aa835b 100644 --- a/extension/src/test/unit/gradleDaemons.test.ts +++ b/extension/src/test/unit/gradleDaemons.test.ts @@ -1,20 +1,13 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ - import * as assert from "assert"; import * as vscode from "vscode"; import * as sinon from "sinon"; import * as path from "path"; -import { - GetDaemonsStatusReply, - DaemonInfo, - StopDaemonReply, - StopDaemonsReply, - Environment, - GradleEnvironment, -} from "../../proto/gradle_pb"; +import { Environment, GradleEnvironment } from "../../proto/gradle_pb"; +import { DaemonInfo } from "../../views/gradleDaemons/models/DaemonInfo"; +import { DaemonStatus } from "../../views/gradleDaemons/models/DaemonStatus"; import { GradleDaemonsTreeDataProvider, GradleDaemonTreeItem } from "../../views"; -// import { Extension } from '../../extension'; import { SinonStub } from "sinon"; import { logger } from "../../logger"; import { @@ -22,7 +15,6 @@ import { resetObjectStubs, buildMockOutputChannel, buildMockWorkspaceFolder, - buildMockClient, buildMockContext, stubWorkspaceFolders, } from "../testUtil"; @@ -31,9 +23,10 @@ import { ICON_DAEMON_STOPPED, ICON_DAEMON_BUSY, ICON_DAEMON_IDLE } from "../../v import { RootProjectsStore } from "../../stores"; import { RefreshDaemonStatusCommand, StopDaemonCommand, StopDaemonsCommand } from "../../commands"; import { sleep } from "../../util"; +import { GradleStatus } from "../../views/gradleDaemons/services/GradleStatus"; +import { GradleConnectionType } from "../../views/gradleDaemons/models/GradleConnectionType"; const mockContext = buildMockContext(); -const mockClient = buildMockClient(); const mockWorkspaceFolder1 = buildMockWorkspaceFolder(0, "folder1", "folder1"); const mockWorkspaceFolder2 = buildMockWorkspaceFolder(1, "folder2", "folder2"); @@ -46,12 +39,11 @@ describe(getSuiteName("Gradle daemons"), () => { let rootProjectsStore: RootProjectsStore; beforeEach(async () => { rootProjectsStore = new RootProjectsStore(); - gradleDaemonsTreeDataProvider = new GradleDaemonsTreeDataProvider(mockContext, rootProjectsStore, mockClient); + gradleDaemonsTreeDataProvider = new GradleDaemonsTreeDataProvider(mockContext, rootProjectsStore); stubWorkspaceFolders([mockWorkspaceFolder1, mockWorkspaceFolder2, mockWorkspaceFolder3]); await rootProjectsStore.populate(); - // GradleClient.getBuild() sets the gradle versions once it receives the gradle environment const projectRoots = await rootProjectsStore.getProjectRoots(); const gradleEnvironment1 = new GradleEnvironment(); gradleEnvironment1.setGradleVersion("6.3"); @@ -89,31 +81,16 @@ describe(getSuiteName("Gradle daemons"), () => { it("should build the daemon treeitems", async () => { await vscode.workspace.getConfiguration("gradle").update("showStoppedDaemons", true, true); - const mockDaemonInfoBusy = new DaemonInfo(); - mockDaemonInfoBusy.setStatus(DaemonInfo.DaemonStatus.BUSY); - mockDaemonInfoBusy.setPid("41716"); - mockDaemonInfoBusy.setInfo("6.3"); - - const mockDaemonInfoIdle = new DaemonInfo(); - mockDaemonInfoIdle.setStatus(DaemonInfo.DaemonStatus.IDLE); - mockDaemonInfoIdle.setPid("41717"); - mockDaemonInfoIdle.setInfo("6.4"); - - const mockDaemonInfoStopped = new DaemonInfo(); - mockDaemonInfoStopped.setStatus(DaemonInfo.DaemonStatus.STOPPED); - mockDaemonInfoStopped.setPid("41718"); - mockDaemonInfoStopped.setInfo("(by user or operating system)"); - - const mockReply1 = new GetDaemonsStatusReply(); - mockReply1.setDaemonInfoList([mockDaemonInfoBusy, mockDaemonInfoStopped]); + const mockDaemonInfoBusy = new DaemonInfo("41716", DaemonStatus.BUSY, "6.3"); + const mockDaemonInfoIdle = new DaemonInfo("41717", DaemonStatus.IDLE, "6.4"); + const mockDaemonInfoStopped = new DaemonInfo("41718", DaemonStatus.STOPPED, "(by user or operating system)"); - const mockReply2 = new GetDaemonsStatusReply(); - mockReply2.setDaemonInfoList([mockDaemonInfoIdle, mockDaemonInfoStopped]); - - mockClient.getDaemonsStatus.withArgs(mockWorkspaceFolder1.uri.fsPath).resolves(mockReply1); - mockClient.getDaemonsStatus.withArgs(mockWorkspaceFolder2.uri.fsPath).resolves(mockReply2); - // NOTE: no reason to mock reply for mockWorkspaceFolder3 as it should be ignored due to - // dupicate gradle version + sinon + .stub(GradleStatus, "getDaemonsStatusList") + .withArgs(mockWorkspaceFolder1.uri.fsPath) + .resolves([mockDaemonInfoBusy, mockDaemonInfoStopped]) + .withArgs(mockWorkspaceFolder2.uri.fsPath) + .resolves([mockDaemonInfoIdle, mockDaemonInfoStopped]); let children = await gradleDaemonsTreeDataProvider.getChildren(); @@ -158,63 +135,51 @@ describe(getSuiteName("Gradle daemons"), () => { }); it("should stop a daemon", async () => { - const mockReply = new StopDaemonReply(); - mockReply.setMessage("Stopped"); - mockClient.stopDaemon.resolves(mockReply); - - const showWarningMessageStub = (sinon.stub(vscode.window, "showWarningMessage") as SinonStub).resolves("Yes"); - - const mockDaemonInfoBusy = new DaemonInfo(); - mockDaemonInfoBusy.setStatus(DaemonInfo.DaemonStatus.BUSY); - mockDaemonInfoBusy.setPid("41716"); - mockDaemonInfoBusy.setInfo("6.4"); - + const mockDaemonInfoBusy = new DaemonInfo("41716", DaemonStatus.BUSY, "6.3"); const mockGradleDaemonTreeItem = new GradleDaemonTreeItem( mockContext, mockDaemonInfoBusy.getPid(), mockDaemonInfoBusy ); - await new StopDaemonCommand(mockClient).run(mockGradleDaemonTreeItem); + const showWarningMessageStub = (sinon.stub(vscode.window, "showWarningMessage") as SinonStub).resolves("Yes"); + const mockStopDaemonCommand = new StopDaemonCommand(); + sinon.stub(mockStopDaemonCommand, "stopDaemon").withArgs(mockDaemonInfoBusy.getPid()).resolves(); + + await mockStopDaemonCommand.run(mockGradleDaemonTreeItem); assert.ok( showWarningMessageStub.calledWith("Are you sure you want to stop the daemon?"), "Stop daemon confirmation message not shown" ); - assert.strictEqual(showWarningMessageStub.callCount, 1); - assert.ok( - mockClient.stopDaemon.calledWith(mockDaemonInfoBusy.getPid()), - "Client stopDaemon not called with daemon PID" - ); - assert.strictEqual(mockClient.stopDaemon.callCount, 1); + assert.ok( - mockOutputChannel.appendLine.calledWith("[info] Stopped"), + mockOutputChannel.appendLine.calledWith("[info] Successfully stopped daemon with PID 41716."), "Output channel appendLine not called with correct message" ); assert.strictEqual(mockOutputChannel.appendLine.callCount, 1); }); it("should stop all daemons", async () => { - const mockReply1 = new StopDaemonsReply(); - mockReply1.setMessage("Stopped 1"); - const mockReply2 = new StopDaemonsReply(); - mockReply2.setMessage("Stopped 2"); - - mockClient.stopDaemons.withArgs(mockWorkspaceFolder1.uri.fsPath).resolves(mockReply1); - mockClient.stopDaemons.withArgs(mockWorkspaceFolder2.uri.fsPath).resolves(mockReply2); - const showWarningMessageStub = (sinon.stub(vscode.window, "showWarningMessage") as SinonStub).resolves("Yes"); + sinon.stub(GradleStatus, "getConnectionType").withArgs(sinon.match.any).resolves(GradleConnectionType.WRAPPER); + + const mockStopDaemonsCommand = new StopDaemonsCommand(rootProjectsStore); + sinon.stub(mockStopDaemonsCommand, "stopDaemons").resolves(); - await new StopDaemonsCommand(mockClient, rootProjectsStore).run(); + await mockStopDaemonsCommand.run(); assert.ok( showWarningMessageStub.calledWith("Are you sure you want to stop the daemons?"), "Stop daemons confirmation message not shown" ); - assert.strictEqual(mockOutputChannel.appendLine.callCount, 2, "Logger not called expected times"); - assert.ok(mockOutputChannel.appendLine.calledWith("[info] Stopped 1"), "Reply for folder 1 not logged"); - assert.ok(mockOutputChannel.appendLine.calledWith("[info] Stopped 2"), "Reply for folder 2 not logged"); + assert.ok( + mockOutputChannel.appendLine.calledWith("[info] Successfully stopped all daemons."), + "Output channel appendLine not called with correct message" + ); + + showWarningMessageStub.restore(); }); it("should refresh the daemons list", async () => { @@ -225,62 +190,112 @@ describe(getSuiteName("Gradle daemons"), () => { assert.strictEqual(onDidChangeSpy.callCount, 1); }); - it("should prevent queing of daemon status requests", async () => { - const mockReply1 = new GetDaemonsStatusReply(); - const mockDaemonInfoBusy = new DaemonInfo(); - mockDaemonInfoBusy.setStatus(DaemonInfo.DaemonStatus.BUSY); - mockDaemonInfoBusy.setPid("41716"); - mockDaemonInfoBusy.setInfo("6.4"); - mockReply1.setDaemonInfoList([mockDaemonInfoBusy]); - const quickReply = Promise.resolve(mockReply1); - - const mockReply2 = new GetDaemonsStatusReply(); - const mockDaemonInfoIdle = new DaemonInfo(); - mockDaemonInfoIdle.setStatus(DaemonInfo.DaemonStatus.IDLE); - mockDaemonInfoIdle.setPid("41716"); - mockDaemonInfoIdle.setInfo("6.4 f00"); - mockReply2.setDaemonInfoList([mockDaemonInfoIdle]); - const longReply = new Promise((resolve) => { + it("should prevent queuing of daemon status requests", async () => { + const mockDaemonInfoBusy = new DaemonInfo("41716", DaemonStatus.BUSY, "6.4"); + const mockDaemonInfoIdle = new DaemonInfo("41716", DaemonStatus.IDLE, "6.4 f00"); + + const quickReply: Promise = Promise.resolve([mockDaemonInfoBusy]); + + const longReply: Promise = new Promise((resolve) => { setTimeout(() => { - resolve(mockReply2); + resolve([mockDaemonInfoIdle]); }, 1000); }); - const workspaceFolder1: vscode.WorkspaceFolder = { - index: 0, - uri: vscode.Uri.file("folder1"), - name: "folder1", - }; - - sinon.stub(vscode.workspace, "workspaceFolders").value([workspaceFolder1]); - - mockClient.getDaemonsStatus.withArgs(mockWorkspaceFolder1.uri.fsPath).returns(quickReply); + sinon.stub(vscode.workspace, "workspaceFolders").value([mockWorkspaceFolder1]); + + const getDaemonsStatusListStub = sinon + .stub(GradleStatus, "getDaemonsStatusList") + .withArgs(mockWorkspaceFolder2.uri.fsPath) + .resolves([mockDaemonInfoIdle]); + + let callCount = 0; + getDaemonsStatusListStub.withArgs(mockWorkspaceFolder1.uri.fsPath).callsFake(async () => { + callCount++; + if (callCount === 1) { + return quickReply; + } else { + return longReply; + } + }); const children = await gradleDaemonsTreeDataProvider.getChildren(); assert.strictEqual(children[0].description, "BUSY"); - mockClient.getDaemonsStatus.withArgs(mockWorkspaceFolder1.uri.fsPath).returns(longReply); - - await new Promise(async (resolve, reject) => { - // This call will return the previous results (quickReply) as we've cancelled - // the request with the subsequent call to refresh() - gradleDaemonsTreeDataProvider - .getChildren() - .then((_children: vscode.TreeItem[]) => { - assert.strictEqual(_children[0].description, "BUSY"); - }) - .catch(reject); - // This call will return the correct results (longReply) - gradleDaemonsTreeDataProvider.refresh(); - await sleep(1000); - gradleDaemonsTreeDataProvider - .getChildren() - .then((_children: vscode.TreeItem[]) => { - assert.strictEqual(_children[0].description, "IDLE"); - resolve(undefined); - }) - .catch(reject); - }); + gradleDaemonsTreeDataProvider.refresh(); + await sleep(1000); + + const refreshedChildren = await gradleDaemonsTreeDataProvider.getChildren(); + + assert.strictEqual(refreshedChildren[0].description, "IDLE"); + }); + + it("should correctly parse daemonInfos from input with Unix and Windows line endings", () => { + // Windows-style input with \r\n + const windowsOutput = ` + 95141 IDLE 8.6\r\n + 12345 BUSY 7.5\r\n + 67890 STOPPED (by user or operating system)\r\n + malformed line\r\n + `; + + const windowsDaemonInfos = GradleStatus.parseDaemonInfo(windowsOutput); + + assert.strictEqual( + windowsDaemonInfos.length, + 3, + "There should be 3 daemons parsed, ignoring malformed lines (Windows)" + ); + + const windowsDaemon1 = windowsDaemonInfos[0]; + assert.strictEqual(windowsDaemon1.getPid(), "95141"); + assert.strictEqual(windowsDaemon1.getStatus(), DaemonStatus.IDLE); + assert.strictEqual(windowsDaemon1.getInfo(), "8.6"); + + const windowsDaemon2 = windowsDaemonInfos[1]; + assert.strictEqual(windowsDaemon2.getPid(), "12345"); + assert.strictEqual(windowsDaemon2.getStatus(), DaemonStatus.BUSY); + assert.strictEqual(windowsDaemon2.getInfo(), "7.5"); + + const windowsDaemon3 = windowsDaemonInfos[2]; + assert.strictEqual(windowsDaemon3.getPid(), "67890"); + assert.strictEqual(windowsDaemon3.getStatus(), DaemonStatus.STOPPED); + assert.strictEqual(windowsDaemon3.getInfo(), "(by user or operating system)"); + + // Unix/Mac-style input with \n + const unixOutput = ` + 95141 IDLE 8.6\n + 12345 BUSY 7.5\n + 67890 STOPPED (by user or operating system)\n + malformed line\n + `; + + const unixDaemonInfos = GradleStatus.parseDaemonInfo(unixOutput); + + assert.strictEqual( + unixDaemonInfos.length, + 3, + "There should be 3 daemons parsed, ignoring malformed lines (Unix/Mac)" + ); + + const unixDaemon1 = unixDaemonInfos[0]; + assert.strictEqual(unixDaemon1.getPid(), "95141"); + assert.strictEqual(unixDaemon1.getStatus(), DaemonStatus.IDLE); + assert.strictEqual(unixDaemon1.getInfo(), "8.6"); + + const unixDaemon2 = unixDaemonInfos[1]; + assert.strictEqual(unixDaemon2.getPid(), "12345"); + assert.strictEqual(unixDaemon2.getStatus(), DaemonStatus.BUSY); + assert.strictEqual(unixDaemon2.getInfo(), "7.5"); + + const unixDaemon3 = unixDaemonInfos[2]; + assert.strictEqual(unixDaemon3.getPid(), "67890"); + assert.strictEqual(unixDaemon3.getStatus(), DaemonStatus.STOPPED); + assert.strictEqual(unixDaemon3.getInfo(), "(by user or operating system)"); + + const emptyOutput = ""; + const emptyDaemonInfos = GradleStatus.parseDaemonInfo(emptyOutput); + assert.strictEqual(emptyDaemonInfos.length, 0, "There should be no daemons parsed for empty output"); }); }); diff --git a/extension/src/util/execAsync.ts b/extension/src/util/execAsync.ts new file mode 100644 index 000000000..2a33f9fab --- /dev/null +++ b/extension/src/util/execAsync.ts @@ -0,0 +1,4 @@ +import { exec } from "child_process"; +import { promisify } from "util"; + +export const execAsync = promisify(exec); diff --git a/extension/src/views/constants.ts b/extension/src/views/constants.ts index 4bb366ecb..b9d48b3b5 100644 --- a/extension/src/views/constants.ts +++ b/extension/src/views/constants.ts @@ -1,4 +1,4 @@ -import { DaemonInfo } from "../proto/gradle_pb"; +import { DaemonStatus } from "./gradleDaemons/models/DaemonStatus"; export const ICON_LOADING = "loading.svg"; export const ICON_GRADLE_TASK = "script.svg"; @@ -24,9 +24,9 @@ export const TREE_ITEM_STATE_FOLDER = "folder"; export const TASK_STATE_RUNNING_REGEX = new RegExp(`^${TREE_ITEM_STATE_TASK_RUNNING}`); export const DAEMON_ICON_MAP = { - [DaemonInfo.DaemonStatus.BUSY]: ICON_DAEMON_BUSY, - [DaemonInfo.DaemonStatus.IDLE]: ICON_DAEMON_IDLE, - [DaemonInfo.DaemonStatus.STOPPED]: ICON_DAEMON_STOPPED, - [DaemonInfo.DaemonStatus.STOPPING]: ICON_DAEMON_STOPPED, - [DaemonInfo.DaemonStatus.CANCELED]: ICON_DAEMON_STOPPED, + [DaemonStatus.BUSY]: ICON_DAEMON_BUSY, + [DaemonStatus.IDLE]: ICON_DAEMON_IDLE, + [DaemonStatus.STOPPED]: ICON_DAEMON_STOPPED, + [DaemonStatus.STOPPING]: ICON_DAEMON_STOPPED, + [DaemonStatus.CANCELED]: ICON_DAEMON_STOPPED, }; diff --git a/extension/src/views/gradleDaemons/GradleDaemonTreeItem.ts b/extension/src/views/gradleDaemons/GradleDaemonTreeItem.ts index d259cb4f7..5b2295c7a 100644 --- a/extension/src/views/gradleDaemons/GradleDaemonTreeItem.ts +++ b/extension/src/views/gradleDaemons/GradleDaemonTreeItem.ts @@ -1,18 +1,8 @@ import * as vscode from "vscode"; import * as path from "path"; -import { DaemonInfo } from "../../proto/gradle_pb"; import { DAEMON_ICON_MAP } from "../constants"; - -interface StatusEnumMapByValue { - [key: number]: string; -} - -const daemonStatusEnumMapByValue: StatusEnumMapByValue = Object.assign( - {}, - ...Object.entries(DaemonInfo.DaemonStatus).map(([a, b]) => ({ - [b]: a, - })) -); +import { DaemonInfo } from "./models/DaemonInfo"; +import { DaemonStatus } from "./models/DaemonStatus"; export class GradleDaemonTreeItem extends vscode.TreeItem { private status: string; @@ -27,7 +17,7 @@ export class GradleDaemonTreeItem extends vscode.TreeItem { light: this.context.asAbsolutePath(path.join("resources", "light", iconName)), dark: this.context.asAbsolutePath(path.join("resources", "dark", iconName)), }; - this.status = daemonStatusEnumMapByValue[daemonInfo.getStatus()]; + this.status = DaemonStatus[daemonInfo.getStatus()]; this.description = this.status; this.contextValue = this.status.toLowerCase(); this.tooltip = `${this.status} - ${daemonInfo.getInfo()}`; diff --git a/extension/src/views/gradleDaemons/GradleDaemonsTreeDataProvider.ts b/extension/src/views/gradleDaemons/GradleDaemonsTreeDataProvider.ts index e79e3d9c0..3a9450891 100644 --- a/extension/src/views/gradleDaemons/GradleDaemonsTreeDataProvider.ts +++ b/extension/src/views/gradleDaemons/GradleDaemonsTreeDataProvider.ts @@ -1,12 +1,11 @@ import * as vscode from "vscode"; import { GradleDaemonTreeItem } from "."; -import { GradleClient } from "../../client"; -import { DaemonInfo } from "../../proto/gradle_pb"; import { RootProjectsStore } from "../../stores"; import { getShowStoppedDaemons, setShowStoppedDaemons } from "../../util/config"; import { Deferred } from "../../util/Deferred"; import { HintItem } from "../gradleTasks/HintItem"; - +import { GradleStatus } from "./services/GradleStatus"; +import { DaemonStatus } from "./models/DaemonStatus"; export class GradleDaemonsTreeDataProvider implements vscode.TreeDataProvider { private cancelDeferred?: Deferred; private treeItems: vscode.TreeItem[] = []; @@ -17,8 +16,7 @@ export class GradleDaemonsTreeDataProvider implements vscode.TreeDataProvider cancellationToken.cancel()); const projectRootFolders = await this.getProjectRootFolders(); - const promises: Promise[] = projectRootFolders.map((projectRootFolder) => - this.client.getDaemonsStatus(projectRootFolder, cancellationToken.token).then((daemonsStatusReply) => { - if (!daemonsStatusReply) { - return []; - } - let daemonInfoList = daemonsStatusReply.getDaemonInfoList(); - if (!getShowStoppedDaemons()) { - daemonInfoList = daemonInfoList.filter((daemonInfo) => { - return daemonInfo.getStatus() !== DaemonInfo.DaemonStatus.STOPPED; - }); - } - return daemonInfoList.map( - (daemonInfo) => new GradleDaemonTreeItem(this.context, daemonInfo.getPid(), daemonInfo) + const promises: Promise[] = projectRootFolders.map(async (projectRootFolder) => { + const daemonInfos = await GradleStatus.getDaemonsStatusList(projectRootFolder); + + let filteredDaemonInfos = daemonInfos; + if (!getShowStoppedDaemons()) { + filteredDaemonInfos = daemonInfos.filter( + (daemonInfo) => daemonInfo.getStatus() !== DaemonStatus.STOPPED ); - }) - ); + } + + return filteredDaemonInfos.map( + (daemonInfo) => new GradleDaemonTreeItem(this.context, daemonInfo.getPid(), daemonInfo) + ); + }); + this.treeItems = await Promise.race([ Promise.all(promises).then((items) => items.flat()), this.cancelDeferred.promise, diff --git a/extension/src/views/gradleDaemons/models/DaemonInfo.ts b/extension/src/views/gradleDaemons/models/DaemonInfo.ts new file mode 100644 index 000000000..b1974ed56 --- /dev/null +++ b/extension/src/views/gradleDaemons/models/DaemonInfo.ts @@ -0,0 +1,29 @@ +import { DaemonStatus } from "./DaemonStatus"; + +export class DaemonInfo { + constructor(private pid: string, private status: DaemonStatus, private info: string) {} + + public getPid(): string { + return this.pid; + } + + public getStatus(): DaemonStatus { + return this.status; + } + + public getInfo(): string { + return this.info; + } + + public setStatus(status: DaemonStatus): void { + this.status = status; + } + + public setInfo(info: string): void { + this.info = info; + } + + public setPid(pid: string): void { + this.pid = pid; + } +} diff --git a/extension/src/views/gradleDaemons/models/DaemonStatus.ts b/extension/src/views/gradleDaemons/models/DaemonStatus.ts new file mode 100644 index 000000000..56a64c354 --- /dev/null +++ b/extension/src/views/gradleDaemons/models/DaemonStatus.ts @@ -0,0 +1,7 @@ +export enum DaemonStatus { + IDLE = "IDLE", + BUSY = "BUSY", + STOPPED = "STOPPED", + STOPPING = "STOPPING", + CANCELED = "CANCELED", +} diff --git a/extension/src/views/gradleDaemons/models/GradleConnectionType.ts b/extension/src/views/gradleDaemons/models/GradleConnectionType.ts new file mode 100644 index 000000000..2674a24c5 --- /dev/null +++ b/extension/src/views/gradleDaemons/models/GradleConnectionType.ts @@ -0,0 +1,5 @@ +export enum GradleConnectionType { + WRAPPER, + LOCALINSTALLATION, + SPECIFICVERSION, +} diff --git a/extension/src/views/gradleDaemons/services/GradleExecution.ts b/extension/src/views/gradleDaemons/services/GradleExecution.ts new file mode 100644 index 000000000..f9ab54e9f --- /dev/null +++ b/extension/src/views/gradleDaemons/services/GradleExecution.ts @@ -0,0 +1,3 @@ +export interface GradleExecution { + exec(args: string[]): Promise; +} diff --git a/extension/src/views/gradleDaemons/services/GradleLocalInstallation.ts b/extension/src/views/gradleDaemons/services/GradleLocalInstallation.ts new file mode 100644 index 000000000..01080782c --- /dev/null +++ b/extension/src/views/gradleDaemons/services/GradleLocalInstallation.ts @@ -0,0 +1,27 @@ +import { GradleExecution } from "./GradleExecution"; +import { execAsync } from "../../../util/execAsync"; +export class GradleLocalInstallation implements GradleExecution { + private gradleHomePath: string; + + constructor(gradleHomePath: string) { + this.gradleHomePath = gradleHomePath; + } + + public async exec(args: string[]): Promise { + if (args.length === 0) { + throw new Error("No gradle args supplied"); + } + + const command = `${this.gradleHomePath} ${args.join(" ")}`; + + try { + const { stdout, stderr } = await execAsync(command); + if (stderr) { + throw new Error(`Error running gradle: ${stderr}`); + } + return stdout; + } catch (error) { + throw new Error(`Error running gradle: ${error.message}`); + } + } +} diff --git a/extension/src/views/gradleDaemons/services/GradleStatus.ts b/extension/src/views/gradleDaemons/services/GradleStatus.ts new file mode 100644 index 000000000..46f7fa46a --- /dev/null +++ b/extension/src/views/gradleDaemons/services/GradleStatus.ts @@ -0,0 +1,71 @@ +import { DaemonInfo } from "../models/DaemonInfo"; +import { DaemonStatus } from "../models/DaemonStatus"; +import { getGradleConfig } from "../../../util/config"; +import { GradleConfig } from "../../../proto/gradle_pb"; +import { GradleWrapper } from "./GradleWrapper"; +import { GradleLocalInstallation } from "./GradleLocalInstallation"; +import { GradleConnectionType } from "../models/GradleConnectionType"; +export class GradleStatus { + public static async getConnectionType(gradleConfig: GradleConfig): Promise { + if (gradleConfig.getWrapperEnabled()) { + return GradleConnectionType.WRAPPER; + } else { + if (gradleConfig.getVersion()) { + return GradleConnectionType.SPECIFICVERSION; + } else if (gradleConfig.getGradleHome()) { + return GradleConnectionType.LOCALINSTALLATION; + } + return GradleConnectionType.WRAPPER; + } + } + + private static async getDaemonsStatusOutput(gradleConfig: GradleConfig, projectRoot: string): Promise { + const connectionType = await this.getConnectionType(gradleConfig); + switch (connectionType) { + case GradleConnectionType.WRAPPER: + if (await GradleWrapper.hasValidWrapper(projectRoot)) { + const wrapper = new GradleWrapper(projectRoot); + return wrapper.exec(["--status", "quiet"]); + } + return ""; + case GradleConnectionType.LOCALINSTALLATION: + const localInstallation = new GradleLocalInstallation(gradleConfig.getGradleHome()); + return localInstallation.exec(["--status", "quiet"]); + case GradleConnectionType.SPECIFICVERSION: + return ""; + default: + throw new Error("Unknown connection type"); + } + } + + public static async getDaemonsStatusList(projectRoot: string): Promise { + const gradleConfig = getGradleConfig(); + const output = await this.getDaemonsStatusOutput(gradleConfig, projectRoot); + + return this.parseDaemonInfo(output); + } + + public static parseDaemonInfo(output: string): DaemonInfo[] { + if (!output) return []; + + const lines = output.split(/\r?\n/); + const daemonInfos: DaemonInfo[] = []; + + const statusRegex = /^\s*([0-9]+)\s+(\w+)\s+(.+)$/; + + lines.forEach((line) => { + const match = line.match(statusRegex); + if (match) { + const pid = match[1]; + const statusString = match[2]; + const info = match[3]; + + const status = statusString as DaemonStatus; + + daemonInfos.push(new DaemonInfo(pid, status, info)); + } + }); + + return daemonInfos; + } +} diff --git a/extension/src/views/gradleDaemons/services/GradleWrapper.ts b/extension/src/views/gradleDaemons/services/GradleWrapper.ts new file mode 100644 index 000000000..210d48b61 --- /dev/null +++ b/extension/src/views/gradleDaemons/services/GradleWrapper.ts @@ -0,0 +1,36 @@ +import * as fse from "fs-extra"; +import { execAsync } from "../../../util/execAsync"; +import { GradleExecution } from "./GradleExecution"; +import * as path from "path"; + +export class GradleWrapper implements GradleExecution { + private gradleWrapperPath: string; + constructor(private projectRoot: string) { + const wrapperName = process.platform === "win32" ? "gradlew.bat" : "gradlew"; + this.gradleWrapperPath = path.join(projectRoot, wrapperName); + } + + public async exec(args: string[]): Promise { + if (args.length === 0) { + throw new Error("No wrapper args supplied"); + } + + const command = `${this.gradleWrapperPath} ${args.join(" ")}`; + try { + const { stdout, stderr } = await execAsync(command, { cwd: this.projectRoot }); + if (stderr) { + throw new Error(`Error running gradle wrapper: ${stderr}`); + } + return stdout; + } catch (error) { + throw new Error(`Error running gradle wrapper: ${error.message}`); + } + } + + public static async hasValidWrapper(projectRoot: string): Promise { + const propertiesPath = path.join(projectRoot, "gradle", "wrapper", "gradle-wrapper.properties"); + + const hasProperties = await fse.pathExists(propertiesPath); + return hasProperties; + } +}