Skip to content

Commit c4c480a

Browse files
authored
Implement last opened functionality (#4633)
* Implement last opened functionality Fixes #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
1 parent b990dab commit c4c480a

31 files changed

+405
-240
lines changed

CHANGELOG.md

+2
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,8 @@ implementation (#4414).
3838
vscode-remote-resource endpoint still can.
3939
- OpenVSX has been made the default marketplace. However this means web
4040
extensions like Vim may be broken.
41+
- The last opened folder/workspace is no longer stored separately in the
42+
settings file (we rely on the already-existing query object instead).
4143

4244
### Deprecated
4345

ci/dev/watch.ts

+1-17
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import { spawn, fork, ChildProcess } from "child_process"
2-
import { promises as fs } from "fs"
32
import * as path from "path"
4-
import { CompilationStats, onLine, OnLineCallback } from "../../src/node/util"
3+
import { onLine, OnLineCallback } from "../../src/node/util"
54

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

@@ -88,7 +86,6 @@ class Watcher {
8886

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

119116
//#region Utilities
120117

121-
/**
122-
* Emits a file containing compilation data.
123-
* This is especially useful when Express needs to determine if VS Code is still compiling.
124-
*/
125-
private emitCompilationStats(): Promise<void> {
126-
const stats: CompilationStats = {
127-
lastCompiledAt: new Date(),
128-
}
129-
130-
console.log("Writing watcher stats...")
131-
return fs.writeFile(this.paths.compilationStatsFile, JSON.stringify(stats, null, 2))
132-
}
133-
134118
private dispose(code: number | null): void {
135119
for (const [processName, devProcess] of Object.entries(this.compilers)) {
136120
console.log(`[${processName}]`, "Killing...\n")

package.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,7 @@
158158
],
159159
"moduleNameMapper": {
160160
"^.+\\.(css|less)$": "<rootDir>/test/utils/cssStub.ts"
161-
}
161+
},
162+
"globalSetup": "<rootDir>/test/utils/globalUnitSetup.ts"
162163
}
163164
}

src/node/http.ts

+13-2
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ import { normalize } from "../common/util"
1010
import { AuthType, DefaultedArgs } from "./cli"
1111
import { version as codeServerVersion } from "./constants"
1212
import { Heart } from "./heart"
13+
import { CoderSettings, SettingsProvider } from "./settings"
14+
import { UpdateProvider } from "./update"
1315
import { getPasswordMethod, IsCookieValidArgs, isCookieValid, sanitizeString, escapeHtml, escapeJSON } from "./util"
1416

1517
/**
@@ -29,6 +31,8 @@ declare global {
2931
export interface Request {
3032
args: DefaultedArgs
3133
heart: Heart
34+
settings: SettingsProvider<CoderSettings>
35+
updater: UpdateProvider
3236
}
3337
}
3438
}
@@ -135,8 +139,8 @@ export const relativeRoot = (originalUrl: string): string => {
135139
}
136140

137141
/**
138-
* Redirect relatively to `/${to}`. Query variables on the current URI will be preserved.
139-
* `to` should be a simple path without any query parameters
142+
* Redirect relatively to `/${to}`. Query variables on the current URI will be
143+
* preserved. `to` should be a simple path without any query parameters
140144
* `override` will merge with the existing query (use `undefined` to unset).
141145
*/
142146
export const redirect = (
@@ -284,3 +288,10 @@ export const getCookieOptions = (req: express.Request): express.CookieOptions =>
284288
sameSite: "lax",
285289
}
286290
}
291+
292+
/**
293+
* Return the full path to the current page, preserving any trailing slash.
294+
*/
295+
export const self = (req: express.Request): string => {
296+
return normalize(`${req.baseUrl}${req.originalUrl.endsWith("/") ? "/" : ""}`, true)
297+
}

src/node/routes/domainProxy.ts

+2-3
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import { Request, Router } from "express"
22
import { HttpCode, HttpError } from "../../common/http"
3-
import { normalize } from "../../common/util"
4-
import { authenticated, ensureAuthenticated, redirect } from "../http"
3+
import { authenticated, ensureAuthenticated, redirect, self } from "../http"
54
import { proxy } from "../proxy"
65
import { Router as WsRouter } from "../wsRouter"
76

