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