diff --git a/.eslintrc.js b/.eslintrc.js index 76239551..b4f25b6e 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -20,5 +20,5 @@ module.exports = { "no-throw-literal": "warn", "semi": "off" }, - ignorePatterns: ["out/"] + ignorePatterns: ["out/", "dist/", "node_modules/"] }; \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 3dcb6931..beb7399d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,48 @@ # PDDL support - What's new? +## [2.22.0] + +- When one or two PDDL files (i.e. domain and/or problem) are selected in the File Explorer and the context menu includes the _PDDL: Run the planner and display the plan_ command. This is extra helpful, when the domain and problem files are in different folders. +- When planner fails, the correct 're-configure planner' action is used (currently it opens the Overview Page). Till now, the UI was offering an obsolete planner re-configuration option. +- When planner fails, custom `PlannerProvider` can offer trouble-shooting actions e.g. start/re-start the service, authenticate, ... +- Planning servers (those planner configurations that populate the `url` property), may also specify the `path` property, which is assumed to be a program/script that starts the service that serves the `url`. The extension now offers to start the service. +- Planner configuration may specify the `cwd` current working directory. + +### Engineering work + +In order to give planner implementers more flexibility to create the planner and handle its lifecycle, the API exposed by this extension changed to: + +```typescript +import { planner } from "pddl-workspace"; +import { Event } from "vscode"; + + +export declare interface PddlExtensionApi { + pddlWorkspace: PddlWorkspace; + plannerExecutableFactory: PlannerExecutableFactory; +} + +/** Creates instances of the PlannerExecutable, so other extensions could wrap them. */ +export declare class PlannerExecutableFactory { + /** + * Creates new instance of `PlannerExecutable`. + * @param plannerPath planner path + * @param plannerRunConfiguration run configuration + * @param providerConfiguration provider configuration + * @returns planner executable that VS Code will call the `plan()` method on. + */ + createPlannerExecutable(plannerPath: string, plannerRunConfiguration: planner.PlannerExecutableRunConfiguration, + providerConfiguration: planner.ProviderConfiguration): PlannerExecutable & planner.Planner; +} + +export declare interface PlannerExecutable { + /** + * Event fired when the planner process exits. The value is the exit code. + */ + onExited: Event; +} +``` + ## 2.21.8 - replaced the planner output target icon for the search debugger @@ -1358,7 +1401,8 @@ Note for open source contributors: all notable changes to the "pddl" extension w Check [Keep a Changelog](http://keepachangelog.com/) for recommendations on how to structure this file. -[Unreleased]: https://github.com/jan-dolejsi/vscode-pddl/compare/v2.21.0...HEAD +[Unreleased]: https://github.com/jan-dolejsi/vscode-pddl/compare/v2.22.0...HEAD +[2.22.0]:https://github.com/jan-dolejsi/vscode-pddl/compare/v2.21.0...v2.22.0 [2.21.0]:https://github.com/jan-dolejsi/vscode-pddl/compare/v2.20.0...v2.21.0 [2.20.0]:https://github.com/jan-dolejsi/vscode-pddl/compare/v2.19.0...v2.20.0 [2.19.0]:https://github.com/jan-dolejsi/vscode-pddl/compare/v2.18.0...v2.19.0 diff --git a/README.md b/README.md index 738b7f2a..406cee39 100644 --- a/README.md +++ b/README.md @@ -141,9 +141,11 @@ The planner can be invoked in the context of a currently edited PDDL file. There There are multiple scenarios supported: * if command is invoked on the domain file, - * and if single corresponding problem file is open, the planner will run without asking further questions - * and if multiple corresponding problem files are open, the list of applicable problem files will appear and the user will select one. -* if command is invoked on a problem file, the domain file (if located in the same folder) will be selected automatically. + * and if single corresponding problem file exists in the same directory, the planner will run without asking further questions + * and if multiple corresponding problem files exist in the same directory, the list of applicable problem files will appear and the user will select one. +* if command is invoked on a problem file, the domain file (if located in the same folder) will be selected automatically (as long as it is unique). + +To invoke the planner on domain/problem pair that are located in different directories, multi-select them both on the File Explorer tree and select the _PDDL: Run the planner and visualize the plan_ option from the context menu. Domain, problem and plan/happenings files correspond to each other, if: diff --git a/package.json b/package.json index 05bb44b7..550457ca 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "description": "Planning Domain Description Language support", "author": "Jan Dolejsi", "license": "MIT", - "version": "2.21.8", + "version": "2.22.0", "publisher": "jan-dolejsi", "engines": { "vscode": "^1.50.0", @@ -469,6 +469,11 @@ } ], "explorer/context": [ + { + "command": "pddl.planAndDisplayResult", + "when": "resourceLangId == pddl", + "group": "navigation" + }, { "command": "pddl.plan.compareNormalized", "when": "viewItem != folder && resourceLangId == plan", @@ -703,6 +708,10 @@ "type": "string", "description": "Path to a local executable." }, + "cwd": { + "type": "string", + "description": "Current working directory (optional). Normally, the extension sets the cwd of the process to the directory containing he domain or the problem." + }, "syntax": { "type": "string", "description": "Command-line syntax structure.", @@ -998,22 +1007,23 @@ "test": "npm run test:unit && npm run test:integration" }, "dependencies": { - "ai-planning-val": "^2.6.2", + "ai-planning-val": "^2.7.1", "await-notify": "^1.0.1", "body-parser": "^1.19.0", - "events": "^3.1.0", + "events": "^3.3.0", "express": "^4.17.1", - "jsonc-parser": "^2.2.1", + "jsonc-parser": "^3.0.0", "open": "^7.0.2", - "pddl-gantt": "^1.5.5", - "pddl-workspace": "^6.3.0", + "pddl-gantt": "^1.5.6", + "pddl-planning-service-client": "0.0.1", + "pddl-workspace": "^7.0.0", "request": "^2.88.2", "semver": "^7.1.3", "tree-kill": "^1.2.2", "vscode-debugadapter": "1.38.0", "vscode-debugprotocol": "1.38.0", "vscode-extension-telemetry-wrapper": "^0.5.0", - "vscode-uri": "^2.1.1" + "vscode-uri": "^3.0.2" }, "devDependencies": { "@types/adm-zip": "^0.4.32", diff --git a/src/configuration/PlannersConfiguration.ts b/src/configuration/PlannersConfiguration.ts index 5f95acd9..064e86c8 100644 --- a/src/configuration/PlannersConfiguration.ts +++ b/src/configuration/PlannersConfiguration.ts @@ -12,7 +12,7 @@ import { PddlWorkspace, planner } from 'pddl-workspace'; import { CommandPlannerProvider, SolveServicePlannerProvider, RequestServicePlannerProvider, ExecutablePlannerProvider, Popf, JavaPlannerProvider, Lpg } from './plannerConfigurations'; import { CONF_PDDL, PDDL_PLANNER, EXECUTABLE_OR_SERVICE, EXECUTABLE_OPTIONS } from './configuration'; import { instrumentOperationAsVsCodeCommand } from 'vscode-extension-telemetry-wrapper'; -import { showError, jsonNodeToRange, fileExists, isHttp } from '../utils'; +import { showError, jsonNodeToRange, fileOrFolderExists, isHttp } from '../utils'; export const CONF_PLANNERS = "planners"; export const CONF_SELECTED_PLANNER = "selectedPlanner"; @@ -45,7 +45,7 @@ export class PlannersConfiguration { private plannerSelector: StatusBarItem | undefined; private plannerOutputSelector: StatusBarItem | undefined; - constructor(context: ExtensionContext, private pddlWorkspace: PddlWorkspace) { + constructor(private context: ExtensionContext, private pddlWorkspace: PddlWorkspace) { context.subscriptions.push(instrumentOperationAsVsCodeCommand(PDDL_ADD_PLANNER, () => this.createPlannerConfiguration().catch(showError))); context.subscriptions.push(instrumentOperationAsVsCodeCommand(PDDL_GET_SELECTED_PLANNER, () => this.getSelectedPlanner()?.configuration)); context.subscriptions.push(instrumentOperationAsVsCodeCommand(PDDL_SELECT_PLANNER, async () => (await this.selectPlanner())?.configuration)); @@ -116,7 +116,7 @@ export class PlannersConfiguration { } const plannerProvider = this.pddlWorkspace.getPlannerRegistrar() - .getPlannerProvider({ kind: plannerConfiguration.configuration.kind }); + .getPlannerProvider(new planner.PlannerKind(plannerConfiguration.configuration.kind)); if (!plannerProvider) { new Error(`Planner provider for '${plannerConfiguration.configuration.kind}' is not currently available. Are you missing an extension?`); @@ -159,8 +159,8 @@ export class PlannersConfiguration { const config = workspace.getConfiguration(PDDL_PLANNER, workspaceFolder); const migratedPlanner = isHttp(legacyPlanner) ? legacyPlanner.endsWith('/solve') - ? new SolveServicePlannerProvider().createPlannerConfiguration(legacyPlanner) - : new RequestServicePlannerProvider().createPlannerConfiguration(legacyPlanner) + ? new SolveServicePlannerProvider([]).createPlannerConfiguration(legacyPlanner) + : new RequestServicePlannerProvider([]).createPlannerConfiguration(legacyPlanner) : new CommandPlannerProvider().createPlannerConfiguration(legacyPlanner, legacySyntax); const target = this.toConfigurationTarget(scope); @@ -212,8 +212,8 @@ export class PlannersConfiguration { [ new ExecutablePlannerProvider(), new CommandPlannerProvider(), - new SolveServicePlannerProvider(), - new RequestServicePlannerProvider(), + new SolveServicePlannerProvider(this.context.subscriptions), + new RequestServicePlannerProvider(this.context.subscriptions), new Popf(), new Lpg(), new JavaPlannerProvider(), @@ -546,11 +546,11 @@ export class PlannersConfiguration { async toDocumentAndRange(setting: { fileUri: Uri | undefined; settingRootPath: (string | number)[] }, index?: number): Promise<{ settingsDoc: TextDocument; range: Range } | undefined> { if (!setting.fileUri) { return undefined; } - const exists = await fileExists(setting.fileUri); + const exists = await fileOrFolderExists(setting.fileUri); if (!exists) { return undefined; } const settingsText = await workspace.fs.readFile(setting.fileUri); const settingsRoot = parseTree(settingsText.toString()); - + if (!settingsRoot) { return undefined; } let path = setting.settingRootPath.concat([CONF_PDDL + '.' + CONF_PLANNERS]); if (index !== undefined) { path = path.concat([index]); diff --git a/src/configuration/configuration.ts b/src/configuration/configuration.ts index 1e88f21f..da4cc462 100644 --- a/src/configuration/configuration.ts +++ b/src/configuration/configuration.ts @@ -22,7 +22,6 @@ const PARSER_SERVICE_AUTHENTICATION_S_TOKEN = PDDL_PARSER + '.serviceAuthenticat export const PDDL_PLANNER = 'pddlPlanner'; export const PLANNER_EXECUTABLE_OR_SERVICE = PDDL_PLANNER + '.' + EXECUTABLE_OR_SERVICE; -const PLANNER_EXECUTABLE_OPTIONS = PDDL_PLANNER + '.' + EXECUTABLE_OPTIONS; const PLANNER_SERVICE_AUTHENTICATION_REFRESH_TOKEN = PDDL_PLANNER + '.serviceAuthenticationRefreshToken'; const PLANNER_SERVICE_AUTHENTICATION_ACCESS_TOKEN = PDDL_PLANNER + '.serviceAuthenticationAccessToken'; const PLANNER_SERVICE_AUTHENTICATION_S_TOKEN = PDDL_PLANNER + '.serviceAuthenticationSToken'; @@ -245,88 +244,6 @@ export class PddlConfiguration { configuration.update(PLANNER_SERVICE_AUTHENTICATION_S_TOKEN, stoken, target); } - /** - * @deprecated - */ - async getPlannerPath(workingFolder?: Uri): Promise { - let plannerPath = workspace.getConfiguration(PDDL_PLANNER, workingFolder).get(EXECUTABLE_OR_SERVICE); - - if (!plannerPath) { - plannerPath = await this.askNewPlannerPath(); - } - - return plannerPath; // this may be 'undefined', if the user canceled - } - - /** - * @deprecated - */ - async askNewPlannerPath(): Promise { - const existingValue = workspace.getConfiguration(PDDL_PLANNER, null).get(EXECUTABLE_OR_SERVICE); - - let newPlannerPath = await window.showInputBox({ - prompt: "Enter PDDL planner path local command or web service URL", - placeHolder: `planner.exe OR java -jar c:\\planner.jar OR http://solver.planning.domains/solve`, - value: existingValue, - ignoreFocusOut: true - }); - - if (newPlannerPath) { - - newPlannerPath = newPlannerPath.trim().replace(/\\/g, '/'); - - // todo: validate that this planner actually works by sending a dummy request to it - - const newPlannerScope = await this.askConfigurationScope(); - - if (!newPlannerScope) { return undefined; } - const configurationToUpdate = this.getConfigurationForScope(newPlannerScope); - if (!configurationToUpdate) { return undefined; } - - if (!isHttp(newPlannerPath)) { - this.askPlannerSyntax(newPlannerScope); - } - - // Update the value in the target - configurationToUpdate.update(PLANNER_EXECUTABLE_OR_SERVICE, newPlannerPath, newPlannerScope.target); - } - - return newPlannerPath; - } - - /** - * @deprecated - */ - async askPlannerSyntax(scope: ScopeQuickPickItem): Promise { - const existingValue = workspace.getConfiguration().get(PLANNER_EXECUTABLE_OPTIONS); - - const newPlannerOptions = await window.showInputBox({ - prompt: "In case you use command line switches and options, override the default syntax. For more info, see (the wiki)[https://github.com/jan-dolejsi/vscode-pddl/wiki/Configuring-the-PDDL-planner].", - placeHolder: `$(planner) $(options) $(domain) $(problem)`, - value: existingValue, - ignoreFocusOut: true - }); - - if (newPlannerOptions) { - // todo: validate that this planner actually works by sending a dummy request to it - - const configurationToUpdate = this.getConfigurationForScope(scope); - if (!configurationToUpdate) { return undefined; } - - // Update the value in the target - configurationToUpdate.update(PLANNER_EXECUTABLE_OPTIONS, newPlannerOptions, scope.target); - } - - return newPlannerOptions; - } - - /** - * @deprecated - */ - getPlannerSyntax(): string | undefined { - return workspace.getConfiguration().get(PLANNER_EXECUTABLE_OPTIONS); - } - getValueSeqPath(): string | undefined { const configuredPath = workspace.getConfiguration().get(PLANNER_VALUE_SEQ_PATH); return ensureAbsoluteGlobalStoragePath(configuredPath, this.context); diff --git a/src/configuration/plannerConfigurations.ts b/src/configuration/plannerConfigurations.ts index 7c370eed..ff37973b 100644 --- a/src/configuration/plannerConfigurations.ts +++ b/src/configuration/plannerConfigurations.ts @@ -11,6 +11,8 @@ import { Uri, window } from 'vscode'; import { planner, OutputAdaptor, utils } from 'pddl-workspace'; import { isHttp } from '../utils'; import { PlannerExecutable } from '../planning/PlannerExecutable'; +import { AsyncServiceConfiguration, PlannerAsyncService, PlannerSyncService } from 'pddl-planning-service-client'; +import { LongRunningPlannerProvider } from './plannerExecution'; export class CommandPlannerProvider implements planner.PlannerProvider { get kind(): planner.PlannerKind { @@ -86,7 +88,7 @@ export class CommandPlannerProvider implements planner.PlannerProvider { } } -export class SolveServicePlannerProvider implements planner.PlannerProvider { +export class SolveServicePlannerProvider extends LongRunningPlannerProvider { get kind(): planner.PlannerKind { return planner.WellKnownPlannerKind.SERVICE_SYNC; } @@ -112,15 +114,16 @@ export class SolveServicePlannerProvider implements planner.PlannerProvider { if (!newPlannerUrl) { return undefined; } - return this.createPlannerConfiguration(newPlannerUrl); + return this.createPlannerConfiguration(newPlannerUrl, previousConfiguration); } - createPlannerConfiguration(newPlannerUrl: string): planner.PlannerConfiguration { + createPlannerConfiguration(newPlannerUrl: string, previousConfiguration?: planner.PlannerConfiguration): planner.PlannerConfiguration { return { kind: this.kind.kind, url: newPlannerUrl, title: newPlannerUrl, - canConfigure: true + canConfigure: true, + path: previousConfiguration?.path }; } @@ -128,9 +131,28 @@ export class SolveServicePlannerProvider implements planner.PlannerProvider { showHelp(_output: OutputAdaptor): void { throw new Error("Method not implemented."); } + + /** Custom `Planner` implementation. */ + createPlanner(configuration: planner.PlannerConfiguration, plannerInvocationOptions: planner.PlannerRunConfiguration): planner.Planner { + return SolveServicePlannerProvider.createDefaultPlanner(configuration, plannerInvocationOptions, this); + } + + /** Default `Planner` implementation. */ + static createDefaultPlanner(configuration: planner.PlannerConfiguration, plannerInvocationOptions: planner.PlannerRunConfiguration, plannerProvider?: planner.PlannerProvider): planner.Planner { + if (!configuration.url) { + throw new Error(`Planner ${configuration.title} does not specify 'url'.`); + } + + const providerConfiguration: planner.ProviderConfiguration = { + configuration: configuration, + provider: plannerProvider + } + + return new PlannerSyncService(configuration.url, plannerInvocationOptions, providerConfiguration); + } } -export class RequestServicePlannerProvider implements planner.PlannerProvider { +export class RequestServicePlannerProvider extends LongRunningPlannerProvider { get kind(): planner.PlannerKind { return planner.WellKnownPlannerKind.SERVICE_ASYNC; } @@ -157,15 +179,16 @@ export class RequestServicePlannerProvider implements planner.PlannerProvider { if (!newPlannerUrl) { return undefined; } - return this.createPlannerConfiguration(newPlannerUrl); + return this.createPlannerConfiguration(newPlannerUrl, previousConfiguration); } - createPlannerConfiguration(newPlannerUrl: string): planner.PlannerConfiguration { + createPlannerConfiguration(newPlannerUrl: string, previousConfiguration?: planner.PlannerConfiguration): planner.PlannerConfiguration { return { kind: this.kind.kind, url: newPlannerUrl, title: newPlannerUrl, - canConfigure: true + canConfigure: true, + path: previousConfiguration?.path }; } @@ -173,6 +196,24 @@ export class RequestServicePlannerProvider implements planner.PlannerProvider { showHelp(_output: OutputAdaptor): void { throw new Error("Method not implemented."); } + + /** Custom `Planner` implementation. */ + createPlanner(configuration: planner.PlannerConfiguration, plannerInvocationOptions: planner.PlannerRunConfiguration): planner.Planner { + return RequestServicePlannerProvider.createDefaultPlanner(configuration, plannerInvocationOptions as AsyncServiceConfiguration, this); + } + + /** Default `Planner` implementation. */ + static createDefaultPlanner(configuration: planner.PlannerConfiguration, plannerInvocationOptions: AsyncServiceConfiguration, plannerProvider?: planner.PlannerProvider): planner.Planner { + if (configuration.url === undefined) { + throw new Error(`Planner ${configuration.title} does not specify 'url'.`); + } + + const providerConfiguration: planner.ProviderConfiguration = { + configuration: configuration, + provider: plannerProvider + } + return new PlannerAsyncService(configuration.url, plannerInvocationOptions, providerConfiguration); + } } export class JavaPlannerProvider implements planner.PlannerProvider { @@ -207,12 +248,18 @@ export class JavaPlannerProvider implements planner.PlannerProvider { showHelp(_output: OutputAdaptor): void { // do nothing } - createPlanner(configuration: planner.PlannerConfiguration, plannerOptions: string, workingDirectory: string): planner.Planner { - if (configuration.path === undefined || configuration.syntax === undefined) { - throw new Error('Incomplete planner configuration. Mandatory attributes: path and syntax'); + createPlanner(configuration: planner.PlannerConfiguration, plannerRunConfiguration: planner.PlannerRunConfiguration): planner.Planner { + if (!configuration.path) { + throw new Error('Incomplete planner configuration. Mandatory attributes: path'); } - return new PlannerExecutable(`java -jar ${utils.Util.q(configuration.path)}`, plannerOptions, configuration.syntax, workingDirectory); + const providerConfiguration: planner.ProviderConfiguration = { + configuration: configuration, + provider: this + } + + return new PlannerExecutable(`java -jar ${utils.Util.q(configuration.path)}`, + plannerRunConfiguration as planner.PlannerExecutableRunConfiguration, providerConfiguration); } } @@ -258,7 +305,7 @@ export class ExecutablePlannerProvider implements planner.PlannerProvider { export class Popf implements planner.PlannerProvider { get kind(): planner.PlannerKind { - return { kind: 'popf' }; + return new planner.PlannerKind('popf'); } getNewPlannerLabel(): string { @@ -320,7 +367,7 @@ export class Popf implements planner.PlannerProvider { export class Lpg implements planner.PlannerProvider { get kind(): planner.PlannerKind { - return { kind: 'lpg-td' }; + return new planner.PlannerKind('lpg-td'); } getNewPlannerLabel(): string { @@ -353,6 +400,25 @@ export class Lpg implements planner.PlannerProvider { return newPlannerConfiguration; } + createPlanner(configuration: planner.PlannerConfiguration, plannerRunConfiguration: planner.PlannerRunConfiguration): planner.Planner { + if (!plannerRunConfiguration.options) { + // ensure mandatory option is set + plannerRunConfiguration.options = "-n 1"; + } + + if (!configuration.path) { + throw new Error('Incomplete planner configuration. Mandatory attributes: path'); + } + + const providerConfiguration: planner.ProviderConfiguration = { + configuration: configuration, + provider: this + } + + return new PlannerExecutable(configuration.path, + plannerRunConfiguration as planner.PlannerExecutableRunConfiguration, providerConfiguration); + } + getPlannerOptions(): planner.PlannerOption[] { // see https://lpg.unibs.it/lpg/README-LPGTD return [ diff --git a/src/configuration/plannerExecution.ts b/src/configuration/plannerExecution.ts new file mode 100644 index 00000000..d842aa8d --- /dev/null +++ b/src/configuration/plannerExecution.ts @@ -0,0 +1,114 @@ +/* -------------------------------------------------------------------------------------------- + * Copyright (c) Jan Dolejsi 2021. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + * ------------------------------------------------------------------------------------------ */ + +/* Warning: This file is shared by source-code between the PDDL extension and at least one other extension. */ + +'use strict'; + +import * as path from 'path'; +import { instanceOfHttpConnectionRefusedError } from 'pddl-planning-service-client'; +import { planner, utils } from 'pddl-workspace'; +import { Disposable, ShellExecution, Task, TaskDefinition, TaskEndEvent, TaskExecution, TaskRevealKind, tasks, TaskScope, Uri } from 'vscode'; +import { fileOrFolderExists } from '../utils'; + + +/** Tracks a long-running planning service execution. */ +export class PlanningServiceExecution { + + constructor(public readonly servicePath: string, public readonly wasAlreadyRunning: boolean, + public readonly execution?: TaskExecution) { + } + + static alreadyRunning(servicePath: string): PlanningServiceExecution { + return new PlanningServiceExecution(servicePath, true); + } + + static justStarted(servicePath: string, execution: TaskExecution): PlanningServiceExecution { + return new PlanningServiceExecution(servicePath, false, execution); + } +} + + +export abstract class LongRunningPlannerProvider implements planner.PlannerProvider { + + abstract kind: planner.PlannerKind; + abstract getNewPlannerLabel(): string; + abstract configurePlanner(previousConfiguration?: planner.PlannerConfiguration): Promise; + + private planningServiceExecutions = new Map(); + + constructor(disposables: Disposable[]) { + tasks.onDidEndTask(this.handleTaskEnded, this, disposables); + } + + private handleTaskEnded(event: TaskEndEvent): void { + const taskExecution = [...this.planningServiceExecutions.values()].find(e => e.execution === event.execution); + if (taskExecution) { + // yes, a relevant task ended. Clean-up. + this.planningServiceExecutions.delete(taskExecution.servicePath); + } + } + + protected isServiceRunning(configuration: planner.PlannerConfiguration): boolean { + return configuration.path !== undefined && this.planningServiceExecutions.has(configuration.path); + } + + + /** Get troubleshooting options */ + async troubleshoot?(failedPlanner: planner.Planner, reason: unknown): Promise { + let troubleShootingInfo = ''; + const troubleShootings = new Map Promise>(); + + if (instanceOfHttpConnectionRefusedError(reason)) { + const configuration = failedPlanner.providerConfiguration.configuration; + troubleShootingInfo += `Service ${configuration.url} cannot be reached.\n`; + + const isLocal = ['localhost', '127.0.0.1'].includes(reason.address); + const serviceExePath = configuration.path; + if (isLocal && serviceExePath !== undefined) { + const fileName = path.basename(serviceExePath); + if (await fileOrFolderExists(Uri.file(serviceExePath))) { + if (this.isServiceRunning(configuration)) { + troubleShootingInfo += `Service appears to be running, but not responding. Click 'Re-start the service'.\n`; + troubleShootings.set(`Re-start the service`, async () => this.startService(serviceExePath, configuration)); + } else { + troubleShootingInfo += `Service does not appear to be running. Click 'Start the service' to execute '${fileName}'.\n`; + troubleShootings.set(`Start the service`, async () => this.startService(serviceExePath, configuration)); + } + } else { + troubleShootingInfo += `Service does not appear to be running. The configured server '${fileName}' is not a valid file.\n`; + } + } + } + + return { + info: troubleShootingInfo, + options: troubleShootings + }; + } + + protected startService(executablePath: string, configuration: planner.PlannerConfiguration): void { + const taskExecution = new ShellExecution(utils.Util.q(executablePath), { + cwd: configuration.cwd ?? + path.isAbsolute(executablePath) ? path.dirname(executablePath) : undefined + }); + const task = new Task(this.taskDefinition, TaskScope.Global, configuration.title, this.taskSource, taskExecution); + task.presentationOptions = { reveal: TaskRevealKind.Always, echo: true }; + task.isBackground = true; + tasks.executeTask(task).then(taskExecution => { + this.planningServiceExecutions.set(executablePath, PlanningServiceExecution.justStarted(executablePath, taskExecution)); + }); + } + + get taskDefinition(): TaskDefinition { + return { + type: 'PlanningService:' + this.kind.kind + }; + } + + get taskSource(): string { + return 'PDDL'; + } +} \ No newline at end of file diff --git a/src/diagnostics/Diagnostics.ts b/src/diagnostics/Diagnostics.ts index ca17e51d..7db68d47 100644 --- a/src/diagnostics/Diagnostics.ts +++ b/src/diagnostics/Diagnostics.ts @@ -8,7 +8,7 @@ import { Diagnostic, DiagnosticSeverity, DiagnosticCollection, Uri, window, Disposable, workspace } from 'vscode'; -import { Authentication } from '../util/Authentication'; +import { SAuthentication } from '../util/Authentication'; import { PddlWorkspace } from 'pddl-workspace'; import { PlanInfo } from 'pddl-workspace'; import { ProblemInfo } from 'pddl-workspace'; @@ -268,7 +268,7 @@ export class Diagnostics extends Disposable { if (isHttp(newParserPath)) { // is a service - const authentication = new Authentication( + const authentication = new SAuthentication( this.pddlParserSettings.serviceAuthenticationUrl, this.pddlParserSettings.serviceAuthenticationRequestEncoded, this.pddlParserSettings.serviceAuthenticationClientId, diff --git a/src/diagnostics/ValidatorService.ts b/src/diagnostics/ValidatorService.ts index ab66d3b1..38d2aaf3 100644 --- a/src/diagnostics/ValidatorService.ts +++ b/src/diagnostics/ValidatorService.ts @@ -12,22 +12,22 @@ import { Validator } from './validator'; import { ProblemInfo } from 'pddl-workspace'; import { DomainInfo } from 'pddl-workspace'; import { FileStatus } from 'pddl-workspace'; -import { Authentication } from '../util/Authentication'; +import { SAuthentication } from '../util/Authentication'; import request = require('request'); export class ValidatorService extends Validator { - constructor(path: string, private useAuthentication: boolean, private authentication: Authentication) { + constructor(path: string, private useAuthentication: boolean, private authentication: SAuthentication) { super(path); } validate(domainInfo: DomainInfo, problemFiles: ProblemInfo[], onSuccess: (diagnostics: Map) => void, onError: (error: string) => void): void { // eslint-disable-next-line @typescript-eslint/no-explicit-any let requestHeader: any = {}; - if(this.useAuthentication && this.authentication.getSToken()) { + if(this.useAuthentication && this.authentication.getToken()) { requestHeader = { - "Authorization": "Bearer " + this.authentication.getSToken() + "Authorization": "Bearer " + this.authentication.getToken() }; } diff --git a/src/extension.ts b/src/extension.ts index c2697129..0530c35d 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -10,7 +10,7 @@ import { Planning } from './planning/planning'; import { PddlWorkspace } from 'pddl-workspace'; import { PDDL, PLAN, HAPPENINGS } from 'pddl-workspace'; import { PddlConfiguration, PDDL_CONFIGURE_COMMAND } from './configuration/configuration'; -import { Authentication } from './util/Authentication'; +import { SAuthentication } from './util/Authentication'; import { AutoCompletion } from './completion/AutoCompletion'; import { SymbolRenameProvider } from './symbols/SymbolRenameProvider'; import { SymbolInfoProvider } from './symbols/SymbolInfoProvider'; @@ -43,6 +43,7 @@ import { ProblemConstraintsView } from './modelView/ProblemConstraintsView'; import { ModelHierarchyProvider } from './symbols/ModelHierarchyProvider'; import { PlannersConfiguration } from './configuration/PlannersConfiguration'; import { registerPlanReport } from './planReport/planReport'; +import { PlannerExecutableFactory } from './planning/PlannerExecutable'; const PDDL_CONFIGURE_PARSER = 'pddl.configureParser'; const PDDL_LOGIN_PARSER_SERVICE = 'pddl.loginParserService'; @@ -61,14 +62,21 @@ export let ptestExplorer: PTestExplorer | undefined; export let packageJson: ExtensionPackage | undefined; -export async function activate(context: ExtensionContext): Promise { + +/** API this extension exposes to other extensions. */ +interface PddlExtensionApi { + pddlWorkspace: PddlWorkspace; + plannerExecutableFactory: PlannerExecutableFactory; +} + +export async function activate(context: ExtensionContext): Promise { const extensionInfo = new ExtensionInfo(); // initialize the instrumentation wrapper const telemetryKey = process.env.VSCODE_PDDL_TELEMETRY_TOKEN; if (telemetryKey) { - await initialize(extensionInfo.getId(), extensionInfo.getVersion(), telemetryKey); + initialize(extensionInfo.getId(), extensionInfo.getVersion(), telemetryKey); } try { @@ -83,7 +91,7 @@ export async function activate(context: ExtensionContext): Promise { +async function activateWithTelemetry(_operationId: string, context: ExtensionContext): Promise { pddlConfiguration = new PddlConfiguration(context); const valDownloader = new ValDownloader(context).registerCommands(); @@ -278,16 +286,19 @@ async function activateWithTelemetry(_operationId: string, context: ExtensionCon planHoverProvider, planDefinitionProvider, happeningsHoverProvider, happeningsDefinitionProvider, problemInitView, problemObjectsView, problemConstraintsView, configureCommand); - return pddlWorkspace; + return { + pddlWorkspace: pddlWorkspace, + plannerExecutableFactory: new PlannerExecutableFactory() + }; } export function deactivate(): void { // nothing to do } -function createAuthentication(pddlConfiguration: PddlConfiguration): Authentication { +function createAuthentication(pddlConfiguration: PddlConfiguration): SAuthentication { const configuration = pddlConfiguration.getPddlParserServiceAuthenticationConfiguration(); - return new Authentication(configuration.url!, configuration.requestEncoded!, configuration.clientId!, configuration.callbackPort!, configuration.timeoutInMs!, + return new SAuthentication(configuration.url!, configuration.requestEncoded!, configuration.clientId!, configuration.callbackPort!, configuration.timeoutInMs!, configuration.tokensvcUrl!, configuration.tokensvcApiKey!, configuration.tokensvcAccessPath!, configuration.tokensvcValidatePath!, configuration.tokensvcCodePath!, configuration.tokensvcRefreshPath!, configuration.tokensvcSvctkPath!, configuration.refreshToken!, configuration.accessToken!, configuration.sToken!); diff --git a/src/planning/PlannerAsyncService.ts b/src/planning/PlannerAsyncService.ts deleted file mode 100644 index 4a3d37a8..00000000 --- a/src/planning/PlannerAsyncService.ts +++ /dev/null @@ -1,179 +0,0 @@ -/* -------------------------------------------------------------------------------------------- - * Copyright (c) Jan Dolejsi. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - * ------------------------------------------------------------------------------------------ */ -'use strict'; - -import { Uri, workspace } from 'vscode'; -import { Plan, ProblemInfo, DomainInfo, parser, planner } from 'pddl-workspace'; -import { Authentication } from '../util/Authentication'; -import { PlannerService } from './PlannerService'; -import { PlannerConfigurationSelector } from './PlannerConfigurationSelector'; - -/** Wraps the `/request` planning web service interface. */ -export class PlannerAsyncService extends PlannerService { - - timeout = 60; //this default is overridden by info from the configuration! - asyncMode = false; - planTimeScale = 1; - - constructor(plannerPath: string, private plannerConfiguration: Uri, authentication?: Authentication) { - super(plannerPath, authentication); - } - - getTimeout(): number { - return this.timeout; - } - - createUrl(): string { - return this.plannerPath + '?async=' + this.asyncMode; - } - - // eslint-disable-next-line @typescript-eslint/no-explicit-any - async createRequestBody(domainFileInfo: DomainInfo, problemFileInfo: ProblemInfo): Promise { - const configuration = await this.getConfiguration(); - if (!configuration) { return null; } - - configuration.planFormat = configuration.planFormat ?? 'JSON'; - if ("timeout" in configuration) { - this.timeout = configuration.timeout; - } - - this.planTimeScale = PlannerAsyncService.getPlanTimeScale(configuration); - - return { - 'domain': { - 'name': domainFileInfo.name, - 'format': 'PDDL', - 'content': domainFileInfo.getText() - }, - 'problem': { - 'name': problemFileInfo.name, - 'format': 'PDDL', - 'content': problemFileInfo.getText() - }, - 'configuration': configuration - }; - } - - // eslint-disable-next-line @typescript-eslint/no-explicit-any - static getPlanTimeScale(configuration: any): number { - const planTimeUnit = configuration['planTimeUnit']; - switch (planTimeUnit) { - case "MINUTE": - return 60; - case "MILLISECOND": - return 1 / 1000; - case "HOUR": - return 60 * 60; - case "DAY": - return 24 * 60 * 60; - case "WEEK": - return 7 * 24 * 60 * 60; - case "SECOND": - default: - return 1; - } - } - - // eslint-disable-next-line @typescript-eslint/no-explicit-any - async processServerResponseBody(responseBody: any, planParser: parser.PddlPlannerOutputParser, callbacks: planner.PlannerResponseHandler, resolve: (plans: Plan[]) => void, reject: (error: Error) => void): Promise { - let _timedOut = false; - const responseStatus: string = responseBody['status']['status']; - if (["STOPPED", "SEARCHING_BETTER_PLAN"].includes(responseStatus)) { - _timedOut = responseBody['status']['reason'] === "TIMEOUT"; - if (responseBody['plans'].length > 0) { - const plansJson = responseBody['plans']; - try { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const parserPromises = plansJson.map((plan: any) => this.parsePlan(plan, planParser)); - await Promise.all(parserPromises); - } - catch (err) { - reject(err); - } - - const plans = planParser.getPlans(); - if (plans.length > 0) { - callbacks.handleOutput(plans[0].getText() + '\n'); - } - else { - callbacks.handleOutput('No plan found.'); - } - - resolve(plans); - return; - } - else { - // todo: no plan found yet. Poll again later. - resolve([]); - return; - } - } - else if (responseStatus === "FAILED") { - const error = responseBody['status']['error']['message']; - reject(new Error(error)); - return; - } - else if (["NOT_INITIALIZED", "INITIATING", "SEARCHING_INITIAL_PLAN"].includes(responseStatus)) { - _timedOut = true; - const error = `After timeout ${this.timeout} the status is ${responseStatus}`; - reject(new Error(error)); - return; - } - - console.log(_timedOut); - } - - // eslint-disable-next-line @typescript-eslint/no-explicit-any - async parsePlan(plan: any, planParser: parser.PddlPlannerOutputParser): Promise { - const makespan: number = plan['makespan']; - const metric: number = plan['metricValue']; - const searchPerformanceInfo = plan['searchPerformanceInfo']; - const statesEvaluated: number = searchPerformanceInfo['statesEvaluated']; - const elapsedTimeInSeconds = parseFloat(searchPerformanceInfo['timeElapsed']) / 1000; - - planParser.setPlanMetaData(makespan, metric, statesEvaluated, elapsedTimeInSeconds, this.planTimeScale); - - const planFormat: string | undefined = plan['format']; - if (planFormat?.toLowerCase() === 'json') { - const planSteps = JSON.parse(plan['content']); - this.parsePlanSteps(planSteps, planParser); - planParser.onPlanFinished(); - } - else if (planFormat?.toLowerCase() === 'tasks') { - const planText = plan['content']; - planParser.appendLine(planText); - planParser.onPlanFinished(); - } - else if (planFormat?.toLowerCase() === 'xplan') { - const planText = plan['content']; - await planParser.appendXplan(planText); // must await the underlying async xml parsing - } - else { - throw new Error('Unsupported plan format: ' + planFormat); - } - } - - // eslint-disable-next-line @typescript-eslint/no-explicit-any - async getConfiguration(): Promise { - if (this.plannerConfiguration.toString() === PlannerConfigurationSelector.DEFAULT.toString()) { - return this.createDefaultConfiguration(); - } - else { - const configurationAbsPath = this.plannerConfiguration.fsPath; - - const configurationDoc = await workspace.openTextDocument(configurationAbsPath); - const configurationString = configurationDoc.getText(); - return JSON.parse(configurationString); - } - } - - // eslint-disable-next-line @typescript-eslint/no-explicit-any - createDefaultConfiguration(): any { - return { - "planFormat": "JSON", - "timeout": this.timeout - }; - } -} diff --git a/src/planning/PlannerConfigurationSelector.ts b/src/planning/PlannerConfigurationSelector.ts index 917e5aef..1876506a 100644 --- a/src/planning/PlannerConfigurationSelector.ts +++ b/src/planning/PlannerConfigurationSelector.ts @@ -2,8 +2,9 @@ * Copyright (c) Jan Dolejsi. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. * ------------------------------------------------------------------------------------------ */ -import { window, Uri, QuickPickItem } from 'vscode'; +import { window, Uri, QuickPickItem, workspace } from 'vscode'; import * as path from 'path'; +import { AsyncServiceOnlyConfiguration, PlannerAsyncService } from 'pddl-planning-service-client'; export class PlannerConfigurationSelector { @@ -44,6 +45,18 @@ export class PlannerConfigurationSelector { return selectedUris[0]; } + static async loadConfiguration(configurationUri: Uri, timeout: number): Promise { + if (configurationUri.toString() === PlannerConfigurationSelector.DEFAULT.toString()) { + return PlannerAsyncService.createDefaultConfiguration(timeout); + } + else { + const configurationDoc = await workspace.openTextDocument(configurationUri); + const configurationString = configurationDoc.getText(); + return JSON.parse(configurationString); + } + } + + } class PlannerConfigurationItem implements QuickPickItem { diff --git a/src/planning/PlannerExecutable.ts b/src/planning/PlannerExecutable.ts index a2c5d158..7b84c670 100644 --- a/src/planning/PlannerExecutable.ts +++ b/src/planning/PlannerExecutable.ts @@ -7,7 +7,7 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ import { - workspace, window + workspace, window, EventEmitter, Event, Disposable } from 'vscode'; import * as process from 'child_process'; @@ -19,18 +19,36 @@ import { import { Util } from 'ai-planning-val'; -/** Planner implemented as an executable process. */ -export class PlannerExecutable extends planner.Planner { +/** Planner implemented as an executable process, which outputs through VS Code facilities. */ +export class PlannerExecutable extends planner.Planner implements Disposable { // this property stores the reference to the planner child process, while planning is in progress private child: process.ChildProcess | undefined; - + private _exited = new EventEmitter(); + static readonly DEFAULT_SYNTAX = "$(planner) $(options) $(domain) $(problem)"; private readonly plannerSyntax: string; + private readonly plannerOptions: string; + + constructor(plannerPath: string, private configuration: planner.PlannerExecutableRunConfiguration, providerConfiguration: planner.ProviderConfiguration) { + super(plannerPath, configuration, providerConfiguration); + this.plannerSyntax = configuration.plannerSyntax ?? providerConfiguration.configuration.syntax ?? PlannerExecutable.DEFAULT_SYNTAX; + this.plannerOptions = configuration.options ?? ''; + } - constructor(plannerPath: string, private plannerOptions: string, plannerSyntax: string | undefined, private workingDirectory: string) { - super(plannerPath); - this.plannerSyntax = plannerSyntax ?? PlannerExecutable.DEFAULT_SYNTAX; + get onExited(): Event { + return this._exited.event; + } + + dispose(): void { + this._exited.dispose(); + } + + get requiresKeyboardInput(): boolean { + return false; + } + get supportsSearchDebugger(): boolean { + return true; } async plan(domainFileInfo: DomainInfo, problemFileInfo: ProblemInfo, planParser: parser.PddlPlannerOutputParser, callbacks: planner.PlannerResponseHandler): Promise { @@ -49,11 +67,10 @@ export class PlannerExecutable extends planner.Planner { callbacks.handleOutput(command + '\n'); const thisPlanner = this; - super.planningProcessKilled = false; if (workspace.getConfiguration("pddlPlanner").get("executionTarget") === "Terminal") { return new Promise((resolve) => { - const terminal = window.createTerminal({ name: "Planner output", cwd: thisPlanner.workingDirectory }); + const terminal = window.createTerminal({ name: "Planner output", cwd: thisPlanner.configuration.workingDirectory }); terminal.sendText(command, true); terminal.show(true); const plans: Plan[] = []; @@ -63,12 +80,12 @@ export class PlannerExecutable extends planner.Planner { return new Promise(function (resolve, reject) { thisPlanner.child = process.exec(command, - { cwd: thisPlanner.workingDirectory }, + { cwd: thisPlanner.configuration.workingDirectory }, (error) => { planParser.onPlanFinished(); if (error && !thisPlanner.child?.killed && !thisPlanner.planningProcessKilled) { - reject(error); + reject(error); // todo: should calle `return` here? } const plans = planParser.getPlans(); @@ -86,6 +103,7 @@ export class PlannerExecutable extends planner.Planner { thisPlanner.child.on("close", (code: any, signal: any) => { if (code) { console.log("Exit code: " + code); } if (signal) { console.log("Exit Signal: " + signal); } + thisPlanner._exited.fire(code); }); }); } @@ -103,4 +121,20 @@ export class PlannerExecutable extends planner.Planner { treeKill(this.child.pid); } } +} + +/** Creates instances of the PlannerExecutable, so other extensions could wrap them. */ +export class PlannerExecutableFactory { + + /** + * Creates new instance of `PlannerExecutable`. + * @param plannerPath planner path + * @param plannerRunConfiguration run configuration + * @param providerConfiguration provider configuration + * @returns planner executable that VS Code will call the `plan()` method on. + */ + createPlannerExecutable(plannerPath: string, plannerRunConfiguration: planner.PlannerExecutableRunConfiguration, + providerConfiguration: planner.ProviderConfiguration): PlannerExecutable { + return new PlannerExecutable(plannerPath, plannerRunConfiguration, providerConfiguration); + } } \ No newline at end of file diff --git a/src/planning/PlannerService.ts b/src/planning/PlannerService.ts deleted file mode 100644 index 6db5cbcd..00000000 --- a/src/planning/PlannerService.ts +++ /dev/null @@ -1,104 +0,0 @@ -/* -------------------------------------------------------------------------------------------- - * Copyright (c) Jan Dolejsi. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - * ------------------------------------------------------------------------------------------ */ -'use strict'; - -import * as request from 'request'; -import { planner, Plan, ProblemInfo, DomainInfo, parser, PlanStep } from 'pddl-workspace'; -import { Authentication } from '../util/Authentication'; -import { window } from 'vscode'; - -export abstract class PlannerService extends planner.Planner { - - constructor(plannerPath: string, private authentication?: Authentication) { - super(plannerPath); - } - - // eslint-disable-next-line @typescript-eslint/no-explicit-any - abstract createRequestBody(domainFileInfo: DomainInfo, problemFileInfo: ProblemInfo): Promise; - - abstract createUrl(): string; - - // eslint-disable-next-line @typescript-eslint/no-explicit-any - abstract processServerResponseBody(responseBody: any, planParser: parser.PddlPlannerOutputParser, parent: planner.PlannerResponseHandler, - resolve: (plans: Plan[]) => void, reject: (error: Error) => void): void; - - async plan(domainFileInfo: DomainInfo, problemFileInfo: ProblemInfo, planParser: parser.PddlPlannerOutputParser, parent: planner.PlannerResponseHandler): Promise { - parent.handleOutput(`Planning service: ${this.plannerPath}\nDomain: ${domainFileInfo.name}, Problem: ${problemFileInfo.name}\n`); - - // eslint-disable-next-line @typescript-eslint/no-explicit-any - let requestHeader: any = {}; - if (this.authentication && this.authentication.getSToken() !== undefined) { - requestHeader = { - "Authorization": "Bearer " + this.authentication.getSToken() - }; - } - - if (parent.providePlannerOptions({ domain: domainFileInfo, problem: problemFileInfo }).some(op => op.length)) { - window.showWarningMessage("Search Debugger is not supported by planning services. Only planner executable may support it."); - } - - const requestBody = await this.createRequestBody(domainFileInfo, problemFileInfo); - if (!requestBody) { return []; } - const url: string = this.createUrl(); - - const timeoutInSec = this.getTimeout(); - - const that = this; - return new Promise(function (resolve, reject) { - - request.post({ url: url, headers: requestHeader, body: requestBody, json: true, timeout: timeoutInSec * 1000 * 1.1 }, (err, httpResponse, responseBody) => { - - if (err !== null) { - reject(err); - return; - } - - if (that.authentication) { - if (httpResponse) { - if (httpResponse.statusCode === 400) { - const message = "Authentication failed. Please login or update tokens."; - const error = new Error(message); - reject(error); - return; - } - else if (httpResponse.statusCode === 401) { - const message = "Invalid token. Please update tokens."; - const error = new Error(message); - reject(error); - return; - } - } - } - - if (httpResponse && httpResponse.statusCode > 202) { - const notificationMessage = `PDDL Planning Service returned code ${httpResponse.statusCode} ${httpResponse.statusMessage}`; - const error = new Error(notificationMessage); - reject(error); - return; - } - - that.processServerResponseBody(responseBody, planParser, parent, resolve, reject); - }); - }); - } - - /** Gets timeout in seconds. */ - abstract getTimeout(): number; - - // eslint-disable-next-line @typescript-eslint/no-explicit-any - parsePlanSteps(planSteps: any, planParser: parser.PddlPlannerOutputParser): void { - for (let index = 0; index < planSteps.length; index++) { - const planStep = planSteps[index]; - const fullActionName = (planStep["name"] as string).replace('(', '').replace(')', ''); - const time = planStep["time"] ?? (index + 1) * planParser.options.epsilon; - let duration = planStep["duration"]; - const isDurative = duration !== undefined && duration !== null; - duration = duration ?? planParser.options.epsilon; - const planStepObj = new PlanStep(time, fullActionName, isDurative, duration, index); - planParser.appendStep(planStepObj); - } - planParser.onPlanFinished(); - } -} \ No newline at end of file diff --git a/src/planning/PlannerSyncService.ts b/src/planning/PlannerSyncService.ts deleted file mode 100644 index 12acb04a..00000000 --- a/src/planning/PlannerSyncService.ts +++ /dev/null @@ -1,83 +0,0 @@ -/* -------------------------------------------------------------------------------------------- - * Copyright (c) Jan Dolejsi. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - * ------------------------------------------------------------------------------------------ */ -'use strict'; - -import { Plan, ProblemInfo, DomainInfo, parser, planner } from 'pddl-workspace'; -import { Authentication } from '../util/Authentication'; -import { PlannerService } from './PlannerService'; - -/** Wraps the `/solve` planning web service interface. */ -export class PlannerSyncService extends PlannerService { - - constructor(plannerPath: string, private plannerOptions: string, authentication?: Authentication) { - super(plannerPath, authentication); - } - - createUrl(): string { - - let url = this.plannerPath; - if (this.plannerOptions) { - url = `${url}?${this.plannerOptions}`; - } - return url; - } - - getTimeout(): number { - return 60; - } - - // eslint-disable-next-line @typescript-eslint/no-explicit-any - createRequestBody(domainFileInfo: DomainInfo, problemFileInfo: ProblemInfo): Promise { - const body = { - "domain": domainFileInfo.getText(), - "problem": problemFileInfo.getText() - }; - - return Promise.resolve(body); - } - - // eslint-disable-next-line @typescript-eslint/no-explicit-any - processServerResponseBody(responseBody: any, planParser: parser.PddlPlannerOutputParser, callbacks: planner.PlannerResponseHandler, - resolve: (plans: Plan[]) => void, reject: (error: Error) => void): void { - const status = responseBody["status"]; - - if (status === "error") { - const result = responseBody["result"]; - - const resultOutput = result["output"]; - if (resultOutput) { - callbacks.handleOutput(resultOutput); - } - - const resultError = result["error"]; - if (resultError) { - callbacks.handleOutput(resultError); - resolve([]); - } - else { - reject(new Error("An error occurred while solving the planning problem: " + JSON.stringify(result))); - } - return; - } - else if (status !== "ok") { - reject(new Error(`Planner service failed with status ${status}.`)); - return; - } - - const result = responseBody["result"]; - const resultOutput = result["output"]; - if (resultOutput) { - callbacks.handleOutput(resultOutput); - } - - this.parsePlanSteps(result['plan'], planParser); - - const plans = planParser.getPlans(); - if (plans.length > 0) { callbacks.handleOutput(plans[0].getText() + '\n'); } - else { callbacks.handleOutput('No plan found.'); } - - resolve(plans); - } -} \ No newline at end of file diff --git a/src/planning/planning.ts b/src/planning/planning.ts index f2e96c60..348af18f 100644 --- a/src/planning/planning.ts +++ b/src/planning/planning.ts @@ -9,6 +9,7 @@ import { MessageItem, ExtensionContext, ProgressLocation, EventEmitter, Event, CancellationToken, Progress, QuickPickItem, TextDocument } from 'vscode'; import { instrumentOperationAsVsCodeCommand } from "vscode-extension-telemetry-wrapper"; +import { PlannerAsyncService, AsyncServiceConfiguration } from "pddl-planning-service-client"; import * as path from 'path'; import { dirname } from 'path'; @@ -17,9 +18,7 @@ import { } from 'pddl-workspace'; import { PddlConfiguration, PDDL_CONFIGURE_COMMAND, PDDL_PLANNER } from '../configuration/configuration'; import { PlannerExecutable } from './PlannerExecutable'; -import { PlannerSyncService } from './PlannerSyncService'; -import { PlannerAsyncService } from './PlannerAsyncService'; -import { Authentication } from '../util/Authentication'; +import { SAuthentication } from '../util/Authentication'; import { PlanningResult } from './PlanningResult'; import { PlanExporter } from './PlanExporter'; import { PlanHappeningsExporter } from './PlanHappeningsExporter'; @@ -33,6 +32,7 @@ import { AssociationProvider } from '../workspace/AssociationProvider'; import { showError, isHttp } from '../utils'; import { CodePddlWorkspace } from '../workspace/CodePddlWorkspace'; import { EXECUTION_TARGET, PDDL_CONFIGURE_PLANNER_OUTPUT_TARGET, PlannersConfiguration } from '../configuration/PlannersConfiguration'; +import { RequestServicePlannerProvider, SolveServicePlannerProvider } from '../configuration/plannerConfigurations'; const PDDL_STOP_PLANNER = 'pddl.stopPlanner'; const PDDL_CONVERT_PLAN_TO_HAPPENINGS = 'pddl.convertPlanToHappenings'; @@ -72,11 +72,17 @@ export class Planning implements planner.PlannerResponseHandler { context.subscriptions.push(this.planView = new PlanView(context, codePddlWorkspace)); context.subscriptions.push(instrumentOperationAsVsCodeCommand(PDDL_PLAN_AND_DISPLAY, - async (domainUri: Uri, problemUri: Uri | EditorId | undefined, workingFolder: string, options?: string) => { - if (problemUri && instanceOfEditorId(problemUri)) { + async (domainUri: Uri, payload: Uri | Uri[] | EditorId | undefined, workingFolder: string, options?: string) => { + if (payload && instanceOfEditorId(payload)) { + // triggered from the editor tab await this.planFromDocument(await workspace.openTextDocument(domainUri)).catch(showError); - } else if (problemUri) { - await this.planByUri(domainUri, problemUri, workingFolder, options).catch(showError); + } else if (Array.isArray(payload)) { + // multi-selected multiple files in the Explorer + const selectedFiles = payload; + this.planByUris(selectedFiles, workingFolder, options).catch(showError); + } else if (payload) { + // payload should be Uri; we ruled out other options + await this.planByUri(domainUri, payload, workingFolder, options).catch(showError); } else { await this.plan().catch(showError); } @@ -157,6 +163,44 @@ export class Planning implements planner.PlannerResponseHandler { this.planExplicit(domainInfo, problemInfo, workingFolder, options); } + async planByUris(selectedFiles: Uri[], workingFolder: string, options?: string): Promise { + if (selectedFiles.length === 1) { + await this.planFromDocument(await workspace.openTextDocument(selectedFiles[0])).catch(showError); + } else if (selectedFiles.length === 2) { + + let problemFileInfo: ProblemInfo | undefined; + let domainFileInfo: DomainInfo | undefined; + + for (let i = 0; i < selectedFiles.length; i++) { + const selectedFile = selectedFiles[i]; + const selectedDoc = await workspace.openTextDocument(selectedFile); + + const fileInfo = await this.codePddlWorkspace.upsertAndParseFile(selectedDoc); + if (fileInfo === undefined) { throw new Error(`Selected file is not a PDDL document: ${selectedDoc.fileName}`); } + + if (fileInfo.isDomain()) { + if (domainFileInfo !== undefined) { + throw new Error('Two domain files were selected. Select a domain file and one problem file.'); + } + domainFileInfo = fileInfo as DomainInfo; + } else if (fileInfo.isProblem()) { + if (problemFileInfo !== undefined) { + throw new Error('Two problem files were selected. Select a domain file and one problem file.'); + } + problemFileInfo = fileInfo as ProblemInfo; + } else { + throw new Error(`File not supported: ${fileInfo.name}. Select a domain file and one problem file.`); + } + } + + if (!domainFileInfo || !problemFileInfo) { + throw new Error('Select a domain file and one problem file.'); + } + + await this.planExplicit(domainFileInfo, problemFileInfo, workingFolder, options); + } + } + /** * Invokes the planner in the context of the currently opened files in the workspace. */ @@ -289,6 +333,9 @@ export class Planning implements planner.PlannerResponseHandler { this.planner = await this.createPlanner(workingDirectory, options); if (!this.planner) { return; } const planner: planner.Planner = this.planner; + if ("dispose" in planner) { + this.context.subscriptions.push(planner); + } this.planningProcessKilled = false; @@ -313,7 +360,7 @@ export class Planning implements planner.PlannerResponseHandler { this.progressUpdater = new ElapsedTimeProgressUpdater(progress, token); return planner.plan(domainFileInfo, problemFileInfo, planParser, this); }) - .then(plans => this.onPlannerFinished(plans), reason => this.onPlannerFailed(reason)); + .then(plans => this.onPlannerFinished(plans), reason => this.onPlannerFailed(reason, planner)); } isSearchDebugger(): boolean { @@ -332,8 +379,13 @@ export class Planning implements planner.PlannerResponseHandler { this.visualizePlans(plans); } + /** + * Called when the planner run fails. + * @param reason reason of the failure + * @param failedPlanner planner that failed + */ // eslint-disable-next-line @typescript-eslint/no-explicit-any - onPlannerFailed(reason: any): void { + async onPlannerFailed(reason: any, failedPlanner: planner.Planner): Promise { if (!this.progressUpdater) { return; } this.progressUpdater.setFinished(); this._onPlansFound.fire(PlanningResult.failure(reason.toString())); @@ -341,14 +393,35 @@ export class Planning implements planner.PlannerResponseHandler { this.planner = null; console.error(reason); - window.showErrorMessage(reason.message, - { title: "Re-configure the planner", setPlanner: true }, - { title: "Ignore", setPlanner: false, isCloseAffordance: true } - ).then(selection => { - if (selection && selection.setPlanner) { - this.pddlConfiguration.askNewPlannerPath(); + // does the planner provide any trouble-shooting options? + const troubleShootingInfo = await failedPlanner.providerConfiguration.provider?.troubleshoot?.(failedPlanner, reason as unknown); + + const options: ProcessErrorMessageItem[] = [ + { + title: failedPlanner.providerConfiguration.configuration.canConfigure ? + "Re-configure the planner" : "Show configuration", + action: (): void => { + // todo, if there is only one configuration of the kind, launch the configuration directly + commands.executeCommand("pddl.showOverview"); + } } - }); + ]; + + if (troubleShootingInfo?.options) { + [...troubleShootingInfo.options.keys()] + .forEach(title => { + options.push({ title: title, action: troubleShootingInfo.options.get(title) }); + }); + } + + options.push({ title: "Dismiss", isCloseAffordance: true }); + + const message = (troubleShootingInfo?.info ?? '') + '\n\n\nError: ' + (reason.message ?? reason); + + const selection = await window.showErrorMessage(message, ...options); + if (selection?.action) { + selection.action(failedPlanner); + } } async adjustWorkingFolder(workingDirectory: string): Promise { @@ -392,7 +465,7 @@ export class Planning implements planner.PlannerResponseHandler { let authentication = undefined; if (useAuthentication) { const configuration = this.pddlConfiguration.getPddlPlannerServiceAuthenticationConfiguration(); - authentication = new Authentication(configuration.url, configuration.requestEncoded, configuration.clientId, configuration.callbackPort, configuration.timeoutInMs, + authentication = new SAuthentication(configuration.url, configuration.requestEncoded, configuration.clientId, configuration.callbackPort, configuration.timeoutInMs, configuration.tokensvcUrl, configuration.tokensvcApiKey, configuration.tokensvcAccessPath, configuration.tokensvcValidatePath, configuration.tokensvcCodePath, configuration.tokensvcRefreshPath, configuration.tokensvcSvctkPath, configuration.refreshToken, configuration.accessToken, configuration.sToken); @@ -402,24 +475,46 @@ export class Planning implements planner.PlannerResponseHandler { options = await this.getPlannerLineOptions(plannerConfiguration, options); if (options === undefined) { return null; } - return new PlannerSyncService(plannerConfiguration.url, options, authentication); + const plannerRunConfiguration: planner.PlannerRunConfiguration = { + options: options, + authentication: authentication + }; + + return this.codePddlWorkspace.pddlWorkspace.getPlannerRegistrar() + .getPlannerProvider(new planner.PlannerKind(plannerConfiguration.kind))?.createPlanner?.(plannerConfiguration, plannerRunConfiguration) ?? + SolveServicePlannerProvider.createDefaultPlanner(plannerConfiguration, plannerRunConfiguration); } else if (plannerConfiguration.url.endsWith("/request")) { - const configuration = options ? this.toAbsoluteUri(options, workingDirectory) : await new PlannerConfigurationSelector(Uri.file(workingDirectory)).getConfiguration(); - if (!configuration) { return null; } // canceled by user - return new PlannerAsyncService(plannerConfiguration.url, configuration, authentication); + const configurationUri = options ? this.toAbsoluteUri(options, workingDirectory) : await new PlannerConfigurationSelector(Uri.file(workingDirectory)).getConfiguration(); + if (!configurationUri) { return null; } // canceled by user + const plannerRunConfiguration = await PlannerConfigurationSelector.loadConfiguration(configurationUri, PlannerAsyncService.DEFAULT_TIMEOUT) as AsyncServiceConfiguration; + plannerRunConfiguration.authentication = authentication; + + return this.codePddlWorkspace.pddlWorkspace.getPlannerRegistrar() + .getPlannerProvider(new planner.PlannerKind(plannerConfiguration.kind))?.createPlanner?.(plannerConfiguration, plannerRunConfiguration) ?? + RequestServicePlannerProvider.createDefaultPlanner(plannerConfiguration, plannerRunConfiguration); } else { throw new Error(`Planning service not supported: ${plannerConfiguration.url}. Only /solve or /request service endpoints are supported.`); } } - else if(plannerConfiguration.path) { + else if (plannerConfiguration.path) { options = await this.getPlannerLineOptions(plannerConfiguration, options); if (options === undefined) { return null; } + const plannerRunConfiguration: planner.PlannerExecutableRunConfiguration = { + options: options, + workingDirectory: workingDirectory, + plannerSyntax: plannerConfiguration.syntax + }; + + const providerConfiguration: planner.ProviderConfiguration = { + configuration: plannerConfiguration + }; + return this.codePddlWorkspace.pddlWorkspace.getPlannerRegistrar() - .getPlannerProvider({ kind: plannerConfiguration.kind })?.createPlanner?.(plannerConfiguration, options, workingDirectory) ?? - new PlannerExecutable(plannerConfiguration.path, options, plannerConfiguration.syntax, workingDirectory); + .getPlannerProvider(new planner.PlannerKind(plannerConfiguration.kind))?.createPlanner?.(plannerConfiguration, plannerRunConfiguration) ?? + new PlannerExecutable(plannerConfiguration.path, plannerRunConfiguration, providerConfiguration); } else { throw new Error(`Planner configuration must define at least one of the properties 'url' or 'path': ${plannerConfiguration.title}`); } @@ -435,7 +530,7 @@ export class Planning implements planner.PlannerResponseHandler { async getPlannerLineOptions(configuration: planner.PlannerConfiguration, options: string | undefined): Promise { if (options === undefined) { const plannerProvider = this.codePddlWorkspace.pddlWorkspace.getPlannerRegistrar() - .getPlannerProvider({ kind: configuration.kind }); + .getPlannerProvider(new planner.PlannerKind(configuration.kind)); return await this.userOptionsProvider.getPlannerOptions(plannerProvider); } @@ -543,7 +638,7 @@ export class Planning implements planner.PlannerResponseHandler { interface ProcessErrorMessageItem extends MessageItem { title: string; isCloseAffordance?: boolean; - setPlanner: boolean; + action?: (planner: planner.Planner) => void | Promise; } class ElapsedTimeProgressUpdater { diff --git a/src/ptest/ManifestGenerator.ts b/src/ptest/ManifestGenerator.ts index 8e941db6..bcd8040a 100644 --- a/src/ptest/ManifestGenerator.ts +++ b/src/ptest/ManifestGenerator.ts @@ -9,7 +9,7 @@ import { basename, extname, dirname, join, relative } from 'path'; import { PTestTreeDataProvider } from './PTestTreeDataProvider'; import { Test } from './Test'; import { Uri, workspace } from 'vscode'; -import { fileExists } from '../utils'; +import { fileOrFolderExists } from '../utils'; export class ManifestGenerator { constructor(private readonly pddlWorkspace: PddlWorkspace, @@ -58,7 +58,7 @@ export class ManifestGenerator { const manifestUri = Uri.file(join(dirname(domainPath), domainFileNameWithoutExt + PTestTreeDataProvider.PTEST_SUFFIX)); - if (await fileExists(manifestUri)) { + if (await fileOrFolderExists(manifestUri)) { const manifestText = await workspace.fs.readFile(manifestUri); const manifestJson = JSON.parse(manifestText.toString()); return TestsManifest.fromJSON(manifestUri.fsPath, manifestJson, context); diff --git a/src/ptest/PTestExplorer.ts b/src/ptest/PTestExplorer.ts index 1c3e8e35..ab011e35 100644 --- a/src/ptest/PTestExplorer.ts +++ b/src/ptest/PTestExplorer.ts @@ -133,7 +133,7 @@ export class PTestExplorer { // use jsonc-parser to find the element in the JSON DOM const manifestText = (await workspace.fs.readFile(Uri.file(manifest.path))).toString(); const rootNode = parseTree(manifestText); - const jsonTestNode = findNodeAtLocation(rootNode, ["cases", test.getIndex() ?? 0]); + const jsonTestNode = rootNode && findNodeAtLocation(rootNode, ["cases", test.getIndex() ?? 0]); const selection = jsonTestNode && jsonNodeToRange(manifestDocument, jsonTestNode); await window.showTextDocument(manifestDocument.uri, { preview: true, viewColumn: ViewColumn.One, selection: selection }); diff --git a/src/test/suite/testUtils.ts b/src/test/suite/testUtils.ts index f1a0fb87..c8224011 100644 --- a/src/test/suite/testUtils.ts +++ b/src/test/suite/testUtils.ts @@ -142,7 +142,7 @@ export class MockPlannerProvider implements planner.PlannerProvider { constructor(private options?: { canConfigure?: boolean }) { } get kind(): planner.PlannerKind { - return { kind: "mock" }; + return new planner.PlannerKind("mock"); } getNewPlannerLabel(): string { diff --git a/src/util/Authentication.ts b/src/util/Authentication.ts index 88b8efc2..02ccbf25 100644 --- a/src/util/Authentication.ts +++ b/src/util/Authentication.ts @@ -10,8 +10,9 @@ import request = require('request'); import bodyParser = require('body-parser'); import http = require('http'); import opn = require('open'); +import { planner } from 'pddl-workspace'; -export class Authentication { +export class SAuthentication implements planner.Authentication { private sToken?: string; private accessToken?: string; @@ -28,7 +29,7 @@ export class Authentication { this.display(); } - getSToken(): string | undefined { + getToken(): string | undefined { return this.sToken; } diff --git a/src/utils.ts b/src/utils.ts index 6927e731..7c9ebead 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -181,7 +181,7 @@ export class UriMap extends utils.StringifyingMap { } } -export async function fileExists(fileOrFolderUri: Uri): Promise { +export async function fileOrFolderExists(fileOrFolderUri: Uri): Promise { try { await workspace.fs.stat(fileOrFolderUri); return true; diff --git a/src/webviewUtils.ts b/src/webviewUtils.ts index f8c62d4c..093d87d3 100644 --- a/src/webviewUtils.ts +++ b/src/webviewUtils.ts @@ -64,7 +64,7 @@ export interface WebViewHtmlOptions { } function getWebviewUri(extensionContext: ExtensionContext, relativePath: string, fileName: string, webview?: Webview): Uri { - return asWebviewUri(Uri.file(extensionContext.asAbsolutePath(path.join(relativePath, fileName))), webview); + return asWebviewUri(Uri.joinPath(extensionContext.extensionUri, relativePath, fileName), webview); } function getAbsoluteWebviewUri(extensionContext: ExtensionContext, webview: Webview, options: WebViewHtmlOptions, uri: Uri): Uri { diff --git a/switch-to-local-pack.cmd b/switch-to-local-pack.cmd index 987e9f3a..4c8739d5 100644 --- a/switch-to-local-pack.cmd +++ b/switch-to-local-pack.cmd @@ -8,7 +8,7 @@ del pddl-workspace-*.tgz call npm pack cd ..\vscode-pddl -call npm install ..\pddl-workspace\pddl-workspace-6.2.0.tgz --save +call npm install ..\pddl-workspace\pddl-workspace-6.3.0.tgz --save call npm uninstall ai-planning-val cd ..\ai-planning-val.js @@ -17,3 +17,7 @@ call npm pack cd ..\vscode-pddl call npm install ..\ai-planning-val.js\ai-planning-val-2.4.1.tgz --save + +:: pddl-planning-service-client +call npm uninstall pddl-planning-service-client +call npm install ..\pddl-planning-service-client\pddl-planning-service-client-0.0.1.tgz --save diff --git a/switch-to-local.cmd b/switch-to-local.cmd index 4e74eade..2ecc6178 100644 --- a/switch-to-local.cmd +++ b/switch-to-local.cmd @@ -2,6 +2,8 @@ call npm uninstall pddl-workspace call npm uninstall ai-planning-val +call npm uninstall pddl-planning-service-client call npm install ..\pddl-workspace --save call npm install ..\ai-planning-val.js --save +call npm install ..\pddl-planning-service-client --save diff --git a/switch-to-npm.cmd b/switch-to-npm.cmd index c20f28d8..d883647a 100644 --- a/switch-to-npm.cmd +++ b/switch-to-npm.cmd @@ -1,11 +1,13 @@ call npm uninstall pddl-workspace call npm uninstall ai-planning-val +call npm uninstall pddl-planning-service-client +call npm uninstall pddl-gantt :: rmdir node_modules\pddl-workspace :: rmdir node_modules\ai-planning-val :: rmdir /S node_modules -call npm install pddl-workspace ai-planning-val --save +call npm install pddl-workspace@latest ai-planning-val@latest pddl-planning-service-client@latest pddl-gantt@latest --save call npm install \ No newline at end of file