graceful-updater 1.0.5
Sign up to get free protection for your applications and to get access to all the features.
- package/README.en.md +145 -0
- package/README.md +145 -0
- package/build/app-updator.js +247 -0
- package/build/common/constants.js +118 -0
- package/build/elelctron-app-adapter.js +33 -0
- package/build/helper/installer.exe +0 -0
- package/build/helper/unzip.exe +0 -0
- package/build/index.js +15 -0
- package/build/mac-updator.js +108 -0
- package/build/utils/download-file.js +78 -0
- package/build/utils/index.js +122 -0
- package/build/utils/install-macos-dmg.js +155 -0
- package/build/utils/sudo-prompt-exec.js +23 -0
- package/build/windows-updator.js +89 -0
- package/package.json +73 -0
package/README.en.md
ADDED
@@ -0,0 +1,145 @@
|
|
1
|
+
# graceful-updater
|
2
|
+
|
3
|
+
[![NPM version][npm-image]][npm-url]
|
4
|
+
[![CI][ci-image]][ci-url]
|
5
|
+
[![node version][node-image]][node-url]
|
6
|
+
[![npm download][download-image]][download-url]
|
7
|
+
|
8
|
+
[npm-image]: https://img.shields.io/npm/v/graceful-updater.svg
|
9
|
+
[npm-url]: https://npmjs.org/package/graceful-updater
|
10
|
+
[ci-image]: https://github.com/electron-modules/graceful-updater/actions/workflows/ci.yml/badge.svg
|
11
|
+
[ci-url]: https://github.com/electron-modules/graceful-updater/actions/workflows/ci.yml
|
12
|
+
[node-image]: https://img.shields.io/badge/node.js-%3E=_16-green.svg
|
13
|
+
[node-url]: http://nodejs.org/download/
|
14
|
+
[download-image]: https://img.shields.io/npm/dm/graceful-updater.svg
|
15
|
+
[download-url]: https://npmjs.org/package/graceful-updater
|
16
|
+
|
17
|
+
> Software updates solution for Electron applications, It is convenient to complete full software update and dynamic update.
|
18
|
+
## Installment
|
19
|
+
|
20
|
+
```bash
|
21
|
+
$ npm i graceful-updater --save
|
22
|
+
```
|
23
|
+
|
24
|
+
## Sample
|
25
|
+
|
26
|
+
please visit: https://github.com/electron-modules/electron-modules-sample
|
27
|
+
|
28
|
+
```typescript
|
29
|
+
// 1. 构造 options
|
30
|
+
const options = {
|
31
|
+
url: getFeedUrl(),
|
32
|
+
logger: console, // logger
|
33
|
+
productName: 'demo',
|
34
|
+
updateInfoFormatter: (res) => {
|
35
|
+
return res;
|
36
|
+
},
|
37
|
+
ifNeedUpdate: (res) => {
|
38
|
+
console.log('local version', currentVersion);
|
39
|
+
console.log('local project version', currentBuildNumber);
|
40
|
+
console.log('remote version', res.version);
|
41
|
+
console.log('remote project version', res.project_version);
|
42
|
+
return semver.gt(res.version, currentVersion) ||
|
43
|
+
res.project_version > currentBuildNumber;
|
44
|
+
},
|
45
|
+
};
|
46
|
+
// 2. 初始化 updator 实例
|
47
|
+
const electronUpdator = new MacUpdator(options);
|
48
|
+
// 3. 绑定全局事件
|
49
|
+
electronUpdator.on(EventType.UPDATE_DOWNLOADED, (...args) => {
|
50
|
+
console.log('updator >> %s, args: %j', EventType.UPDATE_DOWNLOADED, args);
|
51
|
+
});
|
52
|
+
electronUpdator.on(EventType.CHECKING_FOR_UPDATE, (...args) => {
|
53
|
+
console.log('updator >> %s, args: %j', EventType.CHECKING_FOR_UPDATE, args);
|
54
|
+
});
|
55
|
+
electronUpdator.on(EventType.UPDATE_AVAILABLE, (data) => {
|
56
|
+
const { version, project_version } = data?.updateInfo || {};
|
57
|
+
const message = [
|
58
|
+
'available',
|
59
|
+
`local version: ${currentVersion}`,
|
60
|
+
`local project version: ${currentBuildNumber}`,
|
61
|
+
`remote version: ${version}`,
|
62
|
+
`remote project version: ${project_version}`,
|
63
|
+
].join('\n');
|
64
|
+
dialog.showMessageBoxSync({
|
65
|
+
message,
|
66
|
+
});
|
67
|
+
});
|
68
|
+
electronUpdator.on(EventType.UPDATE_NOT_AVAILABLE, (data) => {
|
69
|
+
const { version, project_version } = data?.updateInfo || {};
|
70
|
+
const message = [
|
71
|
+
'not available',
|
72
|
+
`local version: ${currentVersion}`,
|
73
|
+
`local project version: ${currentBuildNumber}`,
|
74
|
+
`remote version: ${version}`,
|
75
|
+
`remote project version: ${project_version}`,
|
76
|
+
].join('\n');
|
77
|
+
dialog.showMessageBoxSync({
|
78
|
+
message,
|
79
|
+
});
|
80
|
+
});
|
81
|
+
electronUpdator.on(EventType.ERROR, (...args) => {
|
82
|
+
console.log('updator >> %s, args: %j', EventType.ERROR, args);
|
83
|
+
});
|
84
|
+
electronUpdator.on(EventType.UPDATE_DOWNLOAD_PROGRESS, (data) => {
|
85
|
+
const { status, progress } = data;
|
86
|
+
console.log('updator >> %s, status: %s, progress: %d', EventType.UPDATE_DOWNLOAD_PROGRESS, status, progress);
|
87
|
+
app.windowManager.get('updator').webContents.send('updator:updateDownloadProgress', { status, progress });
|
88
|
+
});
|
89
|
+
```
|
90
|
+
|
91
|
+
## Documents
|
92
|
+
|
93
|
+
### Options
|
94
|
+
|
95
|
+
| 字段 | 类型 | 是否必须 | 描述 | 备注 |
|
96
|
+
| --- | --- | --- | --- | --- |
|
97
|
+
| url | String | 必须 | 检测更新的远程地址,返回数据遵循 UpdateInfo 字段 | |
|
98
|
+
| ifNeedUpdate | Function | 非必须 | 返回是否需要更新 | |
|
99
|
+
| updateInfoFormatter | Function | 非必须 | 服务端返回数据格式适配 | 如果返回的格式无法与需要的字段相匹配时,参数进行格式化 |
|
100
|
+
| logger | Object | 非必须 | 日志 | |
|
101
|
+
| productName | String | 产品名 | 应用名 | |
|
102
|
+
|
103
|
+
### UpdateInfo
|
104
|
+
|
105
|
+
| 字段 | 类型 | 是否必须 | 描述 | 备注 |
|
106
|
+
| --- | --- | --- | --- | --- |
|
107
|
+
| version | String | 必须 | 版本号 | |
|
108
|
+
| projectVersion | Number | 必须 | 构建号 | |
|
109
|
+
| files | Array<Object> | 必须 | 需要下载的文件列表 | |
|
110
|
+
| updateType | Enum<String> | 必须 | 更新类型,是否动态替换更新 | Package or Asar |
|
111
|
+
| releaseNotes | Array<String> | 必须 | 更新日志 | |
|
112
|
+
|
113
|
+
### Methods
|
114
|
+
|
115
|
+
1. checkForUpdates()
|
116
|
+
|
117
|
+
检测是否有需要更新的内容
|
118
|
+
|
119
|
+
2. downloadUpdate()
|
120
|
+
|
121
|
+
开始下载
|
122
|
+
|
123
|
+
3. quitAndInstall()
|
124
|
+
|
125
|
+
### Events
|
126
|
+
|
127
|
+
1. checking-for-update
|
128
|
+
|
129
|
+
当开始检查更新的时候触发
|
130
|
+
|
131
|
+
<!-- GITCONTRIBUTOR_START -->
|
132
|
+
|
133
|
+
## Contributors
|
134
|
+
|
135
|
+
|[<img src="https://avatars.githubusercontent.com/u/4081746?v=4" width="100px;"/><br/><sub><b>zlyi</b></sub>](https://github.com/zlyi)<br/>|[<img src="https://avatars.githubusercontent.com/u/1011681?v=4" width="100px;"/><br/><sub><b>xudafeng</b></sub>](https://github.com/xudafeng)<br/>|[<img src="https://avatars.githubusercontent.com/u/52845048?v=4" width="100px;"/><br/><sub><b>snapre</b></sub>](https://github.com/snapre)<br/>|
|
136
|
+
| :---: | :---: | :---: |
|
137
|
+
|
138
|
+
|
139
|
+
This project follows the git-contributor [spec](https://github.com/xudafeng/git-contributor), auto updated at `Tue Jan 31 2023 14:31:40 GMT+0800`.
|
140
|
+
|
141
|
+
<!-- GITCONTRIBUTOR_END -->
|
142
|
+
|
143
|
+
## License
|
144
|
+
|
145
|
+
The MIT License (MIT)
|
package/README.md
ADDED
@@ -0,0 +1,145 @@
|
|
1
|
+
# graceful-updater
|
2
|
+
|
3
|
+
[![NPM version][npm-image]][npm-url]
|
4
|
+
[![CI][ci-image]][ci-url]
|
5
|
+
[![node version][node-image]][node-url]
|
6
|
+
[![npm download][download-image]][download-url]
|
7
|
+
|
8
|
+
[npm-image]: https://img.shields.io/npm/v/graceful-updater.svg
|
9
|
+
[npm-url]: https://npmjs.org/package/graceful-updater
|
10
|
+
[ci-image]: https://github.com/electron-modules/graceful-updater/actions/workflows/ci.yml/badge.svg
|
11
|
+
[ci-url]: https://github.com/electron-modules/graceful-updater/actions/workflows/ci.yml
|
12
|
+
[node-image]: https://img.shields.io/badge/node.js-%3E=_16-green.svg
|
13
|
+
[node-url]: http://nodejs.org/download/
|
14
|
+
[download-image]: https://img.shields.io/npm/dm/graceful-updater.svg
|
15
|
+
[download-url]: https://npmjs.org/package/graceful-updater
|
16
|
+
|
17
|
+
> Software updates solution for Electron applications, It is convenient to complete full software update and dynamic update.
|
18
|
+
## Installment
|
19
|
+
|
20
|
+
```bash
|
21
|
+
$ npm i graceful-updater --save
|
22
|
+
```
|
23
|
+
|
24
|
+
## Sample
|
25
|
+
|
26
|
+
please visit: https://github.com/electron-modules/electron-modules-sample
|
27
|
+
|
28
|
+
```typescript
|
29
|
+
// 1. 构造 options
|
30
|
+
const options = {
|
31
|
+
url: getFeedUrl(),
|
32
|
+
logger: console, // logger
|
33
|
+
productName: 'demo',
|
34
|
+
updateInfoFormatter: (res) => {
|
35
|
+
return res;
|
36
|
+
},
|
37
|
+
ifNeedUpdate: (res) => {
|
38
|
+
console.log('local version', currentVersion);
|
39
|
+
console.log('local project version', currentBuildNumber);
|
40
|
+
console.log('remote version', res.version);
|
41
|
+
console.log('remote project version', res.project_version);
|
42
|
+
return semver.gt(res.version, currentVersion) ||
|
43
|
+
res.project_version > currentBuildNumber;
|
44
|
+
},
|
45
|
+
};
|
46
|
+
// 2. 初始化 updator 实例
|
47
|
+
const electronUpdator = new MacUpdator(options);
|
48
|
+
// 3. 绑定全局事件
|
49
|
+
electronUpdator.on(EventType.UPDATE_DOWNLOADED, (...args) => {
|
50
|
+
console.log('updator >> %s, args: %j', EventType.UPDATE_DOWNLOADED, args);
|
51
|
+
});
|
52
|
+
electronUpdator.on(EventType.CHECKING_FOR_UPDATE, (...args) => {
|
53
|
+
console.log('updator >> %s, args: %j', EventType.CHECKING_FOR_UPDATE, args);
|
54
|
+
});
|
55
|
+
electronUpdator.on(EventType.UPDATE_AVAILABLE, (data) => {
|
56
|
+
const { version, project_version } = data?.updateInfo || {};
|
57
|
+
const message = [
|
58
|
+
'available',
|
59
|
+
`local version: ${currentVersion}`,
|
60
|
+
`local project version: ${currentBuildNumber}`,
|
61
|
+
`remote version: ${version}`,
|
62
|
+
`remote project version: ${project_version}`,
|
63
|
+
].join('\n');
|
64
|
+
dialog.showMessageBoxSync({
|
65
|
+
message,
|
66
|
+
});
|
67
|
+
});
|
68
|
+
electronUpdator.on(EventType.UPDATE_NOT_AVAILABLE, (data) => {
|
69
|
+
const { version, project_version } = data?.updateInfo || {};
|
70
|
+
const message = [
|
71
|
+
'not available',
|
72
|
+
`local version: ${currentVersion}`,
|
73
|
+
`local project version: ${currentBuildNumber}`,
|
74
|
+
`remote version: ${version}`,
|
75
|
+
`remote project version: ${project_version}`,
|
76
|
+
].join('\n');
|
77
|
+
dialog.showMessageBoxSync({
|
78
|
+
message,
|
79
|
+
});
|
80
|
+
});
|
81
|
+
electronUpdator.on(EventType.ERROR, (...args) => {
|
82
|
+
console.log('updator >> %s, args: %j', EventType.ERROR, args);
|
83
|
+
});
|
84
|
+
electronUpdator.on(EventType.UPDATE_DOWNLOAD_PROGRESS, (data) => {
|
85
|
+
const { status, progress } = data;
|
86
|
+
console.log('updator >> %s, status: %s, progress: %d', EventType.UPDATE_DOWNLOAD_PROGRESS, status, progress);
|
87
|
+
app.windowManager.get('updator').webContents.send('updator:updateDownloadProgress', { status, progress });
|
88
|
+
});
|
89
|
+
```
|
90
|
+
|
91
|
+
## Documents
|
92
|
+
|
93
|
+
### Options
|
94
|
+
|
95
|
+
| 字段 | 类型 | 是否必须 | 描述 | 备注 |
|
96
|
+
| --- | --- | --- | --- | --- |
|
97
|
+
| url | String | 必须 | 检测更新的远程地址,返回数据遵循 UpdateInfo 字段 | |
|
98
|
+
| ifNeedUpdate | Function | 非必须 | 返回是否需要更新 | |
|
99
|
+
| updateInfoFormatter | Function | 非必须 | 服务端返回数据格式适配 | 如果返回的格式无法与需要的字段相匹配时,参数进行格式化 |
|
100
|
+
| logger | Object | 非必须 | 日志 | |
|
101
|
+
| productName | String | 产品名 | 应用名 | |
|
102
|
+
|
103
|
+
### UpdateInfo
|
104
|
+
|
105
|
+
| 字段 | 类型 | 是否必须 | 描述 | 备注 |
|
106
|
+
| --- | --- | --- | --- | --- |
|
107
|
+
| version | String | 必须 | 版本号 | |
|
108
|
+
| projectVersion | Number | 必须 | 构建号 | |
|
109
|
+
| files | Array<Object> | 必须 | 需要下载的文件列表 | |
|
110
|
+
| updateType | Enum<String> | 必须 | 更新类型,是否动态替换更新 | Package or Asar |
|
111
|
+
| releaseNotes | Array<String> | 必须 | 更新日志 | |
|
112
|
+
|
113
|
+
### Methods
|
114
|
+
|
115
|
+
1. checkForUpdates()
|
116
|
+
|
117
|
+
检测是否有需要更新的内容
|
118
|
+
|
119
|
+
2. downloadUpdate()
|
120
|
+
|
121
|
+
开始下载
|
122
|
+
|
123
|
+
3. quitAndInstall()
|
124
|
+
|
125
|
+
### Events
|
126
|
+
|
127
|
+
1. checking-for-update
|
128
|
+
|
129
|
+
当开始检查更新的时候触发
|
130
|
+
|
131
|
+
<!-- GITCONTRIBUTOR_START -->
|
132
|
+
|
133
|
+
## Contributors
|
134
|
+
|
135
|
+
|[<img src="https://avatars.githubusercontent.com/u/4081746?v=4" width="100px;"/><br/><sub><b>zlyi</b></sub>](https://github.com/zlyi)<br/>|[<img src="https://avatars.githubusercontent.com/u/1011681?v=4" width="100px;"/><br/><sub><b>xudafeng</b></sub>](https://github.com/xudafeng)<br/>|[<img src="https://avatars.githubusercontent.com/u/52845048?v=4" width="100px;"/><br/><sub><b>snapre</b></sub>](https://github.com/snapre)<br/>|
|
136
|
+
| :---: | :---: | :---: |
|
137
|
+
|
138
|
+
|
139
|
+
This project follows the git-contributor [spec](https://github.com/xudafeng/git-contributor), auto updated at `Tue Jan 31 2023 14:31:40 GMT+0800`.
|
140
|
+
|
141
|
+
<!-- GITCONTRIBUTOR_END -->
|
142
|
+
|
143
|
+
## License
|
144
|
+
|
145
|
+
The MIT License (MIT)
|
@@ -0,0 +1,247 @@
|
|
1
|
+
"use strict";
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
4
|
+
};
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
6
|
+
exports.AppUpdator = void 0;
|
7
|
+
const eventemitter3_1 = require("eventemitter3");
|
8
|
+
const nanoid_1 = require("nanoid");
|
9
|
+
const path_1 = __importDefault(require("path"));
|
10
|
+
const constants_1 = require("./common/constants");
|
11
|
+
const download_file_1 = require("./utils/download-file");
|
12
|
+
const utils_1 = require("./utils");
|
13
|
+
const elelctron_app_adapter_1 = require("./elelctron-app-adapter");
|
14
|
+
const DEFAULT_EXEFILE_DIR = path_1.default.join(__dirname, '..', 'helper');
|
15
|
+
class AppUpdator extends eventemitter3_1.EventEmitter {
|
16
|
+
constructor(options, app) {
|
17
|
+
super();
|
18
|
+
this.state = constants_1.StateType.Idle;
|
19
|
+
this.options = options;
|
20
|
+
this._windowHelperExeDir = this.options?.getWindowsHelperExeDir?.() || DEFAULT_EXEFILE_DIR;
|
21
|
+
this.logger = this._wrapLogger(options.logger);
|
22
|
+
this.app = app || new elelctron_app_adapter_1.ElectronAppAdapter();
|
23
|
+
this.startUuid = this._getStartUuid();
|
24
|
+
this.logger.info('constructor');
|
25
|
+
this.availableUpdate = {
|
26
|
+
resourcePath: '',
|
27
|
+
latestAsarPath: '',
|
28
|
+
downloadTargetDir: '',
|
29
|
+
};
|
30
|
+
}
|
31
|
+
_getStartUuid() {
|
32
|
+
return `${(0, nanoid_1.nanoid)(16)}${Date.now()}`;
|
33
|
+
}
|
34
|
+
_wrapLogger(logger) {
|
35
|
+
if (!logger) {
|
36
|
+
logger = console;
|
37
|
+
}
|
38
|
+
const _logger = { ...logger };
|
39
|
+
const _wrap = (message, args, callback) => {
|
40
|
+
callback(`[ElectronUpdator][${this.startUuid}]#${message}`, ...args);
|
41
|
+
};
|
42
|
+
_logger.error = (message, ...args) => {
|
43
|
+
_wrap(message, args, logger.error);
|
44
|
+
};
|
45
|
+
_logger.info = (message, ...args) => {
|
46
|
+
_wrap(message, args, logger.info);
|
47
|
+
};
|
48
|
+
_logger.warn = (message, ...args) => {
|
49
|
+
_wrap(message, args, logger.warn);
|
50
|
+
};
|
51
|
+
return _logger;
|
52
|
+
}
|
53
|
+
setState(state) {
|
54
|
+
this.logger.info(`setState:${state}`);
|
55
|
+
this.state = state;
|
56
|
+
}
|
57
|
+
setFeedUrl(url) {
|
58
|
+
this.logger.info(`setFeedUrl:url is ${url}`);
|
59
|
+
if (url && this.options) {
|
60
|
+
this.options.url = url;
|
61
|
+
}
|
62
|
+
}
|
63
|
+
async checkForUpdates(executeType = constants_1.ExecuteType.Auto) {
|
64
|
+
this.logger.info(`checkForUpdates:state is ${this.state}`);
|
65
|
+
this.setState(constants_1.StateType.Idle);
|
66
|
+
try {
|
67
|
+
// 新一轮更新流程,更新 startUuid
|
68
|
+
this.startUuid = this._getStartUuid();
|
69
|
+
this.setState(constants_1.StateType.CheckingForUpdate);
|
70
|
+
this.emit(constants_1.EventType.CHECKING_FOR_UPDATE);
|
71
|
+
const updateInfoResponse = await (0, utils_1.requestUpdateInfo)(this.options);
|
72
|
+
this.updateInfo = (this.options?.updateInfoFormatter ? this.options?.updateInfoFormatter(updateInfoResponse) : updateInfoResponse);
|
73
|
+
const ifNeedUpdate = this.options?.ifNeedUpdate(updateInfoResponse);
|
74
|
+
if (!ifNeedUpdate) {
|
75
|
+
this.logger.info(`checkForUpdates:updateInfo is ${JSON.stringify(this.updateInfo)},ifNeedUpdate is false`);
|
76
|
+
this.emit(constants_1.EventType.UPDATE_NOT_AVAILABLE, {
|
77
|
+
updateInfo: this.updateInfo,
|
78
|
+
executeType,
|
79
|
+
});
|
80
|
+
this.setState(constants_1.StateType.Idle);
|
81
|
+
return;
|
82
|
+
}
|
83
|
+
this.logger.info('checkForUpdates:ifNeedUpdate is true');
|
84
|
+
this.availableUpdate = this.doGetAvailableUpdateInfo(this.updateInfo);
|
85
|
+
if (!this.options?.autoDownload || executeType === constants_1.ExecuteType.User) {
|
86
|
+
this.logger.info('checkForUpdates:emit UPDATE_AVAILABLE');
|
87
|
+
this.emit(constants_1.EventType.UPDATE_AVAILABLE, {
|
88
|
+
updateInfo: this.updateInfo,
|
89
|
+
executeType,
|
90
|
+
});
|
91
|
+
return;
|
92
|
+
}
|
93
|
+
this.downloadUpdate(executeType);
|
94
|
+
}
|
95
|
+
catch (e) {
|
96
|
+
e.customMessage = e.customMessage ? e.customMessage : `${constants_1.InstallResultType.CheckForUpdatesError}_${e.message}`;
|
97
|
+
this.logError(e);
|
98
|
+
this.setState(constants_1.StateType.Idle);
|
99
|
+
}
|
100
|
+
}
|
101
|
+
async downloadUpdate(executeType = constants_1.ExecuteType.User) {
|
102
|
+
this.logger.info(`downloadUpdate:executeType is ${executeType}`);
|
103
|
+
await this.downloadUpdateFile(this.updateInfo, executeType);
|
104
|
+
const result = await this.preCheck(executeType);
|
105
|
+
if (result.success) {
|
106
|
+
this.logger.info('downloadUpdate:emit UPDATE_DOWNLOADED');
|
107
|
+
this.emit(constants_1.EventType.UPDATE_DOWNLOADED, {
|
108
|
+
executeType,
|
109
|
+
});
|
110
|
+
}
|
111
|
+
else {
|
112
|
+
this.logError(result.error);
|
113
|
+
this.setState(constants_1.StateType.Idle);
|
114
|
+
}
|
115
|
+
}
|
116
|
+
async quitAndInstall() {
|
117
|
+
this.logger.info(`quitAndInstall:state is ${this.state}`);
|
118
|
+
if (this.state !== constants_1.StateType.Downloaded) {
|
119
|
+
this.downloadUpdate();
|
120
|
+
return;
|
121
|
+
}
|
122
|
+
this.setState(constants_1.StateType.Idle);
|
123
|
+
try {
|
124
|
+
let result = { success: false };
|
125
|
+
this.emit(constants_1.EventType.BEFORE_QUIT_FOR_UPDATE);
|
126
|
+
if (this.updateInfo?.updateType === constants_1.UpdateType.Package) {
|
127
|
+
result = await this.doQuitAndInstallPackage();
|
128
|
+
}
|
129
|
+
else {
|
130
|
+
result = await this.doQuitAndInstallAsar();
|
131
|
+
}
|
132
|
+
if (!result.success) {
|
133
|
+
result.message = `error: ${result.error?.message}`;
|
134
|
+
this.dispatchError(result.error);
|
135
|
+
}
|
136
|
+
}
|
137
|
+
catch (e) {
|
138
|
+
this.dispatchError(e);
|
139
|
+
}
|
140
|
+
}
|
141
|
+
async preCheckForAsar() {
|
142
|
+
this.logger.info('preCheckForAsar');
|
143
|
+
return await this.unzip();
|
144
|
+
}
|
145
|
+
async preCheck(executeType) {
|
146
|
+
this.logger.info('preCheck');
|
147
|
+
const { resourcePath } = this.availableUpdate;
|
148
|
+
if (this.state !== constants_1.StateType.Downloaded) {
|
149
|
+
return {
|
150
|
+
success: false,
|
151
|
+
error: new Error(`preCheck:update status(${this.state}) error`),
|
152
|
+
};
|
153
|
+
}
|
154
|
+
// 清理老包
|
155
|
+
try {
|
156
|
+
this.logger.info('preCheck:cleanOldArchive');
|
157
|
+
await (0, utils_1.cleanOldArchive)(resourcePath);
|
158
|
+
}
|
159
|
+
catch (e) {
|
160
|
+
this.logError(e);
|
161
|
+
}
|
162
|
+
let result = { success: true };
|
163
|
+
const { downloadTargetDir } = this.availableUpdate;
|
164
|
+
try {
|
165
|
+
const hasLatestFile = await (0, utils_1.existsAsync)(downloadTargetDir);
|
166
|
+
// 下载失败返回提示
|
167
|
+
if (!hasLatestFile) {
|
168
|
+
return {
|
169
|
+
success: false,
|
170
|
+
error: new Error('file is notfound'),
|
171
|
+
};
|
172
|
+
}
|
173
|
+
}
|
174
|
+
catch (e) {
|
175
|
+
return {
|
176
|
+
success: false,
|
177
|
+
error: e,
|
178
|
+
};
|
179
|
+
}
|
180
|
+
if (this.updateInfo?.updateType === constants_1.UpdateType.Package) {
|
181
|
+
if (executeType === constants_1.ExecuteType.Auto) {
|
182
|
+
// 自动安装时,才进行预检查,提升安装效率
|
183
|
+
result = await this.doPreCheckForPackage();
|
184
|
+
}
|
185
|
+
}
|
186
|
+
else {
|
187
|
+
result = await this.preCheckForAsar();
|
188
|
+
}
|
189
|
+
return result;
|
190
|
+
}
|
191
|
+
async downloadUpdateFile(updateInfo, executeType) {
|
192
|
+
if (this.state !== constants_1.StateType.CheckingForUpdate) {
|
193
|
+
throw new Error(`downloadUpdateFile:update status(${this.state}) error`);
|
194
|
+
}
|
195
|
+
const { url, signature } = updateInfo.files[0];
|
196
|
+
const { downloadTargetDir } = this.availableUpdate;
|
197
|
+
this.setState(constants_1.StateType.Downloading);
|
198
|
+
try {
|
199
|
+
await (0, download_file_1.downloadFile)({
|
200
|
+
logger: this.logger,
|
201
|
+
url,
|
202
|
+
signature,
|
203
|
+
targetDir: downloadTargetDir,
|
204
|
+
emit: this.emit,
|
205
|
+
progressHandle: (data) => {
|
206
|
+
this.emit(constants_1.EventType.UPDATE_DOWNLOAD_PROGRESS, { ...data, executeType });
|
207
|
+
},
|
208
|
+
});
|
209
|
+
this.logger.info('downloadUpdateFile:Downloaded');
|
210
|
+
this.setState(constants_1.StateType.Downloaded);
|
211
|
+
}
|
212
|
+
catch (e) {
|
213
|
+
this.setState(constants_1.StateType.Idle);
|
214
|
+
e.customMessage = `${constants_1.InstallResultType.DownloadError}_${e.message}`;
|
215
|
+
this.logError(e);
|
216
|
+
}
|
217
|
+
}
|
218
|
+
async unzip() {
|
219
|
+
this.logger.info('unzip:start');
|
220
|
+
try {
|
221
|
+
const result = await this.doUnzip();
|
222
|
+
if (!result.success) {
|
223
|
+
return result;
|
224
|
+
}
|
225
|
+
const { latestAsarPath } = this.availableUpdate;
|
226
|
+
const exist = await (0, utils_1.existFile)(latestAsarPath);
|
227
|
+
return Promise.resolve({ success: exist });
|
228
|
+
}
|
229
|
+
catch (error) {
|
230
|
+
error.customMessage = constants_1.InstallResultType.UpdateUnzipError;
|
231
|
+
return Promise.resolve({
|
232
|
+
success: false,
|
233
|
+
error,
|
234
|
+
});
|
235
|
+
}
|
236
|
+
}
|
237
|
+
dispatchError(e) {
|
238
|
+
this.logError(e);
|
239
|
+
this.emit(constants_1.EventType.ERROR, e.message);
|
240
|
+
}
|
241
|
+
logError(e) {
|
242
|
+
const message = (e?.stack || e).toString();
|
243
|
+
this.logger.error(message);
|
244
|
+
}
|
245
|
+
}
|
246
|
+
exports.AppUpdator = AppUpdator;
|
247
|
+
//# sourceMappingURL=app-updator.js.map
|
@@ -0,0 +1,118 @@
|
|
1
|
+
"use strict";
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
3
|
+
exports.EventType = exports.DownloadProgressStatus = exports.ExecuteType = exports.UpdateType = exports.StateType = exports.InstallResultType = exports.OldArchivePrefix = void 0;
|
4
|
+
/**
|
5
|
+
* 更新包备份前缀
|
6
|
+
*/
|
7
|
+
exports.OldArchivePrefix = 'old-';
|
8
|
+
/**
|
9
|
+
* 安装结果类型
|
10
|
+
*/
|
11
|
+
var InstallResultType;
|
12
|
+
(function (InstallResultType) {
|
13
|
+
InstallResultType["UpdateUnzipError"] = "update-unzip-error";
|
14
|
+
InstallResultType["DownloadError"] = "download-error";
|
15
|
+
InstallResultType["UpdateSuccess"] = "update-success";
|
16
|
+
InstallResultType["CheckForUpdatesError"] = "check-for-updates-error";
|
17
|
+
})(InstallResultType = exports.InstallResultType || (exports.InstallResultType = {}));
|
18
|
+
/**
|
19
|
+
* 更新状态
|
20
|
+
*/
|
21
|
+
var StateType;
|
22
|
+
(function (StateType) {
|
23
|
+
/**
|
24
|
+
* 空闲
|
25
|
+
*/
|
26
|
+
StateType["Idle"] = "idle";
|
27
|
+
/**
|
28
|
+
* 检查更新中
|
29
|
+
*/
|
30
|
+
StateType["CheckingForUpdate"] = "checking-for-update";
|
31
|
+
/**
|
32
|
+
* 下载中
|
33
|
+
*/
|
34
|
+
StateType["Downloading"] = "downloading";
|
35
|
+
/**
|
36
|
+
* 下载完成
|
37
|
+
*/
|
38
|
+
StateType["Downloaded"] = "downloaded";
|
39
|
+
})(StateType = exports.StateType || (exports.StateType = {}));
|
40
|
+
/**
|
41
|
+
* 更新类型
|
42
|
+
*/
|
43
|
+
var UpdateType;
|
44
|
+
(function (UpdateType) {
|
45
|
+
/** 全量更新 */
|
46
|
+
UpdateType["Package"] = "package";
|
47
|
+
/** 动态更新 */
|
48
|
+
UpdateType["Asar"] = "asar";
|
49
|
+
/** 更新包更新 */
|
50
|
+
UpdateType["Assets"] = "assets";
|
51
|
+
})(UpdateType = exports.UpdateType || (exports.UpdateType = {}));
|
52
|
+
/**
|
53
|
+
* 执行类型
|
54
|
+
*/
|
55
|
+
var ExecuteType;
|
56
|
+
(function (ExecuteType) {
|
57
|
+
/**
|
58
|
+
* 自动触发
|
59
|
+
*/
|
60
|
+
ExecuteType["User"] = "user";
|
61
|
+
/**
|
62
|
+
* 手动触发
|
63
|
+
*/
|
64
|
+
ExecuteType["Auto"] = "auto";
|
65
|
+
})(ExecuteType = exports.ExecuteType || (exports.ExecuteType = {}));
|
66
|
+
/**
|
67
|
+
* 执行类型
|
68
|
+
*/
|
69
|
+
var DownloadProgressStatus;
|
70
|
+
(function (DownloadProgressStatus) {
|
71
|
+
/**
|
72
|
+
* 开始
|
73
|
+
*/
|
74
|
+
DownloadProgressStatus["Begin"] = "begin";
|
75
|
+
/**
|
76
|
+
* 下载中
|
77
|
+
*/
|
78
|
+
DownloadProgressStatus["Downloading"] = "downloading";
|
79
|
+
/**
|
80
|
+
* 结束
|
81
|
+
*/
|
82
|
+
DownloadProgressStatus["End"] = "end";
|
83
|
+
})(DownloadProgressStatus = exports.DownloadProgressStatus || (exports.DownloadProgressStatus = {}));
|
84
|
+
/**
|
85
|
+
* 事件类型
|
86
|
+
*/
|
87
|
+
var EventType;
|
88
|
+
(function (EventType) {
|
89
|
+
/**
|
90
|
+
* 下载完成
|
91
|
+
*/
|
92
|
+
EventType["UPDATE_DOWNLOADED"] = "update-downloaded";
|
93
|
+
/**
|
94
|
+
* 下载进度
|
95
|
+
*/
|
96
|
+
EventType["UPDATE_DOWNLOAD_PROGRESS"] = "update-download-progress";
|
97
|
+
/**
|
98
|
+
* 用户调用 quitAndInstall 之后发出,可在次事件后完成关闭窗口、重启等操作
|
99
|
+
*/
|
100
|
+
EventType["BEFORE_QUIT_FOR_UPDATE"] = "before-quit-for-update";
|
101
|
+
/**
|
102
|
+
* 有可用更新
|
103
|
+
*/
|
104
|
+
EventType["UPDATE_AVAILABLE"] = "update-available";
|
105
|
+
/**
|
106
|
+
* 开始检查更新
|
107
|
+
*/
|
108
|
+
EventType["CHECKING_FOR_UPDATE"] = "checking-for-update";
|
109
|
+
/**
|
110
|
+
* 无可用更新
|
111
|
+
*/
|
112
|
+
EventType["UPDATE_NOT_AVAILABLE"] = "update-not-available";
|
113
|
+
/**
|
114
|
+
* 错误
|
115
|
+
*/
|
116
|
+
EventType["ERROR"] = "error";
|
117
|
+
})(EventType = exports.EventType || (exports.EventType = {}));
|
118
|
+
//# sourceMappingURL=constants.js.map
|
@@ -0,0 +1,33 @@
|
|
1
|
+
"use strict";
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
3
|
+
exports.ElectronAppAdapter = void 0;
|
4
|
+
const electron_1 = require("electron");
|
5
|
+
class ElectronAppAdapter {
|
6
|
+
constructor(app = electron_1.app) {
|
7
|
+
this.app = app;
|
8
|
+
}
|
9
|
+
get name() {
|
10
|
+
return this.app.getName();
|
11
|
+
}
|
12
|
+
get isPackaged() {
|
13
|
+
return this.app.isPackaged === true;
|
14
|
+
}
|
15
|
+
get userDataPath() {
|
16
|
+
return this.app.getPath('userData');
|
17
|
+
}
|
18
|
+
get exePath() {
|
19
|
+
return this.app.getPath('exe');
|
20
|
+
}
|
21
|
+
exit(code) {
|
22
|
+
this.app.exit(code);
|
23
|
+
}
|
24
|
+
relaunch(code) {
|
25
|
+
this.app.relaunch();
|
26
|
+
this.app.exit(code);
|
27
|
+
}
|
28
|
+
quit() {
|
29
|
+
this.app.quit();
|
30
|
+
}
|
31
|
+
}
|
32
|
+
exports.ElectronAppAdapter = ElectronAppAdapter;
|
33
|
+
//# sourceMappingURL=elelctron-app-adapter.js.map
|
Binary file
|
Binary file
|
package/build/index.js
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
"use strict";
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
3
|
+
const constants_1 = require("./common/constants");
|
4
|
+
const mac_updator_1 = require("./mac-updator");
|
5
|
+
const windows_updator_1 = require("./windows-updator");
|
6
|
+
exports.default = {
|
7
|
+
UpdateType: constants_1.UpdateType,
|
8
|
+
EventType: constants_1.EventType,
|
9
|
+
ExecuteType: constants_1.ExecuteType,
|
10
|
+
StateType: constants_1.StateType,
|
11
|
+
MacUpdator: mac_updator_1.MacUpdator,
|
12
|
+
WindowsUpdator: windows_updator_1.WindowsUpdator,
|
13
|
+
DownloadProgressStatus: constants_1.DownloadProgressStatus,
|
14
|
+
};
|
15
|
+
//# sourceMappingURL=index.js.map
|
@@ -0,0 +1,108 @@
|
|
1
|
+
"use strict";
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
4
|
+
};
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
6
|
+
exports.MacUpdator = void 0;
|
7
|
+
const path_1 = __importDefault(require("path"));
|
8
|
+
const app_updator_1 = require("./app-updator");
|
9
|
+
const constants_1 = require("./common/constants");
|
10
|
+
const install_macos_dmg_1 = __importDefault(require("./utils/install-macos-dmg"));
|
11
|
+
const utils_1 = require("./utils");
|
12
|
+
class MacUpdator extends app_updator_1.AppUpdator {
|
13
|
+
doGetAvailableUpdateInfo(updateInfo) {
|
14
|
+
this.logger.info('ElectronUpdator#MacUpdator#doGetAvailableUpdateInfo:start');
|
15
|
+
const exePath = this.app.exePath;
|
16
|
+
const resourcePath = path_1.default.resolve(exePath, '..', '..', 'Resources');
|
17
|
+
const latestAsarPath = path_1.default.resolve(resourcePath, 'latest.asar');
|
18
|
+
const latestAppPath = path_1.default.resolve(resourcePath, 'latest');
|
19
|
+
let downloadTargetDir = `${latestAsarPath}.zip`;
|
20
|
+
if (updateInfo.updateType === constants_1.UpdateType.Package) {
|
21
|
+
downloadTargetDir = `${latestAppPath}.dmg`;
|
22
|
+
}
|
23
|
+
return {
|
24
|
+
resourcePath,
|
25
|
+
downloadTargetDir,
|
26
|
+
latestAsarPath,
|
27
|
+
};
|
28
|
+
}
|
29
|
+
async doPreCheckForPackage() {
|
30
|
+
this.logger.info('ElectronUpdator#MacUpdator#doPreCheckForPackage:start');
|
31
|
+
// Mac 全量安装前,先进行 dmg 安装检查
|
32
|
+
return await (0, install_macos_dmg_1.default)(this.options, this.logger, this.availableUpdate, this.updateInfo, true);
|
33
|
+
}
|
34
|
+
/**
|
35
|
+
* 资源解压
|
36
|
+
* @return
|
37
|
+
*/
|
38
|
+
async doUnzip() {
|
39
|
+
this.logger.info('MacUpdator#doUnzip:start');
|
40
|
+
const { resourcePath, downloadTargetDir } = this.availableUpdate;
|
41
|
+
try {
|
42
|
+
// 直接解压
|
43
|
+
await (0, utils_1.execAsync)(`unzip -o ${downloadTargetDir}`, {
|
44
|
+
cwd: resourcePath,
|
45
|
+
maxBuffer: 2 ** 28,
|
46
|
+
});
|
47
|
+
return {
|
48
|
+
success: true,
|
49
|
+
};
|
50
|
+
}
|
51
|
+
catch (error) {
|
52
|
+
return {
|
53
|
+
success: false,
|
54
|
+
error,
|
55
|
+
};
|
56
|
+
}
|
57
|
+
}
|
58
|
+
async doQuitAndInstallAsar() {
|
59
|
+
this.logger.info('ElectronUpdator#MacUpdator#doQuitAndInstallAsar:start');
|
60
|
+
if (!this.availableUpdate) {
|
61
|
+
this.logger.error('ElectronUpdator#MacUpdator#doQuitAndInstallAsar:not availableUpdate');
|
62
|
+
return Promise.resolve({ success: false });
|
63
|
+
}
|
64
|
+
const { resourcePath, latestAsarPath } = this.availableUpdate;
|
65
|
+
const oldAsarPath = path_1.default.resolve(resourcePath, `${constants_1.OldArchivePrefix}${new Date().getTime()}.asar`);
|
66
|
+
const currentAsarPath = path_1.default.resolve(resourcePath, 'app.asar');
|
67
|
+
try {
|
68
|
+
// 将老包改名 app.asar => old-xxxx.asar
|
69
|
+
if (await (0, utils_1.existsAsync)(currentAsarPath)) {
|
70
|
+
await (0, utils_1.renameAsync)(currentAsarPath, oldAsarPath);
|
71
|
+
}
|
72
|
+
}
|
73
|
+
catch (error) {
|
74
|
+
return {
|
75
|
+
success: false,
|
76
|
+
error,
|
77
|
+
};
|
78
|
+
}
|
79
|
+
try {
|
80
|
+
// 新包替换
|
81
|
+
await (0, utils_1.renameAsync)(latestAsarPath, currentAsarPath);
|
82
|
+
}
|
83
|
+
catch (error) {
|
84
|
+
// 替换出错,需要把老包还原
|
85
|
+
await (0, utils_1.renameAsync)(oldAsarPath, currentAsarPath);
|
86
|
+
return {
|
87
|
+
success: false,
|
88
|
+
error,
|
89
|
+
};
|
90
|
+
}
|
91
|
+
this.logger.warn('quitAndInstall:install success');
|
92
|
+
this.app.relaunch();
|
93
|
+
return {
|
94
|
+
success: true,
|
95
|
+
};
|
96
|
+
}
|
97
|
+
async doQuitAndInstallPackage() {
|
98
|
+
this.logger.info('ElectronUpdator#doQuitAndInstallPackage:start');
|
99
|
+
const result = await (0, install_macos_dmg_1.default)(this.options, this.logger, this.availableUpdate, this.updateInfo);
|
100
|
+
if (result.success) {
|
101
|
+
this.logger.warn('quitAndInstall:install success');
|
102
|
+
this.app.relaunch();
|
103
|
+
}
|
104
|
+
return result;
|
105
|
+
}
|
106
|
+
}
|
107
|
+
exports.MacUpdator = MacUpdator;
|
108
|
+
//# sourceMappingURL=mac-updator.js.map
|
@@ -0,0 +1,78 @@
|
|
1
|
+
"use strict";
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
4
|
+
};
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
6
|
+
exports.downloadFile = void 0;
|
7
|
+
const urllib_1 = __importDefault(require("urllib"));
|
8
|
+
const constants_1 = require("../common/constants");
|
9
|
+
const _1 = require(".");
|
10
|
+
/**
|
11
|
+
* 文件下载
|
12
|
+
* @param updator 更新实例
|
13
|
+
* @param url 下载地址
|
14
|
+
* @param signature 下载签名
|
15
|
+
* @param targeDir 下载目录
|
16
|
+
* @param progressHandle 下载状态回调
|
17
|
+
* @return
|
18
|
+
*/
|
19
|
+
const downloadFile = async ({ logger, url, signature, targetDir, progressHandle }) => {
|
20
|
+
logger.info('downloadFile#downloadFile (start)');
|
21
|
+
const writeStream = (0, _1.createWriteStream)(targetDir);
|
22
|
+
let currentLength = 0;
|
23
|
+
let currentProgress = 0;
|
24
|
+
progressHandle({
|
25
|
+
status: constants_1.DownloadProgressStatus.Begin,
|
26
|
+
});
|
27
|
+
return new Promise((resolve, reject) => {
|
28
|
+
urllib_1.default
|
29
|
+
.request(url, {
|
30
|
+
streaming: true,
|
31
|
+
followRedirect: true,
|
32
|
+
timeout: 10 * 60 * 1000,
|
33
|
+
})
|
34
|
+
.then((res) => {
|
35
|
+
const totalLength = res.headers['content-length'];
|
36
|
+
logger.info(`downloadFile#downloadFile (then),totalLength is ${totalLength}`);
|
37
|
+
res.res.on('data', (data) => {
|
38
|
+
try {
|
39
|
+
currentLength += data.length;
|
40
|
+
const progress = (currentLength / totalLength) * 100;
|
41
|
+
if (progress > currentProgress) {
|
42
|
+
currentProgress++;
|
43
|
+
}
|
44
|
+
progressHandle({
|
45
|
+
status: constants_1.DownloadProgressStatus.Downloading,
|
46
|
+
progress: currentProgress,
|
47
|
+
url,
|
48
|
+
signature,
|
49
|
+
data,
|
50
|
+
});
|
51
|
+
writeStream.write(data);
|
52
|
+
}
|
53
|
+
catch (e) {
|
54
|
+
reject(e);
|
55
|
+
}
|
56
|
+
});
|
57
|
+
res.res.on('end', () => {
|
58
|
+
// 推迟调用 end(): https://stackoverflow.com/a/53878933
|
59
|
+
process.nextTick(() => writeStream.end());
|
60
|
+
try {
|
61
|
+
progressHandle({
|
62
|
+
status: constants_1.DownloadProgressStatus.End,
|
63
|
+
url,
|
64
|
+
signature,
|
65
|
+
});
|
66
|
+
logger.info('downloadFile#download file success, url:%s, to %s', url, targetDir);
|
67
|
+
resolve();
|
68
|
+
}
|
69
|
+
catch (e) {
|
70
|
+
reject(e);
|
71
|
+
}
|
72
|
+
});
|
73
|
+
res.res.on('error', reject);
|
74
|
+
});
|
75
|
+
});
|
76
|
+
};
|
77
|
+
exports.downloadFile = downloadFile;
|
78
|
+
//# sourceMappingURL=download-file.js.map
|
@@ -0,0 +1,122 @@
|
|
1
|
+
"use strict";
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
4
|
+
};
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
6
|
+
exports.getExecuteFile = exports.requestUpdateInfo = exports.existFile = exports.cleanOldArchive = exports.spawnAsync = exports.waitUntil = exports.sleep = exports.getMacOSAppPath = exports.sudoPrompt = exports.existsSync = exports.createWriteStream = exports.isMac = exports.isWin = exports.execAsync = exports.rimrafAsync = exports.readdirAsync = exports.existsAsync = exports.renameAsync = void 0;
|
7
|
+
const path_1 = __importDefault(require("path"));
|
8
|
+
const util_1 = __importDefault(require("util"));
|
9
|
+
const urllib_1 = __importDefault(require("urllib"));
|
10
|
+
const sudo_prompt_alt_1 = __importDefault(require("sudo-prompt-alt"));
|
11
|
+
exports.sudoPrompt = sudo_prompt_alt_1.default;
|
12
|
+
const rimraf_alt_1 = __importDefault(require("rimraf-alt"));
|
13
|
+
const child_process_1 = require("child_process");
|
14
|
+
const original_fs_1 = require("original-fs");
|
15
|
+
Object.defineProperty(exports, "existsSync", { enumerable: true, get: function () { return original_fs_1.existsSync; } });
|
16
|
+
Object.defineProperty(exports, "createWriteStream", { enumerable: true, get: function () { return original_fs_1.createWriteStream; } });
|
17
|
+
const constants_1 = require("../common/constants");
|
18
|
+
exports.renameAsync = util_1.default.promisify(original_fs_1.rename);
|
19
|
+
exports.existsAsync = util_1.default.promisify(original_fs_1.exists);
|
20
|
+
exports.readdirAsync = util_1.default.promisify(original_fs_1.readdir);
|
21
|
+
exports.rimrafAsync = util_1.default.promisify(rimraf_alt_1.default);
|
22
|
+
exports.execAsync = util_1.default.promisify(child_process_1.exec);
|
23
|
+
exports.isWin = process.platform === 'win32';
|
24
|
+
exports.isMac = process.platform === 'darwin';
|
25
|
+
const getMacOSAppPath = () => {
|
26
|
+
const sep = path_1.default.sep;
|
27
|
+
const execPath = process.execPath;
|
28
|
+
const contentPath = ['', 'Contents', 'MacOS'].join(sep);
|
29
|
+
return execPath.split(contentPath)[0];
|
30
|
+
};
|
31
|
+
exports.getMacOSAppPath = getMacOSAppPath;
|
32
|
+
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
33
|
+
exports.sleep = sleep;
|
34
|
+
const waitUntil = async (handle, options = { retryTime: 10, ms: 1000 }) => {
|
35
|
+
let retryTime = 0;
|
36
|
+
const p = async () => {
|
37
|
+
const isOk = handle();
|
38
|
+
if (!isOk) {
|
39
|
+
if (retryTime === options.retryTime) {
|
40
|
+
return false;
|
41
|
+
}
|
42
|
+
retryTime++;
|
43
|
+
await (0, exports.sleep)(options.ms);
|
44
|
+
return await p();
|
45
|
+
}
|
46
|
+
return true;
|
47
|
+
};
|
48
|
+
return await p();
|
49
|
+
};
|
50
|
+
exports.waitUntil = waitUntil;
|
51
|
+
const spawnAsync = (command, options) => {
|
52
|
+
let stdout = '';
|
53
|
+
let stderr = '';
|
54
|
+
const child = (0, child_process_1.spawn)(command, options);
|
55
|
+
child.stdout.on('data', (data) => {
|
56
|
+
stdout += data;
|
57
|
+
});
|
58
|
+
child.stderr.on('data', (data) => {
|
59
|
+
stderr += data;
|
60
|
+
});
|
61
|
+
return new Promise((resolve, reject) => {
|
62
|
+
child.on('error', reject);
|
63
|
+
child.on('close', (code) => {
|
64
|
+
if (code === 0) {
|
65
|
+
return resolve(stdout.toString());
|
66
|
+
}
|
67
|
+
const err = new Error(`child exited with code ${code}`);
|
68
|
+
err.code = code;
|
69
|
+
err.stderr = stderr.toString();
|
70
|
+
reject(err);
|
71
|
+
});
|
72
|
+
});
|
73
|
+
};
|
74
|
+
exports.spawnAsync = spawnAsync;
|
75
|
+
const cleanOldArchive = async (resourcePath) => {
|
76
|
+
const resourceArray = await (0, exports.readdirAsync)(resourcePath);
|
77
|
+
if (!resourceArray || !resourceArray.length) {
|
78
|
+
return;
|
79
|
+
}
|
80
|
+
const oldArchiveSource = resourceArray.filter((item) => item.startsWith(constants_1.OldArchivePrefix));
|
81
|
+
return await Promise.all(oldArchiveSource.map(async (file) => {
|
82
|
+
const filePath = path_1.default.join(resourcePath, file);
|
83
|
+
if (await (0, exports.existsAsync)(filePath)) {
|
84
|
+
await (0, exports.rimrafAsync)(filePath);
|
85
|
+
}
|
86
|
+
}));
|
87
|
+
};
|
88
|
+
exports.cleanOldArchive = cleanOldArchive;
|
89
|
+
const existFile = async (path) => {
|
90
|
+
const _existFile = await (0, exports.waitUntil)(() => (0, original_fs_1.existsSync)(path), {
|
91
|
+
ms: 1000,
|
92
|
+
retryTime: 30,
|
93
|
+
});
|
94
|
+
return _existFile;
|
95
|
+
};
|
96
|
+
exports.existFile = existFile;
|
97
|
+
const requestUpdateInfo = async (options) => {
|
98
|
+
const { url } = options;
|
99
|
+
if (!url) {
|
100
|
+
throw new Error('request url can\'t be empty');
|
101
|
+
}
|
102
|
+
let res = null;
|
103
|
+
try {
|
104
|
+
res = await urllib_1.default.request(url, {
|
105
|
+
dataType: 'json',
|
106
|
+
timeout: 10 * 1000,
|
107
|
+
});
|
108
|
+
}
|
109
|
+
catch (e) {
|
110
|
+
throw e;
|
111
|
+
}
|
112
|
+
if (!res || res.status !== 200) {
|
113
|
+
throw new Error(`request failure,status is ${res.status}`);
|
114
|
+
}
|
115
|
+
return res.data;
|
116
|
+
};
|
117
|
+
exports.requestUpdateInfo = requestUpdateInfo;
|
118
|
+
const getExecuteFile = (helperExeFileDir, file) => {
|
119
|
+
return path_1.default.join(helperExeFileDir, file);
|
120
|
+
};
|
121
|
+
exports.getExecuteFile = getExecuteFile;
|
122
|
+
//# sourceMappingURL=index.js.map
|
@@ -0,0 +1,155 @@
|
|
1
|
+
"use strict";
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
4
|
+
};
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
6
|
+
const os_1 = __importDefault(require("os"));
|
7
|
+
const path_1 = __importDefault(require("path"));
|
8
|
+
const _1 = require(".");
|
9
|
+
const index_1 = require("./index");
|
10
|
+
const _log = (logger, e) => {
|
11
|
+
const message = (e.stack || e).toString();
|
12
|
+
logger.error?.(message);
|
13
|
+
};
|
14
|
+
exports.default = async (updatorOptions, logger, availableUpdate, updateInfo, preCheck = false) => {
|
15
|
+
const { productName } = updatorOptions || {};
|
16
|
+
const { downloadTargetDir } = availableUpdate || {};
|
17
|
+
const appPath = (0, _1.getMacOSAppPath)();
|
18
|
+
const tmpPath = path_1.default.join(os_1.default.tmpdir(), String(new Date().getTime())); // 本地临时文件使用的目录
|
19
|
+
const volumesPath = path_1.default.join('/Volumes', updatorOptions.dmgTitleFormatter ? updatorOptions.dmgTitleFormatter(updatorOptions, updateInfo) : productName);
|
20
|
+
const volumesAppPath = path_1.default.join(volumesPath, `${productName}.app`);
|
21
|
+
// step 1: 检测是否支持 hdiutil;
|
22
|
+
try {
|
23
|
+
const res = (await (0, index_1.spawnAsync)('which', ['hdiutil']));
|
24
|
+
if (!res.includes('/bin/hdiutil')) {
|
25
|
+
throw new Error('hdiutil not found');
|
26
|
+
}
|
27
|
+
}
|
28
|
+
catch (error) {
|
29
|
+
if (preCheck) {
|
30
|
+
_log(logger, error);
|
31
|
+
}
|
32
|
+
return {
|
33
|
+
success: false,
|
34
|
+
error,
|
35
|
+
};
|
36
|
+
}
|
37
|
+
// step 2: eject 一次 volume 下的 app,兜底,避免上一次结束时执行失败
|
38
|
+
try {
|
39
|
+
await (0, index_1.spawnAsync)('hdiutil', ['eject', volumesPath]);
|
40
|
+
}
|
41
|
+
catch (e) {
|
42
|
+
// 如果前一次更新时 eject 成功,这里文件不存在会报错,是个正常的 case,不用处理上报
|
43
|
+
}
|
44
|
+
finally {
|
45
|
+
const volumeAppNotExist = await (0, _1.waitUntil)(() => !(0, index_1.existsSync)(volumesAppPath), {
|
46
|
+
ms: 300,
|
47
|
+
retryTime: 5,
|
48
|
+
});
|
49
|
+
if (!volumeAppNotExist) {
|
50
|
+
const error = new Error('volume not exists');
|
51
|
+
if (preCheck) {
|
52
|
+
_log(logger, error);
|
53
|
+
}
|
54
|
+
// eslint-disable-next-line
|
55
|
+
return {
|
56
|
+
success: false,
|
57
|
+
error,
|
58
|
+
};
|
59
|
+
}
|
60
|
+
}
|
61
|
+
// step 3: 执行 hdiutil attach,挂载 dmg
|
62
|
+
try {
|
63
|
+
await (0, index_1.spawnAsync)('hdiutil', ['attach', downloadTargetDir]);
|
64
|
+
}
|
65
|
+
catch (error) {
|
66
|
+
_log(logger, error);
|
67
|
+
}
|
68
|
+
finally {
|
69
|
+
// 判断有没有新的 dmg 文件,没有的话提示用户重新下载
|
70
|
+
const volumeAppExist = await (0, _1.waitUntil)(() => (0, index_1.existsSync)(volumesAppPath), {
|
71
|
+
ms: 300,
|
72
|
+
retryTime: 5,
|
73
|
+
});
|
74
|
+
if (!volumeAppExist) {
|
75
|
+
const error = new Error('attach fail');
|
76
|
+
if (preCheck) {
|
77
|
+
_log(logger, error);
|
78
|
+
}
|
79
|
+
// eslint-disable-next-line
|
80
|
+
return {
|
81
|
+
success: false,
|
82
|
+
error,
|
83
|
+
};
|
84
|
+
}
|
85
|
+
}
|
86
|
+
if (preCheck) {
|
87
|
+
// 如果是预检查,直接返回
|
88
|
+
try {
|
89
|
+
await (0, index_1.spawnAsync)('hdiutil', ['eject', volumesPath]);
|
90
|
+
}
|
91
|
+
catch (error) {
|
92
|
+
_log(logger, error);
|
93
|
+
}
|
94
|
+
return {
|
95
|
+
success: true,
|
96
|
+
};
|
97
|
+
}
|
98
|
+
// step 4: 将当前目录下的 app 移到临时目录,如果后续操作失败了兜底用
|
99
|
+
try {
|
100
|
+
await (0, index_1.spawnAsync)('mv', [appPath, tmpPath]);
|
101
|
+
}
|
102
|
+
catch (error) {
|
103
|
+
error.customMessage = 'step4 mv to tmp path error';
|
104
|
+
_log(logger, error);
|
105
|
+
}
|
106
|
+
finally {
|
107
|
+
// 看临时目录文件是否移动成功
|
108
|
+
const tmpPathExist = await (0, _1.waitUntil)(() => (0, index_1.existsSync)(tmpPath), {
|
109
|
+
ms: 300,
|
110
|
+
retryTime: 5,
|
111
|
+
});
|
112
|
+
if (!tmpPathExist) {
|
113
|
+
const error = new Error('cp to tmp path fail');
|
114
|
+
// eslint-disable-next-line
|
115
|
+
return {
|
116
|
+
success: false,
|
117
|
+
error,
|
118
|
+
};
|
119
|
+
}
|
120
|
+
}
|
121
|
+
// step 5: 将新的 app 文件移动到 Applications 目录下,如果失败 or 查不到移入文件,将临时目录下的文件移动回来
|
122
|
+
try {
|
123
|
+
await (0, index_1.spawnAsync)('cp', ['-R', volumesAppPath, appPath]);
|
124
|
+
}
|
125
|
+
catch (error) {
|
126
|
+
_log(logger, error);
|
127
|
+
}
|
128
|
+
finally {
|
129
|
+
const appExist = await (0, _1.waitUntil)(() => (0, index_1.existsSync)(appPath), {
|
130
|
+
ms: 300,
|
131
|
+
retryTime: 5,
|
132
|
+
});
|
133
|
+
// 查不到新移入的 dmg,将原 dmg 再移动回来
|
134
|
+
if (!appExist) {
|
135
|
+
const error = new Error('cp to app fail');
|
136
|
+
await (0, index_1.spawnAsync)('mv', [tmpPath, appPath]);
|
137
|
+
// eslint-disable-next-line
|
138
|
+
return {
|
139
|
+
success: false,
|
140
|
+
error,
|
141
|
+
};
|
142
|
+
}
|
143
|
+
}
|
144
|
+
// step 6: 执行 hdiutil eject,推出
|
145
|
+
try {
|
146
|
+
await (0, index_1.spawnAsync)('hdiutil', ['eject', volumesPath]);
|
147
|
+
}
|
148
|
+
catch (error) {
|
149
|
+
_log(logger, error);
|
150
|
+
}
|
151
|
+
return {
|
152
|
+
success: true,
|
153
|
+
};
|
154
|
+
};
|
155
|
+
//# sourceMappingURL=install-macos-dmg.js.map
|
@@ -0,0 +1,23 @@
|
|
1
|
+
"use strict";
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
3
|
+
exports.sudoPromptExec = void 0;
|
4
|
+
const index_1 = require("./index");
|
5
|
+
function sudoPromptExec(logger, shell) {
|
6
|
+
const options = {
|
7
|
+
name: 'SoftwareUpdate',
|
8
|
+
};
|
9
|
+
return new Promise((resolve, reject) => {
|
10
|
+
logger.warn(`sudoPromptExec#_shell_${shell}`);
|
11
|
+
index_1.sudoPrompt.exec(shell, options, (error, stdout) => {
|
12
|
+
if (error) {
|
13
|
+
reject(error);
|
14
|
+
logger.error(`sudoPromptExec#error_${error}`);
|
15
|
+
return;
|
16
|
+
}
|
17
|
+
resolve(stdout);
|
18
|
+
logger.warn(`sudoPromptExec#stdout_${stdout}`);
|
19
|
+
});
|
20
|
+
});
|
21
|
+
}
|
22
|
+
exports.sudoPromptExec = sudoPromptExec;
|
23
|
+
//# sourceMappingURL=sudo-prompt-exec.js.map
|
@@ -0,0 +1,89 @@
|
|
1
|
+
"use strict";
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
4
|
+
};
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
6
|
+
exports.WindowsUpdator = void 0;
|
7
|
+
const path_1 = __importDefault(require("path"));
|
8
|
+
const electron_1 = require("electron");
|
9
|
+
const constants_1 = require("./common/constants");
|
10
|
+
const sudo_prompt_exec_1 = require("./utils/sudo-prompt-exec");
|
11
|
+
const app_updator_1 = require("./app-updator");
|
12
|
+
const utils_1 = require("./utils");
|
13
|
+
class WindowsUpdator extends app_updator_1.AppUpdator {
|
14
|
+
doGetAvailableUpdateInfo(updateInfo) {
|
15
|
+
this.logger.info('WindowsUpdator#doGetAvailableUpdateInfo:start');
|
16
|
+
const resourcePath = path_1.default.resolve(this.app.userDataPath);
|
17
|
+
const latestAsarPath = path_1.default.resolve(resourcePath, 'latest.asar');
|
18
|
+
const latestAppPath = path_1.default.resolve(resourcePath, 'latest');
|
19
|
+
let downloadTargetDir = `${latestAsarPath}.zip`;
|
20
|
+
if (updateInfo.updateType === constants_1.UpdateType.Package) {
|
21
|
+
downloadTargetDir = `${latestAppPath}.exe`;
|
22
|
+
}
|
23
|
+
return {
|
24
|
+
resourcePath,
|
25
|
+
downloadTargetDir,
|
26
|
+
latestAsarPath,
|
27
|
+
};
|
28
|
+
}
|
29
|
+
async doPreCheckForPackage() {
|
30
|
+
this.logger.info('WindowsUpdator#doPreCheckForPackage:start');
|
31
|
+
// Windows 全量安装默认预检正常
|
32
|
+
return Promise.resolve({ success: true });
|
33
|
+
}
|
34
|
+
async doUnzip() {
|
35
|
+
this.logger.info('WindowsUpdator#doUnzip:start');
|
36
|
+
try {
|
37
|
+
const { downloadTargetDir, resourcePath } = this.availableUpdate;
|
38
|
+
const unzipExe = (0, utils_1.getExecuteFile)(this._windowHelperExeDir, 'unzip.exe');
|
39
|
+
const executeCommand = `"${unzipExe}" -o "${downloadTargetDir}" -d "${resourcePath}"`;
|
40
|
+
await (0, utils_1.execAsync)(executeCommand);
|
41
|
+
}
|
42
|
+
catch (error) {
|
43
|
+
return {
|
44
|
+
success: false,
|
45
|
+
error,
|
46
|
+
};
|
47
|
+
}
|
48
|
+
this.logger.info('WindowsUpdator#doUnzip:success');
|
49
|
+
return {
|
50
|
+
success: true,
|
51
|
+
};
|
52
|
+
}
|
53
|
+
async doQuitAndInstallPackage() {
|
54
|
+
this.logger.info('WindowsUpdator#doQuitAndInstallPackage:success');
|
55
|
+
const { downloadTargetDir } = this.availableUpdate;
|
56
|
+
try {
|
57
|
+
// Windows 全量安装
|
58
|
+
const shellOpen = electron_1.shell.openItem || electron_1.shell.openPath;
|
59
|
+
shellOpen(downloadTargetDir);
|
60
|
+
setTimeout(() => {
|
61
|
+
this.app.exit(0);
|
62
|
+
}, 30);
|
63
|
+
this.logger.warn('WindowsUpdator#quitAndInstall:install success');
|
64
|
+
return Promise.resolve({ success: true });
|
65
|
+
}
|
66
|
+
catch (error) {
|
67
|
+
return Promise.resolve({ success: false, error });
|
68
|
+
}
|
69
|
+
}
|
70
|
+
async doQuitAndInstallAsar() {
|
71
|
+
this.logger.info('WindowsUpdator#doQuitAndInstallAsar:start');
|
72
|
+
const productName = this.options?.productName;
|
73
|
+
const { resourcePath } = this.availableUpdate;
|
74
|
+
const exePath = this.app.exePath;
|
75
|
+
const updateExePath = (0, utils_1.getExecuteFile)(this._windowHelperExeDir, 'installer.exe');
|
76
|
+
const targetPath = path_1.default.resolve(exePath, '..', 'resources');
|
77
|
+
const executeCommand = `"${updateExePath}" "${targetPath}" "${resourcePath}" "${productName}.exe" "${exePath}"`;
|
78
|
+
try {
|
79
|
+
await (0, sudo_prompt_exec_1.sudoPromptExec)(this.logger, executeCommand);
|
80
|
+
this.logger.warn('WindowsUpdator#quitAndInstall:install success');
|
81
|
+
return Promise.resolve({ success: true });
|
82
|
+
}
|
83
|
+
catch (error) {
|
84
|
+
return Promise.resolve({ success: false, error });
|
85
|
+
}
|
86
|
+
}
|
87
|
+
}
|
88
|
+
exports.WindowsUpdator = WindowsUpdator;
|
89
|
+
//# sourceMappingURL=windows-updator.js.map
|
package/package.json
ADDED
@@ -0,0 +1,73 @@
|
|
1
|
+
{
|
2
|
+
"name": "graceful-updater",
|
3
|
+
"version": "1.0.5",
|
4
|
+
"description": "graceful-updater is a software updator management solution for Electron applications, It is convenient to complete full software update and dynamic update.",
|
5
|
+
"scripts": {
|
6
|
+
"build": "sh ./build.sh",
|
7
|
+
"test": "egg-bin test test/**/index.test.ts",
|
8
|
+
"lint": "eslint --fix --quiet --ext .js,.ts ./src",
|
9
|
+
"prepublishOnly": "npm run build",
|
10
|
+
"contributor": "git-contributor",
|
11
|
+
"dev": "ttsc -p tsconfig.json -watch"
|
12
|
+
},
|
13
|
+
"main": "./build",
|
14
|
+
"keywords": [
|
15
|
+
"electron",
|
16
|
+
"updator",
|
17
|
+
"auto-updator"
|
18
|
+
],
|
19
|
+
"files": [
|
20
|
+
"build/**/*.js",
|
21
|
+
"build/**/*.exe"
|
22
|
+
],
|
23
|
+
"repository": {
|
24
|
+
"type": "git",
|
25
|
+
"url": "https://github.com/electron-modules/graceful-updater"
|
26
|
+
},
|
27
|
+
"dependencies": {
|
28
|
+
"eventemitter3": "^4.0.0",
|
29
|
+
"lodash": "4",
|
30
|
+
"moment": "2",
|
31
|
+
"nanoid": "^3.3.4",
|
32
|
+
"rimraf-alt": "*",
|
33
|
+
"sudo-prompt-alt": "9",
|
34
|
+
"urllib": "2"
|
35
|
+
},
|
36
|
+
"devDependencies": {
|
37
|
+
"@applint/spec": "^1.2.0",
|
38
|
+
"@types/lodash": "^4.14.181",
|
39
|
+
"@types/mocha": "^10.0.1",
|
40
|
+
"@types/mock-require": "^2.0.1",
|
41
|
+
"@types/react-dom": "^17.0.16",
|
42
|
+
"@typescript-eslint/parser": "^5.19.0",
|
43
|
+
"babel-plugin-module-resolver": "^4.1.0",
|
44
|
+
"concurrently": "^5.3.0",
|
45
|
+
"copyfiles": "^2.4.1",
|
46
|
+
"cross-env": "^7.0.3",
|
47
|
+
"detect-port": "1",
|
48
|
+
"egg-bin": "^5.9.0",
|
49
|
+
"electron": "18",
|
50
|
+
"electron-windows": "18",
|
51
|
+
"eslint": "^7.32.0",
|
52
|
+
"eslint-config-egg": "^12.1.0",
|
53
|
+
"eslint-plugin-no-only-tests": "^3.1.0",
|
54
|
+
"git-contributor": "*",
|
55
|
+
"husky": "^7.0.4",
|
56
|
+
"mm": "^3.0.2",
|
57
|
+
"mock-require": "^3.0.3",
|
58
|
+
"prettier": "^2.6.2",
|
59
|
+
"prop-types": "^15.7.2",
|
60
|
+
"style-loader": "^1.1.2",
|
61
|
+
"ts-loader": "^9.2.8",
|
62
|
+
"ts-node": "^10.7.0",
|
63
|
+
"ttypescript": "^1.5.15",
|
64
|
+
"typescript": "^4.6.3",
|
65
|
+
"typescript-transform-paths": "^3.3.1"
|
66
|
+
},
|
67
|
+
"husky": {
|
68
|
+
"hooks": {
|
69
|
+
"pre-commit": "npm run lint"
|
70
|
+
}
|
71
|
+
},
|
72
|
+
"license": "MIT"
|
73
|
+
}
|