diff --git a/.github/workflows/binary-spawn-check.yml b/.github/workflows/binary-spawn-check.yml new file mode 100644 index 0000000..1af3741 --- /dev/null +++ b/.github/workflows/binary-spawn-check.yml @@ -0,0 +1,23 @@ +name: Binary extract and spawn check +on: + push: + pull_request: + +jobs: + spawn: + runs-on: ${{ matrix.os }} + timeout-minutes: 20 + strategy: + fail-fast: false + matrix: + os: [windows-latest, ubuntu-latest, macos-latest] + node: [18, 22] + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: ${{ matrix.node }} + - run: npm install --omit=dev --no-audit --no-fund + - run: node scripts/check-binary-spawn.js 15 + - name: Spawn check with slow binary close (simulated AV scan-on-close) + run: node -r ./scripts/slow-close.js scripts/check-binary-spawn.js 5 diff --git a/lib/cfg/node-tunnel-config-v3-latest.json b/lib/cfg/node-tunnel-config-v3-latest.json index e6fcb4b..79ed1a9 100644 --- a/lib/cfg/node-tunnel-config-v3-latest.json +++ b/lib/cfg/node-tunnel-config-v3-latest.json @@ -63,20 +63,20 @@ { "httpPath": "https://downloads.lambdatest.com/tunnel/v3/freebsd/32bit/LT_Freebsd.zip", "binaryName": "LT_Freebsd.zip", - "hash": "3525242664a3a24aa9e49969d137dadb" + "hash": "0747869934c7838e1e812d508c242d50" }, "64bit": { "httpPath": "https://downloads.lambdatest.com/tunnel/v3/freebsd/64bit/LT_Freebsd.zip", "binaryName": "LT_Freebsd.zip", - "hash": "e5fec50196cd1fa02306bdd74acccea3" + "hash": "5a64de0bf4e821da8ddf26c830fc322a" } } } }, "AuthUrl": "https://accounts.lambdatest.com/api/user/token/auth", "logEnable": true, - "latest": "4.0.12", + "latest": "4.0.14", "supportedVersions": [ "1.0.0", @@ -125,6 +125,8 @@ "4.0.9", "4.0.10", "4.0.11", - "4.0.12" + "4.0.12", + "4.0.13", + "4.0.14" ] } diff --git a/lib/tunnel_binary.js b/lib/tunnel_binary.js index 1c63b62..e92bfe8 100644 --- a/lib/tunnel_binary.js +++ b/lib/tunnel_binary.js @@ -140,7 +140,20 @@ function TunnelBinary(httpTunnelConfig, options) { console.log(`Extracting binary`); var zip = new StreamZip.async({ file: binaryPath }); zip - .extract(null, path.resolve(destParentDir)) + .stream(destBinaryName) + .then(function(entryStream) { + // Resolve on 'close', not 'finish': spawning before the fd closes fails with EBUSY on Windows. + return new Promise(function(resolve, reject) { + var out = fs.createWriteStream(destBinaryPath); + entryStream.on('error', function(e) { + out.destroy(); + reject(e); + }); + out.on('error', reject); + out.on('close', resolve); + entryStream.pipe(out); + }); + }) .then(function() { return zip.close(); }) diff --git a/package-lock.json b/package-lock.json index bfb1b02..4352cfe 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@lambdatest/node-tunnel", - "version": "4.0.13", + "version": "4.0.14", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@lambdatest/node-tunnel", - "version": "4.0.13", + "version": "4.0.14", "license": "ISC", "dependencies": { "axios": "^1.6.2", diff --git a/package.json b/package.json index 134ffb1..219c171 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@lambdatest/node-tunnel", - "version": "4.0.13", + "version": "4.0.14", "description": "Nodejs bindings for LambdaTest Tunnel", "main": "index.js", "files": [ diff --git a/scripts/check-binary-spawn.js b/scripts/check-binary-spawn.js new file mode 100644 index 0000000..4176247 --- /dev/null +++ b/scripts/check-binary-spawn.js @@ -0,0 +1,46 @@ +// Downloads and extracts the tunnel binary into a fresh dir, then spawns it immediately (as Tunnel.start does), N times. +var fs = require('fs'), + os = require('os'), + path = require('path'), + childProcess = require('child_process'), + TunnelBinary = require('../lib/tunnel_binary'), + cfg = require('../lib/cfg/node-tunnel-config-v3-latest.json'); + +var runs = Number(process.argv[2]) || 10; +var failures = 0; + +function once(i, done) { + var tb = new TunnelBinary({ jsonResponse: cfg, logger: { log: function() {} } }, {}); + var dir = fs.mkdtempSync(path.join(os.tmpdir(), 'lt-')); + var timer = setTimeout(function() { + done(new Error('timed out waiting for download/extract')); + }, 120000); + tb.download_({}, dir, 1, function(binaryPath) { + clearTimeout(timer); + var proc; + try { + proc = childProcess.spawn(binaryPath, ['--help'], { stdio: 'ignore' }); + } catch (e) { + return done(e); + } + proc.on('error', done); + proc.on('spawn', function() { + proc.kill(); + done(); + }); + }); +} + +function next(i) { + if (i > runs) { + console.log('RESULT ' + (runs - failures) + '/' + runs + ' spawned ok'); + process.exit(failures ? 1 : 0); + } + once(i, function(err) { + if (err) failures++; + console.log('run ' + i + ': ' + (err ? 'FAIL ' + (err.code || err.message) : 'ok')); + next(i + 1); + }); +} + +next(1); diff --git a/scripts/slow-close.js b/scripts/slow-close.js new file mode 100644 index 0000000..5c1982b --- /dev/null +++ b/scripts/slow-close.js @@ -0,0 +1,25 @@ +// Preload that delays closing the extracted LT binary by 800ms, mimicking real-time AV scan-on-close on Windows. +var fs = require('fs'); +var binaryFds = {}; +var origOpen = fs.open; +var origClose = fs.close; + +fs.open = function(file) { + var args = Array.prototype.slice.call(arguments); + var cb = args[args.length - 1]; + args[args.length - 1] = function(err, fd) { + if (!err && /[\\/]LT(\.exe)?$/.test(String(file))) binaryFds[fd] = true; + cb(err, fd); + }; + return origOpen.apply(fs, args); +}; + +fs.close = function(fd, cb) { + if (binaryFds[fd]) { + delete binaryFds[fd]; + return setTimeout(function() { + origClose.call(fs, fd, cb); + }, 800); + } + return origClose.apply(fs, arguments); +};