-
Notifications
You must be signed in to change notification settings - Fork 0
/
installer.js
351 lines (320 loc) · 14.7 KB
/
installer.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
const Unzipper = require("decompress-zip");
const childProcess = require("child_process");
const path = require("path");
// note: this does not need to be a secret, its just used to check if PM is actually installed on startup
const verificationKey = "WVVkR2IxbFRRakZKUjNoMllqSjBiRnBCUFQwPQ==";
const connectionVerify = "https://penguinmod.com/favicon.ico";
const archivePath = "https://github.com/PenguinMod/penguinmod.github.io/archive/refs/heads/develop.zip";
const homePath = "https://github.com/PenguinMod/PenguinMod-Home/archive/refs/heads/main.zip";
class Installer {
static logs = [];
static _listeners = [];
static fs;
static log(text) {
console.log(text);
Installer.logs.push(String(text));
for (const listener of Installer._listeners) {
listener(String(text));
}
}
static addLogListener(listener) {
Installer._listeners.push(listener);
}
static wait(ms) {
return new Promise((resolve) => {
const time = Date.now();
setTimeout(() => {
resolve(Date.now() - time);
}, ms);
});
}
static checkOnline() {
return new Promise((resolve) => {
fetch(connectionVerify).then(res => {
if (!res.ok) {
resolve(false);
return;
}
resolve(true);
}).catch(() => {
resolve(false);
});
})
}
/**
* fs.writeFile but as a promise
*/
static writeFile(path, data, options) {
return new Promise((resolve, reject) => {
const callback = (err) => {
if (err) return reject(err);
resolve();
};
if (!options) {
return Installer.fs.writeFile(path, data, callback);
}
Installer.fs.writeFile(path, data, options, callback);
});
}
/**
* child_process.exec but as a promise
*/
static exec(command, options) {
return new Promise((resolve, reject) => {
const callback = (error, stdout, stderr) => {
if (error) return reject({ error, stdout, stderr });
resolve({ stdout, stderr });
};
if (options) {
childProcess.exec(command, options, callback);
}
childProcess.exec(command, callback);
});
}
static async getCommandResultAsVersion(command) {
let result = { error: null, stdout: "", stderr: "" };
try {
result = await Installer.exec(command);
} catch {
return;
}
let version = String(result.stdout).trim().replace(/v/gmi, '');
version = version.replace('.', ',');
version = version.replace(/[^0-9,]+/gmi, '');
version = version.replace(',', '.');
version = version.replace(/,/gmi, '');
return Number(version);
}
static async getNodeVersion() {
const result = await Installer.getCommandResultAsVersion('node -v');
return result;
}
static async getNpmVersion() {
const result = await Installer.getCommandResultAsVersion('npm -v');
return result;
}
static async install(filepath) {
// gives time for the app to be closed probably
Installer.log('Prepping installation, please wait...');
await Installer.wait(1000);
// check for node
// if not found, send them to https://nodejs.org/en/download
Installer.log('Checking for Node...');
const notInstalledMessage = 'PenguinMod Launcher requires Node + NPM to work.\nThe installation page for Node will open now.\n\nPlease select "LTS" and choose the option with your operating system.\n\nAfter installation, you will need to restart the app and you may need to restart your computer.';
const nodeVersion = await Installer.getNodeVersion();
if (nodeVersion && (nodeVersion >= 16)) {
console.log(nodeVersion);
Installer.log(`Node is already installed and up-to-date.`);
} else {
Installer.log('Node was either not found or older than v16. Opening installation page.');
alert(notInstalledMessage);
window.open('https://nodejs.org/en/download');
throw new Error('Node not found or older than v16.');
}
// return; // debug
// check for npm
// if not found, send them to https://nodejs.org/en/download
Installer.log('Checking for NPM...');
const npmVersion = await Installer.getNpmVersion();
if (npmVersion && (npmVersion >= 7)) {
console.log(npmVersion);
Installer.log(`NPM is already installed and up-to-date.`);
} else {
Installer.log('NPM was either not found or older than v7. Opening installation page.');
alert(notInstalledMessage);
window.open('https://nodejs.org/en/download');
throw new Error('NPM not found or older than v7.');
}
// return; // debug
const realPath = path.join(__dirname, filepath);
Installer.log(`Installing to ${realPath}`);
// debug
// throw new Error('Debug');
// create directories
Installer.log('Creating directories...');
try {
// this will create all directories here, so we dont need to individually do it
Installer.fs.mkdirSync(path.join(__dirname, "./penguinmod/compilation/extraction"), { recursive: true });
// throw new Error('Debug');
} catch (err) {
Installer.log(`Unexpected error creating directories: ${err}`);
throw new Error(`Unexpected error creating directories: ${err}`);
}
// make sure we are online for this
Installer.log('Checking connection, please wait...');
const connectionTestTime = Date.now();
// this just accesses a PM icon, doesnt query github so we dont get ratelimited
const connectedOnline = await Installer.checkOnline();
if (!connectedOnline) {
// if (true) { // debug
Installer.log('Failed connection test. You may not be connected to the internet.');
Installer.log('Please click "Refresh" to try again when you are ready.');
throw new Error('Unable to view PenguinMod');
}
// we are online, now we can query github
Installer.log(`Passed connection test. (${Date.now() - connectionTestTime}ms)`);
Installer.log('Please wait, grabbing repository contents...');
// ---- INSTALLATION ----
// Install Home Page
Installer.log(`Grabbing repository from "${homePath}"`);
await (async () => {
let response;
try {
// TODO: should we fetch with user agent headers for github to know the app name
response = await fetch(homePath);
// throw new Error('Debug');
} catch (err) {
Installer.log('Failed to get the home ZIP file!');
Installer.log('You may not be connected to the internet.');
Installer.log(`${err}`);
throw new Error(`Unexpected error fetching archive: ${err}`);
}
// we need to convert to array boofer
Installer.log('Processing...');
let arraybuffer;
try {
arraybuffer = await response.arrayBuffer();
// throw new Error('Debug');
} catch (err) {
Installer.log('Unknown error getting ArrayBuffer.');
Installer.log(`${err}`);
throw new Error(`Unknown error getting ArrayBuffer: ${err}`);
}
// save
Installer.log('Downloading archive...');
const downloadedArchivePath = path.join(filepath, "/_repository_h.zip");
try {
const buffer = Buffer.from(arraybuffer);
await Installer.writeFile(downloadedArchivePath, buffer);
// throw new Error('Debug');
} catch (err) {
Installer.log('Failed to download PenguinMod to device!');
Installer.log('You may not have enough storage space. It is recommended that at least 2.5 GB is free for the full installation process.');
Installer.log('If you do not have enough space on your main drive, try moving the Launcher folder to another drive.');
Installer.log(`${err}`);
throw new Error(`Failed to write file: ${err}`);
}
// unzip
Installer.log('Extracting archive...');
// we made this dir earlier to make sure we could but just remake it to clear its contents
const extractedArchivePath = path.resolve(path.join(filepath, "/extraction"));
Installer.fs.rmSync(extractedArchivePath, { recursive: true, force: true });
Installer.fs.mkdirSync(extractedArchivePath, { recursive: true });
// create extractor so we can listen for events
const extractor = new Unzipper(downloadedArchivePath);
extractor.on('error', (err) => {
Installer.log('Failed to extract ZIP archive file.');
Installer.log('You may need to manually reopen the Launcher application to try again.');
Installer.log(`${err}`);
throw new Error(`Failed to extract zip archive: ${err}`);
});
extractor.on('progress', (fileIndex, fileCount) => {
Installer.log('Extracted file ' + (fileIndex + 1) + ' of ' + fileCount);
});
const waitingExtraction = new Promise((resolve) => {
extractor.on('extract', () => {
Installer.log("Finished extracting");
resolve();
});
});
extractor.extract({
path: extractedArchivePath,
restrict: true, // prevents some security problems
filter: function (file) {
return file.type !== "SymbolicLink"; // same here
}
});
await waitingExtraction;
})();
// Install GUI
/*
Installer.log(`Grabbing repository from "${archivePath}"`);
await (async () => {
let response;
try {
// TODO: should we fetch with user agent headers for github to know the app name
response = await fetch(archivePath);
// throw new Error('Debug');
} catch (err) {
Installer.log('Failed to get the GUI repository ZIP file!');
Installer.log('You may not be connected to the internet.');
Installer.log(`${err}`);
throw new Error(`Unexpected error fetching archive: ${err}`);
}
// we need to convert to array boofer
Installer.log('Processing...');
let arraybuffer;
try {
arraybuffer = await response.arrayBuffer();
// throw new Error('Debug');
} catch (err) {
Installer.log('Unknown error getting ArrayBuffer.');
Installer.log(`${err}`);
throw new Error(`Unknown error getting ArrayBuffer: ${err}`);
}
// save
Installer.log('Downloading archive...');
const downloadedArchivePath = path.join(filepath, "/_repository_g.zip");
try {
const buffer = Buffer.from(arraybuffer);
await Installer.writeFile(downloadedArchivePath, buffer);
// throw new Error('Debug');
} catch (err) {
Installer.log('Failed to download PenguinMod to device!');
Installer.log('You may not have enough storage space. It is recommended that at least 2.5 GB is free for the full installation process.');
Installer.log('If you do not have enough space on your main drive, try moving the Launcher folder to another drive.');
Installer.log(`${err}`);
throw new Error(`Failed to write file: ${err}`);
}
// unzip
Installer.log('Extracting archive...');
// we made this dir earlier to make sure we could but just remake it to clear its contents
const extractedArchivePath = path.resolve(path.join(filepath, "/extraction"));
Installer.fs.rmSync(extractedArchivePath, { recursive: true, force: true });
Installer.fs.mkdirSync(extractedArchivePath, { recursive: true });
// create extractor so we can listen for events
const extractor = new Unzipper(downloadedArchivePath);
extractor.on('error', (err) => {
Installer.log('Failed to extract ZIP archive file.');
Installer.log('You may need to manually reopen the Launcher application to try again.');
Installer.log(`${err}`);
throw new Error(`Failed to extract zip archive: ${err}`);
});
extractor.on('progress', (fileIndex, fileCount) => {
Installer.log('Extracted file ' + (fileIndex + 1) + ' of ' + fileCount);
});
const waitingExtraction = new Promise((resolve) => {
extractor.on('extract', () => {
Installer.log("Finished extracting");
resolve();
});
});
extractor.extract({
path: extractedArchivePath,
restrict: true, // prevents some security problems
filter: function (file) {
return file.type !== "SymbolicLink"; // same here
}
});
await waitingExtraction;
})();
// */
// ---- COMPILATION ----
const extractedArchivesPath = path.resolve(path.join(filepath, "/extraction"));
// Compile Home Page
const homePagePath = path.join(extractedArchivesPath, 'PenguinMod-Home-main');
Installer.log('Downloading all dependencies for Home, please wait...');
// we cant actually compile svelte locally, so we will just run in dev later
await Installer.exec(`cd ${JSON.stringify(homePagePath)} && npm i --force`);
// Compile GUI page
/*
// */
// create the verification file (used by the base program to see if penguinmod is even installed at all)
Installer.log('Creating verification...');
const verificationPath = path.resolve(path.join(filepath, "verification.txt"));
await Installer.writeFile(verificationPath, verificationKey, "utf8");
Installer.log('Actions completed');
await Installer.wait(1000);
}
}
module.exports = Installer;