Simplify dashboard

This commit is contained in:
Asher
2020-03-16 12:43:32 -05:00
parent d832f61d5b
commit d192726e80
14 changed files with 205 additions and 304 deletions

View File

@ -11,22 +11,15 @@ import {
ApplicationsResponse,
ClientMessage,
RecentResponse,
RunningResponse,
ServerMessage,
SessionError,
SessionResponse,
} from "../../common/api"
import { ApiEndpoint, HttpCode, HttpError } from "../../common/http"
import { normalize } from "../../common/util"
import { HttpProvider, HttpProviderOptions, HttpResponse, HttpServer, Route } from "../http"
import { findApplications, findWhitelistedApplications, Vscode } from "./bin"
import { VscodeHttpProvider } from "./vscode"
interface ServerSession {
process?: cp.ChildProcess
readonly app: Application
}
interface VsRecents {
[key: string]: (string | { configURIPath: string })[]
}
@ -38,7 +31,6 @@ type VsSettings = [string, string][]
*/
export class ApiHttpProvider extends HttpProvider {
private readonly ws = new WebSocket.Server({ noServer: true })
private readonly sessions = new Map<string, ServerSession>()
public constructor(
options: HttpProviderOptions,
@ -49,14 +41,6 @@ export class ApiHttpProvider extends HttpProvider {
super(options)
}
public dispose(): void {
this.sessions.forEach((s) => {
if (s.process) {
s.process.kill()
}
})
}
public async handleRequest(route: Route, request: http.IncomingMessage): Promise<HttpResponse> {
this.ensureAuthenticated(request)
if (route.requestPath !== "/index.html") {
@ -67,22 +51,19 @@ export class ApiHttpProvider extends HttpProvider {
case ApiEndpoint.applications:
this.ensureMethod(request)
return {
mime: "application/json",
content: {
applications: await this.applications(),
},
} as HttpResponse<ApplicationsResponse>
case ApiEndpoint.session:
return this.session(request)
case ApiEndpoint.process:
return this.process(request)
case ApiEndpoint.recent:
this.ensureMethod(request)
return {
mime: "application/json",
content: await this.recent(),
} as HttpResponse<RecentResponse>
case ApiEndpoint.running:
this.ensureMethod(request)
return {
content: await this.running(),
} as HttpResponse<RunningResponse>
}
throw new HttpError("Not found", HttpCode.NotFound)
@ -137,36 +118,31 @@ export class ApiHttpProvider extends HttpProvider {
}
/**
* A socket that connects to a session.
* A socket that connects to the process.
*/
private async handleRunSocket(
route: Route,
_route: Route,
request: http.IncomingMessage,
socket: net.Socket,
head: Buffer,
): Promise<void> {
const sessionId = route.requestPath.replace(/^\//, "")
logger.debug("connecting session", field("sessionId", sessionId))
logger.debug("connecting to process")
const ws = await new Promise<WebSocket>((resolve, reject) => {
this.ws.handleUpgrade(request, socket, head, (socket) => {
socket.binaryType = "arraybuffer"
const session = this.sessions.get(sessionId)
if (!session) {
socket.close(SessionError.NotFound)
return reject(new Error("session not found"))
}
resolve(socket as WebSocket)
socket.on("error", (error) => {
socket.close(SessionError.FailedToStart)
logger.error("got error while connecting socket", field("error", error))
reject(error)
})
resolve(socket as WebSocket)
})
})
logger.debug("connected to process")
// Send ready message.
ws.send(
Buffer.from(
@ -192,61 +168,40 @@ export class ApiHttpProvider extends HttpProvider {
}
/**
* Get a running application.
* Handle /process endpoint.
*/
public getRunningApplication(sessionIdOrPath?: string): Application | undefined {
if (!sessionIdOrPath) {
return undefined
}
const sessionId = sessionIdOrPath.replace(/\//g, "")
let session = this.sessions.get(sessionId)
if (session) {
logger.debug("found application by session id", field("id", sessionId))
return session.app
}
const base = normalize("/" + sessionIdOrPath)
session = Array.from(this.sessions.values()).find((s) => s.app.path === base)
if (session) {
logger.debug("found application by path", field("path", base))
return session.app
}
logger.debug("no application found matching route", field("route", sessionIdOrPath))
return undefined
}
/**
* Handle /session endpoint.
*/
private async session(request: http.IncomingMessage): Promise<HttpResponse> {
private async process(request: http.IncomingMessage): Promise<HttpResponse> {
this.ensureMethod(request, ["DELETE", "POST"])
const data = await this.getData(request)
if (!data) {
throw new HttpError("Not found", HttpCode.NotFound)
throw new HttpError("No data was provided", HttpCode.BadRequest)
}
const parsed: Application = JSON.parse(data)
switch (request.method) {
case "DELETE":
return this.deleteSession(JSON.parse(data).sessionId)
case "POST": {
// Prevent spawning the same app multiple times.
const parsed: Application = JSON.parse(data)
const app = this.getRunningApplication(parsed.sessionId || parsed.path)
if (app) {
return {
content: {
created: false,
sessionId: app.sessionId,
},
} as HttpResponse<SessionResponse>
if (parsed.pid) {
await this.killProcess(parsed.pid)
} else if (parsed.path) {
await this.killProcess(parsed.path)
} else {
throw new Error("No pid or path was provided")
}
return {
mime: "application/json",
code: HttpCode.Ok,
}
case "POST": {
if (!parsed.exec) {
throw new Error("No exec was provided")
}
return {
mime: "application/json",
content: {
created: true,
sessionId: await this.createSession(parsed),
pid: await this.spawnProcess(parsed.exec),
},
} as HttpResponse<SessionResponse>
}
@ -256,55 +211,39 @@ export class ApiHttpProvider extends HttpProvider {
}
/**
* Kill a session identified by `app.sessionId`.
* Kill a process identified by pid or path if a web app.
*/
public async deleteSession(sessionId: string): Promise<HttpResponse> {
logger.debug("deleting session", field("sessionId", sessionId))
switch (sessionId) {
case "vscode":
await this.vscode.dispose()
return { code: HttpCode.Ok }
default: {
const session = this.sessions.get(sessionId)
if (!session) {
throw new Error("session does not exist")
}
if (session.process) {
session.process.kill()
}
this.sessions.delete(sessionId)
return { code: HttpCode.Ok }
public async killProcess(pid: number | string): Promise<void> {
if (typeof pid === "string") {
switch (pid) {
case Vscode.path:
await this.vscode.dispose()
break
default:
throw new Error(`Process "${pid}" does not exist`)
}
} else {
process.kill(pid)
}
}
/**
* Create a new session and return the session ID.
* Spawn a process and return the pid.
*/
public async createSession(app: Application): Promise<string> {
const sessionId = Math.floor(Math.random() * 10000).toString()
if (this.sessions.has(sessionId)) {
throw new Error("conflicting session id")
}
if (!app.exec) {
throw new Error("cannot execute application with no `exec`")
}
const appSession: ServerSession = {
app: {
...app,
sessionId,
public async spawnProcess(exec: string): Promise<number> {
const proc = cp.spawn(exec, {
shell: process.env.SHELL || true,
env: {
...process.env,
},
}
this.sessions.set(sessionId, appSession)
})
try {
throw new Error("TODO")
} catch (error) {
this.sessions.delete(sessionId)
throw error
}
proc.on("error", (error) => logger.error("process errored", field("pid", proc.pid), field("error", error)))
proc.on("exit", () => logger.debug("process exited", field("pid", proc.pid)))
logger.debug("started process", field("pid", proc.pid))
return proc.pid
}
/**
@ -319,7 +258,7 @@ export class ApiHttpProvider extends HttpProvider {
const state: VsSettings = JSON.parse(await fs.readFile(path.join(this.dataDir, "User/state/global.json"), "utf8"))
const setting = Array.isArray(state) && state.find((item) => item[0] === "recently.opened")
if (!setting) {
throw new Error("settings appear malformed")
return { paths: [], workspaces: [] }
}
const pathPromises: { [key: string]: Promise<string> } = {}
@ -360,34 +299,13 @@ export class ApiHttpProvider extends HttpProvider {
return { paths: [], workspaces: [] }
}
/**
* Return running sessions.
*/
public async running(): Promise<RunningResponse> {
return {
applications: (this.vscode.running
? [
{
...Vscode,
sessionId: "vscode",
},
]
: []
).concat(
Array.from(this.sessions).map(([sessionId, session]) => ({
...session.app,
sessionId,
})),
),
}
}
/**
* For these, just return the error message since they'll be requested as
* JSON.
*/
public async getErrorRoot(_route: Route, _title: string, _header: string, error: string): Promise<HttpResponse> {
return {
mime: "application/json",
content: JSON.stringify({ error }),
}
}