UNPKG

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