graceful-updater 1.0.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|
+
}
|