koffi 1.0.1 → 1.0.2

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.
Files changed (48) hide show
  1. package/CMakeLists.txt +12 -11
  2. package/build/qemu/1.0.2/koffi_darwin_x64.tar.gz +0 -0
  3. package/build/qemu/1.0.2/koffi_freebsd_arm64.tar.gz +0 -0
  4. package/build/qemu/1.0.2/koffi_freebsd_ia32.tar.gz +0 -0
  5. package/build/qemu/1.0.2/koffi_freebsd_x64.tar.gz +0 -0
  6. package/build/qemu/1.0.2/koffi_linux_arm.tar.gz +0 -0
  7. package/build/qemu/1.0.2/koffi_linux_arm64.tar.gz +0 -0
  8. package/build/qemu/1.0.2/koffi_linux_ia32.tar.gz +0 -0
  9. package/build/qemu/1.0.2/koffi_linux_x64.tar.gz +0 -0
  10. package/build/qemu/1.0.2/koffi_win32_ia32.tar.gz +0 -0
  11. package/build/qemu/1.0.2/koffi_win32_x64.tar.gz +0 -0
  12. package/package.json +8 -4
  13. package/qemu/qemu.js +794 -0
  14. package/qemu/registry/machines.json +415 -0
  15. package/qemu/registry/sha256sum.txt +45 -0
  16. package/src/{call_arm32.cc → abi_arm32.cc} +76 -50
  17. package/src/{call_arm32_fwd.S → abi_arm32_fwd.S} +0 -0
  18. package/src/{call_arm64.cc → abi_arm64.cc} +79 -51
  19. package/src/{call_arm64_fwd.S → abi_arm64_fwd.S} +0 -0
  20. package/src/{call_x64_sysv.cc → abi_x64_sysv.cc} +77 -49
  21. package/src/{call_x64_sysv_fwd.S → abi_x64_sysv_fwd.S} +0 -0
  22. package/src/{call_x64_win.cc → abi_x64_win.cc} +71 -47
  23. package/src/{call_x64_win_fwd.asm → abi_x64_win_fwd.asm} +0 -0
  24. package/src/{call_x86.cc → abi_x86.cc} +74 -56
  25. package/src/{call_x86_fwd.S → abi_x86_fwd.S} +0 -0
  26. package/src/{call_x86_fwd.asm → abi_x86_fwd.asm} +0 -0
  27. package/src/call.cc +228 -0
  28. package/src/call.hh +96 -4
  29. package/src/ffi.cc +7 -2
  30. package/src/ffi.hh +2 -0
  31. package/src/util.cc +11 -157
  32. package/src/util.hh +0 -91
  33. package/test/CMakeLists.txt +1 -0
  34. package/test/misc.c +289 -0
  35. package/test/misc.def +3 -0
  36. package/test/misc.js +180 -0
  37. package/test/raylib.js +165 -0
  38. package/test/sqlite.js +104 -0
  39. package/build/qemu/1.0.1/koffi_darwin_x64.tar.gz +0 -0
  40. package/build/qemu/1.0.1/koffi_freebsd_arm64.tar.gz +0 -0
  41. package/build/qemu/1.0.1/koffi_freebsd_ia32.tar.gz +0 -0
  42. package/build/qemu/1.0.1/koffi_freebsd_x64.tar.gz +0 -0
  43. package/build/qemu/1.0.1/koffi_linux_arm.tar.gz +0 -0
  44. package/build/qemu/1.0.1/koffi_linux_arm64.tar.gz +0 -0
  45. package/build/qemu/1.0.1/koffi_linux_ia32.tar.gz +0 -0
  46. package/build/qemu/1.0.1/koffi_linux_x64.tar.gz +0 -0
  47. package/build/qemu/1.0.1/koffi_win32_ia32.tar.gz +0 -0
  48. package/build/qemu/1.0.1/koffi_win32_x64.tar.gz +0 -0
