1 | "use strict";
|
2 | Object.defineProperty(exports, "__esModule", { value: true });
|
3 | exports.NoOpLogger = exports.AppUpdater = void 0;
|
4 | const builder_util_runtime_1 = require("builder-util-runtime");
|
5 | const crypto_1 = require("crypto");
|
6 | const events_1 = require("events");
|
7 | const fs_extra_1 = require("fs-extra");
|
8 | const promises_1 = require("fs/promises");
|
9 | const js_yaml_1 = require("js-yaml");
|
10 | const lazy_val_1 = require("lazy-val");
|
11 | const path = require("path");
|
12 | const semver_1 = require("semver");
|
13 | const DownloadedUpdateHelper_1 = require("./DownloadedUpdateHelper");
|
14 | const ElectronAppAdapter_1 = require("./ElectronAppAdapter");
|
15 | const electronHttpExecutor_1 = require("./electronHttpExecutor");
|
16 | const GenericProvider_1 = require("./providers/GenericProvider");
|
17 | const main_1 = require("./main");
|
18 | const providerFactory_1 = require("./providerFactory");
|
19 | class AppUpdater extends events_1.EventEmitter {
|
20 | constructor(options, app) {
|
21 | super();
|
22 | |
23 |
|
24 |
|
25 | this.autoDownload = true;
|
26 | |
27 |
|
28 |
|
29 | this.autoInstallOnAppQuit = true;
|
30 | |
31 |
|
32 |
|
33 |
|
34 |
|
35 | this.allowPrerelease = false;
|
36 | |
37 |
|
38 |
|
39 |
|
40 | this.fullChangelog = false;
|
41 | |
42 |
|
43 |
|
44 |
|
45 |
|
46 |
|
47 |
|
48 | this.allowDowngrade = false;
|
49 | this._channel = null;
|
50 | this.downloadedUpdateHelper = null;
|
51 | |
52 |
|
53 |
|
54 | this.requestHeaders = null;
|
55 | this._logger = console;
|
56 |
|
57 | |
58 |
|
59 |
|
60 | this.signals = new main_1.UpdaterSignal(this);
|
61 | this._appUpdateConfigPath = null;
|
62 | this.clientPromise = null;
|
63 | this.stagingUserIdPromise = new lazy_val_1.Lazy(() => this.getOrCreateStagingUserId());
|
64 |
|
65 |
|
66 | this.configOnDisk = new lazy_val_1.Lazy(() => this.loadUpdateConfig());
|
67 | this.checkForUpdatesPromise = null;
|
68 | this.updateInfoAndProvider = null;
|
69 | |
70 |
|
71 |
|
72 |
|
73 | this._testOnlyOptions = null;
|
74 | this.on("error", (error) => {
|
75 | this._logger.error(`Error: ${error.stack || error.message}`);
|
76 | });
|
77 | if (app == null) {
|
78 | this.app = new ElectronAppAdapter_1.ElectronAppAdapter();
|
79 | this.httpExecutor = new electronHttpExecutor_1.ElectronHttpExecutor((authInfo, callback) => this.emit("login", authInfo, callback));
|
80 | }
|
81 | else {
|
82 | this.app = app;
|
83 | this.httpExecutor = null;
|
84 | }
|
85 | const currentVersionString = this.app.version;
|
86 | const currentVersion = semver_1.parse(currentVersionString);
|
87 | if (currentVersion == null) {
|
88 | throw builder_util_runtime_1.newError(`App version is not a valid semver version: "${currentVersionString}"`, "ERR_UPDATER_INVALID_VERSION");
|
89 | }
|
90 | this.currentVersion = currentVersion;
|
91 | this.allowPrerelease = hasPrereleaseComponents(currentVersion);
|
92 | if (options != null) {
|
93 | this.setFeedURL(options);
|
94 | if (typeof options !== "string" && options.requestHeaders) {
|
95 | this.requestHeaders = options.requestHeaders;
|
96 | }
|
97 | }
|
98 | }
|
99 | |
100 |
|
101 |
|
102 | get channel() {
|
103 | return this._channel;
|
104 | }
|
105 | |
106 |
|
107 |
|
108 |
|
109 |
|
110 | set channel(value) {
|
111 | if (this._channel != null) {
|
112 |
|
113 | if (typeof value !== "string") {
|
114 | throw builder_util_runtime_1.newError(`Channel must be a string, but got: ${value}`, "ERR_UPDATER_INVALID_CHANNEL");
|
115 | }
|
116 | else if (value.length === 0) {
|
117 | throw builder_util_runtime_1.newError(`Channel must be not an empty string`, "ERR_UPDATER_INVALID_CHANNEL");
|
118 | }
|
119 | }
|
120 | this._channel = value;
|
121 | this.allowDowngrade = true;
|
122 | }
|
123 | |
124 |
|
125 |
|
126 | addAuthHeader(token) {
|
127 | this.requestHeaders = Object.assign({}, this.requestHeaders, {
|
128 | authorization: token,
|
129 | });
|
130 | }
|
131 |
|
132 | get netSession() {
|
133 | return electronHttpExecutor_1.getNetSession();
|
134 | }
|
135 | |
136 |
|
137 |
|
138 |
|
139 | get logger() {
|
140 | return this._logger;
|
141 | }
|
142 | set logger(value) {
|
143 | this._logger = value == null ? new NoOpLogger() : value;
|
144 | }
|
145 |
|
146 | |
147 |
|
148 |
|
149 |
|
150 | set updateConfigPath(value) {
|
151 | this.clientPromise = null;
|
152 | this._appUpdateConfigPath = value;
|
153 | this.configOnDisk = new lazy_val_1.Lazy(() => this.loadUpdateConfig());
|
154 | }
|
155 |
|
156 | getFeedURL() {
|
157 | return "Deprecated. Do not use it.";
|
158 | }
|
159 | |
160 |
|
161 |
|
162 |
|
163 | setFeedURL(options) {
|
164 | const runtimeOptions = this.createProviderRuntimeOptions();
|
165 |
|
166 | let provider;
|
167 | if (typeof options === "string") {
|
168 | provider = new GenericProvider_1.GenericProvider({ provider: "generic", url: options }, this, {
|
169 | ...runtimeOptions,
|
170 | isUseMultipleRangeRequest: providerFactory_1.isUrlProbablySupportMultiRangeRequests(options),
|
171 | });
|
172 | }
|
173 | else {
|
174 | provider = providerFactory_1.createClient(options, this, runtimeOptions);
|
175 | }
|
176 | this.clientPromise = Promise.resolve(provider);
|
177 | }
|
178 | |
179 |
|
180 |
|
181 | checkForUpdates() {
|
182 | let checkForUpdatesPromise = this.checkForUpdatesPromise;
|
183 | if (checkForUpdatesPromise != null) {
|
184 | this._logger.info("Checking for update (already in progress)");
|
185 | return checkForUpdatesPromise;
|
186 | }
|
187 | const nullizePromise = () => (this.checkForUpdatesPromise = null);
|
188 | this._logger.info("Checking for update");
|
189 | checkForUpdatesPromise = this.doCheckForUpdates()
|
190 | .then(it => {
|
191 | nullizePromise();
|
192 | return it;
|
193 | })
|
194 | .catch(e => {
|
195 | nullizePromise();
|
196 | this.emit("error", e, `Cannot check for updates: ${(e.stack || e).toString()}`);
|
197 | throw e;
|
198 | });
|
199 | this.checkForUpdatesPromise = checkForUpdatesPromise;
|
200 | return checkForUpdatesPromise;
|
201 | }
|
202 | isUpdaterActive() {
|
203 | if (!this.app.isPackaged) {
|
204 | this._logger.info("Skip checkForUpdatesAndNotify because application is not packed");
|
205 | return false;
|
206 | }
|
207 | return true;
|
208 | }
|
209 |
|
210 | checkForUpdatesAndNotify(downloadNotification) {
|
211 | if (!this.isUpdaterActive()) {
|
212 | return Promise.resolve(null);
|
213 | }
|
214 | return this.checkForUpdates().then(it => {
|
215 | const downloadPromise = it.downloadPromise;
|
216 | if (downloadPromise == null) {
|
217 | if (this._logger.debug != null) {
|
218 | this._logger.debug("checkForUpdatesAndNotify called, downloadPromise is null");
|
219 | }
|
220 | return it;
|
221 | }
|
222 | void downloadPromise.then(() => {
|
223 | const notificationContent = AppUpdater.formatDownloadNotification(it.updateInfo.version, this.app.name, downloadNotification);
|
224 | new (require("electron").Notification)(notificationContent).show();
|
225 | });
|
226 | return it;
|
227 | });
|
228 | }
|
229 | static formatDownloadNotification(version, appName, downloadNotification) {
|
230 | if (downloadNotification == null) {
|
231 | downloadNotification = {
|
232 | title: "A new update is ready to install",
|
233 | body: `{appName} version {version} has been downloaded and will be automatically installed on exit`,
|
234 | };
|
235 | }
|
236 | downloadNotification = {
|
237 | title: downloadNotification.title.replace("{appName}", appName).replace("{version}", version),
|
238 | body: downloadNotification.body.replace("{appName}", appName).replace("{version}", version),
|
239 | };
|
240 | return downloadNotification;
|
241 | }
|
242 | async isStagingMatch(updateInfo) {
|
243 | const rawStagingPercentage = updateInfo.stagingPercentage;
|
244 | let stagingPercentage = rawStagingPercentage;
|
245 | if (stagingPercentage == null) {
|
246 | return true;
|
247 | }
|
248 | stagingPercentage = parseInt(stagingPercentage, 10);
|
249 | if (isNaN(stagingPercentage)) {
|
250 | this._logger.warn(`Staging percentage is NaN: ${rawStagingPercentage}`);
|
251 | return true;
|
252 | }
|
253 |
|
254 | stagingPercentage = stagingPercentage / 100;
|
255 | const stagingUserId = await this.stagingUserIdPromise.value;
|
256 | const val = builder_util_runtime_1.UUID.parse(stagingUserId).readUInt32BE(12);
|
257 | const percentage = val / 0xffffffff;
|
258 | this._logger.info(`Staging percentage: ${stagingPercentage}, percentage: ${percentage}, user id: ${stagingUserId}`);
|
259 | return percentage < stagingPercentage;
|
260 | }
|
261 | computeFinalHeaders(headers) {
|
262 | if (this.requestHeaders != null) {
|
263 | Object.assign(headers, this.requestHeaders);
|
264 | }
|
265 | return headers;
|
266 | }
|
267 | async isUpdateAvailable(updateInfo) {
|
268 | const latestVersion = semver_1.parse(updateInfo.version);
|
269 | if (latestVersion == null) {
|
270 | throw builder_util_runtime_1.newError(`This file could not be downloaded, or the latest version (from update server) does not have a valid semver version: "${updateInfo.version}"`, "ERR_UPDATER_INVALID_VERSION");
|
271 | }
|
272 | const currentVersion = this.currentVersion;
|
273 | if (semver_1.eq(latestVersion, currentVersion)) {
|
274 | return false;
|
275 | }
|
276 | const isStagingMatch = await this.isStagingMatch(updateInfo);
|
277 | if (!isStagingMatch) {
|
278 | return false;
|
279 | }
|
280 |
|
281 |
|
282 | const isLatestVersionNewer = semver_1.gt(latestVersion, currentVersion);
|
283 | const isLatestVersionOlder = semver_1.lt(latestVersion, currentVersion);
|
284 | if (isLatestVersionNewer) {
|
285 | return true;
|
286 | }
|
287 | return this.allowDowngrade && isLatestVersionOlder;
|
288 | }
|
289 | async getUpdateInfoAndProvider() {
|
290 | await this.app.whenReady();
|
291 | if (this.clientPromise == null) {
|
292 | this.clientPromise = this.configOnDisk.value.then(it => providerFactory_1.createClient(it, this, this.createProviderRuntimeOptions()));
|
293 | }
|
294 | const client = await this.clientPromise;
|
295 | const stagingUserId = await this.stagingUserIdPromise.value;
|
296 | client.setRequestHeaders(this.computeFinalHeaders({ "x-user-staging-id": stagingUserId }));
|
297 | return {
|
298 | info: await client.getLatestVersion(),
|
299 | provider: client,
|
300 | };
|
301 | }
|
302 |
|
303 | createProviderRuntimeOptions() {
|
304 | return {
|
305 | isUseMultipleRangeRequest: true,
|
306 | platform: this._testOnlyOptions == null ? process.platform : this._testOnlyOptions.platform,
|
307 | executor: this.httpExecutor,
|
308 | };
|
309 | }
|
310 | async doCheckForUpdates() {
|
311 | this.emit("checking-for-update");
|
312 | const result = await this.getUpdateInfoAndProvider();
|
313 | const updateInfo = result.info;
|
314 | if (!(await this.isUpdateAvailable(updateInfo))) {
|
315 | this._logger.info(`Update for version ${this.currentVersion} is not available (latest version: ${updateInfo.version}, downgrade is ${this.allowDowngrade ? "allowed" : "disallowed"}).`);
|
316 | this.emit("update-not-available", updateInfo);
|
317 | return {
|
318 | versionInfo: updateInfo,
|
319 | updateInfo,
|
320 | };
|
321 | }
|
322 | this.updateInfoAndProvider = result;
|
323 | this.onUpdateAvailable(updateInfo);
|
324 | const cancellationToken = new builder_util_runtime_1.CancellationToken();
|
325 |
|
326 | return {
|
327 | versionInfo: updateInfo,
|
328 | updateInfo,
|
329 | cancellationToken,
|
330 | downloadPromise: this.autoDownload ? this.downloadUpdate(cancellationToken) : null,
|
331 | };
|
332 | }
|
333 | onUpdateAvailable(updateInfo) {
|
334 | this._logger.info(`Found version ${updateInfo.version} (url: ${builder_util_runtime_1.asArray(updateInfo.files)
|
335 | .map(it => it.url)
|
336 | .join(", ")})`);
|
337 | this.emit("update-available", updateInfo);
|
338 | }
|
339 | |
340 |
|
341 |
|
342 |
|
343 | downloadUpdate(cancellationToken = new builder_util_runtime_1.CancellationToken()) {
|
344 | const updateInfoAndProvider = this.updateInfoAndProvider;
|
345 | if (updateInfoAndProvider == null) {
|
346 | const error = new Error("Please check update first");
|
347 | this.dispatchError(error);
|
348 | return Promise.reject(error);
|
349 | }
|
350 | this._logger.info(`Downloading update from ${builder_util_runtime_1.asArray(updateInfoAndProvider.info.files)
|
351 | .map(it => it.url)
|
352 | .join(", ")}`);
|
353 | const errorHandler = (e) => {
|
354 |
|
355 | if (!(e instanceof builder_util_runtime_1.CancellationError)) {
|
356 | try {
|
357 | this.dispatchError(e);
|
358 | }
|
359 | catch (nestedError) {
|
360 | this._logger.warn(`Cannot dispatch error event: ${nestedError.stack || nestedError}`);
|
361 | }
|
362 | }
|
363 | return e;
|
364 | };
|
365 | try {
|
366 | return this.doDownloadUpdate({
|
367 | updateInfoAndProvider,
|
368 | requestHeaders: this.computeRequestHeaders(updateInfoAndProvider.provider),
|
369 | cancellationToken,
|
370 | }).catch(e => {
|
371 | throw errorHandler(e);
|
372 | });
|
373 | }
|
374 | catch (e) {
|
375 | return Promise.reject(errorHandler(e));
|
376 | }
|
377 | }
|
378 | dispatchError(e) {
|
379 | this.emit("error", e, (e.stack || e).toString());
|
380 | }
|
381 | dispatchUpdateDownloaded(event) {
|
382 | this.emit(main_1.UPDATE_DOWNLOADED, event);
|
383 | }
|
384 | async loadUpdateConfig() {
|
385 | if (this._appUpdateConfigPath == null) {
|
386 | this._appUpdateConfigPath = this.app.appUpdateConfigPath;
|
387 | }
|
388 | return js_yaml_1.load(await promises_1.readFile(this._appUpdateConfigPath, "utf-8"));
|
389 | }
|
390 | computeRequestHeaders(provider) {
|
391 | const fileExtraDownloadHeaders = provider.fileExtraDownloadHeaders;
|
392 | if (fileExtraDownloadHeaders != null) {
|
393 | const requestHeaders = this.requestHeaders;
|
394 | return requestHeaders == null
|
395 | ? fileExtraDownloadHeaders
|
396 | : {
|
397 | ...fileExtraDownloadHeaders,
|
398 | ...requestHeaders,
|
399 | };
|
400 | }
|
401 | return this.computeFinalHeaders({ accept: "*/*" });
|
402 | }
|
403 | async getOrCreateStagingUserId() {
|
404 | const file = path.join(this.app.userDataPath, ".updaterId");
|
405 | try {
|
406 | const id = await promises_1.readFile(file, "utf-8");
|
407 | if (builder_util_runtime_1.UUID.check(id)) {
|
408 | return id;
|
409 | }
|
410 | else {
|
411 | this._logger.warn(`Staging user id file exists, but content was invalid: ${id}`);
|
412 | }
|
413 | }
|
414 | catch (e) {
|
415 | if (e.code !== "ENOENT") {
|
416 | this._logger.warn(`Couldn't read staging user ID, creating a blank one: ${e}`);
|
417 | }
|
418 | }
|
419 | const id = builder_util_runtime_1.UUID.v5(crypto_1.randomBytes(4096), builder_util_runtime_1.UUID.OID);
|
420 | this._logger.info(`Generated new staging user ID: ${id}`);
|
421 | try {
|
422 | await fs_extra_1.outputFile(file, id);
|
423 | }
|
424 | catch (e) {
|
425 | this._logger.warn(`Couldn't write out staging user ID: ${e}`);
|
426 | }
|
427 | return id;
|
428 | }
|
429 |
|
430 | get isAddNoCacheQuery() {
|
431 | const headers = this.requestHeaders;
|
432 |
|
433 | if (headers == null) {
|
434 | return true;
|
435 | }
|
436 | for (const headerName of Object.keys(headers)) {
|
437 | const s = headerName.toLowerCase();
|
438 | if (s === "authorization" || s === "private-token") {
|
439 | return false;
|
440 | }
|
441 | }
|
442 | return true;
|
443 | }
|
444 | async getOrCreateDownloadHelper() {
|
445 | let result = this.downloadedUpdateHelper;
|
446 | if (result == null) {
|
447 | const dirName = (await this.configOnDisk.value).updaterCacheDirName;
|
448 | const logger = this._logger;
|
449 | if (dirName == null) {
|
450 | logger.error("updaterCacheDirName is not specified in app-update.yml Was app build using at least electron-builder 20.34.0?");
|
451 | }
|
452 | const cacheDir = path.join(this.app.baseCachePath, dirName || this.app.name);
|
453 | if (logger.debug != null) {
|
454 | logger.debug(`updater cache dir: ${cacheDir}`);
|
455 | }
|
456 | result = new DownloadedUpdateHelper_1.DownloadedUpdateHelper(cacheDir);
|
457 | this.downloadedUpdateHelper = result;
|
458 | }
|
459 | return result;
|
460 | }
|
461 | async executeDownload(taskOptions) {
|
462 | const fileInfo = taskOptions.fileInfo;
|
463 | const downloadOptions = {
|
464 | headers: taskOptions.downloadUpdateOptions.requestHeaders,
|
465 | cancellationToken: taskOptions.downloadUpdateOptions.cancellationToken,
|
466 | sha2: fileInfo.info.sha2,
|
467 | sha512: fileInfo.info.sha512,
|
468 | };
|
469 | if (this.listenerCount(main_1.DOWNLOAD_PROGRESS) > 0) {
|
470 | downloadOptions.onProgress = it => this.emit(main_1.DOWNLOAD_PROGRESS, it);
|
471 | }
|
472 | const updateInfo = taskOptions.downloadUpdateOptions.updateInfoAndProvider.info;
|
473 | const version = updateInfo.version;
|
474 | const packageInfo = fileInfo.packageInfo;
|
475 | function getCacheUpdateFileName() {
|
476 |
|
477 | const urlPath = decodeURIComponent(taskOptions.fileInfo.url.pathname);
|
478 | if (urlPath.endsWith(`.${taskOptions.fileExtension}`)) {
|
479 | return path.posix.basename(urlPath);
|
480 | }
|
481 | else {
|
482 |
|
483 | return `update.${taskOptions.fileExtension}`;
|
484 | }
|
485 | }
|
486 | const downloadedUpdateHelper = await this.getOrCreateDownloadHelper();
|
487 | const cacheDir = downloadedUpdateHelper.cacheDirForPendingUpdate;
|
488 | await promises_1.mkdir(cacheDir, { recursive: true });
|
489 | const updateFileName = getCacheUpdateFileName();
|
490 | let updateFile = path.join(cacheDir, updateFileName);
|
491 | const packageFile = packageInfo == null ? null : path.join(cacheDir, `package-${version}${path.extname(packageInfo.path) || ".7z"}`);
|
492 | const done = async (isSaveCache) => {
|
493 | await downloadedUpdateHelper.setDownloadedFile(updateFile, packageFile, updateInfo, fileInfo, updateFileName, isSaveCache);
|
494 | await taskOptions.done({
|
495 | ...updateInfo,
|
496 | downloadedFile: updateFile,
|
497 | });
|
498 | return packageFile == null ? [updateFile] : [updateFile, packageFile];
|
499 | };
|
500 | const log = this._logger;
|
501 | const cachedUpdateFile = await downloadedUpdateHelper.validateDownloadedPath(updateFile, updateInfo, fileInfo, log);
|
502 | if (cachedUpdateFile != null) {
|
503 | updateFile = cachedUpdateFile;
|
504 | return await done(false);
|
505 | }
|
506 | const removeFileIfAny = async () => {
|
507 | await downloadedUpdateHelper.clear().catch(() => {
|
508 |
|
509 | });
|
510 | return await promises_1.unlink(updateFile).catch(() => {
|
511 |
|
512 | });
|
513 | };
|
514 | const tempUpdateFile = await DownloadedUpdateHelper_1.createTempUpdateFile(`temp-${updateFileName}`, cacheDir, log);
|
515 | try {
|
516 | await taskOptions.task(tempUpdateFile, downloadOptions, packageFile, removeFileIfAny);
|
517 | await promises_1.rename(tempUpdateFile, updateFile);
|
518 | }
|
519 | catch (e) {
|
520 | await removeFileIfAny();
|
521 | if (e instanceof builder_util_runtime_1.CancellationError) {
|
522 | log.info("cancelled");
|
523 | this.emit("update-cancelled", updateInfo);
|
524 | }
|
525 | throw e;
|
526 | }
|
527 | log.info(`New version ${version} has been downloaded to ${updateFile}`);
|
528 | return await done(true);
|
529 | }
|
530 | }
|
531 | exports.AppUpdater = AppUpdater;
|
532 | function hasPrereleaseComponents(version) {
|
533 | const versionPrereleaseComponent = semver_1.prerelease(version);
|
534 | return versionPrereleaseComponent != null && versionPrereleaseComponent.length > 0;
|
535 | }
|
536 |
|
537 | class NoOpLogger {
|
538 |
|
539 | info(message) {
|
540 |
|
541 | }
|
542 |
|
543 | warn(message) {
|
544 |
|
545 | }
|
546 |
|
547 | error(message) {
|
548 |
|
549 | }
|
550 | }
|
551 | exports.NoOpLogger = NoOpLogger;
|
552 |
|
\ | No newline at end of file |