@@ -56,7 +55,7 @@ router.all("*", async (req, res, next) => {
5655
return next()
5756
}
5857
// Redirect all other pages to the login.
59-
const to = normalize(`${req.baseUrl}${req.path}`)
58+
const to = self(req)
6059
return redirect(req, res, "login", {
6160
to: to !== "/" ? to : undefined,
6261
})

src/node/routes/index.ts

+7
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ import { commit, rootPath } from "../constants"
1414
import { Heart } from "../heart"
1515
import { ensureAuthenticated, redirect } from "../http"
1616
import { PluginAPI } from "../plugin"
17+
import { CoderSettings, SettingsProvider } from "../settings"
18+
import { UpdateProvider } from "../update"
1719
import { getMediaMime, paths } from "../util"
1820
import * as apps from "./apps"
1921
import * as domainProxy from "./domainProxy"
@@ -47,6 +49,9 @@ export const register = async (app: App, args: DefaultedArgs): Promise<Disposabl
4749
app.router.use(cookieParser())
4850
app.wsRouter.use(cookieParser())
4951

52+
const settings = new SettingsProvider<CoderSettings>(path.join(args["user-data-dir"], "coder.json"))
53+
const updater = new UpdateProvider("https://api.github.com/repos/coder/code-server/releases/latest", settings)
54+
5055
const common: express.RequestHandler = (req, _, next) => {
5156
// /healthz|/healthz/ needs to be excluded otherwise health checks will make
5257
// it look like code-server is always in use.
@@ -57,6 +62,8 @@ export const register = async (app: App, args: DefaultedArgs): Promise<Disposabl
5762
// Add common variables routes can use.
5863
req.args = args
5964
req.heart = heart
65+
req.settings = settings
66+
req.updater = updater
6067

6168
next()
6269
}

src/node/routes/pathProxy.ts

+2-3
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,7 @@ import * as path from "path"
33
import * as qs from "qs"
44
import * as pluginapi from "../../../typings/pluginapi"
55
import { HttpCode, HttpError } from "../../common/http"
6-
import { normalize } from "../../common/util"
7-
import { authenticated, ensureAuthenticated, redirect } from "../http"
6+
import { authenticated, ensureAuthenticated, redirect, self } from "../http"
87
import { proxy as _proxy } from "../proxy"
98

109
const getProxyTarget = (req: Request, passthroughPath?: boolean): string => {
@@ -25,7 +24,7 @@ export function proxy(
2524
if (!authenticated(req)) {
2625
// If visiting the root (/:port only) redirect to the login page.
2726
if (!req.params[0] || req.params[0] === "/") {
28-
const to = normalize(`${req.baseUrl}${req.path}`)
27+
const to = self(req)
2928
return redirect(req, res, "login", {
3029
to: to !== "/" ? to : undefined,
3130
})

src/node/routes/update.ts

+2-5
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,15 @@
11
import { Router } from "express"
22
import { version } from "../constants"
33
import { ensureAuthenticated } from "../http"
4-
import { UpdateProvider } from "../update"
54

65
export const router = Router()
76

8-
const provider = new UpdateProvider()
9-
107
router.get("/check", ensureAuthenticated, async (req, res) => {
11-
const update = await provider.getUpdate(req.query.force === "true")
8+
const update = await req.updater.getUpdate(req.query.force === "true")
129
res.json({
1310
checked: update.checked,
1411
latest: update.version,
1512
current: version,
16-
isLatest: provider.isLatestVersion(update),
13+
isLatest: req.updater.isLatestVersion(update),
1714
})
1815
})

src/node/routes/vscode.ts

+38-17
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,10 @@ import { logger } from "@coder/logger"
22
import * as express from "express"
33
import { WebsocketRequest } from "../../../typings/pluginapi"
44
import { logError } from "../../common/util"
5-
import { isDevMode } from "../constants"
65
import { toVsCodeArgs } from "../cli"
7-
import { ensureAuthenticated, authenticated, redirect } from "../http"
8-
import { loadAMDModule, readCompilationStats } from "../util"
6+
import { isDevMode } from "../constants"
7+
import { authenticated, ensureAuthenticated, redirect, self } from "../http"
8+
import { loadAMDModule } from "../util"
99
import { Router as WsRouter } from "../wsRouter"
1010
import { errorHandler } from "./errors"
1111

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

2727
if (!isAuthenticated) {
28+
const to = self(req)
2829
return redirect(req, res, "login", {
29-
// req.baseUrl can be blank if already at the root.
30-
to: req.baseUrl && req.baseUrl !== "/" ? req.baseUrl : undefined,
30+
to: to !== "/" ? to : undefined,
3131
})
3232
}
3333

34+
const { query } = await req.settings.read()
35+
if (query) {
36+
// Ew means the workspace was closed so clear the last folder/workspace.
37+
if (req.query.ew) {
38+
delete query.folder
39+
delete query.workspace
40+
}
41+
42+
// Redirect to the last folder/workspace if nothing else is opened.
43+
if (
44+
!req.query.folder &&
45+
!req.query.workspace &&
46+
(query.folder || query.workspace) &&
47+
!req.args["ignore-last-opened"] // This flag disables this behavior.
48+
) {
49+
const to = self(req)
50+
return redirect(req, res, to, {
51+
folder: query.folder,
52+
workspace: query.workspace,
53+
})
54+
}
55+
}
56+
57+
// Store the query parameters so we can use them on the next load. This
58+
// also allows users to create functionality around query parameters.
59+
await req.settings.write({ query: req.query })
60+
3461
next()
3562
}
3663

@@ -66,15 +93,6 @@ export class CodeServerRouteWrapper {
6693
return next()
6794
}
6895

69-
if (isDevMode) {
70-
// Is the development mode file watcher still busy?
71-
const compileStats = await readCompilationStats()
72-
73-
if (!compileStats || !compileStats.lastCompiledAt) {
74-
return next(new Error("VS Code may still be compiling..."))
75-
}
76-
}
77-
7896
// Create the server...
7997

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

90108
try {
91109
this._codeServerMain = await createVSServer(null, await toVsCodeArgs(args))
92-
} catch (createServerError) {
93-
logError(logger, "CodeServerRouteWrapper", createServerError)
94-
return next(createServerError)
110+
} catch (error) {
111+
logError(logger, "CodeServerRouteWrapper", error)
112+
if (isDevMode) {
113+
return next(new Error((error instanceof Error ? error.message : error) + " (VS Code may still be compiling)"))
114+
}
115+
return next(error)
95116
}
96117

97118
return next()

src/node/settings.ts

+1-12
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
import { logger } from "@coder/logger"
22
import { Query } from "express-serve-static-core"
33
import { promises as fs } from "fs"
4-
import * as path from "path"
5-
import { paths } from "./util"
64

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

@@ -54,14 +52,5 @@ export interface UpdateSettings {
5452
* Global code-server settings.
5553
*/
5654
export interface CoderSettings extends UpdateSettings {
57-
lastVisited: {
58-
url: string
59-
workspace: boolean
60-
}
61-
query: Query
55+
query?: Query
6256
}
63-
64-
/**
65-
* Global code-server settings file.
66-
*/
67-
export const settings = new SettingsProvider<CoderSettings>(path.join(paths.data, "coder.json"))

src/node/update.ts

+4-5
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import * as https from "https"
44
import * as semver from "semver"
55
import * as url from "url"
66
import { version } from "./constants"
7-
import { settings as globalSettings, SettingsProvider, UpdateSettings } from "./settings"
7+
import { SettingsProvider, UpdateSettings } from "./settings"
88

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

3837
/**

src/node/util.ts

+2-34
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,14 @@ import * as argon2 from "argon2"
33
import * as cp from "child_process"
44
import * as crypto from "crypto"
55
import envPaths from "env-paths"
6-
import { promises as fs, Stats } from "fs"
6+
import { promises as fs } from "fs"
77
import * as net from "net"
88
import * as os from "os"
99
import * as path from "path"
1010
import safeCompare from "safe-compare"
1111
import * as util from "util"
1212
import xdgBasedir from "xdg-basedir"
13-
import { logError } from "../common/util"
14-
import { isDevMode, rootPath, vsRootPath } from "./constants"
13+
import { vsRootPath } from "./constants"
1514

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

524523
return module[exportName] as T
525524
}
526-
527-
export interface CompilationStats {
528-
lastCompiledAt: Date
529-
}
530-
531-
export const readCompilationStats = async (): Promise<null | CompilationStats> => {
532-
if (!isDevMode) {
533-
throw new Error("Compilation stats are only present in development")
534-
}
535-
536-
const filePath = path.join(rootPath, "out/watcher.json")
537-
let stat: Stats
538-
try {
539-
stat = await fs.stat(filePath)
540-
} catch (error) {
541-
return null
542-
}
543-
544-
if (!stat.isFile()) {
545-
return null
546-
}
547-
548-
try {
549-
const file = await fs.readFile(filePath)
550-
return JSON.parse(file.toString("utf-8"))
551-
} catch (error) {
552-
logError(logger, "VS Code", error)
553-
}
554-
555-
return null
556-
}

0 commit comments

Comments
 (0)