@phystack/cli 4.4.29
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/LICENSE.md +19 -0
- package/README.md +24 -0
- package/bin/index.js +2 -0
- package/dist/commands/app/build-apps.js +66 -0
- package/dist/commands/app/build-apps.js.map +1 -0
- package/dist/commands/app/build-container-remote.js +171 -0
- package/dist/commands/app/build-container-remote.js.map +1 -0
- package/dist/commands/app/build-container.js +322 -0
- package/dist/commands/app/build-container.js.map +1 -0
- package/dist/commands/app/build.js +375 -0
- package/dist/commands/app/build.js.map +1 -0
- package/dist/commands/app/create.js +409 -0
- package/dist/commands/app/create.js.map +1 -0
- package/dist/commands/app/deploy.js +176 -0
- package/dist/commands/app/deploy.js.map +1 -0
- package/dist/commands/app/device-picker.js +150 -0
- package/dist/commands/app/device-picker.js.map +1 -0
- package/dist/commands/app/import.js +303 -0
- package/dist/commands/app/import.js.map +1 -0
- package/dist/commands/app/list.js +26 -0
- package/dist/commands/app/list.js.map +1 -0
- package/dist/commands/app/publish.js +327 -0
- package/dist/commands/app/publish.js.map +1 -0
- package/dist/commands/app/settings.js +129 -0
- package/dist/commands/app/settings.js.map +1 -0
- package/dist/commands/app/types.js +13 -0
- package/dist/commands/app/types.js.map +1 -0
- package/dist/commands/app/upload-description.js +139 -0
- package/dist/commands/app/upload-description.js.map +1 -0
- package/dist/commands/app/upload-settings.js +29 -0
- package/dist/commands/app/upload-settings.js.map +1 -0
- package/dist/commands/app/utils.js +86 -0
- package/dist/commands/app/utils.js.map +1 -0
- package/dist/commands/auth/login.js +111 -0
- package/dist/commands/auth/login.js.map +1 -0
- package/dist/commands/auth/logout.js +19 -0
- package/dist/commands/auth/logout.js.map +1 -0
- package/dist/commands/descriptor/create.js +143 -0
- package/dist/commands/descriptor/create.js.map +1 -0
- package/dist/commands/descriptor/index.js +36 -0
- package/dist/commands/descriptor/index.js.map +1 -0
- package/dist/commands/descriptor/publish.js +163 -0
- package/dist/commands/descriptor/publish.js.map +1 -0
- package/dist/commands/descriptor/show.js +68 -0
- package/dist/commands/descriptor/show.js.map +1 -0
- package/dist/commands/dev/develop.js +175 -0
- package/dist/commands/dev/develop.js.map +1 -0
- package/dist/commands/dev/forward.js +118 -0
- package/dist/commands/dev/forward.js.map +1 -0
- package/dist/commands/dev/index.js +66 -0
- package/dist/commands/dev/index.js.map +1 -0
- package/dist/commands/dev/list.js +96 -0
- package/dist/commands/dev/list.js.map +1 -0
- package/dist/commands/dev/screen-devtools.js +156 -0
- package/dist/commands/dev/screen-devtools.js.map +1 -0
- package/dist/commands/dev/select.js +118 -0
- package/dist/commands/dev/select.js.map +1 -0
- package/dist/commands/dev/shell.js +171 -0
- package/dist/commands/dev/shell.js.map +1 -0
- package/dist/commands/dev/vnc.js +75 -0
- package/dist/commands/dev/vnc.js.map +1 -0
- package/dist/commands/device/select.js +118 -0
- package/dist/commands/device/select.js.map +1 -0
- package/dist/commands/flash.js +1120 -0
- package/dist/commands/flash.js.map +1 -0
- package/dist/commands/inst/create.js +55 -0
- package/dist/commands/inst/create.js.map +1 -0
- package/dist/commands/inst/index.js +15 -0
- package/dist/commands/inst/index.js.map +1 -0
- package/dist/commands/inst/list.js +26 -0
- package/dist/commands/inst/list.js.map +1 -0
- package/dist/commands/legacydev/debug.js +11 -0
- package/dist/commands/legacydev/debug.js.map +1 -0
- package/dist/commands/legacydev/deploy.js +15 -0
- package/dist/commands/legacydev/deploy.js.map +1 -0
- package/dist/commands/legacydev/dumpTwin.js +27 -0
- package/dist/commands/legacydev/dumpTwin.js.map +1 -0
- package/dist/commands/legacydev/forward.js +104 -0
- package/dist/commands/legacydev/forward.js.map +1 -0
- package/dist/commands/legacydev/index.js +188 -0
- package/dist/commands/legacydev/index.js.map +1 -0
- package/dist/commands/legacydev/invoke.js +29 -0
- package/dist/commands/legacydev/invoke.js.map +1 -0
- package/dist/commands/legacydev/js.js +69 -0
- package/dist/commands/legacydev/js.js.map +1 -0
- package/dist/commands/legacydev/list.js +196 -0
- package/dist/commands/legacydev/list.js.map +1 -0
- package/dist/commands/legacydev/logs.js +60 -0
- package/dist/commands/legacydev/logs.js.map +1 -0
- package/dist/commands/legacydev/modules.js +50 -0
- package/dist/commands/legacydev/modules.js.map +1 -0
- package/dist/commands/legacydev/move.js +23 -0
- package/dist/commands/legacydev/move.js.map +1 -0
- package/dist/commands/legacydev/ota.js +88 -0
- package/dist/commands/legacydev/ota.js.map +1 -0
- package/dist/commands/legacydev/patchTwin.js +21 -0
- package/dist/commands/legacydev/patchTwin.js.map +1 -0
- package/dist/commands/legacydev/pin.js +23 -0
- package/dist/commands/legacydev/pin.js.map +1 -0
- package/dist/commands/legacydev/pub.js +25 -0
- package/dist/commands/legacydev/pub.js.map +1 -0
- package/dist/commands/legacydev/rdp.js +64 -0
- package/dist/commands/legacydev/rdp.js.map +1 -0
- package/dist/commands/legacydev/screen-devtools.js +142 -0
- package/dist/commands/legacydev/screen-devtools.js.map +1 -0
- package/dist/commands/legacydev/settingsShow.js +89 -0
- package/dist/commands/legacydev/settingsShow.js.map +1 -0
- package/dist/commands/legacydev/settingsUpdate.js +114 -0
- package/dist/commands/legacydev/settingsUpdate.js.map +1 -0
- package/dist/commands/legacydev/shell.js +167 -0
- package/dist/commands/legacydev/shell.js.map +1 -0
- package/dist/commands/legacydev/showTwin.js +9 -0
- package/dist/commands/legacydev/showTwin.js.map +1 -0
- package/dist/commands/legacydev/statusLog.js +56 -0
- package/dist/commands/legacydev/statusLog.js.map +1 -0
- package/dist/commands/legacydev/sub.js +39 -0
- package/dist/commands/legacydev/sub.js.map +1 -0
- package/dist/commands/legacydev/vnc.js +61 -0
- package/dist/commands/legacydev/vnc.js.map +1 -0
- package/dist/commands/tenant/index.js +21 -0
- package/dist/commands/tenant/index.js.map +1 -0
- package/dist/commands/tenant/list.js +14 -0
- package/dist/commands/tenant/list.js.map +1 -0
- package/dist/commands/tenant/select.js +87 -0
- package/dist/commands/tenant/select.js.map +1 -0
- package/dist/commands/vm/create.js +718 -0
- package/dist/commands/vm/create.js.map +1 -0
- package/dist/commands/vm/index.js +130 -0
- package/dist/commands/vm/index.js.map +1 -0
- package/dist/commands/vm/list.js +124 -0
- package/dist/commands/vm/list.js.map +1 -0
- package/dist/commands/vm/logs.js +66 -0
- package/dist/commands/vm/logs.js.map +1 -0
- package/dist/commands/vm/remove.js +180 -0
- package/dist/commands/vm/remove.js.map +1 -0
- package/dist/commands/vm/shell.js +400 -0
- package/dist/commands/vm/shell.js.map +1 -0
- package/dist/commands/vm/start.js +861 -0
- package/dist/commands/vm/start.js.map +1 -0
- package/dist/commands/vm/stop.js +232 -0
- package/dist/commands/vm/stop.js.map +1 -0
- package/dist/index.js +158 -0
- package/dist/index.js.map +1 -0
- package/dist/services/admin-api/admin-api.types.js +3 -0
- package/dist/services/admin-api/admin-api.types.js.map +1 -0
- package/dist/services/admin-api/device-modules.admin-api.service.js +58 -0
- package/dist/services/admin-api/device-modules.admin-api.service.js.map +1 -0
- package/dist/services/admin-api/devices-admin-api.service.js +213 -0
- package/dist/services/admin-api/devices-admin-api.service.js.map +1 -0
- package/dist/services/admin-api/gridapps-admin-api.service.js +59 -0
- package/dist/services/admin-api/gridapps-admin-api.service.js.map +1 -0
- package/dist/services/admin-api/index.js +157 -0
- package/dist/services/admin-api/index.js.map +1 -0
- package/dist/services/admin-api/installations-admin-api.service.js +29 -0
- package/dist/services/admin-api/installations-admin-api.service.js.map +1 -0
- package/dist/services/admin-api/organizations-admin-api.service.js +53 -0
- package/dist/services/admin-api/organizations-admin-api.service.js.map +1 -0
- package/dist/services/auth/device-grant-auth.service.js +224 -0
- package/dist/services/auth/device-grant-auth.service.js.map +1 -0
- package/dist/services/phyhub/index.js +200 -0
- package/dist/services/phyhub/index.js.map +1 -0
- package/dist/services/phyhub/phyhub.types.js +3 -0
- package/dist/services/phyhub/phyhub.types.js.map +1 -0
- package/dist/utils/device-fetcher.js +92 -0
- package/dist/utils/device-fetcher.js.map +1 -0
- package/dist/utils/devices.js +41 -0
- package/dist/utils/devices.js.map +1 -0
- package/dist/utils/docker-credentials.js +546 -0
- package/dist/utils/docker-credentials.js.map +1 -0
- package/dist/utils/emulated-device.js +91 -0
- package/dist/utils/emulated-device.js.map +1 -0
- package/dist/utils/index.js +180 -0
- package/dist/utils/index.js.map +1 -0
- package/dist/utils/modules.js +36 -0
- package/dist/utils/modules.js.map +1 -0
- package/dist/utils/org-selector.js +108 -0
- package/dist/utils/org-selector.js.map +1 -0
- package/dist/utils/proxy.js +31 -0
- package/dist/utils/proxy.js.map +1 -0
- package/dist/utils/registry-credentials.js +113 -0
- package/dist/utils/registry-credentials.js.map +1 -0
- package/dist/utils/statuses.js +124 -0
- package/dist/utils/statuses.js.map +1 -0
- package/dist/utils/templates.js +189 -0
- package/dist/utils/templates.js.map +1 -0
- package/dist/utils/tenant-storage.js +88 -0
- package/dist/utils/tenant-storage.js.map +1 -0
- package/dist/utils/vm.js +434 -0
- package/dist/utils/vm.js.map +1 -0
- package/dist/utils/with-spinner.js +20 -0
- package/dist/utils/with-spinner.js.map +1 -0
- package/package.json +103 -0
|
@@ -0,0 +1,861 @@
|
|
|
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.default = startVm;
|
|
7
|
+
const fs_1 = __importDefault(require("fs"));
|
|
8
|
+
const path_1 = __importDefault(require("path"));
|
|
9
|
+
const chalk_1 = __importDefault(require("chalk"));
|
|
10
|
+
const child_process_1 = require("child_process");
|
|
11
|
+
const vm_1 = require("../../utils/vm");
|
|
12
|
+
const inquirer_1 = __importDefault(require("inquirer"));
|
|
13
|
+
/**
|
|
14
|
+
* Starts a single PhyOS VM with QEMU (helper function)
|
|
15
|
+
*/
|
|
16
|
+
async function startSingleVm(name, options) {
|
|
17
|
+
const runningOnMac = isMacOS();
|
|
18
|
+
const needsSudo = runningOnMac && process.getuid && process.getuid() !== 0;
|
|
19
|
+
// If on macOS and needs bridged networking (default) but not running as root, re-run with sudo
|
|
20
|
+
if (needsSudo) {
|
|
21
|
+
console.log(chalk_1.default.yellow('Bridged networking on macOS requires root privileges. Attempting to re-run with sudo...'));
|
|
22
|
+
// Get the original arguments passed to the phy command
|
|
23
|
+
// process.argv is typically [node executable, script path, ...args]
|
|
24
|
+
// We want to run 'sudo phy ...args'
|
|
25
|
+
const originalArgs = process.argv.slice(2); // e.g., ['vm', 'start', 'myvm', '--headless']
|
|
26
|
+
const phyCommand = process.argv[1]; // Path to the phy script being executed
|
|
27
|
+
const sudoArgs = [phyCommand, ...originalArgs]; // Explicitly typed array
|
|
28
|
+
try {
|
|
29
|
+
// Explicitly use the string 'sudo' as the command
|
|
30
|
+
const sudoProcess = (0, child_process_1.spawn)('sudo', sudoArgs, { stdio: 'inherit' });
|
|
31
|
+
sudoProcess.on('error', (err) => {
|
|
32
|
+
console.error(chalk_1.default.red(`Failed to execute sudo: ${err.message}`));
|
|
33
|
+
console.error(chalk_1.default.red('Please ensure \'sudo\' is available and try running the command manually with sudo:'));
|
|
34
|
+
console.error(chalk_1.default.cyan(` sudo phy ${originalArgs.join(' ')}`));
|
|
35
|
+
process.exit(1);
|
|
36
|
+
});
|
|
37
|
+
// Wait for the sudo process to exit and exit this script with the same code
|
|
38
|
+
sudoProcess.on('close', (code) => {
|
|
39
|
+
process.exit(code === null ? 1 : code);
|
|
40
|
+
});
|
|
41
|
+
// Prevent the non-sudo version from continuing
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
catch (error) {
|
|
45
|
+
console.error(chalk_1.default.red(`Error attempting to spawn sudo: ${error.message}`));
|
|
46
|
+
process.exit(1);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
// Check if VM exists (do this *after* potential sudo relaunch)
|
|
50
|
+
if (!(0, vm_1.vmExists)(name)) {
|
|
51
|
+
throw new Error(`VM "${name}" not found`);
|
|
52
|
+
}
|
|
53
|
+
// Check if VM is already running
|
|
54
|
+
if ((0, vm_1.isVmRunning)(name)) {
|
|
55
|
+
console.log(chalk_1.default.yellow(`VM "${name}" is already running`));
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
// Read VM configuration and settings
|
|
59
|
+
const config = (0, vm_1.readVmConfig)(name);
|
|
60
|
+
const settings = (0, vm_1.readVmSettings)(name);
|
|
61
|
+
const vmDir = (0, vm_1.getVmDir)(name);
|
|
62
|
+
// Determine memory to use - priority: command option > settings.json > config.json
|
|
63
|
+
const memory = options.memory
|
|
64
|
+
? parseInt(options.memory, 10)
|
|
65
|
+
: settings.memory
|
|
66
|
+
? settings.memory
|
|
67
|
+
: config.memory || 2048;
|
|
68
|
+
// Get host architecture to determine emulation status
|
|
69
|
+
const hostArch = getHostArch();
|
|
70
|
+
const isEmulated = config.arch !== hostArch;
|
|
71
|
+
// Force headless mode for cross-architecture emulation due to compatibility issues
|
|
72
|
+
let headless = options.headless !== undefined
|
|
73
|
+
? options.headless
|
|
74
|
+
: settings.headless || false;
|
|
75
|
+
// Force headless mode for cross-architecture emulation (both directions)
|
|
76
|
+
if (isEmulated && ((config.arch === 'amd64' && hostArch === 'arm64') ||
|
|
77
|
+
(config.arch === 'arm64' && hostArch === 'amd64'))) {
|
|
78
|
+
const wasHeadless = headless;
|
|
79
|
+
headless = true; // Force headless mode
|
|
80
|
+
// If user explicitly requested non-headless mode, inform them
|
|
81
|
+
if (options.headless === false || (!options.headless && !settings.headless)) {
|
|
82
|
+
console.log(chalk_1.default.yellow(`⚠️ Running ${config.arch} VM on ${hostArch} architecture requires headless mode`));
|
|
83
|
+
console.log(chalk_1.default.yellow(` GUI mode has been disabled and switched to headless operation`));
|
|
84
|
+
console.log(chalk_1.default.blue(` Use 'phy vm shell ${name}' to access the console`));
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
// Build paths to image and disk files
|
|
88
|
+
const imageFile = path_1.default.join(vmDir, config.image);
|
|
89
|
+
const diskFile = path_1.default.join(vmDir, config.disk);
|
|
90
|
+
// Construct initramfs path if the filename exists in config
|
|
91
|
+
const initramfsFile = config.initramfs
|
|
92
|
+
? path_1.default.join(vmDir, config.initramfs)
|
|
93
|
+
: undefined;
|
|
94
|
+
// Check if image and disk files exist
|
|
95
|
+
if (!fs_1.default.existsSync(imageFile) || !fs_1.default.existsSync(diskFile)) {
|
|
96
|
+
throw new Error(`VM image or disk file not found for "${name}"`);
|
|
97
|
+
}
|
|
98
|
+
// Also check initramfs file if it exists in config
|
|
99
|
+
if (config.initramfs && !fs_1.default.existsSync(initramfsFile)) {
|
|
100
|
+
throw new Error(`VM initramfs file not found for "${name}": ${initramfsFile}`);
|
|
101
|
+
}
|
|
102
|
+
// Get serial number from settings.json or serial.txt for backward compatibility
|
|
103
|
+
let serialNumber = settings.serial;
|
|
104
|
+
// If serial not in settings, try reading from serial.txt (backward compatibility)
|
|
105
|
+
if (!serialNumber) {
|
|
106
|
+
const serialPath = (0, vm_1.getVmSerialPath)(name);
|
|
107
|
+
if (fs_1.default.existsSync(serialPath)) {
|
|
108
|
+
serialNumber = fs_1.default.readFileSync(serialPath, 'utf8').trim();
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
if (serialNumber) {
|
|
112
|
+
console.log(chalk_1.default.blue(`Using serial number: ${serialNumber}`));
|
|
113
|
+
}
|
|
114
|
+
else {
|
|
115
|
+
console.log(chalk_1.default.yellow(`No serial number found for VM "${name}"`));
|
|
116
|
+
}
|
|
117
|
+
// Determine QEMU binary based on architecture
|
|
118
|
+
const qemuBin = config.arch === 'amd64'
|
|
119
|
+
? 'qemu-system-x86_64'
|
|
120
|
+
: 'qemu-system-aarch64';
|
|
121
|
+
// Check if QEMU is installed first
|
|
122
|
+
try {
|
|
123
|
+
await checkQemuInstalled(qemuBin);
|
|
124
|
+
}
|
|
125
|
+
catch (error) {
|
|
126
|
+
// Attempt to install QEMU automatically
|
|
127
|
+
const installed = await attemptQemuInstall(qemuBin);
|
|
128
|
+
if (!installed) {
|
|
129
|
+
throw new Error(`QEMU binary '${qemuBin}' not found. Please install QEMU: ${error.message}`);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
// Build QEMU command arguments - must exactly match the bash script format
|
|
133
|
+
const args = buildExactQemuArgs(config.arch, imageFile, diskFile, initramfsFile, memory, serialNumber, headless, null);
|
|
134
|
+
console.log(chalk_1.default.blue(`Starting PhyOS VM "${name}"...`));
|
|
135
|
+
console.log(chalk_1.default.blue(`Memory allocation: ${memory}MB`));
|
|
136
|
+
if (isEmulated) {
|
|
137
|
+
console.log(chalk_1.default.yellow(`Running in cross-architecture emulation mode: ${config.arch} VM on ${hostArch} host`));
|
|
138
|
+
if (process.platform === 'darwin') {
|
|
139
|
+
console.log(chalk_1.default.yellow(`Note: Cross-architecture emulation on macOS can be significantly slower than native.`));
|
|
140
|
+
if (config.arch === 'amd64') {
|
|
141
|
+
console.log(chalk_1.default.yellow(`Tip: On Apple Silicon Macs, install Rosetta 2 for better x86 emulation performance.`));
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
else {
|
|
145
|
+
console.log(chalk_1.default.yellow(`Note: Performance may be reduced in emulation mode`));
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
else {
|
|
149
|
+
console.log(chalk_1.default.green(`Running with native architecture: ${config.arch}`));
|
|
150
|
+
}
|
|
151
|
+
// Log the exact command being run
|
|
152
|
+
console.log(chalk_1.default.gray(`Running: ${qemuBin} ${args.join(' ')}`));
|
|
153
|
+
// Show headless status
|
|
154
|
+
if (headless) {
|
|
155
|
+
console.log(chalk_1.default.blue(`Running in headless mode - no GUI display`));
|
|
156
|
+
}
|
|
157
|
+
else {
|
|
158
|
+
console.log(chalk_1.default.blue(`Running with GUI display`));
|
|
159
|
+
}
|
|
160
|
+
// Open log file for writing
|
|
161
|
+
const logPath = (0, vm_1.getVmLogPath)(name);
|
|
162
|
+
const timestamp = new Date().toISOString();
|
|
163
|
+
const logFile = fs_1.default.openSync(logPath, 'a');
|
|
164
|
+
// Add startup timestamp to log
|
|
165
|
+
fs_1.default.writeSync(logFile, `\n\n========== Starting VM at ${timestamp} ==========\n\n`);
|
|
166
|
+
fs_1.default.writeSync(logFile, `Command: ${qemuBin} ${args.join(' ')}\n\n`);
|
|
167
|
+
// Create temporary file to capture error output
|
|
168
|
+
const errorLogPath = path_1.default.join((0, vm_1.getVmDir)(name), 'error.log');
|
|
169
|
+
const errorLogFile = fs_1.default.openSync(errorLogPath, 'w');
|
|
170
|
+
// Spawn QEMU process in detached mode, but capture stderr to help with debugging
|
|
171
|
+
const qemuProcess = (0, child_process_1.spawn)(qemuBin, args, {
|
|
172
|
+
detached: true,
|
|
173
|
+
stdio: ['ignore', logFile, errorLogFile]
|
|
174
|
+
});
|
|
175
|
+
// Handle process startup errors
|
|
176
|
+
qemuProcess.on('error', (err) => {
|
|
177
|
+
fs_1.default.writeSync(errorLogFile, `Process startup error: ${err.message}\n`);
|
|
178
|
+
console.error(chalk_1.default.red(`Failed to start QEMU: ${err.message}`));
|
|
179
|
+
throw err;
|
|
180
|
+
});
|
|
181
|
+
// Wait briefly to see if process starts successfully
|
|
182
|
+
await new Promise(resolve => setTimeout(resolve, 500));
|
|
183
|
+
// Check if process is still running
|
|
184
|
+
const pid = qemuProcess.pid;
|
|
185
|
+
if (!pid) {
|
|
186
|
+
// Read any error output to diagnose the issue
|
|
187
|
+
let errorOutput = '';
|
|
188
|
+
try {
|
|
189
|
+
fs_1.default.closeSync(errorLogFile);
|
|
190
|
+
if (fs_1.default.existsSync(errorLogPath)) {
|
|
191
|
+
errorOutput = fs_1.default.readFileSync(errorLogPath, 'utf8');
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
catch (e) {
|
|
195
|
+
// Ignore errors reading the error log
|
|
196
|
+
}
|
|
197
|
+
if (errorOutput) {
|
|
198
|
+
console.error(chalk_1.default.red(`QEMU Error Output: ${errorOutput}`));
|
|
199
|
+
throw new Error(`Failed to start VM process. QEMU Error: ${errorOutput}`);
|
|
200
|
+
}
|
|
201
|
+
else {
|
|
202
|
+
throw new Error('Failed to start VM process - no PID returned. Check if QEMU is properly installed.');
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
// Save PID to file atomically
|
|
206
|
+
const pidPath = (0, vm_1.getVmPidPath)(name);
|
|
207
|
+
const tempPidPath = `${pidPath}.tmp`;
|
|
208
|
+
try {
|
|
209
|
+
// Write to a temporary file first
|
|
210
|
+
fs_1.default.writeFileSync(tempPidPath, String(pid));
|
|
211
|
+
// Verify the temp file was actually created
|
|
212
|
+
if (!fs_1.default.existsSync(tempPidPath)) {
|
|
213
|
+
throw new Error('Failed to create temporary PID file');
|
|
214
|
+
}
|
|
215
|
+
// Ensure it's written to disk by syncing the file descriptor
|
|
216
|
+
const tempFd = fs_1.default.openSync(tempPidPath, 'r');
|
|
217
|
+
fs_1.default.fsyncSync(tempFd);
|
|
218
|
+
fs_1.default.closeSync(tempFd);
|
|
219
|
+
// Atomically rename the temporary file to the final file
|
|
220
|
+
fs_1.default.renameSync(tempPidPath, pidPath);
|
|
221
|
+
}
|
|
222
|
+
catch (error) {
|
|
223
|
+
// Clean up temp file if it exists
|
|
224
|
+
try {
|
|
225
|
+
if (fs_1.default.existsSync(tempPidPath)) {
|
|
226
|
+
fs_1.default.unlinkSync(tempPidPath);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
catch (e) {
|
|
230
|
+
// Ignore cleanup errors
|
|
231
|
+
}
|
|
232
|
+
// If the PID file already exists (race condition), check if the process is actually running
|
|
233
|
+
if (fs_1.default.existsSync(pidPath)) {
|
|
234
|
+
try {
|
|
235
|
+
const existingPid = parseInt(fs_1.default.readFileSync(pidPath, 'utf8').trim(), 10);
|
|
236
|
+
// Check if the process is actually running
|
|
237
|
+
process.kill(existingPid, 0);
|
|
238
|
+
// If we get here, the process is running, so this is a legitimate "already running" case
|
|
239
|
+
console.log(chalk_1.default.yellow(`VM "${name}" is already running (PID: ${existingPid})`));
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
catch (pidError) {
|
|
243
|
+
// Process is not running, remove stale PID file and retry
|
|
244
|
+
try {
|
|
245
|
+
fs_1.default.unlinkSync(pidPath);
|
|
246
|
+
// Retry writing the PID file once
|
|
247
|
+
fs_1.default.writeFileSync(tempPidPath, String(pid));
|
|
248
|
+
// Verify the temp file was created
|
|
249
|
+
if (!fs_1.default.existsSync(tempPidPath)) {
|
|
250
|
+
throw new Error('Failed to create temporary PID file on retry');
|
|
251
|
+
}
|
|
252
|
+
const tempFd = fs_1.default.openSync(tempPidPath, 'r');
|
|
253
|
+
fs_1.default.fsyncSync(tempFd);
|
|
254
|
+
fs_1.default.closeSync(tempFd);
|
|
255
|
+
fs_1.default.renameSync(tempPidPath, pidPath);
|
|
256
|
+
}
|
|
257
|
+
catch (retryError) {
|
|
258
|
+
// If we still can't write the PID file, but the process is running,
|
|
259
|
+
// we can still consider it a success
|
|
260
|
+
try {
|
|
261
|
+
process.kill(pid, 0);
|
|
262
|
+
console.log(chalk_1.default.yellow(`VM "${name}" started successfully but PID file could not be written`));
|
|
263
|
+
console.log(chalk_1.default.yellow(`Process is running with PID: ${pid}`));
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
catch (killError) {
|
|
267
|
+
throw new Error(`Failed to write PID file after cleanup: ${retryError.message}`);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
else {
|
|
273
|
+
// If we can't write the PID file but the process is running, consider it a success
|
|
274
|
+
try {
|
|
275
|
+
process.kill(pid, 0);
|
|
276
|
+
console.log(chalk_1.default.yellow(`VM "${name}" started successfully but PID file could not be written`));
|
|
277
|
+
console.log(chalk_1.default.yellow(`Process is running with PID: ${pid}`));
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
catch (killError) {
|
|
281
|
+
throw new Error(`Failed to write PID file: ${error.message}`);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
// Now that we have the PID, generate the real args with proper socket paths
|
|
286
|
+
const finalArgs = buildExactQemuArgs(config.arch, imageFile, diskFile, initramfsFile, memory, serialNumber, headless, pid);
|
|
287
|
+
// Log the actual command that should be used for debugging
|
|
288
|
+
const actualCommand = `${qemuBin} ${finalArgs.join(' ')}`;
|
|
289
|
+
fs_1.default.writeSync(logFile, `Actual command (with PID): ${actualCommand}\n\n`);
|
|
290
|
+
// Create socket paths with actual PID (not placeholder) and VM name
|
|
291
|
+
// Ensure pid is a valid value before using it
|
|
292
|
+
const safePid = pid || 'temp';
|
|
293
|
+
const vmName = path_1.default.basename(path_1.default.dirname(imageFile || '.'));
|
|
294
|
+
const monitorSocketPath = pid ? `/tmp/qemu-monitor-${vmName}-${safePid}.sock` : `/tmp/qemu-monitor-${vmName}-temp.sock`;
|
|
295
|
+
const serialSocketPath = pid ? `/tmp/qemu-serial-${vmName}-${safePid}.sock` : `/tmp/qemu-serial-${vmName}-temp.sock`;
|
|
296
|
+
// Unref the process so it can run in the background
|
|
297
|
+
qemuProcess.unref();
|
|
298
|
+
// Log the actual socket paths for debugging
|
|
299
|
+
fs_1.default.writeSync(logFile, `Monitor socket: ${monitorSocketPath}\n`);
|
|
300
|
+
fs_1.default.writeSync(logFile, `Serial socket: ${serialSocketPath}\n\n`);
|
|
301
|
+
// Log troubleshooting info
|
|
302
|
+
fs_1.default.writeSync(logFile, `To connect to this VM: phy vm shell ${name}\n`);
|
|
303
|
+
fs_1.default.writeSync(logFile, `Socket must exist at: ${serialSocketPath}\n\n`);
|
|
304
|
+
// Remove the existing dummy file creation that causes "Socket operation on non-socket" error
|
|
305
|
+
// QEMU will create the proper socket files when it starts up
|
|
306
|
+
try {
|
|
307
|
+
// If dummy socket files already exist from a previous run, delete them first
|
|
308
|
+
if (fs_1.default.existsSync(serialSocketPath)) {
|
|
309
|
+
fs_1.default.unlinkSync(serialSocketPath);
|
|
310
|
+
}
|
|
311
|
+
if (fs_1.default.existsSync(monitorSocketPath)) {
|
|
312
|
+
fs_1.default.unlinkSync(monitorSocketPath);
|
|
313
|
+
}
|
|
314
|
+
// Wait briefly for QEMU to start and potentially create the socket
|
|
315
|
+
console.log(chalk_1.default.blue(`Waiting for VM to initialize...`));
|
|
316
|
+
await new Promise(resolve => setTimeout(resolve, 2000));
|
|
317
|
+
}
|
|
318
|
+
catch (error) {
|
|
319
|
+
console.log(chalk_1.default.yellow(`Warning when cleaning up socket files: ${error.message}`));
|
|
320
|
+
}
|
|
321
|
+
console.log(chalk_1.default.green(`VM "${name}" started successfully`));
|
|
322
|
+
console.log(`To view logs: ${chalk_1.default.blue('phy vm logs')} ${name}`);
|
|
323
|
+
console.log(`To stop VM: ${chalk_1.default.blue('phy vm stop')} ${name}`);
|
|
324
|
+
// If headless, inform about shell access
|
|
325
|
+
if (headless) {
|
|
326
|
+
console.log(`To connect to console: ${chalk_1.default.blue('phy vm shell')} ${name}`);
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
/**
|
|
330
|
+
* Detects the host system architecture
|
|
331
|
+
*/
|
|
332
|
+
function getHostArch() {
|
|
333
|
+
const systemArch = process.arch;
|
|
334
|
+
if (systemArch === 'x64')
|
|
335
|
+
return 'amd64';
|
|
336
|
+
if (systemArch === 'arm64')
|
|
337
|
+
return 'arm64';
|
|
338
|
+
return 'amd64'; // Default fallback
|
|
339
|
+
}
|
|
340
|
+
/**
|
|
341
|
+
* Detects if running on macOS
|
|
342
|
+
*/
|
|
343
|
+
function isMacOS() {
|
|
344
|
+
return process.platform === 'darwin';
|
|
345
|
+
}
|
|
346
|
+
/**
|
|
347
|
+
* Gets the default network interface name on macOS
|
|
348
|
+
*/
|
|
349
|
+
function getDefaultMacInterface() {
|
|
350
|
+
if (!isMacOS()) {
|
|
351
|
+
return null; // Only applicable on macOS
|
|
352
|
+
}
|
|
353
|
+
try {
|
|
354
|
+
// Run the command to get the default route
|
|
355
|
+
const output = (0, child_process_1.execSync)('route -n get default | grep interface:').toString();
|
|
356
|
+
// Parse the output (e.g., " interface: en8")
|
|
357
|
+
const match = output.match(/interface:\s*(\S+)/);
|
|
358
|
+
if (match && match[1]) {
|
|
359
|
+
console.log(chalk_1.default.blue(`Detected default network interface: ${match[1]}`));
|
|
360
|
+
return match[1];
|
|
361
|
+
}
|
|
362
|
+
console.log(chalk_1.default.yellow('Warning: Could not parse default interface from route command.'));
|
|
363
|
+
return null;
|
|
364
|
+
}
|
|
365
|
+
catch (error) {
|
|
366
|
+
console.log(chalk_1.default.yellow(`Warning: Failed to get default interface: ${error.message}`));
|
|
367
|
+
return null;
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
/**
|
|
371
|
+
* Builds QEMU command arguments to exactly match the original bash script
|
|
372
|
+
* Important: Each argument must be a separate array element without quotes
|
|
373
|
+
* Adds cross-architecture emulation support
|
|
374
|
+
*/
|
|
375
|
+
function buildExactQemuArgs(arch, imageFile, diskFile, initramfsFile, memory, serialNumber, headless, pid) {
|
|
376
|
+
const hostArch = getHostArch();
|
|
377
|
+
const isEmulated = arch !== hostArch;
|
|
378
|
+
const runningOnMac = isMacOS();
|
|
379
|
+
// Get VM name from the image file path
|
|
380
|
+
const vmName = path_1.default.basename(path_1.default.dirname(imageFile || '.'));
|
|
381
|
+
// Create socket paths with actual PID (not placeholder) and VM name
|
|
382
|
+
// Ensure pid is a valid value before using it
|
|
383
|
+
const safePid = pid || 'temp';
|
|
384
|
+
const monitorSocketPath = pid ? `/tmp/qemu-monitor-${vmName}-${safePid}.sock` : `/tmp/qemu-monitor-${vmName}-temp.sock`;
|
|
385
|
+
const serialSocketPath = pid ? `/tmp/qemu-serial-${vmName}-${safePid}.sock` : `/tmp/qemu-serial-${vmName}-temp.sock`;
|
|
386
|
+
// Ensure memory is valid and convert to string
|
|
387
|
+
const memoryStr = memory ? String(memory) : '2048';
|
|
388
|
+
// Function to generate network arguments based on platform
|
|
389
|
+
const getNetworkArgs = () => {
|
|
390
|
+
if (runningOnMac) {
|
|
391
|
+
const defaultInterface = getDefaultMacInterface();
|
|
392
|
+
if (defaultInterface) {
|
|
393
|
+
console.log(chalk_1.default.blue(`Using vmnet-bridged with interface: ${defaultInterface}`));
|
|
394
|
+
return [
|
|
395
|
+
'-netdev', `vmnet-bridged,id=net0,ifname=${defaultInterface}`,
|
|
396
|
+
'-device', 'virtio-net-pci,netdev=net0'
|
|
397
|
+
];
|
|
398
|
+
}
|
|
399
|
+
else {
|
|
400
|
+
console.log(chalk_1.default.yellow('Warning: Falling back to default vmnet-bridged mode without specific interface.'));
|
|
401
|
+
return [
|
|
402
|
+
'-netdev', 'vmnet-bridged,id=net0',
|
|
403
|
+
'-device', 'virtio-net-pci,netdev=net0'
|
|
404
|
+
];
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
else {
|
|
408
|
+
// Use user-mode networking on other platforms
|
|
409
|
+
return [
|
|
410
|
+
'-netdev', 'user,id=net0',
|
|
411
|
+
'-device', 'virtio-net-pci,netdev=net0'
|
|
412
|
+
];
|
|
413
|
+
}
|
|
414
|
+
};
|
|
415
|
+
if (arch === 'amd64') {
|
|
416
|
+
// x86_64 architecture
|
|
417
|
+
const args = [
|
|
418
|
+
'-machine', 'q35',
|
|
419
|
+
'-cpu', isEmulated ? 'max' : 'host',
|
|
420
|
+
'-m', memoryStr,
|
|
421
|
+
'-smp', '4',
|
|
422
|
+
'-kernel', imageFile,
|
|
423
|
+
'-drive', `if=none,file=${diskFile},format=raw,id=hd0`,
|
|
424
|
+
'-device', 'virtio-blk-pci,drive=hd0',
|
|
425
|
+
...getNetworkArgs(), // Add network args
|
|
426
|
+
'-append', 'root=/dev/vda rw console=ttyS0'
|
|
427
|
+
];
|
|
428
|
+
// Add KVM acceleration if running on Ubuntu/Debian Linux
|
|
429
|
+
if (process.platform === 'linux') {
|
|
430
|
+
// Check if running on Ubuntu/Debian by looking for common files
|
|
431
|
+
const isUbuntuDebian = fs_1.default.existsSync('/etc/debian_version') ||
|
|
432
|
+
fs_1.default.existsSync('/etc/lsb-release') ||
|
|
433
|
+
process.env.DISTRIB_ID === 'Ubuntu';
|
|
434
|
+
if (isUbuntuDebian) {
|
|
435
|
+
args.push('-enable-kvm');
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
// Add initramfs if available
|
|
439
|
+
if (initramfsFile) {
|
|
440
|
+
args.push('-initrd', initramfsFile);
|
|
441
|
+
}
|
|
442
|
+
// Add serial number if available
|
|
443
|
+
if (serialNumber) {
|
|
444
|
+
args.push('-smbios', `type=1,serial=${serialNumber}`);
|
|
445
|
+
}
|
|
446
|
+
// Add monitor and serial sockets with actual paths
|
|
447
|
+
args.push('-monitor', `unix:${monitorSocketPath},server,nowait`);
|
|
448
|
+
args.push('-serial', `unix:${serialSocketPath},server,nowait`);
|
|
449
|
+
if (headless) {
|
|
450
|
+
args.push('-display', 'none', '-nographic');
|
|
451
|
+
}
|
|
452
|
+
else {
|
|
453
|
+
// Use appropriate display for the platform
|
|
454
|
+
if (isEmulated) {
|
|
455
|
+
if (runningOnMac) {
|
|
456
|
+
// For macOS, use cocoa display
|
|
457
|
+
args.push('-display', 'cocoa');
|
|
458
|
+
}
|
|
459
|
+
else {
|
|
460
|
+
// For Linux and other platforms, try SDL
|
|
461
|
+
args.push('-display', 'sdl');
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
else {
|
|
465
|
+
args.push('-display', 'sdl', '-device', 'virtio-gpu-pci');
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
// Always add device controllers
|
|
469
|
+
args.push('-device', 'qemu-xhci', '-device', 'usb-tablet', '-device', 'usb-kbd');
|
|
470
|
+
return args;
|
|
471
|
+
}
|
|
472
|
+
else { // arm64
|
|
473
|
+
// Build kernel command line arguments
|
|
474
|
+
let kernelAppend = 'root=/dev/vda rw console=ttyAMA0';
|
|
475
|
+
if (serialNumber) {
|
|
476
|
+
kernelAppend += ` phygrid.serial=${serialNumber}`;
|
|
477
|
+
}
|
|
478
|
+
const args = [
|
|
479
|
+
'-machine', isEmulated ? 'virt' : 'virt,accel=hvf,highmem=off',
|
|
480
|
+
'-cpu', isEmulated ? 'cortex-a72' : 'host',
|
|
481
|
+
'-m', memoryStr,
|
|
482
|
+
'-smp', '4',
|
|
483
|
+
'-kernel', imageFile,
|
|
484
|
+
'-initrd', initramfsFile,
|
|
485
|
+
'-drive', `if=none,file=${diskFile},format=raw,id=hd0`,
|
|
486
|
+
'-device', 'virtio-blk-pci,drive=hd0',
|
|
487
|
+
...getNetworkArgs(), // Add network args
|
|
488
|
+
'-append', kernelAppend
|
|
489
|
+
];
|
|
490
|
+
// For ARM64 virt machines, SMBIOS is not fully supported, so we pass the serial via kernel cmdline
|
|
491
|
+
// Keep SMBIOS as fallback in case future QEMU versions support it better
|
|
492
|
+
if (serialNumber) {
|
|
493
|
+
args.push('-smbios', `type=1,serial=${serialNumber}`);
|
|
494
|
+
}
|
|
495
|
+
// Add monitor and serial sockets with actual paths
|
|
496
|
+
args.push('-monitor', `unix:${monitorSocketPath},server,nowait`);
|
|
497
|
+
args.push('-serial', `unix:${serialSocketPath},server,nowait`);
|
|
498
|
+
if (headless) {
|
|
499
|
+
args.push('-display', 'none', '-nographic');
|
|
500
|
+
}
|
|
501
|
+
else {
|
|
502
|
+
// Use appropriate display for the platform and emulation mode
|
|
503
|
+
if (isEmulated) {
|
|
504
|
+
if (runningOnMac) {
|
|
505
|
+
// For macOS emulating ARM, use cocoa display
|
|
506
|
+
args.push('-display', 'cocoa', '-device', 'virtio-gpu-pci');
|
|
507
|
+
}
|
|
508
|
+
else {
|
|
509
|
+
// For Linux and other platforms
|
|
510
|
+
args.push('-display', 'sdl', '-device', 'virtio-gpu-pci');
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
else {
|
|
514
|
+
args.push('-device', 'virtio-gpu-gl-pci', '-display', 'cocoa,gl=es', '-device', 'virtio-gpu-pci');
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
args.push('-device', 'qemu-xhci', '-device', 'usb-tablet', '-device', 'usb-kbd');
|
|
518
|
+
return args;
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
/**
|
|
522
|
+
* Check if QEMU is installed and available, prioritizing phygrid/qemu-virgl on macOS aarch64
|
|
523
|
+
*/
|
|
524
|
+
async function checkQemuInstalled(qemuBin) {
|
|
525
|
+
if (qemuBin === 'qemu-system-aarch64' && isMacOS()) {
|
|
526
|
+
// macOS aarch64: Prioritize phygrid/qemu-virgl/qemu-virgl
|
|
527
|
+
try {
|
|
528
|
+
// Use the new helper for brew commands
|
|
529
|
+
(0, vm_1.runBrewCommandAsUser)(['ls', '--versions', 'phygrid/qemu-virgl/qemu-virgl']);
|
|
530
|
+
console.log(chalk_1.default.green('Found installed phygrid/qemu-virgl/qemu-virgl.'));
|
|
531
|
+
return true;
|
|
532
|
+
}
|
|
533
|
+
catch (phygridError) {
|
|
534
|
+
// phygrid/qemu-virgl/qemu-virgl is NOT installed. Check for standard qemu conflict.
|
|
535
|
+
try {
|
|
536
|
+
// Use the new helper for brew commands
|
|
537
|
+
(0, vm_1.runBrewCommandAsUser)(['ls', '--versions', 'qemu']);
|
|
538
|
+
// Standard qemu IS installed. Does it provide the binary?
|
|
539
|
+
try {
|
|
540
|
+
await execPromise(`which ${qemuBin}`); // Keep execPromise for non-brew 'which'
|
|
541
|
+
// Standard qemu is installed AND provides the binary - CONFLICT!
|
|
542
|
+
throw new Error(`Conflict: Standard 'qemu' package provides ${qemuBin}. Please uninstall it ('brew uninstall qemu') and install the required 'phygrid/qemu-virgl/qemu-virgl' formula.`);
|
|
543
|
+
}
|
|
544
|
+
catch (whichError) {
|
|
545
|
+
// Standard qemu installed, but doesn't provide the binary (unexpected?) or which failed.
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
catch (standardQemuError) {
|
|
549
|
+
// Standard qemu is also not installed. Good.
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
// Standard check using 'which' for non-macOS-aarch64 or as a fallback
|
|
554
|
+
return new Promise((resolve, reject) => {
|
|
555
|
+
(0, child_process_1.exec)(`which ${qemuBin}`, (error, stdout, stderr) => {
|
|
556
|
+
if (error) {
|
|
557
|
+
let message = `${qemuBin} is not installed or not in PATH.`;
|
|
558
|
+
if (qemuBin === 'qemu-system-aarch64' && isMacOS()) {
|
|
559
|
+
message += ` Ensure 'phygrid/qemu-virgl/qemu-virgl' is installed via Homebrew.`;
|
|
560
|
+
}
|
|
561
|
+
reject(new Error(message));
|
|
562
|
+
return;
|
|
563
|
+
}
|
|
564
|
+
if (!(qemuBin === 'qemu-system-aarch64' && isMacOS())) {
|
|
565
|
+
console.log(chalk_1.default.green(`Found ${qemuBin} at: ${stdout.trim()}`));
|
|
566
|
+
}
|
|
567
|
+
resolve(true);
|
|
568
|
+
});
|
|
569
|
+
});
|
|
570
|
+
}
|
|
571
|
+
/**
|
|
572
|
+
* Attempts to install QEMU based on the current OS
|
|
573
|
+
*/
|
|
574
|
+
async function attemptQemuInstall(qemuBin) {
|
|
575
|
+
const platform = process.platform;
|
|
576
|
+
let targetPackageName;
|
|
577
|
+
let isPhygridFormula = false;
|
|
578
|
+
let manualInstallInstructions = `Please install ${qemuBin} manually.`;
|
|
579
|
+
if (platform === 'linux') {
|
|
580
|
+
// Determine Linux package name and manual instructions
|
|
581
|
+
try {
|
|
582
|
+
await execPromise('which apt-get');
|
|
583
|
+
if (qemuBin === 'qemu-system-x86_64') {
|
|
584
|
+
targetPackageName = 'qemu-system-x86';
|
|
585
|
+
}
|
|
586
|
+
else {
|
|
587
|
+
targetPackageName = 'qemu-system-arm qemu-efi-aarch64 qemu-system-aarch64';
|
|
588
|
+
}
|
|
589
|
+
manualInstallInstructions = `sudo apt-get update && sudo apt-get install -y ${targetPackageName.split(' ').join(' ')}`;
|
|
590
|
+
}
|
|
591
|
+
catch (e) {
|
|
592
|
+
try {
|
|
593
|
+
await execPromise('which yum');
|
|
594
|
+
if (qemuBin === 'qemu-system-x86_64') {
|
|
595
|
+
targetPackageName = 'qemu-system-x86';
|
|
596
|
+
}
|
|
597
|
+
else {
|
|
598
|
+
targetPackageName = 'qemu-system-aarch64';
|
|
599
|
+
}
|
|
600
|
+
manualInstallInstructions = `sudo yum install -y ${targetPackageName}`;
|
|
601
|
+
}
|
|
602
|
+
catch (e2) {
|
|
603
|
+
try {
|
|
604
|
+
await execPromise('which pacman');
|
|
605
|
+
targetPackageName = 'qemu';
|
|
606
|
+
manualInstallInstructions = `sudo pacman -S ${targetPackageName}`;
|
|
607
|
+
}
|
|
608
|
+
catch (e3) {
|
|
609
|
+
console.log(chalk_1.default.yellow('Could not detect Linux package manager for automatic installation'));
|
|
610
|
+
return false;
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
else if (platform === 'darwin') {
|
|
616
|
+
// Determine macOS package name and handle potential conflicts
|
|
617
|
+
try {
|
|
618
|
+
await execPromise('which brew');
|
|
619
|
+
if (qemuBin === 'qemu-system-aarch64') {
|
|
620
|
+
targetPackageName = 'phygrid/qemu-virgl/qemu-virgl';
|
|
621
|
+
isPhygridFormula = true;
|
|
622
|
+
manualInstallInstructions = `1. brew tap phygrid/qemu-virgl\n2. brew install ${targetPackageName}`;
|
|
623
|
+
// Check for conflicting standard qemu
|
|
624
|
+
try {
|
|
625
|
+
(0, vm_1.runBrewCommandAsUser)(['ls', '--versions', 'qemu']);
|
|
626
|
+
console.log(chalk_1.default.yellow("Warning: Found standard 'qemu' package installed, which may conflict."));
|
|
627
|
+
const { confirmUninstall } = await inquirer_1.default.prompt([{
|
|
628
|
+
type: 'confirm',
|
|
629
|
+
name: 'confirmUninstall',
|
|
630
|
+
message: `To install ${targetPackageName}, the standard 'qemu' package must be uninstalled first. Uninstall 'qemu' now?`,
|
|
631
|
+
default: false
|
|
632
|
+
}]);
|
|
633
|
+
if (confirmUninstall) {
|
|
634
|
+
console.log(chalk_1.default.blue('Uninstalling standard qemu (as user)...'));
|
|
635
|
+
(0, vm_1.runBrewCommandAsUser)(['uninstall', 'qemu']);
|
|
636
|
+
console.log(chalk_1.default.green('Standard qemu uninstalled.'));
|
|
637
|
+
}
|
|
638
|
+
else {
|
|
639
|
+
console.log(chalk_1.default.red("Installation aborted. Please manually uninstall 'qemu'."));
|
|
640
|
+
return false;
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
catch (e) { /* Standard qemu not found, ignore */ }
|
|
644
|
+
}
|
|
645
|
+
else { // x86_64 on macOS
|
|
646
|
+
targetPackageName = 'qemu';
|
|
647
|
+
manualInstallInstructions = `brew install ${targetPackageName}`;
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
catch (e) {
|
|
651
|
+
// Handle brew check/execution errors
|
|
652
|
+
if (e.message && e.message.includes('Failed to run brew command')) {
|
|
653
|
+
console.error(chalk_1.default.red(`Brew command failed: ${e.message}`));
|
|
654
|
+
}
|
|
655
|
+
else {
|
|
656
|
+
console.log(chalk_1.default.yellow('Homebrew (brew) command not found or failed. Ensure it is installed and in PATH.'));
|
|
657
|
+
console.log(chalk_1.default.blue('Visit https://brew.sh'));
|
|
658
|
+
}
|
|
659
|
+
return false;
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
else {
|
|
663
|
+
console.log(chalk_1.default.yellow(`Automatic installation not supported on ${platform}.`));
|
|
664
|
+
return false;
|
|
665
|
+
}
|
|
666
|
+
// Ensure we determined a package name
|
|
667
|
+
if (!targetPackageName) {
|
|
668
|
+
console.log(chalk_1.default.red('Internal error: Could not determine package name for installation.'));
|
|
669
|
+
return false;
|
|
670
|
+
}
|
|
671
|
+
// Confirm installation with user
|
|
672
|
+
const { confirmInstall } = await inquirer_1.default.prompt([{
|
|
673
|
+
type: 'confirm',
|
|
674
|
+
name: 'confirmInstall',
|
|
675
|
+
message: `${targetPackageName} is required but not installed. Install now?`,
|
|
676
|
+
default: true
|
|
677
|
+
}]);
|
|
678
|
+
if (!confirmInstall) {
|
|
679
|
+
console.log(chalk_1.default.yellow('Installation cancelled by user.'));
|
|
680
|
+
console.log(chalk_1.default.blue(`Manual instructions: ${manualInstallInstructions}`));
|
|
681
|
+
return false;
|
|
682
|
+
}
|
|
683
|
+
// Perform the installation
|
|
684
|
+
console.log(chalk_1.default.blue(`Attempting to install ${targetPackageName}...`));
|
|
685
|
+
try {
|
|
686
|
+
if (platform === 'darwin') {
|
|
687
|
+
// macOS installation (using helper)
|
|
688
|
+
if (isPhygridFormula) {
|
|
689
|
+
console.log(chalk_1.default.gray('Running: brew tap phygrid/qemu-virgl (as user)'));
|
|
690
|
+
(0, vm_1.runBrewCommandAsUser)(['tap', 'phygrid/qemu-virgl']);
|
|
691
|
+
}
|
|
692
|
+
console.log(chalk_1.default.gray(`Running: brew install ${targetPackageName} (as user)`));
|
|
693
|
+
(0, vm_1.runBrewCommandAsUser)(['install', targetPackageName]);
|
|
694
|
+
}
|
|
695
|
+
else {
|
|
696
|
+
// Linux installation (requires sudo directly)
|
|
697
|
+
console.log(chalk_1.default.gray(`Running: ${manualInstallInstructions}`));
|
|
698
|
+
await new Promise((resolve, reject) => {
|
|
699
|
+
// Execute the command string which includes sudo
|
|
700
|
+
const childProcess = (0, child_process_1.spawn)(manualInstallInstructions, { shell: true, stdio: 'inherit' });
|
|
701
|
+
childProcess.on('close', (code) => code === 0 ? resolve(true) : reject(new Error(`Installation failed with code ${code}`)));
|
|
702
|
+
childProcess.on('error', reject);
|
|
703
|
+
});
|
|
704
|
+
}
|
|
705
|
+
console.log(chalk_1.default.green(`Successfully initiated installation for ${targetPackageName}`));
|
|
706
|
+
// Verify installation (may take a moment for PATH to update)
|
|
707
|
+
console.log(chalk_1.default.blue('Verifying installation...'));
|
|
708
|
+
await new Promise(resolve => setTimeout(resolve, 1500)); // Brief pause
|
|
709
|
+
try {
|
|
710
|
+
await checkQemuInstalled(qemuBin);
|
|
711
|
+
console.log(chalk_1.default.green('Verification successful.'));
|
|
712
|
+
return true;
|
|
713
|
+
}
|
|
714
|
+
catch (e) {
|
|
715
|
+
console.log(chalk_1.default.red(`Installation done, but ${qemuBin} verification failed.`));
|
|
716
|
+
console.log(chalk_1.default.yellow(e.message || 'Unknown verification error.'));
|
|
717
|
+
console.log(chalk_1.default.yellow('Check PATH or try running the phy command again.'));
|
|
718
|
+
return false;
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
catch (error) {
|
|
722
|
+
console.log(chalk_1.default.red(`Failed to install ${targetPackageName}: ${error.message}`));
|
|
723
|
+
console.log(chalk_1.default.yellow('Please try the manual installation steps:'));
|
|
724
|
+
console.log(chalk_1.default.blue(manualInstallInstructions));
|
|
725
|
+
return false;
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
/**
|
|
729
|
+
* Promise-based wrapper for exec
|
|
730
|
+
*/
|
|
731
|
+
function execPromise(command) {
|
|
732
|
+
return new Promise((resolve, reject) => {
|
|
733
|
+
(0, child_process_1.exec)(command, (error, stdout, stderr) => {
|
|
734
|
+
if (error) {
|
|
735
|
+
// Log stderr for context if available
|
|
736
|
+
if (stderr) {
|
|
737
|
+
// console.error(`Command failed: ${command}\nstderr: ${stderr.trim()}`);
|
|
738
|
+
}
|
|
739
|
+
reject(error);
|
|
740
|
+
return;
|
|
741
|
+
}
|
|
742
|
+
resolve(stdout.trim());
|
|
743
|
+
});
|
|
744
|
+
});
|
|
745
|
+
}
|
|
746
|
+
/**
|
|
747
|
+
* Starts one or more PhyOS VMs
|
|
748
|
+
*/
|
|
749
|
+
async function startVm(names, options) {
|
|
750
|
+
// Handle single name (for backward compatibility)
|
|
751
|
+
const vmNames = Array.isArray(names) ? names : [names];
|
|
752
|
+
if (vmNames.length === 0) {
|
|
753
|
+
throw new Error('At least one VM name is required');
|
|
754
|
+
}
|
|
755
|
+
// Check which VMs exist and which don't
|
|
756
|
+
const existingVms = [];
|
|
757
|
+
const missingVms = [];
|
|
758
|
+
const alreadyRunningVms = [];
|
|
759
|
+
vmNames.forEach(name => {
|
|
760
|
+
if (!(0, vm_1.vmExists)(name)) {
|
|
761
|
+
missingVms.push(name);
|
|
762
|
+
}
|
|
763
|
+
else if ((0, vm_1.isVmRunning)(name)) {
|
|
764
|
+
alreadyRunningVms.push(name);
|
|
765
|
+
}
|
|
766
|
+
else {
|
|
767
|
+
existingVms.push(name);
|
|
768
|
+
}
|
|
769
|
+
});
|
|
770
|
+
// Report status
|
|
771
|
+
if (missingVms.length > 0) {
|
|
772
|
+
console.error(chalk_1.default.red(`❗️ The following VMs were not found: ${missingVms.join(', ')}`));
|
|
773
|
+
}
|
|
774
|
+
if (alreadyRunningVms.length > 0) {
|
|
775
|
+
console.log(chalk_1.default.yellow(`⚠️ The following VMs are already running: ${alreadyRunningVms.join(', ')}`));
|
|
776
|
+
}
|
|
777
|
+
// If no VMs to start, exit
|
|
778
|
+
if (existingVms.length === 0) {
|
|
779
|
+
if (alreadyRunningVms.length > 0 && missingVms.length === 0) {
|
|
780
|
+
console.log(chalk_1.default.green('All specified VMs are already running'));
|
|
781
|
+
return;
|
|
782
|
+
}
|
|
783
|
+
throw new Error('No VMs found to start');
|
|
784
|
+
}
|
|
785
|
+
// Show what will be started
|
|
786
|
+
console.log(chalk_1.default.blue(`Starting ${existingVms.length} VM${existingVms.length > 1 ? 's' : ''}: ${existingVms.join(', ')}`));
|
|
787
|
+
// Start VMs sequentially to avoid resource conflicts
|
|
788
|
+
const startedVms = [];
|
|
789
|
+
const failedVms = [];
|
|
790
|
+
for (const name of existingVms) {
|
|
791
|
+
try {
|
|
792
|
+
console.log(chalk_1.default.blue(`\n[${startedVms.length + failedVms.length + alreadyRunningVms.length + 1}/${existingVms.length}] Starting VM: ${name}`));
|
|
793
|
+
// Double-check if VM is running before starting (in case it was started by another process)
|
|
794
|
+
if ((0, vm_1.isVmRunning)(name)) {
|
|
795
|
+
console.log(chalk_1.default.yellow(`VM "${name}" is already running`));
|
|
796
|
+
alreadyRunningVms.push(name);
|
|
797
|
+
continue;
|
|
798
|
+
}
|
|
799
|
+
await startSingleVm(name, options);
|
|
800
|
+
startedVms.push(name);
|
|
801
|
+
console.log(chalk_1.default.green(`✅ VM "${name}" started successfully`));
|
|
802
|
+
}
|
|
803
|
+
catch (error) {
|
|
804
|
+
// Check if the VM is actually running despite the error (race condition)
|
|
805
|
+
if ((0, vm_1.isVmRunning)(name)) {
|
|
806
|
+
console.log(chalk_1.default.yellow(`VM "${name}" is already running (detected after start attempt)`));
|
|
807
|
+
alreadyRunningVms.push(name);
|
|
808
|
+
}
|
|
809
|
+
else {
|
|
810
|
+
// Check if the process is running by trying to find it in the process list
|
|
811
|
+
// This handles cases where PID file operations failed but the VM is actually running
|
|
812
|
+
try {
|
|
813
|
+
const config = (0, vm_1.readVmConfig)(name);
|
|
814
|
+
const qemuBin = config.arch === 'amd64' ? 'qemu-system-x86_64' : 'qemu-system-aarch64';
|
|
815
|
+
// Use ps to check if the QEMU process is running for this VM
|
|
816
|
+
const psOutput = (0, child_process_1.execSync)(`ps aux | grep "${qemuBin}" | grep "${name}" | grep -v grep`, { encoding: 'utf8' });
|
|
817
|
+
if (psOutput.trim()) {
|
|
818
|
+
console.log(chalk_1.default.yellow(`VM "${name}" is running (detected via process list) but PID file is missing`));
|
|
819
|
+
console.log(chalk_1.default.yellow(`This may be due to permission issues with PID file creation`));
|
|
820
|
+
// Try to create the PID file manually
|
|
821
|
+
if ((0, vm_1.createPidFileForRunningVm)(name)) {
|
|
822
|
+
console.log(chalk_1.default.green(`✅ Successfully created PID file for VM "${name}"`));
|
|
823
|
+
}
|
|
824
|
+
else {
|
|
825
|
+
console.log(chalk_1.default.yellow(`⚠️ Could not create PID file for VM "${name}" - VM status may be inconsistent`));
|
|
826
|
+
}
|
|
827
|
+
alreadyRunningVms.push(name);
|
|
828
|
+
}
|
|
829
|
+
else {
|
|
830
|
+
console.error(chalk_1.default.red(`❗️ Failed to start VM "${name}": ${error.message}`));
|
|
831
|
+
failedVms.push({ name, error: error.message });
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
catch (psError) {
|
|
835
|
+
// If we can't check the process list, fall back to the original error
|
|
836
|
+
console.error(chalk_1.default.red(`❗️ Failed to start VM "${name}": ${error.message}`));
|
|
837
|
+
failedVms.push({ name, error: error.message });
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
}
|
|
842
|
+
// Summary
|
|
843
|
+
console.log(chalk_1.default.blue('\n📊 Summary:'));
|
|
844
|
+
if (startedVms.length > 0) {
|
|
845
|
+
console.log(chalk_1.default.green(`✅ Successfully started ${startedVms.length} VM${startedVms.length > 1 ? 's' : ''}: ${startedVms.join(', ')}`));
|
|
846
|
+
}
|
|
847
|
+
if (alreadyRunningVms.length > 0) {
|
|
848
|
+
console.log(chalk_1.default.yellow(`⚠️ Already running: ${alreadyRunningVms.join(', ')}`));
|
|
849
|
+
}
|
|
850
|
+
if (failedVms.length > 0) {
|
|
851
|
+
console.log(chalk_1.default.red(`❌ Failed to start ${failedVms.length} VM${failedVms.length > 1 ? 's' : ''}: ${failedVms.map(f => f.name).join(', ')}`));
|
|
852
|
+
}
|
|
853
|
+
if (missingVms.length > 0) {
|
|
854
|
+
console.log(chalk_1.default.yellow(`⚠️ VMs not found: ${missingVms.join(', ')}`));
|
|
855
|
+
}
|
|
856
|
+
// Throw error if any operations failed
|
|
857
|
+
if (failedVms.length > 0 || missingVms.length > 0) {
|
|
858
|
+
throw new Error(`Some VM start operations failed or VMs were not found`);
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
//# sourceMappingURL=start.js.map
|