diff --git a/app/src/entry.preload.ts b/app/src/entry.preload.ts index 8478ea2f..9a2987a3 100644 --- a/app/src/entry.preload.ts +++ b/app/src/entry.preload.ts @@ -31,3 +31,7 @@ process.on('uncaughtException', (err) => { Raven.captureException(err) console.error(err) }) + +const childProcess = require('child_process') +childProcess.spawn = require('electron').remote.require('child_process').spawn +childProcess.exec = require('electron').remote.require('child_process').exec diff --git a/app/webpack.config.js b/app/webpack.config.js index 0c44a674..8fccad2a 100644 --- a/app/webpack.config.js +++ b/app/webpack.config.js @@ -55,6 +55,7 @@ module.exports = { '@angular/forms': 'commonjs @angular/forms', '@angular/common': 'commonjs @angular/common', '@ng-bootstrap/ng-bootstrap': 'commonjs @ng-bootstrap/ng-bootstrap', + 'child_process': 'commonjs child_process', 'electron': 'commonjs electron', 'electron-is-dev': 'commonjs electron-is-dev', 'module': 'commonjs module', diff --git a/terminus-core/src/components/appRoot.component.pug b/terminus-core/src/components/appRoot.component.pug index c9832dc3..6a63790f 100644 --- a/terminus-core/src/components/appRoot.component.pug +++ b/terminus-core/src/components/appRoot.component.pug @@ -19,7 +19,7 @@ title-bar( [class.drag-region]='hostApp.platform == Platform.macOS', @animateTab, (click)='app.selectTab(tab)', - (closeClicked)='app.closeTab(tab)', + (closeClicked)='app.closeTab(tab, true)', ) .btn-group diff --git a/terminus-core/src/components/appRoot.component.ts b/terminus-core/src/components/appRoot.component.ts index 31fd6763..aa6a38df 100644 --- a/terminus-core/src/components/appRoot.component.ts +++ b/terminus-core/src/components/appRoot.component.ts @@ -79,7 +79,7 @@ export class AppRootComponent { } if (this.app.activeTab) { if (hotkey === 'close-tab') { - this.app.closeTab(this.app.activeTab) + this.app.closeTab(this.app.activeTab, true) } if (hotkey === 'toggle-last-tab') { this.app.toggleLastTab() @@ -138,16 +138,6 @@ export class AppRootComponent { } } - private getToolbarButtons (aboveZero: boolean): IToolbarButton[] { - let buttons: IToolbarButton[] = [] - this.toolbarButtonProviders.forEach((provider) => { - buttons = buttons.concat(provider.provide()) - }) - return buttons - .filter((button) => (button.weight > 0) === aboveZero) - .sort((a: IToolbarButton, b: IToolbarButton) => (a.weight || 0) - (b.weight || 0)) - } - @HostListener('dragover') onDragOver () { return false @@ -157,4 +147,14 @@ export class AppRootComponent { onDrop () { return false } + + private getToolbarButtons (aboveZero: boolean): IToolbarButton[] { + let buttons: IToolbarButton[] = [] + this.toolbarButtonProviders.forEach((provider) => { + buttons = buttons.concat(provider.provide()) + }) + return buttons + .filter((button) => (button.weight > 0) === aboveZero) + .sort((a: IToolbarButton, b: IToolbarButton) => (a.weight || 0) - (b.weight || 0)) + } } diff --git a/terminus-core/src/components/baseTab.component.ts b/terminus-core/src/components/baseTab.component.ts index d7794a4f..77a15538 100644 --- a/terminus-core/src/components/baseTab.component.ts +++ b/terminus-core/src/components/baseTab.component.ts @@ -31,6 +31,10 @@ export abstract class BaseTabComponent { return null } + async canClose (): Promise { + return true + } + destroy (): void { this.focused$.complete() this.blurred$.complete() diff --git a/terminus-core/src/services/app.service.ts b/terminus-core/src/services/app.service.ts index fe72f0bb..757cae87 100644 --- a/terminus-core/src/services/app.service.ts +++ b/terminus-core/src/services/app.service.ts @@ -82,10 +82,16 @@ export class AppService { } } - closeTab (tab: BaseTabComponent) { + async closeTab (tab: BaseTabComponent, checkCanClose?: boolean): Promise { + if (!this.tabs.includes(tab)) { + return + } + if (checkCanClose && !await tab.canClose()) { + return + } + this.tabs = this.tabs.filter((x) => x !== tab) tab.destroy() let newIndex = Math.max(0, this.tabs.indexOf(tab) - 1) - this.tabs = this.tabs.filter((x) => x !== tab) if (tab === this.activeTab) { this.selectTab(this.tabs[newIndex]) } diff --git a/terminus-core/src/services/electron.service.ts b/terminus-core/src/services/electron.service.ts index 96214da3..3be9d648 100644 --- a/terminus-core/src/services/electron.service.ts +++ b/terminus-core/src/services/electron.service.ts @@ -27,4 +27,8 @@ export class ElectronService { remoteRequire (name: string): any { return this.remote.require(name) } + + remoteRequirePluginModule (plugin: string, module: string, globals: any): any { + return this.remoteRequire(globals.require.resolve(`${plugin}/node_modules/${module}`)) + } } diff --git a/terminus-terminal/package.json b/terminus-terminal/package.json index fe718273..81a05c58 100644 --- a/terminus-terminal/package.json +++ b/terminus-terminal/package.json @@ -41,6 +41,7 @@ "hterm-umdjs": "1.1.3", "mz": "^2.6.0", "node-pty": "0.6.8", + "ps-node": "^0.1.6", "runes": "^0.4.2", "winreg": "^1.2.3" }, diff --git a/terminus-terminal/src/api.ts b/terminus-terminal/src/api.ts index 36163305..584ae33a 100644 --- a/terminus-terminal/src/api.ts +++ b/terminus-terminal/src/api.ts @@ -1,6 +1,7 @@ import { Observable } from 'rxjs' import { TerminalTabComponent } from './components/terminalTab.component' export { TerminalTabComponent } +export { IChildProcess } from './services/sessions.service' export abstract class TerminalDecorator { // tslint:disable-next-line no-empty diff --git a/terminus-terminal/src/components/terminalTab.component.ts b/terminus-terminal/src/components/terminalTab.component.ts index 4dbeb652..355f8674 100644 --- a/terminus-terminal/src/components/terminalTab.component.ts +++ b/terminus-terminal/src/components/terminalTab.component.ts @@ -1,4 +1,3 @@ -const dataurl = require('dataurl') import { BehaviorSubject, Subject, Subscription } from 'rxjs' import 'rxjs/add/operator/bufferTime' import { Component, NgZone, Inject, Optional, ViewChild, HostBinding, Input } from '@angular/core' @@ -297,12 +296,7 @@ export class TerminalTabComponent extends BaseTabComponent { ` } css += config.appearance.css - preferenceManager.set('user-css', dataurl.convert({ - data: css, - mimetype: 'text/css', - charset: 'utf8', - })) - + this.hterm.setCSS(css) this.hterm.setBracketedPaste(config.terminal.bracketedPaste) } @@ -345,6 +339,14 @@ export class TerminalTabComponent extends BaseTabComponent { } } + async canClose (): Promise { + let children = await this.session.getChildProcesses() + if (children.length === 0) { + return true + } + return confirm(`"${children[0].command}" is still running. Close?`) + } + private setFontSize () { preferenceManager.set('font-size', this.config.store.terminal.fontSize * Math.pow(1.1, this.zoom)) } diff --git a/terminus-terminal/src/hterm.ts b/terminus-terminal/src/hterm.ts index f0e6617f..6ce77d4b 100644 --- a/terminus-terminal/src/hterm.ts +++ b/terminus-terminal/src/hterm.ts @@ -22,6 +22,16 @@ preferenceManager.set('color-palette-overrides', { hterm.hterm.Terminal.prototype.showOverlay = () => null +hterm.hterm.Terminal.prototype.setCSS = function (css) { + const doc = this.scrollPort_.document_ + if (!doc.querySelector('#user-css')) { + const node = doc.createElement('style') + node.id = 'user-css' + doc.head.appendChild(node) + } + doc.querySelector('#user-css').innerText = css +} + const oldCharWidthDisregardAmbiguous = hterm.lib.wc.charWidthDisregardAmbiguous hterm.lib.wc.charWidthDisregardAmbiguous = codepoint => { if ((codepoint >= 0x1f300 && codepoint <= 0x1f64f) || diff --git a/terminus-terminal/src/pathDrop.ts b/terminus-terminal/src/pathDrop.ts index 8196c3cf..5514d602 100644 --- a/terminus-terminal/src/pathDrop.ts +++ b/terminus-terminal/src/pathDrop.ts @@ -2,7 +2,6 @@ import { Injectable } from '@angular/core' import { TerminalDecorator } from './api' import { TerminalTabComponent } from './components/terminalTab.component' - @Injectable() export class PathDropDecorator extends TerminalDecorator { attach (terminal: TerminalTabComponent): void { diff --git a/terminus-terminal/src/persistenceProviders.ts b/terminus-terminal/src/persistenceProviders.ts index e00e3379..1bdb1b16 100644 --- a/terminus-terminal/src/persistenceProviders.ts +++ b/terminus-terminal/src/persistenceProviders.ts @@ -64,12 +64,13 @@ export class ScreenPersistenceProvider extends SessionPersistenceProvider { recoveryId, recoveredTruePID$: truePID$.asObservable(), command: 'screen', - args: ['-r', recoveryId], + args: ['-d', '-r', recoveryId], } } async extractShellPID (screenPID: number): Promise { - let child = (await listProcesses()).find(x => x.ppid === screenPID) + let processes = await listProcesses() + let child = processes.find(x => x.ppid === screenPID) if (!child) { throw new Error(`Could not find any children of the screen process (PID ${screenPID})!`) @@ -77,7 +78,7 @@ export class ScreenPersistenceProvider extends SessionPersistenceProvider { if (child.command === 'login') { await delay(1000) - child = (await listProcesses()).find(x => x.ppid === child.pid) + child = processes.find(x => x.ppid === child.pid) } return child.pid diff --git a/terminus-terminal/src/services/sessions.service.ts b/terminus-terminal/src/services/sessions.service.ts index 79231a00..e9145fc5 100644 --- a/terminus-terminal/src/services/sessions.service.ts +++ b/terminus-terminal/src/services/sessions.service.ts @@ -1,12 +1,20 @@ -import * as nodePTY from 'node-pty' +const psNode = require('ps-node') +// import * as nodePTY from 'node-pty' +let nodePTY import * as fs from 'mz/fs' import { Subject } from 'rxjs' import { Injectable } from '@angular/core' -import { Logger, LogService } from 'terminus-core' +import { Logger, LogService, ElectronService } from 'terminus-core' import { exec } from 'mz/child_process' import { SessionOptions, SessionPersistenceProvider } from '../api' +export interface IChildProcess { + pid: number + ppid: number + command: string +} + export class Session { open: boolean name: string @@ -101,6 +109,20 @@ export class Session { this.pty.kill(signal) } + async getChildProcesses (): Promise { + if (!this.truePID) { + return [] + } + return new Promise((resolve, reject) => { + psNode.lookup({ ppid: this.truePID }, (err, processes) => { + if (err) { + return reject(err) + } + resolve(processes as IChildProcess[]) + }) + }) + } + async gracefullyKillProcess (): Promise { if (process.platform === 'win32') { this.kill() @@ -157,8 +179,10 @@ export class SessionsService { constructor ( private persistence: SessionPersistenceProvider, + electron: ElectronService, log: LogService, ) { + nodePTY = electron.remoteRequirePluginModule('terminus-terminal', 'node-pty', global as any) this.logger = log.create('sessions') } diff --git a/terminus-terminal/tsconfig.json b/terminus-terminal/tsconfig.json index 1d6cfcbf..d2383e0e 100644 --- a/terminus-terminal/tsconfig.json +++ b/terminus-terminal/tsconfig.json @@ -3,6 +3,10 @@ "exclude": ["node_modules", "dist"], "compilerOptions": { "baseUrl": "src", - "declarationDir": "dist" + "declarationDir": "dist", + "paths": { + "terminus-*": ["terminus-*"], + "*": ["app/node_modules/*"] + } } } diff --git a/terminus-terminal/webpack.config.js b/terminus-terminal/webpack.config.js index 02c5ad5e..52546f03 100644 --- a/terminus-terminal/webpack.config.js +++ b/terminus-terminal/webpack.config.js @@ -44,6 +44,7 @@ module.exports = { ] }, externals: [ + 'electron', 'fs', 'font-manager', 'path', diff --git a/terminus-terminal/yarn.lock b/terminus-terminal/yarn.lock index 8ff5d7f0..f145d65a 100644 --- a/terminus-terminal/yarn.lock +++ b/terminus-terminal/yarn.lock @@ -32,6 +32,10 @@ big.js@^3.1.3: version "3.1.3" resolved "https://registry.yarnpkg.com/big.js/-/big.js-3.1.3.tgz#4cada2193652eb3ca9ec8e55c9015669c9806978" +connected-domain@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/connected-domain/-/connected-domain-1.0.0.tgz#bfe77238c74be453a79f0cb6058deeb4f2358e93" + dataurl@0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/dataurl/-/dataurl-0.1.0.tgz#1f4734feddec05ffe445747978d86759c4b33199" @@ -98,10 +102,22 @@ object-assign@^4.0.1: version "4.1.1" resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" +ps-node@^0.1.6: + version "0.1.6" + resolved "https://registry.yarnpkg.com/ps-node/-/ps-node-0.1.6.tgz#9af67a99d7b1d0132e51a503099d38a8d2ace2c3" + dependencies: + table-parser "^0.1.3" + runes@^0.4.2: version "0.4.2" resolved "https://registry.yarnpkg.com/runes/-/runes-0.4.2.tgz#1ddc1ea41de769cb32fc068a64fbbc45cd21052e" +table-parser@^0.1.3: + version "0.1.3" + resolved "https://registry.yarnpkg.com/table-parser/-/table-parser-0.1.3.tgz#0441cfce16a59481684c27d1b5a67ff15a43c7b0" + dependencies: + connected-domain "^1.0.0" + thenify-all@^1.0.0: version "1.6.0" resolved "https://registry.yarnpkg.com/thenify-all/-/thenify-all-1.6.0.tgz#1a1918d402d8fc3f98fbf234db0bcc8cc10e9726"