package/qemu/qemu.js ADDED
@@ -0,0 +1,794 @@
1
+ #!/usr/bin/env node
2
+
3
+ // This program is free software: you can redistribute it and/or modify
4
+ // it under the terms of the GNU Affero General Public License as published by
5
+ // the Free Software Foundation, either version 3 of the License, or
6
+ // (at your option) any later version.
7
+ //
8
+ // This program is distributed in the hope that it will be useful,
9
+ // but WITHOUT ANY WARRANTY; without even the implied warranty of
10
+ // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11
+ // GNU Affero General Public License for more details.
12
+ //
13
+ // You should have received a copy of the GNU Affero General Public License
14
+ // along with this program. If not, see https://www.gnu.org/licenses/.
15
+
16
+ 'use strict';
17
+
18
+ const process = require('process');
19
+ const fs = require('fs');
20
+ const os = require('os');
21
+ const path = require('path');
22
+ const util = require('util');
23
+ const { spawn, spawnSync } = require('child_process');
24
+ const { NodeSSH } = require('node-ssh');
25
+ const chalk = require('chalk');
26
+ const minimatch = require('minimatch');
27
+ const tar = require('tar');
28
+
29
+ // Globals
30
+
31
+ let script_dir = null;
32
+ let root_dir = null;
33
+
34
+ let machines = null;
35
+ let accelerate = true;
36
+ let ignore = new Set;
37
+
38
+ let qemu_prefix = null;
39
+
40
+ // Main
41
+
42
+ main();
43
+
44
+ async function main() {
45
+ script_dir = fs.realpathSync(path.dirname(__filename));
46
+ root_dir = fs.realpathSync(script_dir + '/../..');
47
+
48
+ // All the code assumes we are working from the script directory
49
+ process.chdir(script_dir);
50
+
51
+ let command = test;
52
+ let patterns = [];
53
+
54
+ // Parse options
55
+ {
56
+ let i = 2;
57
+
58
+ if (process.argv.length >= 3 && process.argv[2][0] != '-') {
59
+ switch (process.argv[2]) {
60
+ case 'pack': { command = pack; i++ } break;
61
+ case 'test': { command = test; i++; } break;
62
+ case 'start': { command = start; i++; } break;
63
+ case 'stop': { command = stop; i++; } break;
64
+ case 'info': { command = info; i++; } break;
65
+ case 'ssh': { command = ssh; i++; } break;
66
+ case 'reset': { command = reset; i++; } break;
67
+ }
68
+ }
69
+
70
+ for (; i < process.argv.length; i++) {
71
+ let arg = process.argv[i];
72
+ let value = null;
73
+
74
+ if (arg[0] == '-') {
75
+ if (arg.length > 2 && arg[1] != '-') {
76
+ value = arg.substr(2);
77
+ arg = arg.substr(0, 2);
78
+ } else if (arg[1] == '-') {
79
+ let offset = arg.indexOf('=');
80
+
81
+ if (offset > 2 && arg.length > offset + 1) {
82
+ value = arg.substr(offset + 1);
83
+ arg = arg.substr(0, offset);
84
+ }
85
+ }
86
+ if (value == null && process.argv[i + 1] != null && process.argv[i + 1][0] != '-') {
87
+ value = process.argv[i + 1];
88
+ i++; // Skip this value next iteration
89
+ }
90
+ }
91
+
92
+ if (arg == '--help') {
93
+ print_usage();
94
+ return;
95
+ } else if ((command == test || command == start) && arg == '--no-accel') {
96
+ accelerate = false;
97
+ } else if (arg[0] == '-') {
98
+ throw new Error(`Unexpected argument '${arg}'`);
99
+ } else {
100
+ if (arg.startsWith('__') || arg.match(/[\\/\.]/))
101
+ throw new Error(`Machine name '${arg} is not valid`);
102
+
103
+ patterns.push(arg);
104
+ }
105
+ }
106
+ }
107
+
108
+ // Sanity checks
109
+ if (parseInt(process.versions.node, 10) < 16)
110
+ throw new Error('Please use Node version >= 16');
111
+ if (spawnSync('ssh', ['-V']).status !== 0)
112
+ throw new Error('Missing ssh binary in PATH');
113
+
114
+ // Load machine registry
115
+ let machines_map;
116
+ {
117
+ let json = fs.readFileSync('registry/machines.json', { encoding: 'utf-8' });
118
+
119
+ machines_map = JSON.parse(json);
120
+ for (let key in machines_map) {
121
+ let machine = machines_map[key];
122
+
123
+ machine.key = key;
124
+ machine.started = false;
125
+
126
+ machine.qemu.accelerate = null;
127
+ if (accelerate) {
128
+ if (process.platform == 'linux') {
129
+ if (process.arch == 'x64' && (machine.info.arch == 'x64' || machine.info.arch == 'ia32'))
130
+ machine.qemu.accelerate = 'kvm';
131
+ } else if (process.platform == 'win32') {
132
+ if (process.arch == 'x64' && machine.info.arch == 'x64')
133
+ machine.qemu.accelerate = 'whpx';
134
+ }
135
+ }
136
+ }
137
+ }
138
+
139
+ if (patterns.length) {
140
+ machines = new Set;
141
+
142
+ for (let pattern of patterns) {
143
+ let re = minimatch.makeRe(pattern);
144
+
145
+ for (let name in machines_map) {
146
+ let machine = machines_map[name];
147
+
148
+ if (name.match(re) || machine.name.match(re))
149
+ machines.add(name);
150
+ }
151
+ }
152
+
153
+ if (!machines.size) {
154
+ console.log('Could not match any machine');
155
+ process.exit(1);
156
+ }
157
+ } else {
158
+ machines = new Set(Object.keys(machines_map));
159
+
160
+ if (!machines.size) {
161
+ console.error('Could not detect any machine');
162
+ process.exit(1);
163
+ }
164
+ }
165
+
166
+ machines = Array.from(machines);
167
+ machines = machines.map(name => {
168
+ let machine = machines_map[name];
169
+ if (machine == null) {
170
+ machine = Object.values(machines_map).find(machine => machine.name == name);
171
+ if (machine == null)
172
+ throw new Error(`Could not find machine ${name}`);
173
+ }
174
+ return machine;
175
+ });
176
+
177
+ console.log('Machines:', machines.map(machine => machine.name).join(', '));
178
+ console.log();
179
+
180
+ try {
181
+ let success = await command();
182
+ process.exit(!success);
183
+ } catch (err) {
184
+ console.error(err);
185
+ process.exit(1);
186
+ }
187
+ }
188
+
189
+ function print_usage() {
190
+ let help = `Usage: node qemu.js [command] [options...]
191
+
192
+ Commands:
193
+ test Run the machines and perform the tests (default)
194
+ pack Use machines to package prebuilt Koffi binaries
195
+
196
+ start Start the machines but don't run anythingh
197
+ stop Stop running machines
198
+ info Print basic information about machine
199
+ ssh Start SSH session with specific machine
200
+
201
+ reset Reset initial disk snapshot
202
+
203
+ Options:
204
+ --no-accel Disable QEMU acceleration
205
+ `;
206
+
207
+ console.log(help);
208
+ }
209
+
210
+ // Commands
211
+
212
+ async function start(detach = true) {
213
+ let success = true;
214
+ let missing = 0;
215
+
216
+ check_qemu();
217
+
218
+ console.log('>> Starting up machines...');
219
+ await Promise.all(machines.map(async machine => {
220
+ if (ignore.has(machine))
221
+ return;
222
+
223
+ let dirname = `qemu/${machine.key}`;
224
+
225
+ if (!fs.existsSync(dirname)) {
226
+ log(machine, 'Missing files', chalk.bold.gray('[ignore]'));
227
+
228
+ ignore.add(machine);
229
+ missing++;
230
+
231
+ return;
232
+ }
233
+
234
+ // Version check
235
+ {
236
+ let filename = dirname + '/VERSION';
237
+ let version = fs.existsSync(filename) ? parseInt(fs.readFileSync(filename).toString(), 10) : 0;
238
+
239
+ if (version != machine.info.version) {
240
+ log(machine, 'Download newer machine', chalk.bold.gray('[ignore]'));
241
+
242
+ ignore.add(machine);
243
+ missing++;
244
+
245
+ return;
246
+ }
247
+ }
248
+
249
+ try {
250
+ await boot(machine, dirname, detach);
251
+
252
+ if (machine.started) {
253
+ let action = `Start (${machine.qemu.accelerate || 'emulated'})`;
254
+ log(machine, action, chalk.bold.green('[ok]'));
255
+ } else {
256
+ log(machine, 'Join', chalk.bold.green('[ok]'));
257
+ }
258
+ } catch (err) {
259
+ log(machine, 'Start', chalk.bold.red('[error]'));
260
+
261
+ ignore.add(machine);
262
+ missing++;
263
+ }
264
+ }));
265
+
266
+ if (success && missing == machines.length)
267
+ throw new Error('No machine available');
268
+
269
+ return success;
270
+ }
271
+
272
+ async function pack() {
273
+ let success = true;
274
+
275
+ let json = fs.readFileSync(root_dir + '/koffi/package.json', { encoding: 'utf-8' });
276
+ let version = JSON.parse(json).version;
277
+
278
+ console.log('>> Checking build archives...');
279
+ for (let machine of machines) {
280
+ let needed = false;
281
+
282
+ for (let suite in machine.builds) {
283
+ let build = machine.builds[suite];
284
+
285
+ let platform = build.platform || machine.info.platform;
286
+ let arch = build.arch || machine.info.arch;
287
+
288
+ let archive_filename = root_dir + `/koffi/build/qemu/${version}/koffi_${platform}_${arch}.tar.gz`;
289
+
290
+ if (fs.existsSync(archive_filename)) {
291
+ log(machine, `${suite} > Status`, chalk.bold.green(`[ok]`));
292
+ } else {
293
+ log(machine, `${suite} > Status`, chalk.bold.red(`[build]`));
294
+ needed = true;
295
+ }
296
+ }
297
+
298
+ if (!needed)
299
+ ignore.add(machine);
300
+ }
301
+
302
+ success &= await start(false);
303
+ success &= await copy(machine => Object.values(machine.builds).map(build => build.directory));
304
+
305
+ console.log('>> Run build commands...');
306
+ await Promise.all(machines.map(async machine => {
307
+ if (ignore.has(machine))
308
+ return;
309
+
310
+ await Promise.all(Object.keys(machine.builds).map(async suite => {
311
+ let build = machine.builds[suite];
312
+
313
+ let cmd = build.build;
314
+ let cwd = build.directory + '/koffi';
315
+
316
+ let start = process.hrtime.bigint();
317
+ let ret = await exec_remote(machine, cmd, cwd);
318
+ let time = Number((process.hrtime.bigint() - start) / 1000000n);
319
+
320
+ if (ret.code == 0) {
321
+ log(machine, `${suite} > Build`, chalk.bold.green(`[${(time / 1000).toFixed(2)}s]`));
322
+ } else {
323
+ log(machine, `${suite} > Build`, chalk.bold.red('[error]'));
324
+
325
+ if (ret.stdout || ret.stderr)
326
+ console.error('');
327
+
328
+ let align = log.align + 9;
329
+ if (ret.stdout) {
330
+ let str = ' '.repeat(align) + 'Standard output:\n' +
331
+ chalk.yellow(ret.stdout.replace(/^/gm, ' '.repeat(align + 4))) + '\n';
332
+ console.error(str);
333
+ }
334
+ if (ret.stderr) {
335
+ let str = ' '.repeat(align) + 'Standard error:\n' +
336
+ chalk.yellow(ret.stderr.replace(/^/gm, ' '.repeat(align + 4))) + '\n';
337
+ console.error(str);
338
+ }
339
+
340
+ success = false;
341
+ }
342
+ }));
343
+ }));
344
+
345
+ console.log('>> Get build artifacts');
346
+ {
347
+ let build_dir = root_dir + '/koffi/build/qemu';
348
+
349
+ // Clean up old files
350
+ if (fs.existsSync(build_dir)) {
351
+ for (let basename of fs.readdirSync(build_dir)) {
352
+ if (basename !== version)
353
+ unlink_recursive(build_dir + '/' + basename);
354
+ }
355
+ }
356
+
357
+ await Promise.all(machines.map(async machine => {
358
+ if (ignore.has(machine))
359
+ return;
360
+
361
+ let copied = true;
362
+
363
+ await Promise.all(Object.keys(machine.builds).map(async suite => {
364
+ let build = machine.builds[suite];
365
+
366
+ let platform = build.platform || machine.info.platform;
367
+ let arch = build.arch || machine.info.arch;
368
+
369
+ let src_dir = build.directory + '/koffi/build';
370
+ let dest_dir = build_dir + `/${version}/koffi_${platform}_${arch}`;
371
+ let dest_filename = dest_dir + '.tar.gz';
372
+
373
+ unlink_recursive(dest_dir + '/build');
374
+ fs.mkdirSync(dest_dir + '/build', { mode: 0o755, recursive: true });
375
+
376
+ try {
377
+ await machine.ssh.getDirectory(dest_dir + '/build', src_dir, {
378
+ recursive: false,
379
+ concurrency: 4,
380
+ validate: filename => !path.basename(filename).match(/^v[0-9]+/)
381
+ });
382
+
383
+ tar.c({
384
+ gzip: true,
385
+ file: dest_filename,
386
+ sync: true,
387
+ cwd: dest_dir + '/..'
388
+ }, [path.basename(dest_dir)]);
389
+ } catch (err) {
390
+ ignore.add(machine);
391
+ success = false;
392
+ copied = false;
393
+ }
394
+ }));
395
+
396
+ let status = copied ? chalk.bold.green('[ok]') : chalk.bold.red('[error]');
397
+ log(machine, 'Download', status);
398
+ }));
399
+ }
400
+
401
+ if (machines.some(machine => machine.started))
402
+ success &= await stop(false);
403
+
404
+ return success;
405
+ }
406
+
407
+ async function copy(func) {
408
+ let success = true;
409
+
410
+ let snapshot_dir = fs.mkdtempSync(path.join(os.tmpdir(), 'luigi_'));
411
+ process.on('exit', () => unlink_recursive(snapshot_dir));
412
+
413
+ console.log('>> Snapshot code...');
414
+ copy_recursive(root_dir, snapshot_dir, filename => {
415
+ let basename = path.basename(filename);
416
+
417
+ return basename !== '.git' &&
418
+ basename !== 'qemu' && !basename.startsWith('qemu_') &&
419
+ basename !== 'node_modules' &&
420
+ basename !== 'node' &&
421
+ basename !== 'build' &&
422
+ basename !== 'luiggi';
423
+ });
424
+
425
+ console.log('>> Copy source code...');
426
+ await Promise.all(machines.map(async machine => {
427
+ if (ignore.has(machine))
428
+ return;
429
+
430
+ let copied = true;
431
+
432
+ for (let directory of func(machine)) {
433
+ try {
434
+ await machine.ssh.exec('rm', ['-rf', directory]);
435
+ } catch (err) {
436
+ // Fails often on Windows (busy directory or whatever), but rarely a problem
437
+ }
438
+
439
+ try {
440
+ await machine.ssh.putDirectory(snapshot_dir, directory, {
441
+ recursive: true,
442
+ concurrency: 4
443
+ });
444
+ } catch (err) {
445
+ ignore.add(machine);
446
+ success = false;
447
+ copied = false;
448
+ }
449
+ }
450
+
451
+ let status = copied ? chalk.bold.green('[ok]') : chalk.bold.red('[error]');
452
+ log(machine, 'Copy', status);
453
+ }));
454
+
455
+ return success;
456
+ }
457
+
458
+ async function test() {
459
+ let success = true;
460
+
461
+ success &= await start(false);
462
+ success &= await copy(machine => Object.values(machine.tests).map(test => test.directory));
463
+
464
+ console.log('>> Run test commands...');
465
+ await Promise.all(machines.map(async machine => {
466
+ if (ignore.has(machine))
467
+ return;
468
+
469
+ await Promise.all(Object.keys(machine.tests).map(async suite => {
470
+ let test = machine.tests[suite];
471
+ let commands = {
472
+ ...test.build,
473
+ ...test.commands
474
+ };
475
+
476
+ for (let name in commands) {
477
+ let cmd = commands[name];
478
+ let cwd = test.directory + '/koffi';
479
+
480
+ let start = process.hrtime.bigint();
481
+ let ret = await exec_remote(machine, cmd, cwd);
482
+ let time = Number((process.hrtime.bigint() - start) / 1000000n);
483
+
484
+ if (ret.code == 0) {
485
+ log(machine, `${suite} > ${name}`, chalk.bold.green(`[${(time / 1000).toFixed(2)}s]`));
486
+ } else {
487
+ log(machine, `${suite} > ${name}`, chalk.bold.red('[error]'));
488
+
489
+ if (ret.stdout || ret.stderr)
490
+ console.error('');
491
+
492
+ let align = log.align + 9;
493
+ if (ret.stdout) {
494
+ let str = ' '.repeat(align) + 'Standard output:\n' +
495
+ chalk.yellow(ret.stdout.replace(/^/gm, ' '.repeat(align + 4))) + '\n';
496
+ console.error(str);
497
+ }
498
+ if (ret.stderr) {
499
+ let str = ' '.repeat(align) + 'Standard error:\n' +
500
+ chalk.yellow(ret.stderr.replace(/^/gm, ' '.repeat(align + 4))) + '\n';
501
+ console.error(str);
502
+ }
503
+
504
+ success = false;
505
+
506
+ if (name in test.build)
507
+ break;
508
+ }
509
+ }
510
+ }));
511
+ }));
512
+
513
+ if (machines.some(machine => machine.started))
514
+ success &= await stop(false);
515
+
516
+ console.log('');
517
+ if (success) {
518
+ console.log('>> Status: ' + chalk.bold.green('SUCCESS'));
519
+ if (ignore.size)
520
+ console.log(' (but some machines could not be tested)');
521
+ } else {
522
+ console.log('>> Status: ' + chalk.bold.red('FAILED'));
523
+ }
524
+
525
+ return success;
526
+ }
527
+
528
+ async function stop(all = true) {
529
+ let success = true;
530
+
531
+ console.log('>> Sending shutdown commands...');
532
+ await Promise.all(machines.map(async machine => {
533
+ if (ignore.has(machine))
534
+ return;
535
+ if (!machine.started && !all)
536
+ return;
537
+
538
+ if (machine.ssh == null) {
539
+ try {
540
+ await join(machine, 2);
541
+ } catch (err) {
542
+ log(machine, 'Already down', chalk.bold.green('[ok]'));
543
+ return;
544
+ }
545
+ }
546
+
547
+ try {
548
+ await new Promise(async (resolve, reject) => {
549
+ if (machine.ssh.connection == null) {
550
+ reject();
551
+ return;
552
+ }
553
+
554
+ machine.ssh.connection.on('close', resolve);
555
+ machine.ssh.connection.on('end', resolve);
556
+ wait(60000).then(() => { reject(new Error('Timeout')) });
557
+
558
+ exec_remote(machine, machine.info.shutdown);
559
+ });
560
+
561
+ log(machine, 'Stop', chalk.bold.green('[ok]'));
562
+ } catch (err) {
563
+ log(machine, 'Stop', chalk.bold.red('[error]'));
564
+ success = false;
565
+ }
566
+ }));
567
+
568
+ return success;
569
+ }
570
+
571
+ async function info() {
572
+ for (let machine of machines) {
573
+ console.log(`>> ${machine.name} (${machine.key})`);
574
+ console.log(` - SSH port: ${machine.info.ssh_port}`);
575
+ console.log(` - VNC port: ${machine.info.vnc_port}`);
576
+ console.log(` - Username: ${machine.info.username}`);
577
+ console.log(` - Password: ${machine.info.password}`);
578
+ }
579
+ }
580
+
581
+ async function ssh() {
582
+ if (machines.length != 1) {
583
+ console.error('The ssh command can only be used with one machine');
584
+ return false;
585
+ }
586
+
587
+ let machine = machines[0];
588
+
589
+ let args = [
590
+ '-p' + machine.info.password,
591
+ 'ssh', '-o', 'StrictHostKeyChecking=no',
592
+ '-o', 'UserKnownHostsFile=' + (process.platform == 'win32' ? '\\\\.\\NUL' : '/dev/null'),
593
+ '-p', machine.info.ssh_port, machine.info.username + '@127.0.0.1'
594
+ ];
595
+
596
+ let proc = spawnSync('sshpass', args, { stdio: 'inherit' });
597
+ if (proc.status !== 0) {
598
+ console.error('Connection failed');
599
+ return false;
600
+ }
601
+
602
+ return true;
603
+ }
604
+
605
+ async function reset() {
606
+ check_qemu();
607
+
608
+ let binary = qemu_prefix + 'qemu-img' + (process.platform == 'win32' ? '.exe' : '');
609
+
610
+ console.log('>> Restoring snapshots...')
611
+ await Promise.all(machines.map(machine => {
612
+ let dirname = `qemu/${machine.key}`;
613
+ let disk = dirname + '/disk.qcow2';
614
+
615
+ if (!fs.existsSync(dirname)) {
616
+ log(machine, 'Missing files', chalk.bold.gray('[ignore]'));
617
+ return;
618
+ }
619
+
620
+ let proc = spawnSync(binary, ['snapshot', disk, '-a', 'base'], { encoding: 'utf-8' });
621
+
622
+ if (proc.status === 0) {
623
+ log(machine, 'Reset disk', chalk.bold.green('[ok]'));
624
+ } else {
625
+ log(machine, 'Reset disk', chalk.bold.red('[error]'));
626
+
627
+ if (proc.stderr) {
628
+ console.error('');
629
+
630
+ let align = log.align + 9;
631
+ let str = ' '.repeat(align) + 'Standard error:\n' +
632
+ chalk.yellow(proc.stderr.replace(/^/gm, ' '.repeat(align + 4))) + '\n';
633
+ console.error(str);
634
+ }
635
+ }
636
+ }));
637
+ }
638
+
639
+ // Utility
640
+
641
+ function check_qemu() {
642
+ if (qemu_prefix != null)
643
+ return;
644
+
645
+ if (spawnSync('qemu-img', ['--version']).status === 0) {
646
+ qemu_prefix = '';
647
+ } else if (process.platform == 'win32') {
648
+ let proc = spawnSync('reg', ['query', 'HKEY_LOCAL_MACHINE\\SOFTWARE\\QEMU', '/v', 'Install_Dir']);
649
+
650
+ if (proc.status === 0) {
651
+ let matches = proc.stdout.toString('utf-8').match(/Install_Dir[ \t]+REG_[A-Z_]+[ \t]+(.*)+/);
652
+
653
+ if (matches != null) {
654
+ let prefix = matches[1].trim() + '/';
655
+ let binary = prefix + 'qemu-img.exe';
656
+
657
+ if (fs.existsSync(binary))
658
+ qemu_prefix = prefix;
659
+ }
660
+ }
661
+ }
662
+
663
+ if (qemu_prefix == null)
664
+ throw new Error('QEMU does not seem to be installed');
665
+ }
666
+
667
+ function copy_recursive(src, dest, validate = filename => true) {
668
+ let entries = fs.readdirSync(src, { withFileTypes: true });
669
+
670
+ for (let entry of entries) {
671
+ let filename = path.join(src, entry.name);
672
+ let destname = path.join(dest, entry.name);
673
+
674
+ if (!validate(filename))
675
+ continue;
676
+
677
+ if (entry.isDirectory()) {
678
+ fs.mkdirSync(destname, { mode: 0o755 });
679
+ copy_recursive(filename, destname, validate);
680
+ } else if (entry.isFile()) {
681
+ fs.copyFileSync(filename, destname);
682
+ }
683
+ }
684
+ }
685
+
686
+ function unlink_recursive(path) {
687
+ try {
688
+ if (fs.rmSync != null) {
689
+ fs.rmSync(path, { recursive: true, maxRetries: process.platform == 'win32' ? 3 : 0 });
690
+ } else {
691
+ fs.rmdirSync(path, { recursive: true, maxRetries: process.platform == 'win32' ? 3 : 0 });
692
+ }
693
+ } catch (err) {
694
+ if (err.code !== 'ENOENT')
695
+ throw err;
696
+ }
697
+ }
698
+
699
+ async function boot(machine, dirname, detach) {
700
+ let args = machine.qemu.arguments.slice();
701
+
702
+ if (machine.qemu.accelerate)
703
+ args.push('-accel', machine.qemu.accelerate);
704
+ // args.push('-display', 'none');
705
+
706
+ try {
707
+ let binary = qemu_prefix + machine.qemu.binary + (process.platform == 'win32' ? '.exe' : '');
708
+
709
+ let proc = spawn(binary, args, {
710
+ cwd: dirname,
711
+ detached: detach,
712
+ stdio: 'ignore'
713
+ });
714
+ if (detach)
715
+ proc.unref();
716
+
717
+ await new Promise((resolve, reject) => {
718
+ proc.on('spawn', () => wait(2 * 1000).then(resolve));
719
+ proc.on('error', reject);
720
+ proc.on('exit', reject);
721
+ });
722
+
723
+ await join(machine, 30);
724
+ machine.started = true;
725
+ } catch (err) {
726
+ if (typeof err != 'number')
727
+ throw err;
728
+
729
+ await join(machine, 2);
730
+ machine.started = false;
731
+ }
732
+ }
733
+
734
+ async function join(machine, tries) {
735
+ let ssh = new NodeSSH;
736
+
737
+ while (tries) {
738
+ try {
739
+ await ssh.connect({
740
+ host: '127.0.0.1',
741
+ port: machine.info.ssh_port,
742
+ username: machine.info.username,
743
+ password: machine.info.password,
744
+ tryKeyboard: true
745
+ });
746
+
747
+ break;
748
+ } catch (err) {
749
+ if (!--tries)
750
+ throw new Error(`Failed to connect to ${machine.name}`);
751
+
752
+ // Try again... a few times
753
+ await wait(10 * 1000);
754
+ }
755
+ }
756
+
757
+ machine.ssh = ssh;
758
+ }
759
+
760
+ function wait(ms) {
761
+ return new Promise(resolve => setTimeout(resolve, ms));
762
+ }
763
+
764
+ function log(machine, action, status) {
765
+ if (log.align == null) {
766
+ let lengths = machines.map(machine => machine.name.length);
767
+ log.align = Math.max(...lengths);
768
+ }
769
+
770
+ let align1 = Math.max(log.align - machine.name.length, 0);
771
+ let align2 = Math.max(34 - action.length, 0);
772
+
773
+ console.log(` [${machine.name}]${' '.repeat(align1)} ${action}${' '.repeat(align2)} ${status}`);
774
+ }
775
+
776
+ async function exec_remote(machine, cmd, cwd = null) {
777
+ try {
778
+ if (machine.info.platform == 'win32') {
779
+ if (cwd != null) {
780
+ cwd = cwd.replaceAll('/', '\\');
781
+ cmd = `cd "${cwd}" && ${cmd}`;
782
+ }
783
+
784
+ let ret = await machine.ssh.execCommand(cmd);
785
+ return ret;
786
+ } else {
787
+ let ret = await machine.ssh.execCommand(cmd, { cwd: cwd });
788
+ return ret;
789
+ }
790
+ } catch (err) {
791
+ console.log(err);
792
+ return err;
793
+ }
794
+ }