@lazycatcloud/lzc-cli 1.1.12 → 1.2.0
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/lib/app/index.js +52 -33
- package/lib/app/lpk_build.js +1 -5
- package/lib/app/lpk_create.js +9 -3
- package/lib/app/lpk_debug_bridge.js +101 -0
- package/lib/app/lpk_devshell.js +59 -129
- package/lib/app/lpk_installer.js +43 -50
- package/lib/appstore/publish.js +18 -8
- package/lib/box/index.js +2 -86
- package/lib/env.js +18 -178
- package/lib/lzc_sdk.js +75 -17
- package/lib/shellapi.js +95 -0
- package/lib/shellapi.proto +301 -0
- package/lib/utils.js +7 -103
- package/package.json +3 -21
- package/scripts/cli.js +6 -6
- package/template/_lpk/busybox-1.35.0 +0 -0
- package/template/_lpk/exec.sh +1 -10
- package/template/_lpk/init_debug_bridge.sh +24 -0
- package/template/_lpk/vue.lzc-build.yml.in +0 -16
- package/template/golang/lzc-build.yml +0 -15
- package/template/ionic_vue3/lzc-build.yml +0 -16
- package/template/ionic_vue3/package-lock.json +8100 -0
- package/template/lite/lzc-build.yml +0 -28
- package/lib/api.js +0 -155
- package/lib/app/lpk_devshell_docker.js +0 -211
- package/lib/app/lpk_log.js +0 -68
- package/lib/app/lpk_status.js +0 -18
- package/lib/app/lpk_uninstall.js +0 -19
- package/lib/box/api/clientapi.js +0 -1322
- package/lib/box/api/empty.js +0 -35
- package/lib/box/check_qemu.js +0 -51
- package/lib/box/qemu_vm_mgr.js +0 -608
- package/lib/box/schemes/vm_box_system_debian.json +0 -47
- package/lib/docker/promise.js +0 -91
- package/lib/docker-compose.js +0 -50
- package/lib/git/git-commit.sh +0 -7
- package/lib/git/git-reset.sh +0 -15
- package/lib/key.js +0 -102
- package/lib/sdk.js +0 -135
package/lib/box/api/empty.js
DELETED
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
/* eslint-disable */
|
|
2
|
-
import _m0 from "protobufjs/minimal.js";
|
|
3
|
-
function createBaseEmpty() {
|
|
4
|
-
return {};
|
|
5
|
-
}
|
|
6
|
-
export const Empty = {
|
|
7
|
-
encode(_, writer = _m0.Writer.create()) {
|
|
8
|
-
return writer;
|
|
9
|
-
},
|
|
10
|
-
decode(input, length) {
|
|
11
|
-
const reader = input instanceof _m0.Reader ? input : new _m0.Reader(input);
|
|
12
|
-
let end = length === undefined ? reader.len : reader.pos + length;
|
|
13
|
-
const message = createBaseEmpty();
|
|
14
|
-
while (reader.pos < end) {
|
|
15
|
-
const tag = reader.uint32();
|
|
16
|
-
switch (tag >>> 3) {
|
|
17
|
-
default:
|
|
18
|
-
reader.skipType(tag & 7);
|
|
19
|
-
break;
|
|
20
|
-
}
|
|
21
|
-
}
|
|
22
|
-
return message;
|
|
23
|
-
},
|
|
24
|
-
fromJSON(_) {
|
|
25
|
-
return {};
|
|
26
|
-
},
|
|
27
|
-
toJSON(_) {
|
|
28
|
-
const obj = {};
|
|
29
|
-
return obj;
|
|
30
|
-
},
|
|
31
|
-
fromPartial(_) {
|
|
32
|
-
const message = createBaseEmpty();
|
|
33
|
-
return message;
|
|
34
|
-
},
|
|
35
|
-
};
|
package/lib/box/check_qemu.js
DELETED
|
@@ -1,51 +0,0 @@
|
|
|
1
|
-
// 检测当前系统是否安装 qemu 软件
|
|
2
|
-
import commandExist from "command-exists";
|
|
3
|
-
import chalk from "chalk";
|
|
4
|
-
import os from "node:os";
|
|
5
|
-
|
|
6
|
-
function linuxPlatform(pkg) {
|
|
7
|
-
let r = os.release();
|
|
8
|
-
if (r.search(/arch/gi) > -1) {
|
|
9
|
-
return `sudo pacman -S ${pkg}`;
|
|
10
|
-
} else if (r.search(/debian/gi) > -1) {
|
|
11
|
-
return `sudo apt install ${pkg}`;
|
|
12
|
-
} else {
|
|
13
|
-
return ``;
|
|
14
|
-
}
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
function platformPackage(pkg) {
|
|
18
|
-
let cmd = "";
|
|
19
|
-
switch (os.platform()) {
|
|
20
|
-
case "darwin":
|
|
21
|
-
cmd = `brew install ${pkg}`;
|
|
22
|
-
break;
|
|
23
|
-
case "linux":
|
|
24
|
-
cmd = linuxPlatform(pkg);
|
|
25
|
-
break;
|
|
26
|
-
}
|
|
27
|
-
return `${pkg}包没有发现,请先通过系统包管理器安装。\n${cmd}`;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
export class CheckQemu {
|
|
31
|
-
constructor() {
|
|
32
|
-
this.commands = ["qemu-img", "qemu-system-x86_64"];
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
async init() {
|
|
36
|
-
for (let cmd of this.commands) {
|
|
37
|
-
if (!commandExist.sync(cmd)) {
|
|
38
|
-
let cmdErr = chalk.red(`${cmd} 命令没有发现`);
|
|
39
|
-
let pkgTips = chalk.blue(platformPackage("qemu"));
|
|
40
|
-
let tips = `${cmdErr}
|
|
41
|
-
|
|
42
|
-
${pkgTips}
|
|
43
|
-
|
|
44
|
-
查看更多信息 https://www.qemu.org/download/
|
|
45
|
-
`;
|
|
46
|
-
throw tips;
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
return this;
|
|
50
|
-
}
|
|
51
|
-
}
|
package/lib/box/qemu_vm_mgr.js
DELETED
|
@@ -1,608 +0,0 @@
|
|
|
1
|
-
#!/bin/node
|
|
2
|
-
import fs from "node:fs";
|
|
3
|
-
import { spawn, spawnSync } from "node:child_process";
|
|
4
|
-
import path from "node:path";
|
|
5
|
-
import inquirer from "inquirer";
|
|
6
|
-
import process from "node:process";
|
|
7
|
-
import net from "node:net";
|
|
8
|
-
import fetch from "node-fetch";
|
|
9
|
-
import os from "node:os";
|
|
10
|
-
import logger from "loglevel";
|
|
11
|
-
import glob from "fast-glob";
|
|
12
|
-
import env from "../env.js";
|
|
13
|
-
|
|
14
|
-
import { Downloader, envsubstrDefault, contextDirname } from "../utils.js";
|
|
15
|
-
import { ShellCoreClientImpl, GrpcWebImpl } from "./api/clientapi.js";
|
|
16
|
-
|
|
17
|
-
const cc = new ShellCoreClientImpl(
|
|
18
|
-
new GrpcWebImpl("http://127.0.0.1:30553", {})
|
|
19
|
-
);
|
|
20
|
-
|
|
21
|
-
const remoteBox = "远程盒子";
|
|
22
|
-
const unRegistry = "未注册";
|
|
23
|
-
|
|
24
|
-
function getBoxStatus(status) {
|
|
25
|
-
switch (status) {
|
|
26
|
-
case 0:
|
|
27
|
-
return "未激活";
|
|
28
|
-
case 1:
|
|
29
|
-
return "连接中";
|
|
30
|
-
case 2:
|
|
31
|
-
return "已连接";
|
|
32
|
-
case 3:
|
|
33
|
-
return "无法连接";
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
async function getFreePort() {
|
|
38
|
-
return new Promise((resolve, reject) => {
|
|
39
|
-
const server = net.createServer();
|
|
40
|
-
server.listen(() => {
|
|
41
|
-
let addr = server.address();
|
|
42
|
-
resolve(addr.port);
|
|
43
|
-
server.close();
|
|
44
|
-
});
|
|
45
|
-
server.on("error", reject);
|
|
46
|
-
});
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
function parseVmPID(vmDir) {
|
|
50
|
-
let p = spawnSync("pgrep", ["-f", path.join(vmDir, "disk-system.qcow2")]);
|
|
51
|
-
if (p.status == 0) {
|
|
52
|
-
return p.stdout.toString();
|
|
53
|
-
} else {
|
|
54
|
-
// 没有找到任何记录
|
|
55
|
-
return "";
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
function isDefault(boxName) {
|
|
60
|
-
const defaultBoxName = env.get("DEFAULT_BOXNAME");
|
|
61
|
-
return boxName == defaultBoxName ? "yes" : "no";
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
function isRunning(vmname) {
|
|
65
|
-
return parseVmPID(vmname) ? "running" : "stop";
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
export class QemuResource {
|
|
69
|
-
/**
|
|
70
|
-
* diskDir qemu 磁盘存储路径,默认 ~/.cache/box-emulator
|
|
71
|
-
* downloadUrl system-disk 下载路径
|
|
72
|
-
*/
|
|
73
|
-
constructor(
|
|
74
|
-
diskDir = "~/.cache/box-emulator",
|
|
75
|
-
downloadUrl = "https://dl.lazycat.cloud/sdk"
|
|
76
|
-
) {
|
|
77
|
-
this.diskDir = diskDir;
|
|
78
|
-
|
|
79
|
-
this.downloadUrlRoot = downloadUrl;
|
|
80
|
-
this.downloadUrl = undefined;
|
|
81
|
-
|
|
82
|
-
this.downloader = new Downloader();
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
async init() {
|
|
86
|
-
// fetch 最新的稳定版
|
|
87
|
-
const res = await fetch(`${this.downloadUrlRoot}/vm/index.json`);
|
|
88
|
-
if (res.status !== 200) {
|
|
89
|
-
throw "获取最新的版本信息出错";
|
|
90
|
-
}
|
|
91
|
-
const body = await res.json();
|
|
92
|
-
const version = body["stable-latest"];
|
|
93
|
-
this.downloadUrl = `${this.downloadUrlRoot}/vm/${version}`;
|
|
94
|
-
|
|
95
|
-
fs.mkdirSync(this.diskDir, { recursive: true });
|
|
96
|
-
let { update, force } = await this.shouldUpdate();
|
|
97
|
-
if (!force && update) {
|
|
98
|
-
let answer = await inquirer.prompt([
|
|
99
|
-
{
|
|
100
|
-
type: "confirm",
|
|
101
|
-
name: "yesorno",
|
|
102
|
-
message: `检测到盒子系统具有更新,是否需要重新下载盒子系统镜像?`,
|
|
103
|
-
default: true,
|
|
104
|
-
},
|
|
105
|
-
]);
|
|
106
|
-
if (!answer.yesorno) {
|
|
107
|
-
update = false;
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
await this.ensureSystemDisk(update);
|
|
111
|
-
return this;
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
/**
|
|
115
|
-
* 根据下载到的 md5 和本地缓存的 md5 进行比较,如果不一样,则代表具有更新,
|
|
116
|
-
* 然后提示用户是否更新。
|
|
117
|
-
* 如果在获取 md5 的过程中,发生网络错误,则跳过更新检查。
|
|
118
|
-
* 目前的更新,只是使用 md5 来判断是否需要重新下载 md5, 并不会使用 md5 来判断所下载的数据是否正确。
|
|
119
|
-
*/
|
|
120
|
-
async shouldUpdate() {
|
|
121
|
-
let oldMd5Path = path.join(this.diskDir, "disk-system.qcow2.md5");
|
|
122
|
-
|
|
123
|
-
// 如果不存在 md5 文件,直接返回需要更新
|
|
124
|
-
try {
|
|
125
|
-
fs.accessSync(oldMd5Path);
|
|
126
|
-
} catch {
|
|
127
|
-
return { update: true, force: true };
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
// 拉取最新的 md5
|
|
131
|
-
let newMd5;
|
|
132
|
-
try {
|
|
133
|
-
let url = `${this.downloadUrl}/disk-system.qcow2.md5`;
|
|
134
|
-
let res = await fetch(url);
|
|
135
|
-
if (res.status !== 200) {
|
|
136
|
-
return { update: false };
|
|
137
|
-
}
|
|
138
|
-
newMd5 = await res.text();
|
|
139
|
-
} catch {
|
|
140
|
-
return { update: false };
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
let oldMd5 = fs.readFileSync(oldMd5Path, { encoding: "utf-8" });
|
|
144
|
-
return { update: oldMd5 !== newMd5 };
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
/**
|
|
148
|
-
* 确保 qemu 系统盘的镜像存在,如果不存在,将会从 downloadUrl 中下载
|
|
149
|
-
*/
|
|
150
|
-
async ensureSystemDisk(update = false) {
|
|
151
|
-
let systemDiskFile = path.join(this.diskDir, "disk-system.qcow2");
|
|
152
|
-
if (!update) {
|
|
153
|
-
try {
|
|
154
|
-
fs.accessSync(systemDiskFile);
|
|
155
|
-
return;
|
|
156
|
-
} catch {}
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
await this.downloadOVMF();
|
|
160
|
-
await this.downloadSystemDisk();
|
|
161
|
-
await this.downloadPrivateKey();
|
|
162
|
-
await this.downloadSystemDiskMd5();
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
async downloadOVMF() {
|
|
166
|
-
let name = `${os.arch()}.OVMF.fd`;
|
|
167
|
-
let savePath = path.join(this.diskDir, name);
|
|
168
|
-
let url = `${this.downloadUrlRoot}/${name}`;
|
|
169
|
-
await this.downloader.download(url, savePath);
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
async downloadSystemDiskMd5() {
|
|
173
|
-
let savePath = path.join(this.diskDir, "disk-system.qcow2.md5");
|
|
174
|
-
let url = `${this.downloadUrl}/disk-system.qcow2.md5`;
|
|
175
|
-
await this.downloader.download(url, savePath);
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
async downloadSystemDisk() {
|
|
179
|
-
let savePath = path.join(this.diskDir, "disk-system.qcow2");
|
|
180
|
-
let url = `${this.downloadUrl}/disk-system.qcow2.gz`;
|
|
181
|
-
await this.downloader.download(url, savePath, true);
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
async downloadPrivateKey() {
|
|
185
|
-
let savePath = path.join(this.diskDir, "id_rsa");
|
|
186
|
-
let url = `${this.downloadUrl}/id_rsa`;
|
|
187
|
-
await this.downloader.download(url, savePath);
|
|
188
|
-
fs.chmodSync(savePath, 0o400);
|
|
189
|
-
return;
|
|
190
|
-
}
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
export class QemuVM {
|
|
194
|
-
/**
|
|
195
|
-
* @param {Object} scheme - 各个操作系统的配置文件.
|
|
196
|
-
*/
|
|
197
|
-
constructor() {
|
|
198
|
-
this.scheme = undefined;
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
async init() {
|
|
202
|
-
let defaultSchemeFile = path.resolve(
|
|
203
|
-
contextDirname(import.meta.url),
|
|
204
|
-
"schemes",
|
|
205
|
-
"vm_box_system_debian.json"
|
|
206
|
-
);
|
|
207
|
-
const content = await envsubstrDefault(
|
|
208
|
-
fs.readFileSync(defaultSchemeFile, "utf8"),
|
|
209
|
-
process.env
|
|
210
|
-
);
|
|
211
|
-
let scheme = JSON.parse(content);
|
|
212
|
-
if (scheme.path && !scheme.path.startsWith("/")) {
|
|
213
|
-
scheme.path = path.resolve(scheme.path);
|
|
214
|
-
}
|
|
215
|
-
fs.mkdirSync(scheme.path, { recursive: true });
|
|
216
|
-
|
|
217
|
-
this.scheme = scheme;
|
|
218
|
-
return this;
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
/**
|
|
222
|
-
* 运行虚拟盒子,并等待 boxid 的出现,确保盒子启动正常
|
|
223
|
-
*/
|
|
224
|
-
async startVM(vmname) {
|
|
225
|
-
let args = await this.buildQemuArgs(vmname);
|
|
226
|
-
let p = spawn("qemu-system-x86_64", args, {
|
|
227
|
-
detached: true,
|
|
228
|
-
stdio: ["pipe", "ignore", "inherit"],
|
|
229
|
-
});
|
|
230
|
-
p.on("error", (e) => {
|
|
231
|
-
throw e;
|
|
232
|
-
});
|
|
233
|
-
logger.info("启动中...");
|
|
234
|
-
|
|
235
|
-
// 需要等待 boxid 的出现
|
|
236
|
-
return new Promise((resolve, reject) => {
|
|
237
|
-
let count = 0;
|
|
238
|
-
let id = setInterval(() => {
|
|
239
|
-
if (count == 15) {
|
|
240
|
-
count = 0;
|
|
241
|
-
}
|
|
242
|
-
process.stdout.write("\r");
|
|
243
|
-
process.stdout.write("等待中" + ".".repeat(count));
|
|
244
|
-
count++;
|
|
245
|
-
|
|
246
|
-
if (p.exitCode) {
|
|
247
|
-
reject(`qemu 已经退出 Code: ${p.exitCode}`);
|
|
248
|
-
clearInterval(id);
|
|
249
|
-
return;
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
let boxId = this.readBoxid(vmname);
|
|
253
|
-
if (boxId) {
|
|
254
|
-
clearInterval(id);
|
|
255
|
-
p.unref();
|
|
256
|
-
resolve(boxId);
|
|
257
|
-
logger.debug("启动成功!");
|
|
258
|
-
}
|
|
259
|
-
}, 1000);
|
|
260
|
-
|
|
261
|
-
// 当在等待期间,ctrl-c 退出
|
|
262
|
-
process.on("SIGINT", () => {
|
|
263
|
-
p.kill();
|
|
264
|
-
clearInterval(id);
|
|
265
|
-
reject("exit");
|
|
266
|
-
});
|
|
267
|
-
});
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
/**
|
|
271
|
-
* 运行一个指定的虚拟机,如果已经运行,将直接退出。如果盒子不存在,将报错退出
|
|
272
|
-
* @param {string} name - 盒子的名称
|
|
273
|
-
* @return {string} id - 盒子的id
|
|
274
|
-
**/
|
|
275
|
-
async runVM(nameOrBoxId) {
|
|
276
|
-
const { vmname, name, id } = await this.findVmname(nameOrBoxId);
|
|
277
|
-
if (!vmname) {
|
|
278
|
-
logger.info(`${name || id} 盒子不存在,请通过 lzc-cli box create 创建`);
|
|
279
|
-
return;
|
|
280
|
-
}
|
|
281
|
-
let pid = parseVmPID(vmname);
|
|
282
|
-
if (pid) {
|
|
283
|
-
return id;
|
|
284
|
-
}
|
|
285
|
-
await this.startVM(vmname);
|
|
286
|
-
logger.info(`${name || id} 盒子启动成功!`);
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
/**
|
|
290
|
-
* 创建一个指定的虚拟机,并启动等待注册成功。
|
|
291
|
-
**/
|
|
292
|
-
async createVM(resourcer, name) {
|
|
293
|
-
const vmDir = this.ensureVmDir(name);
|
|
294
|
-
this.ensureVolumeDir(vmDir);
|
|
295
|
-
await this.buildDisks(vmDir);
|
|
296
|
-
|
|
297
|
-
let boxId = await this.startVM(path.basename(vmDir));
|
|
298
|
-
return boxId;
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
/**
|
|
302
|
-
* 根据虚拟机名称确保缓存文件夹存在,返回对应的虚拟机缓存文件夹
|
|
303
|
-
*/
|
|
304
|
-
ensureVmDir(name) {
|
|
305
|
-
const vmDir = path.resolve(this.scheme.path, `vm-${name}`);
|
|
306
|
-
fs.mkdirSync(vmDir, { recursive: true });
|
|
307
|
-
return vmDir;
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
ensureVolumeDir(vmDir) {
|
|
311
|
-
for (let key in this.scheme.volume) {
|
|
312
|
-
let bindPath = `${vmDir}/volume-${key}`;
|
|
313
|
-
fs.mkdirSync(bindPath, { recursive: true });
|
|
314
|
-
}
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
/**
|
|
318
|
-
* 构建硬盘,从 scheme 中读取 disk 的配置信息,并构建。
|
|
319
|
-
*/
|
|
320
|
-
async buildDisks(vmDir) {
|
|
321
|
-
for (const diskInfo of this.scheme.disks) {
|
|
322
|
-
let name = `disk-${diskInfo.id}.qcow2`;
|
|
323
|
-
let diskPath = path.join(vmDir, name);
|
|
324
|
-
|
|
325
|
-
try {
|
|
326
|
-
fs.accessSync(diskPath, fs.constants.F_OK);
|
|
327
|
-
continue;
|
|
328
|
-
} catch {}
|
|
329
|
-
|
|
330
|
-
if (diskInfo.system) {
|
|
331
|
-
await this.buildSystemDisk(name, diskPath, diskInfo);
|
|
332
|
-
} else {
|
|
333
|
-
await this.buildDataDisk(name, diskPath, diskInfo);
|
|
334
|
-
}
|
|
335
|
-
}
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
async buildSystemDisk(name, diskPath, diskInfo) {
|
|
339
|
-
logger.debug(`构建系统盘快照:${diskPath}`);
|
|
340
|
-
let baseImage = path.join(this.scheme.path, name);
|
|
341
|
-
return spawnSync(
|
|
342
|
-
"qemu-img",
|
|
343
|
-
["create", "-f", "qcow2", "-b", baseImage, "-F", "qcow2", diskPath],
|
|
344
|
-
{ stdio: "inherit" }
|
|
345
|
-
);
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
async buildDataDisk(name, diskPath, diskInfo) {
|
|
349
|
-
logger.debug(`构建数据盘:${diskPath}`);
|
|
350
|
-
return spawnSync(
|
|
351
|
-
"qemu-img",
|
|
352
|
-
["create", "-f", "qcow2", diskPath, diskInfo.size],
|
|
353
|
-
{ stdio: "inherit" }
|
|
354
|
-
);
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
/**
|
|
358
|
-
* 寻找默认可用的 bios 文件
|
|
359
|
-
*/
|
|
360
|
-
findBIOS() {
|
|
361
|
-
let includes = [
|
|
362
|
-
"/usr/share/ovmf/x64/OVMF.fd",
|
|
363
|
-
"/usr/share/ovmf/OVMF.fd",
|
|
364
|
-
path.join(this.scheme.path, `${os.arch()}.OVMF.fd`),
|
|
365
|
-
];
|
|
366
|
-
for (let file of includes) {
|
|
367
|
-
try {
|
|
368
|
-
fs.accessSync(file, fs.constants.F_OK);
|
|
369
|
-
return file;
|
|
370
|
-
} catch {}
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
throw "找不到 ovmf 文件";
|
|
374
|
-
}
|
|
375
|
-
|
|
376
|
-
/**
|
|
377
|
-
* 构建 qemu 启动的参数
|
|
378
|
-
*/
|
|
379
|
-
async buildQemuArgs(vmname) {
|
|
380
|
-
const vmDir = path.resolve(this.scheme.path, vmname);
|
|
381
|
-
let sshPort = await getFreePort();
|
|
382
|
-
let bios = this.findBIOS();
|
|
383
|
-
// prettier-ignore
|
|
384
|
-
let args = [
|
|
385
|
-
"-name", `${this.scheme.name}-${this.scheme.uuid}-${vmname}`,
|
|
386
|
-
"-machine", "pc,accel=kvm,accel=kvf,accel=xen,accel=hax,accel=nvmm,accel=whpx,accel=tcg",
|
|
387
|
-
"-m", `${this.scheme.memory}`,
|
|
388
|
-
"-smp", "4,sockets=4,cores=1,threads=1",
|
|
389
|
-
"-bios", bios,
|
|
390
|
-
"-device", "piix3-usb-uhci",
|
|
391
|
-
"-vnc", `unix:${vmDir}/.vnc`,
|
|
392
|
-
"-monitor", `unix:${vmDir}/.monitor,server,nowait`,
|
|
393
|
-
"-serial", `unix:${vmDir}/.serial,server,nowait`,
|
|
394
|
-
"-parallel", `unix:${vmDir}/.parallel,server,nowait`,
|
|
395
|
-
"-netdev", `user,hostfwd=tcp::${sshPort}-:22,id=eth`,
|
|
396
|
-
"-device", "virtio-net-pci,netdev=eth",
|
|
397
|
-
"-chardev", "stdio,id=s1,signal=off",
|
|
398
|
-
"-device", "isa-serial,chardev=s1",
|
|
399
|
-
]
|
|
400
|
-
|
|
401
|
-
this.scheme.disks.forEach((diskInfo, index) => {
|
|
402
|
-
let file = `file=${vmDir}/disk-${diskInfo.id}.qcow2,format=qcow2,index=${index},media=disk,if=virtio`;
|
|
403
|
-
args.push("-drive", file);
|
|
404
|
-
});
|
|
405
|
-
|
|
406
|
-
Object.keys(this.scheme.volume).forEach((key) => {
|
|
407
|
-
let volumePath = this.scheme.volume[key];
|
|
408
|
-
let bindPath = `${vmDir}/volume-${key}`;
|
|
409
|
-
args.push(
|
|
410
|
-
"-virtfs",
|
|
411
|
-
`local,path=${bindPath},mount_tag=id_2245023265ae4cf87d02c8b6,security_model=mapped-xattr,id=id_2245023265ae4cf87d02c8b6`
|
|
412
|
-
);
|
|
413
|
-
});
|
|
414
|
-
|
|
415
|
-
return args;
|
|
416
|
-
}
|
|
417
|
-
|
|
418
|
-
/**
|
|
419
|
-
* 停止虚拟盒子
|
|
420
|
-
*/
|
|
421
|
-
async stopVM(nameOrBoxId) {
|
|
422
|
-
const { vmname, name, id } = await this.findVmname(nameOrBoxId);
|
|
423
|
-
let pid = parseVmPID(vmname);
|
|
424
|
-
if (pid) {
|
|
425
|
-
process.kill(pid);
|
|
426
|
-
}
|
|
427
|
-
logger.info(`${name || id} 盒子已停止`);
|
|
428
|
-
}
|
|
429
|
-
|
|
430
|
-
/**
|
|
431
|
-
* 清理盒子,用于注册失败或者在注册过程中用户手动中止的情况
|
|
432
|
-
*/
|
|
433
|
-
async cleanVM(nameOrBoxId) {
|
|
434
|
-
const { vmname } = await this.findVmname(nameOrBoxId);
|
|
435
|
-
const vmDir = path.resolve(this.scheme.path, vmname);
|
|
436
|
-
try {
|
|
437
|
-
fs.accessSync(vmDir);
|
|
438
|
-
} catch {
|
|
439
|
-
return;
|
|
440
|
-
}
|
|
441
|
-
fs.rmSync(vmDir, { recursive: true, force: true });
|
|
442
|
-
}
|
|
443
|
-
|
|
444
|
-
async deleteVM(nameOrBoxId) {
|
|
445
|
-
const boxes = await this.listAllVM();
|
|
446
|
-
const box = boxes.find((b) => b.id == nameOrBoxId || b.name == nameOrBoxId);
|
|
447
|
-
if (!box) {
|
|
448
|
-
logger.info(`${nameOrBoxId} 盒子不存在`);
|
|
449
|
-
return;
|
|
450
|
-
}
|
|
451
|
-
if (box.status !== unRegistry) {
|
|
452
|
-
await cc.DelBox({ id: box.id });
|
|
453
|
-
}
|
|
454
|
-
|
|
455
|
-
if (box["vm status"] === remoteBox) {
|
|
456
|
-
return;
|
|
457
|
-
}
|
|
458
|
-
|
|
459
|
-
const { vmname, name, id } = box;
|
|
460
|
-
const vmDir = path.resolve(this.scheme.path, vmname);
|
|
461
|
-
let pid = parseVmPID(vmDir);
|
|
462
|
-
if (pid) {
|
|
463
|
-
await inquirer
|
|
464
|
-
.prompt([
|
|
465
|
-
{
|
|
466
|
-
type: "confirm",
|
|
467
|
-
name: "yesorno",
|
|
468
|
-
message: `${name || id} 正在运行中,是否停止?`,
|
|
469
|
-
default: false,
|
|
470
|
-
},
|
|
471
|
-
])
|
|
472
|
-
.then(async (answer) => {
|
|
473
|
-
if (!answer.yesorno) {
|
|
474
|
-
return;
|
|
475
|
-
}
|
|
476
|
-
process.kill(pid);
|
|
477
|
-
logger.info(`${name || id} 盒子已停止`);
|
|
478
|
-
});
|
|
479
|
-
}
|
|
480
|
-
|
|
481
|
-
return inquirer
|
|
482
|
-
.prompt([
|
|
483
|
-
{
|
|
484
|
-
type: "confirm",
|
|
485
|
-
name: "yesorno",
|
|
486
|
-
message: `确定要删除 ${name || id} 盒子吗?`,
|
|
487
|
-
default: false,
|
|
488
|
-
},
|
|
489
|
-
])
|
|
490
|
-
.then(async (answer) => {
|
|
491
|
-
if (!answer.yesorno) {
|
|
492
|
-
return;
|
|
493
|
-
}
|
|
494
|
-
fs.rmSync(vmDir, { recursive: true, force: true });
|
|
495
|
-
});
|
|
496
|
-
}
|
|
497
|
-
|
|
498
|
-
async listAllVM() {
|
|
499
|
-
const boxes = (await cc.QueryBoxList({})).boxes;
|
|
500
|
-
const entries = await this.loadBoxIdAndBoxName();
|
|
501
|
-
|
|
502
|
-
let result = [];
|
|
503
|
-
Object.keys(entries).forEach((vmname) => {
|
|
504
|
-
let info = entries[vmname];
|
|
505
|
-
const box = boxes.find((box) => box.boxId == info.id);
|
|
506
|
-
const status = box ? getBoxStatus(box.status) : unRegistry;
|
|
507
|
-
result.push({
|
|
508
|
-
...info,
|
|
509
|
-
status,
|
|
510
|
-
default: isDefault(info.name),
|
|
511
|
-
"vm status": isRunning(vmname),
|
|
512
|
-
});
|
|
513
|
-
});
|
|
514
|
-
|
|
515
|
-
boxes.forEach((box) => {
|
|
516
|
-
let info = result.find((r) => r.id == box.boxId);
|
|
517
|
-
if (!info) {
|
|
518
|
-
result.push({
|
|
519
|
-
id: box.boxId,
|
|
520
|
-
name: box.boxName,
|
|
521
|
-
status: getBoxStatus(box.status),
|
|
522
|
-
default: isDefault(box.boxName),
|
|
523
|
-
"vm status": remoteBox,
|
|
524
|
-
});
|
|
525
|
-
}
|
|
526
|
-
});
|
|
527
|
-
return result;
|
|
528
|
-
}
|
|
529
|
-
|
|
530
|
-
/**
|
|
531
|
-
* 根据 scheme 中的 path 和 uuid 和 name 获取盒子的信息
|
|
532
|
-
*/
|
|
533
|
-
async listVM(nameOrBoxId) {
|
|
534
|
-
let infos = await this.listAllVM();
|
|
535
|
-
if (nameOrBoxId) {
|
|
536
|
-
let info = infos.find(
|
|
537
|
-
(i) => i.name === nameOrBoxId || i.id === nameOrBoxId
|
|
538
|
-
);
|
|
539
|
-
if (info) {
|
|
540
|
-
infos = [info];
|
|
541
|
-
}
|
|
542
|
-
}
|
|
543
|
-
|
|
544
|
-
return infos.map((info) => {
|
|
545
|
-
delete info["vmname"];
|
|
546
|
-
return info;
|
|
547
|
-
});
|
|
548
|
-
}
|
|
549
|
-
|
|
550
|
-
/**
|
|
551
|
-
* 根据 scheme 中的 path 和 uuid 和 name 获取盒子的boxid
|
|
552
|
-
*/
|
|
553
|
-
readBoxid(vmname) {
|
|
554
|
-
if (!this.scheme.volume["config"]) {
|
|
555
|
-
return "";
|
|
556
|
-
}
|
|
557
|
-
|
|
558
|
-
let vmDir = path.join(this.scheme.path, vmname);
|
|
559
|
-
let volumeDir = path.join(vmDir, `volume-config`);
|
|
560
|
-
|
|
561
|
-
try {
|
|
562
|
-
let boxid = fs.readFileSync(path.join(volumeDir, "box.id"), {
|
|
563
|
-
encoding: "utf8",
|
|
564
|
-
});
|
|
565
|
-
return boxid;
|
|
566
|
-
} catch {
|
|
567
|
-
return "";
|
|
568
|
-
}
|
|
569
|
-
}
|
|
570
|
-
|
|
571
|
-
async loadBoxIdAndBoxName() {
|
|
572
|
-
const files = await glob(
|
|
573
|
-
["vm-*/volume-config/box.id", "vm-*/volume-config/box.name"],
|
|
574
|
-
{
|
|
575
|
-
cwd: this.scheme.path,
|
|
576
|
-
dot: false,
|
|
577
|
-
}
|
|
578
|
-
);
|
|
579
|
-
const result = {};
|
|
580
|
-
files.forEach((file) => {
|
|
581
|
-
const vmDir = file.split(path.sep)[0];
|
|
582
|
-
const type = path.extname(file).toLowerCase() === ".name" ? "name" : "id";
|
|
583
|
-
const value = fs.readFileSync(
|
|
584
|
-
path.resolve(this.scheme.path, file),
|
|
585
|
-
"utf8"
|
|
586
|
-
);
|
|
587
|
-
if (!result[vmDir]) {
|
|
588
|
-
result[vmDir] = { id: "", name: "", vmname: "" };
|
|
589
|
-
}
|
|
590
|
-
result[vmDir][type] = value;
|
|
591
|
-
result[vmDir]["vmname"] = vmDir;
|
|
592
|
-
});
|
|
593
|
-
return result;
|
|
594
|
-
}
|
|
595
|
-
|
|
596
|
-
async findVmname(nameOrBoxId) {
|
|
597
|
-
const entries = await this.loadBoxIdAndBoxName();
|
|
598
|
-
const vmname = Object.keys(entries).find((key) => {
|
|
599
|
-
const e = entries[key];
|
|
600
|
-
return e?.name === nameOrBoxId || e.id === nameOrBoxId;
|
|
601
|
-
});
|
|
602
|
-
if (!vmname) {
|
|
603
|
-
throw `${nameOrBoxId} 盒子不存在,请检查盒子名称或者ID是否正确`;
|
|
604
|
-
}
|
|
605
|
-
|
|
606
|
-
return entries[vmname];
|
|
607
|
-
}
|
|
608
|
-
}
|