diff --git a/package-lock.json b/package-lock.json index a1f1c23..4c161a3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "coc-powershell", - "version": "0.0.4", + "version": "0.0.5", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index 5b678ec..155e6bd 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "description": "PowerShell Language Support using PowerShell Editor Services", "author": "Yatao Li, Tyler Leonhardt, Cory Knox", "license": "MIT", - "version": "0.0.4", + "version": "0.0.5", "publisher": "yatli, tylerl0706, corbob", "repository": { "type": "git", diff --git a/src/client/extension.ts b/src/client/extension.ts index 9cf2d42..6f63a7c 100644 --- a/src/client/extension.ts +++ b/src/client/extension.ts @@ -7,67 +7,33 @@ import * as crypto from "crypto"; import * as fs from 'fs'; import * as path from 'path'; +import * as net from 'net'; import { commands, workspace, ExtensionContext, events } from 'coc.nvim'; -import { LanguageClient, LanguageClientOptions, ServerOptions, TransportKind } from 'coc.nvim'; -import { getDefaultPowerShellPath, getPlatformDetails } from './platform'; +import { LanguageClient, LanguageClientOptions, ServerOptions, TransportKind, StreamInfo } from 'coc.nvim'; import { fileURLToPath } from './utils' -import * as settings from './settings'; -import Shell from "node-powershell"; - -// Important paths. -const config = settings.load() -const cocPowerShellRoot = path.join(__dirname, "..", ".."); -const bundledModulesPath = path.join(cocPowerShellRoot, "PowerShellEditorServices"); -const logPath = path.join(cocPowerShellRoot, `/.pses/logs/${crypto.randomBytes(16).toString("hex")}-${process.pid}`); -const logger = workspace.createOutputChannel('powershell') +import { getDefaultPowerShellPath, getPlatformDetails } from './platform'; +import settings = require("./settings"); +import * as process from './process'; export async function activate(context: ExtensionContext) { + let config = settings.load() let pwshPath = config.powerShellExePath - ? config.powerShellExePath + ? this.config.powerShellExePath : getDefaultPowerShellPath(getPlatformDetails()) - logger.appendLine("starting.") - logger.appendLine(`pwshPath = ${pwshPath}`) - logger.appendLine(`bundledModulesPath = ${bundledModulesPath}`) - - // If PowerShellEditorServices is not downloaded yet, run the install script to do so. - if (!fs.existsSync(bundledModulesPath)) { - let notification = workspace.createStatusBarItem(0, { progress: true}) - notification.text = "Downloading PowerShellEditorServices..." - notification.show() - - const ps = new Shell({ - executionPolicy: 'Bypass', - noProfile: true - }); - - ps.addCommand(path.join(cocPowerShellRoot, "src", "downloadPSES.ps1")); - await ps.invoke() - .catch(e => logger.appendLine("error downloading PSES: " + e)) - .finally(() => { - notification.hide() - notification.dispose() - }); + let proc = new process.PowerShellProcess(config, pwshPath, "PowerShell REPL") - } - - let serverOptions: ServerOptions = { - command: pwshPath, - args: [ - "-NoProfile", - "-NonInteractive", - path.join(bundledModulesPath, "/PowerShellEditorServices/Start-EditorServices.ps1"), - "-HostName", "coc.vim", - "-HostProfileId", "0", - "-HostVersion", "2.0.0", - "-LogPath", path.join(logPath, "log.txt"), - "-LogLevel", "Diagnostic", - "-BundledModulesPath", bundledModulesPath, - "-Stdio", - "-SessionDetailsPath", path.join(logPath, "session")], - transport: TransportKind.stdio - } + let sessionDetails = await proc.start() + let socket = net.connect(sessionDetails.languageServicePipeName); + let streamInfo = () => new Promise((resolve, reject) => { + socket.on( + "connect", + () => { + proc.log.appendLine("Language service connected."); + resolve({writer: socket, reader: socket}); + }); + }); workspace.addRootPatterns('ps1', ['*.ps1', '*.psd1', '*.psm1', '.vim', '.git', '.hg']) @@ -88,7 +54,7 @@ export async function activate(context: ExtensionContext) { } // Create the language client and start the client. - let client = new LanguageClient('ps1', 'PowerShell Language Server', serverOptions, clientOptions); + let client = new LanguageClient('ps1', 'PowerShell Language Server', streamInfo, clientOptions); let disposable = client.start(); // Status bar entry showing PS version diff --git a/src/client/process.ts b/src/client/process.ts new file mode 100644 index 0000000..c06c0df --- /dev/null +++ b/src/client/process.ts @@ -0,0 +1,160 @@ +/*--------------------------------------------------------- + * Copyright (C) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------*/ + +import cp = require("child_process"); +import fs = require("fs"); +import net = require("net"); +import os = require("os"); +import path = require("path"); +import vscode = require("coc.nvim"); +import Settings = require("./settings"); +import utils = require("./utils"); +import crypto = require("crypto"); +import Shell from "node-powershell"; + +export class PowerShellProcess { + public static escapeSingleQuotes(pspath: string): string { + return pspath.replace(new RegExp("'", "g"), "''"); + } + + public onExited: vscode.Event; + private onExitedEmitter = new vscode.Emitter(); + + private consoleTerminal: vscode.Terminal = undefined; + private consoleCloseSubscription: vscode.Disposable; + private sessionFilePath: string + private sessionDetails: utils.IEditorServicesSessionDetails; + + public log = vscode.workspace.createOutputChannel('powershell') + private cocPowerShellRoot = path.join(__dirname, "..", ".."); + private bundledModulesPath = path.join(this.cocPowerShellRoot, "PowerShellEditorServices"); + + constructor( + private config: Settings.ISettings, + private pwshPath: string, + private title: string) { + + this.onExited = this.onExitedEmitter.event; + } + + public async start(): Promise { + + // If PowerShellEditorServices is not downloaded yet, run the install script to do so. + if (!fs.existsSync(this.bundledModulesPath)) { + let notification = vscode.workspace.createStatusBarItem(0, { progress: true}) + notification.text = "Downloading PowerShellEditorServices..." + notification.show() + + const ps = new Shell({ + executionPolicy: 'Bypass', + noProfile: true + }); + + ps.addCommand(path.join(this.cocPowerShellRoot, "src", "downloadPSES.ps1")); + await ps.invoke() + .catch(e => logger.appendLine("error downloading PSES: " + e)) + .finally(() => { + notification.hide() + notification.dispose() + }); + + } + + this.log.appendLine("starting.") + this.log.appendLine(`pwshPath = ${this.pwshPath}`) + this.log.appendLine(`bundledModulesPath = ${this.bundledModulesPath}`) + + let logDir = path.join(this.cocPowerShellRoot, `/.pses/logs/${crypto.randomBytes(16).toString("hex")}-${process.pid}`) + utils.ensurePathExists(logDir) + this.sessionFilePath = path.join(logDir, "session") + + // Make sure no old session file exists + utils.deleteSessionFile(this.sessionFilePath); + + let powerShellArgs: string[] = [] + + // Only add ExecutionPolicy param on Windows + if (utils.isWindowsOS()) { + powerShellArgs.push("-ExecutionPolicy", "Bypass"); + } + + powerShellArgs.push( + "-NoProfile", + "-NonInteractive", + path.join(this.bundledModulesPath, "/PowerShellEditorServices/Start-EditorServices.ps1"), + "-HostName", "coc.vim", + "-HostProfileId", "0", + "-HostVersion", "2.0.0", + "-LogPath", path.join(logDir, "log.txt"), + "-LogLevel", "Diagnostic", + "-BundledModulesPath", this.bundledModulesPath, + "-EnableConsoleRepl", + "-SessionDetailsPath", this.sessionFilePath) + + + this.consoleTerminal = await vscode.workspace.createTerminal({ + name: this.title, + shellPath: this.pwshPath, + shellArgs: powerShellArgs + }) + + if (!this.config.integratedConsole.showOnStartup) { + this.consoleTerminal.hide() + } + + await new Promise((resolve, reject) => { + // Start the language client + utils.waitForSessionFile( + this.sessionFilePath, + (sessionDetails, error) => { + // Clean up the session file + utils.deleteSessionFile(this.sessionFilePath); + + if (error) { + reject(error); + } else { + this.sessionDetails = sessionDetails; + resolve(this.sessionDetails); + } + }); + }) + + this.consoleCloseSubscription = + vscode.workspace.onDidCloseTerminal( + (terminal) => { + if (terminal === this.consoleTerminal) { + this.log.appendLine("powershell.exe terminated or terminal UI was closed"); + this.onExitedEmitter.fire(); + } + }, this); + + this.consoleTerminal.processId.then( + (pid) => { this.log.appendLine(`powershell.exe started, pid: ${pid}`); }); + + return this.sessionDetails + } + + public showConsole(preserveFocus: boolean) { + if (this.consoleTerminal) { + this.consoleTerminal.show(preserveFocus); + } + } + + public dispose() { + + // Clean up the session file + utils.deleteSessionFile(this.sessionFilePath); + + if (this.consoleCloseSubscription) { + this.consoleCloseSubscription.dispose(); + this.consoleCloseSubscription = undefined; + } + + if (this.consoleTerminal) { + this.log.appendLine("Terminating PowerShell process..."); + this.consoleTerminal.dispose(); + this.consoleTerminal = undefined; + } + } +}