Simplify dashboard
This commit is contained in:
parent
d832f61d5b
commit
d192726e80
@ -6,8 +6,11 @@
|
|||||||
name="viewport"
|
name="viewport"
|
||||||
content="width=device-width, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0, user-scalable=no"
|
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'; manifest-src 'self'; img-src 'self' data:;" />
|
<meta
|
||||||
<title>code-server — {{APP_NAME}}</title>
|
http-equiv="Content-Security-Policy"
|
||||||
|
content="style-src 'self' 'unsafe-inline'; manifest-src 'self'; img-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="icon" href="{{BASE}}/static/{{COMMIT}}/src/browser/media/favicon.ico" type="image/x-icon" />
|
||||||
<link
|
<link
|
||||||
rel="manifest"
|
rel="manifest"
|
||||||
@ -20,6 +23,6 @@
|
|||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<script src="{{BASE}}/static/{{COMMIT}}/dist/register.js"></script>
|
<script src="{{BASE}}/static/{{COMMIT}}/dist/register.js"></script>
|
||||||
<script src="{{BASE}}/static/{{COMMIT}}/dist/app.js"></script>
|
<script src="{{BASE}}/static/{{COMMIT}}/dist/pages/app.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import { getOptions } from "../../common/util"
|
import { getOptions, normalize } from "../../common/util"
|
||||||
|
import { ApiEndpoint } from "../../common/http"
|
||||||
|
|
||||||
import "./app.css"
|
import "./app.css"
|
||||||
import "./error.css"
|
import "./error.css"
|
||||||
@ -9,4 +10,29 @@ import "./update.css"
|
|||||||
|
|
||||||
const options = getOptions()
|
const options = getOptions()
|
||||||
|
|
||||||
console.log(options)
|
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)
|
||||||
|
@ -71,6 +71,15 @@ button {
|
|||||||
padding: 40px;
|
padding: 40px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.card-box > .content > .none {
|
||||||
|
margin: 2px 0;
|
||||||
|
}
|
||||||
|
|
||||||
.card-box + .card-box {
|
.card-box + .card-box {
|
||||||
margin-top: 26px;
|
margin-top: 26px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
canvas {
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
}
|
||||||
|
@ -7,8 +7,11 @@
|
|||||||
margin: 2px 0;
|
margin: 2px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.block-row > .item.-row {
|
.block-row > button.item {
|
||||||
display: flex;
|
background: none;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
text-align: left;
|
||||||
}
|
}
|
||||||
|
|
||||||
.block-row > .item > .sub {
|
.block-row > .item > .sub {
|
||||||
@ -34,6 +37,7 @@
|
|||||||
|
|
||||||
.block-row > .item > .icon.-missing {
|
.block-row > .item > .icon.-missing {
|
||||||
background-color: rgba(87, 114, 245, 0.2);
|
background-color: rgba(87, 114, 245, 0.2);
|
||||||
|
display: inline-block;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -6,7 +6,7 @@
|
|||||||
name="viewport"
|
name="viewport"
|
||||||
content="width=device-width, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0, user-scalable=no"
|
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'; manifest-src 'self'; img-src 'self' data:;" />
|
<meta http-equiv="Content-Security-Policy" content="style-src 'self' 'unsafe-inline'; manifest-src 'self'; img-src 'self' data:;" />
|
||||||
<title>code-server</title>
|
<title>code-server</title>
|
||||||
<link rel="icon" href="{{BASE}}/static/{{COMMIT}}/src/browser/media/favicon.ico" type="image/x-icon" />
|
<link rel="icon" href="{{BASE}}/static/{{COMMIT}}/src/browser/media/favicon.ico" type="image/x-icon" />
|
||||||
<link
|
<link
|
||||||
@ -20,26 +20,6 @@
|
|||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="center-container">
|
<div class="center-container">
|
||||||
<div class="card-box">
|
|
||||||
<div class="header">
|
|
||||||
<h2 class="main">Running</h2>
|
|
||||||
<div class="sub">Currently running applications.</div>
|
|
||||||
</div>
|
|
||||||
<div class="content">
|
|
||||||
{{APP_LIST:RUNNING}}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="card-box">
|
|
||||||
<div class="header">
|
|
||||||
<h2 class="main">Recent</h2>
|
|
||||||
<div class="sub">Choose a recent directory or workspace to launch below.</div>
|
|
||||||
</div>
|
|
||||||
<div class="content">
|
|
||||||
{{APP_LIST:RECENT_PROJECTS}}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="card-box">
|
<div class="card-box">
|
||||||
<div class="header">
|
<div class="header">
|
||||||
<h2 class="main">Editors</h2>
|
<h2 class="main">Editors</h2>
|
||||||
@ -50,15 +30,15 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- <div class="card-box"> -->
|
<div class="card-box">
|
||||||
<!-- <div class="header"> -->
|
<div class="header">
|
||||||
<!-- <h2 class="main">Other</h2> -->
|
<h2 class="main">Other</h2>
|
||||||
<!-- <div class="sub">Choose an application to launch below.</div> -->
|
<div class="sub">Choose an application to launch below.</div>
|
||||||
<!-- </div> -->
|
</div>
|
||||||
<!-- <div class="content"> -->
|
<div class="content">
|
||||||
<!-- {{APP_LIST:OTHER}} -->
|
{{APP_LIST:OTHER}}
|
||||||
<!-- </div> -->
|
</div>
|
||||||
<!-- </div> -->
|
</div>
|
||||||
|
|
||||||
<div class="card-box">
|
<div class="card-box">
|
||||||
<div class="header">
|
<div class="header">
|
||||||
@ -71,5 +51,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<script src="{{BASE}}/static/{{COMMIT}}/dist/register.js"></script>
|
<script src="{{BASE}}/static/{{COMMIT}}/dist/register.js"></script>
|
||||||
|
<script src="{{BASE}}/static/{{COMMIT}}/dist/pages/app.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
@ -7,8 +7,14 @@ export interface Application {
|
|||||||
readonly icon?: string
|
readonly icon?: string
|
||||||
readonly installed?: boolean
|
readonly installed?: boolean
|
||||||
readonly name: string
|
readonly name: string
|
||||||
|
/**
|
||||||
|
* Path if this is a browser app (like VS Code).
|
||||||
|
*/
|
||||||
readonly path?: string
|
readonly path?: string
|
||||||
readonly sessionId?: string
|
/**
|
||||||
|
* PID if this is a process.
|
||||||
|
*/
|
||||||
|
readonly pid?: number
|
||||||
readonly version?: string
|
readonly version?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -17,19 +23,18 @@ export interface ApplicationsResponse {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export enum SessionError {
|
export enum SessionError {
|
||||||
NotFound = 4000,
|
FailedToStart = 4000,
|
||||||
FailedToStart,
|
Starting = 4001,
|
||||||
Starting,
|
InvalidState = 4002,
|
||||||
InvalidState,
|
Unknown = 4003,
|
||||||
Unknown,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SessionResponse {
|
export interface SessionResponse {
|
||||||
/**
|
/**
|
||||||
* Whether the session was created or an existing one was returned.
|
* Whether the process was spawned or an existing one was returned.
|
||||||
*/
|
*/
|
||||||
created: boolean
|
created: boolean
|
||||||
sessionId: string
|
pid: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface RecentResponse {
|
export interface RecentResponse {
|
||||||
@ -37,10 +42,6 @@ export interface RecentResponse {
|
|||||||
readonly workspaces: string[]
|
readonly workspaces: string[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface RunningResponse {
|
|
||||||
readonly applications: ReadonlyArray<Application>
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface HealthRequest {
|
export interface HealthRequest {
|
||||||
readonly event: "health"
|
readonly event: "health"
|
||||||
}
|
}
|
||||||
|
@ -17,9 +17,8 @@ export class HttpError extends Error {
|
|||||||
|
|
||||||
export enum ApiEndpoint {
|
export enum ApiEndpoint {
|
||||||
applications = "/applications",
|
applications = "/applications",
|
||||||
|
process = "/process",
|
||||||
recent = "/recent",
|
recent = "/recent",
|
||||||
run = "/run",
|
run = "/run",
|
||||||
running = "/running",
|
|
||||||
session = "/session",
|
|
||||||
status = "/status",
|
status = "/status",
|
||||||
}
|
}
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
import { logger } from "@coder/logger"
|
import { logger, field } from "@coder/logger"
|
||||||
|
|
||||||
export interface Options {
|
export interface Options {
|
||||||
base: string
|
base: string
|
||||||
commit: string
|
commit: string
|
||||||
logLevel: number
|
logLevel: number
|
||||||
sessionId?: string
|
pid?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -34,14 +34,12 @@ export const normalize = (url: string, keepTrailing = false): string => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get options embedded in the HTML from the server.
|
* Get options embedded in the HTML or query params.
|
||||||
*/
|
*/
|
||||||
export const getOptions = <T extends Options>(): T => {
|
export const getOptions = <T extends Options>(): T => {
|
||||||
if (typeof document === "undefined") {
|
let options: T
|
||||||
return {} as T
|
|
||||||
}
|
|
||||||
const el = document.getElementById("coder-options")
|
|
||||||
try {
|
try {
|
||||||
|
const el = document.getElementById("coder-options")
|
||||||
if (!el) {
|
if (!el) {
|
||||||
throw new Error("no options element")
|
throw new Error("no options element")
|
||||||
}
|
}
|
||||||
@ -49,19 +47,31 @@ export const getOptions = <T extends Options>(): T => {
|
|||||||
if (!value) {
|
if (!value) {
|
||||||
throw new Error("no options value")
|
throw new Error("no options value")
|
||||||
}
|
}
|
||||||
const options = JSON.parse(value)
|
options = JSON.parse(value)
|
||||||
|
} catch (error) {
|
||||||
|
options = {} as T
|
||||||
|
}
|
||||||
|
|
||||||
|
const params = new URLSearchParams(location.search)
|
||||||
|
const queryOpts = params.get("options")
|
||||||
|
if (queryOpts) {
|
||||||
|
options = {
|
||||||
|
...options,
|
||||||
|
...JSON.parse(queryOpts),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (typeof options.logLevel !== "undefined") {
|
if (typeof options.logLevel !== "undefined") {
|
||||||
logger.level = options.logLevel
|
logger.level = options.logLevel
|
||||||
}
|
}
|
||||||
const parts = window.location.pathname.replace(/^\//g, "").split("/")
|
if (options.base) {
|
||||||
|
const parts = location.pathname.replace(/^\//g, "").split("/")
|
||||||
parts[parts.length - 1] = options.base
|
parts[parts.length - 1] = options.base
|
||||||
const url = new URL(window.location.origin + "/" + parts.join("/"))
|
const url = new URL(location.origin + "/" + parts.join("/"))
|
||||||
return {
|
options.base = normalize(url.pathname, true)
|
||||||
...options,
|
|
||||||
base: normalize(url.pathname, true),
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
logger.warn(error.message)
|
|
||||||
return {} as T
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
logger.debug("got options", field("options", options))
|
||||||
|
|
||||||
|
return options
|
||||||
}
|
}
|
||||||
|
@ -11,22 +11,15 @@ import {
|
|||||||
ApplicationsResponse,
|
ApplicationsResponse,
|
||||||
ClientMessage,
|
ClientMessage,
|
||||||
RecentResponse,
|
RecentResponse,
|
||||||
RunningResponse,
|
|
||||||
ServerMessage,
|
ServerMessage,
|
||||||
SessionError,
|
SessionError,
|
||||||
SessionResponse,
|
SessionResponse,
|
||||||
} from "../../common/api"
|
} from "../../common/api"
|
||||||
import { ApiEndpoint, HttpCode, HttpError } from "../../common/http"
|
import { ApiEndpoint, HttpCode, HttpError } from "../../common/http"
|
||||||
import { normalize } from "../../common/util"
|
|
||||||
import { HttpProvider, HttpProviderOptions, HttpResponse, HttpServer, Route } from "../http"
|
import { HttpProvider, HttpProviderOptions, HttpResponse, HttpServer, Route } from "../http"
|
||||||
import { findApplications, findWhitelistedApplications, Vscode } from "./bin"
|
import { findApplications, findWhitelistedApplications, Vscode } from "./bin"
|
||||||
import { VscodeHttpProvider } from "./vscode"
|
import { VscodeHttpProvider } from "./vscode"
|
||||||
|
|
||||||
interface ServerSession {
|
|
||||||
process?: cp.ChildProcess
|
|
||||||
readonly app: Application
|
|
||||||
}
|
|
||||||
|
|
||||||
interface VsRecents {
|
interface VsRecents {
|
||||||
[key: string]: (string | { configURIPath: string })[]
|
[key: string]: (string | { configURIPath: string })[]
|
||||||
}
|
}
|
||||||
@ -38,7 +31,6 @@ type VsSettings = [string, string][]
|
|||||||
*/
|
*/
|
||||||
export class ApiHttpProvider extends HttpProvider {
|
export class ApiHttpProvider extends HttpProvider {
|
||||||
private readonly ws = new WebSocket.Server({ noServer: true })
|
private readonly ws = new WebSocket.Server({ noServer: true })
|
||||||
private readonly sessions = new Map<string, ServerSession>()
|
|
||||||
|
|
||||||
public constructor(
|
public constructor(
|
||||||
options: HttpProviderOptions,
|
options: HttpProviderOptions,
|
||||||
@ -49,14 +41,6 @@ export class ApiHttpProvider extends HttpProvider {
|
|||||||
super(options)
|
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> {
|
public async handleRequest(route: Route, request: http.IncomingMessage): Promise<HttpResponse> {
|
||||||
this.ensureAuthenticated(request)
|
this.ensureAuthenticated(request)
|
||||||
if (route.requestPath !== "/index.html") {
|
if (route.requestPath !== "/index.html") {
|
||||||
@ -67,22 +51,19 @@ export class ApiHttpProvider extends HttpProvider {
|
|||||||
case ApiEndpoint.applications:
|
case ApiEndpoint.applications:
|
||||||
this.ensureMethod(request)
|
this.ensureMethod(request)
|
||||||
return {
|
return {
|
||||||
|
mime: "application/json",
|
||||||
content: {
|
content: {
|
||||||
applications: await this.applications(),
|
applications: await this.applications(),
|
||||||
},
|
},
|
||||||
} as HttpResponse<ApplicationsResponse>
|
} as HttpResponse<ApplicationsResponse>
|
||||||
case ApiEndpoint.session:
|
case ApiEndpoint.process:
|
||||||
return this.session(request)
|
return this.process(request)
|
||||||
case ApiEndpoint.recent:
|
case ApiEndpoint.recent:
|
||||||
this.ensureMethod(request)
|
this.ensureMethod(request)
|
||||||
return {
|
return {
|
||||||
|
mime: "application/json",
|
||||||
content: await this.recent(),
|
content: await this.recent(),
|
||||||
} as HttpResponse<RecentResponse>
|
} as HttpResponse<RecentResponse>
|
||||||
case ApiEndpoint.running:
|
|
||||||
this.ensureMethod(request)
|
|
||||||
return {
|
|
||||||
content: await this.running(),
|
|
||||||
} as HttpResponse<RunningResponse>
|
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new HttpError("Not found", HttpCode.NotFound)
|
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(
|
private async handleRunSocket(
|
||||||
route: Route,
|
_route: Route,
|
||||||
request: http.IncomingMessage,
|
request: http.IncomingMessage,
|
||||||
socket: net.Socket,
|
socket: net.Socket,
|
||||||
head: Buffer,
|
head: Buffer,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const sessionId = route.requestPath.replace(/^\//, "")
|
logger.debug("connecting to process")
|
||||||
logger.debug("connecting session", field("sessionId", sessionId))
|
|
||||||
const ws = await new Promise<WebSocket>((resolve, reject) => {
|
const ws = await new Promise<WebSocket>((resolve, reject) => {
|
||||||
this.ws.handleUpgrade(request, socket, head, (socket) => {
|
this.ws.handleUpgrade(request, socket, head, (socket) => {
|
||||||
socket.binaryType = "arraybuffer"
|
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.on("error", (error) => {
|
||||||
socket.close(SessionError.FailedToStart)
|
socket.close(SessionError.FailedToStart)
|
||||||
logger.error("got error while connecting socket", field("error", error))
|
logger.error("got error while connecting socket", field("error", error))
|
||||||
reject(error)
|
reject(error)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
resolve(socket as WebSocket)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
logger.debug("connected to process")
|
||||||
|
|
||||||
// Send ready message.
|
// Send ready message.
|
||||||
ws.send(
|
ws.send(
|
||||||
Buffer.from(
|
Buffer.from(
|
||||||
@ -192,61 +168,40 @@ export class ApiHttpProvider extends HttpProvider {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get a running application.
|
* Handle /process endpoint.
|
||||||
*/
|
*/
|
||||||
public getRunningApplication(sessionIdOrPath?: string): Application | undefined {
|
private async process(request: http.IncomingMessage): Promise<HttpResponse> {
|
||||||
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> {
|
|
||||||
this.ensureMethod(request, ["DELETE", "POST"])
|
this.ensureMethod(request, ["DELETE", "POST"])
|
||||||
|
|
||||||
const data = await this.getData(request)
|
const data = await this.getData(request)
|
||||||
if (!data) {
|
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) {
|
switch (request.method) {
|
||||||
case "DELETE":
|
case "DELETE":
|
||||||
return this.deleteSession(JSON.parse(data).sessionId)
|
if (parsed.pid) {
|
||||||
case "POST": {
|
await this.killProcess(parsed.pid)
|
||||||
// Prevent spawning the same app multiple times.
|
} else if (parsed.path) {
|
||||||
const parsed: Application = JSON.parse(data)
|
await this.killProcess(parsed.path)
|
||||||
const app = this.getRunningApplication(parsed.sessionId || parsed.path)
|
} else {
|
||||||
if (app) {
|
throw new Error("No pid or path was provided")
|
||||||
return {
|
|
||||||
content: {
|
|
||||||
created: false,
|
|
||||||
sessionId: app.sessionId,
|
|
||||||
},
|
|
||||||
} as HttpResponse<SessionResponse>
|
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
|
mime: "application/json",
|
||||||
|
code: HttpCode.Ok,
|
||||||
|
}
|
||||||
|
case "POST": {
|
||||||
|
if (!parsed.exec) {
|
||||||
|
throw new Error("No exec was provided")
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
mime: "application/json",
|
||||||
content: {
|
content: {
|
||||||
created: true,
|
created: true,
|
||||||
sessionId: await this.createSession(parsed),
|
pid: await this.spawnProcess(parsed.exec),
|
||||||
},
|
},
|
||||||
} as HttpResponse<SessionResponse>
|
} 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> {
|
public async killProcess(pid: number | string): Promise<void> {
|
||||||
logger.debug("deleting session", field("sessionId", sessionId))
|
if (typeof pid === "string") {
|
||||||
switch (sessionId) {
|
switch (pid) {
|
||||||
case "vscode":
|
case Vscode.path:
|
||||||
await this.vscode.dispose()
|
await this.vscode.dispose()
|
||||||
return { code: HttpCode.Ok }
|
break
|
||||||
default: {
|
default:
|
||||||
const session = this.sessions.get(sessionId)
|
throw new Error(`Process "${pid}" does not exist`)
|
||||||
if (!session) {
|
|
||||||
throw new Error("session does not exist")
|
|
||||||
}
|
|
||||||
if (session.process) {
|
|
||||||
session.process.kill()
|
|
||||||
}
|
|
||||||
this.sessions.delete(sessionId)
|
|
||||||
return { code: HttpCode.Ok }
|
|
||||||
}
|
}
|
||||||
|
} 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> {
|
public async spawnProcess(exec: string): Promise<number> {
|
||||||
const sessionId = Math.floor(Math.random() * 10000).toString()
|
const proc = cp.spawn(exec, {
|
||||||
if (this.sessions.has(sessionId)) {
|
shell: process.env.SHELL || true,
|
||||||
throw new Error("conflicting session id")
|
env: {
|
||||||
}
|
...process.env,
|
||||||
|
|
||||||
if (!app.exec) {
|
|
||||||
throw new Error("cannot execute application with no `exec`")
|
|
||||||
}
|
|
||||||
|
|
||||||
const appSession: ServerSession = {
|
|
||||||
app: {
|
|
||||||
...app,
|
|
||||||
sessionId,
|
|
||||||
},
|
},
|
||||||
}
|
})
|
||||||
this.sessions.set(sessionId, appSession)
|
|
||||||
|
|
||||||
try {
|
proc.on("error", (error) => logger.error("process errored", field("pid", proc.pid), field("error", error)))
|
||||||
throw new Error("TODO")
|
proc.on("exit", () => logger.debug("process exited", field("pid", proc.pid)))
|
||||||
} catch (error) {
|
|
||||||
this.sessions.delete(sessionId)
|
logger.debug("started process", field("pid", proc.pid))
|
||||||
throw error
|
|
||||||
}
|
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 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")
|
const setting = Array.isArray(state) && state.find((item) => item[0] === "recently.opened")
|
||||||
if (!setting) {
|
if (!setting) {
|
||||||
throw new Error("settings appear malformed")
|
return { paths: [], workspaces: [] }
|
||||||
}
|
}
|
||||||
|
|
||||||
const pathPromises: { [key: string]: Promise<string> } = {}
|
const pathPromises: { [key: string]: Promise<string> } = {}
|
||||||
@ -360,34 +299,13 @@ export class ApiHttpProvider extends HttpProvider {
|
|||||||
return { paths: [], workspaces: [] }
|
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
|
* For these, just return the error message since they'll be requested as
|
||||||
* JSON.
|
* JSON.
|
||||||
*/
|
*/
|
||||||
public async getErrorRoot(_route: Route, _title: string, _header: string, error: string): Promise<HttpResponse> {
|
public async getErrorRoot(_route: Route, _title: string, _header: string, error: string): Promise<HttpResponse> {
|
||||||
return {
|
return {
|
||||||
|
mime: "application/json",
|
||||||
content: JSON.stringify({ error }),
|
content: JSON.stringify({ error }),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,46 +0,0 @@
|
|||||||
import * as http from "http"
|
|
||||||
import { HttpCode, HttpError } from "../../common/http"
|
|
||||||
import { HttpProvider, HttpProviderOptions, HttpResponse, Route } from "../http"
|
|
||||||
import { ApiHttpProvider } from "./api"
|
|
||||||
|
|
||||||
/**
|
|
||||||
* App/fallback HTTP provider.
|
|
||||||
*/
|
|
||||||
export class AppHttpProvider extends HttpProvider {
|
|
||||||
public constructor(options: HttpProviderOptions, private readonly api: ApiHttpProvider) {
|
|
||||||
super(options)
|
|
||||||
}
|
|
||||||
|
|
||||||
public async handleRequest(route: Route, request: http.IncomingMessage): Promise<HttpResponse> {
|
|
||||||
if (!this.authenticated(request)) {
|
|
||||||
return { redirect: "/login", query: { to: route.fullPath } }
|
|
||||||
}
|
|
||||||
|
|
||||||
this.ensureMethod(request)
|
|
||||||
if (route.requestPath !== "/index.html") {
|
|
||||||
throw new HttpError("Not found", HttpCode.NotFound)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Run an existing app, but if it doesn't exist go ahead and start it.
|
|
||||||
let app = this.api.getRunningApplication(route.base)
|
|
||||||
let sessionId = app && app.sessionId
|
|
||||||
if (!app) {
|
|
||||||
app = (await this.api.installedApplications()).find((a) => a.path === route.base)
|
|
||||||
if (app && app.exec) {
|
|
||||||
sessionId = await this.api.createSession(app)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (sessionId) {
|
|
||||||
return this.getAppRoot(route, (app && app.name) || "", sessionId)
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new HttpError("Application not found", HttpCode.NotFound)
|
|
||||||
}
|
|
||||||
|
|
||||||
public async getAppRoot(route: Route, name: string, sessionId: string): Promise<HttpResponse> {
|
|
||||||
const response = await this.getUtf8Resource(this.rootPath, "src/browser/pages/app.html")
|
|
||||||
response.content = response.content.replace(/{{APP_NAME}}/, name)
|
|
||||||
return this.replaceTemplates(route, response, sessionId)
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,10 +1,10 @@
|
|||||||
import * as http from "http"
|
import * as http from "http"
|
||||||
import * as querystring from "querystring"
|
import * as querystring from "querystring"
|
||||||
import { Application, RecentResponse } from "../../common/api"
|
import { Application } from "../../common/api"
|
||||||
import { HttpCode, HttpError } from "../../common/http"
|
import { HttpCode, HttpError } from "../../common/http"
|
||||||
|
import { normalize } from "../../common/util"
|
||||||
import { HttpProvider, HttpProviderOptions, HttpResponse, Route } from "../http"
|
import { HttpProvider, HttpProviderOptions, HttpResponse, Route } from "../http"
|
||||||
import { ApiHttpProvider } from "./api"
|
import { ApiHttpProvider } from "./api"
|
||||||
import { Vscode } from "./bin"
|
|
||||||
import { UpdateHttpProvider } from "./update"
|
import { UpdateHttpProvider } from "./update"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -25,21 +25,27 @@ export class DashboardHttpProvider extends HttpProvider {
|
|||||||
}
|
}
|
||||||
|
|
||||||
switch (route.base) {
|
switch (route.base) {
|
||||||
case "/delete": {
|
case "/spawn": {
|
||||||
this.ensureAuthenticated(request)
|
this.ensureAuthenticated(request)
|
||||||
this.ensureMethod(request, "POST")
|
this.ensureMethod(request, "POST")
|
||||||
const data = await this.getData(request)
|
const data = await this.getData(request)
|
||||||
const p = data ? querystring.parse(data) : {}
|
const app = data ? querystring.parse(data) : {}
|
||||||
this.api.deleteSession(p.sessionId as string)
|
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 }
|
return { redirect: this.options.base }
|
||||||
}
|
}
|
||||||
|
case "/app":
|
||||||
case "/": {
|
case "/": {
|
||||||
this.ensureMethod(request)
|
this.ensureMethod(request)
|
||||||
if (!this.authenticated(request)) {
|
if (!this.authenticated(request)) {
|
||||||
return { redirect: "/login", query: { to: this.options.base } }
|
return { redirect: "/login", query: { to: this.options.base } }
|
||||||
}
|
}
|
||||||
return this.getRoot(route)
|
return route.base === "/" ? this.getRoot(route) : this.getAppRoot(route)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -52,8 +58,6 @@ export class DashboardHttpProvider extends HttpProvider {
|
|||||||
const response = await this.getUtf8Resource(this.rootPath, "src/browser/pages/home.html")
|
const response = await this.getUtf8Resource(this.rootPath, "src/browser/pages/home.html")
|
||||||
response.content = response.content
|
response.content = response.content
|
||||||
.replace(/{{UPDATE:NAME}}/, await this.getUpdate(base))
|
.replace(/{{UPDATE:NAME}}/, await this.getUpdate(base))
|
||||||
.replace(/{{APP_LIST:RUNNING}}/, this.getAppRows(base, (await this.api.running()).applications))
|
|
||||||
.replace(/{{APP_LIST:RECENT_PROJECTS}}/, this.getRecentProjectRows(base, await this.api.recent()))
|
|
||||||
.replace(
|
.replace(
|
||||||
/{{APP_LIST:EDITORS}}/,
|
/{{APP_LIST:EDITORS}}/,
|
||||||
this.getAppRows(
|
this.getAppRows(
|
||||||
@ -71,46 +75,32 @@ export class DashboardHttpProvider extends HttpProvider {
|
|||||||
return this.replaceTemplates(route, response)
|
return this.replaceTemplates(route, response)
|
||||||
}
|
}
|
||||||
|
|
||||||
private getRecentProjectRows(base: string, recents: RecentResponse): string {
|
public async getAppRoot(route: Route): Promise<HttpResponse> {
|
||||||
return recents.paths.length > 0 || recents.workspaces.length > 0
|
const response = await this.getUtf8Resource(this.rootPath, "src/browser/pages/app.html")
|
||||||
? recents.paths.map((recent) => this.getRecentProjectRow(base, recent)).join("\n") +
|
return this.replaceTemplates(route, response)
|
||||||
recents.workspaces.map((recent) => this.getRecentProjectRow(base, recent, true)).join("\n")
|
|
||||||
: `<div class="none">No recent directories or workspaces.</div>`
|
|
||||||
}
|
|
||||||
|
|
||||||
private getRecentProjectRow(base: string, recent: string, workspace?: boolean): string {
|
|
||||||
return `<div class="block-row">
|
|
||||||
<a class="item -row -link" href="${base}${Vscode.path}?${workspace ? "workspace" : "folder"}=${recent}">
|
|
||||||
<div class="name">${recent}${workspace ? " (workspace)" : ""}</div>
|
|
||||||
</a>
|
|
||||||
</div>`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private getAppRows(base: string, apps: ReadonlyArray<Application>): string {
|
private getAppRows(base: string, apps: ReadonlyArray<Application>): string {
|
||||||
return apps.length > 0
|
return apps.length > 0
|
||||||
? apps.map((app) => this.getAppRow(base, app)).join("\n")
|
? apps.map((app) => this.getAppRow(base, app)).join("\n")
|
||||||
: `<div class="none">No applications are currently running.</div>`
|
: `<div class="none">No applications found.</div>`
|
||||||
}
|
}
|
||||||
|
|
||||||
private getAppRow(base: string, app: Application): string {
|
private getAppRow(base: string, app: Application): string {
|
||||||
return `<div class="block-row">
|
return `<form class="block-row${app.exec ? " -x11" : ""}" method="post" action="${normalize(
|
||||||
<a class="item -row -link" href="${base}${app.path}">
|
`${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
|
app.icon
|
||||||
? `<img class="icon" src="data:image/png;base64,${app.icon}"></img>`
|
? `<img class="icon" src="data:image/png;base64,${app.icon}"></img>`
|
||||||
: `<div class="icon -missing"></div>`
|
: `<span class="icon -missing"></span>`
|
||||||
}
|
}
|
||||||
<div class="name">${app.name}</div>
|
<span class="name">${app.name}</span>
|
||||||
</a>
|
</button>
|
||||||
${
|
|
||||||
app.sessionId
|
|
||||||
? `<form class="kill-form" action="${base}${this.options.base}/delete" method="POST">
|
|
||||||
<input type="hidden" name="sessionId" value="${app.sessionId}">
|
|
||||||
<button class="kill -button" type="submit">Kill</button>
|
|
||||||
</form>`
|
</form>`
|
||||||
: ""
|
|
||||||
}
|
|
||||||
</div>`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async getUpdate(base: string): Promise<string> {
|
private async getUpdate(base: string): Promise<string> {
|
||||||
|
@ -140,7 +140,7 @@ export class UpdateHttpProvider extends HttpProvider {
|
|||||||
update = { checked: now, version: data.name }
|
update = { checked: now, version: data.name }
|
||||||
await this.settings.write({ update })
|
await this.settings.write({ update })
|
||||||
}
|
}
|
||||||
logger.debug("Got latest version", field("latest", update.version))
|
logger.debug("got latest version", field("latest", update.version))
|
||||||
return update
|
return update
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error("Failed to get latest version", field("error", error.message))
|
logger.error("Failed to get latest version", field("error", error.message))
|
||||||
@ -160,7 +160,7 @@ export class UpdateHttpProvider extends HttpProvider {
|
|||||||
*/
|
*/
|
||||||
public isLatestVersion(latest: Update): boolean {
|
public isLatestVersion(latest: Update): boolean {
|
||||||
const version = this.currentVersion
|
const version = this.currentVersion
|
||||||
logger.debug("Comparing versions", field("current", version), field("latest", latest.version))
|
logger.debug("comparing versions", field("current", version), field("latest", latest.version))
|
||||||
try {
|
try {
|
||||||
return latest.version === version || semver.lt(latest.version, version)
|
return latest.version === version || semver.lt(latest.version, version)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
@ -3,7 +3,6 @@ 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 { ApiHttpProvider } from "./app/api"
|
||||||
import { AppHttpProvider } from "./app/app"
|
|
||||||
import { DashboardHttpProvider } from "./app/dashboard"
|
import { DashboardHttpProvider } from "./app/dashboard"
|
||||||
import { LoginHttpProvider } from "./app/login"
|
import { LoginHttpProvider } from "./app/login"
|
||||||
import { StaticHttpProvider } from "./app/static"
|
import { StaticHttpProvider } from "./app/static"
|
||||||
@ -49,7 +48,6 @@ const main = async (args: Args): Promise<void> => {
|
|||||||
const vscode = httpServer.registerHttpProvider("/", VscodeHttpProvider, args)
|
const vscode = httpServer.registerHttpProvider("/", VscodeHttpProvider, args)
|
||||||
const api = httpServer.registerHttpProvider("/api", ApiHttpProvider, httpServer, vscode, args["user-data-dir"])
|
const api = httpServer.registerHttpProvider("/api", ApiHttpProvider, httpServer, vscode, args["user-data-dir"])
|
||||||
const update = httpServer.registerHttpProvider("/update", UpdateHttpProvider, !args["disable-updates"])
|
const update = httpServer.registerHttpProvider("/update", UpdateHttpProvider, !args["disable-updates"])
|
||||||
httpServer.registerHttpProvider("/app", AppHttpProvider, api)
|
|
||||||
httpServer.registerHttpProvider("/login", LoginHttpProvider)
|
httpServer.registerHttpProvider("/login", LoginHttpProvider)
|
||||||
httpServer.registerHttpProvider("/static", StaticHttpProvider)
|
httpServer.registerHttpProvider("/static", StaticHttpProvider)
|
||||||
httpServer.registerHttpProvider("/dashboard", DashboardHttpProvider, api, update)
|
httpServer.registerHttpProvider("/dashboard", DashboardHttpProvider, api, update)
|
||||||
|
@ -514,8 +514,7 @@ export class HttpServer {
|
|||||||
private onRequest = async (request: http.IncomingMessage, response: http.ServerResponse): Promise<void> => {
|
private onRequest = async (request: http.IncomingMessage, response: http.ServerResponse): Promise<void> => {
|
||||||
this.heart.beat()
|
this.heart.beat()
|
||||||
const route = this.parseUrl(request)
|
const route = this.parseUrl(request)
|
||||||
try {
|
const write = (payload: HttpResponse): void => {
|
||||||
const payload = this.maybeRedirect(request, route) || (await route.provider.handleRequest(route, request))
|
|
||||||
response.writeHead(payload.redirect ? HttpCode.Redirect : payload.code || HttpCode.Ok, {
|
response.writeHead(payload.redirect ? HttpCode.Redirect : payload.code || HttpCode.Ok, {
|
||||||
"Content-Type": payload.mime || getMediaMime(payload.filePath),
|
"Content-Type": payload.mime || getMediaMime(payload.filePath),
|
||||||
...(payload.redirect ? { Location: this.constructRedirect(request, route, payload as RedirectResponse) } : {}),
|
...(payload.redirect ? { Location: this.constructRedirect(request, route, payload as RedirectResponse) } : {}),
|
||||||
@ -547,6 +546,13 @@ export class HttpServer {
|
|||||||
} else {
|
} else {
|
||||||
response.end()
|
response.end()
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const payload = this.maybeRedirect(request, route) || (await route.provider.handleRequest(route, request))
|
||||||
|
if (!payload) {
|
||||||
|
throw new HttpError("Not found", HttpCode.NotFound)
|
||||||
|
}
|
||||||
|
write(payload)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
let e = error
|
let e = error
|
||||||
if (error.code === "ENOENT" || error.code === "EISDIR") {
|
if (error.code === "ENOENT" || error.code === "EISDIR") {
|
||||||
@ -555,9 +561,11 @@ export class HttpServer {
|
|||||||
logger.debug("Request error", field("url", request.url))
|
logger.debug("Request error", field("url", request.url))
|
||||||
logger.debug(error.stack)
|
logger.debug(error.stack)
|
||||||
const code = typeof e.code === "number" ? e.code : HttpCode.ServerError
|
const code = typeof e.code === "number" ? e.code : HttpCode.ServerError
|
||||||
const content = (await route.provider.getErrorRoot(route, code, code, e.message)).content
|
const payload = await route.provider.getErrorRoot(route, code, code, e.message)
|
||||||
response.writeHead(code)
|
write({
|
||||||
response.end(content)
|
code,
|
||||||
|
...payload,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -595,7 +603,7 @@ export class HttpServer {
|
|||||||
(this.options.cert && !secure ? `${this.protocol}://${request.headers.host}/` : "") +
|
(this.options.cert && !secure ? `${this.protocol}://${request.headers.host}/` : "") +
|
||||||
normalize(`${route.provider.base(route)}/${payload.redirect}`, true) +
|
normalize(`${route.provider.base(route)}/${payload.redirect}`, true) +
|
||||||
(Object.keys(query).length > 0 ? `?${querystring.stringify(query)}` : "")
|
(Object.keys(query).length > 0 ? `?${querystring.stringify(query)}` : "")
|
||||||
logger.debug("Redirecting", field("secure", !!secure), field("from", request.url), field("to", redirect))
|
logger.debug("redirecting", field("secure", !!secure), field("from", request.url), field("to", redirect))
|
||||||
return redirect
|
return redirect
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user