Implement last opened functionality (#4633)
* Implement last opened functionality Fixes https://github.com/cdr/code-server/issues/4619 * Fix test temp dirs not being cleaned up * Mock logger everywhere This suppresses all the error and debug output we generate which makes it hard to actually find which test has failed. It also gives us a standard way to test logging for the few places we do that. * Use separate data directories for unit test instances Exactly as we do for the e2e tests. * Add integration tests for vscode route * Make settings use --user-data-dir Without this test instances step on each other feet and they also clobber your own non-test settings. * Make redirects consistent They will preserve the trailing slash if there is one. * Remove compilation check If you do a regular non-watch build there are no compilation stats so this bricks VS Code in CI when running the unit tests. I am not sure how best to fix this for the case where you have a build that has not been packaged yet so I just removed it for now and added a message to check if VS Code is compiling when in dev mode. * Update code-server update endpoint name
This commit is contained in:
@ -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"
|
||||
|
||||
/**
|
||||
@ -29,6 +31,8 @@ declare global {
|
||||
export interface Request {
|
||||
args: DefaultedArgs
|
||||
heart: Heart
|
||||
settings: SettingsProvider<CoderSettings>
|
||||
updater: UpdateProvider
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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 = (
|
||||
@ -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)
|
||||
}
|
||||
|
@ -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"
|
||||
|
||||
@ -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,
|
||||
})
|
||||
|
@ -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"
|
||||
@ -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/coder/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.
|
||||
@ -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()
|
||||
}
|
||||
|
@ -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 => {
|
||||
@ -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,
|
||||
})
|
||||
|
@ -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),
|
||||
})
|
||||
})
|
||||
|
@ -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"
|
||||
|
||||
@ -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()
|
||||
}
|
||||
|
||||
@ -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
|
||||
@ -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()
|
||||
|
@ -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 }
|
||||
|
||||
@ -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"))
|
||||
|
@ -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
|
||||
@ -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>,
|
||||
) {}
|
||||
|
||||
/**
|
||||
|
@ -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
|
||||
@ -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
|
||||
}
|
||||
|
Reference in New Issue
Block a user