koffi 0.9.36 → 0.9.39

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/test/test.js DELETED
@@ -1,752 +0,0 @@
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 'build': { command = build; 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 test.js [command] [options...]
191
-
192
- Commands:
193
- test Run the machines and perform the tests (default)
194
- start Start the machines but don't run anythingh
195
- stop Stop running machines
196
- info Print basic information about machine
197
- ssh Start SSH session with specific machine
198
- reset Reset initial disk snapshot
199
-
200
- Options:
201
- --no-accel Disable QEMU acceleration
202
- `;
203
-
204
- console.log(help);
205
- }
206
-
207
- // Commands
208
-
209
- async function start(detach = true) {
210
- let success = true;
211
- let missing = 0;
212
-
213
- check_qemu();
214
-
215
- console.log('>> Starting up machines...');
216
- await Promise.all(machines.map(async machine => {
217
- let dirname = `qemu/${machine.key}`;
218
-
219
- if (!fs.existsSync(dirname)) {
220
- log(machine, 'Missing files', chalk.bold.gray('[ignore]'));
221
-
222
- ignore.add(machine);
223
- missing++;
224
-
225
- return;
226
- }
227
-
228
- // Version check
229
- {
230
- let filename = dirname + '/VERSION';
231
- let version = fs.existsSync(filename) ? parseInt(fs.readFileSync(filename).toString(), 10) : 0;
232
-
233
- if (version != machine.info.version) {
234
- log(machine, 'Download newer machine', chalk.bold.gray('[ignore]'));
235
-
236
- ignore.add(machine);
237
- missing++;
238
-
239
- return;
240
- }
241
- }
242
-
243
- try {
244
- await boot(machine, dirname, detach);
245
-
246
- if (machine.started) {
247
- let action = `Start (${machine.qemu.accelerate || 'emulated'})`;
248
- log(machine, action, chalk.bold.green('[ok]'));
249
- } else {
250
- log(machine, 'Join', chalk.bold.green('[ok]'));
251
- }
252
- } catch (err) {
253
- log(machine, 'Start', chalk.bold.red('[error]'));
254
-
255
- ignore.add(machine);
256
- missing++;
257
- }
258
- }));
259
-
260
- if (success && missing == machines.length)
261
- throw new Error('No machine available');
262
-
263
- return success;
264
- }
265
-
266
- async function build() {
267
- let success = true;
268
-
269
- success &= await start(false);
270
- success &= await copy(machine => Object.values(machine.builds).map(build => build.directory));
271
-
272
- console.log('>> Run build commands...');
273
- await Promise.all(machines.map(async machine => {
274
- if (ignore.has(machine))
275
- return;
276
-
277
- await Promise.all(Object.keys(machine.builds).map(async suite => {
278
- let build = machine.builds[suite];
279
-
280
- let cmd = build.build;
281
- let cwd = build.directory + '/koffi';
282
-
283
- let start = process.hrtime.bigint();
284
- let ret = await exec_remote(machine, cmd, cwd);
285
- let time = Number((process.hrtime.bigint() - start) / 1000000n);
286
-
287
- if (ret.code == 0) {
288
- log(machine, `${suite} > Build`, chalk.bold.green(`[${(time / 1000).toFixed(2)}s]`));
289
- } else {
290
- log(machine, `${suite} > Build`, chalk.bold.red('[error]'));
291
-
292
- if (ret.stdout || ret.stderr)
293
- console.error('');
294
-
295
- let align = log.align + 9;
296
- if (ret.stdout) {
297
- let str = ' '.repeat(align) + 'Standard output:\n' +
298
- chalk.yellow(ret.stdout.replace(/^/gm, ' '.repeat(align + 4))) + '\n';
299
- console.error(str);
300
- }
301
- if (ret.stderr) {
302
- let str = ' '.repeat(align) + 'Standard error:\n' +
303
- chalk.yellow(ret.stderr.replace(/^/gm, ' '.repeat(align + 4))) + '\n';
304
- console.error(str);
305
- }
306
-
307
- success = false;
308
- }
309
- }));
310
- }));
311
-
312
- console.log('>> Get build artifacts');
313
- {
314
- let json = fs.readFileSync(root_dir + '/koffi/package.json', { encoding: 'utf-8' });
315
- let version = JSON.parse(json).version;
316
-
317
- await Promise.all(machines.map(async machine => {
318
- if (ignore.has(machine))
319
- return;
320
-
321
- let copied = true;
322
-
323
- await Promise.all(Object.keys(machine.builds).map(async suite => {
324
- let build = machine.builds[suite];
325
-
326
- let platform = build.platform || machine.info.platform;
327
- let arch = build.arch || machine.info.arch;
328
-
329
- let src_dir = build.directory + '/koffi/build';
330
- let dest_dir = root_dir + `/koffi/build/qemu/${version}/koffi_${platform}_${arch}`;
331
-
332
- fs.mkdirSync(dest_dir + '/build', { mode: 0o755, recursive: true });
333
-
334
- try {
335
- await machine.ssh.getDirectory(dest_dir + '/build', src_dir, {
336
- recursive: false,
337
- concurrency: 4
338
- });
339
-
340
- tar.c({
341
- gzip: true,
342
- file: dest_dir + '.tar.gz',
343
- sync: true,
344
- cwd: dest_dir + '/..'
345
- }, [path.basename(dest_dir)]);
346
- } catch (err) {
347
- console.log(err);
348
- ignore.add(machine);
349
- success = false;
350
- copied = false;
351
- }
352
- }));
353
-
354
- let status = copied ? chalk.bold.green('[ok]') : chalk.bold.red('[error]');
355
- log(machine, 'Download', status);
356
- }));
357
- }
358
-
359
- if (machines.some(machine => machine.started))
360
- success &= await stop(false);
361
-
362
- return success;
363
- }
364
-
365
- async function copy(func) {
366
- let success = true;
367
-
368
- let snapshot_dir = fs.mkdtempSync(path.join(os.tmpdir(), 'luigi_'));
369
- process.on('exit', () => unlink_recursive(snapshot_dir));
370
-
371
- console.log('>> Snapshot code...');
372
- copy_recursive(root_dir, snapshot_dir, filename => {
373
- let basename = path.basename(filename);
374
-
375
- return basename !== '.git' &&
376
- basename !== 'qemu' && !basename.startsWith('qemu_') &&
377
- basename !== 'node_modules' &&
378
- basename !== 'node' &&
379
- basename !== 'build' &&
380
- basename !== 'luiggi';
381
- });
382
-
383
- console.log('>> Copy source code...');
384
- await Promise.all(machines.map(async machine => {
385
- if (ignore.has(machine))
386
- return;
387
-
388
- let copied = true;
389
-
390
- for (let directory of func(machine)) {
391
- try {
392
- await machine.ssh.exec('rm', ['-rf', directory]);
393
- } catch (err) {
394
- // Fails often on Windows (busy directory or whatever), but rarely a problem
395
- }
396
-
397
- try {
398
- await machine.ssh.putDirectory(snapshot_dir, directory, {
399
- recursive: true,
400
- concurrency: 4
401
- });
402
- } catch (err) {
403
- ignore.add(machine);
404
- success = false;
405
- copied = false;
406
- }
407
- }
408
-
409
- let status = copied ? chalk.bold.green('[ok]') : chalk.bold.red('[error]');
410
- log(machine, 'Copy', status);
411
- }));
412
-
413
- return success;
414
- }
415
-
416
- async function test() {
417
- let success = true;
418
-
419
- success &= await start(false);
420
- success &= await copy(machine => Object.values(machine.tests).map(test => test.directory));
421
-
422
- console.log('>> Run test commands...');
423
- await Promise.all(machines.map(async machine => {
424
- if (ignore.has(machine))
425
- return;
426
-
427
- await Promise.all(Object.keys(machine.tests).map(async suite => {
428
- let test = machine.tests[suite];
429
- let commands = {
430
- ...test.build,
431
- ...test.commands
432
- };
433
-
434
- for (let name in commands) {
435
- let cmd = commands[name];
436
- let cwd = test.directory + '/koffi';
437
-
438
- let start = process.hrtime.bigint();
439
- let ret = await exec_remote(machine, cmd, cwd);
440
- let time = Number((process.hrtime.bigint() - start) / 1000000n);
441
-
442
- if (ret.code == 0) {
443
- log(machine, `${suite} > ${name}`, chalk.bold.green(`[${(time / 1000).toFixed(2)}s]`));
444
- } else {
445
- log(machine, `${suite} > ${name}`, chalk.bold.red('[error]'));
446
-
447
- if (ret.stdout || ret.stderr)
448
- console.error('');
449
-
450
- let align = log.align + 9;
451
- if (ret.stdout) {
452
- let str = ' '.repeat(align) + 'Standard output:\n' +
453
- chalk.yellow(ret.stdout.replace(/^/gm, ' '.repeat(align + 4))) + '\n';
454
- console.error(str);
455
- }
456
- if (ret.stderr) {
457
- let str = ' '.repeat(align) + 'Standard error:\n' +
458
- chalk.yellow(ret.stderr.replace(/^/gm, ' '.repeat(align + 4))) + '\n';
459
- console.error(str);
460
- }
461
-
462
- success = false;
463
-
464
- if (name in test.build)
465
- break;
466
- }
467
- }
468
- }));
469
- }));
470
-
471
- if (machines.some(machine => machine.started))
472
- success &= await stop(false);
473
-
474
- console.log('');
475
- if (success) {
476
- console.log('>> Status: ' + chalk.bold.green('SUCCESS'));
477
- if (ignore.size)
478
- console.log(' (but some machines could not be tested)');
479
- } else {
480
- console.log('>> Status: ' + chalk.bold.red('FAILED'));
481
- }
482
-
483
- return success;
484
- }
485
-
486
- async function stop(all = true) {
487
- let success = true;
488
-
489
- console.log('>> Sending shutdown commands...');
490
- await Promise.all(machines.map(async machine => {
491
- if (ignore.has(machine))
492
- return;
493
- if (!machine.started && !all)
494
- return;
495
-
496
- if (machine.ssh == null) {
497
- try {
498
- await join(machine, 2);
499
- } catch (err) {
500
- log(machine, 'Already down', chalk.bold.green('[ok]'));
501
- return;
502
- }
503
- }
504
-
505
- try {
506
- await new Promise(async (resolve, reject) => {
507
- if (machine.ssh.connection == null) {
508
- reject();
509
- return;
510
- }
511
-
512
- machine.ssh.connection.on('close', resolve);
513
- machine.ssh.connection.on('end', resolve);
514
- wait(60000).then(() => { reject(new Error('Timeout')) });
515
-
516
- exec_remote(machine, machine.info.shutdown);
517
- });
518
-
519
- log(machine, 'Stop', chalk.bold.green('[ok]'));
520
- } catch (err) {
521
- log(machine, 'Stop', chalk.bold.red('[error]'));
522
- success = false;
523
- }
524
- }));
525
-
526
- return success;
527
- }
528
-
529
- async function info() {
530
- for (let machine of machines) {
531
- console.log(`>> ${machine.name} (${machine.key})`);
532
- console.log(` - SSH port: ${machine.info.ssh_port}`);
533
- console.log(` - VNC port: ${machine.info.vnc_port}`);
534
- console.log(` - Username: ${machine.info.username}`);
535
- console.log(` - Password: ${machine.info.password}`);
536
- }
537
- }
538
-
539
- async function ssh() {
540
- if (machines.length != 1) {
541
- console.error('The ssh command can only be used with one machine');
542
- return false;
543
- }
544
-
545
- let machine = machines[0];
546
-
547
- let args = [
548
- '-p' + machine.info.password,
549
- 'ssh', '-o', 'StrictHostKeyChecking=no',
550
- '-o', 'UserKnownHostsFile=' + (process.platform == 'win32' ? '\\\\.\\NUL' : '/dev/null'),
551
- '-p', machine.info.ssh_port, machine.info.username + '@127.0.0.1'
552
- ];
553
-
554
- let proc = spawnSync('sshpass', args, { stdio: 'inherit' });
555
- if (proc.status !== 0) {
556
- console.error('Connection failed');
557
- return false;
558
- }
559
-
560
- return true;
561
- }
562
-
563
- async function reset() {
564
- check_qemu();
565
-
566
- let binary = qemu_prefix + 'qemu-img' + (process.platform == 'win32' ? '.exe' : '');
567
-
568
- console.log('>> Restoring snapshots...')
569
- await Promise.all(machines.map(machine => {
570
- let dirname = `qemu/${machine.key}`;
571
- let disk = dirname + '/disk.qcow2';
572
-
573
- if (!fs.existsSync(dirname)) {
574
- log(machine, 'Missing files', chalk.bold.gray('[ignore]'));
575
- return;
576
- }
577
-
578
- let proc = spawnSync(binary, ['snapshot', disk, '-a', 'base'], { encoding: 'utf-8' });
579
-
580
- if (proc.status === 0) {
581
- log(machine, 'Reset disk', chalk.bold.green('[ok]'));
582
- } else {
583
- log(machine, 'Reset disk', chalk.bold.red('[error]'));
584
-
585
- if (proc.stderr) {
586
- console.error('');
587
-
588
- let align = log.align + 9;
589
- let str = ' '.repeat(align) + 'Standard error:\n' +
590
- chalk.yellow(proc.stderr.replace(/^/gm, ' '.repeat(align + 4))) + '\n';
591
- console.error(str);
592
- }
593
- }
594
- }));
595
- }
596
-
597
- // Utility
598
-
599
- function check_qemu() {
600
- if (qemu_prefix != null)
601
- return;
602
-
603
- if (spawnSync('qemu-img', ['--version']).status === 0) {
604
- qemu_prefix = '';
605
- } else if (process.platform == 'win32') {
606
- let proc = spawnSync('reg', ['query', 'HKEY_LOCAL_MACHINE\\SOFTWARE\\QEMU', '/v', 'Install_Dir']);
607
-
608
- if (proc.status === 0) {
609
- let matches = proc.stdout.toString('utf-8').match(/Install_Dir[ \t]+REG_[A-Z_]+[ \t]+(.*)+/);
610
-
611
- if (matches != null) {
612
- let prefix = matches[1].trim() + '/';
613
- let binary = prefix + 'qemu-img.exe';
614
-
615
- if (fs.existsSync(binary))
616
- qemu_prefix = prefix;
617
- }
618
- }
619
- }
620
-
621
- if (qemu_prefix == null)
622
- throw new Error('QEMU does not seem to be installed');
623
- }
624
-
625
- function copy_recursive(src, dest, validate = filename => true) {
626
- let entries = fs.readdirSync(src, { withFileTypes: true });
627
-
628
- for (let entry of entries) {
629
- let filename = path.join(src, entry.name);
630
- let destname = path.join(dest, entry.name);
631
-
632
- if (!validate(filename))
633
- continue;
634
-
635
- if (entry.isDirectory()) {
636
- fs.mkdirSync(destname, { mode: 0o755 });
637
- copy_recursive(filename, destname, validate);
638
- } else if (entry.isFile()) {
639
- fs.copyFileSync(filename, destname);
640
- }
641
- }
642
- }
643
-
644
- function unlink_recursive(path) {
645
- try {
646
- if (fs.rmSync != null) {
647
- fs.rmSync(path, { recursive: true, maxRetries: process.platform == 'win32' ? 3 : 0 });
648
- } else {
649
- fs.rmdirSync(path, { recursive: true, maxRetries: process.platform == 'win32' ? 3 : 0 });
650
- }
651
- } catch (err) {
652
- if (err.code !== 'ENOENT')
653
- throw err;
654
- }
655
- }
656
-
657
- async function boot(machine, dirname, detach) {
658
- let args = machine.qemu.arguments.slice();
659
-
660
- if (machine.qemu.accelerate)
661
- args.push('-accel', machine.qemu.accelerate);
662
- // args.push('-display', 'none');
663
-
664
- try {
665
- let binary = qemu_prefix + machine.qemu.binary + (process.platform == 'win32' ? '.exe' : '');
666
-
667
- let proc = spawn(binary, args, {
668
- cwd: dirname,
669
- detached: detach,
670
- stdio: 'ignore'
671
- });
672
- if (detach)
673
- proc.unref();
674
-
675
- await new Promise((resolve, reject) => {
676
- proc.on('spawn', () => wait(2 * 1000).then(resolve));
677
- proc.on('error', reject);
678
- proc.on('exit', reject);
679
- });
680
-
681
- await join(machine, 30);
682
- machine.started = true;
683
- } catch (err) {
684
- if (typeof err != 'number')
685
- throw err;
686
-
687
- await join(machine, 2);
688
- machine.started = false;
689
- }
690
- }
691
-
692
- async function join(machine, tries) {
693
- let ssh = new NodeSSH;
694
-
695
- while (tries) {
696
- try {
697
- await ssh.connect({
698
- host: '127.0.0.1',
699
- port: machine.info.ssh_port,
700
- username: machine.info.username,
701
- password: machine.info.password,
702
- tryKeyboard: true
703
- });
704
-
705
- break;
706
- } catch (err) {
707
- if (!--tries)
708
- throw new Error(`Failed to connect to ${machine.name}`);
709
-
710
- // Try again... a few times
711
- await wait(10 * 1000);
712
- }
713
- }
714
-
715
- machine.ssh = ssh;
716
- }
717
-
718
- function wait(ms) {
719
- return new Promise(resolve => setTimeout(resolve, ms));
720
- }
721
-
722
- function log(machine, action, status) {
723
- if (log.align == null) {
724
- let lengths = machines.map(machine => machine.name.length);
725
- log.align = Math.max(...lengths);
726
- }
727
-
728
- let align1 = Math.max(log.align - machine.name.length, 0);
729
- let align2 = Math.max(34 - action.length, 0);
730
-
731
- console.log(` [${machine.name}]${' '.repeat(align1)} ${action}${' '.repeat(align2)} ${status}`);
732
- }
733
-
734
- async function exec_remote(machine, cmd, cwd = null) {
735
- try {
736
- if (machine.info.platform == 'win32') {
737
- if (cwd != null) {
738
- cwd = cwd.replaceAll('/', '\\');
739
- cmd = `cd "${cwd}" && ${cmd}`;
740
- }
741
-
742
- let ret = await machine.ssh.execCommand(cmd);
743
- return ret;
744
- } else {
745
- let ret = await machine.ssh.execCommand(cmd, { cwd: cwd });
746
- return ret;
747
- }
748
- } catch (err) {
749
- console.log(err);
750
- return err;
751
- }
752
- }