Reset application on updates

Fixes #16.
This commit is contained in:
Florine W. Dekker 2019-12-01 23:31:03 +01:00
parent 90701ab1eb
commit f8415552f9
Signed by: FWDekker
GPG Key ID: B1B567AF58D6EE0F
7 changed files with 156 additions and 61 deletions

BIN
package-lock.json generated

Binary file not shown.

View File

@ -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"

View File

@ -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 [<b>-e</b> | <b>--escape-html</b>] <u>file</u> <u>...</u>`,
`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 <b>--html</b> \\\
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 === "")

View File

@ -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 = "" +
"<span style=\"color:red\">This website has been updated. To prevent unexpected errors, all previous " +
"user changes have been reset.</span>\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"),

View File

@ -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);
}
}

View File

@ -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.
*

View File

@ -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();
}