Skip to content
This repository was archived by the owner on Dec 13, 2024. It is now read-only.

Commit e75750c

Browse files
committed
feat: ✨ Add self updating for standalone executable
Copied from eea905e3fe9a7b1507af0fac27939e73417db76a of meiszwflz/LemLink
1 parent 5c11fb6 commit e75750c

File tree

4 files changed

+140
-7
lines changed

4 files changed

+140
-7
lines changed

config.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"standalone": true
3+
}

src/main.ts

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,20 @@
1-
var argv = require('yargs/yargs')(process.argv.slice(2)).argv;
1+
import yargs from "yargs";
2+
import { standalone } from "../config.json";
3+
import { SelfUpdateUtility } from "./update";
24

3-
if (argv.ships > 3 && argv.distance < 53.5) {
4-
console.log('Plunder more riffiwobbles!');
5-
} else {
6-
console.log('Retreat from the xupptumblers!');
7-
}
5+
if (standalone) SelfUpdateUtility.init();
6+
7+
yargs(process.argv.slice(2)).command(
8+
"update",
9+
"update",
10+
{
11+
url: {
12+
alias: "u",
13+
describe: "the URL to download the new executable from",
14+
},
15+
},
16+
async (argv) => {
17+
if (typeof argv.url === "string")
18+
await SelfUpdateUtility.get().updateCLI(argv.url);
19+
},
20+
);

src/update.ts

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
import { renameSync, unlinkSync, createWriteStream, existsSync } from "fs";
2+
import child_process = require("child_process");
3+
import { get as httpGet, type IncomingMessage } from "http";
4+
import { standalone } from "../config.json";
5+
6+
/**
7+
* self updates the cli executable
8+
* @warning DO NOT USE THIS IF NOT PART OF STANDALONE EXECUTABLE
9+
*/
10+
export class SelfUpdateUtility {
11+
/**
12+
* So how the heck does this CLI update itself?
13+
*
14+
* 1. Attempt to download new executable from downloadUrl to newPath
15+
* 2. Rename current executable to oldPath
16+
* 3. Rename newly downloaded executable to filePath
17+
* 4. On startup of the cli, we attempt to remove oldPath
18+
*/
19+
20+
protected readonly filePath: string; // normal executable path
21+
protected readonly newPath: string; // the new executable that will be downloaded
22+
protected readonly oldPath: string;
23+
24+
private static singleton?: SelfUpdateUtility;
25+
/**
26+
* self updates the cli executable
27+
* @warning DO NOT USE THIS IF NOT PART OF STANDALONE EXECUTABLE
28+
*/
29+
private constructor() {
30+
if (!standalone)
31+
throw new Error(
32+
"LemLink is not part of a standalone executable, and thus cannot self update",
33+
);
34+
const nodeExecutablePath = process.env._;
35+
if (nodeExecutablePath === undefined) {
36+
throw new Error("Could not find node executable path");
37+
}
38+
39+
this.filePath = nodeExecutablePath;
40+
this.newPath = this.filePath + "-new";
41+
this.oldPath = this.filePath + "-old";
42+
43+
this.attemptToDeleteOldPathFile();
44+
}
45+
46+
/**
47+
* Should be run at the start of CLI program
48+
*/
49+
public static init(): void {
50+
if (this.singleton !== undefined) {
51+
console.error(
52+
"Self Update Utility has already been initialized, canceling init..",
53+
);
54+
return;
55+
}
56+
this.singleton = new SelfUpdateUtility();
57+
}
58+
59+
public static get(): SelfUpdateUtility {
60+
if (this.singleton === undefined)
61+
throw new Error(
62+
"Self Update Utility was not initialized at the start of CLI program!!",
63+
);
64+
return this.singleton;
65+
}
66+
67+
/**
68+
* @returns true if oldPath exists, false if otherwise
69+
*/
70+
private attemptToDeleteOldPathFile(): boolean {
71+
if (existsSync(this.oldPath)) {
72+
unlinkSync(this.oldPath);
73+
return true;
74+
}
75+
return false;
76+
}
77+
78+
/**
79+
* Replaces the current CLI executable with the one found at {@link downloadUrl}
80+
* @param downloadUrl url from which a new executable should be downloaded
81+
*/
82+
public async updateCLI(downloadUrl: string): Promise<void> {
83+
/**
84+
* See top of this file for info on how this works
85+
*/
86+
console.log("Downloading new file...");
87+
const fileStream = createWriteStream(this.newPath);
88+
89+
const response = await new Promise<IncomingMessage>((resolve) =>
90+
httpGet(downloadUrl, resolve),
91+
);
92+
93+
response.pipe(fileStream);
94+
await new Promise((resolve) => fileStream.on("finish", resolve));
95+
fileStream.close();
96+
97+
console.log("File downloaded successfully.");
98+
99+
console.log("Replacing current file...");
100+
renameSync(this.filePath, this.oldPath);
101+
renameSync(this.newPath, this.filePath);
102+
console.log("Replaced current file successfully!");
103+
104+
console.log("Restarting...");
105+
child_process.spawn(
106+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
107+
process.argv.shift()!,
108+
process.argv,
109+
{
110+
cwd: process.cwd(),
111+
detached: true,
112+
stdio: "inherit",
113+
shell: true,
114+
},
115+
);
116+
}
117+
}

tsconfig.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@
4141
// "resolvePackageJsonExports": true, /* Use the package.json 'exports' field when resolving package imports. */
4242
// "resolvePackageJsonImports": true, /* Use the package.json 'imports' field when resolving imports. */
4343
// "customConditions": [], /* Conditions to set in addition to the resolver-specific defaults when resolving imports. */
44-
// "resolveJsonModule": true, /* Enable importing .json files. */
44+
"resolveJsonModule": true, /* Enable importing .json files. */
4545
// "allowArbitraryExtensions": true, /* Enable importing files with any extension, provided a declaration file is present. */
4646
// "noResolve": true, /* Disallow 'import's, 'require's or '<reference>'s from expanding the number of files TypeScript should add to a project. */
4747

0 commit comments

Comments
 (0)