From df261d82c5ee7d349f53f9d9e1680ecf76c014ec Mon Sep 17 00:00:00 2001 From: Stephen Palmer Date: Fri, 14 Jun 2019 10:43:56 -0500 Subject: [PATCH] Initial code commit --- .gitignore | 2 + .nvmrc | 1 + README.md | 25 +++ lib/client_stream_debugger.js | 83 ++++++++++ package-lock.json | 288 ++++++++++++++++++++++++++++++++++ package.json | 23 +++ stream_player.js | 231 +++++++++++++++++++++++++++ 7 files changed, 653 insertions(+) create mode 100644 .gitignore create mode 100644 .nvmrc create mode 100644 README.md create mode 100644 lib/client_stream_debugger.js create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 stream_player.js diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9cf6fff --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +node_modules/ +.idea diff --git a/.nvmrc b/.nvmrc new file mode 100644 index 0000000..10ff020 --- /dev/null +++ b/.nvmrc @@ -0,0 +1 @@ +v10.16.0 \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..e545db7 --- /dev/null +++ b/README.md @@ -0,0 +1,25 @@ +## Unity Cache Server Diagnostic Tools + +### Client Stream Player + + +#### Usage + +`stream_player.js [options] [ServerAddress]` + +Option | Description +------------------------------- | ----------- + -i --iterations | Number of times to send the recorded session to the server (default: 1) + -c --max-concurrency | Number of concurrent connections to make to the server (default: 1) + -d --debug-protocol | Print protocol stream debugging data to the console. + -q --no-verbose | Do not show progress and result statistics. + -h, --help | Show usage information. + +#### Description + +The stream player can read one ore more recorded client session at ``, optionally print the protocol stream to the console, and optionally send the protocol stream to a remote Cache Server at `[ServerAddress]` for e.g. performance load testing. + +#### Notes + +* If `` is a directory, all files within the directory (recursively) will be read and played back. Some rudimentary validation is done on each file to detect whether or not it is a valid client session stream. +* If `[ServerAddress]` is omitted, data will be sent to a temporary "no-op" TCP server. This is useful if you are only concerned with reading the debug protocol stream with the `-d` option. diff --git a/lib/client_stream_debugger.js b/lib/client_stream_debugger.js new file mode 100644 index 0000000..380b709 --- /dev/null +++ b/lib/client_stream_debugger.js @@ -0,0 +1,83 @@ +const { Constants, Helpers } = require('unity-cache-server'); +const { Transform } = require('stream'); +const crypto = require('crypto'); + +class ClientStreamDebugger extends Transform { + constructor(options) { + super(options); + + this._writeHandlers = { + putStream: this._handleWrite.bind(this), + command: this._handleCommand.bind(this), + version: this._handleVersion.bind(this) + }; + + this._putHash = null; + this._putSize = 0; + this._putSent = 0; + this._writeHandler = this._writeHandlers.version; + } + + _transform(chunk, encoding, callback) { + this._writeHandler(chunk); + this.push(chunk); + callback(); + } + + /** + * + * @param {Buffer} data + * @private + */ + _handleVersion(data) { + this.emit('debug', [Helpers.readUInt32(data)]); + this._writeHandler = this._writeHandlers.command; + } + + /** + * + * @param {Buffer} data + * @private + */ + _handleWrite(data) { + this._putSent += data.length; + this._putHash.update(data, 'ascii'); + if(this._putSent === this._putSize) { + this.emit('debug', [``]); + this._writeHandler = this._writeHandlers.command; + this._putSent = 0; + this._putSize = 0; + } + } + + /** + * + * @param {Buffer} data + * @private + */ + _handleCommand(data) { + const cmd = data.slice(0, Math.min(data.length, 2)).toString('ascii'); + const eventData = [cmd]; + let size, guid, hash = null; + + if(data.length > 1) { + if (data.length === 2 + Constants.ID_SIZE) { + guid = Buffer.from(data.slice(2, 2 + Constants.GUID_SIZE)); + hash = Buffer.from(data.slice(2 + Constants.HASH_SIZE)); + eventData.push(Helpers.GUIDBufferToString(guid)); + eventData.push(hash.toString('hex')); + } + else if (data.length === 2 + Constants.SIZE_SIZE) { + size = Helpers.readUInt64(data.slice(2)); + this._putSize = size; + this._putHash = crypto.createHash('sha256'); + this._writeHandler = this._writeHandlers.putStream; + eventData.push(size.toString()); + } + } + + this.emit('debug', eventData); + } +} + +module.exports = ClientStreamDebugger; diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..dc15e05 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,288 @@ +{ + "name": "ucs-diag-utils", + "version": "1.0.0", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "requires": { + "color-convert": "^1.9.0" + } + }, + "argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "requires": { + "sprintf-js": "~1.0.2" + } + }, + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + }, + "cli-cursor": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-2.1.0.tgz", + "integrity": "sha1-s12sN2R5+sw+lHR9QdDQ9SOP/LU=", + "requires": { + "restore-cursor": "^2.0.0" + } + }, + "cli-spinners": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-1.3.1.tgz", + "integrity": "sha512-1QL4544moEsDVH9T/l6Cemov/37iv1RtoKf7NJ04A60+4MREXNfx/QvavbH6QoGdsD4N4Mwy49cmaINR/o2mdg==" + }, + "color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "requires": { + "color-name": "1.1.3" + } + }, + "color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=" + }, + "commander": { + "version": "2.20.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.0.tgz", + "integrity": "sha1-1YuytcHuj4ew00ACfp6U4iLFpCI=" + }, + "config": { + "version": "1.31.0", + "resolved": "https://registry.npmjs.org/config/-/config-1.31.0.tgz", + "integrity": "sha512-Ep/l9Rd1J9IPueztJfpbOqVzuKHQh4ZODMNt9xqTYdBBNRXbV4oTu34kCkkfdRVcDq0ohtpaeXGgb+c0LQxFRA==", + "requires": { + "json5": "^1.0.1" + } + }, + "escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=" + }, + "esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==" + }, + "filesize": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/filesize/-/filesize-4.1.2.tgz", + "integrity": "sha1-/NVwrxNTzql4l75k9WGDrbmVmUs=" + }, + "fs-extra": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.0.1.tgz", + "integrity": "sha1-kClAgfl4sfGC80ekQKIJFUNEKFs=", + "requires": { + "graceful-fs": "^4.1.2", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + }, + "dependencies": { + "graceful-fs": { + "version": "4.1.15", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.15.tgz", + "integrity": "sha1-/7cD4QZuig7qpMi4C6klPu77+wA=" + }, + "jsonfile": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss=", + "requires": { + "graceful-fs": "^4.1.6" + } + }, + "universalify": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", + "integrity": "sha1-tkb2m+OULavOzJ1mOcgNwQXvqmY=" + } + } + }, + "graceful-fs": { + "version": "4.1.15", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.15.tgz", + "integrity": "sha512-6uHUhOPEBgQ24HM+r6b/QwWfZq+yiFcipKFrOFiBEnWdy5sdzYoi+pJeQaPI5qOLRFqWmAXUPQNsielzdLoecA==" + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=" + }, + "ip": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/ip/-/ip-1.1.5.tgz", + "integrity": "sha1-vd7XARQpCCjAoDnnLvJfWq7ENUo=" + }, + "js-yaml": { + "version": "3.13.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.13.1.tgz", + "integrity": "sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw==", + "requires": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + } + }, + "json5": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz", + "integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==", + "requires": { + "minimist": "^1.2.0" + } + }, + "jsonfile": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss=", + "requires": { + "graceful-fs": "^4.1.6" + } + }, + "lodash": { + "version": "4.17.11", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.11.tgz", + "integrity": "sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg==" + }, + "log-symbols": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-2.2.0.tgz", + "integrity": "sha512-VeIAFslyIerEJLXHziedo2basKbMKtTw3vfn5IzG0XTjhAVEJyNHnL2p7vc+wBDSdQuUpNw3M2u6xb9QsAY5Eg==", + "requires": { + "chalk": "^2.0.1" + } + }, + "lokijs": { + "version": "1.5.6", + "resolved": "https://registry.npmjs.org/lokijs/-/lokijs-1.5.6.tgz", + "integrity": "sha512-xJoDXy8TASTjmXMKr4F8vvNUCu4dqlwY5gmn0g5BajGt1GM3goDCafNiGAh/sfrWgkfWu1J4OfsxWm8yrWweJA==" + }, + "mimic-fn": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-1.2.0.tgz", + "integrity": "sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ==" + }, + "minimist": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", + "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=" + }, + "moment": { + "version": "2.24.0", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.24.0.tgz", + "integrity": "sha512-bV7f+6l2QigeBBZSM/6yTNq4P2fNpSWj/0e7jQcy87A8e7o2nAfP/34/2ky5Vw4B9S446EtIhodAzkFCcR4dQg==" + }, + "onetime": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-2.0.1.tgz", + "integrity": "sha1-BnQoIw/WdEOyeUsiu6UotoZ5YtQ=", + "requires": { + "mimic-fn": "^1.0.0" + } + }, + "ora": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/ora/-/ora-1.4.0.tgz", + "integrity": "sha512-iMK1DOQxzzh2MBlVsU42G80mnrvUhqsMh74phHtDlrcTZPK0pH6o7l7DRshK+0YsxDyEuaOkziVdvM3T0QTzpw==", + "requires": { + "chalk": "^2.1.0", + "cli-cursor": "^2.1.0", + "cli-spinners": "^1.0.1", + "log-symbols": "^2.1.0" + } + }, + "progress": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", + "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==" + }, + "restore-cursor": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-2.0.0.tgz", + "integrity": "sha1-n37ih/gv0ybU/RYpI9YhKe7g368=", + "requires": { + "onetime": "^2.0.0", + "signal-exit": "^3.0.2" + } + }, + "signal-exit": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz", + "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=" + }, + "sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=" + }, + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "requires": { + "has-flag": "^3.0.0" + } + }, + "unity-cache-server": { + "version": "6.4.0-beta3", + "resolved": "https://registry.npmjs.org/unity-cache-server/-/unity-cache-server-6.4.0-beta3.tgz", + "integrity": "sha512-/H7wNcCgzuLIjopjpIH/4jpV/v6gu4NLzFObAx9dU+4wqbv5uE9RZkb7zi+K5XGkNKalX4+leoB/2KW1U4AeLw==", + "requires": { + "commander": "^2.19.0", + "config": "^1.31.0", + "filesize": "^3.5.11", + "fs-extra": "^5.0.0", + "ip": "^1.1.5", + "js-yaml": "^3.13.1", + "lodash": "^4.17.11", + "lokijs": "^1.5.5", + "moment": "^2.23.0", + "ora": "^1.4.0", + "progress": "^2.0.3", + "uuid": "^3.3.2" + }, + "dependencies": { + "filesize": { + "version": "3.6.1", + "resolved": "https://registry.npmjs.org/filesize/-/filesize-3.6.1.tgz", + "integrity": "sha512-7KjR1vv6qnicaPMi1iiTcI85CyYwRO/PSFCu6SvqL8jN2Wjt/NIYQTFtFs7fSDCYOstUkEWIQGFUg5YZQfjlcg==" + }, + "fs-extra": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-5.0.0.tgz", + "integrity": "sha512-66Pm4RYbjzdyeuqudYqhFiNBbCIuI9kgRqLPSHIlXHidW8NIQtVdkM1yeZ4lXwuhbTETv3EUGMNHAAw6hiundQ==", + "requires": { + "graceful-fs": "^4.1.2", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + } + } + } + }, + "universalify": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", + "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==" + }, + "uuid": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz", + "integrity": "sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA==" + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..8ebfcd0 --- /dev/null +++ b/package.json @@ -0,0 +1,23 @@ +{ + "name": "ucs-diag-utils", + "version": "1.0.0", + "description": "Diagnostic utilities for the Unity Cache Server (V1)", + "main": "index.js", + "scripts": {}, + "repository": { + "type": "git", + "url": "https://github.com/Unity-Technologies/ucs-diag-utils.git" + }, + "author": "Stephen Palmer", + "license": "MIT", + "bugs": { + "url": "https://github.com/Unity-Technologies/ucs-diag-utils/issues" + }, + "homepage": "https://github.com/Unity-Technologies/ucs-diag-utils", + "dependencies": { + "commander": "^2.20.0", + "filesize": "^4.1.2", + "fs-extra": "^8.0.1", + "unity-cache-server": "^6.4.0-beta3" + } +} diff --git a/stream_player.js b/stream_player.js new file mode 100644 index 0000000..8ecf885 --- /dev/null +++ b/stream_player.js @@ -0,0 +1,231 @@ +#!/usr/bin/env node + +const program = require('commander'); +const net = require('net'); +const fs = require('fs-extra'); +const filesize = require('filesize'); +const crypto = require('crypto'); +const ClientStreamDebugger = require('./lib/client_stream_debugger'); +const { Constants, Helpers, ServerStreamProcessor, ClientStreamProcessor } = require('unity-cache-server'); + +program.arguments(' [ServerAddress]') + .option('-i --iterations ', 'Number of times to send the recorded session to the server', 1) + .option('-c --max-concurrency ', 'Number of concurrent connections to make to the server', 1) + .option('-d --debug-protocol', 'Print protocol stream debugging data to the console', false) + .option('-q --no-verbose', 'Do not show progress and result statistics') + .action((filePath, serverAddress) => { + const options = { + numIterations: parseInt(program.iterations), + numConcurrent: parseInt(program.maxConcurrency), + verbose: program.verbose, + debugProtocol: program.debugProtocol + }; + + run(filePath, serverAddress, options) + .then(stats => { + if(options.verbose) { + if(stats.bytesSent > 0) { + const sendTime = stats.sendTime / 1000; + const sendBps = stats.bytesSent / sendTime || 0; + console.log(`Sent ${filesize(stats.bytesSent)} in ${sendTime} seconds (${filesize(sendBps)}/second)`); + } + + if(stats.bytesReceived > 0) { + const receiveTime = stats.receiveTime / 1000; + const receiveBps = stats.bytesReceived / receiveTime || 0; + console.log(`Received ${filesize(stats.bytesReceived)} in ${receiveTime} seconds (${filesize(receiveBps)}/second)`); + } + } + }) + .catch(err => { + console.log(err); + process.exit(1); + }); + }); + +program.parse(process.argv); + +async function run(filePath, serverAddress, options) { + let nullServer = null; + + if(!serverAddress) { + nullServer = net.createServer({}, socket => { + socket.on('data', () => {}); + }); + + await new Promise(resolve => { + nullServer.listen(0, "0.0.0.0", () => resolve()); + }); + } + + if(nullServer !== null) { + options.nullServer = true; + const a = nullServer.address(); + serverAddress = `${a.address}:${a.port}`; + } + + // Gather files + const files = []; + const stat = await fs.stat(filePath); + if(stat.isDirectory()) { + await Helpers.readDir(filePath, f => files.push(f.path)); + } + else { + files.push(filePath); + } + + // Validate files + const verBuf = Buffer.alloc(Constants.VERSION_SIZE, 'ascii'); + for(let i = 0; i < files.length; i++) { + const fd = await fs.open(files[i], "r"); + await fs.read(fd, verBuf, 0, Constants.VERSION_SIZE, 0); + if(Helpers.readUInt32(verBuf) !== Constants.PROTOCOL_VERSION) { + if(options.verbose) { + console.log(`Skipping unrecognized file ${files[i]}`); + } + files[i] = null; + } + + await fs.close(fd); + } + + const jobs = []; + const results = []; + let i = 0; + while(i < options.numIterations) { + files.forEach(f => { + if(f === null) return; + jobs.push((n, t) => { + if(options.verbose) console.log(`[${n}/${t}] Playing ${f}`); + return playStream(f, serverAddress, options) + .then(stats => results.push(stats)) + .catch(err => { throw(err); }); + }); + }); + + i++; + } + + const totalJobs = jobs.length; + let nextJobNum = 0; + while(jobs.length > 0) { + nextJobNum += Math.min(jobs.length, options.numConcurrent); + const next = jobs.splice(0, options.numConcurrent); + await Promise.all(next.map(t => t(nextJobNum, totalJobs))); + } + + if(nullServer !== null) nullServer.close(); + + return results.reduce((prev, cur) => { + cur.bytesSent += prev.bytesSent; + cur.bytesReceived += prev.bytesReceived; + cur.sendTime += prev.sendTime; + cur.receiveTime += prev.receiveTime; + return cur; + }, { + receiveTime: 0, + bytesReceived: 0, + sendTime: 0, + bytesSent: 0 + }); +} + +async function playStream(filePath, serverAddress, options) { + let bytesReceived = 0, fileOpen = false, receiveStartTime, receiveEndTime, sendStartTime, sendEndTime, dataHash; + + if(!await fs.pathExists(filePath)) throw new Error(`Cannot find ${filePath}`); + + const fileStats = await fs.stat(filePath); + const address = await Helpers.parseAndValidateAddressString(serverAddress, Constants.DEFAULT_PORT); + const client = new net.Socket(); + await new Promise(resolve => client.connect(address.port, address.host, () => resolve())); + + client.on('error', (err) => { + console.log(err); + }); + + const fileStream = fs.createReadStream(filePath); + + const endClient = () => { + if(options.nullServer || (reqCount === 0 && !fileOpen)) { + process.nextTick(() => client.end('')); + } + }; + + fileStream.on('open', () => { + fileOpen = true; + sendStartTime = Date.now(); + }).on('close', () => { + fileOpen = false; + sendEndTime = Date.now(); + endClient(); + }); + + let reqCount = 0; + + const ssp = new ServerStreamProcessor(); + + ssp.once('header', () => { + receiveStartTime = Date.now(); + }).on('header', () => { + reqCount--; + if(reqCount === 0) endClient(); + }).on('data', (chunk) => { + bytesReceived += chunk.length; + }).on('dataEnd', () => { + receiveEndTime = Date.now(); + }); + + if(options.debugProtocol) { + ssp.on('header', header => { + dataHash = crypto.createHash('sha256'); + + const debugData = [header.cmd]; + if(header.size) { + debugData.push(header.size); + } + + debugData.push(Helpers.GUIDBufferToString(header.guid)); + debugData.push(header.hash.toString('hex')); + + const txt = `<<< ${debugData.join(' ')}`; + + if(header.size) { + process.stdout.write(txt); + } else { + console.log(txt) + } + }).on('data', (chunk) => { + dataHash.update(chunk, 'ascii'); + }).on('dataEnd', () => { + console.log(` `); + }); + } + + const csp = new ClientStreamProcessor({}); + + csp.on('cmd', cmd => { + if(cmd[0] === 'g' || cmd === 'ts') reqCount++; + if(cmd === 'te') reqCount --; + }); + + let stream = fileStream.pipe(csp); + + if(options.debugProtocol) { + stream = stream.pipe(new ClientStreamDebugger({})) + .on('debug', data => console.log(`>>> ${data.join(' ')}`)); + } + + stream.pipe(client, {end: false}).pipe(ssp); + + return new Promise(resolve => { + client.on('close', () => { + resolve({ + bytesSent: fileStats.size, + bytesReceived: bytesReceived, + sendTime: sendEndTime - sendStartTime, + receiveTime: receiveEndTime - receiveStartTime, + }); + }); + }); +} \ No newline at end of file