├── .gitignore
├── fixtures
├── static
│ ├── base2.html
│ └── base.html
├── run_server.js
└── server.js
├── test
├── custom_agent_module.js
├── subtitle_test.js
├── custom_agent_module_test.js
├── base-chrome-pool-proxy-test.js
├── base-chrome-pool-no-reuse-test.js
├── base-proxy-test.js
├── chrome-pool-navigate-test.js
├── chrome-pool-screen-recording-test.js
├── chrome-pool-extra-headers-test.js
├── chrome-pool-script-test.js
└── chrome-pool-cookies-test.js
├── .eslintrc.js
├── examples
├── example.json
├── screen_recorder.js
├── add_script.js
└── screen_recorder.py
├── .travis.yml
├── LICENSE.txt
├── package.json
├── scripts
└── run-tests.sh
├── index.js
├── bin
└── chromedriver-proxy
├── lib
├── chrome_agent_slave.js
├── chrome_agent_manager.js
├── chromedriver.js
├── chrome_pool.js
├── chrome_agent.js
├── realtime_screen_recorder.js
├── proxy.js
└── screen_recorder.js
├── clients
├── py
│ └── chrome_driver_proxy.py
└── js
│ └── chrome_driver_proxy.js
└── README.md
/.gitignore:
--------------------------------------------------------------------------------
1 | .git
2 | node_modules
3 |
--------------------------------------------------------------------------------
/fixtures/static/base2.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | base 2 page
4 |
5 |
6 | base 2 page
7 |
8 |
9 |
--------------------------------------------------------------------------------
/fixtures/static/base.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | base page
4 |
5 |
6 | base page
7 | second page
8 |
9 |
10 |
--------------------------------------------------------------------------------
/test/custom_agent_module.js:
--------------------------------------------------------------------------------
1 |
2 | const ChromeAgent = require('../lib/chrome_agent.js')
3 |
4 | class CustomAgent extends ChromeAgent {
5 | constructor(options) {
6 | super(options);
7 | }
8 | }
9 |
10 | module.exports = CustomAgent;
11 |
--------------------------------------------------------------------------------
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | "extends": "airbnb-base",
3 | rules: {
4 | "indent": ["error", 2],
5 | "linebreak-style": ["error", "unix"],
6 | "global-require": ["warn"],
7 | "import/no-dynamic-require": ["warn"],
8 | "no-console": ["warn"],
9 | }
10 | };
--------------------------------------------------------------------------------
/examples/example.json:
--------------------------------------------------------------------------------
1 | {
2 | "tmpDir": "/tmp",
3 | "proxy": {
4 | "port": 4444,
5 | "baseUrl": "/wd/hub"
6 | },
7 | "chromedriver": {
8 | "chromedriverPath": "/home/jgowan/bin/chromedriver",
9 | "port": 4445,
10 | "autoRestart": true
11 | },
12 | "chromePool": {
13 | "enable": true,
14 | "chromePath": "/usr/bin/google-chrome",
15 | "reuse": true,
16 | "chromeAgent": {
17 | "screenRecorder": {
18 | "videoFormat": "mp4"
19 | }
20 | }
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/examples/screen_recorder.js:
--------------------------------------------------------------------------------
1 |
2 |
3 | const ChromeOptions = require('selenium-webdriver/chrome').Options;
4 | const Driver = require('../clients/js/chrome_driver_proxy');
5 |
6 | const chromeOptions = new ChromeOptions();
7 | chromeOptions.addArguments(
8 | '--headless',
9 | '--disable-gpu',
10 | '--no-first-run',
11 | '--no-sandbox',
12 | );
13 | const options = chromeOptions.toCapabilities();
14 |
15 | const driver = Driver.createSession('http://127.0.0.1:4444/wd/hub', options);
16 | driver.startScreencast({ params: { format: 'jpeg', quality: 80, everyNthFrame: 2 } }).then(() => driver.get('https://www.ziprecruiter.com')).then(() => driver.sleep(2)).then(() => driver.get('https://www.ziprecruiter.com/candidate/search?search=accountant&location='))
17 | .then(() => driver.sleep(1))
18 | .then(() => driver.stopScreencast())
19 | .then(() => driver.getScreencastPath())
20 | .then((result) => {
21 | console.log(result);
22 | return driver.quit();
23 | });
24 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 | env:
3 | - CHROMEDRIVER_BIN=/tmp/chromedriver
4 | matrix:
5 | include:
6 | - node_js: "8"
7 | dist: trusty
8 | sudo: false
9 | cache:
10 | directories:
11 | - node_modules
12 | - chrome
13 | before_script:
14 | - curl https://chromedriver.storage.googleapis.com/2.42/chromedriver_linux64.zip > /tmp/chromedriver_linux64.zip
15 | - unzip -d /tmp /tmp/chromedriver_linux64.zip
16 | - /tmp/chromedriver --version
17 | - google-chrome-beta --version
18 | - sudo rm /usr/bin/google-chrome
19 | - sudo ln -s /usr/bin/google-chrome-beta /usr/bin/google-chrome
20 | - google-chrome --version
21 | - npm list
22 | - wget https://johnvansickle.com/ffmpeg/releases/ffmpeg-release-64bit-static.tar.xz -O /tmp/ffmpeg-release-64bit-static.tar.xz
23 | - tar -xJf /tmp/ffmpeg-release-64bit-static.tar.xz -C /tmp
24 | - /tmp/ffmpeg-4.0.2-64bit-static/ffmpeg -version
25 | - sudo mv /tmp/ffmpeg-4.0.2-64bit-static/ffmpeg /usr/bin/ffmpeg
26 | addons:
27 | chrome: beta
28 | apt:
29 | packages:
30 | - xz-utils
31 |
--------------------------------------------------------------------------------
/LICENSE.txt:
--------------------------------------------------------------------------------
1 | Copyright (c) 2017 ZipRecruiter
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining a copy
4 | of this software and associated documentation files (the "Software"), to deal
5 | in the Software without restriction, including without limitation the rights
6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7 | copies of the Software, and to permit persons to whom the Software is
8 | furnished to do so, subject to the following conditions:
9 |
10 | The above copyright notice and this permission notice shall be included in all
11 | copies or substantial portions of the Software.
12 |
13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
19 | SOFTWARE.
20 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "chromedriver-proxy",
3 | "version": "1.0.6",
4 | "description": "An extensible proxy to chromedriver",
5 | "keywords": [
6 | "selenium",
7 | "chrome",
8 | "chromedriver",
9 | "headless"
10 | ],
11 | "engines": {
12 | "node": ">=8 <11"
13 | },
14 | "author": "Jason Gowan",
15 | "license": "MIT",
16 | "files": [
17 | "README.md",
18 | "LICENSE.txt",
19 | "index.js",
20 | "package.json",
21 | "package-lock.json",
22 | "bin/",
23 | "lib/"
24 | ],
25 | "dependencies": {
26 | "aws-sdk": "^2.1287.0",
27 | "chrome-remote-interface": "*",
28 | "commander": "^2.20.3",
29 | "debug": "^3.2.7",
30 | "get-port": "^3.2.0",
31 | "http-proxy": "^1.18.1",
32 | "yarn": "^1.22.18"
33 | },
34 | "devDependencies": {
35 | "chai": "^4.3.7",
36 | "js-yaml": "^3.14.1",
37 | "mocha": "^9.2.2",
38 | "node-simple-router": "^0.10.2",
39 | "pkg.json": "*",
40 | "selenium-webdriver": "~4.1.0"
41 | },
42 | "scripts": {
43 | "test": "./scripts/run-tests.sh"
44 | },
45 | "bin": {
46 | "chromedriver-proxy": "./bin/chromedriver-proxy"
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/fixtures/run_server.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) 2017 ZipRecruiter
3 | *
4 | * Permission is hereby granted, free of charge, to any person obtaining a copy
5 | * of this software and associated documentation files (the "Software"), to deal
6 | * in the Software without restriction, including without limitation the rights
7 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8 | * copies of the Software, and to permit persons to whom the Software is
9 | * furnished to do so, subject to the following conditions:
10 | *
11 | * The above copyright notice and this permission notice shall be included in all
12 | * copies or substantial portions of the Software.
13 | *
14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
20 | * SOFTWARE.
21 | **/
22 |
23 | 'use strict'
24 |
25 | require('./server')(8080)
26 |
--------------------------------------------------------------------------------
/scripts/run-tests.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | # Copyright (c) 2017 ZipRecruiter
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 all
13 | # 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 THE
21 | # SOFTWARE.
22 |
23 | export DEBUG=chromedriver_proxy:*
24 |
25 | fuser -k 8080/tcp
26 |
27 | (node fixtures/run_server.js &) && mocha --exit --timeout 10000 && fuser -k 8080/tcp
28 |
--------------------------------------------------------------------------------
/index.js:
--------------------------------------------------------------------------------
1 |
2 |
3 | /**
4 | * Copyright (c) 2017 ZipRecruiter
5 | *
6 | * Permission is hereby granted, free of charge, to any person obtaining a copy
7 | * of this software and associated documentation files (the "Software"), to deal
8 | * in the Software without restriction, including without limitation the rights
9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | * copies of the Software, and to permit persons to whom the Software is
11 | * furnished to do so, subject to the following conditions:
12 | *
13 | * The above copyright notice and this permission notice shall be included in all
14 | * copies or substantial portions of the Software.
15 | *
16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 | * SOFTWARE.
23 | * */
24 |
25 | const HttpServer = require('./lib/proxy');
26 | const ChromePool = require('./lib/chrome_pool');
27 | const ChromeAgent = require('./lib/chrome_agent');
28 | const ScreenRecorder = require('./lib/screen_recorder');
29 |
30 | module.exports.HttpServer = HttpServer;
31 | module.exports.ChromePool = ChromePool;
32 | module.exports.ScreenRecorder = ScreenRecorder;
33 | module.exports.ChromeAgent = ChromeAgent;
34 |
--------------------------------------------------------------------------------
/bin/chromedriver-proxy:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 |
3 | /**
4 | * Copyright (c) 2017 ZipRecruiter
5 | *
6 | * Permission is hereby granted, free of charge, to any person obtaining a copy
7 | * of this software and associated documentation files (the "Software"), to deal
8 | * in the Software without restriction, including without limitation the rights
9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | * copies of the Software, and to permit persons to whom the Software is
11 | * furnished to do so, subject to the following conditions:
12 | *
13 | * The above copyright notice and this permission notice shall be included in all
14 | * copies or substantial portions of the Software.
15 | *
16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 | * SOFTWARE.
23 | * */
24 |
25 | const program = require('commander');
26 | const { HttpServer } = require('..');
27 | const fs = require('fs');
28 |
29 | program
30 | .version('1.0.6')
31 | .option('--config [value]', 'json config file')
32 | .parse(process.argv);
33 |
34 | let config = {};
35 |
36 | if (program.config) {
37 | config = JSON.parse(fs.readFileSync(program.config));
38 | }
39 |
40 | const s = new HttpServer(config.proxy);
41 | s.start(config);
42 | process.on('SIGINT', () => {
43 | s.stop();
44 | process.exit();
45 | });
46 |
--------------------------------------------------------------------------------
/examples/add_script.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) 2017 ZipRecruiter
3 | *
4 | * Permission is hereby granted, free of charge, to any person obtaining a copy
5 | * of this software and associated documentation files (the "Software"), to deal
6 | * in the Software without restriction, including without limitation the rights
7 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8 | * copies of the Software, and to permit persons to whom the Software is
9 | * furnished to do so, subject to the following conditions:
10 | *
11 | * The above copyright notice and this permission notice shall be included in all
12 | * copies or substantial portions of the Software.
13 | *
14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
20 | * SOFTWARE.
21 | * */
22 |
23 |
24 | const ChromeOptions = require('selenium-webdriver/chrome').Options;
25 | const Driver = require('../clients/js/chrome_driver_proxy');
26 |
27 | const chromeOptions = new ChromeOptions();
28 | chromeOptions.addArguments(
29 | // '--headless',
30 | // '--disable-gpu',
31 | '--disable-xss-auditor',
32 | '--no-first-run',
33 | '--no-sandbox',
34 | );
35 | const options = chromeOptions.toCapabilities();
36 |
37 | const driver = Driver.createSession('http://127.0.0.1:4444/wd/hub', options);
38 | const myScript = 'console.log("inject script");'
39 |
40 | driver.addScript(myScript).then((result) => {
41 | driver.get('https://google.com');
42 | }).then(() => driver.sleep(10000))
43 | .then(() => {
44 | return driver.quit();
45 | }).catch(err => console.log(err) );
46 |
--------------------------------------------------------------------------------
/test/subtitle_test.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) 2020 ZipRecruiter
3 | *
4 | * Permission is hereby granted, free of charge, to any person obtaining a copy
5 | * of this software and associated documentation files (the "Software"), to deal
6 | * in the Software without restriction, including without limitation the rights
7 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8 | * copies of the Software, and to permit persons to whom the Software is
9 | * furnished to do so, subject to the following conditions:
10 | *
11 | * The above copyright notice and this permission notice shall be included in all
12 | * copies or substantial portions of the Software.
13 | *
14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
20 | * SOFTWARE.
21 | * */
22 |
23 |
24 | const chai = require('chai');
25 |
26 | const expect = chai.expect;
27 | const fs = require('fs');
28 |
29 | const ScreenRecorder = require('..').ScreenRecorder;
30 |
31 | describe('Create subtitle blob', () => {
32 |
33 | it('can create subtitles', (done) => {
34 | const recorder = new ScreenRecorder({});
35 |
36 | recorder.firstFrameTimestamp = 100;
37 | recorder.currentDuration = 0;
38 | recorder.addSubtitle({text: 'first subtitle'});
39 | recorder.currentDuration = 0;
40 | recorder.addSubtitle({text: 'first subtitle'});
41 | recorder.currentDuration = 102;
42 | recorder.addSubtitle({text: 'second subtitle'});
43 | recorder.currentDuration = 105;
44 |
45 | const subtitles = recorder.createSubtitle();
46 |
47 | const expected = 'WEBVTT\n\n00:00.000 --> 01:42.000\nfirst subtitle\n\n01:42.000 --> 02:15.000\nsecond subtitle\n\n';
48 |
49 | expect(subtitles.blob).to.be.equal(expected);
50 |
51 | done();
52 |
53 | });
54 | });
55 |
--------------------------------------------------------------------------------
/lib/chrome_agent_slave.js:
--------------------------------------------------------------------------------
1 |
2 |
3 | /**
4 | * Copyright (c) 2017 ZipRecruiter
5 | *
6 | * Permission is hereby granted, free of charge, to any person obtaining a copy
7 | * of this software and associated documentation files (the "Software"), to deal
8 | * in the Software without restriction, including without limitation the rights
9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | * copies of the Software, and to permit persons to whom the Software is
11 | * furnished to do so, subject to the following conditions:
12 | *
13 | * The above copyright notice and this permission notice shall be included in all
14 | * copies or substantial portions of the Software.
15 | *
16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 | * SOFTWARE.
23 | * */
24 |
25 | const process = require('process');
26 | const debug = require('debug')('chromedriver_proxy:chrome_agent_slave');
27 | const path = require('path');
28 |
29 | const options = JSON.parse(process.argv[2]);
30 |
31 | if (require.main === module) {
32 | debug('successfully forked chrome agent');
33 | try {
34 | const ChromeAgent = require(path.resolve(options.chromeAgentModule));
35 | const chromeAgent = new ChromeAgent(options);
36 | process.on('message', (msg) => {
37 | const blob = JSON.parse(msg);
38 | chromeAgent.handle(blob).then((result) => {
39 | process.send(JSON.stringify({ action: 'req', result }));
40 | }).catch((err) => {
41 | process.send(JSON.stringify({ action: 'req', result: { error: err.stack } }));
42 | });
43 | });
44 | process.send(JSON.stringify({ action: 'init', result: {} }));
45 | } catch (err) {
46 | debug(err);
47 | process.send(JSON.stringify({ action: 'error', result: { error: err } }));
48 | process.send(JSON.stringify({ action: 'init', result: { error: err } }));
49 | process.exit(1);
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/examples/screen_recorder.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 |
3 | # Copyright (c) 2017 ZipRecruiter
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 all
13 | # 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 THE
21 | # SOFTWARE.
22 |
23 | from __future__ import absolute_import
24 | from __future__ import print_function
25 | from selenium import webdriver as wd
26 | import sys
27 | import os
28 | sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '../clients/py')))
29 | import time
30 | from chrome_driver_proxy import ChromeDriverProxy
31 |
32 | chrome_options = wd.ChromeOptions()
33 | chrome_options.add_argument('--headless')
34 | chrome_options.add_argument('--disable-gpu')
35 | chrome_options.add_argument('--no-sandbox')
36 | chrome_options.add_argument('--no-first-run')
37 | chrome_options.add_argument('--window-size=1680,1050')
38 |
39 | capabilities = wd.DesiredCapabilities.CHROME.copy()
40 | capabilities.update(chrome_options.to_capabilities())
41 |
42 | driver = ChromeDriverProxy(
43 | command_executor='http://127.0.0.1:4444/wd/hub',
44 | desired_capabilities=capabilities,
45 | keep_alive=True)
46 |
47 | driver.start_screencast(params=dict(format='jpeg', quality=80, everyNthFrame=2))
48 | driver.get('https://www.ziprecruiter.com')
49 | time.sleep(2)
50 | driver.get('https://www.ziprecruiter.com/candidate/search?search=accountant&location=')
51 | time.sleep(1)
52 | driver.stop_screencast()
53 | path = driver.get_screencast_path()
54 |
55 | print(path)
56 | driver.quit()
57 |
--------------------------------------------------------------------------------
/fixtures/server.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) 2017 ZipRecruiter
3 | *
4 | * Permission is hereby granted, free of charge, to any person obtaining a copy
5 | * of this software and associated documentation files (the "Software"), to deal
6 | * in the Software without restriction, including without limitation the rights
7 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8 | * copies of the Software, and to permit persons to whom the Software is
9 | * furnished to do so, subject to the following conditions:
10 | *
11 | * The above copyright notice and this permission notice shall be included in all
12 | * copies or substantial portions of the Software.
13 | *
14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
20 | * SOFTWARE.
21 | **/
22 |
23 | 'use strict'
24 |
25 | const http = require('http')
26 | const Router = require('node-simple-router')
27 | const url = require('url')
28 | const fs = require('fs')
29 |
30 | function parseCookies (request) {
31 | // https://stackoverflow.com/a/3409200
32 | const list = {},
33 | rc = request.headers.cookie;
34 |
35 | rc && rc.split(';').forEach(function( cookie ) {
36 | const parts = cookie.split('=');
37 | list[parts.shift().trim()] = decodeURI(parts.join('='));
38 | });
39 |
40 | return list;
41 | }
42 |
43 | module.exports = function (port, callback) {
44 | const router = new Router({static_route: __dirname + '/static'})
45 |
46 | router.get('/cookies', function(req, res) {
47 | const cookies = parseCookies(req)
48 | let content = []
49 | for (let name in cookies) {
50 | content.push(`${name}${cookies[name]}
`)
51 | }
52 | res.write(content.join(''))
53 | res.end()
54 | })
55 | router.get('/headers', function(req, res) {
56 | const headers = req.headers
57 | let content = []
58 | for (let name in headers) {
59 | content.push(`${name}: ${headers[name]}
`)
60 | }
61 | res.write(content.join(''))
62 | res.end()
63 | })
64 | const server = http.createServer(router)
65 | server.listen(port, callback)
66 | return server
67 | }
68 |
--------------------------------------------------------------------------------
/test/custom_agent_module_test.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) 2017 ZipRecruiter
3 | *
4 | * Permission is hereby granted, free of charge, to any person obtaining a copy
5 | * of this software and associated documentation files (the "Software"), to deal
6 | * in the Software without restriction, including without limitation the rights
7 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8 | * copies of the Software, and to permit persons to whom the Software is
9 | * furnished to do so, subject to the following conditions:
10 | *
11 | * The above copyright notice and this permission notice shall be included in all
12 | * copies or substantial portions of the Software.
13 | *
14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
20 | * SOFTWARE.
21 | * */
22 |
23 |
24 | const chai = require('chai');
25 |
26 | const expect = chai.expect;
27 | const HttpServer = require('..').HttpServer;
28 |
29 | const Driver = require('../clients/js/chrome_driver_proxy');
30 | const Chrome = require('selenium-webdriver/chrome').Driver;
31 | const ChromeOptions = require('selenium-webdriver/chrome').Options;
32 |
33 | const chromedriverBin = process.env.CHROMEDRIVER_BIN || '/usr/bin/chromedriver';
34 |
35 | describe('Proxy with chrome pool enabled', () => {
36 | let server;
37 | let driver;
38 | const mockServerUrl = 'http://127.0.0.1:8080';
39 |
40 | before(function (done) {
41 | const config = {
42 | proxy: {
43 | port: 4444,
44 | baseUrl: '/wd/hub',
45 | },
46 | chromedriver: {
47 | chromedriverPath: chromedriverBin,
48 | port: 4445,
49 | autoRestart: false,
50 | },
51 | chromePool: {
52 | enable: true,
53 | reuse: true,
54 | chromePath: '/usr/bin/google-chrome',
55 | chromeAgentModule: 'test/custom_agent_module.js',
56 | },
57 | };
58 | server = new HttpServer(config.proxy);
59 | this.timeout(5000);
60 | server.start(config, done);
61 | });
62 |
63 | after((done) => {
64 | server.stop(done);
65 | });
66 |
67 | beforeEach(() => {
68 | const chromeOptions = new ChromeOptions();
69 | chromeOptions.addArguments(
70 | 'headless',
71 | 'disable-gpu',
72 | 'no-first-run',
73 | 'no-sandbox',
74 | );
75 |
76 | driver = Driver.createSession('http://127.0.0.1:4444/wd/hub', chromeOptions);
77 | });
78 |
79 | afterEach((done) => {
80 | driver.quit().then(() => { done(); });
81 | });
82 |
83 | it('can run basic selenium test on custom agent', (done) => {
84 | driver.get(`${mockServerUrl}/base.html`).then(() => { done(); });
85 | });
86 | });
87 |
--------------------------------------------------------------------------------
/lib/chrome_agent_manager.js:
--------------------------------------------------------------------------------
1 |
2 | /**
3 | * Copyright (c) 2017 ZipRecruiter
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 all
13 | * 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 THE
21 | * SOFTWARE.
22 | * */
23 |
24 | const childProcess = require('child_process');
25 | const path = require('path');
26 | const debug = require('debug')('chromedriver_proxy:chrome_agent_manager');
27 | const EventEmitter = require('events');
28 |
29 | class ChromeAgentManager extends EventEmitter {
30 | start(options) {
31 | const self = this;
32 | const JsonOptions = JSON.stringify(options);
33 | debug(`fork agent with options: ${JsonOptions}`);
34 | self.child = childProcess.fork(`${__dirname}${path.sep}chrome_agent_slave.js`, [JsonOptions]);
35 | self.child.on('error', () => {
36 | debug('chrome agent has exited');
37 | });
38 | self.child.on('exit', (status) => {
39 | debug(`chrome agent has exit code: ${status}`);
40 | self.emit('exit', status);
41 | });
42 | self.child.on('disconnect', () => {
43 | debug('chrome agent has disconnected');
44 | });
45 |
46 | self.child.on('message', (msg) => {
47 | const blob = JSON.parse(msg);
48 | self.emit(blob.action, blob.result);
49 | });
50 | }
51 |
52 | send(options) {
53 | const self = this;
54 | return new Promise((resolve, reject) => {
55 | const exitListener = function exitListener() {
56 | reject();
57 | };
58 | self.on('exit', exitListener);
59 | self.once(options.action, (result) => {
60 | self.removeListener('exit', exitListener);
61 | if (typeof result === 'undefined') {
62 | reject(new Error('Unknown error'));
63 | }
64 | if ('error' in result) {
65 | reject(new Error(result.error));
66 | } else {
67 | resolve(result);
68 | }
69 | });
70 | self.child.send(JSON.stringify(options));
71 | });
72 | }
73 |
74 | stop() {
75 | const self = this;
76 | return self.send({ action: 'stop' }).catch(() => {});
77 | }
78 | }
79 |
80 | module.exports = ChromeAgentManager;
81 |
--------------------------------------------------------------------------------
/test/base-chrome-pool-proxy-test.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) 2017 ZipRecruiter
3 | *
4 | * Permission is hereby granted, free of charge, to any person obtaining a copy
5 | * of this software and associated documentation files (the "Software"), to deal
6 | * in the Software without restriction, including without limitation the rights
7 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8 | * copies of the Software, and to permit persons to whom the Software is
9 | * furnished to do so, subject to the following conditions:
10 | *
11 | * The above copyright notice and this permission notice shall be included in all
12 | * copies or substantial portions of the Software.
13 | *
14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
20 | * SOFTWARE.
21 | * */
22 |
23 |
24 | const chai = require('chai');
25 |
26 | const expect = chai.expect;
27 | const HttpServer = require('..').HttpServer;
28 |
29 | const Driver = require('../clients/js/chrome_driver_proxy');
30 | const Chrome = require('selenium-webdriver/chrome').Driver;
31 | const ChromeOptions = require('selenium-webdriver/chrome').Options;
32 |
33 | const chromedriverBin = process.env.CHROMEDRIVER_BIN || '/usr/bin/chromedriver';
34 |
35 | describe('Proxy with chrome pool enabled', () => {
36 | let server;
37 | let driver;
38 | const mockServerUrl = 'http://127.0.0.1:8080';
39 |
40 | before(function (done) {
41 | const config = {
42 | proxy: {
43 | port: 4444,
44 | baseUrl: '/wd/hub',
45 | },
46 | chromedriver: {
47 | chromedriverPath: chromedriverBin,
48 | port: 4445,
49 | autoRestart: false,
50 | },
51 | chromePool: {
52 | enable: true,
53 | reuse: true,
54 | chromePath: '/usr/bin/google-chrome',
55 | },
56 | };
57 | server = new HttpServer(config.proxy);
58 | this.timeout(5000);
59 | server.start(config, done);
60 | });
61 |
62 | after((done) => {
63 | server.stop(done);
64 | });
65 |
66 | beforeEach(() => {
67 | const chromeOptions = new ChromeOptions();
68 | chromeOptions.addArguments(
69 | 'headless',
70 | 'disable-gpu',
71 | 'no-first-run',
72 | 'no-sandbox',
73 | );
74 |
75 | driver = Driver.createSession('http://127.0.0.1:4444/wd/hub', chromeOptions);
76 | });
77 |
78 | afterEach((done) => {
79 | driver.quit().then(() => { done(); });
80 | });
81 |
82 | it('can run basic selenium test', (done) => {
83 | driver.get(`${mockServerUrl}/base.html`).then(() => { done(); });
84 | });
85 |
86 | it('can run another selenium test', (done) => {
87 | driver.get(`${mockServerUrl}/base.html`).then(() => { done(); });
88 | });
89 | });
90 |
--------------------------------------------------------------------------------
/test/base-chrome-pool-no-reuse-test.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) 2017 ZipRecruiter
3 | *
4 | * Permission is hereby granted, free of charge, to any person obtaining a copy
5 | * of this software and associated documentation files (the "Software"), to deal
6 | * in the Software without restriction, including without limitation the rights
7 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8 | * copies of the Software, and to permit persons to whom the Software is
9 | * furnished to do so, subject to the following conditions:
10 | *
11 | * The above copyright notice and this permission notice shall be included in all
12 | * copies or substantial portions of the Software.
13 | *
14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
20 | * SOFTWARE.
21 | * */
22 |
23 |
24 | const chai = require('chai');
25 |
26 | const expect = chai.expect;
27 | const HttpServer = require('..').HttpServer;
28 |
29 | const Driver = require('../clients/js/chrome_driver_proxy');
30 | const Chrome = require('selenium-webdriver/chrome').Driver;
31 | const ChromeOptions = require('selenium-webdriver/chrome').Options;
32 |
33 | const chromedriverBin = process.env.CHROMEDRIVER_BIN || '/usr/bin/chromedriver';
34 |
35 | describe('Proxy with chrome pool enabled no reuse', () => {
36 | let server;
37 | let driver;
38 | const mockServerUrl = 'http://127.0.0.1:8080';
39 |
40 | before(function (done) {
41 | const config = {
42 | proxy: {
43 | port: 4444,
44 | baseUrl: '/wd/hub',
45 | },
46 | chromedriver: {
47 | chromedriverPath: chromedriverBin,
48 | port: 4445,
49 | autoRestart: false,
50 | },
51 | chromePool: {
52 | enable: true,
53 | reuse: false,
54 | chromePath: '/usr/bin/google-chrome',
55 | },
56 | };
57 | server = new HttpServer(config.proxy);
58 | this.timeout(5000);
59 | server.start(config, done);
60 | });
61 |
62 | after((done) => {
63 | server.stop(done);
64 | });
65 |
66 | beforeEach(() => {
67 | const chromeOptions = new ChromeOptions();
68 | chromeOptions.addArguments(
69 | 'headless',
70 | 'disable-gpu',
71 | 'no-first-run',
72 | 'no-sandbox',
73 | );
74 |
75 | driver = Driver.createSession('http://127.0.0.1:4444/wd/hub', chromeOptions);
76 | });
77 |
78 | afterEach((done) => {
79 | driver.quit().then(() => { done(); });
80 | });
81 |
82 | it('can run basic selenium test', (done) => {
83 | driver.get(`${mockServerUrl}/base.html`).then(() => { done(); });
84 | });
85 |
86 | it('can run another selenium test', (done) => {
87 | driver.get(`${mockServerUrl}/base.html`).then(() => { done(); });
88 | });
89 | });
90 |
--------------------------------------------------------------------------------
/test/base-proxy-test.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) 2017 ZipRecruiter
3 | *
4 | * Permission is hereby granted, free of charge, to any person obtaining a copy
5 | * of this software and associated documentation files (the "Software"), to deal
6 | * in the Software without restriction, including without limitation the rights
7 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8 | * copies of the Software, and to permit persons to whom the Software is
9 | * furnished to do so, subject to the following conditions:
10 | *
11 | * The above copyright notice and this permission notice shall be included in all
12 | * copies or substantial portions of the Software.
13 | *
14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
20 | * SOFTWARE.
21 | * */
22 |
23 |
24 | const chai = require('chai');
25 |
26 | const expect = chai.expect;
27 | const HttpServer = require('..').HttpServer;
28 |
29 | const Driver = require('../clients/js/chrome_driver_proxy');
30 | const Chrome = require('selenium-webdriver/chrome').Driver;
31 | const ChromeOptions = require('selenium-webdriver/chrome').Options;
32 |
33 | const chromedriverBin = process.env.CHROMEDRIVER_BIN || '/usr/bin/chromedriver';
34 |
35 | describe('Proxy with chrome pool disabled', () => {
36 | let server;
37 | let driver;
38 | const mockServerUrl = 'http://127.0.0.1:8080';
39 |
40 | before(function (done) {
41 | const config = {
42 | timeout: 2000,
43 | proxy: {
44 | port: 4444,
45 | baseUrl: '/wd/hub',
46 | },
47 | chromedriver: {
48 | chromedriverPath: chromedriverBin,
49 | port: 4445,
50 | autoRestart: false,
51 | },
52 | chromePool: {
53 | chromePath: '/usr/bin/google-chrome',
54 | enable: false,
55 | },
56 | };
57 | server = new HttpServer(config.proxy);
58 | this.timeout(5000);
59 | server.start(config, done);
60 | });
61 |
62 | after((done) => {
63 | server.stop(done);
64 | });
65 |
66 | beforeEach(() => {
67 | const chromeOptions = new ChromeOptions();
68 | chromeOptions.addArguments(
69 | 'headless',
70 | 'disable-gpu',
71 | 'no-first-run',
72 | 'no-sandbox',
73 | );
74 | chromeOptions.setChromeBinaryPath('/usr/bin/google-chrome');
75 |
76 | driver = Driver.createSession('http://127.0.0.1:4444/wd/hub', chromeOptions);
77 | });
78 |
79 | afterEach((done) => {
80 | driver.quit().then(() => { done(); });
81 | });
82 |
83 | it('can run basic selenium test', (done) => {
84 | driver.get(`${mockServerUrl}/base.html`).then(() => { done(); });
85 | });
86 |
87 | it('can run another selenium test', (done) => {
88 | driver.get(`${mockServerUrl}/base.html`).then(() => { done(); });
89 | });
90 | });
91 |
--------------------------------------------------------------------------------
/test/chrome-pool-navigate-test.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) 2017 ZipRecruiter
3 | *
4 | * Permission is hereby granted, free of charge, to any person obtaining a copy
5 | * of this software and associated documentation files (the "Software"), to deal
6 | * in the Software without restriction, including without limitation the rights
7 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8 | * copies of the Software, and to permit persons to whom the Software is
9 | * furnished to do so, subject to the following conditions:
10 | *
11 | * The above copyright notice and this permission notice shall be included in all
12 | * copies or substantial portions of the Software.
13 | *
14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
20 | * SOFTWARE.
21 | * */
22 |
23 |
24 | const chai = require('chai');
25 |
26 | const expect = chai.expect;
27 | const HttpServer = require('..').HttpServer;
28 |
29 | const Driver = require('../clients/js/chrome_driver_proxy');
30 | const Chrome = require('selenium-webdriver/chrome').Driver;
31 | const ChromeOptions = require('selenium-webdriver/chrome').Options;
32 |
33 | const chromedriverBin = process.env.CHROMEDRIVER_BIN || '/usr/bin/chromedriver';
34 |
35 | // need chrome 64+ see https://bugs.chromium.org/p/chromium/issues/detail?id=767683
36 | describe.skip('Proxy with navigate', () => {
37 | let server;
38 | let driver;
39 | const mockServerUrl = 'http://127.0.0.1:8080';
40 |
41 | before(function (done) {
42 | const config = {
43 | proxy: {
44 | port: 4444,
45 | baseUrl: '/wd/hub',
46 | },
47 | chromedriver: {
48 | chromedriverPath: chromedriverBin,
49 | port: 4445,
50 | autoRestart: false,
51 | },
52 | chromePool: {
53 | enable: true,
54 | reuse: true,
55 | chromePath: '/usr/bin/google-chrome',
56 | },
57 | };
58 | server = new HttpServer(config.proxy);
59 | this.timeout(5000);
60 | server.start(config, done);
61 | });
62 |
63 | after((done) => {
64 | server.stop(done);
65 | });
66 |
67 | beforeEach(() => {
68 | const chromeOptions = new ChromeOptions();
69 | chromeOptions.addArguments(
70 | 'headless',
71 | 'disable-gpu',
72 | 'no-first-run',
73 | 'no-sandbox',
74 | );
75 |
76 | driver = Driver.createSession('http://127.0.0.1:4444/wd/hub', chromeOptions);
77 | });
78 |
79 | afterEach((done) => {
80 | driver.quit().then(() => { done(); });
81 | });
82 |
83 | it('can navigate with custom referrer', (done) => {
84 | const referrer = 'https://google.com';
85 | driver.pageNavigate({url: `${mockServerUrl}/headers`, referrer: referrer}).then(() => {
86 | return driver.findElement({ css: '#referrer span.value' });
87 | })
88 | .then(elem => elem.getText())
89 | .then((value) => {
90 | expect(value).to.equal(referrer);
91 | })
92 | .then(() => {
93 | done();
94 | })
95 | .catch((err) => {
96 | done(err);
97 | });
98 | });
99 | });
100 |
--------------------------------------------------------------------------------
/test/chrome-pool-screen-recording-test.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) 2017 ZipRecruiter
3 | *
4 | * Permission is hereby granted, free of charge, to any person obtaining a copy
5 | * of this software and associated documentation files (the "Software"), to deal
6 | * in the Software without restriction, including without limitation the rights
7 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8 | * copies of the Software, and to permit persons to whom the Software is
9 | * furnished to do so, subject to the following conditions:
10 | *
11 | * The above copyright notice and this permission notice shall be included in all
12 | * copies or substantial portions of the Software.
13 | *
14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
20 | * SOFTWARE.
21 | * */
22 |
23 |
24 | const chai = require('chai');
25 |
26 | const expect = chai.expect;
27 | const HttpServer = require('..').HttpServer;
28 | const fs = require('fs');
29 |
30 | const Driver = require('../clients/js/chrome_driver_proxy');
31 | const Chrome = require('selenium-webdriver/chrome').Driver;
32 | const ChromeOptions = require('selenium-webdriver/chrome').Options;
33 |
34 | const chromedriverBin = process.env.CHROMEDRIVER_BIN || '/usr/bin/chromedriver';
35 |
36 | describe('Proxy with screen recording', () => {
37 | let server;
38 | let driver;
39 | const mockServerUrl = 'http://127.0.0.1:8080';
40 |
41 | before((done) => {
42 | const config = {
43 | proxy: {
44 | port: 4444,
45 | baseUrl: '/wd/hub',
46 | },
47 | chromedriver: {
48 | chromedriverPath: chromedriverBin,
49 | port: 4445,
50 | autoRestart: false,
51 | },
52 | chromePool: {
53 | enable: true,
54 | reuse: true,
55 | chromePath: '/usr/bin/google-chrome',
56 | chromeAgent: {
57 | screenRecorder: {
58 | videoFormat: 'mp4',
59 | },
60 | },
61 | },
62 | };
63 | server = new HttpServer(config.proxy);
64 | server.start(config, done);
65 | });
66 |
67 | after((done) => {
68 | server.stop(done);
69 | });
70 |
71 | beforeEach(() => {
72 | const chromeOptions = new ChromeOptions();
73 | chromeOptions.addArguments(
74 | 'headless',
75 | 'disable-gpu',
76 | 'no-first-run',
77 | 'no-sandbox',
78 | );
79 |
80 | driver = Driver.createSession('http://127.0.0.1:4444/wd/hub', chromeOptions);
81 | });
82 |
83 | afterEach((done) => {
84 | driver.quit().then(() => { done(); });
85 | });
86 |
87 | it('can record video', (done) => {
88 | driver.startScreencast({ params: { format: 'jpeg', quality: 80, everyNthFrame: 1 } }).then(() => driver.get(`${mockServerUrl}/base.html`)).then(() => driver.get(`${mockServerUrl}/cookies`)).then(() => driver.stopScreencast())
89 | .then(result => driver.getScreencastPath())
90 | .then((result) => {
91 | console.log(result);
92 | expect(result).to.have.property('path');
93 | expect(fs.existsSync(result['path'])).to.be.true;
94 | done();
95 | })
96 | .catch((err) => {
97 | done(err);
98 | });
99 | });
100 | });
101 |
--------------------------------------------------------------------------------
/clients/py/chrome_driver_proxy.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 |
3 | # Copyright (c) 2017 ZipRecruiter
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 all
13 | # 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 THE
21 | # SOFTWARE.
22 |
23 | from __future__ import absolute_import
24 | import selenium.webdriver as wd
25 | from selenium.webdriver.remote.errorhandler import ErrorHandler
26 | from selenium.webdriver.remote.remote_connection import RemoteConnection
27 |
28 | class ChromeDriverProxyRemoteConnection(RemoteConnection):
29 |
30 | def __init__(self, remote_server_addr, keep_alive=True):
31 | RemoteConnection.__init__(self, remote_server_addr, keep_alive)
32 | self._commands["startScreencast"] = ('POST', '/session/$sessionId/chromedriver-proxy/screencast/start')
33 | self._commands["stopScreencast"] = ('POST', '/session/$sessionId/chromedriver-proxy/screencast/stop')
34 | self._commands["getScreencastPath"] = ('GET', '/session/$sessionId/chromedriver-proxy/screencast/path')
35 | self._commands["getScreencastS3"] = ('GET', '/session/$sessionId/chromedriver-proxy/screencast/s3')
36 | self._commands["setHeaders"] = ('POST', '/session/$sessionId/chromedriver-proxy/headers')
37 | self._commands["addScript"] = ('POST', '/session/$sessionId/chromedriver-proxy/script')
38 | self._commands["removeAllScripts"] = ('DELETE', '/session/$sessionId/chromedriver-proxy/scripts')
39 | self._commands["setClearStorage"] = ('POST', '/session/$sessionId/chromedriver-proxy/storage')
40 | self._commands["navigate"] = ('POST', '/session/$sessionId/chromedriver-proxy/navigate')
41 |
42 |
43 | class ChromeDriverProxy(wd.Remote):
44 |
45 | def __init__(self, *args, **kwargs):
46 | kwargs['command_executor'] = ChromeDriverProxyRemoteConnection(kwargs['command_executor'], keep_alive=kwargs['keep_alive'])
47 | super(self.__class__, self).__init__(*args, **kwargs)
48 | self.error_handler = ErrorHandler()
49 |
50 | def start_screencast(self, **kwargs):
51 | self.execute('startScreencast', kwargs)
52 |
53 | def stop_screencast(self, **kwargs):
54 | result = self.execute('stopScreencast', kwargs)
55 | return result['value']
56 |
57 | def get_screencast_path(self):
58 | result = self.execute('getScreencastPath')
59 | return result['value']['path']
60 |
61 | def get_screencast_s3(self):
62 | result = self.execute('getScreencastS3')
63 | return result['value']
64 |
65 | def set_extra_headers(self, headers):
66 | self.execute('setHeaders', dict(headers=headers))
67 |
68 | def add_script(self, script):
69 | self.execute('addScript', dict(scriptSource=script))
70 |
71 | def remove_all_scripts(self):
72 | self.execute('removeAllScripts')
73 |
74 | def setClearStorage(self, options):
75 | self.execute('setClearStorage', dict(values=options))
76 |
77 | def navigate(self, options):
78 | self.execute('navigate', dict(values=options))
79 |
--------------------------------------------------------------------------------
/test/chrome-pool-extra-headers-test.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) 2017 ZipRecruiter
3 | *
4 | * Permission is hereby granted, free of charge, to any person obtaining a copy
5 | * of this software and associated documentation files (the "Software"), to deal
6 | * in the Software without restriction, including without limitation the rights
7 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8 | * copies of the Software, and to permit persons to whom the Software is
9 | * furnished to do so, subject to the following conditions:
10 | *
11 | * The above copyright notice and this permission notice shall be included in all
12 | * copies or substantial portions of the Software.
13 | *
14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
20 | * SOFTWARE.
21 | * */
22 |
23 |
24 | const chai = require('chai');
25 |
26 | const expect = chai.expect;
27 | const HttpServer = require('..').HttpServer;
28 |
29 | const Driver = require('../clients/js/chrome_driver_proxy');
30 | const Chrome = require('selenium-webdriver/chrome').Driver;
31 | const ChromeOptions = require('selenium-webdriver/chrome').Options;
32 |
33 | const chromedriverBin = process.env.CHROMEDRIVER_BIN || '/usr/bin/chromedriver';
34 |
35 | describe('Proxy with extra headers', () => {
36 | let server;
37 | let driver;
38 | const mockServerUrl = 'http://127.0.0.1:8080';
39 |
40 | before(function (done) {
41 | const config = {
42 | proxy: {
43 | port: 4444,
44 | baseUrl: '/wd/hub',
45 | },
46 | chromedriver: {
47 | chromedriverPath: chromedriverBin,
48 | port: 4445,
49 | autoRestart: false,
50 | },
51 | chromePool: {
52 | enable: true,
53 | reuse: true,
54 | chromePath: '/usr/bin/google-chrome',
55 | },
56 | };
57 | server = new HttpServer(config.proxy);
58 | this.timeout(5000);
59 | server.start(config, done);
60 | });
61 |
62 | after((done) => {
63 | server.stop(done);
64 | });
65 |
66 | beforeEach(() => {
67 | const chromeOptions = new ChromeOptions();
68 | chromeOptions.addArguments(
69 | 'headless',
70 | 'disable-gpu',
71 | 'no-first-run',
72 | 'no-sandbox',
73 | );
74 |
75 | driver = Driver.createSession('http://127.0.0.1:4444/wd/hub', chromeOptions);
76 | });
77 |
78 | afterEach((done) => {
79 | driver.quit().then(() => { done(); });
80 | });
81 |
82 | // need chrome 64+ see https://bugs.chromium.org/p/chromium/issues/detail?id=767683
83 | it('can add extra headers', (done) => {
84 | driver.get(`${mockServerUrl}/base.html`).then(() => { return driver.setExtraHeaders({ Foo: 'bar' }); }).then(() => driver.get(`${mockServerUrl}/headers`)).then(() => driver.getPageSource())
85 | .then((source) => {
86 | console.log(source);
87 | return driver.findElement({ css: '#foo span.value' });
88 | })
89 | .then(elem => elem.getText())
90 | .then((value) => {
91 | expect(value).to.equal('bar');
92 | })
93 | .then(() => {
94 | done();
95 | })
96 | .catch((err) => {
97 | done(err);
98 | });
99 | });
100 | it('can set custom user agent', (done) => {
101 | driver.get(`${mockServerUrl}/base.html`).then(() => driver.setUserAgent('bandit')).then(() => driver.get(`${mockServerUrl}/headers`)).then(() => driver.getPageSource())
102 | .then((source) => {
103 | console.log(source);
104 | return driver.findElement({ css: '#user-agent span.value' });
105 | })
106 | .then(elem => elem.getText())
107 | .then((value) => {
108 | expect(value).to.equal('bandit');
109 | })
110 | .then(() => {
111 | done();
112 | })
113 | .catch((err) => {
114 | done(err);
115 | });
116 | });
117 | });
118 |
--------------------------------------------------------------------------------
/test/chrome-pool-script-test.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) 2017 ZipRecruiter
3 | *
4 | * Permission is hereby granted, free of charge, to any person obtaining a copy
5 | * of this software and associated documentation files (the "Software"), to deal
6 | * in the Software without restriction, including without limitation the rights
7 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8 | * copies of the Software, and to permit persons to whom the Software is
9 | * furnished to do so, subject to the following conditions:
10 | *
11 | * The above copyright notice and this permission notice shall be included in all
12 | * copies or substantial portions of the Software.
13 | *
14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
20 | * SOFTWARE.
21 | * */
22 |
23 |
24 | const chai = require('chai');
25 |
26 | const expect = chai.expect;
27 | const HttpServer = require('..').HttpServer;
28 |
29 | const Driver = require('../clients/js/chrome_driver_proxy');
30 | const Chrome = require('selenium-webdriver/chrome').Driver;
31 | const ChromeOptions = require('selenium-webdriver/chrome').Options;
32 |
33 | const chromedriverBin = process.env.CHROMEDRIVER_BIN || '/usr/bin/chromedriver';
34 |
35 | describe('Proxy with script support', () => {
36 | let server;
37 | let driver;
38 | const mockServerUrl = 'http://127.0.0.1:8080';
39 |
40 | before(function (done) {
41 | const config = {
42 | proxy: {
43 | port: 4444,
44 | baseUrl: '/wd/hub',
45 | },
46 | chromedriver: {
47 | chromedriverPath: chromedriverBin,
48 | port: 4445,
49 | autoRestart: false,
50 | },
51 | chromePool: {
52 | enable: true,
53 | reuse: true,
54 | chromePath: '/usr/bin/google-chrome',
55 | },
56 | };
57 | server = new HttpServer(config.proxy);
58 | this.timeout(5000);
59 | server.start(config, done);
60 | });
61 |
62 | after((done) => {
63 | server.stop(done);
64 | });
65 |
66 | beforeEach(() => {
67 | const chromeOptions = new ChromeOptions();
68 | chromeOptions.addArguments(
69 | 'headless',
70 | 'disable-gpu',
71 | 'no-first-run',
72 | 'no-sandbox',
73 | );
74 |
75 | driver = Driver.createSession('http://127.0.0.1:4444/wd/hub', chromeOptions);
76 | });
77 |
78 | afterEach((done) => {
79 | driver.quit().then(() => { done(); });
80 | });
81 |
82 | it('can add a script', (done) => {
83 | driver.addScript('window.FOO="BAR"').then(() => driver.get(`${mockServerUrl}/base.html`)).then(() => {
84 | return driver.executeScript('return window.FOO');
85 | })
86 | .then((result) => {
87 | expect(result).to.equal('BAR');
88 | })
89 | .then(() => {
90 | done();
91 | })
92 | .catch((err) => {
93 | done(err);
94 | });
95 | });
96 |
97 | it('can add a script on navigation', (done) => {
98 | driver.addScript('window.FOO="BAR"').then(() => driver.get(`${mockServerUrl}/base.html`)).then(() => {
99 | return driver.findElement({css: 'a[href*="base2.html"]'});
100 | })
101 | .then((elem) => {
102 | return elem.click()
103 | }).then(() => {
104 | return driver.getTitle()
105 | }).then((title) => {
106 | expect(title).to.equal('base 2 page');
107 | }).then(() => {
108 | return driver.executeScript('return window.FOO');
109 | })
110 | .then((result) => {
111 | expect(result).to.equal('BAR');
112 | })
113 | .then(() => {
114 | done();
115 | })
116 | .catch((err) => {
117 | done(err);
118 | });
119 | });
120 |
121 |
122 | xit('can remove all scripts', (done) => {
123 | driver.addScript('window.FOO="BAR"').then((result) => {
124 | return driver.get(`${mockServerUrl}/base.html`)
125 | }).then((result) => {
126 | return driver.removeAllScripts()
127 | })
128 | .then((result) => {
129 | return driver.get(`${mockServerUrl}/base2.html`)
130 | }).then(() => {
131 | return driver.executeScript('return window.FOO');
132 | })
133 | .then((result) => {
134 | expect(result).to.not.equal('BAR');
135 | }).then(() => {
136 | done();
137 | })
138 | .catch((err) => {
139 | done(err);
140 | });
141 | });
142 | });
143 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | [](https://www.npmjs.com/package/chromedriver-proxy)
2 |
3 | Chromedriver-Proxy is an extensible proxy to ChromeDriver.
4 |
5 | # Features
6 |
7 | * Reuse browsers. The browser will be cleaned between each selenium session.
8 | * Connect to Chromedriver from remote host without modifying the whitelist
9 | * Record video and upload the video to s3. Compatible with chrome in headless mode.
10 | * Support mp4, webm, and m3u8 video format
11 | * Set extra headers
12 | * Evaluate script on each page load
13 | * Provide your own custom extensions.
14 |
15 | # Requirements
16 |
17 | nodejs >= 8, ffmpeg, chrome >= 64 and the current version of [chromedriver](https://sites.google.com/a/chromium.org/chromedriver/downloads).
18 |
19 | # Usage
20 |
21 | ```
22 | DEBUG=chromedriver_proxy:* chromedriver-proxy --config config.json
23 | ```
24 |
25 | example configuration:
26 | ```json
27 | {
28 | "tmpDir": "/tmp",
29 | "proxy": {
30 | "port": 4444,
31 | "baseUrl": "/wd/hub"
32 | },
33 | "chromedriver": {
34 | "chromedriverPath": "/usr/bin/chromedriver",
35 | "port": 4445,
36 | "autoRestart": true // restart chromedriver if it crashes
37 | },
38 | "chromePool": {
39 | "enable": true,
40 | "chromePath": "/usr/bin/google-chrome",
41 | "reuse": true, // reuse the browser instances
42 | "chromeStartupTimeOut": 1000, // time to wait for chrome to startup
43 | // chromeAgentModule should extend the builtin ChromeAgent.
44 | "chromeAgentModule": "path to custom module",
45 | "clearStorage": [
46 | // https://chromedevtools.github.io/devtools-protocol/tot/Storage/#method-clearDataForOrigin
47 | {
48 | "origin": ".ziprecruiter.com",
49 | "storageTypes": "cookies,localstorage"
50 | }
51 | ],
52 | "chromeAgent": {
53 | "screenRecorder": {
54 | "videoFormat": "",
55 |
56 | "s3": {
57 | "region": ""
58 | // additional options to the constructor http://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/S3.html#constructor-property
59 | },
60 | "s3Upload": {
61 | "Bucket": ""
62 | // additional options to the upload function http://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/S3.html#upload-property
63 | }
64 | }
65 | }
66 | }
67 | }
68 | ```
69 |
70 | ## Clients
71 |
72 | The project provides a [python](clients/py) client and a [javascript](clients/js) client. See the [examples](examples) for basic usage.
73 |
74 | Feel free to add support for any of the languages that the selenium project supports (java, python, javascript, c#, ruby).
75 |
76 | ## Issues
77 |
78 | Please report any issues using the [ChromeDriver Proxy issue tracker](https://github.com/ZipRecruiter/chromedriver-proxy/issues). When using
79 | the issue tracker
80 |
81 | - __Do__ include a detailed description of the problem.
82 | - __Do__ include a link to a [gist](http://gist.github.com/) with any
83 | interesting stack traces/logs (you may also attach these directly to the bug
84 | report).
85 | - __Do__ include a reduced test case.
86 | - __Do not__ use the issue tracker to submit basic help requests.
87 | - __Do not__ post empty "I see this too" or "Any updates?" comments. These
88 | provide no additional information and clutter the log.
89 | - __Do not__ report regressions on closed bugs as they are not actively
90 | monitored for updates (especially bugs that are >6 months old). Please open a
91 | new issue and reference the original bug in your report.
92 |
93 | # License
94 |
95 | Copyright (c) 2017 ZipRecruiter
96 |
97 | Permission is hereby granted, free of charge, to any person obtaining a copy
98 | of this software and associated documentation files (the "Software"), to deal
99 | in the Software without restriction, including without limitation the rights
100 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
101 | copies of the Software, and to permit persons to whom the Software is
102 | furnished to do so, subject to the following conditions:
103 |
104 | The above copyright notice and this permission notice shall be included in all
105 | copies or substantial portions of the Software.
106 |
107 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
108 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
109 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
110 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
111 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
112 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
113 | SOFTWARE.
114 |
--------------------------------------------------------------------------------
/lib/chromedriver.js:
--------------------------------------------------------------------------------
1 |
2 |
3 | /**
4 | * Copyright (c) 2017 ZipRecruiter
5 | *
6 | * Permission is hereby granted, free of charge, to any person obtaining a copy
7 | * of this software and associated documentation files (the "Software"), to deal
8 | * in the Software without restriction, including without limitation the rights
9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | * copies of the Software, and to permit persons to whom the Software is
11 | * furnished to do so, subject to the following conditions:
12 | *
13 | * The above copyright notice and this permission notice shall be included in all
14 | * copies or substantial portions of the Software.
15 | *
16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 | * SOFTWARE.
23 | * */
24 |
25 | const { spawn } = require('child_process');
26 | const debug = require('debug')('chromedriver_proxy:chromedriver');
27 | const http = require('http');
28 |
29 | class ChromeDriver {
30 | constructor(options) {
31 | this.port = options.port || 4445;
32 | this.autoRestart = options.autoRestart || false;
33 | this.chromeDriverPath = options.chromedriverPath || '/usr/bin/chromedriver';
34 | this.args = options.args || [];
35 | this.shutdown = false;
36 | }
37 |
38 | createArgs() {
39 | const self = this;
40 | const args = [];
41 | args.push(`--port=${self.port}`);
42 |
43 | for (let i = 0; i < self.args.length; i += 1) {
44 | args.push(self.args[i]);
45 | }
46 |
47 | return args;
48 | }
49 |
50 | verifyStarted() {
51 | const self = this;
52 | const statusOptions = {
53 | timeout: 2000,
54 | port: self.port,
55 | host: '127.0.0.1',
56 | path: '/status',
57 | };
58 | return new Promise((resolve, reject) => {
59 | http.get(statusOptions, (res) => {
60 | if (res.statusCode === 200) {
61 | resolve();
62 | } else {
63 | reject(new Error(`chromedriver status: ${res.statusCode}`));
64 | }
65 | }).on('socket', (socket) => {
66 | socket.setTimeout(2000);
67 | }).on('error', (err) => {
68 | debug(err.stack);
69 | reject(err);
70 | });
71 | });
72 | }
73 |
74 | start() {
75 | const self = this;
76 | const args = self.createArgs();
77 |
78 | debug(`${self.chromeDriverPath} ${args.join(' ')}`);
79 |
80 | return new Promise((resolve, reject) => {
81 | const exitListener = function exitListener(code, signal) {
82 | debug(`chromedriver exited with status: ${code} signal: ${signal}`);
83 | reject();
84 | };
85 | const startupListener = function startupListener(data) {
86 | const chunk = data.toString('utf-8');
87 | if (chunk.indexOf('Starting ChromeDriver') !== -1) {
88 | console.log(chunk);
89 | self.child.removeListener('exit', exitListener);
90 | self.child.stdout.removeListener('data', startupListener);
91 | }
92 | const maxRetries = 3;
93 | let retryCount = 0;
94 | const verifyChromedriverConn = () => {
95 | self.verifyStarted().then(() => {
96 | resolve();
97 | }).catch((err) => {
98 | retryCount += 1;
99 | if (retryCount < maxRetries) {
100 | setTimeout(verifyChromedriverConn, 50);
101 | } else {
102 | reject(err);
103 | }
104 | });
105 | };
106 | verifyChromedriverConn();
107 | };
108 | self.child = spawn(self.chromeDriverPath, args);
109 | self.child.addListener('exit', exitListener);
110 | self.child.stdout.addListener('data', startupListener);
111 |
112 | debug(`ChromeDriver Pid: ${self.child.pid}`);
113 |
114 | self.child.on('close', () => {
115 | if (self.shutdown || !self.autoRestart) { return; }
116 |
117 | setTimeout(() => {
118 | debug('Restarting chromediver');
119 | self.start();
120 | }, 10);
121 | });
122 | });
123 | }
124 |
125 | stop() {
126 | const self = this;
127 | debug('stop chromedriver');
128 | self.shutdown = true;
129 | return new Promise((resolve) => {
130 | self.child.once('exit', (code, signal) => {
131 | debug(`chromedriver exited: ${code} ${signal}`);
132 | resolve();
133 | });
134 | self.child.kill('SIGKILL');
135 | });
136 | }
137 | }
138 |
139 | module.exports = ChromeDriver;
140 |
--------------------------------------------------------------------------------
/clients/js/chrome_driver_proxy.js:
--------------------------------------------------------------------------------
1 |
2 |
3 | /**
4 | * Copyright (c) 2017 ZipRecruiter
5 | *
6 | * Permission is hereby granted, free of charge, to any person obtaining a copy
7 | * of this software and associated documentation files (the "Software"), to deal
8 | * in the Software without restriction, including without limitation the rights
9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | * copies of the Software, and to permit persons to whom the Software is
11 | * furnished to do so, subject to the following conditions:
12 | *
13 | * The above copyright notice and this permission notice shall be included in all
14 | * copies or substantial portions of the Software.
15 | *
16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 | * SOFTWARE.
23 | * */
24 |
25 | const Chrome = require('selenium-webdriver/chrome').Driver;
26 | const http = require('selenium-webdriver/http');
27 | const command = require('selenium-webdriver/lib/command');
28 | const Capabilities = require('selenium-webdriver/lib/capabilities').Capabilities;
29 |
30 | const Command = {
31 | START_SCREENCAST: 'startScreencast',
32 | STOP_SCREENCAST: 'stopScreencast',
33 | GET_SCREENCAST_PATH: 'getScreencastPath',
34 | GET_SCREENCAST_S3: 'getScreencastS3',
35 | SET_HEADERS: 'setHeaders',
36 | SET_USER_AGENT: 'setUserAgent',
37 | ADD_SCRIPT: 'addScript',
38 | REMOVE_ALL_SCRIPTS: 'removeAllScripts',
39 | SET_CLEAR_STORAGE: 'setClearStorage',
40 | };
41 |
42 | function configureExecutor(executor) {
43 | executor.defineCommand(
44 | Command.START_SCREENCAST,
45 | 'POST',
46 | '/session/:sessionId/chromedriver-proxy/screencast/start',
47 | );
48 | executor.defineCommand(
49 | Command.STOP_SCREENCAST,
50 | 'POST',
51 | '/session/:sessionId/chromedriver-proxy/screencast/stop',
52 | );
53 | executor.defineCommand(
54 | Command.GET_SCREENCAST_PATH,
55 | 'GET',
56 | '/session/:sessionId/chromedriver-proxy/screencast/path',
57 | );
58 | executor.defineCommand(
59 | Command.GET_SCREENCAST_S3,
60 | 'GET',
61 | '/session/:sessionId/chromedriver-proxy/screencast/s3',
62 | );
63 | executor.defineCommand(
64 | Command.SET_EXTRA_HEADERS,
65 | 'POST',
66 | '/session/:sessionId/chromedriver-proxy/headers',
67 | );
68 | executor.defineCommand(
69 | Command.SET_USER_AGENT,
70 | 'POST',
71 | '/session/:sessionId/chromedriver-proxy/useragent',
72 | );
73 | executor.defineCommand(
74 | Command.ADD_SCRIPT,
75 | 'POST',
76 | '/session/:sessionId/chromedriver-proxy/script',
77 | );
78 | executor.defineCommand(
79 | Command.REMOVE_ALL_SCRIPTS,
80 | 'DELETE',
81 | '/session/:sessionId/chromedriver-proxy/scripts',
82 | );
83 | executor.defineCommand(
84 | Command.SET_CLEAR_STORAGE,
85 | 'POST',
86 | '/session/:sessionId/chromedriver-proxy/storage',
87 | );
88 | // executor.defineCommand(
89 | // Command.NAVIGATE,
90 | // 'POST',
91 | // '/session/:sessionId/chromedriver-proxy/navigate',
92 | // );
93 | }
94 |
95 | function createExecutor(url) {
96 | const client = url.then(u => new http.HttpClient(u));
97 | const executor = new http.Executor(client);
98 | configureExecutor(executor);
99 | return executor;
100 | }
101 |
102 |
103 | class Driver extends Chrome {
104 |
105 | static createSession(url, opts) {
106 | const caps = Capabilities.chrome();
107 | caps.merge(opts);
108 |
109 | let client = Promise.resolve(url).then(
110 | (url) => new http.HttpClient(url)
111 | )
112 | let executor = new http.Executor(client);
113 |
114 | return super.createSession(caps, createExecutor(Promise.resolve(url)), null);;
115 | }
116 |
117 | startScreencast(params) {
118 | return this.execute(
119 | new command.Command(Command.START_SCREENCAST).setParameters(params),
120 | 'ChromeDriverProxy.startScreencast',
121 | );
122 | }
123 |
124 | stopScreencast() {
125 | return this.execute(
126 | new command.Command(Command.STOP_SCREENCAST),
127 | 'ChromeDriverProxy.stopScreencast',
128 | );
129 | }
130 |
131 | getScreencastPath() {
132 | return this.execute(
133 | new command.Command(Command.GET_SCREENCAST_PATH),
134 | 'ChromeDriverProxy.getScreencastPath',
135 | );
136 | }
137 |
138 | getScreencastS3() {
139 | return this.execute(
140 | new command.Command(Command.GET_SCREENCAST_S3),
141 | 'ChromeDriverProxy.getScreencastS3',
142 | );
143 | }
144 |
145 | setExtraHeaders(headers) {
146 | return this.execute(
147 | new command.Command(Command.SET_EXTRA_HEADERS).setParameter('headers', headers),
148 | 'ChromeDriverProxy.setExtraHeaders',
149 | );
150 | }
151 |
152 | setUserAgent(userAgent) {
153 | return this.execute(
154 | new command.Command(Command.SET_USER_AGENT).setParameter('userAgent', userAgent),
155 | 'ChromeDriverProxy.setUserAgent',
156 | );
157 | }
158 |
159 | addScript(script) {
160 | return this.execute(
161 | new command.Command(Command.ADD_SCRIPT).setParameter('scriptSource', script),
162 | 'ChromeDriverProxy.addScript',
163 | );
164 | }
165 |
166 | removeAllScripts() {
167 | return this.execute(
168 | new command.Command(Command.REMOVE_ALL_SCRIPTS),
169 | 'ChromeDriverProxy.removeAllScripts',
170 | );
171 | }
172 |
173 | setClearStorage(options) {
174 | return this.execute(
175 | new command.Command(Command.SET_CLEAR_STORAGE).setParameter('values', options),
176 | 'ChromeDriverProxy.setClearStorage',
177 | );
178 | }
179 |
180 | // pageNavigate(options) {
181 | // return this.execute(
182 | // new command.Command(Command.NAVIGATE).setParameter('options', options),
183 | // 'ChromeDriverProxy.NAVIGATE',
184 | // );
185 | // }
186 | }
187 |
188 | module.exports = Driver;
189 |
--------------------------------------------------------------------------------
/test/chrome-pool-cookies-test.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) 2017 ZipRecruiter
3 | *
4 | * Permission is hereby granted, free of charge, to any person obtaining a copy
5 | * of this software and associated documentation files (the "Software"), to deal
6 | * in the Software without restriction, including without limitation the rights
7 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8 | * copies of the Software, and to permit persons to whom the Software is
9 | * furnished to do so, subject to the following conditions:
10 | *
11 | * The above copyright notice and this permission notice shall be included in all
12 | * copies or substantial portions of the Software.
13 | *
14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
20 | * SOFTWARE.
21 | * */
22 |
23 |
24 | const chai = require('chai');
25 |
26 | const expect = chai.expect;
27 | const HttpServer = require('..').HttpServer;
28 |
29 | const Driver = require('../clients/js/chrome_driver_proxy');
30 | const Chrome = require('selenium-webdriver/chrome').Driver;
31 | const ChromeOptions = require('selenium-webdriver/chrome').Options;
32 |
33 | const chromedriverBin = process.env.CHROMEDRIVER_BIN || '/usr/bin/chromedriver';
34 |
35 | describe('Proxy with chrome pool cookie test', () => {
36 | let server;
37 | let driver;
38 | let options;
39 | let chromeOptions;
40 | const mockServerUrl = 'http://127.0.0.1:8080';
41 |
42 | beforeEach((done) => {
43 | const config = {
44 | proxy: {
45 | port: 4444,
46 | baseUrl: '/wd/hub',
47 | },
48 | chromedriver: {
49 | chromedriverPath: chromedriverBin,
50 | port: 4445,
51 | autoRestart: false,
52 | },
53 | chromePool: {
54 | enable: true,
55 | reuse: true,
56 | chromePath: '/usr/bin/google-chrome',
57 | clearStorage: [
58 | {
59 | origin: '.localhost',
60 | storageTypes: 'cookies,localstorage',
61 | },
62 | ],
63 | },
64 | };
65 | server = new HttpServer(config.proxy);
66 | server.start(config, () => {
67 | chromeOptions = new ChromeOptions();
68 | chromeOptions.addArguments(
69 | '--headless',
70 | '--disable-gpu',
71 | '--no-first-run',
72 | '--no-sandbox',
73 | '--disable-dev-shm-usage',
74 | );
75 | chromeOptions.setChromeBinaryPath('/usr/bin/google-chrome');
76 |
77 | driver = Driver.createSession('http://127.0.0.1:4444/wd/hub', chromeOptions);
78 | done();
79 | });
80 | });
81 |
82 | afterEach((done) => {
83 | server.stop(done);
84 | });
85 |
86 | it('can clear cookies between sessions', (done) => {
87 | driver.get(`${mockServerUrl}/base.html`).then(() => driver.manage().addCookie({ name: 'foo', value: 'bar' })).then(() => driver.get(`${mockServerUrl}/cookies`)).then(() => driver.getPageSource())
88 | .then(source => driver.findElement({ css: '#foo span.value' }))
89 | .then(elem => elem.getText())
90 | .then((cookieValue) => {
91 | expect(cookieValue).to.equal('bar');
92 | return driver.quit();
93 | })
94 | .then(() => {
95 | driver = Driver.createSession('http://127.0.0.1:4444/wd/hub', chromeOptions);
96 | return driver.get(`${mockServerUrl}/base.html`);
97 | })
98 | .then(() => {
99 | return driver.manage().getCookie('foo').catch((err) => {
100 | return null;
101 | });
102 | })
103 | .then((cookie) => {
104 | expect(cookie).to.be.null;
105 | })
106 | .then(() => driver.quit())
107 | .then(() => {
108 | done();
109 | })
110 | .catch((err) => {
111 | done(err);
112 | });
113 | });
114 |
115 | it('can override default for clearing local storage', (done) => {
116 | driver.get(`${mockServerUrl}/base.html`).then(() => driver.setClearStorage([])).then(() => driver.get(`${mockServerUrl}/cookies`)).then(() => driver.executeScript('window.localStorage.setItem("foo", "bar")'))
117 | .then(() => driver.executeScript('return window.localStorage.getItem("foo")'))
118 | .then((value) => {
119 | expect(value).to.equal('bar');
120 | return driver.quit();
121 | })
122 | .then(() => {
123 | // TODO find a better way to wait for the browser to be available in the pool
124 | return driver.sleep(5);
125 | }).then(() => {
126 | driver = Driver.createSession('http://127.0.0.1:4444/wd/hub', chromeOptions);
127 | return driver.get(`${mockServerUrl}/base.html`);
128 | })
129 | .then(() => driver.executeScript('return window.localStorage.getItem("foo")'))
130 | .then((value) => {
131 | expect(value).to.equal('bar');
132 | })
133 | .then(() => driver.quit())
134 | .then(() => {
135 | done();
136 | })
137 | .catch((err) => {
138 | done(err);
139 | });
140 | });
141 |
142 | it('can clear local storage between sessions', (done) => {
143 | driver.get(`${mockServerUrl}/base.html`).then(() => driver.manage().addCookie({ name: 'foo', value: 'bar' })).then(() => driver.get(`${mockServerUrl}/cookies`)).then(() => driver.executeScript('window.localStorage.setItem("foo", "bar")'))
144 | .then(() => driver.executeScript('return window.localStorage.getItem("foo")'))
145 | .then((value) => {
146 | expect(value).to.equal('bar');
147 | return driver.quit();
148 | })
149 | .then(() => {
150 | driver = Driver.createSession('http://127.0.0.1:4444/wd/hub', chromeOptions);
151 | return driver.get(`${mockServerUrl}/base.html`);
152 | })
153 | .then(() => driver.executeScript('return window.localStorage.getItem("foo")'))
154 | .then((value) => {
155 | expect(value).to.be.null;
156 | })
157 | .then(() => driver.quit())
158 | .then(() => {
159 | done();
160 | })
161 | .catch((err) => {
162 | done(err);
163 | });
164 | });
165 |
166 | it('can close extra windows between sessions', (done) => {
167 | driver.get(`${mockServerUrl}/base.html`).then(() => driver.executeScript(`open("${mockServerUrl}/base.html"); open("${mockServerUrl}/base.html")`)).then(() => driver.getAllWindowHandles()).then((handles) => {
168 | expect(handles.length).to.equal(3);
169 | return driver.quit();
170 | })
171 | .then(() => {
172 | driver = Driver.createSession('http://127.0.0.1:4444/wd/hub', chromeOptions);
173 | return driver.get(`${mockServerUrl}/base.html`);
174 | })
175 | .then(() => driver.getAllWindowHandles())
176 | .then((handles) => {
177 | expect(handles.length).to.equal(1);
178 | return driver.quit();
179 | })
180 | .then(() => {
181 | done();
182 | })
183 | .catch((err) => {
184 | done(err);
185 | });
186 | });
187 | });
188 |
--------------------------------------------------------------------------------
/lib/chrome_pool.js:
--------------------------------------------------------------------------------
1 |
2 |
3 | /**
4 | * Copyright (c) 2017 ZipRecruiter
5 | *
6 | * Permission is hereby granted, free of charge, to any person obtaining a copy
7 | * of this software and associated documentation files (the "Software"), to deal
8 | * in the Software without restriction, including without limitation the rights
9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | * copies of the Software, and to permit persons to whom the Software is
11 | * furnished to do so, subject to the following conditions:
12 | *
13 | * The above copyright notice and this permission notice shall be included in all
14 | * copies or substantial portions of the Software.
15 | *
16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 | * SOFTWARE.
23 | * */
24 |
25 | const crypto = require('crypto');
26 | const getPort = require('get-port');
27 | const { execFile } = require('child_process');
28 | const CDP = require('chrome-remote-interface');
29 | const debug = require('debug')('chromedriver_proxy:chrome_pool');
30 | const debugBrowser = require('debug')('chromedriver_proxy:chrome_pool_browser');
31 | const path = require('path');
32 | const ChromeAgentManager = require('./chrome_agent_manager');
33 |
34 | class ChromePool {
35 | constructor(options) {
36 | const o = options || {};
37 | this.chrome_binary = o.chromePath || '/usr/bin/google-chrome';
38 | this.chromeStorage = o.clearStorage || [];
39 | this.tmpDir = o.tmpDir || '/tmp';
40 | this.reuse = o.reuse || false;
41 | this.enable = typeof o.enable === 'undefined' ? false : o.enable;
42 | this.chromeAgentModule = o.chromeAgentModule || `${__dirname}${path.sep}chrome_agent.js`;
43 | this.chromeAgentOptions = o.chromeAgent || {};
44 | this.chromeStartupTimeOut = o.chromeStartupTimeOut || 1000;
45 | this.inavtivePool = [];
46 | this.pool = {};
47 | this.agents = {};
48 | }
49 |
50 | // takes the chrome command line args
51 | get(options) {
52 | const self = this;
53 | const args = options.args || [];
54 | let port = null;
55 |
56 | // transform args
57 | for (let i = 0; i < args.length; i += 1) {
58 | const arg = args[i];
59 | if (arg.substring(0, 2) !== '--') {
60 | args[i] = `--${args[i]}`;
61 | }
62 | }
63 |
64 | for (let i = 0; i < self.inavtivePool.length; i += 1) {
65 | let match = false;
66 | const browser = self.pool[self.inavtivePool[i]];
67 | if (browser === undefined) {
68 | debug(`ERROR: browser at port ${self.inavtivePool[i]} is undefined`);
69 | continue;
70 | }
71 | const currentArgs = browser.args;
72 | if (currentArgs.length === args.length) {
73 | match = true;
74 | }
75 | for (let j = 0; j < args.length; j += 1) {
76 | if (currentArgs[j] !== args[j]) {
77 | match = false;
78 | break;
79 | }
80 | }
81 | if (match) {
82 | port = self.inavtivePool[i];
83 | self.inavtivePool.splice(i, 1);
84 | debug(`Reuse browser at port: ${port}`);
85 | return Promise.resolve(port);
86 | }
87 | }
88 |
89 | return self.startBrowser(options);
90 | }
91 |
92 | startBrowser(options) {
93 | const self = this;
94 | self.profile = `${self.tmpDir}/chrome-profile-${crypto.randomBytes(16).toString('hex')}`;
95 | const args = options.args ? options.args.slice(0) : [];
96 | args.reverse();
97 | args.push(`--user-data-dir=${self.profile}`);
98 |
99 | return getPort().then((port) => {
100 | args.push(`--remote-debugging-port=${port}`);
101 | debug(`launch browser: ${self.chrome_binary} ${args.join(' ')}`);
102 | const browser = execFile(self.chrome_binary, args);
103 | browser.stderr.on('data', (chunk) => {
104 | debugBrowser(`browser port: ${port} pid: ${browser.pid} message: ${chunk.trim()}`);
105 | });
106 | return new Promise((resolve, reject) => {
107 | browser.once('exit', (code) => {
108 | reject(Error(`browser port: ${port} pid: ${browser.pid} exit with code: ${code}`));
109 | });
110 | browser.stderr.once('data', () => {
111 | debug(`Started Browser: port => ${port} pid => ${browser.pid}`);
112 |
113 | // getting the target is temperamental so we retry on failure
114 | const endTime = Date.now() + self.chromeStartupTimeOut;
115 | const resolveTarget = function resolveTarget() {
116 | CDP.List({ host: '127.0.0.1', port }, (err, targets) => {
117 | if ((err && Date.now() <= endTime) || (typeof targets === 'undefined' || targets.length === 0)) {
118 | setTimeout(resolveTarget, 50);
119 | return;
120 | } else if (err) {
121 | reject(err);
122 | return;
123 | }
124 | self.pool[port] = {
125 | args: options.args,
126 | process: browser,
127 | target: targets[0].id,
128 | };
129 | resolve(port);
130 | });
131 | };
132 | setTimeout(resolveTarget, 10);
133 | });
134 | });
135 | });
136 | }
137 |
138 | stopBrowser(port) {
139 | const self = this;
140 | const p = self.pool[port].process;
141 | return new Promise((resolve) => {
142 | p.once('exit', (code, signal) => {
143 | debug(`browser at port: ${port} exited ${code} ${signal}`);
144 | resolve();
145 | });
146 | p.kill('SIGKILL');
147 | debug(`SIGKILL sent to browser at port: ${port}`);
148 | delete self.pool[port];
149 | });
150 | }
151 |
152 | killAll() {
153 | const self = this;
154 | Object.keys(self.pool).forEach((v) => {
155 | self.pool[v].process.kill('SIGKILL');
156 | });
157 | self.pool = {};
158 | self.inavtivePool = [];
159 | debug('kill all chrome instances');
160 | }
161 |
162 | async put(port) {
163 | const self = this;
164 | if (self.reuse) {
165 | try {
166 | const agent = self.agents[port];
167 | await agent.stop();
168 | delete self.agents[port];
169 | } catch (err) {
170 | debug(`error in chrome ${port} cleanup: ${err}`);
171 | delete self.agents[port];
172 | return self.stopBrowser(port);
173 | }
174 | } else {
175 | return self.stopBrowser(port);
176 | }
177 | self.inavtivePool.push(port);
178 | return Promise.resolve();
179 | }
180 |
181 | sendToAgent(options) {
182 | const self = this;
183 | return self.getAgent(options).then(agent => agent.send(options));
184 | }
185 |
186 | getAgent(options) {
187 | const self = this;
188 | const { port } = options;
189 |
190 | if (self.agents[port]) {
191 | debug('agent already exists');
192 | return Promise.resolve(self.agents[port]);
193 | }
194 | debug('agent does NOT already exists');
195 | const agent = new ChromeAgentManager();
196 | const chromePid = self.pool[port].process.pid;
197 | const defaultOptions = {
198 | chromeAgentModule: self.chromeAgentModule,
199 | host: '127.0.0.1',
200 | port,
201 | target: self.pool[port].target,
202 | chromeStorage: self.chromeStorage,
203 | chromePid,
204 | };
205 | Object.assign(defaultOptions, self.chromeAgentOptions);
206 | agent.start(defaultOptions);
207 | self.agents[port] = agent;
208 |
209 | return Promise.resolve(agent);
210 | }
211 | }
212 |
213 | module.exports = ChromePool;
214 |
--------------------------------------------------------------------------------
/lib/chrome_agent.js:
--------------------------------------------------------------------------------
1 |
2 |
3 | /**
4 | * Copyright (c) 2017 ZipRecruiter
5 | *
6 | * Permission is hereby granted, free of charge, to any person obtaining a copy
7 | * of this software and associated documentation files (the "Software"), to deal
8 | * in the Software without restriction, including without limitation the rights
9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | * copies of the Software, and to permit persons to whom the Software is
11 | * furnished to do so, subject to the following conditions:
12 | *
13 | * The above copyright notice and this permission notice shall be included in all
14 | * copies or substantial portions of the Software.
15 | *
16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 | * SOFTWARE.
23 | * */
24 |
25 | const process = require('process');
26 | const CDP = require('chrome-remote-interface');
27 | const debug = require('debug')('chromedriver_proxy:chrome_agent');
28 | const gpath = require('path');
29 |
30 | const ScreenRecorder = require(gpath.join(__dirname, 'screen_recorder'));
31 | const RealTimeScreenRecorder = require(gpath.join(__dirname, 'realtime_screen_recorder'));
32 |
33 | class ChromeAgent {
34 | constructor(options) {
35 | Object.keys(options).forEach((i) => {
36 | this[i] = options[i];
37 | });
38 | this.screenRecorderOptions = this.screenRecorder || {};
39 | delete this.screenRecorder;
40 | this.scriptIds = new Set();
41 |
42 | this.routes = [
43 | ['POST', 'screencast/start', 'startScreencast'],
44 | ['POST', 'screencast/subtitle/add', 'addSubtitle'],
45 | ['POST', 'screencast/stop', 'stopScreencast'],
46 | ['GET', 'screencast/path', 'getScreencastPath'],
47 | ['GET', 'screencast/s3', 'getScreencastS3'],
48 | ['POST', 'headers', 'setHeaders'],
49 | ['POST', 'useragent', 'setUserAgent'],
50 | ['POST', 'script', 'addScript'],
51 | ['DELETE', 'scripts', 'removeAllScripts'],
52 | ['POST', 'storage', 'setClearStorage'],
53 | ['POST', 'navigate', 'navigate'],
54 | ];
55 | }
56 |
57 |
58 | async setClearStorage(options) {
59 | const self = this;
60 | self.chromeStorage = options.values;
61 | return {};
62 | }
63 |
64 | async getConn() {
65 | const self = this;
66 | if (self.client) {
67 | return self.client;
68 | }
69 | return new Promise((resolve, reject) => {
70 | CDP({ host: '127.0.0.1', port: self.port, target: self.target }, (client) => {
71 | self.client = client;
72 | resolve(client);
73 | }).on('error', (err) => {
74 | debug('unable to connect to debugger');
75 | reject(err);
76 | });
77 | });
78 | }
79 |
80 | async navigate(options) {
81 | const self = this;
82 | const { Page } = await self.getConn();
83 | await Page.enable();
84 | debug(`navigate ${JSON.stringify(options.values)}`);
85 | const result = await Page.navigate(options.values);
86 | if (typeof result.errorText !== 'undefined') {
87 | debug(`navigation error ${result.errorText}`);
88 | return Promise.reject(new Error(result.errorText));
89 | }
90 | return result;
91 | }
92 |
93 | async addScript(options) {
94 | const self = this;
95 | const opts = options;
96 | const { Page } = await self.getConn();
97 | await Page.enable();
98 | // for backwards compatibility
99 | if ('scriptSource' in opts) {
100 | opts.source = opts.scriptSource;
101 | delete opts.scriptSource;
102 | }
103 | debug(`add script ${JSON.stringify(opts)}`);
104 | // BUG returns an empty string for an identifier
105 | const result = await Page.addScriptToEvaluateOnNewDocument(opts);
106 | debug(`added script: ${JSON.stringify(result)}`);
107 | self.scriptIds.add(result.identifier);
108 | return result;
109 | }
110 |
111 | async removeAllScripts() {
112 | const self = this;
113 | const { Page } = await self.getConn();
114 | await Page.enable();
115 | debug('remove all scripts start');
116 | const p = [];
117 | self.scriptIds.forEach((id) => {
118 | p.push(Page.removeScriptToEvaluateOnLoad({ identifier: id }));
119 | });
120 | await Promise.all(p);
121 | self.scriptIds = new Set();
122 | debug('remove all scripts done');
123 | }
124 |
125 | async setHeaders(options) {
126 | const self = this;
127 | const { Network } = await self.getConn();
128 | Network.enable();
129 | debug(`set extra headers ${JSON.stringify(options)}`);
130 | return Network.setExtraHTTPHeaders({ headers: options.headers });
131 | }
132 |
133 | async setUserAgent(options) {
134 | const self = this;
135 | const { Network } = await self.getConn();
136 | debug(`set user agent ${JSON.stringify(options)}`);
137 | return Network.setUserAgentOverride({ userAgent: options.userAgent });
138 | }
139 |
140 | async getScreencastPath() {
141 | const self = this;
142 | const result = await self.screenRecorderVideo;
143 | debug(`local screencast path: ${result}`);
144 | return { path: result };
145 | }
146 |
147 | async getScreencastS3() {
148 | const self = this;
149 | const result = await self.screenRecorderVideo.then(() => self.sr.s3UploadResult);
150 | debug(`s3 upload: ${JSON.stringify(result)}`);
151 | return result;
152 | }
153 |
154 | async startScreencast(options) {
155 | const self = this;
156 | debug('start screencast');
157 | const client = await self.getConn();
158 | const defaultOptions = self.screenRecorderOptions;
159 | defaultOptions.client = client;
160 | if (options.format === 'm3u8') {
161 | self.sr = new RealTimeScreenRecorder(defaultOptions);
162 | } else {
163 | self.sr = new ScreenRecorder(defaultOptions);
164 | }
165 |
166 | const { Page } = client;
167 | await Page.enable();
168 | debug(`screen recorder options ${JSON.stringify(options)}`);
169 | await self.sr.start(options);
170 | return self.sr.expectedResult();
171 | }
172 |
173 | async stopScreencast(options) {
174 | const self = this;
175 | debug('stop screencast');
176 | if (!self.sr) {
177 | throw new Error('must call startScreencast before calling stopScreencast');
178 | }
179 | self.screenRecorderVideo = self.sr.stop(options);
180 | return self.sr.expectedResult();
181 | }
182 |
183 | async addSubtitle(options) {
184 | const self = this;
185 | if (!self.sr) {
186 | throw new Error('must call startScreencast before calling addSubtitle');
187 | }
188 | self.sr.addSubtitle(options);
189 | return {};
190 | }
191 |
192 | async cleanBrowser() {
193 | const self = this;
194 | debug('start browser cleanup');
195 | try {
196 | const {
197 | Network, Target, Page, Storage,
198 | } = await self.getConn();
199 |
200 | const closeTarget = function closeTarget(target) {
201 | return Target.activateTarget(target).then(() => Target.closeTarget(target));
202 | };
203 |
204 | const clearData = [Network.clearBrowserCookies()];
205 | self.chromeStorage.forEach((e) => {
206 | clearData.push(Storage.clearDataForOrigin(e));
207 | });
208 | await Promise.all(clearData);
209 | await Page.navigate({ url: 'about:blank' });
210 | const result = await Target.getTargets();
211 |
212 | let p = Promise.resolve();
213 | result.targetInfos.forEach((target) => {
214 | // close everything but the main target
215 | if (target.targetId !== self.target) {
216 | p = p.then(() => closeTarget(target));
217 | }
218 | });
219 | await p;
220 | } catch (err) {
221 | debug(`Browser cleanup error: ${err}`);
222 | throw err;
223 | }
224 | }
225 |
226 | async stop() {
227 | const self = this;
228 | debug('stop agent');
229 | if (typeof self.screenRecorderVideo !== 'undefined') {
230 | await self.screenRecorderVideo.then(() => self.sr.s3UploadResult);
231 | }
232 | if (self.client) {
233 | await self.removeAllScripts();
234 | await self.cleanBrowser();
235 | self.client.close();
236 | }
237 | }
238 |
239 | handle(blob) {
240 | const self = this;
241 | try {
242 | if (blob.action === 'stop') {
243 | return self.stop().then(() => {
244 | debug('stop agent success');
245 | process.exit(0);
246 | }).catch((err) => {
247 | debug(`error when stopping agent: ${err}`);
248 | process.exit(1);
249 | });
250 | }
251 | const { value } = blob;
252 | const { path, sessionId, httpMethod } = value;
253 | const body = value.body ? JSON.parse(blob.value.body) : {};
254 | body.sessionId = sessionId;
255 |
256 | let action = null;
257 | const route = Object.values(self.routes).find((r) => {
258 | if (httpMethod === r[0] && path === r[1]) {
259 | return true;
260 | }
261 | return false;
262 | });
263 | action = route && route[2];
264 | if (action === null) {
265 | return Promise.reject(new Error(`unknown path: ${httpMethod} ${path}`));
266 | }
267 | if (typeof (self[action]) !== 'function') {
268 | return Promise.reject(new Error(`undefined method: ${action}`));
269 | }
270 | return self[action](body);
271 | } catch (err) {
272 | return Promise.reject(new Error(`unknown error: ${err}`));
273 | }
274 | }
275 | }
276 |
277 |
278 | module.exports = ChromeAgent;
279 |
--------------------------------------------------------------------------------
/lib/realtime_screen_recorder.js:
--------------------------------------------------------------------------------
1 |
2 |
3 | /**
4 | * Copyright (c) 2017 ZipRecruiter
5 | *
6 | * Permission is hereby granted, free of charge, to any person obtaining a copy
7 | * of this software and associated documentation files (the "Software"), to deal
8 | * in the Software without restriction, including without limitation the rights
9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | * copies of the Software, and to permit persons to whom the Software is
11 | * furnished to do so, subject to the following conditions:
12 | *
13 | * The above copyright notice and this permission notice shall be included in all
14 | * copies or substantial portions of the Software.
15 | *
16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 | * SOFTWARE.
23 | * */
24 |
25 | const CDP = require('chrome-remote-interface');
26 | const fs = require('fs');
27 | const cp = require('child_process');
28 | const util = require('util');
29 | const debug = require('debug')('chromedriver_proxy:realtime_screen_recorder');
30 | const S3 = require('aws-sdk/clients/s3');
31 | const path = require('path');
32 |
33 | const readFile = util.promisify(fs.readFile);
34 |
35 | const defaultVideoFormatArgs = {
36 | m3u8: [
37 | '-an',
38 | '-g',
39 | '10',
40 | '-c:v',
41 | 'libx264',
42 | '-f',
43 | 'hls',
44 | '-flags',
45 | '-global_header',
46 | '-hls_time',
47 | '3',
48 | '-hls_list_size',
49 | '0',
50 | '-hls_playlist_type',
51 | 'event',
52 | '-y',
53 | ],
54 | };
55 |
56 | class ScreenRecorder {
57 | constructor(options) {
58 | this.videoFormat = options.videoFormat || 'm3u8';
59 | this.extraArgs = options.extraArgs || defaultVideoFormatArgs[this.videoFormat] || [];
60 | this.tmpDir = options.tmpDir || '/tmp';
61 | this.ffmpegPath = options.ffmpegPath || '/usr/bin/ffmpeg';
62 | this.s3Options = options.s3 || {};
63 | this.s3UploadOptions = options.s3Upload || {};
64 | this.client = options.client;
65 | this.index = 0;
66 | this.end = false;
67 | this.segments = null;
68 | this.segmentsMeta = [];
69 | this.uploadedSegments = new Set();
70 | }
71 |
72 | static create(options) {
73 | return new Promise((resolve, reject) => {
74 | CDP({ host: '127.0.0.1', port: options.port, target: options.target }, (client) => {
75 | const screenRecorder = new ScreenRecorder({
76 | client,
77 | tmpDir: options.tmpDir,
78 | ffmpegPath: options.ffmpegPath,
79 | videoFormat: options.videoFormat,
80 | extraArgs: options.extraArgs,
81 | });
82 | const { Page } = client;
83 | Page.enable().then(() => {
84 | resolve(screenRecorder);
85 | });
86 | }).on('error', (err) => {
87 | debug('reject on connecting');
88 | reject(err);
89 | });
90 | });
91 | }
92 |
93 | async start(options) {
94 | const self = this;
95 | const params = options.params || {};
96 | const format = params.format || 'png';
97 | self.s3Prefix = options.s3Prefix || '';
98 | self.sessionId = options.sessionId;
99 | self.dir = options.dir || self.tmpDir;
100 | const { Page } = self.client;
101 | self.frames = [];
102 |
103 | const output = self.getMetaPath();
104 | self.output = output;
105 | const touchOutput = new Promise((resolve, reject) => {
106 | fs.open(output, 'w', (err) => {
107 | if (err) {
108 | reject(err);
109 | } else {
110 | resolve();
111 | }
112 | });
113 | });
114 | await touchOutput;
115 | debug(`output: ${output}`);
116 | fs.watchFile(output, async () => {
117 | debug(`file change: ${output}`);
118 | await self.updateStream({ path: output });
119 | });
120 | self.ffmpegChild = self.streamImagesToVideo({
121 | output,
122 | imgFormat: format,
123 | });
124 | const errors = [];
125 | const ffmpegErrorListener = (data) => {
126 | errors.push(data);
127 | };
128 | self.ffmpegChild.stderr.addListener('data', ffmpegErrorListener);
129 | self.ffmpegChild.on('close', (code) => {
130 | self.ffmpegChild.stderr.removeListener('data', ffmpegErrorListener);
131 | if (code !== 0) {
132 | debug(errors.join('\n'));
133 | }
134 | });
135 | self.ffmpegChild.stdin.on('error', (err) => {
136 | debug(err);
137 | });
138 |
139 |
140 | // register listener 1st
141 | self.client.on('Page.screencastFrame', (result) => {
142 | Page.screencastFrameAck({ sessionId: result.sessionId });
143 | const binaryBlob = Buffer.from(result.data, 'base64');
144 | self.ffmpegChild.stdin.write(binaryBlob);
145 | });
146 |
147 | self.client.on('Page.screencastVisibilityChanged', (visible) => {
148 | debug(`visiblility changed: ${JSON.stringify(visible)}`);
149 | });
150 |
151 | debug('start screencast');
152 | return Page.startScreencast(params).catch((err) => {
153 | debug(err);
154 | });
155 | }
156 |
157 | async updateStream(options) {
158 | const self = this;
159 | debug('begin update stream');
160 | const data = await readFile(options.path);
161 | const newSegments = [];
162 |
163 | let segmentFlag = false;
164 | data.toString().split('\n').forEach((e) => {
165 | const absPath = `${this.tmpDir}/${e}`;
166 | if (segmentFlag) {
167 | if (!self.uploadedSegments.has(absPath)) {
168 | newSegments.push(absPath);
169 | }
170 | segmentFlag = false;
171 | }
172 | if (e.startsWith('#EXTINF:')) {
173 | segmentFlag = true;
174 | }
175 | });
176 | debug(`new segments: ${JSON.stringify(newSegments)}`);
177 | await self.uploadSegments(newSegments);
178 | const result = await self.uploadMeta(data);
179 | debug('end update stream');
180 | return result;
181 | }
182 |
183 | async uploadSegments(newSegments) {
184 | const self = this;
185 | debug(`upload: ${JSON.stringify(newSegments)}`);
186 | newSegments.forEach(e => self.uploadedSegments.add(e));
187 | await Promise.all(newSegments.map(s => self.uploadToS3(s)));
188 | }
189 |
190 | async uploadMeta(data) {
191 | const self = this;
192 | debug('upload meta');
193 | return self.uploadToS3(data);
194 | }
195 |
196 |
197 | streamImagesToVideo(options) {
198 | const self = this;
199 | const ffmpegExe = this.ffmpegPath;
200 | const args = [
201 | '-f',
202 | 'image2pipe',
203 | '-i',
204 | `pipe:.${options.imgFormat}`,
205 | ].concat(self.extraArgs).concat(options.extraArgs || []);
206 | args.push(options.output);
207 |
208 | debug(`${ffmpegExe} ${args.join(' ')}`);
209 |
210 | return cp.spawn(ffmpegExe, args);
211 | }
212 |
213 | getSegmentPath(index) {
214 | const self = this;
215 | return path.join(self.tmpDir, self.getSegmentName(index));
216 | }
217 |
218 | getMetaPath() {
219 | const self = this;
220 | return path.join(self.tmpDir, self.getMetaName());
221 | }
222 |
223 | getMetaName() {
224 | const self = this;
225 | return `${self.sessionId}.m3u8`;
226 | }
227 |
228 | getSegmentName(index) {
229 | const self = this;
230 | return `${self.sessionId}${index}.ts`;
231 | }
232 |
233 | getFilePath() {
234 | const self = this;
235 | return path.join(self.tmpDir, self.getMetaName());
236 | }
237 |
238 | getFileName() {
239 | const self = this;
240 | return `${self.sessionId}.${self.videoFormat}`;
241 | }
242 |
243 | getS3Key(name) {
244 | const self = this;
245 | return `${self.s3Prefix || ''}${name || self.getFileName()}`;
246 | }
247 |
248 | uploadToS3(file) {
249 | const self = this;
250 | let key;
251 | let ext;
252 | if (Buffer.isBuffer(file)) {
253 | key = self.getS3Key(self.getMetaName());
254 | ext = 'm3u8';
255 | } else {
256 | key = self.getS3Key(path.basename(file));
257 | ext = path.extname(file).slice(1);
258 | }
259 |
260 | if (!this.s3UploadOptions.Bucket) {
261 | debug('skipping s3 upload: Bucket has not been set');
262 | return Promise.resolve();
263 | }
264 |
265 | return new Promise((resolve, reject) => {
266 | try {
267 | const s3 = new S3(self.s3Options);
268 | const stream = Buffer.isBuffer(file) ? file : fs.createReadStream(file);
269 |
270 | const params = {
271 | ContentType: `video/${ext}`,
272 | Key: key,
273 | Body: stream,
274 | };
275 | Object.assign(params, self.s3UploadOptions);
276 |
277 | debug('start file upload');
278 | s3.upload(params, (err) => {
279 | if (err) {
280 | debug(`s3 upload error: ${err}`);
281 | reject(err);
282 | } else {
283 | resolve({ bucket: self.s3UploadOptions.Bucket, key });
284 | debug(`successfully uploaded ${key} to ${self.s3UploadOptions.Bucket}`);
285 | }
286 | });
287 | } catch (err) {
288 | debug(`s3 upload error: ${err}`);
289 | reject(err);
290 | }
291 | });
292 | }
293 |
294 | expectedResult() {
295 | const self = this;
296 | return {
297 | path: self.getFilePath(),
298 | s3: {
299 | bucket: self.s3UploadOptions.Bucket,
300 | key: self.getS3Key(),
301 | },
302 | };
303 | }
304 |
305 | async stop() {
306 | const self = this;
307 | self.end = true;
308 | const { Page } = self.client;
309 | try {
310 | debug('stop screencast');
311 | self.client.removeAllListeners('Page.screencastVisibilityChanged');
312 | self.client.removeAllListeners('Page.screencastFrame');
313 | await Page.stopScreencast();
314 | const waitForFFmpeg = new Promise((resolve) => {
315 | self.ffmpegChild.on('close', () => {
316 | resolve();
317 | });
318 | });
319 | self.ffmpegChild.stdin.end();
320 | await waitForFFmpeg;
321 | await self.updateStream({ path: self.output });
322 | } catch (err) {
323 | debug(`error in screencast cleanup: ${err}`);
324 | }
325 | return self.expectedResult;
326 | }
327 | }
328 |
329 | module.exports = ScreenRecorder;
330 |
--------------------------------------------------------------------------------
/lib/proxy.js:
--------------------------------------------------------------------------------
1 |
2 |
3 | /**
4 | * Copyright (c) 2017 ZipRecruiter
5 | *
6 | * Permission is hereby granted, free of charge, to any person obtaining a copy
7 | * of this software and associated documentation files (the "Software"), to deal
8 | * in the Software without restriction, including without limitation the rights
9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | * copies of the Software, and to permit persons to whom the Software is
11 | * furnished to do so, subject to the following conditions:
12 | *
13 | * The above copyright notice and this permission notice shall be included in all
14 | * copies or substantial portions of the Software.
15 | *
16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 | * SOFTWARE.
23 | * */
24 |
25 | const http = require('http');
26 | const httpProxy = require('http-proxy');
27 | const ChromeDriver = require('./chromedriver.js');
28 | const ChromePool = require('./chrome_pool.js');
29 | const debug = require('debug')('chromedriver_proxy:proxy');
30 |
31 | class HttpServer {
32 | constructor(config) {
33 | const c = config || {};
34 | this.port = c.port || 4444;
35 | this.baseUrl = c.baseUrl || null;
36 | if (this.baseUrl !== null && !this.baseUrl.startsWith('/')) {
37 | this.baseUrl = `/${this.baseUrl}`;
38 | }
39 | if (this.baseUrl !== null && this.baseUrl.endsWith('/')) {
40 | this.baseUrl = this.baseUrl.substring(0, this.baseUrl.length - 1);
41 | }
42 | if (this.baseUrl !== null) {
43 | this.endBaseUrl = this.baseUrl.length;
44 | }
45 | }
46 |
47 | start(opts, fn) {
48 | const options = opts || {};
49 | const self = this;
50 | const timeout = options.timeout || -1;
51 | const keepAliveTimeout = options.keepAliveTimeout || 0;
52 |
53 | options.chromedriver = options.chromedriver || {};
54 | options.chromePool = options.chromePool || {};
55 | options.screenRecorder = options.chromePool.screenRecorder || {};
56 |
57 | const tmpDir = options.tmpDir || '/tmp';
58 | options.chromedriver.tmpDir = options.chromedriver.tmpDir || tmpDir;
59 | options.chromePool.tmpDir = options.chromePool.tmpDir || tmpDir;
60 | options.screenRecorder.tmpDir = options.screenRecorder.tmpDir || tmpDir;
61 |
62 |
63 | self.chromedriver = new ChromeDriver(options.chromedriver);
64 | const chromedriverStart = self.chromedriver.start();
65 |
66 | self.chromepool = new ChromePool(options.chromePool);
67 | const bypassChromePool = !self.chromepool.enable;
68 |
69 | self.activeSessions = {};
70 | self.screenRecorders = {};
71 |
72 | self.httpAgent = http.globalAgent;
73 | self.httpAgent.maxSockets = 10000;
74 | self.httpAgent.keepAlive = true;
75 | self.httpAgent.keepAliveMsecs = 30000;
76 |
77 | //
78 | // Create a proxy server with custom application logic
79 | //
80 | const proxyConfig = {
81 | agent: self.httpAgent,
82 | };
83 | if (timeout !== -1) {
84 | proxyConfig.proxyTimeout = timeout;
85 | }
86 | const proxy = httpProxy.createProxyServer(proxyConfig);
87 |
88 | proxy.on('error', (err, req, res) => {
89 | res.writeHead(500, {});
90 | const blob = JSON.stringify({
91 | message: err.message,
92 | stack: err.stack,
93 | });
94 | res.end(blob);
95 | debug(`proxy error: ${blob}`);
96 | });
97 |
98 |
99 | if (!bypassChromePool) {
100 | proxy.on('proxyRes', (proxyRes, req) => {
101 | if (req.method === 'DELETE' && req.url.length === 41) {
102 | const sessionId = req.url.substring(9);
103 | const port = self.activeSessions[sessionId];
104 | delete self.activeSessions[sessionId];
105 | self.chromepool.put(port);
106 | debug(`Delete session: ${sessionId} at port: ${port}`);
107 | }
108 | });
109 | }
110 |
111 | const chromedriverEndpoint = `http://127.0.0.1:${self.chromedriver.port}`;
112 |
113 | //
114 | // Create your custom server and just call `proxy.web()` to proxy
115 | // a web request to the target passed in the options
116 | // also you can use `proxy.ws()` to proxy a websockets request
117 | //
118 | self.server = http.createServer((req, res) => {
119 | if (timeout !== -1) {
120 | res.setTimeout(timeout, () => {
121 | res.write(JSON.stringify({ value: { error: 'TIMEOUT', message: 'proxy timeout', stacktrace: '' } }));
122 | res.end();
123 | });
124 | }
125 | if (self.baseUrl !== null) {
126 | req.url = req.url.substring(self.endBaseUrl);
127 | }
128 | if (bypassChromePool) {
129 | proxy.web(req, res, { target: chromedriverEndpoint });
130 | return;
131 | }
132 |
133 | const path = req.url.slice(42);
134 | if (req.url === '/session' && req.method === 'POST') {
135 | let body = '';
136 | req.on('data', (chunk) => {
137 | body += chunk;
138 | });
139 |
140 | req.on('end', () => {
141 | const caps = JSON.parse(body);
142 | let chromeOptions = {};
143 | if (caps.desiredCapabilities) {
144 | chromeOptions = caps.desiredCapabilities.chromeOptions || caps.desiredCapabilities['goog:chromeOptions'];
145 | } else if (caps.capabilities.alwaysMatch) {
146 | chromeOptions = caps.capabilities.alwaysMatch['goog:chromeOptions'];
147 | }
148 | self.chromepool.get({ args: chromeOptions.args })
149 | .then(port => self.chromepool.getAgent({ port }).then(() => port)).then((port) => {
150 | chromeOptions.debuggerAddress = `localhost:${port}`;
151 | if (caps.capabilities !== undefined && caps.capabilities.alwaysMatch !== undefined) {
152 | caps.capabilities.alwaysMatch['goog:chromeOptions'].debuggerAddress = chromeOptions.debuggerAddress;
153 | }
154 | const nbody = JSON.stringify(caps);
155 | debug(`capabilities: ${nbody}`);
156 | req.headers['Content-Length'] = Buffer.byteLength(nbody);
157 |
158 | const proxiedReq = http.request({
159 | port: 4445,
160 | method: req.method,
161 | path: req.url,
162 | headers: req.headers,
163 | agent: self.httpAgent,
164 | timeout: 2000,
165 | hostname: 'localhost',
166 | }, (resp) => {
167 | res.writeHead(resp.statusCode, resp.headers);
168 | let sessionBlob = '';
169 | resp.on('data', (chunk) => {
170 | sessionBlob += chunk;
171 | res.write(chunk);
172 | });
173 | resp.on('end', () => {
174 | const sessionInfo = JSON.parse(sessionBlob);
175 | if (sessionInfo.status === 0) {
176 | const { sessionId } = sessionInfo;
177 | self.activeSessions[sessionId] = port;
178 | debug(`Started session: ${sessionId} at port: ${port}`);
179 | } else if (sessionInfo.value) {
180 | const { sessionId } = sessionInfo.value;
181 | self.activeSessions[sessionId] = port;
182 | debug(`Started session: ${sessionId} at port: ${port}`);
183 | } else {
184 | debug(`Error: ${sessionBlob}`);
185 | // ahhhhhhhhhhhh something broke!!! ...
186 | }
187 | res.end();
188 | });
189 | });
190 |
191 | proxiedReq.on('error', (err) => {
192 | debug(err);
193 | res.writeHead(500);
194 | res.end();
195 | });
196 |
197 | proxiedReq.write(nbody);
198 | proxiedReq.end();
199 | }).catch((err) => {
200 | debug(err);
201 | res.writeHead(500);
202 | res.end(err);
203 | });
204 | });
205 | } else if (path.slice(0, 18) === 'chromedriver-proxy') {
206 | const sessionId = req.url.substring(9, 41);
207 | const body = [];
208 | const port = self.activeSessions[sessionId];
209 | req.on('data', (chunk) => { body.push(chunk); });
210 | req.on('end', () => {
211 | debug(`start proxy request to chrome agent port: ${port} path: ${req.url}`);
212 | self.chromepool.sendToAgent({
213 | action: 'req',
214 | port,
215 | value: {
216 | path: path.slice(19),
217 | sessionId,
218 | httpMethod: req.method,
219 | body: Buffer.concat(body).toString(),
220 | },
221 | }).then((result) => {
222 | res.write(JSON.stringify({
223 | status: 0,
224 | value: result,
225 | }));
226 | res.end();
227 | debug(`end proxy request to chrome agent port: ${port} path: ${req.url}`);
228 | }).catch((err) => {
229 | res.writeHead(500);
230 | res.end();
231 | debug(`error proxy request to chrome agent port: ${port} path: ${req.url} error: ${err}`);
232 | });
233 | });
234 | } else {
235 | // You can define here your custom logic to handle the request
236 | // and then proxy the request.
237 | proxy.web(req, res, { target: chromedriverEndpoint });
238 | }
239 | });
240 |
241 | self.server.keepAliveTimeout = keepAliveTimeout;
242 |
243 | self.server.listen(self.port, () => {
244 | chromedriverStart.then(() => {
245 | if (fn) {
246 | fn();
247 | }
248 | debug(`started proxy server at port: ${self.port}`);
249 | }).catch((err) => {
250 | console.error(`FATAL UNABLE TO START CHROMEDRIVER: ${err}`);
251 | process.exit(1);
252 | });
253 | });
254 | }
255 |
256 | stop(fn) {
257 | const self = this;
258 | debug('start shutdown');
259 | self.httpAgent.destroy();
260 | debug('stop proxy');
261 | self.server.close(() => {
262 | debug('server shutdown');
263 | self.chromedriver.stop().then(() => {
264 | debug('chromedriver shutdown');
265 | self.chromepool.killAll();
266 | if (fn) {
267 | fn();
268 | }
269 | });
270 | });
271 | }
272 | }
273 |
274 | module.exports = HttpServer;
275 |
--------------------------------------------------------------------------------
/lib/screen_recorder.js:
--------------------------------------------------------------------------------
1 |
2 |
3 | /**
4 | * Copyright (c) 2017 ZipRecruiter
5 | *
6 | * Permission is hereby granted, free of charge, to any person obtaining a copy
7 | * of this software and associated documentation files (the "Software"), to deal
8 | * in the Software without restriction, including without limitation the rights
9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | * copies of the Software, and to permit persons to whom the Software is
11 | * furnished to do so, subject to the following conditions:
12 | *
13 | * The above copyright notice and this permission notice shall be included in all
14 | * copies or substantial portions of the Software.
15 | *
16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 | * SOFTWARE.
23 | * */
24 |
25 | const CDP = require('chrome-remote-interface');
26 | const fs = require('fs');
27 | const cp = require('child_process');
28 | const util = require('util');
29 | const debug = require('debug')('chromedriver_proxy:screen_recorder');
30 | const debugFrame = require('debug')('chromedriver_proxy:screen_recorder_frame');
31 | const S3 = require('aws-sdk/clients/s3');
32 | const path = require('path');
33 |
34 | const writeFile = util.promisify(fs.writeFile);
35 | const rm = util.promisify(fs.unlink);
36 |
37 | const defaultVideoFormatArgs = {
38 | webm: [
39 | '-crf',
40 | '30',
41 | '-minrate',
42 | '500k',
43 | '-b:v',
44 | '2000k',
45 | '-c:v',
46 | 'libvpx-vp9',
47 | ],
48 | mp4: [
49 | '-crf',
50 | '30',
51 | '-minrate',
52 | '500k',
53 | '-maxrate',
54 | '2000k',
55 | '-c:v',
56 | 'libx264',
57 | '-pix_fmt',
58 | 'yuv420p',
59 | ],
60 | };
61 |
62 |
63 | function vttTime(timestamp) {
64 | let t = timestamp;
65 | if (t === 0) {
66 | return '00:00.000';
67 | }
68 | // convert minutes
69 | let minutes = '00';
70 | const actualMinutes = Math.floor(t / 60);
71 | if (actualMinutes > 10) {
72 | minutes = `${actualMinutes}`;
73 | } else if (actualMinutes >= 1) {
74 | minutes = `0${actualMinutes}`;
75 | }
76 | if (actualMinutes >= 1) {
77 | t -= (60 * actualMinutes);
78 | }
79 |
80 | let seconds = '00';
81 | // convert seconds
82 | if (t > 9) {
83 | seconds = `${Number.parseFloat(t).toFixed(3)}`;
84 | } else if (t > 0) {
85 | seconds = `0${Number.parseFloat(t).toFixed(3)}`;
86 | }
87 |
88 | return `${minutes}:${seconds}`;
89 | }
90 |
91 | class ScreenRecorder {
92 | constructor(options) {
93 | this.videoFormat = options.videoFormat || 'webm';
94 | this.extraArgs = options.extraArgs || defaultVideoFormatArgs[this.videoFormat] || [];
95 | this.tmpDir = options.tmpDir || '/tmp';
96 | this.ffmpegPath = options.ffmpegPath || '/usr/bin/ffmpeg';
97 | this.s3Options = options.s3 || {};
98 | this.s3UploadOptions = options.s3Upload || {};
99 | this.client = options.client;
100 | this.index = 0;
101 | this.subtitles = [];
102 | this.firstFrameTimestamp = null;
103 | this.currentDuration = 0;
104 | }
105 |
106 | static create(options) {
107 | return new Promise((resolve, reject) => {
108 | CDP({ host: '127.0.0.1', port: options.port, target: options.target }, (client) => {
109 | const screenRecorder = new ScreenRecorder({
110 | client,
111 | tmpDir: options.tmpDir,
112 | ffmpegPath: options.ffmpegPath,
113 | videoFormat: options.videoFormat,
114 | extraArgs: options.extraArgs,
115 | });
116 | const { Page } = client;
117 | Page.enable().then(() => {
118 | resolve(screenRecorder);
119 | });
120 | }).on('error', (err) => {
121 | debug('reject on connecting');
122 | reject(err);
123 | });
124 | });
125 | }
126 |
127 | start(options) {
128 | const self = this;
129 | const params = options.params || {};
130 | const format = params.format || 'png';
131 | self.s3Prefix = options.s3Prefix || '';
132 | self.sessionId = options.sessionId;
133 | self.s3FileName = options.s3FileName || `${self.sessionId}.${self.videoFormat}`;
134 | self.s3SubsFileName = options.s3SubtitleFileName || `${self.sessionId}.vtt`;
135 | self.dir = options.dir || self.tmpDir;
136 | const { Page } = self.client;
137 | self.frames = [];
138 |
139 | // register listener 1st
140 | self.client.on('Page.screencastFrame', (result) => {
141 | Page.screencastFrameAck({ sessionId: result.sessionId });
142 | const binaryBlob = Buffer.from(result.data, 'base64');
143 | const framePath = `${self.dir}/${self.sessionId}-${self.index}.${format}`;
144 | self.frames.push({ file: framePath, timestamp: result.metadata.timestamp });
145 | debugFrame(`screencast frame: ${framePath} frame id: ${result.sessionId} time: ${result.metadata.timestamp}`);
146 | if (self.firstFrameTimestamp === null) {
147 | self.firstFrameTimestamp = result.metadata.timestamp;
148 | }
149 | self.currentDuration = result.metadata.timestamp - self.firstFrameTimestamp;
150 | self.index += 1;
151 | fs.writeFile(framePath, binaryBlob, (err) => {
152 | if (err) {
153 | debug(`error unable to write screen frame to file: ${err}`);
154 | }
155 | });
156 | });
157 |
158 | self.client.on('Page.screencastVisibilityChanged', (visible) => {
159 | debug(`visiblility changed: ${JSON.stringify(visible)}`);
160 | });
161 |
162 | debug('start screencast');
163 | return Page.startScreencast(params).catch((err) => {
164 | debug(err);
165 | });
166 | }
167 |
168 | addSubtitle(options) {
169 | const self = this;
170 | const text = options.text || '';
171 | const subtitle = {
172 | text,
173 | timestamp: self.currentDuration,
174 | };
175 | debug(subtitle);
176 | self.subtitles.push(subtitle);
177 | }
178 |
179 | createSubtitle() {
180 | const self = this;
181 | if (!self.subtitles.length) {
182 | return null;
183 | }
184 |
185 | // create webvtt formatted subtitle string
186 | let b = 'WEBVTT\n\n';
187 | let lastTime = vttTime(self.subtitles[0].timestamp);
188 | let subtitle = self.subtitles[0];
189 |
190 | for (let i = 1; i < self.subtitles.length; i += 1) {
191 | const nextTime = vttTime(self.subtitles[i].timestamp);
192 | if (nextTime !== '00:00.000') {
193 | b += `${lastTime} --> ${nextTime}\n`;
194 | b += `${subtitle.text}\n\n`;
195 | }
196 | lastTime = nextTime;
197 | subtitle = self.subtitles[i];
198 | }
199 |
200 | b += `${lastTime} --> ${vttTime(self.currentDuration + 30)}\n`;
201 | b += `${subtitle.text}\n\n`;
202 |
203 | return {
204 | path: path.join(self.tmpDir, `${self.sessionId}-video.vtt`),
205 | blob: b,
206 | };
207 | }
208 |
209 | imagesToVideo(options) {
210 | const self = this;
211 | const ffmpegExe = '/usr/bin/ffmpeg';
212 | const args = [
213 | '-safe',
214 | '0',
215 | '-f',
216 | 'concat',
217 | '-i',
218 | options.input,
219 | ].concat(self.extraArgs);
220 | args.push(options.output);
221 |
222 | debug(`${ffmpegExe} ${args.join(' ')}`);
223 |
224 | const ffmpegChild = cp.spawn(ffmpegExe, args);
225 |
226 | return new Promise((resolve, reject) => {
227 | const errors = [];
228 | const ffmpegErrorListener = (data) => {
229 | errors.push(data);
230 | };
231 | ffmpegChild.stderr.addListener('data', ffmpegErrorListener);
232 | ffmpegChild.on('close', (code) => {
233 | ffmpegChild.stderr.removeListener('data', ffmpegErrorListener);
234 | if (code === 0) {
235 | resolve(0);
236 | } else {
237 | reject(errors.join('\n'));
238 | }
239 | });
240 | });
241 | }
242 |
243 | expectedResult() {
244 | const self = this;
245 | return {
246 | path: self.getFilePath(),
247 | s3: {
248 | bucket: self.s3UploadOptions.Bucket,
249 | key: self.getS3Key(),
250 | subtitlesKey: self.getS3SubsKey(),
251 | },
252 | };
253 | }
254 |
255 | getFilePath() {
256 | const self = this;
257 | return path.join(self.tmpDir, self.getFileName());
258 | }
259 |
260 | getFileName() {
261 | const self = this;
262 | return `${self.sessionId}.${self.videoFormat}`;
263 | }
264 |
265 | getS3Key() {
266 | const self = this;
267 | return `${self.s3Prefix || ''}${self.s3FileName}`;
268 | }
269 |
270 | getS3SubsKey() {
271 | const self = this;
272 | return `${self.s3Prefix || ''}${self.s3SubsFileName}`;
273 | }
274 |
275 | uploadToS3(options) {
276 | const self = this;
277 | if (!this.s3UploadOptions.Bucket) {
278 | debug('skipping s3 upload: Bucket has not been set');
279 | return Promise.resolve();
280 | }
281 |
282 | const { file } = options;
283 | const { s3Key } = options;
284 |
285 | return new Promise((resolve, reject) => {
286 | try {
287 | const key = s3Key;
288 | const ext = path.extname(file).slice(1);
289 |
290 | const s3 = new S3(self.s3Options);
291 | const stream = fs.createReadStream(file);
292 |
293 | const params = {
294 | ContentType: `video/${ext}`,
295 | Key: key,
296 | Body: stream,
297 | };
298 | Object.assign(params, self.s3UploadOptions);
299 |
300 | debug('start file upload');
301 | s3.upload(params, (err) => {
302 | if (err) {
303 | debug(`s3 upload error: ${err}`);
304 | reject(err);
305 | } else {
306 | resolve({ bucket: self.s3UploadOptions.Bucket, key });
307 | debug(`successfully uploaded ${key} to ${self.s3UploadOptions.Bucket}`);
308 | }
309 | });
310 | } catch (err) {
311 | debug(`s3 upload error: ${err}`);
312 | reject(err);
313 | }
314 | });
315 | }
316 |
317 | async stop() {
318 | const self = this;
319 | const { Page } = self.client;
320 | let output;
321 | try {
322 | debug('stop screencast');
323 | self.client.removeAllListeners('Page.screencastVisibilityChanged');
324 | self.client.removeAllListeners('Page.screencastFrame');
325 | await Page.stopScreencast();
326 | let b = `file '${self.frames[0].file}'\n`;
327 | let lasttimestamp = self.frames[0].timestamp;
328 | let lastfile;
329 |
330 | for (let i = 1; i < self.frames.length; i += 1) {
331 | const e = self.frames[i];
332 | b += `duration ${e.timestamp - lasttimestamp}\n`;
333 | b += `file '${e.file}'\n`;
334 | lasttimestamp = e.timestamp;
335 | lastfile = e.file;
336 | }
337 | // so we see the last frame
338 | b += 'duration 1\n';
339 | b += `file ${lastfile}\n`;
340 |
341 | const ffmpegFile = path.join(self.tmpDir, `${self.sessionId}-ffmpeg.txt`);
342 | await writeFile(ffmpegFile, b);
343 | debug(`ffmpeg config file: ${ffmpegFile}`);
344 | output = self.getFilePath();
345 | await self.imagesToVideo({
346 | input: ffmpegFile,
347 | output,
348 | });
349 | debug(`sucessfully created screencast: ${output}`);
350 | const subtitle = self.createSubtitle();
351 | self.s3UploadResult = await self.uploadToS3({ file: output, s3Key: self.getS3Key() })
352 | .catch(e => ({ error: e }));
353 | if (subtitle) {
354 | await writeFile(subtitle.path, subtitle.blob);
355 | await self.uploadToS3({ file: subtitle.path, s3Key: self.getS3SubsKey() })
356 | .catch(e => ({ error: e }));
357 | }
358 | const cleanup = [ffmpegFile];
359 | self.frames.forEach((frame) => {
360 | cleanup.push(rm(frame.file));
361 | });
362 | await Promise.all(cleanup);
363 | } catch (err) {
364 | debug(`error in screencast cleanup: ${err}`);
365 | return Promise.reject(err);
366 | }
367 | return output;
368 | }
369 | }
370 |
371 | module.exports = ScreenRecorder;
372 |
--------------------------------------------------------------------------------