parent
638ab7c557
commit
2819fd51e2
|
@ -21,7 +21,6 @@ main() {
|
||||||
--public-url "/static/$(git rev-parse HEAD)/dist" \
|
--public-url "/static/$(git rev-parse HEAD)/dist" \
|
||||||
--out-dir dist \
|
--out-dir dist \
|
||||||
$([[ $MINIFY ]] || echo --no-minify) \
|
$([[ $MINIFY ]] || echo --no-minify) \
|
||||||
src/browser/pages/app.ts \
|
|
||||||
src/browser/register.ts \
|
src/browser/register.ts \
|
||||||
src/browser/serviceWorker.ts
|
src/browser/serviceWorker.ts
|
||||||
}
|
}
|
||||||
|
|
|
@ -144,11 +144,7 @@ class Watcher {
|
||||||
|
|
||||||
private createBundler(out = "dist"): Bundler {
|
private createBundler(out = "dist"): Bundler {
|
||||||
return new Bundler(
|
return new Bundler(
|
||||||
[
|
[path.join(this.rootPath, "src/browser/register.ts"), path.join(this.rootPath, "src/browser/serviceWorker.ts")],
|
||||||
path.join(this.rootPath, "src/browser/pages/app.ts"),
|
|
||||||
path.join(this.rootPath, "src/browser/register.ts"),
|
|
||||||
path.join(this.rootPath, "src/browser/serviceWorker.ts"),
|
|
||||||
],
|
|
||||||
{
|
{
|
||||||
outDir: path.join(this.rootPath, out),
|
outDir: path.join(this.rootPath, out),
|
||||||
cacheDir: path.join(this.rootPath, ".cache"),
|
cacheDir: path.join(this.rootPath, ".cache"),
|
||||||
|
|
|
@ -1,28 +0,0 @@
|
||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8" />
|
|
||||||
<meta
|
|
||||||
name="viewport"
|
|
||||||
content="width=device-width, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0, user-scalable=no"
|
|
||||||
/>
|
|
||||||
<meta
|
|
||||||
http-equiv="Content-Security-Policy"
|
|
||||||
content="style-src 'self' 'unsafe-inline'; manifest-src 'self'; img-src 'self' data:; font-src 'self' data:;"
|
|
||||||
/>
|
|
||||||
<title>code-server</title>
|
|
||||||
<link rel="icon" href="{{BASE}}/static/{{COMMIT}}/src/browser/media/favicon.ico" type="image/x-icon" />
|
|
||||||
<link
|
|
||||||
rel="manifest"
|
|
||||||
href="{{BASE}}/static/{{COMMIT}}/src/browser/media/manifest.json"
|
|
||||||
crossorigin="use-credentials"
|
|
||||||
/>
|
|
||||||
<link rel="apple-touch-icon" href="{{BASE}}/static/{{COMMIT}}/src/browser/media/pwa-icon-384.png" />
|
|
||||||
<link href="{{BASE}}/static/{{COMMIT}}/dist/pages/app.css" rel="stylesheet" />
|
|
||||||
<meta id="coder-options" data-settings="{{OPTIONS}}" />
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<script data-cfasync="false" src="{{BASE}}/static/{{COMMIT}}/dist/register.js"></script>
|
|
||||||
<script data-cfasync="false" src="{{BASE}}/static/{{COMMIT}}/dist/pages/app.js"></script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
|
@ -1,37 +0,0 @@
|
||||||
import { getOptions, normalize } from "../../common/util"
|
|
||||||
import { ApiEndpoint } from "../../common/http"
|
|
||||||
|
|
||||||
import "./error.css"
|
|
||||||
import "./global.css"
|
|
||||||
import "./home.css"
|
|
||||||
import "./login.css"
|
|
||||||
import "./update.css"
|
|
||||||
|
|
||||||
const options = getOptions()
|
|
||||||
|
|
||||||
const isInput = (el: Element): el is HTMLInputElement => {
|
|
||||||
return !!(el as HTMLInputElement).name
|
|
||||||
}
|
|
||||||
|
|
||||||
document.querySelectorAll("form").forEach((form) => {
|
|
||||||
if (!form.classList.contains("-x11")) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
form.addEventListener("submit", (event) => {
|
|
||||||
event.preventDefault()
|
|
||||||
const values: { [key: string]: string } = {}
|
|
||||||
Array.from(form.elements).forEach((element) => {
|
|
||||||
if (isInput(element)) {
|
|
||||||
values[element.name] = element.value
|
|
||||||
}
|
|
||||||
})
|
|
||||||
fetch(normalize(`${options.base}/api/${ApiEndpoint.process}`), {
|
|
||||||
method: "POST",
|
|
||||||
body: JSON.stringify(values),
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
// TEMP: Until we can get the real ready event.
|
|
||||||
const event = new CustomEvent("ide-ready")
|
|
||||||
window.dispatchEvent(event)
|
|
|
@ -18,7 +18,7 @@
|
||||||
crossorigin="use-credentials"
|
crossorigin="use-credentials"
|
||||||
/>
|
/>
|
||||||
<link rel="apple-touch-icon" href="{{BASE}}/static/{{COMMIT}}/src/browser/media/pwa-icon-384.png" />
|
<link rel="apple-touch-icon" href="{{BASE}}/static/{{COMMIT}}/src/browser/media/pwa-icon-384.png" />
|
||||||
<link href="{{BASE}}/static/{{COMMIT}}/dist/pages/app.css" rel="stylesheet" />
|
<link href="{{BASE}}/static/{{COMMIT}}/dist/register.css" rel="stylesheet" />
|
||||||
<meta id="coder-options" data-settings="{{OPTIONS}}" />
|
<meta id="coder-options" data-settings="{{OPTIONS}}" />
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|
|
@ -1,51 +0,0 @@
|
||||||
.block-row {
|
|
||||||
display: flex;
|
|
||||||
}
|
|
||||||
|
|
||||||
.block-row > .item {
|
|
||||||
flex: 1;
|
|
||||||
margin: 2px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.block-row > button.item {
|
|
||||||
background: none;
|
|
||||||
border: none;
|
|
||||||
cursor: pointer;
|
|
||||||
text-align: left;
|
|
||||||
}
|
|
||||||
|
|
||||||
.block-row > .item > .sub {
|
|
||||||
font-size: 0.95em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.block-row .-link {
|
|
||||||
color: rgb(87, 114, 245);
|
|
||||||
display: block;
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.block-row .-link:hover {
|
|
||||||
text-decoration: underline;
|
|
||||||
}
|
|
||||||
|
|
||||||
.block-row > .item > .icon {
|
|
||||||
height: 1rem;
|
|
||||||
margin-right: 5px;
|
|
||||||
vertical-align: top;
|
|
||||||
width: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.block-row > .item > .icon.-missing {
|
|
||||||
background-color: rgba(87, 114, 245, 0.2);
|
|
||||||
display: inline-block;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.kill-form {
|
|
||||||
display: inline-block;
|
|
||||||
}
|
|
||||||
|
|
||||||
.kill-form > .kill {
|
|
||||||
border-radius: 3px;
|
|
||||||
padding: 2px 5px;
|
|
||||||
}
|
|
|
@ -1,59 +0,0 @@
|
||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8" />
|
|
||||||
<meta
|
|
||||||
name="viewport"
|
|
||||||
content="width=device-width, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0, user-scalable=no"
|
|
||||||
/>
|
|
||||||
<meta
|
|
||||||
http-equiv="Content-Security-Policy"
|
|
||||||
content="style-src 'self' 'unsafe-inline'; manifest-src 'self'; img-src 'self' data:; font-src 'self' data:;"
|
|
||||||
/>
|
|
||||||
<title>code-server</title>
|
|
||||||
<link rel="icon" href="{{BASE}}/static/{{COMMIT}}/src/browser/media/favicon.ico" type="image/x-icon" />
|
|
||||||
<link
|
|
||||||
rel="manifest"
|
|
||||||
href="{{BASE}}/static/{{COMMIT}}/src/browser/media/manifest.json"
|
|
||||||
crossorigin="use-credentials"
|
|
||||||
/>
|
|
||||||
<link rel="apple-touch-icon" href="{{BASE}}/static/{{COMMIT}}/src/browser/media/pwa-icon-384.png" />
|
|
||||||
<link href="{{BASE}}/static/{{COMMIT}}/dist/pages/app.css" rel="stylesheet" />
|
|
||||||
<meta id="coder-options" data-settings="{{OPTIONS}}" />
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div class="center-container">
|
|
||||||
<div class="card-box">
|
|
||||||
<div class="header">
|
|
||||||
<h2 class="main">Editors</h2>
|
|
||||||
<div class="sub">Choose an editor to launch below.</div>
|
|
||||||
</div>
|
|
||||||
<div class="content">
|
|
||||||
{{APP_LIST:EDITORS}}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="card-box">
|
|
||||||
<div class="header">
|
|
||||||
<h2 class="main">Other</h2>
|
|
||||||
<div class="sub">Choose an application to launch below.</div>
|
|
||||||
</div>
|
|
||||||
<div class="content">
|
|
||||||
{{APP_LIST:OTHER}}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="card-box">
|
|
||||||
<div class="header">
|
|
||||||
<h2 class="main">Version</h2>
|
|
||||||
<div class="sub">Version information and updates.</div>
|
|
||||||
</div>
|
|
||||||
<div class="content">
|
|
||||||
{{UPDATE:NAME}}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<script data-cfasync="false" src="{{BASE}}/static/{{COMMIT}}/dist/register.js"></script>
|
|
||||||
<script data-cfasync="false" src="{{BASE}}/static/{{COMMIT}}/dist/pages/app.js"></script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
|
@ -18,7 +18,7 @@
|
||||||
crossorigin="use-credentials"
|
crossorigin="use-credentials"
|
||||||
/>
|
/>
|
||||||
<link rel="apple-touch-icon" href="{{BASE}}/static/{{COMMIT}}/src/browser/media/pwa-icon-384.png" />
|
<link rel="apple-touch-icon" href="{{BASE}}/static/{{COMMIT}}/src/browser/media/pwa-icon-384.png" />
|
||||||
<link href="{{BASE}}/static/{{COMMIT}}/dist/pages/app.css" rel="stylesheet" />
|
<link href="{{BASE}}/static/{{COMMIT}}/dist/register.css" rel="stylesheet" />
|
||||||
<meta id="coder-options" data-settings="{{OPTIONS}}" />
|
<meta id="coder-options" data-settings="{{OPTIONS}}" />
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|
|
@ -18,7 +18,7 @@
|
||||||
crossorigin="use-credentials"
|
crossorigin="use-credentials"
|
||||||
/>
|
/>
|
||||||
<link rel="apple-touch-icon" href="{{BASE}}/static/{{COMMIT}}/src/browser/media/pwa-icon-384.png" />
|
<link rel="apple-touch-icon" href="{{BASE}}/static/{{COMMIT}}/src/browser/media/pwa-icon-384.png" />
|
||||||
<link href="{{BASE}}/static/{{COMMIT}}/dist/pages/app.css" rel="stylesheet" />
|
<link href="{{BASE}}/static/{{COMMIT}}/dist/register.css" rel="stylesheet" />
|
||||||
<meta id="coder-options" data-settings="{{OPTIONS}}" />
|
<meta id="coder-options" data-settings="{{OPTIONS}}" />
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|
|
@ -2,13 +2,17 @@ import { getOptions, normalize } from "../common/util"
|
||||||
|
|
||||||
const options = getOptions()
|
const options = getOptions()
|
||||||
|
|
||||||
|
import "./pages/error.css"
|
||||||
|
import "./pages/global.css"
|
||||||
|
import "./pages/login.css"
|
||||||
|
|
||||||
if ("serviceWorker" in navigator) {
|
if ("serviceWorker" in navigator) {
|
||||||
const path = normalize(`${options.base}/static/${options.commit}/dist/serviceWorker.js`)
|
const path = normalize(`${options.base}/static/${options.commit}/dist/serviceWorker.js`)
|
||||||
navigator.serviceWorker
|
navigator.serviceWorker
|
||||||
.register(path, {
|
.register(path, {
|
||||||
scope: options.base || "/",
|
scope: options.base || "/",
|
||||||
})
|
})
|
||||||
.then(function () {
|
.then(() => {
|
||||||
console.log("[Service Worker] registered")
|
console.log("[Service Worker] registered")
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,60 +0,0 @@
|
||||||
export interface Application {
|
|
||||||
readonly categories?: string[]
|
|
||||||
readonly comment?: string
|
|
||||||
readonly directory?: string
|
|
||||||
readonly exec?: string
|
|
||||||
readonly genericName?: string
|
|
||||||
readonly icon?: string
|
|
||||||
readonly installed?: boolean
|
|
||||||
readonly name: string
|
|
||||||
/**
|
|
||||||
* Path if this is a browser app (like VS Code).
|
|
||||||
*/
|
|
||||||
readonly path?: string
|
|
||||||
/**
|
|
||||||
* PID if this is a process.
|
|
||||||
*/
|
|
||||||
readonly pid?: number
|
|
||||||
readonly version?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ApplicationsResponse {
|
|
||||||
readonly applications: ReadonlyArray<Application>
|
|
||||||
}
|
|
||||||
|
|
||||||
export enum SessionError {
|
|
||||||
FailedToStart = 4000,
|
|
||||||
Starting = 4001,
|
|
||||||
InvalidState = 4002,
|
|
||||||
Unknown = 4003,
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface SessionResponse {
|
|
||||||
/**
|
|
||||||
* Whether the process was spawned or an existing one was returned.
|
|
||||||
*/
|
|
||||||
created: boolean
|
|
||||||
pid: number
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface RecentResponse {
|
|
||||||
readonly paths: string[]
|
|
||||||
readonly workspaces: string[]
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface HealthRequest {
|
|
||||||
readonly event: "health"
|
|
||||||
}
|
|
||||||
|
|
||||||
export type ClientMessage = HealthRequest
|
|
||||||
|
|
||||||
export interface HealthResponse {
|
|
||||||
readonly event: "health"
|
|
||||||
readonly connections: number
|
|
||||||
}
|
|
||||||
|
|
||||||
export type ServerMessage = HealthResponse
|
|
||||||
|
|
||||||
export interface ReadyMessage {
|
|
||||||
protocol: string
|
|
||||||
}
|
|
|
@ -14,11 +14,3 @@ export class HttpError extends Error {
|
||||||
this.name = this.constructor.name
|
this.name = this.constructor.name
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum ApiEndpoint {
|
|
||||||
applications = "/applications",
|
|
||||||
process = "/process",
|
|
||||||
recent = "/recent",
|
|
||||||
run = "/run",
|
|
||||||
status = "/status",
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,312 +0,0 @@
|
||||||
import { field, logger } from "@coder/logger"
|
|
||||||
import * as cp from "child_process"
|
|
||||||
import * as fs from "fs-extra"
|
|
||||||
import * as http from "http"
|
|
||||||
import * as net from "net"
|
|
||||||
import * as path from "path"
|
|
||||||
import * as url from "url"
|
|
||||||
import * as WebSocket from "ws"
|
|
||||||
import {
|
|
||||||
Application,
|
|
||||||
ApplicationsResponse,
|
|
||||||
ClientMessage,
|
|
||||||
RecentResponse,
|
|
||||||
ServerMessage,
|
|
||||||
SessionError,
|
|
||||||
SessionResponse,
|
|
||||||
} from "../../common/api"
|
|
||||||
import { ApiEndpoint, HttpCode, HttpError } from "../../common/http"
|
|
||||||
import { HttpProvider, HttpProviderOptions, HttpResponse, HttpServer, Route } from "../http"
|
|
||||||
import { findApplications, findWhitelistedApplications, Vscode } from "./bin"
|
|
||||||
import { VscodeHttpProvider } from "./vscode"
|
|
||||||
|
|
||||||
interface VsRecents {
|
|
||||||
[key: string]: (string | { configURIPath: string })[]
|
|
||||||
}
|
|
||||||
|
|
||||||
type VsSettings = [string, string][]
|
|
||||||
|
|
||||||
/**
|
|
||||||
* API HTTP provider.
|
|
||||||
*/
|
|
||||||
export class ApiHttpProvider extends HttpProvider {
|
|
||||||
private readonly ws = new WebSocket.Server({ noServer: true })
|
|
||||||
|
|
||||||
public constructor(
|
|
||||||
options: HttpProviderOptions,
|
|
||||||
private readonly server: HttpServer,
|
|
||||||
private readonly vscode: VscodeHttpProvider,
|
|
||||||
private readonly dataDir?: string,
|
|
||||||
) {
|
|
||||||
super(options)
|
|
||||||
}
|
|
||||||
|
|
||||||
public async handleRequest(route: Route, request: http.IncomingMessage): Promise<HttpResponse> {
|
|
||||||
this.ensureAuthenticated(request)
|
|
||||||
if (!this.isRoot(route)) {
|
|
||||||
throw new HttpError("Not found", HttpCode.NotFound)
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (route.base) {
|
|
||||||
case ApiEndpoint.applications:
|
|
||||||
this.ensureMethod(request)
|
|
||||||
return {
|
|
||||||
mime: "application/json",
|
|
||||||
content: {
|
|
||||||
applications: await this.applications(),
|
|
||||||
},
|
|
||||||
} as HttpResponse<ApplicationsResponse>
|
|
||||||
case ApiEndpoint.process:
|
|
||||||
return this.process(request)
|
|
||||||
case ApiEndpoint.recent:
|
|
||||||
this.ensureMethod(request)
|
|
||||||
return {
|
|
||||||
mime: "application/json",
|
|
||||||
content: await this.recent(),
|
|
||||||
} as HttpResponse<RecentResponse>
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new HttpError("Not found", HttpCode.NotFound)
|
|
||||||
}
|
|
||||||
|
|
||||||
public async handleWebSocket(
|
|
||||||
route: Route,
|
|
||||||
request: http.IncomingMessage,
|
|
||||||
socket: net.Socket,
|
|
||||||
head: Buffer,
|
|
||||||
): Promise<void> {
|
|
||||||
if (!this.authenticated(request)) {
|
|
||||||
throw new Error("not authenticated")
|
|
||||||
}
|
|
||||||
switch (route.base) {
|
|
||||||
case ApiEndpoint.status:
|
|
||||||
return this.handleStatusSocket(request, socket, head)
|
|
||||||
case ApiEndpoint.run:
|
|
||||||
return this.handleRunSocket(route, request, socket, head)
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new HttpError("Not found", HttpCode.NotFound)
|
|
||||||
}
|
|
||||||
|
|
||||||
private async handleStatusSocket(request: http.IncomingMessage, socket: net.Socket, head: Buffer): Promise<void> {
|
|
||||||
const getMessageResponse = async (event: "health"): Promise<ServerMessage> => {
|
|
||||||
switch (event) {
|
|
||||||
case "health":
|
|
||||||
return { event, connections: await this.server.getConnections() }
|
|
||||||
default:
|
|
||||||
throw new Error("unexpected message")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
await new Promise<WebSocket>((resolve) => {
|
|
||||||
this.ws.handleUpgrade(request, socket, head, (ws) => {
|
|
||||||
const send = (event: ServerMessage): void => {
|
|
||||||
ws.send(JSON.stringify(event))
|
|
||||||
}
|
|
||||||
ws.on("message", (data) => {
|
|
||||||
logger.trace("got message", field("message", data))
|
|
||||||
try {
|
|
||||||
const message: ClientMessage = JSON.parse(data.toString())
|
|
||||||
getMessageResponse(message.event).then(send)
|
|
||||||
} catch (error) {
|
|
||||||
logger.error(error.message, field("message", data))
|
|
||||||
}
|
|
||||||
})
|
|
||||||
resolve()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A socket that connects to the process.
|
|
||||||
*/
|
|
||||||
private async handleRunSocket(
|
|
||||||
_route: Route,
|
|
||||||
request: http.IncomingMessage,
|
|
||||||
socket: net.Socket,
|
|
||||||
head: Buffer,
|
|
||||||
): Promise<void> {
|
|
||||||
logger.debug("connecting to process")
|
|
||||||
const ws = await new Promise<WebSocket>((resolve, reject) => {
|
|
||||||
this.ws.handleUpgrade(request, socket, head, (socket) => {
|
|
||||||
socket.binaryType = "arraybuffer"
|
|
||||||
|
|
||||||
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(
|
|
||||||
JSON.stringify({
|
|
||||||
protocol: "TODO",
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return whitelisted applications.
|
|
||||||
*/
|
|
||||||
public async applications(): Promise<ReadonlyArray<Application>> {
|
|
||||||
return findWhitelistedApplications()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return installed applications.
|
|
||||||
*/
|
|
||||||
public async installedApplications(): Promise<ReadonlyArray<Application>> {
|
|
||||||
return findApplications()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handle /process endpoint.
|
|
||||||
*/
|
|
||||||
private async process(request: http.IncomingMessage): Promise<HttpResponse> {
|
|
||||||
this.ensureMethod(request, ["DELETE", "POST"])
|
|
||||||
|
|
||||||
const data = await this.getData(request)
|
|
||||||
if (!data) {
|
|
||||||
throw new HttpError("No data was provided", HttpCode.BadRequest)
|
|
||||||
}
|
|
||||||
|
|
||||||
const parsed: Application = JSON.parse(data)
|
|
||||||
|
|
||||||
switch (request.method) {
|
|
||||||
case "DELETE":
|
|
||||||
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,
|
|
||||||
pid: await this.spawnProcess(parsed.exec),
|
|
||||||
},
|
|
||||||
} as HttpResponse<SessionResponse>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new HttpError("Not found", HttpCode.NotFound)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Kill a process identified by pid or path if a web app.
|
|
||||||
*/
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Spawn a process and return the pid.
|
|
||||||
*/
|
|
||||||
public async spawnProcess(exec: string): Promise<number> {
|
|
||||||
const proc = cp.spawn(exec, {
|
|
||||||
shell: process.env.SHELL || true,
|
|
||||||
env: {
|
|
||||||
...process.env,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return VS Code's recent paths.
|
|
||||||
*/
|
|
||||||
public async recent(): Promise<RecentResponse> {
|
|
||||||
try {
|
|
||||||
if (!this.dataDir) {
|
|
||||||
throw new Error("data directory is not set")
|
|
||||||
}
|
|
||||||
|
|
||||||
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) {
|
|
||||||
return { paths: [], workspaces: [] }
|
|
||||||
}
|
|
||||||
|
|
||||||
const pathPromises: { [key: string]: Promise<string> } = {}
|
|
||||||
const workspacePromises: { [key: string]: Promise<string> } = {}
|
|
||||||
Object.values(JSON.parse(setting[1]) as VsRecents).forEach((recents) => {
|
|
||||||
recents.forEach((recent) => {
|
|
||||||
try {
|
|
||||||
const target = typeof recent === "string" ? pathPromises : workspacePromises
|
|
||||||
const pathname = url.parse(typeof recent === "string" ? recent : recent.configURIPath).pathname
|
|
||||||
if (pathname && !target[pathname]) {
|
|
||||||
target[pathname] = new Promise<string>((resolve) => {
|
|
||||||
fs.stat(pathname)
|
|
||||||
.then(() => resolve(pathname))
|
|
||||||
.catch(() => resolve())
|
|
||||||
})
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
logger.debug("invalid path", field("path", recent))
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
const [paths, workspaces] = await Promise.all([
|
|
||||||
Promise.all(Object.values(pathPromises)),
|
|
||||||
Promise.all(Object.values(workspacePromises)),
|
|
||||||
])
|
|
||||||
|
|
||||||
return {
|
|
||||||
paths: paths.filter((p) => !!p),
|
|
||||||
workspaces: workspaces.filter((p) => !!p),
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
if (error.code !== "ENOENT") {
|
|
||||||
throw error
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return { paths: [], workspaces: [] }
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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 }),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,30 +0,0 @@
|
||||||
import * as fs from "fs"
|
|
||||||
import * as path from "path"
|
|
||||||
import { Application } from "../../common/api"
|
|
||||||
|
|
||||||
const getVscodeVersion = (): string => {
|
|
||||||
try {
|
|
||||||
return require(path.resolve(__dirname, "../../../lib/vscode/package.json")).version
|
|
||||||
} catch (error) {
|
|
||||||
return "unknown"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const Vscode: Application = {
|
|
||||||
categories: ["Editor"],
|
|
||||||
icon: fs.readFileSync(path.resolve(__dirname, "../../../lib/vscode/resources/linux/code.png")).toString("base64"),
|
|
||||||
installed: true,
|
|
||||||
name: "VS Code",
|
|
||||||
path: "/",
|
|
||||||
version: getVscodeVersion(),
|
|
||||||
}
|
|
||||||
|
|
||||||
export const findApplications = async (): Promise<ReadonlyArray<Application>> => {
|
|
||||||
const apps: Application[] = [Vscode]
|
|
||||||
|
|
||||||
return apps.sort((a, b): number => a.name.localeCompare(b.name))
|
|
||||||
}
|
|
||||||
|
|
||||||
export const findWhitelistedApplications = async (): Promise<ReadonlyArray<Application>> => {
|
|
||||||
return [Vscode]
|
|
||||||
}
|
|
|
@ -1,147 +0,0 @@
|
||||||
import * as http from "http"
|
|
||||||
import * as querystring from "querystring"
|
|
||||||
import { Application } from "../../common/api"
|
|
||||||
import { HttpCode, HttpError } from "../../common/http"
|
|
||||||
import { normalize } from "../../common/util"
|
|
||||||
import { HttpProvider, HttpProviderOptions, HttpResponse, Route } from "../http"
|
|
||||||
import { ApiHttpProvider } from "./api"
|
|
||||||
import { UpdateHttpProvider } from "./update"
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Dashboard HTTP provider.
|
|
||||||
*/
|
|
||||||
export class DashboardHttpProvider extends HttpProvider {
|
|
||||||
public constructor(
|
|
||||||
options: HttpProviderOptions,
|
|
||||||
private readonly api: ApiHttpProvider,
|
|
||||||
private readonly update: UpdateHttpProvider,
|
|
||||||
) {
|
|
||||||
super(options)
|
|
||||||
}
|
|
||||||
|
|
||||||
public async handleRequest(route: Route, request: http.IncomingMessage): Promise<HttpResponse> {
|
|
||||||
if (!this.isRoot(route)) {
|
|
||||||
throw new HttpError("Not found", HttpCode.NotFound)
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (route.base) {
|
|
||||||
case "/spawn": {
|
|
||||||
this.ensureAuthenticated(request)
|
|
||||||
this.ensureMethod(request, "POST")
|
|
||||||
const data = await this.getData(request)
|
|
||||||
const app = data ? querystring.parse(data) : {}
|
|
||||||
if (app.path) {
|
|
||||||
return { redirect: Array.isArray(app.path) ? app.path[0] : app.path }
|
|
||||||
}
|
|
||||||
if (!app.exec) {
|
|
||||||
throw new Error("No exec was provided")
|
|
||||||
}
|
|
||||||
this.api.spawnProcess(Array.isArray(app.exec) ? app.exec[0] : app.exec)
|
|
||||||
return { redirect: this.options.base }
|
|
||||||
}
|
|
||||||
case "/app":
|
|
||||||
case "/": {
|
|
||||||
this.ensureMethod(request)
|
|
||||||
if (!this.authenticated(request)) {
|
|
||||||
return { redirect: "/login", query: { to: this.options.base } }
|
|
||||||
}
|
|
||||||
return route.base === "/" ? this.getRoot(route) : this.getAppRoot(route)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new HttpError("Not found", HttpCode.NotFound)
|
|
||||||
}
|
|
||||||
|
|
||||||
public async getRoot(route: Route): Promise<HttpResponse> {
|
|
||||||
const base = this.base(route)
|
|
||||||
const apps = await this.api.installedApplications()
|
|
||||||
const response = await this.getUtf8Resource(this.rootPath, "src/browser/pages/home.html")
|
|
||||||
response.content = response.content
|
|
||||||
.replace(/{{UPDATE:NAME}}/, await this.getUpdate(base))
|
|
||||||
.replace(
|
|
||||||
/{{APP_LIST:EDITORS}}/,
|
|
||||||
this.getAppRows(
|
|
||||||
base,
|
|
||||||
apps.filter((app) => app.categories && app.categories.includes("Editor")),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.replace(
|
|
||||||
/{{APP_LIST:OTHER}}/,
|
|
||||||
this.getAppRows(
|
|
||||||
base,
|
|
||||||
apps.filter((app) => !app.categories || !app.categories.includes("Editor")),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
return this.replaceTemplates(route, response)
|
|
||||||
}
|
|
||||||
|
|
||||||
public async getAppRoot(route: Route): Promise<HttpResponse> {
|
|
||||||
const response = await this.getUtf8Resource(this.rootPath, "src/browser/pages/app.html")
|
|
||||||
return this.replaceTemplates(route, response)
|
|
||||||
}
|
|
||||||
|
|
||||||
private getAppRows(base: string, apps: ReadonlyArray<Application>): string {
|
|
||||||
return apps.length > 0
|
|
||||||
? apps.map((app) => this.getAppRow(base, app)).join("\n")
|
|
||||||
: `<div class="none">No applications found.</div>`
|
|
||||||
}
|
|
||||||
|
|
||||||
private getAppRow(base: string, app: Application): string {
|
|
||||||
return `<form class="block-row${app.exec ? " -x11" : ""}" method="post" action="${normalize(
|
|
||||||
`${base}${this.options.base}/spawn`,
|
|
||||||
)}">
|
|
||||||
<button class="item -row -link">
|
|
||||||
<input type="hidden" name="path" value="${app.path || ""}">
|
|
||||||
<input type="hidden" name="exec" value="${app.exec || ""}">
|
|
||||||
${
|
|
||||||
app.icon
|
|
||||||
? `<img class="icon" src="data:image/png;base64,${app.icon}"></img>`
|
|
||||||
: `<span class="icon -missing"></span>`
|
|
||||||
}
|
|
||||||
<span class="name">${app.name}</span>
|
|
||||||
</button>
|
|
||||||
</form>`
|
|
||||||
}
|
|
||||||
|
|
||||||
private async getUpdate(base: string): Promise<string> {
|
|
||||||
if (!this.update.enabled) {
|
|
||||||
return `<div class="block-row"><div class="item"><div class="sub">Updates are disabled</div></div></div>`
|
|
||||||
}
|
|
||||||
|
|
||||||
const humanize = (time: number): string => {
|
|
||||||
const d = new Date(time)
|
|
||||||
const pad = (t: number): string => (t < 10 ? "0" : "") + t
|
|
||||||
return (
|
|
||||||
`${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}` +
|
|
||||||
` ${pad(d.getHours())}:${pad(d.getMinutes())}`
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const update = await this.update.getUpdate()
|
|
||||||
if (this.update.isLatestVersion(update)) {
|
|
||||||
return `<div class="block-row">
|
|
||||||
<div class="item">
|
|
||||||
Latest: ${update.version}
|
|
||||||
<div class="sub">Up to date</div>
|
|
||||||
</div>
|
|
||||||
<div class="item">
|
|
||||||
${humanize(update.checked)}
|
|
||||||
<a class="sub -link" href="${base}/update/check?to=${this.options.base}">Check now</a>
|
|
||||||
</div>
|
|
||||||
<div class="item" >Current: ${this.update.currentVersion}</div>
|
|
||||||
</div>`
|
|
||||||
}
|
|
||||||
|
|
||||||
return `<div class="block-row">
|
|
||||||
<div class="item">
|
|
||||||
Latest: ${update.version}
|
|
||||||
<div class="sub">Out of date</div>
|
|
||||||
</div>
|
|
||||||
<div class="item">
|
|
||||||
${humanize(update.checked)}
|
|
||||||
<a class="sub -link" href="${base}/update?to=${this.options.base}">Update now</a>
|
|
||||||
</div>
|
|
||||||
<div class="item" >Current: ${this.update.currentVersion}</div>
|
|
||||||
</div>`
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -2,8 +2,6 @@ import { field, logger } from "@coder/logger"
|
||||||
import * as cp from "child_process"
|
import * as cp from "child_process"
|
||||||
import * as path from "path"
|
import * as path from "path"
|
||||||
import { CliMessage } from "../../lib/vscode/src/vs/server/ipc"
|
import { CliMessage } from "../../lib/vscode/src/vs/server/ipc"
|
||||||
import { ApiHttpProvider } from "./app/api"
|
|
||||||
import { DashboardHttpProvider } from "./app/dashboard"
|
|
||||||
import { LoginHttpProvider } from "./app/login"
|
import { LoginHttpProvider } from "./app/login"
|
||||||
import { ProxyHttpProvider } from "./app/proxy"
|
import { ProxyHttpProvider } from "./app/proxy"
|
||||||
import { StaticHttpProvider } from "./app/static"
|
import { StaticHttpProvider } from "./app/static"
|
||||||
|
@ -73,13 +71,11 @@ const main = async (args: Args, cliArgs: Args, configArgs: Args): Promise<void>
|
||||||
}
|
}
|
||||||
|
|
||||||
const httpServer = new HttpServer(options)
|
const httpServer = new HttpServer(options)
|
||||||
const vscode = httpServer.registerHttpProvider("/", VscodeHttpProvider, args)
|
httpServer.registerHttpProvider("/", VscodeHttpProvider, args)
|
||||||
const api = httpServer.registerHttpProvider("/api", ApiHttpProvider, httpServer, vscode, args["user-data-dir"])
|
httpServer.registerHttpProvider("/update", UpdateHttpProvider, false)
|
||||||
const update = httpServer.registerHttpProvider("/update", UpdateHttpProvider, false)
|
|
||||||
httpServer.registerHttpProvider("/proxy", ProxyHttpProvider)
|
httpServer.registerHttpProvider("/proxy", ProxyHttpProvider)
|
||||||
httpServer.registerHttpProvider("/login", LoginHttpProvider, args.config!, envPassword)
|
httpServer.registerHttpProvider("/login", LoginHttpProvider, args.config!, envPassword)
|
||||||
httpServer.registerHttpProvider("/static", StaticHttpProvider)
|
httpServer.registerHttpProvider("/static", StaticHttpProvider)
|
||||||
httpServer.registerHttpProvider("/dashboard", DashboardHttpProvider, api, update)
|
|
||||||
|
|
||||||
ipcMain().onDispose(() => httpServer.dispose())
|
ipcMain().onDispose(() => httpServer.dispose())
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue