UNPKG

9.08 kBPlain TextView Raw
1/* eslint-disable @typescript-eslint/camelcase */
2import path from 'path';
3import { spawn } from 'child_process';
4import chalk from 'chalk';
5import inquirer from 'inquirer';
6import semver from 'semver';
7import Table from 'easy-table';
8import DBInstance from '../../shared/db';
9import {
10 HEART_BEAT_COLLECTION,
11 UPDATE_COLLECTION,
12 BEAT_GAP,
13 CHECK_UPDATE_GAP
14} from '../../shared/constant';
15import { safeDump } from '../../shared/yaml';
16
17const updateBeatProcess = path.join(__dirname, './updateBeat');
18const updateProcess = path.join(__dirname, './update');
19const isSilent = process.argv.slice(3).includes('--slient');
20const disableCheck = process.argv.slice(3).includes('--disable-check');
21let db: DBInstance;
22let heartDB: DBInstance;
23const table = new Table();
24const uTable = new Table();
25
26function startUpdateBeat(ctx: any) {
27 const child = spawn(process.argv[0], [updateBeatProcess], {
28 detached: true, // 使子进程在父进程退出后继续运行
29 stdio: 'ignore', // 保持后台运行
30 env: {
31 ...process.env, // env 无法把 ctx 传进去,会自动 string 化
32 debug: ctx.args.debug,
33 silent: ctx.args.silent
34 },
35 windowsHide: true
36 });
37
38 // 父进程不会等待子进程
39 child.unref();
40}
41
42function startUpdate(ctx: any, cacheValidate: any, latestVersion: any) {
43 const child = spawn(process.argv[0], [updateProcess], {
44 detached: true,
45 stdio: 'ignore',
46 env: {
47 ...process.env,
48 debug: ctx.args.debug,
49 silent: ctx.args.silent,
50 cacheValidate,
51 latestVersion
52 },
53 windowsHide: true
54 });
55
56 // 父进程不会等待子进程
57 child.unref();
58}
59
60async function _checkUpdateMsg(ctx: any, updateData: any = {}) {
61 const updateError = await db.read('update_error');
62 const exception = await db.read('exception');
63
64 const _showCliUpdateM = () => {
65 const updateMsg = updateData['cli_update_msg'];
66 if (updateMsg) {
67 const { version, latestVersion } = updateMsg;
68 ctx.logger.info(
69 `@feflow/cil has been updated from ${version} to ${latestVersion}. Enjoy it.`
70 );
71 updateData['cli_update_msg'] = '';
72 }
73 };
74
75 const _showPluginsUpdateM = () => {
76 const updatePkg = updateData['plugins_update_msg'];
77 if (updatePkg) {
78 updatePkg.forEach((pkg: any) => {
79 const { name, localVersion, latestVersion } = pkg;
80 table.cell('Name', name);
81 table.cell(
82 'Version',
83 localVersion === latestVersion
84 ? localVersion
85 : localVersion + ' -> ' + latestVersion
86 );
87 table.cell('Tag', 'latest');
88 table.cell('Update', localVersion === latestVersion ? 'N' : 'Y');
89 table.newRow();
90 });
91
92 ctx.logger.info(
93 'Your local templates or plugins has been updated last time.'
94 );
95 if (!isSilent) console.log(table.toString());
96
97 updateData['plugins_update_msg'] = '';
98 }
99 };
100
101 const _showUniversalPluginsM = () => {
102 const updatePkg = updateData['universal_plugins_update_msg'];
103
104 if (updatePkg) {
105 updatePkg.forEach((pkg: any) => {
106 const { name, localVersion, latestVersion } = pkg;
107 uTable.cell('Name', name);
108 uTable.cell(
109 'Version',
110 localVersion === latestVersion
111 ? localVersion
112 : localVersion + ' -> ' + latestVersion
113 );
114 uTable.cell('Tag', 'latest');
115 uTable.cell('Update', localVersion === latestVersion ? 'N' : 'Y');
116 uTable.newRow();
117 });
118
119 ctx.logger.info(
120 'Your local universal plugins has been updated last time.'
121 );
122 if (!isSilent) console.log(uTable.toString());
123
124 updateData['universal_plugins_update_msg'] = '';
125 }
126 };
127
128 const _showErrorM = () => {
129 const errorMsg = updateError?.['value'] || {};
130 const errorKeys = Object.keys(errorMsg);
131
132 if (errorKeys.length) {
133 ctx.logger.warn('Some problems occurred while auto-updating');
134 errorKeys.forEach(key => {
135 ctx.logger.error(`${key}: ${errorMsg[key]}`);
136 });
137 ctx.logger.warn(
138 'These templates or plugins need to be updated manually util problems fixed'
139 );
140 }
141
142 const exceptionMsg = exception?.['value'];
143 if (exceptionMsg) {
144 ctx.logger.error('Excetion exists in auto-updating:');
145 ctx.logger.error(exceptionMsg);
146 ctx.logger.warn('Auto-updating will not work util exception fixed');
147 }
148 };
149
150 // cli -> tnpm -> universal
151 _showCliUpdateM();
152 _showPluginsUpdateM();
153 _showUniversalPluginsM();
154 _showErrorM();
155
156 await db.update('update_data', updateData);
157 await db.update('update_error', '');
158 await db.update('exception', '');
159}
160
161async function _checkLock(updateData: any) {
162 const updateLock = updateData?.['update_lock'];
163 const nowTime = new Date().getTime();
164 if (
165 updateLock &&
166 updateLock['time'] &&
167 nowTime - updateLock['time'] < CHECK_UPDATE_GAP
168 ) {
169 return true;
170 } else {
171 updateData['update_lock'] = {
172 time: String(nowTime),
173 pid: process.pid
174 };
175 await db.update('update_data', updateData);
176
177 // Optimistic Concurrency Control
178 let nowUpdateData = await db.read('update_data');
179 nowUpdateData = nowUpdateData?.['value'];
180 const nowUpdateLock = nowUpdateData?.['update_lock'];
181 if (nowUpdateLock && nowUpdateLock['pid'] !== process.pid) {
182 return true;
183 }
184 }
185 return false;
186}
187
188export async function checkUpdate(ctx: any) {
189 const dbFile = path.join(ctx.root, UPDATE_COLLECTION);
190 const autoUpdate =
191 ctx.args['auto-update'] || String(ctx.config.autoUpdate) === 'true';
192 const nowTime = new Date().getTime();
193 let latestVersion: any = '';
194 let cacheValidate = false;
195
196 if (!db) {
197 db = new DBInstance(dbFile);
198 }
199
200 const heartDBFile = path.join(ctx.root, HEART_BEAT_COLLECTION);
201 if (!heartDB) {
202 heartDB = new DBInstance(heartDBFile);
203 }
204
205 let updateData = await db.read('update_data');
206 updateData = updateData?.['value'];
207 if (updateData) {
208 // add lock to keep only one updating process is running
209 const isLocked = await _checkLock(updateData);
210 if (isLocked) return ctx.logger.debug('one updating process is running');
211
212 await _checkUpdateMsg(ctx, updateData);
213
214 const data = await heartDB.read('beat_time');
215 if (data) {
216 const lastBeatTime = parseInt(data['value'], 10);
217
218 cacheValidate = nowTime - lastBeatTime <= BEAT_GAP;
219 ctx.logger.debug(`heart-beat process cache validate ${cacheValidate}`);
220 // 子进程心跳停止了
221 if (!cacheValidate) {
222 // todo:进程检测,清理一下僵死的进程(兼容不同系统)
223 startUpdateBeat(ctx);
224 }
225 // 即便 心跳 停止了,latest_cli_version 也应该是之前检测到的最新值
226 latestVersion = updateData['latest_cli_version'];
227 }
228 } else {
229 // init
230 ctx.logger.debug('init heart-beat for update detective');
231 await Promise.all([
232 // 初始化心跳数据
233 heartDB.create('beat_time', String(nowTime)),
234 db.create('update_data', {
235 // 初始化自动更新任务数据
236 latest_cli_version: '',
237 latest_plugins: '',
238 latest_universal_plugins: '',
239 // 初始化更新信息
240 cli_update_msg: '',
241 plugins_update_msg: '',
242 universal_plugins_update_msg: '',
243 // 初始化更新锁,保持只有一个进程在更新
244 update_lock: {
245 time: String(nowTime),
246 pid: process.pid
247 }
248 }),
249 db.create('update_error', ''),
250 db.create('exception', '')
251 ]);
252 startUpdateBeat(ctx);
253 }
254
255 // 开启更新时
256 if (!disableCheck && latestVersion && semver.gt(latestVersion, ctx.version)) {
257 ctx.logger.debug(
258 `Find new version, current version: ${ctx.version}, latest version: ${latestVersion}`
259 );
260 if (autoUpdate) {
261 ctx.logger.debug(
262 `Feflow will auto update version from ${ctx.version} to ${latestVersion}.`
263 );
264 ctx.logger.debug('Update message will be shown next time.');
265 return startUpdate(ctx, cacheValidate, latestVersion);
266 }
267
268 const askIfUpdateCli = [
269 {
270 type: 'confirm',
271 name: 'ifUpdate',
272 message: `${chalk.yellow(
273 `@feflow/cli's latest version is ${chalk.green(
274 `${latestVersion}`
275 )}, but your current version is ${chalk.red(
276 `${ctx.version}`
277 )}. Do you want to update it?`
278 )}`,
279 default: true
280 }
281 ];
282 const answer = await inquirer.prompt(askIfUpdateCli);
283 if (answer.ifUpdate) {
284 ctx.logger.debug(
285 `Feflow will update from version ${ctx.version} to ${latestVersion}.`
286 );
287 ctx.logger.debug('Update message will be shown next time.');
288 return startUpdate(ctx, cacheValidate, latestVersion);
289 } else {
290 safeDump(
291 {
292 ...ctx.config,
293 lastUpdateCheck: +new Date()
294 },
295 ctx.configPath
296 );
297 }
298 } else {
299 ctx.logger.debug(`Current cli version is already latest.`);
300 return startUpdate(ctx, cacheValidate, '');
301 }
302}