Skip to content

Implement last opened functionality #4633

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 14 commits into from
Dec 17, 2021
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ implementation (#4414).
vscode-remote-resource endpoint still can.
- OpenVSX has been made the default marketplace. However this means web
extensions like Vim may be broken.
- The last opened folder/workspace is no longer stored separately in the
settings file (we rely on the already-existing query object instead).

### Deprecated

Expand Down
18 changes: 1 addition & 17 deletions ci/dev/watch.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { spawn, fork, ChildProcess } from "child_process"
import { promises as fs } from "fs"
import * as path from "path"
import { CompilationStats, onLine, OnLineCallback } from "../../src/node/util"
import { onLine, OnLineCallback } from "../../src/node/util"

interface DevelopmentCompilers {
[key: string]: ChildProcess | undefined
Expand All @@ -16,7 +15,6 @@ class Watcher {
private readonly paths = {
/** Path to uncompiled VS Code source. */
vscodeDir: path.join(this.rootPath, "vendor", "modules", "code-oss-dev"),
compilationStatsFile: path.join(this.rootPath, "out", "watcher.json"),
pluginDir: process.env.PLUGIN_DIR,
}

Expand Down Expand Up @@ -88,7 +86,6 @@ class Watcher {

if (strippedLine.includes("Finished compilation with")) {
console.log("[VS Code] ✨ Finished compiling! ✨", "(Refresh your web browser ♻️)")
this.emitCompilationStats()
this.reloadWebServer()
}
}
Expand Down Expand Up @@ -118,19 +115,6 @@ class Watcher {

//#region Utilities

/**
* Emits a file containing compilation data.
* This is especially useful when Express needs to determine if VS Code is still compiling.
*/
private emitCompilationStats(): Promise<void> {
const stats: CompilationStats = {
lastCompiledAt: new Date(),
}

console.log("Writing watcher stats...")
return fs.writeFile(this.paths.compilationStatsFile, JSON.stringify(stats, null, 2))
}

private dispose(code: number | null): void {
for (const [processName, devProcess] of Object.entries(this.compilers)) {
console.log(`[${processName}]`, "Killing...\n")
Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,7 @@
],
"moduleNameMapper": {
"^.+\\.(css|less)$": "<rootDir>/test/utils/cssStub.ts"
}
},
"globalSetup": "<rootDir>/test/utils/globalUnitSetup.ts"
}
}
15 changes: 13 additions & 2 deletions src/node/http.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ import { normalize } from "../common/util"
import { AuthType, DefaultedArgs } from "./cli"
import { version as codeServerVersion } from "./constants"
import { Heart } from "./heart"
import { CoderSettings, SettingsProvider } from "./settings"
import { UpdateProvider } from "./update"
import { getPasswordMethod, IsCookieValidArgs, isCookieValid, sanitizeString, escapeHtml, escapeJSON } from "./util"

/**
Expand All @@ -29,6 +31,8 @@ declare global {
export interface Request {
args: DefaultedArgs
heart: Heart
settings: SettingsProvider<CoderSettings>
updater: UpdateProvider
}
}
}
Expand Down Expand Up @@ -135,8 +139,8 @@ export const relativeRoot = (originalUrl: string): string => {
}

/**
* Redirect relatively to `/${to}`. Query variables on the current URI will be preserved.
* `to` should be a simple path without any query parameters
* Redirect relatively to `/${to}`. Query variables on the current URI will be
* preserved. `to` should be a simple path without any query parameters
* `override` will merge with the existing query (use `undefined` to unset).
*/
export const redirect = (
Expand Down Expand Up @@ -284,3 +288,10 @@ export const getCookieOptions = (req: express.Request): express.CookieOptions =>
sameSite: "lax",
}
}

/**
* Return the full path to the current page, preserving any trailing slash.
*/
export const self = (req: express.Request): string => {
return normalize(`${req.baseUrl}${req.originalUrl.endsWith("/") ? "/" : ""}`, true)
}
5 changes: 2 additions & 3 deletions src/node/routes/domainProxy.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { Request, Router } from "express"
import { HttpCode, HttpError } from "../../common/http"
import { normalize } from "../../common/util"
import { authenticated, ensureAuthenticated, redirect } from "../http"
import { authenticated, ensureAuthenticated, redirect, self } from "../http"
import { proxy } from "../proxy"
import { Router as WsRouter } from "../wsRouter"

Expand Down Expand Up @@ -56,7 +55,7 @@ router.all("*", async (req, res, next) => {
return next()
}
// Redirect all other pages to the login.
const to = normalize(`${req.baseUrl}${req.path}`)
const to = self(req)
return redirect(req, res, "login", {
to: to !== "/" ? to : undefined,
})
Expand Down
7 changes: 7 additions & 0 deletions src/node/routes/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ import { commit, rootPath } from "../constants"
import { Heart } from "../heart"
import { ensureAuthenticated, redirect } from "../http"
import { PluginAPI } from "../plugin"
import { CoderSettings, SettingsProvider } from "../settings"
import { UpdateProvider } from "../update"
import { getMediaMime, paths } from "../util"
import * as apps from "./apps"
import * as domainProxy from "./domainProxy"
Expand Down Expand Up @@ -47,6 +49,9 @@ export const register = async (app: App, args: DefaultedArgs): Promise<Disposabl
app.router.use(cookieParser())
app.wsRouter.use(cookieParser())

const settings = new SettingsProvider<CoderSettings>(path.join(args["user-data-dir"], "coder.json"))
const updater = new UpdateProvider("https://api.github.com/repos/cdr/code-server/releases/latest", settings)

const common: express.RequestHandler = (req, _, next) => {
// /healthz|/healthz/ needs to be excluded otherwise health checks will make
// it look like code-server is always in use.
Expand All @@ -57,6 +62,8 @@ export const register = async (app: App, args: DefaultedArgs): Promise<Disposabl
// Add common variables routes can use.
req.args = args
req.heart = heart
req.settings = settings
req.updater = updater

next()
}
Expand Down
5 changes: 2 additions & 3 deletions src/node/routes/pathProxy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,7 @@ import * as path from "path"
import * as qs from "qs"
import * as pluginapi from "../../../typings/pluginapi"
import { HttpCode, HttpError } from "../../common/http"
import { normalize } from "../../common/util"
import { authenticated, ensureAuthenticated, redirect } from "../http"
import { authenticated, ensureAuthenticated, redirect, self } from "../http"
import { proxy as _proxy } from "../proxy"

const getProxyTarget = (req: Request, passthroughPath?: boolean): string => {
Expand All @@ -25,7 +24,7 @@ export function proxy(
if (!authenticated(req)) {
// If visiting the root (/:port only) redirect to the login page.
if (!req.params[0] || req.params[0] === "/") {
const to = normalize(`${req.baseUrl}${req.path}`)
const to = self(req)
return redirect(req, res, "login", {
to: to !== "/" ? to : undefined,
})
Expand Down
7 changes: 2 additions & 5 deletions src/node/routes/update.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,15 @@
import { Router } from "express"
import { version } from "../constants"
import { ensureAuthenticated } from "../http"
import { UpdateProvider } from "../update"

export const router = Router()

const provider = new UpdateProvider()

router.get("/check", ensureAuthenticated, async (req, res) => {
const update = await provider.getUpdate(req.query.force === "true")
const update = await req.updater.getUpdate(req.query.force === "true")
res.json({
checked: update.checked,
latest: update.version,
current: version,
isLatest: provider.isLatestVersion(update),
isLatest: req.updater.isLatestVersion(update),
})
})
55 changes: 38 additions & 17 deletions src/node/routes/vscode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@ import { logger } from "@coder/logger"
import * as express from "express"
import { WebsocketRequest } from "../../../typings/pluginapi"
import { logError } from "../../common/util"
import { isDevMode } from "../constants"
import { toVsCodeArgs } from "../cli"
import { ensureAuthenticated, authenticated, redirect } from "../http"
import { loadAMDModule, readCompilationStats } from "../util"
import { isDevMode } from "../constants"
import { authenticated, ensureAuthenticated, redirect, self } from "../http"
import { loadAMDModule } from "../util"
import { Router as WsRouter } from "../wsRouter"
import { errorHandler } from "./errors"

Expand All @@ -25,12 +25,39 @@ export class CodeServerRouteWrapper {
const isAuthenticated = await authenticated(req)

if (!isAuthenticated) {
const to = self(req)
return redirect(req, res, "login", {
// req.baseUrl can be blank if already at the root.
to: req.baseUrl && req.baseUrl !== "/" ? req.baseUrl : undefined,
to: to !== "/" ? to : undefined,
})
}

const { query } = await req.settings.read()
if (query) {
// Ew means the workspace was closed so clear the last folder/workspace.
if (req.query.ew) {
delete query.folder
delete query.workspace
}

// Redirect to the last folder/workspace if nothing else is opened.
if (
!req.query.folder &&
!req.query.workspace &&
(query.folder || query.workspace) &&
!req.args["ignore-last-opened"] // This flag disables this behavior.
) {
const to = self(req)
return redirect(req, res, to, {
folder: query.folder,
workspace: query.workspace,
})
}
}

// Store the query parameters so we can use them on the next load. This
// also allows users to create functionality around query parameters.
await req.settings.write({ query: req.query })

next()
}

Expand Down Expand Up @@ -66,15 +93,6 @@ export class CodeServerRouteWrapper {
return next()
}

if (isDevMode) {
// Is the development mode file watcher still busy?
const compileStats = await readCompilationStats()

if (!compileStats || !compileStats.lastCompiledAt) {
return next(new Error("VS Code may still be compiling..."))
}
}

// Create the server...

const { args } = req
Expand All @@ -89,9 +107,12 @@ export class CodeServerRouteWrapper {

try {
this._codeServerMain = await createVSServer(null, await toVsCodeArgs(args))
} catch (createServerError) {
logError(logger, "CodeServerRouteWrapper", createServerError)
return next(createServerError)
} catch (error) {
logError(logger, "CodeServerRouteWrapper", error)
if (isDevMode) {
return next(new Error((error instanceof Error ? error.message : error) + " (VS Code may still be compiling)"))
}
return next(error)
}

return next()
Expand Down
13 changes: 1 addition & 12 deletions src/node/settings.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
import { logger } from "@coder/logger"
import { Query } from "express-serve-static-core"
import { promises as fs } from "fs"
import * as path from "path"
import { paths } from "./util"

export type Settings = { [key: string]: Settings | string | boolean | number }

Expand Down Expand Up @@ -54,14 +52,5 @@ export interface UpdateSettings {
* Global code-server settings.
*/
export interface CoderSettings extends UpdateSettings {
lastVisited: {
url: string
workspace: boolean
}
query: Query
query?: Query
}

/**
* Global code-server settings file.
*/
export const settings = new SettingsProvider<CoderSettings>(path.join(paths.data, "coder.json"))
9 changes: 4 additions & 5 deletions src/node/update.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import * as https from "https"
import * as semver from "semver"
import * as url from "url"
import { version } from "./constants"
import { settings as globalSettings, SettingsProvider, UpdateSettings } from "./settings"
import { SettingsProvider, UpdateSettings } from "./settings"

export interface Update {
checked: number
Expand All @@ -27,12 +27,11 @@ export class UpdateProvider {
* The URL for getting the latest version of code-server. Should return JSON
* that fulfills `LatestResponse`.
*/
private readonly latestUrl = "https://api.github.com/repos/cdr/code-server/releases/latest",
private readonly latestUrl: string,
/**
* Update information will be stored here. If not provided, the global
* settings will be used.
* Update information will be stored here.
*/
private readonly settings: SettingsProvider<UpdateSettings> = globalSettings,
private readonly settings: SettingsProvider<UpdateSettings>,
) {}

/**
Expand Down
36 changes: 2 additions & 34 deletions src/node/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,14 @@ import * as argon2 from "argon2"
import * as cp from "child_process"
import * as crypto from "crypto"
import envPaths from "env-paths"
import { promises as fs, Stats } from "fs"
import { promises as fs } from "fs"
import * as net from "net"
import * as os from "os"
import * as path from "path"
import safeCompare from "safe-compare"
import * as util from "util"
import xdgBasedir from "xdg-basedir"
import { logError } from "../common/util"
import { isDevMode, rootPath, vsRootPath } from "./constants"
import { vsRootPath } from "./constants"

export interface Paths {
data: string
Expand Down Expand Up @@ -523,34 +522,3 @@ export const loadAMDModule = async <T>(amdPath: string, exportName: string): Pro

return module[exportName] as T
}

export interface CompilationStats {
lastCompiledAt: Date
}

export const readCompilationStats = async (): Promise<null | CompilationStats> => {
if (!isDevMode) {
throw new Error("Compilation stats are only present in development")
}

const filePath = path.join(rootPath, "out/watcher.json")
let stat: Stats
try {
stat = await fs.stat(filePath)
} catch (error) {
return null
}

if (!stat.isFile()) {
return null
}

try {
const file = await fs.readFile(filePath)
return JSON.parse(file.toString("utf-8"))
} catch (error) {
logError(logger, "VS Code", error)
}

return null
}
Loading