├── .eslintrc.json ├── .github ├── issue_template.md └── workflows │ └── Semgrep.yml ├── .gitignore ├── .travis.yml ├── CODEOWNERS ├── MIT-LICENSE.txt ├── README.md ├── index.d.ts ├── index.js ├── lib ├── Local.js ├── LocalBinary.js ├── LocalError.js └── download.js ├── node-example.js ├── package-lock.json ├── package.json └── test └── local.js /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "indent": [ 4 | 1, 5 | 2 6 | ], 7 | "quotes": [ 8 | 2, 9 | "single" 10 | ], 11 | "semi": [ 12 | 2, 13 | "always" 14 | ], 15 | "no-console": 0, 16 | "no-trailing-spaces": 2, 17 | "no-irregular-whitespace": 2, 18 | "camelcase": 2, 19 | "brace-style": [2, "1tbs", { "allowSingleLine": true }], 20 | "no-constant-condition": 0 21 | }, 22 | "env": { 23 | "es6": true, 24 | "node": true 25 | }, 26 | "extends": "eslint:recommended", 27 | "plugins": [ 28 | "mocha" 29 | ] 30 | } 31 | -------------------------------------------------------------------------------- /.github/issue_template.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## Expected Behavior 4 | 5 | 6 | ## Actual Behavior 7 | 8 | 9 | ## Steps to Reproduce the Problem 10 | 11 | 1. 12 | 2. 13 | 3. 14 | 15 | ## browserstack local arguments 16 | 17 | 18 | ## Platform details 19 | 20 | 1. browserstack-local-nodejs version: 21 | 2. node version: 22 | 3. os type and version: 23 | 24 | ## Details 25 | 26 | -------------------------------------------------------------------------------- /.github/workflows/Semgrep.yml: -------------------------------------------------------------------------------- 1 | # Name of this GitHub Actions workflow. 2 | name: Semgrep 3 | 4 | on: 5 | # Scan changed files in PRs (diff-aware scanning): 6 | # The branches below must be a subset of the branches above 7 | pull_request: 8 | branches: ["master", "main"] 9 | push: 10 | branches: ["master", "main"] 11 | schedule: 12 | - cron: '0 6 * * *' 13 | 14 | 15 | permissions: 16 | contents: read 17 | 18 | jobs: 19 | semgrep: 20 | # User definable name of this GitHub Actions job. 21 | permissions: 22 | contents: read # for actions/checkout to fetch code 23 | security-events: write # for github/codeql-action/upload-sarif to upload SARIF results 24 | name: semgrep/ci 25 | # If you are self-hosting, change the following `runs-on` value: 26 | runs-on: ubuntu-latest 27 | 28 | container: 29 | # A Docker image with Semgrep installed. Do not change this. 30 | image: returntocorp/semgrep 31 | 32 | # Skip any PR created by dependabot to avoid permission issues: 33 | if: (github.actor != 'dependabot[bot]') 34 | 35 | steps: 36 | # Fetch project source with GitHub Actions Checkout. 37 | - uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3 38 | # Run the "semgrep ci" command on the command line of the docker image. 39 | - run: semgrep ci --sarif --output=semgrep.sarif 40 | env: 41 | # Add the rules that Semgrep uses by setting the SEMGREP_RULES environment variable. 42 | SEMGREP_RULES: p/default # more at semgrep.dev/explore 43 | 44 | - name: Upload SARIF file for GitHub Advanced Security Dashboard 45 | uses: github/codeql-action/upload-sarif@6c089f53dd51dc3fc7e599c3cb5356453a52ca9e # v2.20.0 46 | with: 47 | sarif_file: semgrep.sarif 48 | if: always() -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | bin/ 2 | node_modules/ 3 | browserstack.err 4 | *.log 5 | .idea/ 6 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "4.3" 4 | - "6" 5 | - "8" 6 | - "10" 7 | -------------------------------------------------------------------------------- /CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @browserstack/local-dev 2 | -------------------------------------------------------------------------------- /MIT-LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 BrowserStack 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # browserstack-local-nodejs 2 | 3 | [![Build Status](https://travis-ci.org/browserstack/browserstack-local-nodejs.svg?branch=master)](https://travis-ci.org/browserstack/browserstack-local-nodejs) 4 | 5 | Nodejs bindings for BrowserStack Local. 6 | 7 | ## Installation 8 | 9 | ``` 10 | npm install browserstack-local 11 | ``` 12 | 13 | ## Example 14 | 15 | ```js 16 | var browserstack = require('browserstack-local'); 17 | 18 | //creates an instance of Local 19 | var bs_local = new browserstack.Local(); 20 | 21 | // replace with your key. You can also set an environment variable - "BROWSERSTACK_ACCESS_KEY". 22 | var bs_local_args = { 'key': '' }; 23 | 24 | // starts the Local instance with the required arguments 25 | bs_local.start(bs_local_args, function() { 26 | console.log("Started BrowserStackLocal"); 27 | 28 | // check if BrowserStack local instance is running 29 | console.log(bs_local.isRunning()); 30 | 31 | // stop the Local instance 32 | bs_local.stop(function() { 33 | console.log("Stopped BrowserStackLocal"); 34 | }); 35 | }); 36 | 37 | ``` 38 | 39 | ## Arguments 40 | 41 | Apart from the key, all other BrowserStack Local modifiers are optional. For the full list of modifiers, refer [BrowserStack Local modifiers](https://www.browserstack.com/local-testing#modifiers). For examples, refer below - 42 | 43 | #### Verbose Logging 44 | To enable verbose logging - 45 | ```js 46 | bs_local_args = { 'key': '', 'verbose': 'true' } 47 | ``` 48 | Note - Possible values for 'verbose' modifier are '1', '2', '3' and 'true' 49 | 50 | #### Folder Testing 51 | To test local folder rather internal server, provide path to folder as value of this option - 52 | ```js 53 | bs_local_args = { 'key': '', 'f': '/my/awesome/folder' } 54 | ``` 55 | 56 | #### Force Start 57 | To kill other running Browserstack Local instances - 58 | ```js 59 | bs_local_args = { 'key': '', 'force': 'true' } 60 | ``` 61 | 62 | #### Only Automate 63 | To disable local testing for Live and Screenshots, and enable only Automate - 64 | ```js 65 | bs_local_args = { 'key': '', 'onlyAutomate': 'true' } 66 | ``` 67 | 68 | #### Force Local 69 | To route all traffic via local(your) machine - 70 | ```js 71 | bs_local_args = { 'key': '', 'forceLocal': 'true' } 72 | ``` 73 | 74 | #### Proxy 75 | To use a proxy for local testing - 76 | 77 | * proxyHost: Hostname/IP of proxy, remaining proxy options are ignored if this option is absent 78 | * proxyPort: Port for the proxy, defaults to 3128 when -proxyHost is used 79 | * proxyUser: Username for connecting to proxy (Basic Auth Only) 80 | * proxyPass: Password for USERNAME, will be ignored if USERNAME is empty or not specified 81 | * useCaCertificate: Path to ca cert file, if required 82 | 83 | ```js 84 | bs_local_args = { 'key': '', 'proxyHost': '127.0.0.1', 'proxyPort': '8000', 'proxyUser': 'user', 'proxyPass': 'password', 'useCaCertificate': '/Users/test/cert.pem' } 85 | ``` 86 | 87 | #### Local Proxy 88 | To use local proxy in local testing - 89 | 90 | * localProxyHost: Hostname/IP of proxy, remaining proxy options are ignored if this option is absent 91 | * localProxyPort: Port for the proxy, defaults to 8081 when -localProxyHost is used 92 | * localProxyUser: Username for connecting to proxy (Basic Auth Only) 93 | * localProxyPass: Password for USERNAME, will be ignored if USERNAME is empty or not specified 94 | 95 | ``` 96 | bs_local_args = { 'key': '', 'localProxyHost': '127.0.0.1', 'localProxyPort': '8000', 'localProxyUser': 'user', 'localProxyPass': 'password' } 97 | ``` 98 | 99 | #### PAC (Proxy Auto-Configuration) 100 | To use PAC (Proxy Auto-Configuration) in local testing - 101 | 102 | * pac-file: PAC (Proxy Auto-Configuration) file’s absolute path 103 | 104 | ``` 105 | bs_local_args = { 'key': '', 'pac-file': '' } 106 | ``` 107 | 108 | #### Local Identifier 109 | If doing simultaneous multiple local testing connections, set this uniquely for different processes - 110 | ```js 111 | bs_local_args = { 'key': '', 'localIdentifier': 'randomstring' } 112 | ``` 113 | 114 | ## Additional Arguments 115 | 116 | #### Binary Path 117 | 118 | By default, BrowserStack local wrappers try downloading and executing the latest version of BrowserStack binary in ~/.browserstack or the present working directory or the tmp folder by order. But you can override these by passing the -binarypath argument. 119 | Path to specify local Binary path - 120 | ```js 121 | bs_local_args = { 'key': '', 'binarypath': '/browserstack/BrowserStackLocal' } 122 | ``` 123 | 124 | #### Logfile 125 | To save the logs to the file while running with the '-v' argument, you can specify the path of the file. By default the logs are saved in the local.log file in the present woring directory. 126 | To specify the path to file where the logs will be saved - 127 | ```js 128 | bs_local_args = { 'key': '', 'verbose': 'true', 'logFile': '/browserstack/logs.txt' } 129 | ``` 130 | 131 | ## Contribute 132 | 133 | ### Instructions 134 | 135 | To run the test suite run, `npm test`. 136 | 137 | ### Reporting bugs 138 | 139 | You can submit bug reports either in the Github issue tracker. 140 | 141 | Before submitting an issue please check if there is already an existing issue. If there is, please add any additional information give it a "+1" in the comments. 142 | 143 | When submitting an issue please describe the issue clearly, including how to reproduce the bug, which situations it appears in, what you expect to happen, what actually happens, and what platform (operating system and version) you are using. 144 | 145 | ### Pull Requests 146 | 147 | We love pull requests! We are very happy to work with you to get your changes merged in, however, please keep the following in mind. 148 | 149 | * Adhere to the coding conventions you see in the surrounding code. 150 | * Include tests, and make sure all tests pass. 151 | * Before submitting a pull-request, clean up the git history by going over your commits and squashing together minor changes and fixes into the corresponding commits. You can do this using the interactive rebase command. 152 | 153 | -------------------------------------------------------------------------------- /index.d.ts: -------------------------------------------------------------------------------- 1 | declare module "browserstack-local" { 2 | interface Options { 3 | key: string; 4 | verbose: boolean; 5 | force: boolean; 6 | only: string; 7 | onlyAutomate: boolean; 8 | forceLocal: boolean; 9 | localIdentifier: string; 10 | folder: string; 11 | proxyHost: string; 12 | proxyPort: string; 13 | proxyUser: string; 14 | proxyPass: string; 15 | forceProxy: boolean; 16 | logFile: string; 17 | parallelRuns: string; 18 | binarypath: string; 19 | [key: string]: string | boolean; 20 | } 21 | 22 | class Local { 23 | start(options: Partial, callback: (error?: Error) => void): void; 24 | isRunning(): boolean; 25 | stop(callback: () => void): void; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | module.exports.Local = require('./lib/Local'); 2 | -------------------------------------------------------------------------------- /lib/Local.js: -------------------------------------------------------------------------------- 1 | var childProcess = require('child_process'), 2 | os = require('os'), 3 | fs = require('fs'), 4 | path = require('path'), 5 | running = require('is-running'), 6 | LocalBinary = require('./LocalBinary'), 7 | LocalError = require('./LocalError'), 8 | version = require('../package.json').version, 9 | psTree = require('ps-tree'); 10 | 11 | function Local(){ 12 | this.sanitizePath = function(rawPath) { 13 | var doubleQuoteIfRequired = this.windows && !rawPath.match(/"[^"]+"/) ? '"' : ''; 14 | return doubleQuoteIfRequired + rawPath + doubleQuoteIfRequired; 15 | }; 16 | 17 | this.windows = os.platform().match(/mswin|msys|mingw|cygwin|bccwin|wince|emc|win32/i); 18 | this.pid = undefined; 19 | this.isProcessRunning = false; 20 | this.retriesLeft = 5; 21 | this.key = process.env.BROWSERSTACK_ACCESS_KEY; 22 | this.logfile = this.sanitizePath(path.join(process.cwd(), 'local.log')); 23 | this.opcode = 'start'; 24 | this.exitCallback; 25 | 26 | this.errorRegex = /\*\*\* Error: [^\r\n]*/i; 27 | this.doneRegex = /Press Ctrl-C to exit/i; 28 | 29 | this.startSync = function(options) { 30 | this.userArgs = []; 31 | var that = this; 32 | this.addArgs(options); 33 | 34 | if(typeof options['onlyCommand'] !== 'undefined') 35 | return; 36 | 37 | const binaryPath = this.getBinaryPath(); 38 | that.binaryPath = binaryPath; 39 | childProcess.exec('echo "" > ' + that.logfile); 40 | that.opcode = 'start'; 41 | if(!this.binaryPath){ 42 | return new LocalError('Couldn\'t find binary file'); 43 | } 44 | try{ 45 | const obj = childProcess.spawnSync(that.binaryPath, that.getBinaryArgs()); 46 | this.tunnel = {pid: obj.pid}; 47 | var data = {}; 48 | if(obj.stdout.length > 0) 49 | data = JSON.parse(obj.stdout); 50 | else if(obj.stderr.length > 0) 51 | data = JSON.parse(obj.stderr); 52 | else 53 | return new LocalError('No output received'); 54 | if(data['state'] != 'connected'){ 55 | return new LocalError(data['message']['message']); 56 | } else { 57 | that.pid = data['pid']; 58 | that.isProcessRunning = true; 59 | return; 60 | } 61 | }catch(error){ 62 | console.error('Error while trying to execute binary', error); 63 | if(that.retriesLeft > 0) { 64 | console.log('Retrying Binary Download. Retries Left', that.retriesLeft); 65 | that.retriesLeft -= 1; 66 | fs.unlinkSync(that.binaryPath); 67 | delete(that.binaryPath); 68 | return that.startSync(options); 69 | } else { 70 | throw new LocalError(error.toString()); 71 | } 72 | } 73 | }; 74 | 75 | this.start = function(options, callback){ 76 | this.userArgs = []; 77 | var that = this; 78 | this.addArgs(options); 79 | 80 | if(typeof options['onlyCommand'] !== 'undefined') 81 | return callback(); 82 | 83 | this.getBinaryPath(function(binaryPath){ 84 | that.binaryPath = binaryPath; 85 | childProcess.exec('echo "" > ' + that.logfile); 86 | 87 | that.opcode = 'start'; 88 | that.tunnel = childProcess.execFile(that.binaryPath, that.getBinaryArgs(), function(error, stdout, stderr){ 89 | if(error) { 90 | console.error('Error while trying to execute binary', error); 91 | if(that.retriesLeft > 0) { 92 | console.log('Retrying Binary Download. Retries Left', that.retriesLeft); 93 | that.retriesLeft -= 1; 94 | fs.unlinkSync(that.binaryPath); 95 | delete(that.binaryPath); 96 | that.start(options, callback); 97 | return; 98 | } else { 99 | callback(new LocalError(error.toString())); 100 | } 101 | } 102 | 103 | var data = {}; 104 | if(stdout) 105 | data = JSON.parse(stdout); 106 | else if(stderr) 107 | data = JSON.parse(stderr); 108 | else 109 | callback(new LocalError('No output received')); 110 | 111 | if(data['state'] != 'connected'){ 112 | callback(new LocalError(data['message']['message'])); 113 | } else { 114 | that.pid = data['pid']; 115 | that.isProcessRunning = true; 116 | callback(); 117 | } 118 | }); 119 | }); 120 | }; 121 | 122 | this.isRunning = function(){ 123 | return this.pid && running(this.pid) && this.isProcessRunning; 124 | }; 125 | 126 | this.stop = function (callback) { 127 | if(!this.pid) return callback(); 128 | this.killAllProcesses(function(error){ 129 | if(error) callback(new LocalError(error.toString())); 130 | callback(); 131 | }); 132 | }; 133 | 134 | this.addArgs = function(options){ 135 | for(var key in options){ 136 | var value = options[key]; 137 | 138 | switch(key){ 139 | case 'key': 140 | if(value) 141 | this.key = value; 142 | break; 143 | 144 | case 'verbose': 145 | if(value.toString() !== 'true') 146 | this.verboseFlag = value; 147 | else { 148 | this.verboseFlag = '1'; 149 | } 150 | break; 151 | 152 | case 'force': 153 | if(value) 154 | this.forceFlag = '--force'; 155 | break; 156 | 157 | case 'only': 158 | if(value) 159 | this.onlyHosts = value; 160 | break; 161 | 162 | case 'onlyAutomate': 163 | if(value) 164 | this.onlyAutomateFlag = '--only-automate'; 165 | break; 166 | 167 | case 'forcelocal': 168 | case 'forceLocal': 169 | if(value) 170 | this.forceLocalFlag = '--force-local'; 171 | break; 172 | 173 | case 'localIdentifier': 174 | if(value) 175 | this.localIdentifierFlag = value; 176 | break; 177 | 178 | case 'f': 179 | case 'folder': 180 | if(value){ 181 | this.folderFlag = '-f'; 182 | this.folderPath = this.sanitizePath(value); 183 | } 184 | break; 185 | 186 | case 'useCaCertificate': 187 | if(value) 188 | this.useCaCertificate = value; 189 | break; 190 | 191 | case 'proxyHost': 192 | if(value) 193 | this.proxyHost = value; 194 | break; 195 | 196 | case 'proxyPort': 197 | if(value) 198 | this.proxyPort = value; 199 | break; 200 | 201 | case 'proxyUser': 202 | if(value) 203 | this.proxyUser = value; 204 | break; 205 | 206 | case 'proxyPass': 207 | if(value) 208 | this.proxyPass = value; 209 | break; 210 | 211 | case 'forceproxy': 212 | case 'forceProxy': 213 | if(value) 214 | this.forceProxyFlag = '--force-proxy'; 215 | break; 216 | 217 | case 'logfile': 218 | case 'logFile': 219 | if(value) 220 | this.logfile = this.sanitizePath(value); 221 | break; 222 | 223 | case 'parallelRuns': 224 | if(value) 225 | this.parallelRunsFlag = value; 226 | break; 227 | 228 | case 'binarypath': 229 | if(value) 230 | this.binaryPath = value; 231 | break; 232 | 233 | default: 234 | if(value.toString().toLowerCase() == 'true'){ 235 | this.userArgs.push('--' + key); 236 | } else { 237 | this.userArgs.push('--' + key); 238 | this.userArgs.push(value); 239 | } 240 | break; 241 | } 242 | } 243 | }; 244 | 245 | this.getBinaryPath = function(callback){ 246 | if(typeof(this.binaryPath) == 'undefined'){ 247 | this.binary = new LocalBinary(); 248 | var conf = {}; 249 | if(this.proxyHost && this.proxyPort){ 250 | conf.proxyHost = this.proxyHost; 251 | conf.proxyPort = this.proxyPort; 252 | } 253 | if (this.useCaCertificate) { 254 | conf.useCaCertificate = this.useCaCertificate; 255 | } 256 | if(!callback) { 257 | return this.binary.binaryPath(conf); 258 | } 259 | this.binary.binaryPath(conf, callback); 260 | } else { 261 | console.log('BINARY PATH IS DEFINED'); 262 | if(!callback) { 263 | return this.binaryPath; 264 | } 265 | callback(this.binaryPath); 266 | } 267 | }; 268 | 269 | this.getBinaryArgs = function(){ 270 | var args = ['--daemon', this.opcode, '--log-file', this.logfile, '--source', `nodejs-${version}`]; 271 | if(this.key) { 272 | args.push('--key'); 273 | args.push(this.key); 274 | } 275 | if(this.folderFlag) 276 | args.push(this.folderFlag); 277 | if(this.folderPath) 278 | args.push(this.folderPath); 279 | if(this.forceLocalFlag) 280 | args.push(this.forceLocalFlag); 281 | if(this.localIdentifierFlag){ 282 | args.push('--local-identifier'); 283 | args.push(this.localIdentifierFlag); 284 | } 285 | if(this.parallelRunsFlag){ 286 | args.push('--parallel-runs'); 287 | args.push(this.parallelRunsFlag.toString()); 288 | } 289 | if(this.onlyHosts) { 290 | args.push('--only'); 291 | args.push(this.onlyHosts); 292 | } 293 | if(this.onlyAutomateFlag) 294 | args.push(this.onlyAutomateFlag); 295 | if (this.useCaCertificate) { 296 | args.push('--use-ca-certificate'); 297 | args.push(this.useCaCertificate); 298 | } 299 | if(this.proxyHost){ 300 | args.push('--proxy-host'); 301 | args.push(this.proxyHost); 302 | } 303 | if(this.proxyPort){ 304 | args.push('--proxy-port'); 305 | args.push(this.proxyPort); 306 | } 307 | if(this.proxyUser){ 308 | args.push('--proxy-user'); 309 | args.push(this.proxyUser); 310 | } 311 | if(this.proxyPass){ 312 | args.push('--proxy-pass'); 313 | args.push(this.proxyPass); 314 | } 315 | if(this.forceProxyFlag) 316 | args.push(this.forceProxyFlag); 317 | if(this.forceFlag) 318 | args.push(this.forceFlag); 319 | if(this.verboseFlag){ 320 | args.push('--verbose'); 321 | args.push(this.verboseFlag.toString()); 322 | } 323 | for(var i in this.userArgs){ 324 | args.push(this.userArgs[i]); 325 | } 326 | return args; 327 | }; 328 | 329 | this.killAllProcesses = function(callback){ 330 | psTree(this.pid, (err, children) => { 331 | var childPids = children.map(val => val.PID); 332 | var killChecker = setInterval(() => { 333 | if(childPids.length === 0) { 334 | clearInterval(killChecker); 335 | try { 336 | process.kill(this.pid); 337 | // This gives time to local binary to send kill signal to railsApp. 338 | setTimeout(() => { 339 | this.isProcessRunning = false; 340 | callback(); 341 | }, 2000); 342 | } catch(err) { 343 | this.isProcessRunning = false; 344 | callback(); 345 | } 346 | } 347 | for(var i in childPids) { 348 | try { 349 | process.kill(childPids[i]); 350 | } catch(err) { 351 | childPids.splice(i, 1); 352 | } 353 | } 354 | },500); 355 | }); 356 | }; 357 | } 358 | 359 | module.exports = Local; 360 | -------------------------------------------------------------------------------- /lib/LocalBinary.js: -------------------------------------------------------------------------------- 1 | var https = require('https'), 2 | url = require('url'), 3 | fs = require('fs'), 4 | path = require('path'), 5 | os = require('os'), 6 | childProcess = require('child_process'), 7 | zlib = require('zlib'), 8 | HttpsProxyAgent = require('https-proxy-agent'), 9 | version = require('../package.json').version, 10 | LocalError = require('./LocalError'); 11 | 12 | const packageName = 'browserstack-local-nodejs'; 13 | 14 | function LocalBinary(){ 15 | this.hostOS = process.platform; 16 | this.is64bits = process.arch == 'x64'; 17 | 18 | this.getDownloadPath = function () { 19 | let sourceURL = 'https://www.browserstack.com/local-testing/downloads/binaries/'; 20 | 21 | if(this.hostOS.match(/darwin|mac os/i)){ 22 | return sourceURL + 'BrowserStackLocal-darwin-x64'; 23 | } else if(this.hostOS.match(/mswin|msys|mingw|cygwin|bccwin|wince|emc|win32/i)) { 24 | this.windows = true; 25 | return sourceURL + 'BrowserStackLocal.exe'; 26 | } else { 27 | if(this.is64bits) { 28 | if(this.isAlpine()) 29 | return sourceURL + 'BrowserStackLocal-alpine'; 30 | else 31 | return sourceURL + 'BrowserStackLocal-linux-x64'; 32 | } else { 33 | return sourceURL + 'BrowserStackLocal-linux-ia32'; 34 | } 35 | } 36 | }; 37 | 38 | this.isAlpine = function() { 39 | try { 40 | return childProcess.execSync('grep -w "NAME" /etc/os-release').includes('Alpine'); 41 | } catch(e) { 42 | return false; 43 | } 44 | }; 45 | 46 | this.httpPath = this.getDownloadPath(); 47 | 48 | 49 | 50 | this.retryBinaryDownload = function(conf, destParentDir, callback, retries, binaryPath) { 51 | var that = this; 52 | if(retries > 0) { 53 | console.log('Retrying Download. Retries left', retries); 54 | fs.stat(binaryPath, function(err) { 55 | if(err == null) { 56 | fs.unlinkSync(binaryPath); 57 | } 58 | if(!callback) { 59 | return that.downloadSync(conf, destParentDir, retries - 1); 60 | } 61 | that.download(conf, destParentDir, callback, retries - 1); 62 | }); 63 | } else { 64 | console.error('Number of retries to download exceeded.'); 65 | } 66 | }; 67 | 68 | this.downloadSync = function(conf, destParentDir, retries) { 69 | console.log('Downloading in sync'); 70 | var that = this; 71 | if(!this.checkPath(destParentDir)) 72 | fs.mkdirSync(destParentDir); 73 | 74 | var destBinaryName = (this.windows) ? 'BrowserStackLocal.exe' : 'BrowserStackLocal'; 75 | var binaryPath = path.join(destParentDir, destBinaryName); 76 | 77 | let cmd, opts; 78 | cmd = 'node'; 79 | opts = [path.join(__dirname, 'download.js'), binaryPath, this.httpPath]; 80 | if(conf.proxyHost && conf.proxyPort) { 81 | opts.push(conf.proxyHost, conf.proxyPort); 82 | if (conf.useCaCertificate) { 83 | opts.push(conf.useCaCertificate); 84 | } 85 | } else if (conf.useCaCertificate) { 86 | opts.push(undefined, undefined, conf.useCaCertificate); 87 | } 88 | 89 | try{ 90 | const userAgent = [packageName, version].join('/'); 91 | const env = Object.assign({ 'USER_AGENT': userAgent }, process.env); 92 | const obj = childProcess.spawnSync(cmd, opts, { env: env }); 93 | let output; 94 | if(obj.stdout.length > 0) { 95 | if(fs.existsSync(binaryPath)){ 96 | fs.chmodSync(binaryPath, '0755'); 97 | return binaryPath; 98 | }else{ 99 | console.log('failed to download'); 100 | return that.retryBinaryDownload(conf, destParentDir, null, retries, binaryPath); 101 | } 102 | } else if(obj.stderr.length > 0) { 103 | output = Buffer.from(JSON.parse(JSON.stringify(obj.stderr)).data).toString(); 104 | console.error(output); 105 | return that.retryBinaryDownload(conf, destParentDir, null, retries, binaryPath); 106 | } 107 | } catch(err) { 108 | console.error('Download failed with error', err); 109 | return that.retryBinaryDownload(conf, destParentDir, null, retries, binaryPath); 110 | } 111 | }; 112 | 113 | this.download = function(conf, destParentDir, callback, retries){ 114 | var that = this; 115 | if(!this.checkPath(destParentDir)) 116 | fs.mkdirSync(destParentDir); 117 | 118 | var destBinaryName = (this.windows) ? 'BrowserStackLocal.exe' : 'BrowserStackLocal'; 119 | var binaryPath = path.join(destParentDir, destBinaryName); 120 | var fileStream = fs.createWriteStream(binaryPath); 121 | 122 | var options = url.parse(this.httpPath); 123 | if(conf.proxyHost && conf.proxyPort) { 124 | options.agent = new HttpsProxyAgent({ 125 | host: conf.proxyHost, 126 | port: conf.proxyPort 127 | }); 128 | } 129 | if (conf.useCaCertificate) { 130 | try { 131 | options.ca = fs.readFileSync(conf.useCaCertificate); 132 | } catch(err) { 133 | console.log('failed to read cert file', err); 134 | } 135 | } 136 | 137 | options.headers = Object.assign({}, options.headers, { 138 | 'accept-encoding': 'gzip, *', 139 | 'user-agent': [packageName, version].join('/'), 140 | }); 141 | 142 | https.get(options, function (response) { 143 | const contentEncoding = response.headers['content-encoding']; 144 | if (typeof contentEncoding === 'string' && contentEncoding.match(/gzip/i)) { 145 | if (process.env.BROWSERSTACK_LOCAL_DEBUG_GZIP) { 146 | console.info('Using gzip in ' + options.headers['user-agent']); 147 | } 148 | 149 | response.pipe(zlib.createGunzip()).pipe(fileStream); 150 | } else { 151 | response.pipe(fileStream); 152 | } 153 | 154 | response.on('error', function(err) { 155 | console.error('Got Error in binary download response', err); 156 | that.retryBinaryDownload(conf, destParentDir, callback, retries, binaryPath); 157 | }); 158 | fileStream.on('error', function (err) { 159 | console.error('Got Error while downloading binary file', err); 160 | that.retryBinaryDownload(conf, destParentDir, callback, retries, binaryPath); 161 | }); 162 | fileStream.on('close', function () { 163 | fs.chmod(binaryPath, '0755', function() { 164 | callback(binaryPath); 165 | }); 166 | }); 167 | }).on('error', function(err) { 168 | console.error('Got Error in binary downloading request', err); 169 | that.retryBinaryDownload(conf, destParentDir, callback, retries, binaryPath); 170 | }); 171 | }; 172 | 173 | this.binaryPath = function(conf, callback){ 174 | var destParentDir = this.getAvailableDirs(); 175 | var destBinaryName = (this.windows) ? 'BrowserStackLocal.exe' : 'BrowserStackLocal'; 176 | var binaryPath = path.join(destParentDir, destBinaryName); 177 | if(this.checkPath(binaryPath, fs.X_OK)){ 178 | if(!callback) { 179 | return binaryPath; 180 | } 181 | callback(binaryPath); 182 | } else { 183 | if(!callback) { 184 | return this.downloadSync(conf, destParentDir, 5); 185 | } 186 | this.download(conf, destParentDir, callback, 5); 187 | } 188 | }; 189 | 190 | this.checkPath = function(path, mode){ 191 | mode = mode || (fs.R_OK | fs.W_OK); 192 | try { 193 | fs.accessSync(path, mode); 194 | return true; 195 | } catch(e){ 196 | if(typeof fs.accessSync !== 'undefined') return false; 197 | 198 | // node v0.10 199 | try { 200 | fs.statSync(path); 201 | return true; 202 | } catch (e){ 203 | return false; 204 | } 205 | } 206 | }; 207 | 208 | this.getAvailableDirs = function(){ 209 | for(var i=0; i < this.orderedPaths.length; i++){ 210 | var path = this.orderedPaths[i]; 211 | if(this.makePath(path)) 212 | return path; 213 | } 214 | throw new LocalError('Error trying to download BrowserStack Local binary'); 215 | }; 216 | 217 | this.makePath = function(path){ 218 | try { 219 | if(!this.checkPath(path)){ 220 | fs.mkdirSync(path); 221 | } 222 | return true; 223 | } catch(e){ 224 | return false; 225 | } 226 | }; 227 | 228 | this.homedir = function() { 229 | if(typeof os.homedir === 'function') return os.homedir(); 230 | 231 | var env = process.env; 232 | var home = env.HOME; 233 | var user = env.LOGNAME || env.USER || env.LNAME || env.USERNAME; 234 | 235 | if (process.platform === 'win32') { 236 | return env.USERPROFILE || env.HOMEDRIVE + env.HOMEPATH || home || null; 237 | } 238 | 239 | if (process.platform === 'darwin') { 240 | return home || (user ? '/Users/' + user : null); 241 | } 242 | 243 | if (process.platform === 'linux') { 244 | return home || (process.getuid() === 0 ? '/root' : (user ? '/home/' + user : null)); 245 | } 246 | 247 | return home || null; 248 | }; 249 | 250 | this.orderedPaths = [ 251 | path.join(this.homedir(), '.browserstack'), 252 | process.cwd(), 253 | os.tmpdir() 254 | ]; 255 | } 256 | 257 | module.exports = LocalBinary; 258 | -------------------------------------------------------------------------------- /lib/LocalError.js: -------------------------------------------------------------------------------- 1 | module.exports = function LocalError(message, extra) { 2 | Error.captureStackTrace(this, this.constructor); 3 | this.name = this.constructor.name; 4 | this.message = message; 5 | this.extra = extra; 6 | }; 7 | 8 | require('util').inherits(module.exports, Error); 9 | -------------------------------------------------------------------------------- /lib/download.js: -------------------------------------------------------------------------------- 1 | const https = require('https'), 2 | fs = require('fs'), 3 | HttpsProxyAgent = require('https-proxy-agent'), 4 | url = require('url'), 5 | zlib = require('zlib'); 6 | 7 | const binaryPath = process.argv[2], httpPath = process.argv[3], proxyHost = process.argv[4], proxyPort = process.argv[5], useCaCertificate = process.argv[6]; 8 | 9 | var fileStream = fs.createWriteStream(binaryPath); 10 | 11 | var options = url.parse(httpPath); 12 | if(proxyHost && proxyPort) { 13 | options.agent = new HttpsProxyAgent({ 14 | host: proxyHost, 15 | port: proxyPort 16 | }); 17 | if (useCaCertificate) { 18 | try { 19 | options.ca = fs.readFileSync(useCaCertificate); 20 | } catch(err) { 21 | console.log('failed to read cert file', err); 22 | } 23 | } 24 | } 25 | 26 | options.headers = Object.assign({}, options.headers, { 27 | 'accept-encoding': 'gzip, *', 28 | 'user-agent': process.env.USER_AGENT, 29 | }); 30 | 31 | https.get(options, function (response) { 32 | const contentEncoding = response.headers['content-encoding']; 33 | if (typeof contentEncoding === 'string' && contentEncoding.match(/gzip/i)) { 34 | if (process.env.BROWSERSTACK_LOCAL_DEBUG_GZIP) { 35 | console.info('Using gzip in ' + options.headers['user-agent']); 36 | } 37 | 38 | response.pipe(zlib.createGunzip()).pipe(fileStream); 39 | } else { 40 | response.pipe(fileStream); 41 | } 42 | 43 | response.on('error', function(err) { 44 | console.error('Got Error in binary download response', err); 45 | }); 46 | fileStream.on('error', function (err) { 47 | console.error('Got Error while downloading binary file', err); 48 | }); 49 | fileStream.on('close', function () { 50 | console.log('Done'); 51 | }); 52 | }).on('error', function(err) { 53 | console.error('Got Error in binary downloading request', err); 54 | }); 55 | -------------------------------------------------------------------------------- /node-example.js: -------------------------------------------------------------------------------- 1 | var browserstack = require('./index'); 2 | 3 | var local = new browserstack.Local(); 4 | var webdriver = require('selenium-webdriver'); 5 | var identifier = 'adqqwdqwd'; 6 | 7 | var capabilities = { 8 | build: 'build', 9 | 'browserName': 'chrome', 10 | 'os': 'OS X', 11 | 'browserstack.local': true 12 | //'browserstack.localIdentifier': identifier 13 | } 14 | 15 | var options = { 16 | 'key': process.env.BROWSERSTACK_ACCESS_KEY, 17 | //hosts: [{ 18 | // name: 'localhost', 19 | // port: 8080, 20 | // sslFlag: 0 21 | //}], 22 | //'-f': __dirname, 23 | //'-binaryPath': '/var/BrowserStackLocal' 24 | }; 25 | 26 | // try { 27 | local.start(options, function() { 28 | console.log('Is Running ' + local.isRunning()); 29 | console.log('Started'); 30 | 31 | capabilities['browserstack.user'] = process.env.BROWSERSTACK_USERNAME; 32 | capabilities['browserstack.key'] = process.env.BROWSERSTACK_ACCESS_KEY; 33 | capabilities['browserstack.local'] = true; 34 | //capabilities['browserstack.localIdentifier'] = identifier; 35 | 36 | driver = new webdriver.Builder().usingServer('http://hub.browserstack.com/wd/hub').withCapabilities(capabilities).build(); 37 | console.log('Is Running ' + local.isRunning()); 38 | driver.get("http://www.google.com").then(function() { 39 | console.log('Is Running ' + local.isRunning()); 40 | driver.quit().then(function() { 41 | console.log('Is Running ' + local.isRunning()); 42 | local.stop(function() { 43 | console.log('Is Running ' + local.isRunning()); 44 | console.log('Stopped'); 45 | }); 46 | }); 47 | }); 48 | }); 49 | // } 50 | // catch(error){ 51 | // console.log("Got Error From Local " + error); 52 | // process.exit(); 53 | // } 54 | 55 | 56 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "browserstack-local", 3 | "version": "1.5.6", 4 | "description": "Nodejs bindings for BrowserStack Local", 5 | "engine": "^0.10.44", 6 | "main": "index.js", 7 | "types": "index.d.ts", 8 | "scripts": { 9 | "pretest": "./node_modules/.bin/eslint lib/* index.js", 10 | "test": "./node_modules/.bin/mocha" 11 | }, 12 | "keywords": [ 13 | "BrowserStack", 14 | "Local", 15 | "selenium", 16 | "testing" 17 | ], 18 | "author": "BrowserStack", 19 | "license": "MIT", 20 | "dependencies": { 21 | "https-proxy-agent": "^5.0.1", 22 | "is-running": "^2.1.0", 23 | "ps-tree": "=1.2.0", 24 | "temp-fs": "^0.9.9", 25 | "agent-base": "^6.0.2" 26 | }, 27 | "devDependencies": { 28 | "eslint": "^7.0.0", 29 | "eslint-plugin-mocha": "^10.1.0", 30 | "expect.js": "0.3.1", 31 | "mocha": "^10.0.0", 32 | "mocks": "0.0.15", 33 | "proxy": "^1.0.2", 34 | "rimraf": "^3.0.2", 35 | "sinon": "^1.17.6" 36 | }, 37 | "bugs": "https://github.com/browserstack/browserstack-local-nodejs/issues", 38 | "homepage": "https://github.com/browserstack/browserstack-local-nodejs", 39 | "repository": { 40 | "type": "git", 41 | "url": "https://github.com/browserstack/browserstack-local-nodejs.git" 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /test/local.js: -------------------------------------------------------------------------------- 1 | var expect = require('expect.js'), 2 | sinon = require('sinon'), 3 | mocks = require('mocks'), 4 | path = require('path'), 5 | fs = require('fs'), 6 | rimraf = require('rimraf'), 7 | Proxy = require('proxy'), 8 | tempfs = require('temp-fs'), 9 | browserstack = require('../index'), 10 | LocalBinary = require('../lib/LocalBinary'); 11 | 12 | 13 | const MAX_TIMEOUT = 600000; 14 | 15 | describe('Local', function () { 16 | var bsLocal; 17 | beforeEach(function () { 18 | bsLocal = new browserstack.Local(); 19 | }); 20 | 21 | it('should have pid when running', function (done) { 22 | this.timeout(600000); 23 | bsLocal.start({ 'key': process.env.BROWSERSTACK_ACCESS_KEY }, function(){ 24 | expect(bsLocal.tunnel.pid).to.not.equal(0); 25 | done(); 26 | }); 27 | }); 28 | 29 | it('should return is running properly', function (done) { 30 | this.timeout(60000); 31 | expect(bsLocal.isRunning()).to.not.equal(true); 32 | bsLocal.start({ 'key': process.env.BROWSERSTACK_ACCESS_KEY }, function(){ 33 | expect(bsLocal.isRunning()).to.equal(true); 34 | done(); 35 | }); 36 | }); 37 | 38 | it.skip('should throw error on running multiple binary', function (done) { 39 | this.timeout(60000); 40 | bsLocal.start({ 'key': process.env.BROWSERSTACK_ACCESS_KEY }, function(error){ 41 | bsLocal_2 = new browserstack.Local(); 42 | var tempLogPath = path.join(process.cwd(), 'log2.log'); 43 | 44 | bsLocal_2.start({ 'key': process.env.BROWSERSTACK_ACCESS_KEY, 'logfile': tempLogPath }, function(error){ 45 | expect(error.toString().trim()).to.equal('LocalError: Either another browserstack local client is running on your machine or some server is listening on port 45690'); 46 | fs.unlinkSync(tempLogPath); 47 | done(); 48 | }); 49 | }); 50 | }); 51 | 52 | it('should enable verbose', function (done) { 53 | bsLocal.start({ 'key': process.env.BROWSERSTACK_ACCESS_KEY, onlyCommand: true, 'verbose': true }, function(){ 54 | expect(bsLocal.getBinaryArgs().indexOf('--verbose')).to.not.equal(-1); 55 | expect(bsLocal.getBinaryArgs().indexOf('1')).to.not.equal(-1); 56 | done(); 57 | }); 58 | }); 59 | 60 | it('should enable verbose with log level', function (done) { 61 | bsLocal.start({ 'key': process.env.BROWSERSTACK_ACCESS_KEY, onlyCommand: true, 'verbose': 2 }, function(){ 62 | expect(bsLocal.getBinaryArgs().indexOf('--verbose')).to.not.equal(-1); 63 | expect(bsLocal.getBinaryArgs().indexOf('2')).to.not.equal(-1); 64 | done(); 65 | }); 66 | }); 67 | 68 | it('should enable verbose with log level string', function (done) { 69 | bsLocal.start({ 'key': process.env.BROWSERSTACK_ACCESS_KEY, onlyCommand: true, 'verbose': '2' }, function(){ 70 | expect(bsLocal.getBinaryArgs().indexOf('--verbose')).to.not.equal(-1); 71 | expect(bsLocal.getBinaryArgs().indexOf('2')).to.not.equal(-1); 72 | done(); 73 | }); 74 | }); 75 | 76 | it('should set folder testing', function (done) { 77 | bsLocal.start({ 'key': process.env.BROWSERSTACK_ACCESS_KEY, onlyCommand: true, 'f': '/var/html' }, function(){ 78 | expect(bsLocal.getBinaryArgs().indexOf('-f')).to.not.equal(-1); 79 | expect(bsLocal.getBinaryArgs().indexOf('/var/html')).to.not.equal(-1); 80 | done(); 81 | }); 82 | }); 83 | 84 | it('should set folder testing with folder option', function (done) { 85 | bsLocal.start({ 'key': process.env.BROWSERSTACK_ACCESS_KEY, onlyCommand: true, 'folder': '/var/html' }, function(){ 86 | expect(bsLocal.getBinaryArgs().indexOf('-f')).to.not.equal(-1); 87 | expect(bsLocal.getBinaryArgs().indexOf('/var/html')).to.not.equal(-1); 88 | done(); 89 | }); 90 | }); 91 | 92 | it('should enable force', function (done) { 93 | bsLocal.start({ 'key': process.env.BROWSERSTACK_ACCESS_KEY, onlyCommand: true, 'force': true }, function(){ 94 | expect(bsLocal.getBinaryArgs().indexOf('--force')).to.not.equal(-1); 95 | done(); 96 | }); 97 | }); 98 | 99 | it('should enable only', function (done) { 100 | bsLocal.start({ 'key': process.env.BROWSERSTACK_ACCESS_KEY, onlyCommand: true, 'only': true }, function(){ 101 | expect(bsLocal.getBinaryArgs().indexOf('--only')).to.not.equal(-1); 102 | done(); 103 | }); 104 | }); 105 | 106 | it('should enable onlyAutomate', function (done) { 107 | bsLocal.start({ 'key': process.env.BROWSERSTACK_ACCESS_KEY, onlyCommand: true, 'onlyAutomate': true }, function(){ 108 | expect(bsLocal.getBinaryArgs().indexOf('--only-automate')).to.not.equal(-1); 109 | done(); 110 | }); 111 | }); 112 | 113 | it('should enable forcelocal', function (done) { 114 | bsLocal.start({ 'key': process.env.BROWSERSTACK_ACCESS_KEY, onlyCommand: true, 'forcelocal': true }, function(){ 115 | expect(bsLocal.getBinaryArgs().indexOf('--force-local')).to.not.equal(-1); 116 | done(); 117 | }); 118 | }); 119 | 120 | it('should enable forcelocal with camel case', function (done) { 121 | bsLocal.start({ 'key': process.env.BROWSERSTACK_ACCESS_KEY, onlyCommand: true, 'forceLocal': true }, function(){ 122 | expect(bsLocal.getBinaryArgs().indexOf('--force-local')).to.not.equal(-1); 123 | done(); 124 | }); 125 | }); 126 | 127 | it('should enable custom boolean args', function (done) { 128 | bsLocal.start({ 'key': process.env.BROWSERSTACK_ACCESS_KEY, onlyCommand: true, 'boolArg1': true, 'boolArg2': true }, function(){ 129 | expect(bsLocal.getBinaryArgs().indexOf('--boolArg1')).to.not.equal(-1); 130 | expect(bsLocal.getBinaryArgs().indexOf('--boolArg2')).to.not.equal(-1); 131 | done(); 132 | }); 133 | }); 134 | 135 | it('should enable custom keyval args', function (done) { 136 | bsLocal.start({ 'key': process.env.BROWSERSTACK_ACCESS_KEY, onlyCommand: true, 'customKey1': 'custom value1', 'customKey2': 'custom value2' }, function(){ 137 | expect(bsLocal.getBinaryArgs().indexOf('--customKey1')).to.not.equal(-1); 138 | expect(bsLocal.getBinaryArgs().indexOf('custom value1')).to.not.equal(-1); 139 | expect(bsLocal.getBinaryArgs().indexOf('--customKey2')).to.not.equal(-1); 140 | expect(bsLocal.getBinaryArgs().indexOf('custom value2')).to.not.equal(-1); 141 | done(); 142 | }); 143 | }); 144 | 145 | it('should enable forceproxy', function (done) { 146 | bsLocal.start({ 'key': process.env.BROWSERSTACK_ACCESS_KEY, onlyCommand: true, 'forceproxy': true }, function(){ 147 | expect(bsLocal.getBinaryArgs().indexOf('--force-proxy')).to.not.equal(-1); 148 | done(); 149 | }); 150 | }); 151 | 152 | it('should enable forceproxy with camel case', function (done) { 153 | bsLocal.start({ 'key': process.env.BROWSERSTACK_ACCESS_KEY, onlyCommand: true, 'forceProxy': true }, function(){ 154 | expect(bsLocal.getBinaryArgs().indexOf('--force-proxy')).to.not.equal(-1); 155 | done(); 156 | }); 157 | }); 158 | 159 | 160 | it('should set localIdentifier', function (done) { 161 | bsLocal.start({ 'key': process.env.BROWSERSTACK_ACCESS_KEY, onlyCommand: true, 'localIdentifier': 'abcdef' }, function(){ 162 | expect(bsLocal.getBinaryArgs().indexOf('--local-identifier')).to.not.equal(-1); 163 | expect(bsLocal.getBinaryArgs().indexOf('abcdef')).to.not.equal(-1); 164 | done(); 165 | }); 166 | }); 167 | 168 | it('should set parallelRuns', function (done) { 169 | bsLocal.start({ 'key': process.env.BROWSERSTACK_ACCESS_KEY, onlyCommand: true, 'parallelRuns': '10' }, function(){ 170 | expect(bsLocal.getBinaryArgs().indexOf('--parallel-runs')).to.not.equal(-1); 171 | expect(bsLocal.getBinaryArgs().indexOf('10')).to.not.equal(-1); 172 | done(); 173 | }); 174 | }); 175 | 176 | it('should set parallelRuns with integer value', function (done) { 177 | bsLocal.start({ 'key': process.env.BROWSERSTACK_ACCESS_KEY, onlyCommand: true, 'parallelRuns': 10 }, function(){ 178 | expect(bsLocal.getBinaryArgs().indexOf('--parallel-runs')).to.not.equal(-1); 179 | expect(bsLocal.getBinaryArgs().indexOf('10')).to.not.equal(-1); 180 | done(); 181 | }); 182 | }); 183 | 184 | it('should set proxy', function (done) { 185 | bsLocal.start({ 186 | 'key': process.env.BROWSERSTACK_ACCESS_KEY, 187 | onlyCommand: true, 188 | 'proxyHost': 'localhost', 189 | 'proxyPort': 8080, 190 | 'proxyUser': 'user', 191 | 'proxyPass': 'pass' 192 | }, function(){ 193 | expect(bsLocal.getBinaryArgs().indexOf('--proxy-host')).to.not.equal(-1); 194 | expect(bsLocal.getBinaryArgs().indexOf('localhost')).to.not.equal(-1); 195 | expect(bsLocal.getBinaryArgs().indexOf('--proxy-port')).to.not.equal(-1); 196 | expect(bsLocal.getBinaryArgs().indexOf(8080)).to.not.equal(-1); 197 | expect(bsLocal.getBinaryArgs().indexOf('--proxy-user')).to.not.equal(-1); 198 | expect(bsLocal.getBinaryArgs().indexOf('user')).to.not.equal(-1); 199 | expect(bsLocal.getBinaryArgs().indexOf('--proxy-pass')).to.not.equal(-1); 200 | expect(bsLocal.getBinaryArgs().indexOf('pass')).to.not.equal(-1); 201 | done(); 202 | }); 203 | }); 204 | 205 | it('should set hosts', function (done) { 206 | bsLocal.start({ 'key': process.env.BROWSERSTACK_ACCESS_KEY, onlyCommand: true, 'only': 'localhost,8000,0'}, function(){ 207 | expect(bsLocal.getBinaryArgs().indexOf('--only')).to.not.equal(-1); 208 | expect(bsLocal.getBinaryArgs().indexOf('localhost,8000,0')).to.not.equal(-1); 209 | done(); 210 | }); 211 | }); 212 | 213 | it('should stop local', function (done) { 214 | this.timeout(MAX_TIMEOUT); 215 | bsLocal.start({ 'key': process.env.BROWSERSTACK_ACCESS_KEY}, function(){ 216 | expect(bsLocal.isRunning()).to.equal(true); 217 | bsLocal.stop(function(){ 218 | expect(bsLocal.isRunning()).to.equal(false); 219 | done(); 220 | }); 221 | }); 222 | }); 223 | 224 | afterEach(function (done) { 225 | this.timeout(60000); 226 | bsLocal.stop(done); 227 | }); 228 | }); 229 | 230 | describe('Start sync', () => { 231 | var bsLocal, bsLocal_2; 232 | beforeEach(function () { 233 | bsLocal = new browserstack.Local(); 234 | }); 235 | 236 | it('should have pid when running', function () { 237 | this.timeout(60000); 238 | bsLocal.startSync({ 'key': process.env.BROWSERSTACK_ACCESS_KEY}); 239 | expect(bsLocal.tunnel.pid).to.not.equal(0); 240 | }); 241 | 242 | it('should return is running properly', function () { 243 | this.timeout(60000); 244 | expect(bsLocal.isRunning()).to.not.equal(true); 245 | bsLocal.startSync({ 'key': process.env.BROWSERSTACK_ACCESS_KEY}); 246 | expect(bsLocal.isRunning()).to.equal(true); 247 | }); 248 | 249 | it.skip('should throw error on running multiple binary', function () { 250 | this.timeout(60000); 251 | bsLocal.startSync({ 'key': process.env.BROWSERSTACK_ACCESS_KEY }); 252 | bsLocal_2 = new browserstack.Local(); 253 | var tempLogPath = path.join(process.cwd(), 'log2.log'); 254 | const error = bsLocal_2.startSync({ 'key': process.env.BROWSERSTACK_ACCESS_KEY, 'logfile': tempLogPath }); 255 | expect(error.toString().trim()).to.equal('LocalError: Either another browserstack local client is running on your machine or some server is listening on port 45690'); 256 | fs.unlinkSync(tempLogPath); 257 | }); 258 | 259 | afterEach(function (done) { 260 | this.timeout(60000); 261 | bsLocal.stop(() => { 262 | if (bsLocal_2) { 263 | bsLocal_2.stop(done); 264 | } else { 265 | done(); 266 | } 267 | }); 268 | }); 269 | }) 270 | 271 | describe('LocalBinary', function () { 272 | describe('Retries', function() { 273 | var unlinkTmp, 274 | defaultBinaryPath, 275 | validBinaryPath, 276 | sandBox; 277 | 278 | before(function(done) { 279 | this.timeout(MAX_TIMEOUT); 280 | // ensure that we have a valid binary downloaded 281 | 282 | // removeIfInvalid(); 283 | (new LocalBinary()).binaryPath({}, function(binaryPath) { 284 | defaultBinaryPath = binaryPath; 285 | tempfs.mkdir({ 286 | recursive: true 287 | }, function(err, dir) { 288 | if(err) { throw err; } 289 | 290 | validBinaryPath = path.join(dir.path, path.basename(binaryPath)); 291 | fs.rename(defaultBinaryPath, validBinaryPath, function(err) { 292 | if(err) { throw err; } 293 | 294 | unlinkTmp = dir.unlink; 295 | done(); 296 | }); 297 | }); 298 | }); 299 | }); 300 | 301 | beforeEach(function() { 302 | sandBox = sinon.sandbox.create(); 303 | }); 304 | 305 | it('Tries to download binary if its corrupted', function(done) { 306 | fs.unlink(defaultBinaryPath, function() { 307 | var localBinary = new LocalBinary(); 308 | var downloadStub = sandBox.stub(localBinary, 'download', function() { 309 | downloadStub.callArgWith(2, [ defaultBinaryPath ]); 310 | expect(downloadStub.args[0][3]).to.be(5); 311 | }); 312 | 313 | fs.writeFile(defaultBinaryPath, 'Random String', function() { 314 | fs.chmod(defaultBinaryPath, '0755', function() { 315 | localBinary.binaryPath({ 316 | }, function(binaryPath) { 317 | expect(downloadStub.called).to.be.true; 318 | done(); 319 | }); 320 | }); 321 | }); 322 | }); 323 | }); 324 | 325 | it('Tries to download binary if its not present', function(done) { 326 | fs.unlink(defaultBinaryPath, function() { 327 | var localBinary = new LocalBinary(); 328 | var downloadStub = sandBox.stub(localBinary, 'download', function() { 329 | downloadStub.callArgWith(2, [ defaultBinaryPath ]); 330 | expect(downloadStub.args[0][3]).to.be(5); 331 | }); 332 | 333 | localBinary.binaryPath({ 334 | }, function(binaryPath) { 335 | expect(downloadStub.called).to.be.true; 336 | done(); 337 | }); 338 | }); 339 | }); 340 | 341 | afterEach(function(done) { 342 | sandBox.restore(); 343 | done(); 344 | }); 345 | 346 | after(function(done) { 347 | fs.rename(validBinaryPath, defaultBinaryPath, function(err) { 348 | if(err) { throw err; } 349 | 350 | unlinkTmp(done); 351 | }); 352 | }); 353 | }); 354 | 355 | describe('Download Path', function() { 356 | var sandBox; 357 | var localBinary; 358 | 359 | beforeEach(function() { 360 | sandBox = sinon.sandbox.create(); 361 | localBinary = new LocalBinary(); 362 | }); 363 | 364 | it('should return download path of darwin binary', function() { 365 | var osNames = ['darwin', 'mac os']; 366 | osNames.forEach(function(os) { 367 | sandBox.stub(localBinary, 'hostOS', os); 368 | expect(localBinary.getDownloadPath()).to.equal('https://www.browserstack.com/local-testing/downloads/binaries/BrowserStackLocal-darwin-x64'); 369 | }); 370 | }); 371 | 372 | it('should return download path of exe binary', function() { 373 | var osNames = ['mswin', 'msys', 'mingw', 'cygwin', 'bccwin', 'wince', 'emc', 'win32']; 374 | osNames.forEach(function(os) { 375 | sandBox.stub(localBinary, 'hostOS', os); 376 | expect(localBinary.getDownloadPath()).to.equal('https://www.browserstack.com/local-testing/downloads/binaries/BrowserStackLocal.exe'); 377 | }); 378 | }); 379 | 380 | it('should return download path of linux 64 arch binary', function() { 381 | sandBox.stub(localBinary, 'hostOS', 'linux'); 382 | sandBox.stub(localBinary, 'is64bits', true); 383 | localBinary.isAlpine = sandBox.stub(localBinary, 'isAlpine').returns(false); 384 | expect(localBinary.getDownloadPath()).to.equal('https://www.browserstack.com/local-testing/downloads/binaries/BrowserStackLocal-linux-x64'); 385 | }); 386 | 387 | it('should return download path of linux 32 arch binary', function() { 388 | sandBox.stub(localBinary, 'hostOS', 'linux'); 389 | sandBox.stub(localBinary, 'is64bits', false); 390 | localBinary.isAlpine = sandBox.stub(localBinary, 'isAlpine').returns(false); 391 | expect(localBinary.getDownloadPath()).to.equal('https://www.browserstack.com/local-testing/downloads/binaries/BrowserStackLocal-linux-ia32'); 392 | }); 393 | 394 | it('should return download path of alpine linux binary', function() { 395 | sandBox.stub(localBinary, 'hostOS', 'linux'); 396 | localBinary.isAlpine = sandBox.stub(localBinary, 'isAlpine').returns(true); 397 | sandBox.stub(localBinary, 'is64bits', true); 398 | expect(localBinary.getDownloadPath()).to.equal('https://www.browserstack.com/local-testing/downloads/binaries/BrowserStackLocal-alpine'); 399 | }); 400 | 401 | afterEach(function(done) { 402 | sandBox.restore(); 403 | done(); 404 | }); 405 | }); 406 | 407 | describe('Download', function() { 408 | var proxy; 409 | var proxyPort; 410 | var binary; 411 | var tempDownloadPath; 412 | 413 | before(function (done) { 414 | // setup HTTP proxy server 415 | proxy = new Proxy(); 416 | proxy.listen(function () { 417 | proxyPort = proxy.address().port; 418 | done(); 419 | }); 420 | }); 421 | 422 | after(function (done) { 423 | proxy.once('close', function () { done(); }); 424 | proxy.close(); 425 | }); 426 | 427 | beforeEach(function () { 428 | binary = new LocalBinary(); 429 | tempDownloadPath = path.join(process.cwd(), 'download'); 430 | }); 431 | 432 | afterEach(function () { 433 | rimraf.sync(tempDownloadPath); 434 | }); 435 | 436 | it('should download binaries without proxy', function (done) { 437 | this.timeout(MAX_TIMEOUT); 438 | var conf = {}; 439 | binary.download(conf, tempDownloadPath, function (result) { 440 | expect(fs.existsSync(result)).to.equal(true); 441 | done(); 442 | }); 443 | }); 444 | 445 | it('should download binaries with proxy', function (done) { 446 | this.timeout(MAX_TIMEOUT); 447 | var conf = { 448 | proxyHost: '127.0.0.1', 449 | proxyPort: proxyPort 450 | }; 451 | binary.download(conf, tempDownloadPath, function (result) { 452 | // test for file existence 453 | expect(fs.existsSync(result)).to.equal(true); 454 | done(); 455 | }); 456 | }); 457 | 458 | it('should download binaries in sync', function () { 459 | this.timeout(MAX_TIMEOUT); 460 | var conf = {}; 461 | const result = binary.downloadSync(conf, tempDownloadPath); 462 | expect(fs.existsSync(result)).to.equal(true); 463 | }); 464 | }); 465 | }); 466 | --------------------------------------------------------------------------------