UNPKG

8.12 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 _showCliUpdateM = () => {
62 const updateMsg = updateData['cli_update_msg'];
63 if (updateMsg) {
64 const { version, latestVersion } = updateMsg;
65 ctx.logger.info(
66 `@feflow/cil has been updated from ${version} to ${latestVersion}. Enjoy it.`
67 );
68 updateData['cli_update_msg'] = '';
69 }
70 };
71
72 const _showPluginsUpdateM = () => {
73 const updatePkg = updateData['plugins_update_msg'];
74 if (updatePkg) {
75 updatePkg.forEach((pkg: any) => {
76 const { name, localVersion, latestVersion } = pkg;
77 table.cell('Name', name);
78 table.cell(
79 'Version',
80 localVersion === latestVersion
81 ? localVersion
82 : localVersion + ' -> ' + latestVersion
83 );
84 table.cell('Tag', 'latest');
85 table.cell('Update', localVersion === latestVersion ? 'N' : 'Y');
86 table.newRow();
87 });
88
89 ctx.logger.info(
90 'Your local templates or plugins has been updated last time.'
91 );
92 if (!isSilent) console.log(table.toString());
93
94 updateData['plugins_update_msg'] = '';
95 }
96 };
97
98 const _showUniversalPluginsM = () => {
99 const updatePkg = updateData['universal_plugins_update_msg'];
100
101 if (updatePkg) {
102 updatePkg.forEach((pkg: any) => {
103 const { name, localVersion, latestVersion } = pkg;
104 uTable.cell('Name', name);
105 uTable.cell(
106 'Version',
107 localVersion === latestVersion
108 ? localVersion
109 : localVersion + ' -> ' + latestVersion
110 );
111 uTable.cell('Tag', 'latest');
112 uTable.cell('Update', localVersion === latestVersion ? 'N' : 'Y');
113 uTable.newRow();
114 });
115
116 ctx.logger.info(
117 'Your local universal plugins has been updated last time.'
118 );
119 if (!isSilent) console.log(uTable.toString());
120
121 updateData['universal_plugins_update_msg'] = '';
122 }
123 };
124
125 // cli -> tnpm -> universal
126 _showCliUpdateM();
127 _showPluginsUpdateM();
128 _showUniversalPluginsM();
129
130 await db.update('update_data', updateData);
131}
132
133async function _checkLock(updateData: any) {
134 const updateLock = updateData?.['update_lock'];
135 const nowTime = new Date().getTime();
136 if (
137 updateLock &&
138 updateLock['time'] &&
139 nowTime - updateLock['time'] < CHECK_UPDATE_GAP
140 ) {
141 return true;
142 } else {
143 updateData['update_lock'] = {
144 time: String(nowTime),
145 pid: process.pid
146 };
147 await db.update('update_data', updateData);
148
149 // Optimistic Concurrency Control
150 let nowUpdateData = await db.read('update_data');
151 nowUpdateData = nowUpdateData?.['value'];
152 const nowUpdateLock = nowUpdateData?.['update_lock'];
153 if (nowUpdateLock && nowUpdateLock['pid'] !== process.pid) {
154 return true;
155 }
156 }
157 return false;
158}
159
160export async function checkUpdate(ctx: any) {
161 const dbFile = path.join(ctx.root, UPDATE_COLLECTION);
162 const autoUpdate =
163 ctx.args['auto-update'] || String(ctx.config.autoUpdate) === 'true';
164 const nowTime = new Date().getTime();
165 let latestVersion: any = '';
166 let cacheValidate = false;
167
168 if (!db) {
169 db = new DBInstance(dbFile);
170 }
171
172 const heartDBFile = path.join(ctx.root, HEART_BEAT_COLLECTION);
173 if (!heartDB) {
174 heartDB = new DBInstance(heartDBFile);
175 }
176
177 let updateData = await db.read('update_data');
178 updateData = updateData?.['value'];
179 if (updateData) {
180 // add lock to keep only one updating process is running
181 const isLocked = await _checkLock(updateData);
182 if (isLocked) return ctx.logger.debug('one updating process is running');
183
184 await _checkUpdateMsg(ctx, updateData);
185
186 const data = await heartDB.read('beat_time');
187 if (data) {
188 const lastBeatTime = parseInt(data['value'], 10);
189
190 cacheValidate = nowTime - lastBeatTime <= BEAT_GAP;
191 ctx.logger.debug(`heart-beat process cache validate ${cacheValidate}`);
192 // 子进程心跳停止了
193 if (!cacheValidate) {
194 // todo:进程检测,清理一下僵死的进程(兼容不同系统)
195 startUpdateBeat(ctx);
196 }
197 // 即便 心跳 停止了,latest_cli_version 也应该是之前检测到的最新值
198 latestVersion = updateData['latest_cli_version'];
199 }
200 } else {
201 // init
202 ctx.logger.debug('init heart-beat for update detective');
203 await Promise.all([
204 // 初始化心跳数据
205 heartDB.create('beat_time', String(nowTime)),
206 db.create('update_data', {
207 // 初始化自动更新任务数据
208 latest_cli_version: '',
209 latest_plugins: '',
210 latest_universal_plugins: '',
211 // 初始化更新信息
212 cli_update_msg: '',
213 plugins_update_msg: '',
214 universal_plugins_update_msg: '',
215 // 初始化更新锁,保持只有一个进程在更新
216 update_lock: {
217 time: String(nowTime),
218 pid: process.pid
219 }
220 })
221 ]);
222 startUpdateBeat(ctx);
223 }
224
225 // 开启更新时
226 if (!disableCheck && latestVersion && semver.gt(latestVersion, ctx.version)) {
227 ctx.logger.debug(
228 `Find new version, current version: ${ctx.version}, latest version: ${latestVersion}`
229 );
230 if (autoUpdate) {
231 ctx.logger.debug(
232 `Feflow will auto update version from ${ctx.version} to ${latestVersion}.`
233 );
234 ctx.logger.debug('Update message will be shown next time.');
235 return startUpdate(ctx, cacheValidate, latestVersion);
236 }
237
238 const askIfUpdateCli = [
239 {
240 type: 'confirm',
241 name: 'ifUpdate',
242 message: `${chalk.yellow(
243 `@feflow/cli's latest version is ${chalk.green(
244 `${latestVersion}`
245 )}, but your current version is ${chalk.red(
246 `${ctx.version}`
247 )}. Do you want to update it?`
248 )}`,
249 default: true
250 }
251 ];
252 const answer = await inquirer.prompt(askIfUpdateCli);
253 if (answer.ifUpdate) {
254 ctx.logger.debug(
255 `Feflow will update from version ${ctx.version} to ${latestVersion}.`
256 );
257 ctx.logger.debug('Update message will be shown next time.');
258 return startUpdate(ctx, cacheValidate, latestVersion);
259 } else {
260 safeDump(
261 {
262 ...ctx.config,
263 lastUpdateCheck: +new Date()
264 },
265 ctx.configPath
266 );
267 }
268 } else {
269 ctx.logger.debug(`Current cli version is already latest.`);
270 return startUpdate(ctx, cacheValidate, '');
271 }
272}