diff --git a/package-lock.json b/package-lock.json index 873cb45..2fe6ebc 100644 Binary files a/package-lock.json and b/package-lock.json differ diff --git a/package.json b/package.json index 89658aa..d35edf2 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "fwdekker.com", - "version": "0.24.4", + "version": "0.25.0", "description": "The source code of [my personal website](https://fwdekker.com/).", "author": "Felix W. Dekker", "repository": { @@ -16,13 +16,15 @@ "coverage": "nyc npm run test" }, "dependencies": { - "js-cookie": "^2.2.1" + "js-cookie": "^2.2.1", + "semver": "^6.3.0" }, "devDependencies": { "@istanbuljs/nyc-config-typescript": "^0.1.3", "@types/chai": "^4.2.5", "@types/js-cookie": "^2.2.4", "@types/mocha": "^5.2.7", + "@types/semver": "^6.2.0", "chai": "^4.2.0", "grunt": "^1.0.4", "grunt-cli": "^1.3.2", @@ -33,7 +35,7 @@ "mocha": "^6.2.2", "nyc": "^14.1.1", "ts-loader": "^6.2.1", - "ts-node": "^8.5.2", + "ts-node": "^8.5.4", "typescript": "^3.7.2", "webpack": "^4.41.2", "webpack-cli": "^3.3.10" diff --git a/src/main/js/Commands.ts b/src/main/js/Commands.ts index 11a9e42..857fa3c 100644 --- a/src/main/js/Commands.ts +++ b/src/main/js/Commands.ts @@ -4,7 +4,7 @@ import {Directory, File, FileSystem, Path} from "./FileSystem" import {InputArgs} from "./InputArgs"; import {InputParser} from "./InputParser"; import {Persistence} from "./Persistence"; -import {escapeHtml, IllegalArgumentError, IllegalStateError, isStandalone} from "./Shared"; +import {escapeHtml, ExpectedGoodbyeError, IllegalArgumentError, IllegalStateError, isStandalone} from "./Shared"; import {EscapeCharacters} from "./Terminal"; import {UserList} from "./UserList"; import {StreamSet} from "./Stream"; @@ -60,7 +60,7 @@ export class Commands { `concatenate and print files`, `cat [-e | --escape-html] file ...`, `Reads files sequentially, writing them to the standard output. - + If the file contains valid HTML, it will be displayed as such by default. If the --html \\\ option is given, special HTML characters are escaped and the raw text contents can be inspected.\\\ `.trimMultiLines(), @@ -270,7 +270,7 @@ export class Commands { if (input.command === "factory-reset") { Persistence.reset(); location.reload(); - throw new Error("Goodbye"); + throw new ExpectedGoodbyeError("Goodbye"); } if (input.command === "") diff --git a/src/main/js/Main.ts b/src/main/js/Main.ts index a6c0ce1..86c06ca 100644 --- a/src/main/js/Main.ts +++ b/src/main/js/Main.ts @@ -1,6 +1,7 @@ import {Persistence} from "./Persistence"; -import {addOnLoad, q} from "./Shared"; +import {addOnLoad, ExpectedGoodbyeError, q} from "./Shared"; import {Terminal} from "./Terminal"; +import * as semver from "semver"; declare global { @@ -19,13 +20,45 @@ declare global { } +/** + * Compares version numbers to ensure no compatibility errors ensure. + */ +addOnLoad(() => { + const userVersion = Persistence.getVersion(); + const latestVersion = "%%VERSION_NUMBER%%"; + + if (semver.lt(userVersion, latestVersion)) { + Persistence.reset(); + Persistence.setWasUpdated(true); // Message is displayed after reload + location.reload(); + throw new ExpectedGoodbyeError("Goodbye"); + } + + if (Persistence.getWasUpdated()) { + q("#terminalOutput").innerHTML = "" + + "This website has been updated. To prevent unexpected errors, all previous " + + "user changes have been reset.\n\n"; + Persistence.setWasUpdated(false); + } + + Persistence.setVersion(latestVersion); +}); + +/** + * Exist the application if the server is "shut down". + */ addOnLoad(() => { if (Persistence.getPoweroff()) { q("#terminalOutput").innerText = "Could not connect to fwdekker.com. Retrying in 10 seconds."; setTimeout(() => location.reload(), 10000); - return; + throw new ExpectedGoodbyeError("Goodbye"); } +}); +/** + * Initializes the application. + */ +addOnLoad(() => { window.terminal = new Terminal( q("#terminal"), q("#terminalCurrentFocusInput"), diff --git a/src/main/js/Persistence.ts b/src/main/js/Persistence.ts index 217b159..970698c 100644 --- a/src/main/js/Persistence.ts +++ b/src/main/js/Persistence.ts @@ -10,51 +10,19 @@ import {UserList} from "./UserList"; */ export class Persistence { /** - * Deserializes an environment from persistent storage, or returns the default environment if the deserialization - * failed. - * - * @param userList the list of users used to validate the `user` environment variable + * Removes all persistent storage. */ - static getEnvironment(userList: UserList): Environment { - const environmentString = Cookies.get("env") ?? "{}"; - - let environment: Environment; - try { - environment = new Environment(["cwd", "home", "user", "status"], JSON.parse(environmentString)); - } catch (error) { - console.warn("Failed to set environment from cookie."); - environment = new Environment(["cwd", "home", "user", "status"]); - } - - // Check user in environment - if (!environment.has("user")) { - environment.set("user", "felix"); - } else if (environment.get("user") !== "" && !userList.has(environment.get("user"))) { - console.warn(`Invalid user '${environment.get("user")}' in environment.`); - environment.set("user", "felix"); - } - - // Set home directory - environment.set("home", userList.get(environment.get("user"))?.home ?? "/"); - - // Check cwd in environment - if (!environment.has("cwd")) - environment.set("cwd", environment.get("home")); - - // Set status - environment.set("status", "0"); - - return environment; + static reset(): void { + sessionStorage.clear(); + localStorage.clear(); + Cookies.remove("env"); + Cookies.remove("poweroff"); } - /** - * Persists the given environment. - * - * @param environment the environment to persist - */ - static setEnvironment(environment: Environment): void { - Cookies.set("env", environment.variables, {"path": "/"}); - } + + /// + /// Long-term storage + /// /** * Deserializes a file system from persistent storage, or returns the default file system if the deserialization @@ -108,21 +76,89 @@ export class Persistence { } /** - * Returns the persisted "power off" setting. + * Deserializes an environment from persistent storage, or returns the default environment if the deserialization + * failed. + * + * @param userList the list of users used to validate the `user` environment variable + */ + static getEnvironment(userList: UserList): Environment { + const environmentString = Cookies.get("env") ?? "{}"; + + let environment: Environment; + try { + environment = new Environment(["cwd", "home", "user", "status"], JSON.parse(environmentString)); + } catch (error) { + console.warn("Failed to set environment from cookie."); + environment = new Environment(["cwd", "home", "user", "status"]); + } + + // Check user in environment + if (!environment.has("user")) { + environment.set("user", "felix"); + } else if (environment.get("user") !== "" && !userList.has(environment.get("user"))) { + console.warn(`Invalid user '${environment.get("user")}' in environment.`); + environment.set("user", "felix"); + } + + // Set home directory + environment.set("home", userList.get(environment.get("user"))?.home ?? "/"); + + // Check cwd in environment + if (!environment.has("cwd")) + environment.set("cwd", environment.get("home")); + + // Set status + environment.set("status", "0"); + + return environment; + } + + /** + * Persists the given environment. + * + * @param environment the environment to persist + */ + static setEnvironment(environment: Environment): void { + Cookies.set("env", environment.variables, {"path": "/"}); + } + + /** + * Returns the version of the scripts that were used the last time the user visited the website. + */ + static getVersion(): string { + return localStorage.getItem("version") ?? "%%VERSION_NUMBER%%"; + } + + /** + * Sets the version of the scripts that were used the last time the user visited the website. + * + * @param version the version of the scripts that were used the last time the user visited the website + */ + static setVersion(version: string) { + localStorage.setItem("version", version); + } + + + /// + /// Short-term storage + /// + + /** + * Returns `true` if and only if the server is "turned off". */ static getPoweroff(): boolean { try { return JSON.parse(Cookies.get("poweroff") ?? "false"); - } catch(error) { + } catch (error) { console.warn("Failed to deserialize 'poweroff' cookie.", error); return false; } } /** - * Persists the "power off" setting. + * Stores whether the server is "turned off". * - * @param value the value to persist for the "power off" setting + * @param value whether the server is "turned off" */ static setPoweroff(value: boolean): void { Cookies.set("poweroff", "" + value, { @@ -132,11 +168,23 @@ export class Persistence { } /** - * Removes all persistent storage. + * Returns `true` if and only if the terminal was updated in this session. */ - static reset(): void { - localStorage.clear(); - Cookies.remove("env"); - Cookies.remove("poweroff"); + static getWasUpdated(): boolean { + try { + return JSON.parse(sessionStorage.getItem("has-updated") ?? "false"); + } catch (error) { + console.warn("Failed to deserialize 'poweroff' cookie.", error); + return false; + } + } + + /** + * Stores whether the terminal was updated in this session. + * + * @param value whether the terminal was updated in this session + */ + static setWasUpdated(value: boolean): void { + sessionStorage.setItem("has-updated", "" + value); } } diff --git a/src/main/js/Shared.ts b/src/main/js/Shared.ts index 4db079d..f4aa270 100644 --- a/src/main/js/Shared.ts +++ b/src/main/js/Shared.ts @@ -129,6 +129,18 @@ export function q(query: string): HTMLElement { } +/** + * Indicates that the application will exit under normal circumstances. + * + * That is, this is not actually an error. This "error" is thrown when the normal flow of execution should be + * interrupted right away so that the application can exit. + */ +export class ExpectedGoodbyeError extends Error { + constructor(message: string) { + super(message); + } +} + /** * Indicates that an argument is given to a function that should not have been given. * diff --git a/src/main/js/Terminal.ts b/src/main/js/Terminal.ts index 84838a2..32d7111 100644 --- a/src/main/js/Terminal.ts +++ b/src/main/js/Terminal.ts @@ -86,8 +86,8 @@ export class Terminal { scrollStartPosition = newPosition; }, {passive: true}); - this.outputText = this.shell.generateHeader(); - this.prefixText = this.shell.generatePrefix(); + this.outputText += this.shell.generateHeader(); + this.prefixText += this.shell.generatePrefix(); this.input.focus(); }