Skip to content
This repository has been archived by the owner on Jun 27, 2023. It is now read-only.

Commit

Permalink
Merge pull request #144 from urbit/allow-terminal-access
Browse files Browse the repository at this point in the history
Allow terminal access
  • Loading branch information
arthyn authored Oct 26, 2021
2 parents 665aa02 + 707fd07 commit ede30dd
Show file tree
Hide file tree
Showing 15 changed files with 928 additions and 147 deletions.
6 changes: 1 addition & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ Formerly called Taisho, Port allows you to spin up, access, and manage your ship

## Installing

Head over to [releases](https://github.com/arthyn/port/releases) and download the installer from the latest release. Currently only available for MacOS and Linux.
Head over to [releases](https://github.com/arthyn/port/releases) and download the installer for your operating system from the latest release. Currently available for all major OSes.

## Screenshots
![](https://hmillerdev.nyc3.digitaloceanspaces.com/nocsyx-lassul/taisho-welcome.jpg)
Expand Down Expand Up @@ -35,8 +35,4 @@ The renderer is a React + Typescript + TailwindCSS application. We use IPC to co

The urbit binaries for each respective OS should live in the `resources` folder under the respective OS' folder. They aren't included because of size, but you can get them by running the `get-urbit.sh` script.

**Windows**

The current plan for Windows is to wait for the port currently being reviewed by the core Urbit team. Once official we'll add support ASAP.

![Mothership](https://hmillerdev.nyc3.digitaloceanspaces.com/nocsyx-lassul/BALEEN%20CLASS_PATREON_190519.jpg)
8 changes: 8 additions & 0 deletions forge.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,14 @@ module.exports = {
js: "./src/renderer/client/preload.ts"
}
},
{
name: "terminal",
html: "./src/renderer/terminal/index.html",
js: "./src/renderer/terminal/index.tsx",
preload: {
js: "./src/renderer/client/preload.ts"
}
},
{
html: "./src/background/server/server.html",
js: "./src/background/main.ts",
Expand Down
9 changes: 6 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "port",
"productName": "Port",
"version": "1.4.0",
"version": "1.5.0",
"description": "A ship runner and manager for Urbit OS",
"repository": {
"type": "git",
Expand Down Expand Up @@ -33,7 +33,7 @@
},
"devDependencies": {
"@davidwinter/electron-forge-maker-snap": "^2.0.3",
"@electron-forge/cli": "^6.0.0-beta.54",
"@electron-forge/cli": "^6.0.0-beta.61",
"@electron-forge/maker-deb": "^6.0.0-beta.54",
"@electron-forge/maker-dmg": "^6.0.0-beta.54",
"@electron-forge/maker-rpm": "^6.0.0-beta.54",
Expand All @@ -42,7 +42,6 @@
"@electron-forge/maker-zip": "^6.0.0-beta.54",
"@electron-forge/plugin-webpack": "^6.0.0-beta.54",
"@electron-forge/publisher-github": "^6.0.0-beta.54",
"@marshallofsound/webpack-asset-relocator-loader": "^0.5.0",
"@types/async": "^3.2.5",
"@types/lodash.debounce": "^4.0.6",
"@types/mv": "^2.1.0",
Expand All @@ -51,6 +50,7 @@
"@types/react-router-dom": "^5.1.7",
"@typescript-eslint/eslint-plugin": "^4.0.1",
"@typescript-eslint/parser": "^4.0.1",
"@vercel/webpack-asset-relocator-loader": "1.7.0",
"autoprefixer": "^10.2.4",
"css-loader": "^4.2.1",
"cssnano": "^4.1.10",
Expand Down Expand Up @@ -98,6 +98,7 @@
"nedb": "^1.8.0",
"nedb-async": "^0.1.6",
"node-ipc": "^9.1.3",
"node-pty": "^0.11.0-beta9",
"patch-package": "^6.4.7",
"query-string": "^6.14.0",
"react": "^17.0.1",
Expand All @@ -109,6 +110,8 @@
"react-router-dom": "^5.2.0",
"update-electron-app": "^2.0.1",
"use-resize-observer": "^7.0.0",
"xterm-addon-fit": "^0.5.0",
"xterm-for-react": "^1.0.4",
"zustand": "^3.3.1"
}
}
15 changes: 9 additions & 6 deletions src/background/main.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,21 @@
import { exec } from 'child_process';
import db from './db';
import { platform, arch } from 'os';
import { Handler, HandlerEntry, HandlerMap, init, send } from './server/ipc';
import { Handler, HandlerEntry, HandlerMap, init } from './server/ipc';
import { OSHandlers, OSService } from './services/os-service';
import { PierHandlers, PierService } from './services/pier-service';
import { ipcRenderer } from 'electron';
import { SettingsHandlers, SettingsService } from './services/settings-service';

start();

export type Handlers = OSHandlers & PierHandlers & SettingsHandlers & {
connected: Handler,
disconnected: Handler
}
export type Handlers =
& OSHandlers
& PierHandlers
& SettingsHandlers
& {
connected: Handler,
disconnected: Handler
}

async function start() {
const handlerMap: HandlerMap<Handlers> = {} as HandlerMap<Handlers>;
Expand Down
2 changes: 1 addition & 1 deletion src/background/server/ipc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ export interface Push {

export type ClientMessage = Reply | Error | Push;

export type Handler<Result = unknown> = (...args: unknown[]) => Promise<Result>
export type Handler<Result = unknown> = (...args: unknown[]) => Promise<Result> | Result

export type HandlerEntry<HandlerSet> = {
name: keyof HandlerSet,
Expand Down
34 changes: 31 additions & 3 deletions src/background/services/pier-service.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { join as joinPath } from 'path';
import { spawn, ChildProcessWithoutNullStreams } from 'child_process';
import { shell, remote } from 'electron';
import { shell, remote, ipcRenderer } from 'electron';
import isDev from 'electron-is-dev';
import axios from 'axios'
import { DB } from '../db'
Expand Down Expand Up @@ -40,6 +40,7 @@ export interface PierHandlers {
'collect-existing-pier': PierService["collectExistingPier"]
'boot-pier': PierService["bootPier"]
'resume-pier': PierService["resumePier"]
'spawn-in-terminal': PierService["spawnInTerminal"]
'check-pier': PierService["checkPier"]
'check-boot': PierService["checkBoot"]
'check-url': PierService["checkUrlAccessible"]
Expand Down Expand Up @@ -72,6 +73,7 @@ export class PierService {
{ name: 'collect-existing-pier', handler: this.collectExistingPier.bind(this) },
{ name: 'boot-pier', handler: this.bootPier.bind(this) },
{ name: 'resume-pier', handler: this.resumePier.bind(this) },
{ name: 'spawn-in-terminal', handler: this.spawnInTerminal.bind(this) },
{ name: 'check-pier', handler: this.checkPier.bind(this) },
{ name: 'check-url', handler: this.checkUrlAccessible.bind(this) },
{ name: 'check-boot', handler: this.checkBoot.bind(this) },
Expand Down Expand Up @@ -368,7 +370,7 @@ export class PierService {
app: 'hood'
},
source: {
dojo: '+hood/ota (sein:title our now our) %kids'
dojo: '+hood/install (sein:title our now our) %kids, =local %base'
}
})
} catch (err) {
Expand Down Expand Up @@ -437,6 +439,26 @@ export class PierService {
})
}

async spawnInTerminal(pier: Pier): Promise<void> {
const stringifiedArgs = this.getSpawnArgs(pier, true).map(arg => arg.replace(/ /g, '\\ ')).join(' ');
const spawnCommand = `${this.urbitPath} ${stringifiedArgs}`;

await this.stopPier(pier);

ipcRenderer.send('terminal-create', {
ship: pier.shipName,
initialCommand: spawnCommand,
exitCommand: '\x04'
})

return new Promise((resolve) => {
setTimeout(async () => {
await this.checkPier(pier);
resolve();
}, 1500);
});
}

private getPierPath(pier: Pier) {
if (pier.directoryAsPierPath) {
return pier.directory;
Expand Down Expand Up @@ -485,10 +507,16 @@ export class PierService {

return new Promise((resolve, reject) => {
urbit.on('close', (code) => {
if (code !== 0) {
if (typeof code === 'undefined') {
reject('bailing out')
}

if (typeof code === 'number' && code !== 0) {
console.error(`Exited with code ${code}`)
reject(code.toString())
}

reject();
})

urbit.on('error', (err) => {
Expand Down
2 changes: 2 additions & 0 deletions src/main/main-window.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
import { initContextMenu } from './context-menu';
import { start as osHelperStart, views } from './os-service-helper'
import { start as settingsHelperStart } from './setting-service-helper'
import { start as terminalServiceStart } from './terminal-service';

const ZOOM_INTERVAL = 0.1;

Expand Down Expand Up @@ -267,6 +268,7 @@ export function createMainWindow(
mainWindow.webContents.session.clearCache();
osHelperStart(mainWindow, createNewWindow, onNewWindow, bgWindow)
settingsHelperStart(mainWindow, menuOptions);
terminalServiceStart();
isDev && mainWindow.webContents.openDevTools();
mainWindow.loadURL(mainUrl);
//mainWindow.on('new-tab' as any, () => createNewTab(mainUrl, true));
Expand Down
152 changes: 152 additions & 0 deletions src/main/terminal-service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
import os from "os"
import { IPty, spawn } from "node-pty";
import { BrowserWindow, ipcMain } from "electron";
import isDev from 'electron-is-dev';
declare const TERMINAL_WEBPACK_ENTRY: string;

const shell = os.platform() === "win32" ? "powershell.exe" : "bash";

export interface Payload {
ship: string;
data: string;
}

interface TerminalProcessParams {
ship: string;
initialCommand?: string;
exitCommand?: string;
}

class TerminalProcess {
ship: string;
initialCommand?: string;
exitCommand?: string;
pty: IPty;
initialized: boolean;
msgCount: number;
window: BrowserWindow;

constructor({ ship, initialCommand, exitCommand }: TerminalProcessParams) {
this.ship = ship;
this.initialCommand = initialCommand;
this.exitCommand = exitCommand;
this.initialized = false;
this.msgCount = 0;
this.window = new BrowserWindow({
title: `${ship} | Terminal`,
height: 450,
width: 800,
backgroundColor: "#000000",
webPreferences: {
nodeIntegration: true
}
});
this.window.on('close', (event) => {
event.preventDefault();
this.destroy();
});
this.window.webContents.on('did-finish-load', () => {
isDev && console.log('loaded, sending ship')
this.window.webContents.send('ship', ship)
})
this.window.loadURL(TERMINAL_WEBPACK_ENTRY);
}

init(): void {
if (this.initialized) {
return;
}

this.initialized = true;
isDev && console.log('initializing pty')
this.pty = spawn(shell, [], {
name: "xterm-color",
cols: 80,
rows: 30,
cwd: process.env.HOME,
env: process.env
});

this.pty.onData(data => {
//console.log('sending terminal data', data)
try {
this.window.webContents.send('terminal-incoming', { ship: this.ship, data });
} catch(err) {
console.log(err)
}

this.msgCount++;
});

this.pty.onExit(({ exitCode, signal}) => {
this.destroy();
console.log('exiting with', exitCode, signal);
})

this.runInitCommand();
}

runInitCommand() {
setTimeout(() => {
if (this.initialCommand && this.msgCount >= 1) {
this.pty.write(this.initialCommand + '\r');
} else {
this.runInitCommand();
}
}, 500);
}

write(key: string): void {
this.pty.write(key);
}

destroy(): void {
if (this.exitCommand) {
this.pty.write(this.exitCommand)
}

try {
this.pty.kill();

setTimeout(() => {
this.window.destroy();
}, 100);
} catch (err) {
console.log('errored on destroy', err)
}
}
}

const procs = new Map<string, TerminalProcess>();

function withTerm(ship: string, cb: (term: TerminalProcess) => void) {
const term = procs.get(ship);

if (term) {
cb(term);
}
}

export function start(): void {
ipcMain.on('terminal-create', (event, params: TerminalProcessParams) => {
const term = new TerminalProcess(params);
isDev && console.log('creating terminal')
procs.set(params.ship, term);
})

ipcMain.on('terminal-loaded', (event, ship) => {
isDev && console.log('terminal loaded')
withTerm(ship, term => term.init());
})

ipcMain.on('terminal-keystroke', (event, { ship, data }: Payload) => {
withTerm(ship, term => term.write(data))
})

ipcMain.on('terminal-kill', (event, ship) => {
withTerm(ship, term => {
term.destroy();
procs.delete(ship);
});
})
}
Loading

0 comments on commit ede30dd

Please sign in to comment.