├── .circleci └── config.yml ├── .editorconfig ├── .github └── FUNDING.yml ├── .gitignore ├── .npmignore ├── .prettierignore ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── aws ├── ec2-spot-instance-specification.json ├── iam-serverless-chrome-automation-role-policy.json ├── iam-serverless-chrome-automation-user-policy.json └── user-data.sh ├── chrome └── README.md ├── docs ├── automation.md ├── chrome.md └── circleci.md ├── examples └── serverless-framework │ └── aws │ ├── README.md │ ├── package.json │ ├── serverless.yml │ ├── src │ ├── chrome │ │ ├── pdf.js │ │ ├── pdf.test.js │ │ ├── screenshot.js │ │ ├── screenshot.test.js │ │ └── version.js │ ├── handlers │ │ ├── pdf.js │ │ ├── pdf.test.js │ │ ├── requestLogger.js │ │ ├── screencast.js │ │ ├── screenshot.js │ │ ├── screenshot.test.js │ │ └── version.js │ └── utils │ │ ├── log.js │ │ └── sleep.js │ ├── webpack.config.js │ └── yarn.lock ├── package.json ├── packages ├── lambda │ ├── README.md │ ├── builds │ │ ├── chromium │ │ │ ├── Dockerfile │ │ │ ├── README.md │ │ │ ├── build │ │ │ │ ├── .gclient │ │ │ │ ├── Dockerfile │ │ │ │ └── build.sh │ │ │ ├── latest.sh │ │ │ └── version.json │ │ ├── firefox │ │ │ └── README.md │ │ └── nss │ │ │ └── README.md │ ├── chrome │ │ ├── chrome-headless-lambda-linux-59.0.3039.0.tar.gz │ │ ├── chrome-headless-lambda-linux-60.0.3089.0.tar.gz │ │ ├── chrome-headless-lambda-linux-60.0.3095.0.zip │ │ ├── chrome-headless-lambda-linux-x64.zip │ │ └── headless-chromium-64.0.3242.2-amazonlinux-2017-03.zip │ ├── index.d.ts │ ├── integration-test │ │ ├── dist │ │ ├── handler.js │ │ ├── package.json │ │ ├── serverless.yml │ │ └── yarn.lock │ ├── package.json │ ├── rollup.config.js │ ├── scripts │ │ ├── latest-versions.sh │ │ ├── package-binaries.sh │ │ ├── postinstall.js │ │ └── test-integration.sh │ ├── src │ │ ├── flags.js │ │ ├── index.js │ │ ├── index.test.js │ │ ├── launcher.js │ │ └── utils.js │ └── yarn.lock └── serverless-plugin │ ├── README.md │ ├── integration-test │ ├── .serverless_plugins │ │ └── serverless-plugin-chrome │ ├── package.json │ ├── serverless.yml │ ├── src │ │ ├── anotherHandler.js │ │ ├── handler.js │ │ ├── noChrome.js │ │ └── typescript-handler.ts │ └── yarn.lock │ ├── package.json │ ├── rollup.config.js │ ├── scripts │ └── test-integration.sh │ ├── src │ ├── constants.js │ ├── index.js │ ├── utils.js │ ├── utils.test.js │ └── wrapper-aws-nodejs.js │ └── yarn.lock ├── scripts ├── ci-daily.sh ├── docker-build-image.sh ├── docker-image-exists.sh ├── docker-login.sh ├── docker-pull-image.sh ├── ec2-build.sh ├── link-package.sh ├── release.sh ├── sync-package-versions.sh └── update-browser-versions.sh └── yarn.lock /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | 3 | # 4 | # Jobs 5 | # 6 | 7 | jobs: 8 | 9 | # This job builds the base project directory (e.g. ~/package.json) 10 | build: 11 | docker: 12 | - image: circleci/node:12 13 | steps: 14 | - checkout 15 | - restore_cache: 16 | key: dependency-cache-{{ checksum "package.json" }} 17 | - run: npm install 18 | - save_cache: 19 | key: dependency-cache-{{ checksum "package.json" }} 20 | paths: 21 | - node_modules 22 | 23 | # This job runs the lint tool on the whole repository 24 | lint: 25 | docker: 26 | - image: circleci/node:12 27 | steps: 28 | - checkout 29 | - restore_cache: 30 | key: dependency-cache-{{ checksum "package.json" }} 31 | - restore_cache: 32 | key: dependency-cache-{{ checksum "packages/lambda/package.json" }} 33 | - restore_cache: 34 | key: dependency-cache-{{ checksum "packages/lambda/package.json" }}-{{ checksum "packages/lambda/integration-test/package.json" }} 35 | - restore_cache: 36 | key: dependency-cache-{{ checksum "packages/serverless-plugin/package.json" }}-{{ checksum "packages/serverless-plugin/integration-test/package.json" }} 37 | - restore_cache: 38 | key: dependency-cache-{{ checksum "examples/serverless-framework/aws/package.json" }} 39 | - restore_cache: 40 | key: build-cache-{{ .Revision }}-package-lambda 41 | # need to set permissions on the npm prefix so that we can npm link packages 42 | - run: sudo chown -R $(whoami) $(npm config get prefix)/{lib/node_modules,bin,share} 43 | - run: ./scripts/link-package.sh packages/serverless-plugin lambda 44 | - run: npm run lint 45 | 46 | # This job runs all of the unit tests in the repository 47 | unit_test: 48 | docker: 49 | # Node 8 so we can avoid transpiling our tests 50 | - image: circleci/node:12 51 | # The unit tests require DevTools on localhost:9222 52 | - image: adieuadieu/headless-chromium-for-aws-lambda:stable 53 | steps: 54 | - checkout 55 | - restore_cache: 56 | key: dependency-cache-{{ checksum "package.json" }} 57 | - restore_cache: 58 | key: dependency-cache-{{ checksum "packages/lambda/package.json" }} 59 | - restore_cache: 60 | key: dependency-cache-{{ checksum "packages/serverless-plugin/package.json" }}-{{ checksum "packages/serverless-plugin/integration-test/package.json" }} 61 | - restore_cache: 62 | key: dependency-cache-{{ checksum "examples/serverless-framework/aws/package.json" }} 63 | - run: 64 | name: Install Chromium for local development Launcher test 65 | command: sudo apt-get install -y chromium 66 | - run: npm run ava 67 | 68 | # This job builds the @serverless-chrome/lambda package 69 | build_lambda: 70 | # use a machine because we extract binaries from Docker images 71 | machine: true 72 | steps: 73 | - checkout 74 | - restore_cache: 75 | key: dependency-cache-{{ checksum "package.json" }} 76 | - restore_cache: 77 | key: dependency-cache-{{ checksum "packages/lambda/package.json" }} 78 | - run: 79 | name: Add chromium binary if missing 80 | command: | 81 | cd packages/lambda 82 | if [ ! -f "dist/headless-chromium" ]; then 83 | ./scripts/package-binaries.sh chromium stable 84 | cp dist/stable-headless-chromium dist/headless-chromium 85 | fi 86 | - run: cd packages/lambda && npm install 87 | - save_cache: 88 | key: dependency-cache-{{ checksum "packages/lambda/package.json" }} 89 | paths: 90 | - packages/lambda/node_modules 91 | - packages/lambda/dist/headless-chromium 92 | - packages/lambda/dist/stable-headless-chromium 93 | - run: cd packages/lambda && npm run build 94 | - save_cache: 95 | key: build-cache-{{ .Revision }}-package-lambda 96 | paths: 97 | - packages/lambda/dist 98 | - run: cd packages/lambda/integration-test && npm install 99 | - save_cache: 100 | key: dependency-cache-{{ checksum "packages/lambda/package.json" }}-{{ checksum "packages/lambda/integration-test/package.json" }} 101 | paths: 102 | - packages/lambda/node_modules 103 | - packages/lambda/integration-test/node_modules 104 | 105 | # This job runs the @serverless-chrome/lambda package's integration tests 106 | integration_test_lambda: 107 | # use a machine because we run the integration tests with Docker (lambci/lambda) 108 | machine: true 109 | steps: 110 | - checkout 111 | - restore_cache: 112 | key: dependency-cache-{{ checksum "package.json" }} 113 | - restore_cache: 114 | key: dependency-cache-{{ checksum "packages/lambda/package.json" }} 115 | - restore_cache: 116 | key: dependency-cache-{{ checksum "packages/lambda/package.json" }}-{{ checksum "packages/lambda/integration-test/package.json" }} 117 | - restore_cache: 118 | key: build-cache-{{ .Revision }}-package-lambda 119 | - run: 120 | name: Integration test 121 | command: cd packages/lambda && npm test 122 | 123 | # This job builds the serverless-plugin package 124 | build_serverless_plugin: 125 | docker: 126 | - image: circleci/node:12 127 | steps: 128 | - checkout 129 | - restore_cache: 130 | key: dependency-cache-{{ checksum "package.json" }} 131 | - restore_cache: 132 | key: dependency-cache-{{ checksum "packages/serverless-plugin/package.json" }}-{{ checksum "packages/serverless-plugin/integration-test/package.json" }} 133 | - restore_cache: 134 | key: build-cache-{{ .Revision }}-package-lambda 135 | # need to set permissions on the npm prefix so that we can npm link packages 136 | - run: sudo chown -R $(whoami) $(npm config get prefix)/{lib/node_modules,bin,share} 137 | - run: ./scripts/link-package.sh packages/serverless-plugin lambda 138 | - run: cd packages/serverless-plugin && npm install 139 | - run: cd packages/serverless-plugin && npm run build 140 | - save_cache: 141 | key: build-cache-{{ .Revision }}-package-serverless-plugin 142 | paths: 143 | - packages/serverless-plugin/dist 144 | - run: | 145 | [ -z "$CIRCLE_TAG" ] || ./scripts/link-package.sh packages/serverless-plugin/integration-test lambda 146 | - run: cd packages/serverless-plugin/integration-test && npm install 147 | - save_cache: 148 | key: dependency-cache-{{ checksum "packages/serverless-plugin/package.json" }}-{{ checksum "packages/serverless-plugin/integration-test/package.json" }} 149 | paths: 150 | - packages/serverless-plugin/node_modules 151 | - packages/serverless-plugin/integration-test/node_modules 152 | 153 | # This job runs the serverless-plugin package's integration tests 154 | integration_test_serverless_plugin: 155 | # use a machine because we run the integration tests with Docker (lambci/lambda) 156 | machine: true 157 | steps: 158 | - checkout 159 | - restore_cache: 160 | key: dependency-cache-{{ checksum "package.json" }} 161 | - restore_cache: 162 | key: dependency-cache-{{ checksum "packages/serverless-plugin/package.json" }}-{{ checksum "packages/serverless-plugin/integration-test/package.json" }} 163 | - restore_cache: 164 | key: build-cache-{{ .Revision }}-package-lambda 165 | - run: 166 | name: Integration test 167 | command: cd packages/serverless-plugin && npm test 168 | 169 | # This job builds the serverless-framework AWS example service 170 | build_serverless_example: 171 | docker: 172 | - image: circleci/node:12 173 | steps: 174 | - checkout 175 | - restore_cache: 176 | key: dependency-cache-{{ checksum "package.json" }} 177 | - restore_cache: 178 | key: dependency-cache-{{ checksum "examples/serverless-framework/aws/package.json" }} 179 | - restore_cache: 180 | key: build-cache-{{ .Revision }}-package-lambda 181 | - restore_cache: 182 | key: build-cache-{{ .Revision }}-package-serverless-plugin 183 | # need to set permissions on the npm prefix so that we can npm link packages 184 | - run: sudo chown -R $(whoami) $(npm config get prefix)/{lib/node_modules,bin,share} 185 | - run: ./scripts/link-package.sh packages/serverless-plugin lambda 186 | - run: ./scripts/link-package.sh examples/serverless-framework/aws serverless-plugin 187 | - run: cd examples/serverless-framework/aws && npm install 188 | - save_cache: 189 | key: dependency-cache-{{ checksum "examples/serverless-framework/aws/package.json" }} 190 | paths: 191 | - examples/serverless-framework/aws/node_modules 192 | 193 | # This job checks for new versions of browsers (chromium) 194 | # and updates the repository code when new docker images have been built 195 | update_browser_versions: 196 | docker: 197 | - image: circleci/node:12 198 | branch: 199 | - master 200 | steps: 201 | - checkout 202 | - run: scripts/update-browser-versions.sh 203 | 204 | # This job handles release automation. Usually run when new binaries of 205 | # browsers have been built. Only stable-channel binaries will trigger a git tag 206 | # from which we make releases. 207 | release: 208 | # use a machine because we run Docker containers while preparing the release 209 | machine: 210 | image: ubuntu-2004:202010-01 211 | branch: 212 | - master 213 | steps: 214 | - checkout 215 | - restore_cache: 216 | key: dependency-cache-{{ checksum "package.json" }} 217 | - restore_cache: 218 | key: dependency-cache-{{ checksum "packages/lambda/package.json" }} 219 | - restore_cache: 220 | key: dependency-cache-{{ checksum "packages/serverless-plugin/package.json" }}-{{ checksum "packages/serverless-plugin/integration-test/package.json" }} 221 | # we need a newer version of NPM than is available by default, e.g. >=4 222 | # because we make use of the "prepublishOnly" package.json script, which was introduces in npm@4 223 | # - run: 224 | # name: Update NPM 225 | # command: npm install -g npm@latest 226 | - run: scripts/release.sh 227 | 228 | # This step triggers builds of binaries when new versions are available 229 | # The builds happen on AWS Spot Instances created by this step and are not run 230 | # on CircleCI due to instance-size/build-time constraints there. 231 | # AWS Spot Instances are configured by ~/aws/ec2-spot-instance-specification.json. 232 | # On start up, the spot instances run the user-data script in ~/aws/user-data.sh. 233 | # Using "amazonlinux:latest" image because it's easy to install aws-cli. 234 | build_new_binaries: 235 | docker: 236 | - image: amazonlinux:latest 237 | branch: 238 | - master 239 | steps: 240 | - checkout 241 | - run: 242 | name: Install AWS CLI & JQ 243 | command: yum install -y aws-cli jq 244 | - run: 245 | name: Build stable-channel Chromium 246 | command: scripts/ci-daily.sh stable chromium 247 | - run: 248 | name: Build beta-channel Chromium 249 | command: scripts/ci-daily.sh beta chromium 250 | - run: 251 | name: Build dev-channel Chromium 252 | command: scripts/ci-daily.sh dev chromium 253 | 254 | 255 | # 256 | # Workflows 257 | # 258 | 259 | workflows: 260 | version: 2 261 | 262 | # Runs on every commit. The jobs install and build dependencies 263 | # and also setup test environments and prerequisites for integration tests 264 | # On tagged commits on master branch, the "release" job automates publishing 265 | # of NPM packages and making a GitHub release. The release and npm packages are published 266 | # by a bot account (botsimo). 267 | build_test_release: 268 | jobs: 269 | - build 270 | 271 | - build_lambda: 272 | requires: 273 | - build 274 | - build_serverless_plugin: 275 | requires: 276 | - build_lambda 277 | - build_serverless_example: 278 | requires: 279 | - build_serverless_plugin 280 | - lint: 281 | requires: 282 | - build 283 | - build_lambda 284 | - build_serverless_plugin 285 | - build_serverless_example 286 | - unit_test: 287 | requires: 288 | - build 289 | - build_lambda 290 | - build_serverless_plugin 291 | - build_serverless_example 292 | - integration_test_lambda: 293 | requires: 294 | - build_lambda 295 | - integration_test_serverless_plugin: 296 | requires: 297 | - build_serverless_plugin 298 | - release: 299 | filters: 300 | branches: 301 | only: master 302 | requires: 303 | - lint 304 | - unit_test 305 | - integration_test_lambda 306 | - integration_test_serverless_plugin 307 | 308 | # Runs daily at 08:00 UTC. The job checks for new versions of 309 | # headless browsers (chromium) and creates an AWS spot-instance-request 310 | # on which to compile/build any new versions. 311 | # Takes about 2h10m for Chromium 64+ on c5.2xlarge 312 | daily_build: 313 | triggers: 314 | - schedule: 315 | cron: "0 8 * * *" 316 | filters: 317 | branches: 318 | only: 319 | - master 320 | jobs: 321 | - build_new_binaries 322 | 323 | # Runs daily at 11:00 UTC. The job checks if any new versions of 324 | # headless browsers (chromium) have been built and updates the repository 325 | # code to point at any new versions. A new stable-channel version will 326 | # trigger an automated release in the "release" job in the build_test_release workflow 327 | daily_version_update: 328 | triggers: 329 | - schedule: 330 | cron: "0 11 * * *" 331 | filters: 332 | branches: 333 | only: 334 | - master 335 | jobs: 336 | - update_browser_versions 337 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | [*] 2 | indent_style = space 3 | end_of_line = lf 4 | indent_size = 2 5 | charset = utf-8 6 | trim_trailing_whitespace = true 7 | 8 | [*.md] 9 | max_line_length = 0 10 | trim_trailing_whitespace = false 11 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: adieuadieu # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | custom: # Replace with a single custom sponsorship URL 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | 11 | # Directory for instrumented libs generated by jscoverage/JSCover 12 | lib-cov 13 | 14 | # Coverage directory used by tools like istanbul 15 | coverage 16 | 17 | # nyc test coverage 18 | .nyc_output 19 | 20 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 21 | .grunt 22 | 23 | # node-waf configuration 24 | .lock-wscript 25 | 26 | # Compiled binary addons (http://nodejs.org/api/addons.html) 27 | build/Release 28 | 29 | # Dependency directories 30 | node_modules 31 | jspm_packages 32 | 33 | # Optional npm cache directory 34 | .npm 35 | 36 | # Optional REPL history 37 | .node_repl_history 38 | 39 | # package directories 40 | node_modules 41 | jspm_packages 42 | 43 | # Serverless directories 44 | .serverless 45 | 46 | 47 | *.log 48 | .DS_Store 49 | .nyc_output 50 | .serverless 51 | .webpack 52 | coverage 53 | node_modules 54 | .idea/ 55 | dist/ 56 | *.iml 57 | webpack-assets.json 58 | webpack-stats.json 59 | npm-debug.log 60 | awsconfig.json 61 | npm-debug.log 62 | dump/ 63 | config.json 64 | /config.js 65 | event.json 66 | .build/ 67 | headless-chromium-amazonlinux-2017-03.zip 68 | .eslintcache 69 | packages/lambda/integration-test/headless-chromium 70 | 71 | .env 72 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | # package directories 2 | node_modules 3 | jspm_packages 4 | 5 | # Serverless directories 6 | .serverless -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | .nyc_output/ 2 | dist/ 3 | package.json 4 | package-lock.json 5 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | All notable changes to this project will be documented in this file. 3 | 4 | The format is based on [Keep a Changelog](http://keepachangelog.com/) 5 | and this project adheres to [Semantic Versioning](http://semver.org/). 6 | 7 | 8 | ## [Unreleased] 9 | ### Added 10 | - Binary support in AWS Lambda/API Gateway example 11 | - Build and release tooling shell scripts and Dockerfile's 12 | - Integration tests and CircleCI setup 13 | - Complete automation of build/test/release workflows 14 | - serverless-plugin-chrome: support for limiting Chrome to only select service functions with the `custom.chrome.functions` parameter. 15 | - @serverless-chrome/lambda NPM package 16 | - serverless-plugin-chrome NPM package for Serverless-framework 17 | - Lots of new and updated documentation 18 | - CHANGELOG.md. 19 | 20 | ### Changed 21 | - example Serverless-framework printToPdf function handler to use the Serverless plugin 22 | - example Serverless-framework captureScreenshot function handler to use the Serverless plugin 23 | 24 | 25 | ## [0.5.0] - 2017-03-11, 2017-05-09 26 | ### Added 27 | - Headless Chrome headless_shell binary version 60.0.3089.0 built for AWS Lambda 28 | - Serverless-framework configuration for deploying to AWS Lambda 29 | - sample printToPdf Lambda function handler 30 | - sample captureScreenshot Lambda function handler 31 | - Initial documentation in README.md 32 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, gender identity and expression, level of experience, 9 | nationality, personal appearance, race, religion, or sexual identity and 10 | orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project owner, Marco Lüthy. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at [http://contributor-covenant.org/version/1/4][version] 72 | 73 | [homepage]: http://contributor-covenant.org 74 | [version]: http://contributor-covenant.org/version/1/4/ 75 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to this project 2 | 3 | [fork]: https://github.com/adieuadieu/serverless-chrome/fork 4 | [pr]: https://github.com/adieuadieu/serverless-chrome/compare 5 | [code-of-conduct]: CODE_OF_CONDUCT.md 6 | 7 | Hi there! We're thrilled that you'd like to contribute to this project. Your help is essential for keeping it great. 8 | 9 | Please note that this project is released with a [Contributor Code of Conduct][code-of-conduct]. By participating in this project you agree to abide by its terms. 10 | 11 | ## Contribution Agreement 12 | 13 | As a contributor, you represent that the code you submit is your original work or that of your employer (in which case you represent you have the right to bind your employer). By submitting code, you (and, if applicable, your employer) are licensing the submitted code to the open source community subject to the MIT license. 14 | 15 | 16 | ## Submitting a pull request 17 | 18 | Please branch from and raise PRs against the `develop` branch. 19 | 20 | 0. [Fork][fork] and clone the repository 21 | 0. Create a new branch: `git checkout -b feature/my-new-feature-name develop` 22 | 0. Make your change 23 | 0. Run the unit tests and make sure they pass and have 100% coverage. 24 | 0. Push to your fork and [submit a pull request][pr] to merge your changes. 25 | 0. Pat your self on the back and wait for your pull request to be reviewed and merged. 26 | 27 | Here are a few things you can do that will increase the likelihood of your pull request being accepted: 28 | 29 | - Keep your change as focused as possible. If there are multiple changes you would like to make that are not dependent upon each other, please submit them as separate pull requests. 30 | - Write a [good commit message](http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html). 31 | - In your pull request description, provide as much detail as possible. This context helps the reviewer to understand the motivation for and impact of the change. 32 | - Make sure that all the unit tests still pass. PRs with failing tests won't be merged. 33 | 34 | ## Resources 35 | 36 | - [How to Contribute to Open Source](https://opensource.guide/how-to-contribute/) 37 | - [Contributing to Open Source on GitHub](https://guides.github.com/activities/contributing-to-open-source/) 38 | - [Using Pull Requests](https://help.github.com/articles/about-pull-requests/) 39 | - [GitHub Help](https://help.github.com) 40 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017-2018 Marco Lüthy 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # serverless-chrome 2 | 3 | Serverless Chrome contains everything you need to get started running headless 4 | Chrome on AWS Lambda (possibly Azure and GCP Functions soon). 5 | 6 | The aim of this project is to provide the scaffolding for using Headless Chrome 7 | during a serverless function invocation. Serverless Chrome takes care of 8 | building and bundling the Chrome binaries and making sure Chrome is running when 9 | your serverless function executes. In addition, this project also provides a few 10 | example services for common patterns (e.g. taking a screenshot of a page, 11 | printing to PDF, some scraping, etc.) 12 | 13 | Why? Because it's neat. It also opens up interesting possibilities for using the 14 | [Chrome DevTools Protocol](https://chromedevtools.github.io/devtools-protocol/tot/) 15 | (and tools like [Chromeless](https://github.com/graphcool/chromeless) or 16 | [Puppeteer](https://github.com/GoogleChrome/puppeteer)) in serverless 17 | architectures and doing testing/CI, web-scraping, pre-rendering, etc. 18 | 19 | [](https://circleci.com/gh/adieuadieu/serverless-chrome) 20 | []() 21 | []() 22 | [](https://github.com/adieuadieu/serverless-chrome) 23 | 24 | ## Contents 25 | 26 | 1. [Quick Start](#quick-start) 27 | 1. [The Project](#the-project) 28 | 1. [Examples](#examples) 29 | 1. [Documentation & Resources](#documentation--resources) 30 | 1. [Building Headless Chrome/Chromium](#building-headless-chromechromium) 31 | 1. [Testing](#testing) 32 | 1. [Articles & Tutorials](#articles--tutorials) 33 | 1. [Troubleshooting](#troubleshooting) 34 | 1. [Roadmap](#roadmap) 35 | 1. [Projects & Companies using serverless-chrome](#projects--companies-using-serverless-chrome) 36 | 1. [Change log](#change-log) 37 | 1. [Contributing](#contributing) 38 | 1. [Prior Art](#prior-art) 39 | 1. [License](#license) 40 | 41 | ## Quick Start 42 | 43 | "Bla bla bla! I just want to start coding!" No problem: 44 | 45 | Using AWS Lambda, the quickest way to get started is with the 46 | [Serverless-framework](https://serverless.com/) CLI. 47 | 48 | First, install `serverless` globally (`npm install -g serverless`) and then: 49 | 50 | ```bash 51 | serverless create -u https://github.com/adieuadieu/serverless-chrome/tree/master/examples/serverless-framework/aws 52 | ``` 53 | 54 | Then, you must configure your AWS credentials either by defining 55 | `AWS_ACCESS_KEY_ID` and `AWS_SECRET_ACCESS_KEY` environmental variables, or 56 | using an AWS profile. You can read more about this on the 57 | [Serverless Credentials Guide](https://serverless.com/framework/docs/providers/aws/guide/credentials/). 58 | 59 | In short, either: 60 | 61 | ```bash 62 | export AWS_PROFILE=<your-profile-name> 63 | ``` 64 | 65 | or 66 | 67 | ```bash 68 | export AWS_ACCESS_KEY_ID=<your-key-here> 69 | export AWS_SECRET_ACCESS_KEY=<your-secret-key-here> 70 | ``` 71 | 72 | Then, to deploy the service and all of its functions: 73 | 74 | ```bash 75 | npm run deploy 76 | ``` 77 | 78 | Further details are available in the 79 | [Serverless Lambda example](examples/serverless-framework/aws). 80 | 81 | ## The Project 82 | 83 | This project contains: 84 | 85 | * **[@serverless-chrome/lambda](packages/lambda)** NPM package<br/> A standalone 86 | module for AWS Lambda which bundles and launches Headless Chrome with support 87 | for local development. For use with—but not limited to—tools like 88 | [Apex](https://github.com/apex/apex), 89 | [Claudia.js](https://github.com/claudiajs/claudia), 90 | [SAM Local](https://github.com/awslabs/aws-sam-local), or 91 | [Serverless](https://serverless.com/). 92 | * **[serverless-plugin-chrome](packages/serverless-plugin)** NPM package<br/> A 93 | plugin for [Serverless-framework](https://serverless.com/) services which 94 | takes care of everything for you. You just write the code to drive Chrome. 95 | * **[Example functions](examples/)** 96 | * [Serverless-framework](https://serverless.com/) AWS Lambda Node.js functions 97 | using `serverless-plugin-chrome` 98 | * **[Build Automation](docs/automation.md) & 99 | [CI/CD](.circleci/config.yml)**<br/> Build and release tooling shell scripts 100 | and Dockerfile for automating the build/release of headless Chrome for 101 | serverless environments (AWS Lambda). 102 | 103 | ## Examples 104 | 105 | A collection of example functions for different providers and frameworks. 106 | 107 | ### Serverless-framework 108 | 109 | * [Serverless-framework](examples/serverless-framework/aws) Some simple 110 | functions for the [Serverless-framework](https://serverless.com/) on AWS 111 | Lambda. It includes the following example functions: 112 | 113 | * Print to PDF 114 | * Capture Screenshot 115 | * Page-load Request Logger 116 | 117 | ## Documentation & Resources 118 | 119 | ### Building Headless Chrome/Chromium 120 | 121 | * Automated, regularly prebuilt binaries can be found on the 122 | [Releases](https://github.com/adieuadieu/serverless-chrome/releases) page 😎 123 | * [adieuadieu/headless-chromium-for-aws-lambda](https://hub.docker.com/r/adieuadieu/headless-chromium-for-aws-lambda/) 124 | Docker image 125 | * [Documentation on building your own binaries](/docs/chrome.md) 126 | * [Medium article on how to do build from scratch](https://medium.com/@marco.luethy/running-headless-chrome-on-aws-lambda-fa82ad33a9eb). 127 | This was the origin of this project. 128 | 129 | ## Testing 130 | 131 | Test with `npm test`. Each package also contains it's own integration tests 132 | which can be run with `npm run test:integration`. 133 | 134 | ## Articles & Tutorials 135 | 136 | A collection of articles and tutorials written by others on using serverless-chrome 137 | 138 | * [AWS DevOps Blog — UI Testing at Scale with AWS Lambda](https://aws.amazon.com/blogs/devops/ui-testing-at-scale-with-aws-lambda/) 139 | * [Running puppeteer and headless chrome on AWS lambda with Serverless](https://nadeeshacabral.com/writing/2018/running-puppeteer-and-headless-chrome-on-aws-lambda-with-serverless) 140 | * [Will it blend? Or how to run Google Chrome in AWS Lambda](https://medium.freecodecamp.org/will-it-blend-or-how-to-run-google-chrome-in-aws-lambda-2c960fee8b74) 141 | * [Running Selenium and Headless Chrome on AWS Lambda](https://medium.com/clog/running-selenium-and-headless-chrome-on-aws-lambda-fb350458e4df) 142 | * [AWS Lambda上のheadless chromeをPythonで動かす](https://qiita.com/nabehide/items/754eb7b7e9fff9a1047d) 143 | * [AWS Lambda上でpuppeteerを動かして、スクレイピングする](https://qiita.com/chimame/items/04c9b45d8467cf32892f) 144 | * [serverless-chrome で日本語を表示できるようにする](http://fd0.hatenablog.jp/entry/2017/09/10/223042) 145 | 146 | ## Troubleshooting 147 | 148 | <details id="troubleshooting-1"> 149 | <summary>Can't get Selenium / ChromeDriver to work</summary> 150 | Make sure that the versions of serverless-chrome, chromedriver, and Selenium are compatible. More details in [#133](https://github.com/adieuadieu/serverless-chrome/issues/133#issuecomment-382743975). 151 | </details> 152 | 153 | ## Roadmap 154 | 155 | _1.1_ 156 | 157 | 1. Support for Google Cloud Functions 158 | 1. Example for Apex 159 | 1. Example for Claudia.js 160 | 161 | _1.2_ 162 | 163 | 1. DOM manipulation and scraping example handler 164 | 165 | _Future_ 166 | 167 | 1. Support for Azure Functions 168 | 1. Headless Firefox 169 | 170 | ## Projects & Companies using serverless-chrome 171 | 172 | Tell us about your project on the 173 | [Wiki](https://github.com/adieuadieu/serverless-chrome/wiki/Projects-&-Companies-Using-serverless-chrome)! 174 | 175 | ## Change log 176 | 177 | See the [CHANGELOG](CHANGELOG.md) 178 | 179 | ## Contributing 180 | 181 | OMG. Yes. Plz, [halp meeee](/CONTRIBUTING.md). 182 | 183 | ## Prior Art 184 | 185 | This project was inspired in various ways by the following projects: 186 | 187 | * [PhantomJS](http://phantomjs.org/) 188 | * [wkhtmltopdf](https://github.com/wkhtmltopdf/wkhtmltopdf) 189 | * [node-webkitgtk](https://github.com/kapouer/node-webkitgtk) 190 | * [electron-pdf](https://github.com/Janpot/electron-pdf) 191 | 192 | ## License 193 | 194 | **serverless-chrome** © [Marco Lüthy](https://github.com/adieuadieu). Released under the [MIT](./LICENSE) license.<br> 195 | Authored and maintained by Marco Lüthy with help from [contributors](https://github.com/adieuadieu/serverless-chrome/contributors). 196 | 197 | > [github.com/adieuadieu](https://github.com/adieuadieu) · GitHub [@adieuadieu](https://github.com/adieuadieu) · Twitter [@adieuadieu](https://twitter.com/adieuadieu) · Medium [@marco.luethy](https://medium.com/@marco.luethy) 198 | -------------------------------------------------------------------------------- /aws/ec2-spot-instance-specification.json: -------------------------------------------------------------------------------- 1 | { 2 | "SpotPrice": "0.34", 3 | "InstanceCount": 1, 4 | "Type": "one-time", 5 | "LaunchSpecification": { 6 | "ImageId": "ami-8c1be5f6", 7 | "InstanceType": "c4.2xlarge", 8 | "IamInstanceProfile": { 9 | "Arn": "this gets updated by the ec2-build.sh script" 10 | }, 11 | "BlockDeviceMappings": [ 12 | { 13 | "DeviceName": "/dev/xvda", 14 | "Ebs": { 15 | "DeleteOnTermination": true, 16 | "VolumeType": "gp2", 17 | "VolumeSize": 64, 18 | "SnapshotId": "snap-080eb3cb2eda29974" 19 | } 20 | } 21 | ], 22 | "UserData": "" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /aws/iam-serverless-chrome-automation-role-policy.json: -------------------------------------------------------------------------------- 1 | { 2 | "Version": "2012-10-17", 3 | "Statement": [ 4 | { 5 | "Effect": "Allow", 6 | "Action": [ 7 | "ssm:GetParameter", 8 | "ssm:GetParameters" 9 | ], 10 | "Resource": [ 11 | "arn:aws:ssm:*:*:parameter/serverless-chrome-automation/*" 12 | ] 13 | }, 14 | { 15 | "Effect": "Allow", 16 | "Action": [ 17 | "logs:CreateLogGroup", 18 | "logs:CreateLogStream", 19 | "logs:PutLogEvents", 20 | "logs:DescribeLogStreams" 21 | ], 22 | "Resource": [ 23 | "arn:aws:logs:*:*:log-group:/serverless-chrome-automation:log-stream:*" 24 | ] 25 | }, 26 | { 27 | "Effect": "Allow", 28 | "Action": [ 29 | "s3:GetObject", 30 | "s3:PutObject" 31 | ], 32 | "Resource": [ 33 | "arn:aws:s3:::serverless-chrome-binaries/*" 34 | ] 35 | } 36 | ] 37 | } 38 | -------------------------------------------------------------------------------- /aws/iam-serverless-chrome-automation-user-policy.json: -------------------------------------------------------------------------------- 1 | { 2 | "Version": "2012-10-17", 3 | "Statement": [ 4 | { 5 | "Sid": "Stmt1510407541000", 6 | "Effect": "Allow", 7 | "Action": [ 8 | "ec2:RequestSpotInstances", 9 | "iam:PassRole" 10 | ], 11 | "Resource": [ 12 | "*" 13 | ] 14 | } 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /aws/user-data.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # shellcheck shell=dash 3 | 4 | # 5 | # Usage: Run as a start-up script on an EC2 instance via user-data cloud-init 6 | # ref: http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/user-data.html 7 | # 8 | 9 | # These get replaced with real values in ~/scripts/ec2-build.sh 10 | BROWSER=INSERT_BROWSER_HERE 11 | CHANNEL=INSERT_CHANNEL_HERE 12 | VERSION=INSERT_VERSION_HERE 13 | DOCKER_ORG=INSERT_DOCKER_ORG_HERE 14 | S3_BUCKET=INSERT_S3_BUCKET_HERE 15 | FORCE_NEW_BUILD=INSERT_FORCE_NEW_BUILD_HERE 16 | 17 | echo "Starting user-data script. $BROWSER $VERSION ($CHANNEL channel)" 18 | 19 | # 20 | # Setup CloudWatch logging 21 | # ref: http://docs.aws.amazon.com/AmazonCloudWatch/latest/logs/QuickStartEC2Instance.html 22 | # 23 | yum install -y --quiet awslogs 24 | 25 | # config ref: http://docs.aws.amazon.com/AmazonCloudWatch/latest/logs/AgentReference.html 26 | printf " 27 | [cloudinit] 28 | log_group_name = /serverless-chrome-automation 29 | log_stream_name = {instance_id}-cloudinit-%s-%s-%s 30 | file = /var/log/cloud-init-output.log 31 | " \ 32 | "$BROWSER" "$VERSION" "$CHANNEL" >> /etc/awslogs/awslogs.conf 33 | 34 | service awslogs start 35 | 36 | # 37 | # Go time (if brower and release channel are set.) 38 | # 39 | if [ -n "$CHANNEL" ] && [ -n "$BROWSER" ]; then 40 | yum update -y --quiet 41 | 42 | yum install -y --quiet docker jq git 43 | 44 | service docker start 45 | 46 | # EC2_INSTANCE_ID=$(curl -s http://instance-data/latest/meta-data/instance-id) 47 | 48 | AWS_REGION=$(curl -s http://instance-data/latest/dynamic/instance-identity/document | \ 49 | jq -r ".region" \ 50 | ) 51 | 52 | DOCKER_USER=$(aws ssm get-parameter \ 53 | --region "$AWS_REGION" \ 54 | --with-decryption \ 55 | --name /serverless-chrome-automation/DOCKER_USER | \ 56 | jq -r ".Parameter.Value" \ 57 | ) 58 | 59 | DOCKER_PASS=$(aws ssm get-parameter \ 60 | --region "$AWS_REGION" \ 61 | --with-decryption \ 62 | --name /serverless-chrome-automation/DOCKER_PASS | \ 63 | jq -r ".Parameter.Value" \ 64 | ) 65 | 66 | export AWS_REGION 67 | export DOCKER_USER 68 | export DOCKER_PASS 69 | export DOCKER_ORG 70 | export S3_BUCKET 71 | export FORCE_NEW_BUILD 72 | 73 | git clone "https://github.com/adieuadieu/serverless-chrome.git" 74 | 75 | cd serverless-chrome || return 76 | 77 | # git checkout develop # in case you want to build develop branch 78 | 79 | ./scripts/docker-build-image.sh "$CHANNEL" "$BROWSER" "$VERSION" 80 | fi 81 | 82 | # 83 | # Shutdown (terminate) the instance 84 | # 85 | 86 | echo "User-data script completed. Shutting down instance.." 87 | 88 | uptime 89 | 90 | # Don't shut down immediately so that CloudWatch Agent has time to push logs to AWS 91 | shutdown -h -t 10 +1 92 | -------------------------------------------------------------------------------- /chrome/README.md: -------------------------------------------------------------------------------- 1 | Hi there. This file has moved [here](/docs/chrome.md). 2 | -------------------------------------------------------------------------------- /docs/automation.md: -------------------------------------------------------------------------------- 1 | # Automation 2 | 3 | These docs are a work-in-progress (read: incomplete). 4 | 5 | Automation of builds. 6 | 7 | There is more documentation concerning building Chrome/Chromium [here](/docs/chrome.md) 8 | 9 | 10 | ## Setup 11 | 12 | - create IAM role "serverless-chrome-automation" with policy defined in `aws/iam-serverless-chrome-automation-role.json` 13 | - if desired, modify `aws/ec2-spot-instance-specification.json` to change instance-types and max spot-price 14 | - AWS_ACCESS_KEY_ID 15 | - AWS_SECRET_ACCESS_KEY 16 | - AWS_IAM_INSTANCE_ARN 17 | 18 | 19 | ## Manual Build 20 | 21 | To perform a manual build on EC2 using a spot instance: 22 | 23 | ```sh 24 | ./scripts/ec2-build.sh chromium stable 62.0.3202.75 25 | ``` 26 | 27 | 28 | ## CI Build 29 | 30 | See `.circleci/config.yml` "daily" workflow for example. 31 | 32 | Example: Build latest Chromium (stable channel): 33 | 34 | ```sh 35 | ./scripts/ci-daily.sh stable chromium 36 | ``` 37 | -------------------------------------------------------------------------------- /docs/chrome.md: -------------------------------------------------------------------------------- 1 | # Chrome/Chromium on AWS Lambda 2 | 3 | ## Contents 4 | 1. [Prebuilt Binaries](#prebuilt-binaries) 5 | 1. [Docker Image](#docker-image) 6 | 1. [Build Yourself](#build-yourself) 7 | 1. [Locally](#locally) 8 | 1. [With AWS EC2](#with-aws-ec2) 9 | 1. [Fonts](#fonts) 10 | 1. [Known Issues / Limitations](#known-issues--limitations) 11 | 12 | 13 | ## Prebuilt Binaries 14 | 15 | Prebuilt binaries are regularly released and made available under [Releases](https://github.com/adieuadieu/serverless-chrome/releases). These binaries have been checked to work within the Lambda Execution Environment. New binaries are released whenever there's a new [stable-channel version](https://omahaproxy.appspot.com/) for Linux (about once every 1-2 weeks). 16 | 17 | Check this project's released binaries against the latest with: 18 | 19 | ```bash 20 | ./packages/lambda/scripts/latest-versions.sh 21 | ``` 22 | 23 | 24 | ## Docker Image 25 | 26 | The prebuild binaries made available under [Releases](https://github.com/adieuadieu/serverless-chrome/releases) are extracted from the public [adieuadieu/headless-chromium-for-aws-lambda](https://hub.docker.com/r/adieuadieu/headless-chromium-for-aws-lambda/) Docker image. This image is updated daily when there's a new Chromium version on any channel (stable, beta, dev). 27 | 28 | The image uses [lambci/lambda](https://hub.docker.com/r/lambci/lambda/) as a base which very closely mimics the live AWS Lambda Environment. We use the [adieuadieu/headless-chromium-for-aws-lambda](https://hub.docker.com/r/adieuadieu/headless-chromium-for-aws-lambda/) image for unit and integration tests. 29 | 30 | Run it yourself with: 31 | 32 | ```bash 33 | docker run -d --rm \ 34 | --name headless-chromium \ 35 | -p 9222:9222 \ 36 | adieuadieu/headless-chromium-for-aws-lambda 37 | ``` 38 | 39 | Headless Chromium is now running and accessible: 40 | 41 | ``` 42 | GET http://localhost:9222/ 43 | ``` 44 | 45 | Extract the headless Chrome binary from the container with: 46 | 47 | ```bash 48 | docker run -dt --rm --name headless-chromium adieuadieu/headless-chromium-for-aws-lambda:stable 49 | docker cp headless-chromium:/bin/headless-chromium ./ 50 | docker stop headless-chromium 51 | ``` 52 | 53 | 54 | ## Build Yourself 55 | 56 | ### Locally 57 | 58 | The easiest way to build headless Chromium locally is with Docker: 59 | 60 | ```bash 61 | cd packages/lambda/builds/chromium 62 | 63 | export CHROMIUM_VERSION=$(./latest.sh stable) 64 | 65 | docker build \ 66 | -t "headless-chromium:$CHROMIUM_VERSION" \ 67 | --build-arg VERSION="$CHROMIUM_VERSION" \ 68 | "build" 69 | ``` 70 | 71 | The script `./packages/lambda/builds/chromium/latest.sh stable` returns the latest "stable" channel version of Chromium, e.g. "62.0.3202.94". 72 | 73 | The [Dockerfile](/packages/lambda/builds/chromium/build/Dockerfile) in [`packages/lambda/builds/chromium/build`](/packages/lambda/builds/chromium/build) builds the Chromium version specified by `CHROMIUM_VERSION` with the [`build.sh`](/packages/lambda/builds/chromium/build/build.sh) script. 74 | 75 | It's also possible to build Chromium without Docker using just the [`build.sh`](/packages/lambda/builds/chromium/build/build.sh) script. However, make sure that you run the script as `root` on a compatible OS environment (e.g. AmazonLinux on EC2): 76 | 77 | ```bash 78 | cd packages/lambda/builds/chromium 79 | 80 | export VERSION=$(./latest.sh stable) 81 | 82 | ./build/build.sh 83 | ``` 84 | 85 | **Note:** On MacOS building with Docker, if you're running into a no-more-disk-space-available error, you may need to [increase the size](https://community.hortonworks.com/articles/65901/how-to-increase-the-size-of-the-base-docker-for-ma.html) of the Docker data sparse image. *Warning!:* This will wipe out all of your local images/containers: 86 | 87 | ```bash 88 | rm ~/Library/Containers/com.docker.docker/Data/com.docker.driver.amd64-linux/Docker.qcow2 89 | qemu-img create -f qcow2 ~/Library/Containers/com.docker.docker/Data/com.docker.driver.amd64-linux/Docker.qcow2 50G 90 | ``` 91 | 92 | Install `qemu-img` with `brew install qemu` 93 | 94 | 95 | ### With AWS EC2 96 | 97 | How the cool kids build. 98 | 99 | Easily build Chromium using an EC2 Spot Instance (spot-block) using the [`ec2-build.sh`](/scripts/ec2-build.sh) script. With a `c5.2xlarge` spot-instance a single build takes rougly 2h15m and usually costs between $0.25 and $0.30 in `us-east-1a`. Or, ~30m on `c5.18xlarge` for about $0.50. To build Chromium, an instance with at least 4GB of memory is required. 100 | 101 | Building on EC2 requires some IAM permissions setup: 102 | 103 | 1. Create a new IAM _user_ with access keys and add [this custom inline policy](/aws/iam-serverless-chrome-automation-user-policy.json). The policy allows the minimum IAM permissions required to initiate a build on an EC2 Spot Instance. 104 | 1. Create a new IAM _role_ called "serverless-chrome-automation" with an EC2 trust entity and add [this custom inline policy](/aws/iam-serverless-chrome-automation-role-policy.json). The policy allows the minimum IAM permissions required to retrieve secrets from the Parameter Store, log the instance's stdout/stderr to CloudWatch, and upload binaries to S3. Be sure to update the Resource Arns where appropriate (e.g. for the S3 bucket). Make note of the `Instance Profile ARN` as you'll need to set the `AWS_IAM_INSTANCE_ARN` environment variable to it. 105 | 106 | Next, export the following environment variables in your shell: 107 | 108 | ```bash 109 | export AWS_ACCESS_KEY_ID=<your-iam-user-access-key-created-in-step-1> 110 | export AWS_SECRET_ACCESS_KEY=<your-iam-user-secret-created-in-step-1> 111 | export AWS_IAM_INSTANCE_ARN=<your-iam-role-instance-arn-created-in-step-2> 112 | export S3_BUCKET=<your-s3-bucket-and-optional-prefix> 113 | export FORCE_NEW_BUILD=1 114 | ``` 115 | 116 | Then to start a new build run the following, replacing the version and/or channel if desired: 117 | 118 | ```bash 119 | ./scripts/ec2-build.sh chromium canary 64.0.3272.0 120 | ``` 121 | 122 | The version can also be ommitted. This will build the latest version based on the channel (one of `stable`, `beta`, or `dev`). Canary builds require an explicit version to be defined. 123 | 124 | If successfull, the binary will show up in the S3 bucket. Check the CloudWatch `serverless-chrome-automation` log group [logs](https://console.aws.amazon.com/cloudwatch/home?region=us-east-1#logStream:group=/serverless-chrome-automation;streamFilter=typeLogStreamPrefix) for errors. 125 | 126 | EC2 Spot Instance specifications such as instance type or spot price can be configured via [`/aws/ec2-spot-instance-specification.json`](/aws/ec2-spot-instance-specification.json). 127 | 128 | ## Fonts 129 | 130 | @TODO: document this. 131 | 132 | 133 | ## Known Issues / Limitations 134 | 135 | 1. Hack to Chrome code to disable `/dev/shm`. Details [here](https://medium.com/@marco.luethy/running-headless-chrome-on-aws-lambda-fa82ad33a9eb). 136 | 1. [Hack](https://github.com/adieuadieu/serverless-chrome/issues/41#issuecomment-341712878) to disable Sandbox IPC Polling. 137 | -------------------------------------------------------------------------------- /docs/circleci.md: -------------------------------------------------------------------------------- 1 | # CircleCI Setup 2 | 3 | How to setup Circle CI for continuous integration/deployment (aka notes for project maintainer in case they forget). 4 | 5 | Jobs and workflows defined in `~/.circleci/config.yml` 6 | 7 | ## Build Settings 8 | 9 | ### Environment Variables 10 | 11 | For automated releases, environment requires the following variables: 12 | 13 | - **AWS_IAM_INSTANCE_ARN** - The instance Arn for Spot instances launched when new binaries are to be built. Something like `arn:aws:iam::000000000000:instance-profile/serverless-chrome-automation` 14 | - **AWS_REGION** - Region in which to launch spot instances 15 | - **NPM_TOKEN** - NPM token for publishing packages. Use a bot account! 16 | - **GIT_USER_EMAIL** - email for commits made during Continuous Deployment processes. 17 | - **GIT_USER_NAME** - user's name for commits made during Continuous Deployment processes 18 | 19 | Bla bla: 20 | 21 | - CODACY_PROJECT_TOKEN 22 | - COVERALLS_REPO_TOKEN 23 | - COVERALLS_SERVICE_NAME 24 | 25 | ### Advanced Settings 26 | 27 | - **Pass secrets to builds from forked pull requests**: Off! 28 | 29 | 30 | ## Permissions 31 | 32 | ### Checkout SSH Keys 33 | 34 | Yes. Requires a user key for github.com so that nightly binary versions updates can be tagged/released when appropriate. Use a bot account! 35 | 36 | 37 | ### AWS Permissions 38 | 39 | Yes. Needed. 40 | 41 | -------------------------------------------------------------------------------- /examples/serverless-framework/aws/README.md: -------------------------------------------------------------------------------- 1 | # Serverless-framework based Example Functions 2 | 3 | A collection of [Serverless-framework](https://github.com/serverless/serverless) based functions for AWS Lambda demonstrating the `serverless-plugin-chrome` plugin for Serverless to run Headless Chrome serverless-ly. The example functions include: 4 | - A Print to PDF handler 5 | - A Capture Screenshot handler 6 | - A Page-load Request Logger handler 7 | - A Version Info handler (💤 ) 8 | 9 | 10 | ## Contents 11 | 1. [Installation](#installation) 12 | 1. [Credentials](#credentials) 13 | 1. [Deployment](#deployment) 14 | 1. [Example Functions](#example-functions) 15 | 1. [Local Development](#local-development) 16 | 1. [Configuration](#configuration) 17 | 18 | 19 | ## Installation 20 | 21 | First, install `serverless` globally: 22 | 23 | ```bash 24 | npm install serverless -g 25 | ``` 26 | 27 | Then pull down the example service: 28 | 29 | ```bash 30 | serverless create -u \ 31 | https://github.com/adieuadieu/serverless-chrome/tree/master/examples/serverless-framework/aws 32 | ``` 33 | 34 | And install the dependencies: 35 | 36 | ```bash 37 | npm install 38 | ``` 39 | 40 | ## Credentials 41 | 42 | _We recommend using a tool like [AWS Vault](https://github.com/99designs/aws-vault) to manage your AWS credentials._ 43 | 44 | You must configure your AWS credentials either by defining `AWS_ACCESS_KEY_ID` and `AWS_SECRET_ACCESS_KEY` environmental variables, or using an AWS profile. You can read more about this on the [Serverless Credentials Guide](https://serverless.com/framework/docs/providers/aws/guide/credentials/). 45 | 46 | In short, either: 47 | 48 | ```bash 49 | export AWS_PROFILE=<your-profile-name> 50 | ``` 51 | 52 | or 53 | 54 | ```bash 55 | export AWS_ACCESS_KEY_ID=<your-key-here> 56 | export AWS_SECRET_ACCESS_KEY=<your-secret-key-here> 57 | ``` 58 | 59 | ## Deployment 60 | 61 | Once Credentials are set up, to deploy the full service run: 62 | 63 | ```bash 64 | npm run deploy 65 | ``` 66 | 67 | ## Example Functions 68 | 69 | This example service includes the following functions, each demonstrating a common pattern/use-case. 70 | 71 | 72 | ### Capture Screenshot of a given URL 73 | When you the serverless function, it creates a Lambda function which will take a screenshot of a URL it's provided. You can provide this URL to the Lambda function via the AWS API Gateway. After a successful deploy, an API endpoint will be provided. Use this URL to call the Lambda function with a url in the query string. E.g. `https://XXXXXXX.execute-api.us-east-1.amazonaws.com/dev/screenshot?url=https://github.com/adieuadieu/serverless-chrome`. Add `&mobile=1` for mobile device view. 74 | 75 | #### Deploying 76 | 77 | To deploy the Capture Screenshot function: 78 | 79 | ```bash 80 | serverless deploy -f screenshot 81 | ``` 82 | 83 | ### Print a given URL to PDF 84 | The printToPdf handler will create a PDF from a URL it's provided. You can provide this URL to the Lambda function via the AWS API Gateway. After a successful deploy, an API endpoint will be provided. Use this URL to call the Lambda function with a url in the query string. E.g. `https://XXXXXXX.execute-api.us-weeast-2.amazonaws.com/dev/pdf?url=https://github.com/adieuadieu/serverless-chrome` 85 | 86 | This handler also supports configuring the "paper" size, orientation, etc. You can pass any of the DevTools Protocol's [`Page.printToPdf()`](https://chromedevtools.github.io/devtools-protocol/tot/Page/#method-printToPDF]) method's parameters. For example, for landscape oriented PDF add `&landscape=true` to the end of the URL. Be sure to remember to escape the value of `url` if it contains query parameters. E.g. `https://XXXXXXX.execute-api.us-east-1.amazonaws.com/dev/pdf?url=https://github.com/adieuadieu/serverless-chrome&landscape=true` 87 | 88 | #### Deploying 89 | 90 | To deploy the Capture Screenshot function: 91 | 92 | ```bash 93 | serverless deploy -f pdf 94 | ``` 95 | 96 | 97 | ### Page-load Request Logger 98 | Returns an array of every request which was made loading a given page. 99 | 100 | #### Deploying 101 | 102 | To deploy the Page-load Request Logger function: 103 | 104 | ```bash 105 | serverless deploy -f request-logger 106 | ``` 107 | 108 | 109 | ### Chrome Version Info 110 | Prints version info of headless chrome binary 111 | 112 | #### Deploying 113 | 114 | To deploy the Chrome Version Info function: 115 | 116 | ```bash 117 | serverless deploy -f version-info 118 | ``` 119 | 120 | ## Configuration 121 | 122 | These are simple functions and don't offer any configuration options. Take a look at the `serverless-plugins-chrome` plugin's [README](/packages/serverless-plugin) for it's configuration options. 123 | 124 | 125 | ## Local Development 126 | 127 | Go for it. Locally, if installed, Chrome will be launched. More in the plugin's [README](/packages/serverless-plugin). 128 | 129 | Invoke a function locally with: 130 | 131 | ```bash 132 | serverless invoke local -f replaceThisWithTheFunctionName 133 | ``` 134 | 135 | 136 | ## Troubleshooting 137 | 138 | <details id="ts-aws-client-timeout"> 139 | <summary>I keep getting a timeout error when deploying and it's really annoying.</summary> 140 | 141 | Indeed, that is annoying. I've had the same problem, and so that's why it's now here in this troubleshooting section. This may be an issue in the underlying AWS SDK when using a slower Internet connection. Try changing the `AWS_CLIENT_TIMEOUT` environment variable to a higher value. For example, in your command prompt enter the following and try deploying again: 142 | 143 | ```bash 144 | export AWS_CLIENT_TIMEOUT=3000000 145 | ``` 146 | </details> 147 | 148 | <details id="ts-argh"> 149 | <summary>Aaaaaarggghhhhhh!!!</summary> 150 | 151 | Uuurrrggghhhhhh! Have you tried [filing an Issue](https://github.com/adieuadieu/serverless-chrome/issues/new)? 152 | </details> 153 | -------------------------------------------------------------------------------- /examples/serverless-framework/aws/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@serverless-chrome/example-serverless-framework-aws", 3 | "private": true, 4 | "version": "1.0.0-70", 5 | "description": "Example serverless functions using the Serverless-framework", 6 | "main": "src/handlers.js", 7 | "engines": { 8 | "node": ">= 6.10.0" 9 | }, 10 | "config": { 11 | "jsSrc": "src/", 12 | "chromiumChannel": "dev", 13 | "chromium_channel": "dev" 14 | }, 15 | "scripts": { 16 | "test": "npm run lint && npm run ava", 17 | "watch:test": "ava --watch", 18 | "ava": "ava", 19 | "lint": "npm run lint:eslint -s", 20 | "lint:eslint": "eslint $npm_package_config_jsSrc", 21 | "deploy": "serverless deploy -v", 22 | "upgrade-dependencies": "yarn upgrade-interactive --latest --exact" 23 | }, 24 | "repository": { 25 | "type": "git", 26 | "url": "https://github.com/adieuadieu/serverless-chrome.git" 27 | }, 28 | "keywords": [ 29 | "serverless", 30 | "chrome", 31 | "chromium", 32 | "headless", 33 | "aws", 34 | "lambda", 35 | "serverless-framework", 36 | "screenshot", 37 | "screen capture", 38 | "pdf" 39 | ], 40 | "author": "Marco Lüthy", 41 | "license": "MIT", 42 | "bugs": { 43 | "url": "https://github.com/adieuadieu/serverless-chrome/issues" 44 | }, 45 | "homepage": "https://github.com/adieuadieu/serverless-chrome/tree/master/examples/serverless-framework/aws", 46 | "dependencies": { 47 | "chrome-remote-interface": "0.25.3" 48 | }, 49 | "devDependencies": { 50 | "ava": "0.23.0", 51 | "babel-core": "6.26.0", 52 | "babel-loader": "7.1.2", 53 | "babel-plugin-transform-object-entries": "1.0.0", 54 | "babel-plugin-transform-object-rest-spread": "6.26.0", 55 | "babel-preset-env": "1.6.1", 56 | "babel-preset-stage-3": "6.24.1", 57 | "babel-register": "6.26.0", 58 | "serverless": "1.24.1", 59 | "serverless-plugin-chrome": "1.0.0-70", 60 | "serverless-webpack": "4.0.0", 61 | "webpack": "3.8.1" 62 | }, 63 | "ava": { 64 | "require": "babel-register", 65 | "babel": "inherits" 66 | }, 67 | "babel": { 68 | "sourceMaps": true, 69 | "presets": [ 70 | [ 71 | "env", 72 | { 73 | "modules": "commonjs", 74 | "targets": { 75 | "node": "6.10" 76 | }, 77 | "include": [ 78 | "es7.object.values", 79 | "es7.object.entries" 80 | ] 81 | } 82 | ], 83 | "stage-3" 84 | ], 85 | "plugins": [ 86 | "transform-object-rest-spread", 87 | "transform-object-entries" 88 | ] 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /examples/serverless-framework/aws/serverless.yml: -------------------------------------------------------------------------------- 1 | service: serverless-chrome-examples 2 | 3 | provider: 4 | name: aws 5 | runtime: nodejs12.x 6 | stage: dev 7 | region: us-east-1 8 | environment: 9 | PAGE_LOAD_TIMEOUT: 20000 10 | LOGGING: true 11 | 12 | plugins: 13 | - serverless-plugin-chrome 14 | - serverless-webpack 15 | 16 | custom: 17 | chrome: 18 | flags: 19 | - --window-size=1280,1696 # Letter size 20 | - --hide-scrollbars 21 | 22 | functions: 23 | version-info: 24 | description: Headless Chrome Serverless-framework version info example 25 | memorySize: 1024 26 | timeout: 30 27 | handler: src/handlers/version.default 28 | events: 29 | - http: 30 | path: version-info 31 | method: get 32 | 33 | request-logger: 34 | description: Headless Chrome Serverless-framework request logging example 35 | memorySize: 1024 36 | timeout: 30 37 | handler: src/handlers/requestLogger.default 38 | events: 39 | - http: 40 | path: request-logger 41 | method: get 42 | 43 | screenshot: 44 | description: Headless Chrome Serverless-framework screenshot example 45 | memorySize: 1536 46 | timeout: 30 47 | handler: src/handlers/screenshot.default 48 | events: 49 | - http: 50 | path: screenshot 51 | method: get 52 | 53 | pdf: 54 | description: Headless Chrome Serverless-framework PDF example 55 | memorySize: 1536 56 | timeout: 30 57 | handler: src/handlers/pdf.default 58 | events: 59 | - http: 60 | path: pdf 61 | method: get 62 | 63 | resources: 64 | Resources: 65 | ApiGatewayRestApi: 66 | Properties: 67 | BinaryMediaTypes: 68 | - "*/*" 69 | 70 | # Enable X-Ray tracing on Lambda functions 71 | # ScreenshotLambdaFunction: 72 | # Properties: 73 | # TracingConfig: 74 | # Mode: Active 75 | # PdfLambdaFunction: 76 | # Properties: 77 | # TracingConfig: 78 | # Mode: Active 79 | -------------------------------------------------------------------------------- /examples/serverless-framework/aws/src/chrome/pdf.js: -------------------------------------------------------------------------------- 1 | // 2 | // 3 | // HEY! Be sure to re-incorporate changes from @albinekb 4 | // https://github.com/adieuadieu/serverless-chrome/commit/fca8328134f1098adf92e115f69002e69df24238 5 | // 6 | // 7 | // 8 | import Cdp from 'chrome-remote-interface' 9 | import log from '../utils/log' 10 | import sleep from '../utils/sleep' 11 | 12 | const defaultPrintOptions = { 13 | landscape: false, 14 | displayHeaderFooter: false, 15 | printBackground: true, 16 | scale: 1, 17 | paperWidth: 8.27, // aka A4 18 | paperHeight: 11.69, // aka A4 19 | marginTop: 0, 20 | marginBottom: 0, 21 | marginLeft: 0, 22 | marginRight: 0, 23 | pageRanges: '', 24 | } 25 | 26 | function cleanPrintOptionValue (type, value) { 27 | const types = { string: String, number: Number, boolean: Boolean } 28 | return types[type](value) 29 | } 30 | 31 | export function makePrintOptions (options = {}) { 32 | return Object.entries(options).reduce( 33 | (printOptions, [option, value]) => ({ 34 | ...printOptions, 35 | [option]: cleanPrintOptionValue( 36 | typeof defaultPrintOptions[option], 37 | value 38 | ), 39 | }), 40 | defaultPrintOptions 41 | ) 42 | } 43 | 44 | export default async function printUrlToPdf ( 45 | url, 46 | printOptions = {}, 47 | mobile = false 48 | ) { 49 | const LOAD_TIMEOUT = process.env.PAGE_LOAD_TIMEOUT || 1000 * 20 50 | let result 51 | 52 | // @TODO: write a better queue, which waits a few seconds when reaching 0 53 | // before emitting "empty". Also see other handlers. 54 | const requestQueue = [] 55 | 56 | const emptyQueue = async () => { 57 | await sleep(1000) 58 | 59 | log('Request queue size:', requestQueue.length, requestQueue) 60 | 61 | if (requestQueue.length > 0) { 62 | await emptyQueue() 63 | } 64 | } 65 | 66 | const tab = await Cdp.New() 67 | const client = await Cdp({ host: '127.0.0.1', target: tab }) 68 | 69 | const { 70 | Network, Page, Runtime, Emulation, 71 | } = client 72 | 73 | Network.requestWillBeSent((data) => { 74 | // only add requestIds which aren't already in the queue 75 | // why? if a request to http gets redirected to https, requestId remains the same 76 | if (!requestQueue.find(item => item === data.requestId)) { 77 | requestQueue.push(data.requestId) 78 | } 79 | 80 | log('Chrome is sending request for:', data.requestId, data.request.url) 81 | }) 82 | 83 | Network.responseReceived(async (data) => { 84 | // @TODO: handle this better. sometimes images, fonts, 85 | // etc aren't done loading before we think loading is finished 86 | // is there a better way to detect this? see if there's any pending 87 | // js being executed? paints? something? 88 | await sleep(100) // wait here, in case this resource has triggered more resources to load. 89 | requestQueue.splice( 90 | requestQueue.findIndex(item => item === data.requestId), 91 | 1 92 | ) 93 | log('Chrome received response for:', data.requestId, data.response.url) 94 | }) 95 | 96 | try { 97 | await Promise.all([Network.enable(), Page.enable()]) 98 | 99 | await Page.navigate({ url }) 100 | 101 | await Page.loadEventFired() 102 | 103 | const { 104 | result: { 105 | value: { height }, 106 | }, 107 | } = await Runtime.evaluate({ 108 | expression: `( 109 | () => { 110 | const height = document.body.scrollHeight 111 | window.scrollTo(0, height) 112 | return { height } 113 | } 114 | )(); 115 | `, 116 | returnByValue: true, 117 | }) 118 | 119 | // setting the viewport to the size of the page will force 120 | // any lazy-loaded images to load 121 | await Emulation.setDeviceMetricsOverride({ 122 | mobile: !!mobile, 123 | deviceScaleFactor: 0, 124 | scale: 1, // mobile ? 2 : 1, 125 | width: mobile ? 375 : 1280, 126 | height, 127 | }) 128 | 129 | await new Promise((resolve, reject) => { 130 | const timeout = setTimeout( 131 | reject, 132 | LOAD_TIMEOUT, 133 | new Error(`Page load timed out after ${LOAD_TIMEOUT} ms.`) 134 | ) 135 | 136 | const load = async () => { 137 | await emptyQueue() 138 | clearTimeout(timeout) 139 | resolve() 140 | } 141 | 142 | load() 143 | }) 144 | 145 | log('We think the page has finished loading. Printing PDF.') 146 | 147 | const pdf = await Page.printToPDF(printOptions) 148 | result = pdf.data 149 | } catch (error) { 150 | console.error(error) 151 | } 152 | 153 | await client.close() 154 | 155 | return result 156 | } 157 | -------------------------------------------------------------------------------- /examples/serverless-framework/aws/src/chrome/pdf.test.js: -------------------------------------------------------------------------------- 1 | import test from 'ava' 2 | import pdf from './pdf' 3 | 4 | const testUrl = 'https://github.com/adieuadieu' 5 | 6 | test('printUrlToPdf() should return base64 encoded application/pdf', async (t) => { 7 | await t.notThrows(async () => { 8 | const result = await pdf(testUrl) 9 | 10 | t.is(typeof result, 'string') 11 | }) 12 | }) 13 | -------------------------------------------------------------------------------- /examples/serverless-framework/aws/src/chrome/screenshot.js: -------------------------------------------------------------------------------- 1 | import Cdp from 'chrome-remote-interface' 2 | import log from '../utils/log' 3 | import sleep from '../utils/sleep' 4 | 5 | export default async function captureScreenshotOfUrl (url, mobile = false) { 6 | const LOAD_TIMEOUT = process.env.PAGE_LOAD_TIMEOUT || 1000 * 60 7 | 8 | let result 9 | let loaded = false 10 | 11 | const loading = async (startTime = Date.now()) => { 12 | if (!loaded && Date.now() - startTime < LOAD_TIMEOUT) { 13 | await sleep(100) 14 | await loading(startTime) 15 | } 16 | } 17 | 18 | const [tab] = await Cdp.List() 19 | const client = await Cdp({ host: '127.0.0.1', target: tab }) 20 | 21 | const { 22 | Network, Page, Runtime, Emulation, 23 | } = client 24 | 25 | Network.requestWillBeSent((params) => { 26 | log('Chrome is sending request for:', params.request.url) 27 | }) 28 | 29 | Page.loadEventFired(() => { 30 | loaded = true 31 | }) 32 | 33 | try { 34 | await Promise.all([Network.enable(), Page.enable()]) 35 | 36 | if (mobile) { 37 | await Network.setUserAgentOverride({ 38 | userAgent: 39 | 'Mozilla/5.0 (iPhone; CPU iPhone OS 10_0_1 like Mac OS X) AppleWebKit/602.1.50 (KHTML, like Gecko) Version/10.0 Mobile/14A403 Safari/602.1', 40 | }) 41 | } 42 | 43 | await Emulation.setDeviceMetricsOverride({ 44 | mobile: !!mobile, 45 | deviceScaleFactor: 0, 46 | scale: 1, // mobile ? 2 : 1, 47 | width: mobile ? 375 : 1280, 48 | height: 0, 49 | }) 50 | 51 | await Page.navigate({ url }) 52 | await Page.loadEventFired() 53 | await loading() 54 | 55 | const { 56 | result: { 57 | value: { height }, 58 | }, 59 | } = await Runtime.evaluate({ 60 | expression: `( 61 | () => ({ height: document.body.scrollHeight }) 62 | )(); 63 | `, 64 | returnByValue: true, 65 | }) 66 | 67 | await Emulation.setDeviceMetricsOverride({ 68 | mobile: !!mobile, 69 | deviceScaleFactor: 0, 70 | scale: 1, // mobile ? 2 : 1, 71 | width: mobile ? 375 : 1280, 72 | height, 73 | }) 74 | 75 | const screenshot = await Page.captureScreenshot({ format: 'png' }) 76 | 77 | result = screenshot.data 78 | } catch (error) { 79 | console.error(error) 80 | } 81 | 82 | await client.close() 83 | 84 | return result 85 | } 86 | -------------------------------------------------------------------------------- /examples/serverless-framework/aws/src/chrome/screenshot.test.js: -------------------------------------------------------------------------------- 1 | import test from 'ava' 2 | import screenshot from './screenshot' 3 | 4 | const testUrl = 'https://github.com/adieuadieu' 5 | 6 | test('screenshot() should return base64 encoded image/png', async (t) => { 7 | await t.notThrows(async () => { 8 | const result = await screenshot(testUrl) 9 | 10 | t.is(typeof result, 'string') 11 | }) 12 | }) 13 | -------------------------------------------------------------------------------- /examples/serverless-framework/aws/src/chrome/version.js: -------------------------------------------------------------------------------- 1 | import Cdp from 'chrome-remote-interface' 2 | 3 | export default async function version () { 4 | return Cdp.Version() 5 | } 6 | -------------------------------------------------------------------------------- /examples/serverless-framework/aws/src/handlers/pdf.js: -------------------------------------------------------------------------------- 1 | // 2 | // 3 | // HEY! Be sure to re-incorporate changes from @albinekb 4 | // https://github.com/adieuadieu/serverless-chrome/commit/fca8328134f1098adf92e115f69002e69df24238 5 | // 6 | // 7 | // 8 | import log from '../utils/log' 9 | import pdf, { makePrintOptions } from '../chrome/pdf' 10 | 11 | export default async function handler (event, context, callback) { 12 | const queryStringParameters = event.queryStringParameters || {} 13 | const { 14 | url = 'https://github.com/adieuadieu/serverless-chrome', 15 | ...printParameters 16 | } = queryStringParameters 17 | const printOptions = makePrintOptions(printParameters) 18 | let data 19 | 20 | log('Processing PDFification for', url, printOptions) 21 | 22 | const startTime = Date.now() 23 | 24 | try { 25 | data = await pdf(url, printOptions) 26 | } catch (error) { 27 | console.error('Error printing pdf for', url, error) 28 | return callback(error) 29 | } 30 | 31 | log(`Chromium took ${Date.now() - startTime}ms to load URL and render PDF.`) 32 | 33 | // TODO: handle cases where the response is > 10MB 34 | // with saving to S3 or something since API Gateway has a body limit of 10MB 35 | return callback(null, { 36 | statusCode: 200, 37 | body: data, 38 | isBase64Encoded: true, 39 | headers: { 40 | 'Content-Type': 'application/pdf', 41 | }, 42 | }) 43 | } 44 | -------------------------------------------------------------------------------- /examples/serverless-framework/aws/src/handlers/pdf.test.js: -------------------------------------------------------------------------------- 1 | import test from 'ava' 2 | import handler from '../handlers/pdf' 3 | 4 | const testUrl = 'https://github.com/adieuadieu' 5 | const testEvent = { 6 | queryStringParameters: { url: testUrl }, 7 | } 8 | 9 | test('PDF handler', async (t) => { 10 | await t.notThrows(async () => { 11 | const result = await handler(testEvent, {}) 12 | 13 | t.is(result.statusCode, 200) 14 | }) 15 | }) 16 | -------------------------------------------------------------------------------- /examples/serverless-framework/aws/src/handlers/requestLogger.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/prefer-default-export */ 2 | import Cdp from 'chrome-remote-interface' 3 | 4 | const LOAD_TIMEOUT = 1000 * 30 5 | 6 | export default async function handler (event, context, callback) { 7 | const queryStringParameters = event.queryStringParameters || {} 8 | const { 9 | url = 'https://github.com/adieuadieu/serverless-chrome', 10 | } = queryStringParameters 11 | const requestsMade = [] 12 | 13 | const [tab] = await Cdp.List() 14 | const client = await Cdp({ host: '127.0.0.1', target: tab }) 15 | 16 | const { Network, Page } = client 17 | 18 | Network.requestWillBeSent(params => requestsMade.push(params)) 19 | 20 | const loadEventFired = Page.loadEventFired() 21 | 22 | // https://chromedevtools.github.io/devtools-protocol/tot/Network/#method-enable 23 | await Network.enable() 24 | 25 | // https://chromedevtools.github.io/devtools-protocol/tot/Page/#method-enable 26 | await Page.enable() 27 | 28 | // https://chromedevtools.github.io/devtools-protocol/tot/Page/#method-navigate 29 | await Page.navigate({ url }) 30 | 31 | // wait until page is done loading, or timeout 32 | await new Promise((resolve, reject) => { 33 | const timeout = setTimeout( 34 | reject, 35 | LOAD_TIMEOUT, 36 | new Error(`Page load timed out after ${LOAD_TIMEOUT} ms.`) 37 | ) 38 | 39 | loadEventFired.then(async () => { 40 | clearTimeout(timeout) 41 | resolve() 42 | }) 43 | }) 44 | // It's important that we close the websocket connection, 45 | // or our Lambda function will not exit properly 46 | await client.close() 47 | 48 | callback(null, { 49 | statusCode: 200, 50 | body: JSON.stringify({ 51 | requestsMade, 52 | }), 53 | headers: { 54 | 'Content-Type': 'application/json', 55 | }, 56 | }) 57 | } 58 | -------------------------------------------------------------------------------- /examples/serverless-framework/aws/src/handlers/screencast.js: -------------------------------------------------------------------------------- 1 | // EXPERIMENTAL 2 | 3 | /* 4 | @todo: time the duration between screenshot frames, and create ffmpeg video 5 | based on duration between framesCaptured 6 | see: https://github.com/peterc/chrome2gif/blob/master/index.js#L34 7 | */ 8 | 9 | import fs from 'fs' 10 | import path from 'path' 11 | import { spawn, execSync } from 'child_process' 12 | import Cdp from 'chrome-remote-interface' 13 | import log from '../utils/log' 14 | import sleep from '../utils/sleep' 15 | 16 | const defaultOptions = { 17 | captureFrameRate: 1, 18 | captureQuality: 50, 19 | videoFrameRate: '5', 20 | videoFrameSize: '848x640', 21 | } 22 | 23 | const FFMPEG_PATH = path.resolve('./ffmpeg') 24 | 25 | function cleanPrintOptionValue (type, value) { 26 | const types = { string: String, number: Number, boolean: Boolean } 27 | return value ? new types[type](value) : undefined 28 | } 29 | 30 | function makePrintOptions (options = {}) { 31 | return Object.entries(options).reduce( 32 | (printOptions, [option, value]) => ({ 33 | ...printOptions, 34 | [option]: cleanPrintOptionValue(typeof defaultOptions[option], value), 35 | }), 36 | defaultOptions 37 | ) 38 | } 39 | 40 | export async function makeVideo (url, options = {}, invokeid = '') { 41 | const LOAD_TIMEOUT = process.env.PAGE_LOAD_TIMEOUT || 1000 * 20 42 | let result 43 | let loaded = false 44 | let framesCaptured = 0 45 | 46 | // @TODO: write a better queue, which waits a few seconds when reaching 0 47 | // before emitting "empty" 48 | const requestQueue = [] 49 | 50 | const loading = async (startTime = Date.now()) => { 51 | log('Request queue size:', requestQueue.length, requestQueue) 52 | 53 | if ((!loaded || requestQueue.length > 0) && Date.now() - startTime < LOAD_TIMEOUT) { 54 | await sleep(100) 55 | await loading(startTime) 56 | } 57 | } 58 | 59 | const tab = await Cdp.New() 60 | const client = await Cdp({ host: '127.0.0.1', target: tab }) 61 | 62 | const { 63 | Network, Page, Input, DOM, 64 | } = client 65 | 66 | Network.requestWillBeSent((data) => { 67 | // only add requestIds which aren't already in the queue 68 | // why? if a request to http gets redirected to https, requestId remains the same 69 | if (!requestQueue.find(item => item === data.requestId)) { 70 | requestQueue.push(data.requestId) 71 | } 72 | 73 | log('Chrome is sending request for:', data.requestId, data.request.url) 74 | }) 75 | 76 | Network.responseReceived(async (data) => { 77 | // @TODO: handle this better. sometimes images, fonts, etc aren't done 78 | // loading before we think loading is finished 79 | // is there a better way to detect this? see if there's any pending js 80 | // being executed? paints? something? 81 | await sleep(100) // wait here, in case this resource has triggered more resources to load. 82 | requestQueue.splice(requestQueue.findIndex(item => item === data.requestId), 1) 83 | log('Chrome received response for:', data.requestId, data.response.url) 84 | }) 85 | 86 | // @TODO: check for/catch error/failures to load a resource 87 | // Network.loadingFailed 88 | // Network.loadingFinished 89 | // @TODO: check for results from cache, which don't trigger responseReceived 90 | // (Network.requestServedFromCache instead) 91 | // - if the request is cached you will get a "requestServedFromCache" event instead 92 | // of "responseReceived" (and no "loadingFinished" event) 93 | Page.loadEventFired((data) => { 94 | loaded = true 95 | log('Page.loadEventFired', data) 96 | }) 97 | 98 | Page.domContentEventFired((data) => { 99 | log('Page.domContentEventFired', data) 100 | }) 101 | 102 | Page.screencastFrame(({ sessionId, data, metadata }) => { 103 | const filename = `/tmp/frame-${invokeid}-${String(metadata.timestamp).replace('.', '')}.jpg` 104 | framesCaptured += 1 105 | 106 | // log('Received screencast frame', sessionId, metadata) 107 | Page.screencastFrameAck({ sessionId }) 108 | 109 | fs.writeFile(filename, data, { encoding: 'base64' }, (error) => { 110 | log('Page.screencastFrame writeFile:', filename, error) 111 | }) 112 | }) 113 | 114 | if (process.env.LOGGING === 'TRUE') { 115 | Cdp.Version((err, info) => { 116 | console.log('CDP version info', err, info) 117 | }) 118 | } 119 | 120 | try { 121 | await Promise.all([Network.enable(), Page.enable(), DOM.enable()]) 122 | 123 | const interactionStartTime = Date.now() 124 | 125 | await client.send('Overlay.enable') // this has to happen after DOM.enable() 126 | await client.send('Overlay.setShowFPSCounter', { show: true }) 127 | 128 | await Page.startScreencast({ 129 | format: 'jpeg', 130 | quality: options.captureQuality, 131 | everyNthFrame: options.captureFrameRate, 132 | }) 133 | 134 | await Page.navigate({ url }) 135 | await loading() 136 | await sleep(2000) 137 | await Input.synthesizeScrollGesture({ x: 50, y: 50, yDistance: -2000 }) 138 | await sleep(1000) 139 | 140 | await Page.stopScreencast() 141 | 142 | log('We think the page has finished doing what it do. Rendering video.') 143 | log(`Interaction took ${Date.now() - interactionStartTime}ms to finish.`) 144 | } catch (error) { 145 | console.error(error) 146 | } 147 | 148 | // @TODO: handle this better — 149 | // If you don't close the tab, an a subsequent Page.navigate() is unable to load the url, 150 | // you'll end up printing a PDF of whatever was loaded in the tab previously 151 | // (e.g. a previous URL) _unless_ you Cdp.New() each time. But still good to close to 152 | // clear up memory in Chrome 153 | try { 154 | log('trying to close tab', tab) 155 | await Cdp.Close({ id: tab.id }) 156 | } catch (error) { 157 | log('unable to close tab', tab, error) 158 | } 159 | 160 | await client.close() 161 | 162 | const renderVideo = async () => { 163 | await new Promise((resolve, reject) => { 164 | const args = [ 165 | '-y', 166 | '-loglevel', 167 | 'warning', // 'debug', 168 | '-f', 169 | 'image2', 170 | '-framerate', 171 | `${options.videoFrameRate}`, 172 | '-pattern_type', 173 | 'glob', 174 | '-i', 175 | `"/tmp/frame-${invokeid}-*.jpg"`, 176 | // '-r', 177 | '-s', 178 | `${options.videoFrameSize}`, 179 | '-c:v', 180 | 'libx264', 181 | '-pix_fmt', 182 | 'yuv420p', 183 | '/tmp/video.mp4', 184 | ] 185 | 186 | log('spawning ffmpeg with args', FFMPEG_PATH, args.join(' ')) 187 | 188 | const ffmpeg = spawn(FFMPEG_PATH, args, { cwd: '/tmp', shell: true }) 189 | ffmpeg.on('message', msg => log('ffmpeg message', msg)) 190 | ffmpeg.on('error', msg => log('ffmpeg error', msg) && reject(msg)) 191 | ffmpeg.on('close', (status) => { 192 | if (status !== 0) { 193 | log('ffmpeg closed with status', status) 194 | return reject(new Error(`ffmpeg closed with status ${status}`)) 195 | } 196 | 197 | return resolve() 198 | }) 199 | 200 | ffmpeg.stdout.on('data', (data) => { 201 | log(`ffmpeg stdout: ${data}`) 202 | }) 203 | 204 | ffmpeg.stderr.on('data', (data) => { 205 | log(`ffmpeg stderr: ${data}`) 206 | }) 207 | }) 208 | 209 | // @TODO: no sync-y syncface sync 210 | return fs.readFileSync('/tmp/video.mp4', { encoding: 'base64' }) 211 | } 212 | 213 | try { 214 | const renderStartTime = Date.now() 215 | result = await renderVideo() 216 | log(`FFmpeg took ${Date.now() - renderStartTime}ms to finish.`) 217 | } catch (error) { 218 | console.error('Error making video', error) 219 | } 220 | 221 | // @TODO: this clean up .. do it better. and not sync 222 | // clean up old frames 223 | console.log('rm', execSync('rm -Rf /tmp/frame-*').toString()) 224 | console.log('rm', execSync('rm -Rf /tmp/video*').toString()) 225 | 226 | return { data: result, framesCaptured } 227 | } 228 | 229 | export default async function handler (event, { invokeid }, callback) { 230 | const queryStringParameters = event.queryStringParameters || {} 231 | const { url, ...printParameters } = queryStringParameters 232 | const options = makePrintOptions(printParameters) 233 | let result = {} 234 | 235 | log('Processing PDFification for', url, options) 236 | 237 | const startTime = Date.now() 238 | 239 | try { 240 | result = await makeVideo(url, options, invokeid) 241 | } catch (error) { 242 | console.error('Error printing pdf for', url, error) 243 | return callback(error) 244 | } 245 | 246 | // TODO: probably better to write the pdf to S3, 247 | // but that's a bit more complicated for this example. 248 | return callback(null, { 249 | statusCode: 200, 250 | // it's not possible to send binary via AWS API Gateway as it expects JSON response from Lambda 251 | body: ` 252 | <html> 253 | <body> 254 | <p><a href="${url}">${url}</a></p> 255 | <p><code>${JSON.stringify(options, null, 2)}</code></p> 256 | <p>It took Chromium & FFmpeg ${Date.now() - 257 | startTime}ms to load URL, interact with the age and render video. Captured ${result.framesCaptured} frames.</p> 258 | <embed src="data:video/mp4;base64,${result.data}" width="100%" height="80%" type='video/mp4'> 259 | </body> 260 | </html> 261 | `, 262 | headers: { 263 | 'Content-Type': 'text/html', 264 | }, 265 | }) 266 | } 267 | -------------------------------------------------------------------------------- /examples/serverless-framework/aws/src/handlers/screenshot.js: -------------------------------------------------------------------------------- 1 | import log from '../utils/log' 2 | import screenshot from '../chrome/screenshot' 3 | 4 | export default async function handler (event, context, callback) { 5 | const queryStringParameters = event.queryStringParameters || {} 6 | const { 7 | url = 'https://github.com/adieuadieu/serverless-chrome', 8 | mobile = false, 9 | } = queryStringParameters 10 | 11 | let data 12 | 13 | log('Processing screenshot capture for', url) 14 | 15 | const startTime = Date.now() 16 | 17 | try { 18 | data = await screenshot(url, mobile) 19 | } catch (error) { 20 | console.error('Error capturing screenshot for', url, error) 21 | return callback(error) 22 | } 23 | 24 | log(`Chromium took ${Date.now() - startTime}ms to load URL and capture screenshot.`) 25 | 26 | return callback(null, { 27 | statusCode: 200, 28 | body: data, 29 | isBase64Encoded: true, 30 | headers: { 31 | 'Content-Type': 'image/png', 32 | }, 33 | }) 34 | } 35 | -------------------------------------------------------------------------------- /examples/serverless-framework/aws/src/handlers/screenshot.test.js: -------------------------------------------------------------------------------- 1 | import test from 'ava' 2 | import handler from './screenshot' 3 | 4 | const testUrl = 'https://github.com/adieuadieu' 5 | const testEvent = { 6 | queryStringParameters: { url: testUrl }, 7 | } 8 | 9 | test('Screenshot handler', async (t) => { 10 | await t.notThrows(async () => { 11 | const result = await handler(testEvent, {}) 12 | 13 | t.is(result.statusCode, 200) 14 | }) 15 | }) 16 | -------------------------------------------------------------------------------- /examples/serverless-framework/aws/src/handlers/version.js: -------------------------------------------------------------------------------- 1 | import log from '../utils/log' 2 | import version from '../chrome/version' 3 | 4 | export default async function handler (event, context, callback) { 5 | let responseBody 6 | 7 | log('Getting version info.') 8 | 9 | try { 10 | responseBody = await version() 11 | } catch (error) { 12 | console.error('Error getting version info') 13 | return callback(error) 14 | } 15 | 16 | return callback(null, { 17 | statusCode: 200, 18 | body: JSON.stringify(responseBody), 19 | headers: { 20 | 'Content-Type': 'application/json', 21 | }, 22 | }) 23 | } 24 | -------------------------------------------------------------------------------- /examples/serverless-framework/aws/src/utils/log.js: -------------------------------------------------------------------------------- 1 | export default function log (...stuffToLog) { 2 | if (process.env.LOGGING) { 3 | console.log(...stuffToLog) 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /examples/serverless-framework/aws/src/utils/sleep.js: -------------------------------------------------------------------------------- 1 | export default function sleep (miliseconds = 100) { 2 | return new Promise(resolve => setTimeout(() => resolve(), miliseconds)) 3 | } 4 | -------------------------------------------------------------------------------- /examples/serverless-framework/aws/webpack.config.js: -------------------------------------------------------------------------------- 1 | const webpack = require('webpack') 2 | const slsw = require('serverless-webpack') 3 | 4 | module.exports = { 5 | devtool: 'source-map', 6 | target: 'node', 7 | node: { 8 | __dirname: true, 9 | }, 10 | module: { 11 | rules: [ 12 | { 13 | test: /\.jsx?$/, 14 | loader: 'babel-loader', 15 | exclude: /node_modules/, 16 | options: { 17 | cacheDirectory: true, 18 | }, 19 | }, 20 | { test: /\.json$/, loader: 'json-loader' }, 21 | ], 22 | }, 23 | resolve: { 24 | symlinks: true, 25 | }, 26 | output: { 27 | libraryTarget: 'commonjs', 28 | path: `${__dirname}/.webpack`, 29 | filename: '[name].js', 30 | }, 31 | externals: ['aws-sdk'], 32 | plugins: [ 33 | new webpack.optimize.LimitChunkCountPlugin({ 34 | maxChunks: 1, 35 | }), 36 | ], 37 | entry: slsw.lib.entries, 38 | } 39 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "serverless-chrome", 3 | "version": "1.0.0-70", 4 | "description": "Run headless Chrome, serverless-ly", 5 | "author": "Marco Lüthy <marco.luethy@gmail.com (https://github.com/adieuadieu)", 6 | "maintainers": [], 7 | "contributors": [], 8 | "main": "handler.js", 9 | "engines": { 10 | "node": ">= 6.10.0" 11 | }, 12 | "scripts": { 13 | "test": "npm run lint && nyc ava", 14 | "watch:test": "ava --watch", 15 | "ava": "ava", 16 | "lint": "npm run lint:eslint -s", 17 | "lint:eslint": "eslint --cache --ext .js .", 18 | "coverage": "nyc report", 19 | "upgrade-dependencies": "yarn upgrade-interactive --latest --exact", 20 | "precommit": "lint-staged", 21 | "prettier": "prettier-eslint --no-semi --single-quote --trailing-comma es5 --write", 22 | "release": "scripts/release.sh", 23 | "postversion": "scripts/sync-package-versions.sh" 24 | }, 25 | "repository": { 26 | "type": "git", 27 | "url": "https://github.com/adieuadieu/serverless-chrome.git" 28 | }, 29 | "keywords": [ 30 | "serverless", 31 | "chrome", 32 | "chromium", 33 | "headless", 34 | "aws", 35 | "lambda", 36 | "serverless-framework" 37 | ], 38 | "license": "MIT", 39 | "bugs": { 40 | "url": "https://github.com/adieuadieu/serverless-chrome/issues" 41 | }, 42 | "homepage": "https://github.com/adieuadieu/serverless-chrome", 43 | "dependencies": {}, 44 | "devDependencies": { 45 | "ava": "0.25.0", 46 | "babel-core": "6.26.3", 47 | "babel-eslint": "8.2.3", 48 | "babel-plugin-transform-runtime": "6.23.0", 49 | "babel-polyfill": "6.26.0", 50 | "babel-preset-env": "1.7.0", 51 | "babel-preset-stage-3": "6.24.1", 52 | "babel-register": "6.26.0", 53 | "babel-runtime": "6.26.0", 54 | "codacy-coverage": "3.0.0", 55 | "coveralls": "3.0.1", 56 | "eslint": "4.19.1", 57 | "eslint-config-airbnb-base": "12.1.0", 58 | "eslint-plugin-ava": "4.5.1", 59 | "eslint-plugin-import": "2.11.0", 60 | "eslint-plugin-node": "6.0.1", 61 | "eslint-plugin-promise": "3.7.0", 62 | "eslint-tap": "2.0.1", 63 | "husky": "0.14.3", 64 | "lint-staged": "7.1.0", 65 | "nyc": "11.8.0", 66 | "prettier": "1.12.1", 67 | "prettier-eslint": "8.8.1", 68 | "prettier-eslint-cli": "4.7.1", 69 | "tap-xunit": "2.3.0" 70 | }, 71 | "ava": { 72 | "require": "babel-register", 73 | "babel": "inherit" 74 | }, 75 | "babel": { 76 | "sourceMaps": "inline", 77 | "plugins": [ 78 | "transform-runtime" 79 | ], 80 | "presets": [ 81 | [ 82 | "env", 83 | { 84 | "targets": { 85 | "node": "6.10" 86 | } 87 | } 88 | ], 89 | "stage-3" 90 | ] 91 | }, 92 | "eslintConfig": { 93 | "parser": "babel-eslint", 94 | "plugins": [ 95 | "ava", 96 | "import" 97 | ], 98 | "extends": [ 99 | "airbnb-base", 100 | "plugin:ava/recommended" 101 | ], 102 | "settings": { 103 | "import/parser": "babel-eslint", 104 | "import/resolve": { 105 | "moduleDirectory": [ 106 | "node_modules", 107 | "src", 108 | "./" 109 | ] 110 | } 111 | }, 112 | "rules": { 113 | "no-console": 0, 114 | "semi": [ 115 | "error", 116 | "never" 117 | ], 118 | "comma-dangle": [ 119 | "error", 120 | "always-multiline" 121 | ], 122 | "space-before-function-paren": [ 123 | "error", 124 | "always" 125 | ] 126 | } 127 | }, 128 | "eslintIgnore": [ 129 | "node_modules", 130 | "dist" 131 | ], 132 | "prettier": { 133 | "printWidth": 80, 134 | "eslintIntegration": true, 135 | "jsonEnable": [], 136 | "semi": false, 137 | "singleQuote": true, 138 | "trailingComma": "es5", 139 | "useTabs": false 140 | }, 141 | "lint-staged": { 142 | "*.{js,jsx}": [ 143 | "yarn prettier", 144 | "yarn lint", 145 | "git add" 146 | ] 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /packages/lambda/README.md: -------------------------------------------------------------------------------- 1 | # serverless-chrome/lambda 2 | 3 | Standalone package to run Headless Chrome on AWS Lambda's Node.js (6.10+) runtime. 4 | 5 | [](https://www.npmjs.com/package/@serverless-chrome/lambda) 6 | 7 | 8 | ## Contents 9 | 1. [Installation](#installation) 10 | 1. [Setup](#setup) 11 | 1. [Local Development](#local-development) 12 | 1. [Framework Plugins](#framework-plugins) 13 | 1. [Specifying Chromium Channel](#specifying-chromium-channel) 14 | 15 | 16 | ## Installation 17 | Install with yarn: 18 | 19 | ```bash 20 | yarn add @serverless-chrome/lambda 21 | ``` 22 | 23 | Install with npm: 24 | 25 | ```bash 26 | npm install --save @serverless-chrome/lambda 27 | ``` 28 | 29 | If you wish to develop locally, you also need to install `chrome-launcher`: 30 | 31 | ```bash 32 | npm install --save-dev chrome-launcher 33 | ``` 34 | 35 | 36 | ## Setup 37 | 38 | Use in your AWS Lambda function. Requires Node 6.10. 39 | 40 | 41 | ```js 42 | const launchChrome = require('@serverless-chrome/lambda') 43 | const CDP = require('chrome-remote-interface') 44 | 45 | module.exports.handler = function handler (event, context, callback) { 46 | launchChrome({ 47 | flags: ['--window-size=1280,1696', '--hide-scrollbars'] 48 | }) 49 | .then((chrome) => { 50 | // Chrome is now running on localhost:9222 51 | 52 | CDP.Version() 53 | .then((versionInfo) => { 54 | callback(null, { 55 | versionInfo, 56 | }) 57 | }) 58 | .catch((error) => { 59 | callback(error) 60 | }) 61 | }) 62 | // Chrome didn't launch correctly 😢 63 | .catch(callback) 64 | } 65 | ``` 66 | 67 | 68 | ## Local Development 69 | 70 | Local development is supported. In a non-lambda environment, the package will use chrome-launcher to launch a locally installed Chrome. You can also pass your own `chromePath`: 71 | 72 | ```js 73 | launchChrome({ chromePath: '/my/local/chrome/path' }) 74 | ``` 75 | 76 | **Command line flags (or "switches")** 77 | 78 | The behavior of Chrome does vary between platforms. It may be necessary to experiment with flags to get the results you desire. On Lambda [default flags](/packages/lambda/src/flags.js) are used, but in development no default flags are used. 79 | 80 | The package has zero external dependencies required for inclusion in your Lambda function's package. 81 | 82 | 83 | ## Framework Plugins 84 | 85 | There are plugins which bundle this package for easy deployment available for the following "serverless" frameworks: 86 | 87 | - [serverless-plugin-chrome](/packages/serverless-plugin) 88 | 89 | 90 | ## Specifying Chromium Channel 91 | 92 | This package will use the latest stable-channel build of Headless Chromium for AWS Lambda. To select a different channel (beta or dev), export either an environment variable `NPM_CONFIG_CHROMIUM_CHANNEL` or add `chromiumChannel` to the `config` section of your `package.json`: 93 | 94 | Your `package.json`: 95 | 96 | ```json 97 | { 98 | "name": "my-cool-project", 99 | "version": "1.0.0", 100 | "config": { 101 | "chromiumChannel": "dev" <-- here 102 | }, 103 | "scripts": { 104 | 105 | }, 106 | "description": { 107 | 108 | } 109 | } 110 | ``` 111 | 112 | Note: the `dev` channel is _almost_ `canary`, so use `dev` if you're looking for the Canary channel. 113 | 114 | You can skip download entirely with `NPM_CONFIG_SERVERLESS_CHROME_SKIP_DOWNLOAD` or setting `skip_download` in the `config` section of your `package.json` 115 | 116 | _Caution_: We test and develop features against the stable channel. Using the beta or dev channel versions of Chromium may lead to unexpected results, especially in relation to the [Chrome DevTools Protocol](https://chromedevtools.github.io/devtools-protocol/tot/Emulation/) (which is used by tools like Chromeless and Puppeteer). 117 | -------------------------------------------------------------------------------- /packages/lambda/builds/chromium/Dockerfile: -------------------------------------------------------------------------------- 1 | # 2 | # Launch headless Chromium with: 3 | # docker run -d --rm --name headless-chromium -p 9222:9222 adieuadieu/headless-chromium-for-aws-lambda 4 | # 5 | 6 | #FROM amazonlinux:2017.03 7 | FROM lambci/lambda 8 | 9 | # ref: https://chromium.googlesource.com/chromium/src.git/+refs 10 | ARG VERSION 11 | ENV VERSION ${VERSION:-master} 12 | 13 | LABEL maintainer="Marco Lüthy <marco.luethy@gmail.com>" 14 | LABEL chromium="${VERSION}" 15 | 16 | WORKDIR / 17 | 18 | ADD dist/headless-chromium /bin/headless-chromium 19 | 20 | EXPOSE 9222 21 | 22 | ENTRYPOINT [ \ 23 | "/bin/headless-chromium", \ 24 | "--disable-dev-shm-usage", \ 25 | "--disable-gpu", \ 26 | "--no-sandbox", \ 27 | "--hide-scrollbars", \ 28 | "--remote-debugging-address=0.0.0.0", \ 29 | "--remote-debugging-port=9222" \ 30 | ] 31 | -------------------------------------------------------------------------------- /packages/lambda/builds/chromium/README.md: -------------------------------------------------------------------------------- 1 | # Build Headless Chromium for AWS Lambda 2 | 3 | Documentation has moved [here](docs/chrome.md) 4 | 5 | If you're looking for instructions on how to compile/build Chromium/Chrome for AWS Lambda have a look at the [build script](packages/lambda/builds/chromium/build/build.sh) or the [Dockerfile](packages/lambda/builds/chromium/build/Dockerfile) or simply use the built [Docker image](https://hub.docker.com/r/adieuadieu/headless-chromium-for-aws-lambda/): 6 | 7 | ```bash 8 | docker run -d --rm \ 9 | --name headless-chromium \ 10 | -p 9222:9222 \ 11 | adieuadieu/headless-chromium-for-aws-lambda 12 | ``` 13 | 14 | Headless Chromium is now running and accessible: 15 | 16 | ``` 17 | GET http://localhost:9222/ 18 | ``` 19 | 20 | 21 | 22 | **Note:** to successfully build the Docker image on MacOS, you may need to [increase the size](https://community.hortonworks.com/articles/65901/how-to-increase-the-size-of-the-base-docker-for-ma.html) of the Docker data sparse image. *Warning!:* This will wipe out all of your local images/containers. 23 | 24 | 25 | ```bash 26 | rm ~/Library/Containers/com.docker.docker/Data/com.docker.driver.amd64-linux/Docker.qcow2 27 | qemu-img create -f qcow2 ~/Library/Containers/com.docker.docker/Data/com.docker.driver.amd64-linux/Docker.qcow2 50G 28 | ``` 29 | 30 | 31 | ---- 32 | 33 | <br/> 34 | <br/> 35 | <br/> 36 | <br/> 37 | <br/> 38 | The rest of this README is outdated. 39 | <br/> 40 | <br/> 41 | <br/> 42 | <br/> 43 | <br/> 44 | 45 | ---- 46 | 47 | # What is this? 48 | 49 | `chrome-headless-lambda-linux-x64.tar.gz` was created on a Linux machine. It contains [Headless Chrome](https://cs.chromium.org/chromium/src/headless/app/) binaries specific to the [Lambda execution environment](http://docs.aws.amazon.com/lambda/latest/dg/current-supported-versions.html). The tarball is used during the Serverless deployment/packaging step to create the zip file for deploying the function to Lambda. 50 | 51 | 52 | ## Building Headless Chrome for AWS Lambda 53 | 54 | How to build headless_shell (headless Chrome) for the lambda execution environment. These steps are based on [this](http://www.zackarychapple.guru/chrome/2016/08/24/chrome-headless.html) and [this](https://chromium.googlesource.com/chromium/src/+/master/docs/linux_build_instructions.md). 55 | 56 | 1. Create a new EC2 instance using the community AMI with name amzn-ami-hvm-2016.03.3.x86_64-gp2 (us-west-2 ami-7172b611). 57 | 2. Pick an Instance Type with at least 16 GB of memory. Compile time will take about 4-5 hours on a t2.xlarge, or 2-3ish on a t2.2xlarge or about 45 min on a c4.4xlarge. 58 | 3. Give yourself a Root Volume that's at least 30 GB (40 GB if you want to compile a debug build—which you won't be able to upload to Lambda because it's too big.) 59 | 4. SSH into the new instance and run: 60 | 61 | ```bash 62 | sudo printf "LANG=en_US.utf-8\nLC_ALL=en_US.utf-8" >> /etc/environment 63 | 64 | sudo yum install -y git redhat-lsb python bzip2 tar pkgconfig atk-devel alsa-lib-devel bison binutils brlapi-devel bluez-libs-devel bzip2-devel cairo-devel cups-devel dbus-devel dbus-glib-devel expat-devel fontconfig-devel freetype-devel gcc-c++ GConf2-devel glib2-devel glibc.i686 gperf glib2-devel gtk2-devel gtk3-devel java-1.*.0-openjdk-devel libatomic libcap-devel libffi-devel libgcc.i686 libgnome-keyring-devel libjpeg-devel libstdc++.i686 libX11-devel libXScrnSaver-devel libXtst-devel libxkbcommon-x11-devel ncurses-compat-libs nspr-devel nss-devel pam-devel pango-devel pciutils-devel pulseaudio-libs-devel zlib.i686 httpd mod_ssl php php-cli python-psutil wdiff --enablerepo=epel 65 | ``` 66 | 67 | _Yum_ will complain about some packages not existing. Whatever. I haven't looked into them. Didn't seem to stop me from building headless_shell, though. Ignore whiney little _Yum_ and move on. Next: 68 | 69 | ```bash 70 | git clone https://chromium.googlesource.com/chromium/tools/depot_tools.git 71 | echo "export PATH=$PATH:$HOME/depot_tools" >> ~/.bash_profile 72 | source ~/.bash_profile 73 | mkdir Chromium && cd Chromium 74 | fetch --no-history chromium 75 | cd src 76 | ``` 77 | 78 | **TODO:** add part here about modifying Chrome to not use /dev/shm. See here: https://groups.google.com/a/chromium.org/d/msg/headless-dev/qqbZVZ2IwEw/CPInd55OBgAJ 79 | 80 | 81 | ```bash 82 | mkdir -p out/Headless 83 | echo 'import("//build/args/headless.gn")' > out/Headless/args.gn 84 | echo 'is_debug = false' >> out/Headless/args.gn 85 | echo 'symbol_level = 0' >> out/Headless/args.gn 86 | echo 'is_component_build = false' >> out/Headless/args.gn 87 | echo 'remove_webcore_debug_symbols = true' >> out/Headless/args.gn 88 | echo 'enable_nacl = false' >> out/Headless/args.gn 89 | gn gen out/Headless 90 | ninja -C out/Headless headless_shell 91 | ``` 92 | 93 | make the tarball: 94 | ```bash 95 | mkdir out/headless-chrome && cd out 96 | cp Headless/headless_shell Headless/libosmesa.so headless-chrome/ 97 | tar -zcvf chrome-headless-lambda-linux-x64.tar.gz headless-chrome/ 98 | zip headless-chrome chrome-headless-lambda-linux-x64.zip 99 | ``` 100 | 101 | ``` 102 | scp -i path/to/your/key-pair.pem ec2-user@<the-instance-public-ip>:/home/ec2-user/Chromium/src/out/chrome-headless-lambda-linux-x64.zip ./ 103 | ``` 104 | 105 | 106 | **TODO:** We don't need `libosmesa.so` cuz we're not using the GPU? See here: https://groups.google.com/a/chromium.org/d/msg/headless-dev/qqbZVZ2IwEw/XMKlEMP3EQAJ 107 | 108 | 109 | ## Updating 110 | 111 | ```bash 112 | git fetch --tags 113 | gclient sync --jobs 16 114 | ``` 115 | 116 | https://omahaproxy.appspot.com/ 117 | -------------------------------------------------------------------------------- /packages/lambda/builds/chromium/build/.gclient: -------------------------------------------------------------------------------- 1 | solutions = [ 2 | { 3 | "managed": False, 4 | "name": "src", 5 | "url": "https://chromium.googlesource.com/chromium/src.git", 6 | "custom_deps": {}, 7 | "deps_file": ".DEPS.git", 8 | "safesync_url": "" 9 | } 10 | ] 11 | -------------------------------------------------------------------------------- /packages/lambda/builds/chromium/build/Dockerfile: -------------------------------------------------------------------------------- 1 | # 2 | # Build with: 3 | # docker build --compress -t adieuadieu/chromium-for-amazonlinux-base:62.0.3202.62 --build-arg VERSION=62.0.3202.62 . 4 | # 5 | # Jump into the container with: 6 | # docker run -i -t --rm --entrypoint /bin/bash adieuadieu/chromium-for-amazonlinux-base 7 | # 8 | # Launch headless Chromium with: 9 | # docker run -d --rm --name headless-chromium -p 9222:9222 adieuadieu/headless-chromium-for-aws-lambda 10 | # 11 | 12 | FROM amazonlinux:2.0.20200722.0-with-sources 13 | 14 | # ref: https://chromium.googlesource.com/chromium/src.git/+refs 15 | ARG VERSION 16 | ENV VERSION ${VERSION:-master} 17 | 18 | LABEL maintainer="Marco Lüthy <marco.luethy@gmail.com>" 19 | LABEL chromium="${VERSION}" 20 | 21 | WORKDIR / 22 | 23 | ADD build.sh / 24 | ADD .gclient /build/chromium/ 25 | 26 | RUN sh /build.sh 27 | 28 | EXPOSE 9222 29 | 30 | ENTRYPOINT [ \ 31 | "/bin/headless-chromium", \ 32 | "--disable-dev-shm-usage", \ 33 | "--disable-gpu", \ 34 | "--no-sandbox", \ 35 | "--hide-scrollbars", \ 36 | "--remote-debugging-address=0.0.0.0", \ 37 | "--remote-debugging-port=9222" \ 38 | ] 39 | -------------------------------------------------------------------------------- /packages/lambda/builds/chromium/build/build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # shellcheck shell=dash 3 | 4 | # 5 | # Build Chromium for Amazon Linux. 6 | # Assumes root privileges. Or, more likely, Docker—take a look at 7 | # the corresponding Dockerfile in this directory. 8 | # 9 | # Requires 10 | # 11 | # Usage: ./build.sh 12 | # 13 | # Further documentation: https://github.com/adieuadieu/serverless-chrome/blob/develop/docs/chrome.md 14 | # 15 | 16 | set -e 17 | 18 | BUILD_BASE=$(pwd) 19 | VERSION=${VERSION:-master} 20 | 21 | printf "LANG=en_US.utf-8\nLC_ALL=en_US.utf-8" >> /etc/environment 22 | 23 | # install dependencies 24 | yum groupinstall -y "Development Tools" 25 | yum install -y \ 26 | alsa-lib-devel atk-devel binutils bison bluez-libs-devel brlapi-devel \ 27 | bzip2 bzip2-devel cairo-devel cmake cups-devel dbus-devel dbus-glib-devel \ 28 | expat-devel fontconfig-devel freetype-devel gcc-c++ git glib2-devel glibc \ 29 | gperf gtk3-devel htop httpd java-1.*.0-openjdk-devel libatomic libcap-devel \ 30 | libffi-devel libgcc libgnome-keyring-devel libjpeg-devel libstdc++ libuuid-devel \ 31 | libX11-devel libxkbcommon-x11-devel libXScrnSaver-devel libXtst-devel mercurial \ 32 | mod_ssl ncurses-compat-libs nspr-devel nss-devel pam-devel pango-devel \ 33 | pciutils-devel php php-cli pkgconfig pulseaudio-libs-devel python python3 \ 34 | tar zlib zlib-devel 35 | 36 | mkdir -p build/chromium 37 | 38 | cd build 39 | 40 | # install dept_tools 41 | git clone https://chromium.googlesource.com/chromium/tools/depot_tools.git 42 | 43 | export PATH="/opt/gtk/bin:$PATH:$BUILD_BASE/build/depot_tools" 44 | 45 | cd chromium 46 | 47 | # fetch chromium source code 48 | # ref: https://www.chromium.org/developers/how-tos/get-the-code/working-with-release-branches 49 | 50 | # git shallow clone, much quicker than a full git clone; see https://stackoverflow.com/a/39067940/3145038 for more details 51 | 52 | git clone --branch "$VERSION" --depth 1 https://chromium.googlesource.com/chromium/src.git 53 | 54 | # Checkout all the submodules at their branch DEPS revisions 55 | gclient sync --with_branch_heads --jobs 16 56 | 57 | cd src 58 | 59 | # the following is no longer necessary since. left here for nostalgia or something. 60 | # ref: https://chromium.googlesource.com/chromium/src/+/1824e5752148268c926f1109ed7e5ef1d937609a%5E%21 61 | # tweak to disable use of the tmpfs mounted at /dev/shm 62 | # sed -e '/if (use_dev_shm) {/i use_dev_shm = false;\n' -i base/files/file_util_posix.cc 63 | 64 | # 65 | # tweak to keep Chrome from crashing after 4-5 Lambda invocations 66 | # see https://github.com/adieuadieu/serverless-chrome/issues/41#issuecomment-340859918 67 | # Thank you, Geert-Jan Brits (@gebrits)! 68 | # 69 | SANDBOX_IPC_SOURCE_PATH="content/browser/sandbox_ipc_linux.cc" 70 | 71 | sed -e 's/PLOG(WARNING) << "poll";/PLOG(WARNING) << "poll"; failed_polls = 0;/g' -i "$SANDBOX_IPC_SOURCE_PATH" 72 | 73 | 74 | # specify build flags 75 | mkdir -p out/Headless && \ 76 | echo 'import("//build/args/headless.gn")' > out/Headless/args.gn && \ 77 | echo 'is_debug = false' >> out/Headless/args.gn && \ 78 | echo 'symbol_level = 0' >> out/Headless/args.gn && \ 79 | echo 'is_component_build = false' >> out/Headless/args.gn && \ 80 | echo 'remove_webcore_debug_symbols = true' >> out/Headless/args.gn && \ 81 | echo 'enable_nacl = false' >> out/Headless/args.gn && \ 82 | gn gen out/Headless 83 | 84 | # build chromium headless shell 85 | ninja -C out/Headless headless_shell 86 | 87 | cp out/Headless/headless_shell "$BUILD_BASE/bin/headless-chromium-unstripped" 88 | 89 | cd "$BUILD_BASE" 90 | 91 | # strip symbols 92 | strip -o "$BUILD_BASE/bin/headless-chromium" build/chromium/src/out/Headless/headless_shell 93 | 94 | # Use UPX to package headless chromium 95 | # this adds 1-1.5 seconds of startup time so generally 96 | # not so great for use in AWS Lambda so we don't actually use it 97 | # but left here in case someone finds it useful 98 | # yum install -y ucl ucl-devel --enablerepo=epel 99 | # cd build 100 | # git clone https://github.com/upx/upx.git 101 | # cd build/upx 102 | # git submodule update --init --recursive 103 | # make all 104 | # cp "$BUILD_BASE/build/chromium/src/out/Headless/headless_shell" "$BUILD_BASE/bin/headless-chromium-packaged" 105 | # src/upx.out "$BUILD_BASE/bin/headless-chromium-packaged" 106 | -------------------------------------------------------------------------------- /packages/lambda/builds/chromium/latest.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # shellcheck shell=dash 3 | 4 | # 5 | # Get current versions of Chromium 6 | # 7 | # Requires jq 8 | # 9 | # Usage: ./latest.sh [stable|beta|dev] 10 | # 11 | 12 | CHANNEL=${1:-stable} 13 | 14 | VERSION=$(curl -s https://omahaproxy.appspot.com/all.json | \ 15 | jq -r ".[] | select(.os == \"linux\") | .versions[] | select(.channel == \"$CHANNEL\") | .current_version" \ 16 | ) 17 | 18 | echo "$VERSION" 19 | -------------------------------------------------------------------------------- /packages/lambda/builds/chromium/version.json: -------------------------------------------------------------------------------- 1 | { 2 | "beta": "90.0.4430.51", 3 | "dev": "91.0.4464.5", 4 | "stable": "89.0.4389.114" 5 | } 6 | -------------------------------------------------------------------------------- /packages/lambda/builds/firefox/README.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adieuadieu/serverless-chrome/4c5894dd888a4bc58c16dafd520c318363c461dc/packages/lambda/builds/firefox/README.md -------------------------------------------------------------------------------- /packages/lambda/builds/nss/README.md: -------------------------------------------------------------------------------- 1 | # NSS 2 | 3 | NSS is a library required by chrome to run. Unfortunately the version provided 4 | by the AWS lambda image is too old, so it's necessary to build it. 5 | 6 | *Note:* This work was originally done by @qubyte in [PR#56](https://github.com/adieuadieu/serverless-chrome/pull/56/files). It's currently not necessary to include a special version of NSS but may become necessary again in the future—for that reason this is left here. 7 | 8 | ## Building 9 | 10 | Start a smallish EC2 instance using the same AMI as lambda. You can find a link 11 | to it [here][1]. Build on the instance using the following (adapted from 12 | instructions found [here][2]): 13 | 14 | ```shell 15 | sudo yum install mercurial 16 | sudo yum groupinstall 'Development Tools' 17 | sudo yum install zlib-devel 18 | 19 | hg clone https://hg.mozilla.org/projects/nspr 20 | hg clone https://hg.mozilla.org/projects/nss 21 | 22 | cd nss 23 | 24 | export BUILD_OPT=1 25 | export USE_64=1 26 | export NSDISTMODE=copy 27 | 28 | gmake nss_build_all 29 | ``` 30 | 31 | Remove any simlinks in the `dist` directory (they'll be links to .chk files) and 32 | zip it up for grabbing by scp. Place the archive in this folder (I've used a 33 | reverse date and the commit hash as a name), and replace the name in the 34 | lambda package file. 35 | 36 | [1]: http://docs.aws.amazon.com/lambda/latest/dg/current-supported-versions.html 37 | [2]: https://developer.mozilla.org/en-US/docs/Mozilla/Projects/NSS/Reference/Building_and_installing_NSS/Build_instructions 38 | -------------------------------------------------------------------------------- /packages/lambda/chrome/chrome-headless-lambda-linux-59.0.3039.0.tar.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adieuadieu/serverless-chrome/4c5894dd888a4bc58c16dafd520c318363c461dc/packages/lambda/chrome/chrome-headless-lambda-linux-59.0.3039.0.tar.gz -------------------------------------------------------------------------------- /packages/lambda/chrome/chrome-headless-lambda-linux-60.0.3089.0.tar.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adieuadieu/serverless-chrome/4c5894dd888a4bc58c16dafd520c318363c461dc/packages/lambda/chrome/chrome-headless-lambda-linux-60.0.3089.0.tar.gz -------------------------------------------------------------------------------- /packages/lambda/chrome/chrome-headless-lambda-linux-60.0.3095.0.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adieuadieu/serverless-chrome/4c5894dd888a4bc58c16dafd520c318363c461dc/packages/lambda/chrome/chrome-headless-lambda-linux-60.0.3095.0.zip -------------------------------------------------------------------------------- /packages/lambda/chrome/chrome-headless-lambda-linux-x64.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adieuadieu/serverless-chrome/4c5894dd888a4bc58c16dafd520c318363c461dc/packages/lambda/chrome/chrome-headless-lambda-linux-x64.zip -------------------------------------------------------------------------------- /packages/lambda/chrome/headless-chromium-64.0.3242.2-amazonlinux-2017-03.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adieuadieu/serverless-chrome/4c5894dd888a4bc58c16dafd520c318363c461dc/packages/lambda/chrome/headless-chromium-64.0.3242.2-amazonlinux-2017-03.zip -------------------------------------------------------------------------------- /packages/lambda/index.d.ts: -------------------------------------------------------------------------------- 1 | export default function ServerlessChromeLambda(options?: Option): Promise<ServerLessChrome>; 2 | 3 | type Option = { 4 | flags?: string[], 5 | chromePath?: string, 6 | port?: number, 7 | forceLambdaLauncher?: boolean, 8 | }; 9 | 10 | type ServerLessChrome = { 11 | pid: number, 12 | port: number, 13 | url: string, 14 | log: string, 15 | errorLog: string, 16 | pidFile: string, 17 | metaData: { 18 | launchTime: number, 19 | didLaunch: boolean, 20 | }, 21 | kill: () => void, 22 | }; 23 | -------------------------------------------------------------------------------- /packages/lambda/integration-test/dist: -------------------------------------------------------------------------------- 1 | ../dist/ -------------------------------------------------------------------------------- /packages/lambda/integration-test/handler.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const chrome = require('./dist/bundle.cjs.js') 3 | const cdp = require('chrome-remote-interface') 4 | 5 | module.exports.run = async function run (event) { 6 | const channel = `${event.channel}-` || '' 7 | 8 | console.log('started. Channel:', channel) 9 | 10 | try { 11 | const instance = await chrome({ 12 | chromePath: path.resolve(__dirname, `./dist/${channel}headless-chromium`), 13 | }) 14 | 15 | console.log('we got here. sweet.', instance) 16 | 17 | return { 18 | statusCode: 200, 19 | body: JSON.stringify({ 20 | event, 21 | instance, 22 | versionInfo: await cdp.Version(), 23 | }), 24 | headers: { 25 | 'Content-Type': 'application/json', 26 | }, 27 | } 28 | } catch (error) { 29 | console.log('error', error) 30 | 31 | return { 32 | statusCode: 200, 33 | body: JSON.stringify({ 34 | error, 35 | }), 36 | headers: { 37 | 'Content-Type': 'application/json', 38 | }, 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /packages/lambda/integration-test/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "integration-test", 3 | "version": "0.0.0", 4 | "private": true, 5 | "description": "", 6 | "main": "handler.js", 7 | "dependencies": { 8 | "chrome-remote-interface": "0.25.5" 9 | }, 10 | "devDependencies": { 11 | "serverless": "1.27.2" 12 | }, 13 | "scripts": { 14 | "test": "echo \"Error: no test specified\" && exit 1" 15 | }, 16 | "author": "", 17 | "license": "ISC" 18 | } 19 | -------------------------------------------------------------------------------- /packages/lambda/integration-test/serverless.yml: -------------------------------------------------------------------------------- 1 | service: serverless-chrome-lambda-pkg-test 2 | 3 | provider: 4 | name: aws 5 | runtime: nodejs8.10 6 | stage: dev 7 | region: us-east-1 8 | environment: 9 | DEBUG: "*" 10 | 11 | functions: 12 | test: 13 | description: serverless-chrome/lambda test 14 | memorySize: 1536 15 | timeout: 30 16 | handler: handler.run 17 | 18 | events: 19 | - http: 20 | path: package/lambda/test 21 | method: get 22 | -------------------------------------------------------------------------------- /packages/lambda/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@serverless-chrome/lambda", 3 | "version": "1.0.0-70", 4 | "description": "Run headless Chrome/Chromium on AWS Lambda", 5 | "author": "Marco Lüthy", 6 | "keywords": [ 7 | "serverless", 8 | "chrome", 9 | "chromium", 10 | "headless", 11 | "aws", 12 | "lambda" 13 | ], 14 | "main": "dist/bundle.cjs.js", 15 | "module": "dist/bundle.es.js", 16 | "files": [ 17 | "dist/bundle.cjs.js", 18 | "dist/bundle.es.js", 19 | "scripts/postinstall.js" 20 | ], 21 | "types": "index.d.ts", 22 | "repository": "git@github.com:adieuadieu/serverless-chrome.git", 23 | "bugs": { 24 | "url": "https://github.com/adieuadieu/serverless-chrome/issues" 25 | }, 26 | "homepage": "https://github.com/adieuadieu/serverless-chrome/tree/master/packages/lambda", 27 | "license": "MIT", 28 | "engines": { 29 | "node": ">= 6.10" 30 | }, 31 | "config": { 32 | "jsSrc": "src/" 33 | }, 34 | "scripts": { 35 | "clean": "rm -Rf dist/ ./**.zip", 36 | "test": "npm run test:integration", 37 | "test:integration": "scripts/test-integration.sh", 38 | "build": "rollup -c", 39 | "dev": "rollup -c -w", 40 | "prepublishOnly": "npm run clean && npm run build", 41 | "postinstall": "node scripts/postinstall.js", 42 | "package-binaries": "scripts/package-binaries.sh", 43 | "latest-browser-versions": "scripts/latest-versions.sh", 44 | "upgrade-dependencies": "yarn upgrade-interactive --latest --exact" 45 | }, 46 | "dependencies": { 47 | "extract-zip": "1.6.6" 48 | }, 49 | "devDependencies": { 50 | "ava": "0.25.0", 51 | "babel-core": "6.26.3", 52 | "babel-preset-env": "1.7.0", 53 | "babel-register": "6.26.0", 54 | "chrome-launcher": "0.10.2", 55 | "rollup": "0.59.1", 56 | "rollup-plugin-babel": "3.0.4", 57 | "rollup-plugin-node-resolve": "3.3.0" 58 | }, 59 | "babel": { 60 | "sourceMaps": true, 61 | "presets": [ 62 | [ 63 | "env", 64 | { 65 | "modules": "commonjs", 66 | "targets": { 67 | "node": "6.10" 68 | } 69 | } 70 | ] 71 | ] 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /packages/lambda/rollup.config.js: -------------------------------------------------------------------------------- 1 | import babel from 'rollup-plugin-babel' 2 | import resolve from 'rollup-plugin-node-resolve' 3 | 4 | export default { 5 | input: 'src/index.js', 6 | output: [ 7 | { file: 'dist/bundle.cjs.js', format: 'cjs' }, 8 | { file: 'dist/bundle.es.js', format: 'es' }, 9 | ], 10 | plugins: [ 11 | resolve({ 12 | // module: true, // Default: true 13 | // jsnext: true, // Default: false 14 | // main: true, // Default: true 15 | extensions: ['.js'], // Default: ['.js'] 16 | // Lock the module search in this path (like a chroot). Module defined 17 | // outside this path will be mark has external 18 | // jail: './', // Default: '/' 19 | // If true, inspect resolved files to check that they are 20 | // ES2015 modules 21 | // modulesOnly: true, // Default: false 22 | }), 23 | babel({ 24 | babelrc: false, 25 | presets: [ 26 | [ 27 | 'env', 28 | { 29 | modules: false, 30 | targets: { 31 | node: '6.10', 32 | }, 33 | }, 34 | ], 35 | ], 36 | }), 37 | ], 38 | external: ['fs', 'child_process', 'net', 'http', 'path', 'chrome-launcher'], 39 | } 40 | -------------------------------------------------------------------------------- /packages/lambda/scripts/latest-versions.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # shellcheck shell=dash 3 | 4 | # 5 | # Get current versions of browsers 6 | # 7 | # Requires jq 8 | # 9 | # Usage: ./latest-versions.sh [chromium|firefox] 10 | # 11 | 12 | set -e 13 | 14 | cd "$(dirname "$0")/.." 15 | 16 | PACKAGE_DIRECTORY=$(pwd) 17 | 18 | version() { 19 | BUILD_NAME=$1 20 | 21 | cd "$PACKAGE_DIRECTORY/builds/$BUILD_NAME" 22 | 23 | echo "--- $BUILD_NAME ---" 24 | 25 | CHANNEL_LIST=$(jq -r ". | keys | tostring" ./version.json | sed -e 's/[^A-Za-z,]//g' | tr , '\n') 26 | 27 | while IFS= read -r CHANNEL; do 28 | # Skip empty lines and lines starting with a hash (#): 29 | [ -z "$CHANNEL" ] || [ "${CHANNEL#\#}" != "$CHANNEL" ] && continue 30 | 31 | OUR_VERSION=$(jq -r ".$CHANNEL" version.json) 32 | LATEST_VERSION=$(./latest.sh "$CHANNEL") 33 | 34 | if [ "$OUR_VERSION" != "$LATEST_VERSION" ]; then 35 | STATUS="new; our version is $OUR_VERSION" 36 | else 37 | STATUS="current" 38 | fi 39 | 40 | echo "$CHANNEL: $LATEST_VERSION ($STATUS)" 41 | done << EOL 42 | $CHANNEL_LIST 43 | EOL 44 | } 45 | 46 | if [ ! -z "$1" ]; then 47 | version "$1" 48 | else 49 | cd "$PACKAGE_DIRECTORY/builds" 50 | 51 | for DOCKER_FILE in */latest.sh; do 52 | version "${DOCKER_FILE%%/*}" 53 | done 54 | fi 55 | -------------------------------------------------------------------------------- /packages/lambda/scripts/package-binaries.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # shellcheck shell=dash 3 | 4 | # 5 | # Builds specified or all headless browsers (chromium, firefox) version defined in package.json 6 | # 7 | # Requires Docker, jq, and zip 8 | # 9 | # Usage: ./build-binaries.sh [chromium|firefox] [stable|beta|dev] 10 | # 11 | 12 | set -e 13 | 14 | cd "$(dirname "$0")/.." 15 | 16 | PACKAGE_DIRECTORY=$(pwd) 17 | 18 | packageBinary() { 19 | BUILD_NAME=$1 20 | CHANNEL=$2 21 | 22 | cd "$PACKAGE_DIRECTORY/builds/$BUILD_NAME" 23 | 24 | DOCKER_IMAGE=headless-$BUILD_NAME-for-aws-lambda 25 | VERSION=$(jq -r ".$CHANNEL" version.json) 26 | BUILD_PATH="dist/$BUILD_NAME" 27 | ZIPFILE_PATH="$CHANNEL-headless-$BUILD_NAME-$VERSION-amazonlinux-2017-03.zip" 28 | 29 | if [ ! -f "dist/$ZIPFILE_PATH" ]; then 30 | echo "Packaging $BUILD_NAME version $VERSION ($CHANNEL)" 31 | 32 | mkdir -p "$BUILD_PATH" 33 | 34 | # Extract binary from docker image 35 | docker run -dt --rm --name "$DOCKER_IMAGE" "adieuadieu/$DOCKER_IMAGE:$VERSION" 36 | docker cp "$DOCKER_IMAGE":/bin/headless-"$BUILD_NAME" "$BUILD_PATH" 37 | docker stop "$DOCKER_IMAGE" 38 | 39 | # Package 40 | cd "$BUILD_PATH" 41 | zip -9 -D "../$ZIPFILE_PATH" "headless-$BUILD_NAME" 42 | cd ../../ 43 | 44 | # stick a copy in packages' dist/ for tests and local dev 45 | mkdir -p "$PACKAGE_DIRECTORY/dist" 46 | cp "$BUILD_PATH/headless-$BUILD_NAME" "$PACKAGE_DIRECTORY/dist/$CHANNEL-headless-$BUILD_NAME" 47 | 48 | # Cleanup 49 | rm -Rf "$BUILD_PATH" 50 | else 51 | echo "$BUILD_NAME version $VERSION was previously package. Skipping." 52 | fi 53 | } 54 | 55 | # main script 56 | 57 | if [ ! -z "$1" ]; then 58 | packageBinary "$1" "$2" 59 | else 60 | cd "$PACKAGE_DIRECTORY/builds" 61 | 62 | for DOCKER_FILE in */Dockerfile; do 63 | packageBinary "${DOCKER_FILE%%/*}" stable 64 | packageBinary "${DOCKER_FILE%%/*}" beta 65 | packageBinary "${DOCKER_FILE%%/*}" dev 66 | done 67 | fi 68 | -------------------------------------------------------------------------------- /packages/lambda/scripts/postinstall.js: -------------------------------------------------------------------------------- 1 | /* 2 | @TODO: checksum/crc check on archive using pkg.config.tarballChecksum 3 | */ 4 | const fs = require('fs') 5 | const path = require('path') 6 | const https = require('https') 7 | const extract = require('extract-zip') 8 | 9 | if ( 10 | process.env.NPM_CONFIG_SERVERLESS_CHROME_SKIP_DOWNLOAD || 11 | process.env.npm_config_serverless_chrome_skip_download 12 | ) { 13 | console.warn('Skipping Chromium download. ' + 14 | '"NPM_CONFIG_SERVERLESS_CHROME_SKIP_DOWNLOAD" was set in environment or NPM config.') 15 | return 16 | } 17 | 18 | const CHROMIUM_CHANNEL = 19 | process.env.NPM_CONFIG_CHROMIUM_CHANNEL || 20 | process.env.npm_config_chromiumChannel || 21 | process.env.npm_config_chromium_channel || 22 | 'stable' 23 | 24 | if (CHROMIUM_CHANNEL !== 'stable') { 25 | console.warn(`Downloading "${CHROMIUM_CHANNEL}" version of Chromium because` + 26 | ' "NPM_CONFIG_CHROMIUM_CHANNEL" was set in environment or NPM config.') 27 | } 28 | 29 | const RELEASE_DOWNLOAD_URL_BASE = 30 | 'https://github.com/adieuadieu/serverless-chrome/releases/download' 31 | 32 | function unlink (filePath) { 33 | return new Promise((resolve, reject) => { 34 | fs.unlink(filePath, error => (error ? reject(error) : resolve())) 35 | }) 36 | } 37 | 38 | function extractFile (file, destination) { 39 | return new Promise((resolve, reject) => { 40 | extract( 41 | file, 42 | { dir: destination }, 43 | error => (error ? reject(error) : resolve()) 44 | ) 45 | }) 46 | } 47 | 48 | function get (url, destination) { 49 | return new Promise((resolve, reject) => { 50 | https 51 | .get(url, (response) => { 52 | if (response.statusCode >= 300 && response.statusCode <= 400) { 53 | return get(response.headers.location, destination) 54 | .then(resolve) 55 | .catch(reject) 56 | } else if (response.statusCode !== 200) { 57 | fs.unlink(destination, () => null) 58 | return reject(new Error(`HTTP ${response.statusCode}: Could not download ${url}`)) 59 | } 60 | 61 | const file = fs.createWriteStream(destination) 62 | 63 | response.pipe(file) 64 | 65 | return file.on('finish', () => { 66 | file.close(resolve) 67 | }) 68 | }) 69 | .on('error', (error) => { 70 | fs.unlink(destination, () => null) 71 | reject(error) 72 | }) 73 | }) 74 | } 75 | 76 | function getChromium () { 77 | const ZIP_FILENAME = `${CHROMIUM_CHANNEL}-headless-chromium-amazonlinux-2.zip` 78 | const ZIP_URL = `${RELEASE_DOWNLOAD_URL_BASE}/v${ 79 | process.env.npm_package_version 80 | }/${ZIP_FILENAME}` 81 | const DOWNLOAD_PATH = path.resolve(__dirname, '..', ZIP_FILENAME) 82 | const EXTRACT_PATH = path.resolve(__dirname, '..', 'dist') 83 | 84 | if (fs.existsSync(path.resolve(EXTRACT_PATH, 'headless-chromium'))) { 85 | console.log('Precompiled headless Chromium binary for AWS Lambda previously downloaded. Skipping download.') 86 | return Promise.resolve() 87 | } 88 | 89 | console.log(`Downloading precompiled headless Chromium binary (${CHROMIUM_CHANNEL} channel) for AWS Lambda.`) 90 | 91 | return get(ZIP_URL, DOWNLOAD_PATH) 92 | .then(() => extractFile(DOWNLOAD_PATH, EXTRACT_PATH)) 93 | .then(() => console.log('Completed Headless Chromium download.')) 94 | .then(() => unlink(DOWNLOAD_PATH)) 95 | } 96 | 97 | if (require.main === module) { 98 | getChromium().catch((error) => { 99 | console.error(error) 100 | process.exit(1) 101 | }) 102 | } 103 | 104 | module.exports = { 105 | get, 106 | extractFile, 107 | } 108 | -------------------------------------------------------------------------------- /packages/lambda/scripts/test-integration.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # shellcheck shell=dash 3 | 4 | # 5 | # Runs a simple integration-test lambda handler in Docker 6 | # 7 | # Requires Docker 8 | # 9 | # Usage: ./integration-test.sh [stable|beta|dev] 10 | # 11 | 12 | set -e 13 | 14 | cd "$(dirname "$0")/.." 15 | 16 | CHANNEL=${1:-stable} 17 | 18 | if [ ! -d "dist/" ]; then 19 | ./scripts/package-binaries.sh chromium "$CHANNEL" 20 | npm run build 21 | fi 22 | 23 | (cd integration-test && npm install) 24 | 25 | docker run \ 26 | -v "$PWD/integration-test":/var/task \ 27 | -v "$PWD/dist":/var/task/dist \ 28 | lambci/lambda:nodejs8.10 \ 29 | handler.run \ 30 | "{\"channel\": \"$CHANNEL\"}" 31 | -------------------------------------------------------------------------------- /packages/lambda/src/flags.js: -------------------------------------------------------------------------------- 1 | const LOGGING_FLAGS = process.env.DEBUG 2 | ? ['--enable-logging', '--log-level=0'] 3 | : [] 4 | 5 | export default [ 6 | ...LOGGING_FLAGS, 7 | '--disable-dev-shm-usage', // disable /dev/shm tmpfs usage on Lambda 8 | 9 | // @TODO: review if these are still relevant: 10 | '--disable-gpu', 11 | '--single-process', // Currently wont work without this :-( 12 | 13 | // https://groups.google.com/a/chromium.org/d/msg/headless-dev/qqbZVZ2IwEw/Y95wJUh2AAAJ 14 | '--no-zygote', // helps avoid zombies 15 | 16 | '--no-sandbox', 17 | ] 18 | -------------------------------------------------------------------------------- /packages/lambda/src/index.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs' 2 | // import path from 'path' 3 | import LambdaChromeLauncher from './launcher' 4 | import { debug, processExists } from './utils' 5 | import DEFAULT_CHROME_FLAGS from './flags' 6 | 7 | const DEVTOOLS_PORT = 9222 8 | const DEVTOOLS_HOST = 'http://127.0.0.1' 9 | 10 | // Prepend NSS related libraries and binaries to the library path and path respectively on lambda. 11 | /* if (process.env.AWS_EXECUTION_ENV) { 12 | const nssSubPath = fs.readFileSync(path.join(__dirname, 'nss', 'latest'), 'utf8').trim(); 13 | const nssPath = path.join(__dirname, 'nss', subnssSubPathPath); 14 | 15 | process.env.LD_LIBRARY_PATH = path.join(nssPath, 'lib') + ':' + process.env.LD_LIBRARY_PATH; 16 | process.env.PATH = path.join(nssPath, 'bin') + ':' + process.env.PATH; 17 | } */ 18 | 19 | // persist the instance across invocations 20 | // when the *lambda* container is reused. 21 | let chromeInstance 22 | 23 | export default async function launch ({ 24 | flags = [], 25 | chromePath, 26 | port = DEVTOOLS_PORT, 27 | forceLambdaLauncher = false, 28 | } = {}) { 29 | const chromeFlags = [...DEFAULT_CHROME_FLAGS, ...flags] 30 | 31 | if (!chromeInstance || !processExists(chromeInstance.pid)) { 32 | if (process.env.AWS_EXECUTION_ENV || forceLambdaLauncher) { 33 | chromeInstance = new LambdaChromeLauncher({ 34 | chromePath, 35 | chromeFlags, 36 | port, 37 | }) 38 | } else { 39 | // This let's us use chrome-launcher in local development, 40 | // but omit it from the lambda function's zip artefact 41 | try { 42 | // eslint-disable-next-line 43 | const { Launcher: LocalChromeLauncher } = require('chrome-launcher') 44 | chromeInstance = new LocalChromeLauncher({ 45 | chromePath, 46 | chromeFlags: flags, 47 | port, 48 | }) 49 | } catch (error) { 50 | throw new Error('@serverless-chrome/lambda: Unable to find "chrome-launcher". ' + 51 | "Make sure it's installed if you wish to develop locally.") 52 | } 53 | } 54 | } 55 | 56 | debug('Spawning headless shell') 57 | 58 | const launchStartTime = Date.now() 59 | 60 | try { 61 | await chromeInstance.launch() 62 | } catch (error) { 63 | debug('Error trying to spawn chrome:', error) 64 | 65 | if (process.env.DEBUG) { 66 | debug( 67 | 'stdout log:', 68 | fs.readFileSync(`${chromeInstance.userDataDir}/chrome-out.log`, 'utf8') 69 | ) 70 | debug( 71 | 'stderr log:', 72 | fs.readFileSync(`${chromeInstance.userDataDir}/chrome-err.log`, 'utf8') 73 | ) 74 | } 75 | 76 | throw new Error('Unable to start Chrome. If you have the DEBUG env variable set,' + 77 | 'there will be more in the logs.') 78 | } 79 | 80 | const launchTime = Date.now() - launchStartTime 81 | 82 | debug(`It took ${launchTime}ms to spawn chrome.`) 83 | 84 | // unref the chrome instance, otherwise the lambda process won't end correctly 85 | /* @TODO: make this an option? 86 | There's an option to change callbackWaitsForEmptyEventLoop in the Lambda context 87 | http://docs.aws.amazon.com/lambda/latest/dg/nodejs-prog-model-context.html 88 | Which means you could log chrome output to cloudwatch directly 89 | without unreffing chrome. 90 | */ 91 | if (chromeInstance.chrome) { 92 | chromeInstance.chrome.removeAllListeners() 93 | chromeInstance.chrome.unref() 94 | } 95 | 96 | return { 97 | pid: chromeInstance.pid, 98 | port: chromeInstance.port, 99 | url: `${DEVTOOLS_HOST}:${chromeInstance.port}`, 100 | log: `${chromeInstance.userDataDir}/chrome-out.log`, 101 | errorLog: `${chromeInstance.userDataDir}/chrome-err.log`, 102 | pidFile: `${chromeInstance.userDataDir}/chrome.pid`, 103 | metaData: { 104 | launchTime, 105 | didLaunch: !!chromeInstance.pid, 106 | }, 107 | async kill () { 108 | // Defer killing chrome process to the end of the execution stack 109 | // so that the node process doesn't end before chrome exists, 110 | // avoiding chrome becoming orphaned. 111 | setTimeout(async () => { 112 | chromeInstance.kill() 113 | chromeInstance = undefined 114 | }, 0) 115 | }, 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /packages/lambda/src/index.test.js: -------------------------------------------------------------------------------- 1 | import test from 'ava' 2 | import * as chromeFinder from 'chrome-launcher/dist/chrome-finder' 3 | import launch from './index' 4 | 5 | const DEFAULT_TEST_FLAGS = ['--headless'] 6 | 7 | async function getLocalChromePath () { 8 | const installations = await chromeFinder[process.platform]() 9 | 10 | if (installations.length === 0) { 11 | throw new Error('No Chrome Installations Found') 12 | } 13 | 14 | return installations[0] 15 | } 16 | 17 | test.serial('Chrome should launch using LocalChromeLauncher', async (t) => { 18 | const chromePath = await getLocalChromePath() 19 | const chrome = launch({ 20 | flags: DEFAULT_TEST_FLAGS, 21 | chromePath, 22 | port: 9220, 23 | }) 24 | 25 | t.notThrows(chrome) 26 | 27 | const instance = await chrome 28 | 29 | t.truthy(instance.pid, 'pid should be set') 30 | t.truthy(instance.port, 'port should be set') 31 | t.is(instance.port, 9220, 'port should be 9220') 32 | 33 | instance.kill() 34 | }) 35 | 36 | // Covered by the integration-test. 37 | test('Chrome should launch using LambdaChromeLauncher', (t) => { 38 | // @TODO: proper test.. 39 | t.pass() 40 | }) 41 | -------------------------------------------------------------------------------- /packages/lambda/src/launcher.js: -------------------------------------------------------------------------------- 1 | /* 2 | Large portion of this is inspired by/taken from Lighthouse/chrome-launcher. 3 | It is Copyright Google Inc, licensed under Apache License, Version 2.0. 4 | https://github.com/GoogleChrome/lighthouse/blob/master/chrome-launcher/chrome-launcher.ts 5 | 6 | We ship a modified version because the original verion comes with too 7 | many dependencies which complicates packaging of serverless services. 8 | */ 9 | 10 | import path from 'path' 11 | import fs from 'fs' 12 | import { execSync, spawn } from 'child_process' 13 | import net from 'net' 14 | import http from 'http' 15 | import { delay, debug, makeTempDir, clearConnection } from './utils' 16 | import DEFAULT_CHROME_FLAGS from './flags' 17 | 18 | const CHROME_PATH = path.resolve(__dirname, './headless-chromium') 19 | 20 | export default class Launcher { 21 | constructor (options = {}) { 22 | const { 23 | chromePath = CHROME_PATH, 24 | chromeFlags = [], 25 | startingUrl = 'about:blank', 26 | port = 0, 27 | } = options 28 | 29 | this.tmpDirandPidFileReady = false 30 | this.pollInterval = 500 31 | this.pidFile = '' 32 | this.startingUrl = 'about:blank' 33 | this.outFile = null 34 | this.errFile = null 35 | this.chromePath = CHROME_PATH 36 | this.chromeFlags = [] 37 | this.requestedPort = 0 38 | this.userDataDir = '' 39 | this.port = 9222 40 | this.pid = null 41 | this.chrome = undefined 42 | 43 | this.options = options 44 | this.startingUrl = startingUrl 45 | this.chromeFlags = chromeFlags 46 | this.chromePath = chromePath 47 | this.requestedPort = port 48 | } 49 | 50 | get flags () { 51 | return [ 52 | ...DEFAULT_CHROME_FLAGS, 53 | `--remote-debugging-port=${this.port}`, 54 | `--user-data-dir=${this.userDataDir}`, 55 | '--disable-setuid-sandbox', 56 | ...this.chromeFlags, 57 | this.startingUrl, 58 | ] 59 | } 60 | 61 | prepare () { 62 | this.userDataDir = this.options.userDataDir || makeTempDir() 63 | this.outFile = fs.openSync(`${this.userDataDir}/chrome-out.log`, 'a') 64 | this.errFile = fs.openSync(`${this.userDataDir}/chrome-err.log`, 'a') 65 | this.pidFile = '/tmp/chrome.pid' 66 | this.tmpDirandPidFileReady = true 67 | } 68 | 69 | // resolves if ready, rejects otherwise 70 | isReady () { 71 | return new Promise((resolve, reject) => { 72 | const client = net.createConnection(this.port) 73 | 74 | client.once('error', (error) => { 75 | clearConnection(client) 76 | reject(error) 77 | }) 78 | 79 | client.once('connect', () => { 80 | clearConnection(client) 81 | resolve() 82 | }) 83 | }) 84 | } 85 | 86 | // resolves when debugger is ready, rejects after 10 polls 87 | waitUntilReady () { 88 | const launcher = this 89 | 90 | return new Promise((resolve, reject) => { 91 | let retries = 0; 92 | (function poll () { 93 | debug('Waiting for Chrome', retries) 94 | 95 | launcher 96 | .isReady() 97 | .then(() => { 98 | debug('Started Chrome') 99 | resolve() 100 | }) 101 | .catch((error) => { 102 | retries += 1 103 | 104 | if (retries > 10) { 105 | return reject(error) 106 | } 107 | 108 | return delay(launcher.pollInterval).then(poll) 109 | }) 110 | }()) 111 | }) 112 | } 113 | 114 | // resolves when chrome is killed, rejects after 10 polls 115 | waitUntilKilled () { 116 | return Promise.all([ 117 | new Promise((resolve, reject) => { 118 | let retries = 0 119 | const server = http.createServer() 120 | 121 | server.once('listening', () => { 122 | debug('Confirmed Chrome killed') 123 | server.close(resolve) 124 | }) 125 | 126 | server.on('error', () => { 127 | retries += 1 128 | 129 | debug('Waiting for Chrome to terminate..', retries) 130 | 131 | if (retries > 10) { 132 | reject(new Error('Chrome is still running after 10 retries')) 133 | } 134 | 135 | setTimeout(() => { 136 | server.listen(this.port) 137 | }, this.pollInterval) 138 | }) 139 | 140 | server.listen(this.port) 141 | }), 142 | new Promise((resolve) => { 143 | this.chrome.on('close', resolve) 144 | }), 145 | ]) 146 | } 147 | 148 | async spawn () { 149 | const spawnPromise = new Promise(async (resolve) => { 150 | if (this.chrome) { 151 | debug(`Chrome already running with pid ${this.chrome.pid}.`) 152 | return resolve(this.chrome.pid) 153 | } 154 | 155 | const chrome = spawn(this.chromePath, this.flags, { 156 | detached: true, 157 | stdio: ['ignore', this.outFile, this.errFile], 158 | }) 159 | 160 | this.chrome = chrome 161 | 162 | // unref the chrome instance, otherwise the lambda process won't end correctly 163 | if (chrome.chrome) { 164 | chrome.chrome.removeAllListeners() 165 | chrome.chrome.unref() 166 | } 167 | 168 | fs.writeFileSync(this.pidFile, chrome.pid.toString()) 169 | 170 | debug( 171 | 'Launcher', 172 | `Chrome running with pid ${chrome.pid} on port ${this.port}.` 173 | ) 174 | 175 | return resolve(chrome.pid) 176 | }) 177 | 178 | const pid = await spawnPromise 179 | await this.waitUntilReady() 180 | return pid 181 | } 182 | 183 | async launch () { 184 | if (this.requestedPort !== 0) { 185 | this.port = this.requestedPort 186 | 187 | // If an explict port is passed first look for an open connection... 188 | try { 189 | return await this.isReady() 190 | } catch (err) { 191 | debug( 192 | 'ChromeLauncher', 193 | `No debugging port found on port ${ 194 | this.port 195 | }, launching a new Chrome.` 196 | ) 197 | } 198 | } 199 | 200 | if (!this.tmpDirandPidFileReady) { 201 | this.prepare() 202 | } 203 | 204 | this.pid = await this.spawn() 205 | return Promise.resolve() 206 | } 207 | 208 | kill () { 209 | return new Promise(async (resolve, reject) => { 210 | if (this.chrome) { 211 | debug('Trying to terminate Chrome instance') 212 | 213 | try { 214 | process.kill(-this.chrome.pid) 215 | 216 | debug('Waiting for Chrome to terminate..') 217 | await this.waitUntilKilled() 218 | debug('Chrome successfully terminated.') 219 | 220 | this.destroyTemp() 221 | 222 | delete this.chrome 223 | return resolve() 224 | } catch (error) { 225 | debug('Chrome could not be killed', error) 226 | return reject(error) 227 | } 228 | } else { 229 | // fail silently as we did not start chrome 230 | return resolve() 231 | } 232 | }) 233 | } 234 | 235 | destroyTemp () { 236 | return new Promise((resolve) => { 237 | // Only clean up the tmp dir if we created it. 238 | if ( 239 | this.userDataDir === undefined || 240 | this.options.userDataDir !== undefined 241 | ) { 242 | return resolve() 243 | } 244 | 245 | if (this.outFile) { 246 | fs.closeSync(this.outFile) 247 | delete this.outFile 248 | } 249 | 250 | if (this.errFile) { 251 | fs.closeSync(this.errFile) 252 | delete this.errFile 253 | } 254 | 255 | return execSync(`rm -Rf ${this.userDataDir}`, resolve) 256 | }) 257 | } 258 | } 259 | -------------------------------------------------------------------------------- /packages/lambda/src/utils.js: -------------------------------------------------------------------------------- 1 | import { execSync } from 'child_process' 2 | 3 | export function clearConnection (client) { 4 | if (client) { 5 | client.removeAllListeners() 6 | client.end() 7 | client.destroy() 8 | client.unref() 9 | } 10 | } 11 | 12 | export function debug (...args) { 13 | return process.env.DEBUG 14 | ? console.log('@serverless-chrome/lambda:', ...args) 15 | : undefined 16 | } 17 | 18 | export async function delay (time) { 19 | return new Promise(resolve => setTimeout(resolve, time)) 20 | } 21 | 22 | export function makeTempDir () { 23 | return execSync('mktemp -d -t chrome.XXXXXXX') 24 | .toString() 25 | .trim() 26 | } 27 | 28 | /** 29 | * Checks if a process currently exists by process id. 30 | * @param pid number process id to check if exists 31 | * @returns boolean true if process exists, false if otherwise 32 | */ 33 | export function processExists (pid) { 34 | let exists = true 35 | try { 36 | process.kill(pid, 0) 37 | } catch (error) { 38 | exists = false 39 | } 40 | return exists 41 | } 42 | -------------------------------------------------------------------------------- /packages/serverless-plugin/README.md: -------------------------------------------------------------------------------- 1 | # Serverless-framework Headless Chrome Plugin 2 | 3 | A [Serverless-framework](https://github.com/serverless/serverless) plugin which bundles the [@serverless-chrome/lambda](/packages/lambda) package and ensures that Headless Chrome is running when your function handler is invoked. 4 | 5 | [](https://www.npmjs.com/package/serverless-plugin-chrome) 6 | 7 | ## Contents 8 | 1. [Installation](#installation) 9 | 1. [Setup](#setup) 10 | 1. [Examples](#examples) 11 | 1. [Local Development](#local-development) 12 | 1. [Configuration](#configuration) 13 | 14 | 15 | ## Installation 16 | 17 | Install with yarn: 18 | 19 | ```bash 20 | yarn add --dev serverless-plugin-chrome 21 | ``` 22 | 23 | Install with npm: 24 | 25 | ```bash 26 | npm install --save-dev serverless-plugin-chrome 27 | ``` 28 | 29 | Requires Node 6.10 runtime. 30 | 31 | 32 | ## Setup 33 | 34 | Add the following plugin to your `serverless.yml`: 35 | 36 | ```yaml 37 | plugins: 38 | - serverless-plugin-chrome 39 | ``` 40 | 41 | Then, in your handler code.. Do whatever you want. Chrome will be running! 42 | 43 | ```js 44 | const CDP = require('chrome-remote-interface') 45 | 46 | module.exports.hello = (event, context, callback, chrome) => { 47 | // Chrome is already running! 48 | 49 | CDP.Version() 50 | .then((versionInfo) => { 51 | callback(null, { 52 | statusCode: 200, 53 | body: JSON.stringify({ 54 | versionInfo, 55 | chrome, 56 | }), 57 | }) 58 | }) 59 | .catch((error) => { 60 | callback(null, { 61 | statusCode: 500, 62 | body: JSON.stringify({ 63 | error, 64 | }), 65 | }) 66 | }) 67 | } 68 | ``` 69 | 70 | Further details are available in the [Serverless Lambda example](/examples/serverless-framework/aws). 71 | 72 | 73 | ## Examples 74 | 75 | Example functions are available [here](/examples/serverless-framework). They include: 76 | 77 | - Screenshot capturing handler: takes a picture of a URL 78 | - print-to-PDF handler: turns a URL into a PDF 79 | 80 | 81 | ## Local Development 82 | 83 | Local development is supported. You must install the `chrome-launcher` package in your project. A locally installed version of Chrome will be launched. 84 | 85 | **Command line flags (or "switches")** 86 | 87 | The behavior of Chrome does vary between platforms. It may be necessary to experiment with flags to get the results you desire. On Lambda [default flags](/packages/lambda/src/flags.js) are used, but in development no default flags are used. 88 | 89 | ## Configuration 90 | 91 | You can pass custom flags with which to launch Chrome using the `custom` section in `serverless.yml`. For example: 92 | 93 | ```yaml 94 | plugins: 95 | - serverless-plugin-chrome 96 | 97 | custom: 98 | chrome: 99 | flags: 100 | - --window-size=1280,1696 # Letter size 101 | - --hide-scrollbars 102 | - --ignore-certificate-errors 103 | functions: 104 | - enableChromeOnThisFunctionName 105 | - mySuperChromeFunction 106 | ``` 107 | 108 | It is also possible to enable Chrome on only specific functions in your service using the `custom.chrome.functions` configuration. For example: 109 | 110 | ```yaml 111 | custom: 112 | chrome: 113 | functions: 114 | - enableChromeOnThisFunctionName 115 | - mySuperChromeFunction 116 | ``` 117 | 118 | You can enable debugging/logging output by specifying the DEBUG env variable in the provider section of `serverless.yml`. For example: 119 | 120 | ```yaml 121 | provider: 122 | name: aws 123 | runtime: nodejs6.10 124 | environment: 125 | DEBUG: "*" 126 | 127 | plugins: 128 | - serverless-plugin-chrome 129 | ``` 130 | 131 | 132 | ### Using with other plugins 133 | 134 | Load order is important. 135 | 136 | For example, if you're using the [serverless-webpack](https://github.com/serverless-heaven/serverless-webpack) plugin, your plugin section should be: 137 | 138 | ```yaml 139 | plugins: 140 | - serverless-plugin-chrome # 1st 141 | - serverless-webpack 142 | ``` 143 | 144 | However, with the [serverless-plugin-typescript](https://github.com/graphcool/serverless-plugin-typescript) plugin, the order is: 145 | 146 | ```yaml 147 | plugins: 148 | - serverless-plugin-typescript 149 | - serverless-plugin-chrome # 2nd 150 | ``` 151 | 152 | 153 | ## Troubleshooting 154 | 155 | <details id="ts-aws-client-timeout"> 156 | <summary>I keep getting a timeout error when deploying and it's really annoying.</summary> 157 | 158 | Indeed, that is annoying. I've had the same problem, and so that's why it's now here in this troubleshooting section. This may be an issue in the underlying AWS SDK when using a slower Internet connection. Try changing the `AWS_CLIENT_TIMEOUT` environment variable to a higher value. For example, in your command prompt enter the following and try deploying again: 159 | 160 | ```bash 161 | export AWS_CLIENT_TIMEOUT=3000000 162 | ``` 163 | </details> 164 | 165 | <details id="ts-argh"> 166 | <summary>Aaaaaarggghhhhhh!!!</summary> 167 | 168 | Uuurrrggghhhhhh! Have you tried [filing an Issue](https://github.com/adieuadieu/serverless-chrome/issues/new)? 169 | </details> 170 | -------------------------------------------------------------------------------- /packages/serverless-plugin/integration-test/.serverless_plugins/serverless-plugin-chrome: -------------------------------------------------------------------------------- 1 | ../../ -------------------------------------------------------------------------------- /packages/serverless-plugin/integration-test/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "serverless-plugin-chrome-e2e", 3 | "version": "0.0.0", 4 | "private": true, 5 | "description": "", 6 | "main": "handler.js", 7 | "scripts": { 8 | "test": "echo \"Error: no test specified\" && exit 1", 9 | "deploy": "serverless deploy", 10 | "invoke": "serverless invoke --function test" 11 | }, 12 | "author": "Marco Lüthy", 13 | "license": "MIT", 14 | "dependencies": { 15 | "chrome-remote-interface": "0.25.5" 16 | }, 17 | "devDependencies": { 18 | "@serverless-chrome/lambda": "1.0.0-44", 19 | "serverless": "1.27.2", 20 | "serverless-plugin-typescript": "1.1.5" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /packages/serverless-plugin/integration-test/serverless.yml: -------------------------------------------------------------------------------- 1 | service: serverless-chrome-sls-plugin-test 2 | 3 | provider: 4 | name: aws 5 | runtime: nodejs8.10 6 | stage: dev 7 | region: us-east-1 8 | environment: 9 | DEBUG: "*" 10 | 11 | plugins: 12 | # - serverless-plugin-typescript 13 | - serverless-plugin-chrome 14 | 15 | custom: 16 | chrome: 17 | flags: 18 | - --window-size=1280,1696 # Letter size 19 | - --hide-scrollbars 20 | - --ignore-certificate-errors 21 | functions: 22 | - test 23 | - anotherTest 24 | 25 | functions: 26 | test: 27 | handler: src/handler.default 28 | anotherTest: 29 | handler: src/anotherHandler.default 30 | package: 31 | # individually: true 32 | noChromeHere: 33 | handler: src/noChrome.default 34 | 35 | 36 | # typescript-test: 37 | # handler: src/typescript-handler.default 38 | -------------------------------------------------------------------------------- /packages/serverless-plugin/integration-test/src/anotherHandler.js: -------------------------------------------------------------------------------- 1 | const cdp = require('chrome-remote-interface') 2 | 3 | module.exports.default = async (event, context, callback, chrome) => ({ 4 | versionInfo: await cdp.Version(), 5 | chrome, 6 | }) 7 | -------------------------------------------------------------------------------- /packages/serverless-plugin/integration-test/src/handler.js: -------------------------------------------------------------------------------- 1 | const cdp = require('chrome-remote-interface') 2 | 3 | module.exports.default = async (event, context, callback, chrome) => ({ 4 | versionInfo: await cdp.Version(), 5 | chrome, 6 | }) 7 | -------------------------------------------------------------------------------- /packages/serverless-plugin/integration-test/src/noChrome.js: -------------------------------------------------------------------------------- 1 | module.exports.default = () => ({ 2 | message: 'No chrome here :-(', 3 | }) 4 | -------------------------------------------------------------------------------- /packages/serverless-plugin/integration-test/src/typescript-handler.ts: -------------------------------------------------------------------------------- 1 | import * as cdp from 'chrome-remote-interface' 2 | 3 | export default async function (event, context, callback, chrome) { 4 | return { 5 | versionInfo: await cdp.Version(), 6 | chrome, 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /packages/serverless-plugin/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "serverless-plugin-chrome", 3 | "version": "1.0.0-70", 4 | "description": "A Serverless-framework plugin that takes care of running headless Chrome so that you can move on with getting things done.", 5 | "keywords": [ 6 | "serverless", 7 | "serverless-framework", 8 | "chrome", 9 | "chromium", 10 | "headless", 11 | "aws", 12 | "lambda" 13 | ], 14 | "main": "dist/index.js", 15 | "module": "dist/index.es.js", 16 | "files": [ 17 | "dist", 18 | "src", 19 | "package.json", 20 | "README.md" 21 | ], 22 | "scripts": { 23 | "clean": "rm -Rf dist/", 24 | "test": "npm run test:integration", 25 | "test:integration": "scripts/test-integration.sh", 26 | "watch:test": "ava --watch", 27 | "build": "rollup -c", 28 | "dev": "rollup -c -w", 29 | "prepublishOnly": "npm run clean && npm run build", 30 | "upgrade-dependencies": "yarn upgrade-interactive --latest --exact" 31 | }, 32 | "repository": { 33 | "type": "git", 34 | "url": "https://github.com/adieuadieu/serverless-chrome.git" 35 | }, 36 | "author": "Marco Lüthy", 37 | "license": "MIT", 38 | "bugs": { 39 | "url": "https://github.com/adieuadieu/serverless-chrome/issues" 40 | }, 41 | "homepage": "https://github.com/adieuadieu/serverless-chrome/tree/master/packages/serverless-plugin", 42 | "dependencies": { 43 | "@serverless-chrome/lambda": "1.0.0-70", 44 | "fs-p": "2.0.0", 45 | "globby": "6.1.0" 46 | }, 47 | "devDependencies": { 48 | "ava": "0.25.0", 49 | "babel-core": "6.26.3", 50 | "babel-plugin-transform-object-rest-spread": "6.26.0", 51 | "babel-preset-env": "1.7.0", 52 | "babel-register": "6.26.0", 53 | "chrome-launcher": "0.10.2", 54 | "rollup": "0.59.1", 55 | "rollup-plugin-babel": "3.0.4", 56 | "rollup-plugin-node-resolve": "3.3.0" 57 | }, 58 | "peerDependences": { 59 | "serverless": "^2.32.0" 60 | }, 61 | "babel": { 62 | "sourceMaps": true, 63 | "presets": [ 64 | [ 65 | "env", 66 | { 67 | "modules": "commonjs", 68 | "targets": { 69 | "node": "6.10" 70 | } 71 | } 72 | ] 73 | ] 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /packages/serverless-plugin/rollup.config.js: -------------------------------------------------------------------------------- 1 | import resolve from 'rollup-plugin-node-resolve' 2 | // import commonjs from 'rollup-plugin-commonjs' 3 | import babel from 'rollup-plugin-babel' 4 | 5 | export default { 6 | input: 'src/index.js', 7 | output: [ 8 | { file: 'dist/index.js', format: 'cjs', sourcemap: true }, 9 | { file: 'dist/index.es.js', format: 'es', sourcemap: true }, 10 | ], 11 | 12 | plugins: [ 13 | resolve({ 14 | // module: true, // Default: true 15 | // jsnext: true, // Default: false 16 | // main: true, // Default: true 17 | extensions: ['.js'], // Default: ['.js'] 18 | // Lock the module search in this path (like a chroot). Module defined 19 | // outside this path will be mark has external 20 | // jail: './', // Default: '/' 21 | // If true, inspect resolved files to check that they are 22 | // ES2015 modules 23 | // modulesOnly: true, // Default: false 24 | }), 25 | // commonjs({}), 26 | babel({ 27 | babelrc: false, 28 | plugins: ['transform-object-rest-spread'], 29 | presets: [ 30 | [ 31 | 'env', 32 | { 33 | modules: false, 34 | targets: { 35 | node: '6.10', 36 | }, 37 | }, 38 | ], 39 | ], 40 | }), 41 | ], 42 | external: ['path', 'globby', 'fs-p'], 43 | } 44 | -------------------------------------------------------------------------------- /packages/serverless-plugin/scripts/test-integration.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # shellcheck shell=dash 3 | 4 | # 5 | # Runs a simple integration-test lambda handler in Docker 6 | # 7 | # Requires Docker, jq 8 | # 9 | # Usage: ./integration-test.sh 10 | # 11 | 12 | set -e 13 | 14 | cd "$(dirname "$0")/.." 15 | 16 | # PACKAGE_DIRECTORY=$(pwd) 17 | TEST_DIRECTORY=".test" 18 | 19 | npm run build 20 | 21 | cd integration-test 22 | 23 | ./node_modules/.bin/serverless package 24 | unzip -o -d "$TEST_DIRECTORY" .serverless/**.zip 25 | 26 | CHROMIUM_VERSION=$(docker run \ 27 | -v "$PWD/$TEST_DIRECTORY":/var/task \ 28 | lambci/lambda:nodejs8.10 \ 29 | src/handler.default | \ 30 | jq -re '.versionInfo.Browser') 31 | 32 | rm -Rf "$TEST_DIRECTORY" 33 | 34 | echo "Chromium version $CHROMIUM_VERSION" 35 | -------------------------------------------------------------------------------- /packages/serverless-plugin/src/constants.js: -------------------------------------------------------------------------------- 1 | export const SERVERLESS_FOLDER = '.serverless' 2 | export const BUILD_FOLDER = '.build' 3 | 4 | export const SUPPORTED_PROVIDERS = ['aws'] 5 | export const SUPPORTED_RUNTIMES = ['nodejs6.10', 'nodejs8.10'] 6 | 7 | export const INCLUDES = [ 8 | 'node_modules/@serverless-chrome/lambda/package.json', 9 | 'node_modules/@serverless-chrome/lambda/dist/bundle.cjs.js', 10 | 'node_modules/@serverless-chrome/lambda/dist/headless-chromium', 11 | ] 12 | -------------------------------------------------------------------------------- /packages/serverless-plugin/src/index.js: -------------------------------------------------------------------------------- 1 | /* 2 | @TODO: 3 | - handle package.individually? 4 | https://github.com/serverless/serverless/blob/master/lib/plugins/package/lib/packageService.js#L37 5 | - support for enabling chrome only on specific functions? 6 | - instead of including fs-p dep, use the fs methods from the Utils class provided by Serverless 7 | or use fs-extra directly. 8 | - config option to, instead of including chrome in artifact zip, download it on 9 | cold-start invocations this could be useful for development, instead of having 10 | to upload 50MB each deploy 11 | - tests. 12 | - custom.chrome.functions breaks when a wrapped and non-wrapped function have the 13 | same handler.js file 14 | */ 15 | 16 | import * as path from 'path' 17 | import * as fs from 'fs-p' // deprecated. use fs-extra? 18 | import globby from 'globby' 19 | 20 | import { SERVERLESS_FOLDER, BUILD_FOLDER, INCLUDES } from './constants' 21 | import { 22 | throwIfUnsupportedProvider, 23 | throwIfUnsupportedRuntime, 24 | throwIfWrongPluginOrder, 25 | getHandlerFileAndExportName, 26 | } from './utils' 27 | 28 | const wrapperTemplateMap = { 29 | 'aws-nodejs6.10': 'wrapper-aws-nodejs.js', 30 | 'aws-nodejs8.10': 'wrapper-aws-nodejs.js', 31 | } 32 | 33 | export default class ServerlessChrome { 34 | constructor (serverless, options) { 35 | this.serverless = serverless 36 | this.options = options 37 | 38 | const { 39 | provider: { name: providerName, runtime }, 40 | plugins, 41 | } = serverless.service 42 | 43 | throwIfUnsupportedProvider(providerName) 44 | throwIfUnsupportedRuntime(runtime) 45 | throwIfWrongPluginOrder(plugins) 46 | 47 | this.hooks = { 48 | 'before:offline:start:init': this.beforeCreateDeploymentArtifacts.bind(this), 49 | 'before:package:createDeploymentArtifacts': this.beforeCreateDeploymentArtifacts.bind(this), 50 | 'after:package:createDeploymentArtifacts': this.afterCreateDeploymentArtifacts.bind(this), 51 | 'before:invoke:local:invoke': this.beforeCreateDeploymentArtifacts.bind(this), 52 | 'after:invoke:local:invoke': this.cleanup.bind(this), 53 | 54 | 'before:webpack:package:packExternalModules': this.webpackPackageBinaries.bind(this), 55 | } 56 | 57 | // only mess with the service path if we're not already known to be within a .build folder 58 | this.messWithServicePath = !plugins.includes('serverless-plugin-typescript') 59 | 60 | // annoyingly we have to do stuff differently if using serverless-webpack plugin. lame. 61 | this.webpack = plugins.includes('serverless-webpack') 62 | } 63 | 64 | async webpackPackageBinaries () { 65 | const { config: { servicePath }, service } = this.serverless 66 | const packagedIdividually = service.package && service.package.individually 67 | 68 | if (packagedIdividually) { 69 | const functionsToCopyTo = 70 | (service.custom && service.custom.chrome && service.custom.chrome.functions) || 71 | service.getAllFunctions() 72 | 73 | await Promise.all(functionsToCopyTo.map(async (functionName) => { 74 | await fs.copy( 75 | path.join(servicePath, 'node_modules/@serverless-chrome/lambda/dist/headless-chromium'), 76 | path.resolve(servicePath, `.webpack/${functionName}/headless-chromium`) 77 | ) 78 | })) 79 | } else { 80 | await fs.copy( 81 | path.join(servicePath, 'node_modules/@serverless-chrome/lambda/dist/headless-chromium'), 82 | path.resolve(servicePath, '.webpack/service/headless-chromium') 83 | ) 84 | } 85 | } 86 | 87 | async beforeCreateDeploymentArtifacts () { 88 | const { 89 | config, 90 | cli, 91 | utils, 92 | service, 93 | service: { 94 | provider: { name: providerName, runtime }, 95 | }, 96 | } = this.serverless 97 | 98 | const functionsToWrap = 99 | (service.custom && 100 | service.custom.chrome && 101 | service.custom.chrome.functions) || 102 | service.getAllFunctions() 103 | 104 | service.package.include = service.package.include || [] 105 | service.package.patterns = service.package.patterns || [] 106 | 107 | cli.log('Injecting Headless Chrome...') 108 | 109 | // Save original service path and functions 110 | this.originalServicePath = config.servicePath 111 | 112 | // Fake service path so that serverless will know what to zip 113 | // Unless, we're already in a .build folder from another plugin 114 | if (this.messWithServicePath) { 115 | config.servicePath = path.join(this.originalServicePath, BUILD_FOLDER) 116 | 117 | if (!fs.existsSync(config.servicePath)) { 118 | fs.mkdirpSync(config.servicePath) 119 | } 120 | 121 | // include node_modules into build 122 | if ( 123 | !fs.existsSync(path.resolve(path.join(BUILD_FOLDER, 'node_modules'))) 124 | ) { 125 | fs.symlinkSync( 126 | path.resolve('node_modules'), 127 | path.resolve(path.join(BUILD_FOLDER, 'node_modules')), 128 | 'junction' 129 | ) 130 | } 131 | 132 | // include any "extras" from the "include" section 133 | const files = await globby( 134 | [...service.package.include, ...service.package.patterns, '**', '!node_modules/**'], 135 | { 136 | cwd: this.originalServicePath, 137 | } 138 | ) 139 | 140 | files.forEach((filename) => { 141 | const sourceFile = path.resolve(path.join(this.originalServicePath, filename)) 142 | const destFileName = path.resolve(path.join(config.servicePath, filename)) 143 | 144 | const dirname = path.dirname(destFileName) 145 | 146 | if (!fs.existsSync(dirname)) { 147 | fs.mkdirpSync(dirname) 148 | } 149 | 150 | if (!fs.existsSync(destFileName)) { 151 | fs.copySync(sourceFile, destFileName) 152 | } 153 | }) 154 | } 155 | 156 | // Add our node_modules dependencies to the package includes 157 | service.package.patterns = [...service.package.patterns, ...INCLUDES] 158 | 159 | await Promise.all(functionsToWrap.map(async (functionName) => { 160 | const { handler } = service.getFunction(functionName) 161 | const { filePath, fileName, exportName } = getHandlerFileAndExportName(handler) 162 | const handlerCodePath = path.join(config.servicePath, filePath) 163 | 164 | const originalFileRenamed = `${utils.generateShortId()}___${fileName}` 165 | 166 | const customPluginOptions = 167 | (service.custom && service.custom.chrome) || {} 168 | 169 | const launcherOptions = { 170 | ...customPluginOptions, 171 | flags: customPluginOptions.flags || [], 172 | chromePath: this.webpack ? '/var/task/headless-chromium' : undefined, 173 | } 174 | 175 | // Read in the wrapper handler code template 176 | const wrapperTemplate = await utils.readFile(path.resolve( 177 | __dirname, 178 | '..', 179 | 'src', 180 | wrapperTemplateMap[`${providerName}-${runtime}`] 181 | )) 182 | 183 | // Include the original handler via require 184 | const wrapperCode = wrapperTemplate 185 | .replace( 186 | "'REPLACE_WITH_HANDLER_REQUIRE'", 187 | `require('./${originalFileRenamed}')` 188 | ) 189 | .replace("'REPLACE_WITH_OPTIONS'", JSON.stringify(launcherOptions)) 190 | .replace(/REPLACE_WITH_EXPORT_NAME/gm, exportName) 191 | 192 | // Move the original handler's file aside 193 | await fs.move( 194 | path.resolve(handlerCodePath, fileName), 195 | path.resolve(handlerCodePath, originalFileRenamed) 196 | ) 197 | 198 | // Write the wrapper code to the function's handler path 199 | await utils.writeFile( 200 | path.resolve(handlerCodePath, fileName), 201 | wrapperCode 202 | ) 203 | })) 204 | } 205 | 206 | async afterCreateDeploymentArtifacts () { 207 | if (this.messWithServicePath) { 208 | // Copy .build to .serverless 209 | await fs.copy( 210 | path.join(this.originalServicePath, BUILD_FOLDER, SERVERLESS_FOLDER), 211 | path.join(this.originalServicePath, SERVERLESS_FOLDER) 212 | ) 213 | 214 | // this.serverless.service.package.artifact = path.join( 215 | // this.originalServicePath, 216 | // SERVERLESS_FOLDER 217 | // path.basename(this.serverless.service.package.artifact) 218 | // ) 219 | 220 | // Cleanup after everything is copied 221 | await this.cleanup() 222 | } 223 | } 224 | 225 | async cleanup () { 226 | // Restore service path 227 | this.serverless.config.servicePath = this.originalServicePath 228 | 229 | // Remove temp build folder 230 | fs.removeSync(path.join(this.originalServicePath, BUILD_FOLDER)) 231 | } 232 | } 233 | -------------------------------------------------------------------------------- /packages/serverless-plugin/src/utils.js: -------------------------------------------------------------------------------- 1 | import * as path from 'path' 2 | import { SUPPORTED_PROVIDERS, SUPPORTED_RUNTIMES } from './constants' 3 | 4 | export function throwIfUnsupportedProvider (provider) { 5 | if (!SUPPORTED_PROVIDERS.includes(provider)) { 6 | throw new Error('The "serverless-plugin-chrome" plugin currently only supports AWS Lambda. ' + 7 | `Your service is using the "${provider}" provider.`) 8 | } 9 | } 10 | 11 | export function throwIfUnsupportedRuntime (runtime) { 12 | if (!SUPPORTED_RUNTIMES.includes(runtime)) { 13 | throw new Error('The "serverless-plugin-chrome" plugin only supports the Node.js 6.10 or 8.10 runtimes. ' + 14 | `Your service is using the "${runtime}" provider.`) 15 | } 16 | } 17 | 18 | export function throwIfWrongPluginOrder (plugins) { 19 | const comesBefore = ['serverless-plugin-typescript'] 20 | const comesAfter = ['serverless-webpack'] 21 | 22 | const ourIndex = plugins.indexOf('serverless-plugin-chrome') 23 | 24 | plugins.forEach((plugin, index) => { 25 | if (comesBefore.includes(plugin) && ourIndex < index) { 26 | throw new Error(`The plugin "${plugin}" should appear before the "serverless-plugin-chrome"` + 27 | ' plugin in the plugin configuration section of serverless.yml.') 28 | } 29 | 30 | if (comesAfter.includes(plugin) && ourIndex > index) { 31 | throw new Error(`The plugin "${plugin}" should appear after the "serverless-plugin-chrome"` + 32 | ' plugin in the plugin configuration section of serverless.yml.') 33 | } 34 | }) 35 | } 36 | 37 | export function getHandlerFileAndExportName (handler = '') { 38 | const fileParts = handler.split('.') 39 | const exportName = fileParts.pop() 40 | const file = fileParts.join('.') 41 | 42 | return { 43 | filePath: path.dirname(file), 44 | fileName: `${path.basename(file)}.js`, // is it OK to assume .js? 45 | exportName, 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /packages/serverless-plugin/src/utils.test.js: -------------------------------------------------------------------------------- 1 | import test from 'ava' 2 | import { 3 | throwIfUnsupportedProvider, 4 | throwIfUnsupportedRuntime, 5 | throwIfWrongPluginOrder, 6 | getHandlerFileAndExportName, 7 | } from './utils' 8 | 9 | test('throwIfUnsupportedProvider()', (t) => { 10 | t.throws(() => throwIfUnsupportedProvider('bogus'), Error) 11 | t.notThrows(() => throwIfUnsupportedProvider('aws')) 12 | }) 13 | 14 | test('throwIfUnsupportedRuntime()', (t) => { 15 | t.throws(() => throwIfUnsupportedRuntime('bogus'), Error) 16 | t.notThrows(() => throwIfUnsupportedRuntime('nodejs6.10')) 17 | }) 18 | 19 | test('throwIfWrongPluginOrder()', (t) => { 20 | t.throws( 21 | () => throwIfWrongPluginOrder(['serverless-plugin-chrome', 'serverless-plugin-typescript']), 22 | Error, 23 | 'Should throw when our plugin comes before the "serverless-plugin-typescript" plugin.' 24 | ) 25 | 26 | t.notThrows( 27 | () => throwIfWrongPluginOrder(['serverless-plugin-typescript', 'serverless-plugin-chrome']), 28 | 'Should not throw when our plugin comes after the "serverless-plugin-typescript" plugin.' 29 | ) 30 | 31 | t.notThrows( 32 | () => throwIfWrongPluginOrder(['serverless-plugin-chrome']), 33 | 'Should not throw when only our plugin is used.' 34 | ) 35 | 36 | t.throws( 37 | () => 38 | throwIfWrongPluginOrder([ 39 | 'serverless-webpack', 40 | 'bogus', 41 | 'serverless-plugin-chrome', 42 | 'serverless-plugin-typescript', 43 | ]), 44 | Error, 45 | 'Should throw when plugins are in order known to not work.' 46 | ) 47 | 48 | t.notThrows( 49 | () => 50 | throwIfWrongPluginOrder([ 51 | 'bogus', 52 | 'serverless-plugin-typescript', 53 | 'serverless-plugin-chrome', 54 | 'bogus', 55 | 'serverless-webpack', 56 | ]), 57 | 'Should not throw when plugins are in order known to work.' 58 | ) 59 | }) 60 | 61 | test('getHandlerFileAndExportName()', (t) => { 62 | const { filePath, fileName, exportName } = getHandlerFileAndExportName('nested/test/handler.foobar.test') 63 | 64 | t.is(filePath, 'nested/test') 65 | t.is(fileName, 'handler.foobar.js') 66 | t.is(exportName, 'test') 67 | }) 68 | -------------------------------------------------------------------------------- /packages/serverless-plugin/src/wrapper-aws-nodejs.js: -------------------------------------------------------------------------------- 1 | const launch = require('@serverless-chrome/lambda') 2 | 3 | const handler = 'REPLACE_WITH_HANDLER_REQUIRE' 4 | const options = 'REPLACE_WITH_OPTIONS' 5 | 6 | module.exports.REPLACE_WITH_EXPORT_NAME = function ensureHeadlessChrome ( 7 | event, 8 | context, 9 | callback 10 | ) { 11 | return (typeof launch === 'function' ? launch : launch.default)(options) 12 | .then(instance => 13 | handler.REPLACE_WITH_EXPORT_NAME(event, context, callback, instance)) 14 | .catch((error) => { 15 | console.error( 16 | 'Error occured in serverless-plugin-chrome wrapper when trying to ' + 17 | 'ensure Chrome for REPLACE_WITH_EXPORT_NAME() handler.', 18 | options, 19 | error 20 | ) 21 | 22 | callback(error) 23 | }) 24 | } 25 | -------------------------------------------------------------------------------- /scripts/ci-daily.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # shellcheck shell=dash 3 | 4 | # 5 | # Builds docker images 6 | # 7 | # Usage: ./ci-daily.sh stable|beta|dev [chromium|firefox] 8 | # 9 | 10 | set -e 11 | 12 | cd "$(dirname "$0")/.." 13 | 14 | PROJECT_DIRECTORY=$(pwd) 15 | PACKAGE_DIRECTORY="$PROJECT_DIRECTORY/packages/lambda" 16 | 17 | CHANNEL=${1:-stable} 18 | 19 | if [ -z "$AWS_ACCESS_KEY_ID" ] || [ -z "$AWS_SECRET_ACCESS_KEY" ]; then 20 | echo "Missing required AWS_ACCESS_KEY_ID and/or AWS_SECRET_ACCESS_KEY environment variables" 21 | exit 1 22 | fi 23 | 24 | if [ -z "$AWS_IAM_INSTANCE_ARN" ]; then 25 | echo "Missing required AWS_IAM_INSTANCE_ARN environment variables" 26 | exit 1 27 | fi 28 | 29 | launch_if_new() { 30 | BUILD_NAME=$1 31 | 32 | cd "$PACKAGE_DIRECTORY/builds/$BUILD_NAME" 33 | 34 | LATEST_VERSION=$(./latest.sh "$CHANNEL") 35 | DOCKER_IMAGE=headless-$BUILD_NAME-for-aws-lambda 36 | 37 | if "$PROJECT_DIRECTORY/scripts/docker-image-exists.sh" "adieuadieu/$DOCKER_IMAGE" "$LATEST_VERSION"; then 38 | echo "$BUILD_NAME version $LATEST_VERSION was previously built. Skipping build." 39 | else 40 | "$PROJECT_DIRECTORY/scripts/ec2-build.sh" "$BUILD_NAME" "$CHANNEL" "$LATEST_VERSION" 41 | fi 42 | } 43 | 44 | # main script 45 | 46 | cd "$PROJECT_DIRECTORY" 47 | 48 | if [ ! -z "$2" ]; then 49 | launch_if_new "$2" 50 | else 51 | cd "$PACKAGE_DIRECTORY/builds" 52 | 53 | for DOCKER_FILE in */Dockerfile; do 54 | launch_if_new "${DOCKER_FILE%%/*}" 55 | done 56 | fi 57 | -------------------------------------------------------------------------------- /scripts/docker-build-image.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # shellcheck shell=dash 3 | 4 | # 5 | # Builds docker images 6 | # 7 | # Requires Docker, jq, and curl 8 | # 9 | # Usage: ./docker-build-image.sh stable|beta|dev [chromium|firefox] [version|git-tag] 10 | # 11 | 12 | set -e 13 | 14 | cd "$(dirname "$0")/.." 15 | 16 | CHANNEL=${1:-stable} 17 | BROWSER=${2:-} 18 | VERSION=${3:-} 19 | 20 | DOCKER_ORG=${DOCKER_ORG:-adieuadieu} 21 | 22 | PROJECT_DIRECTORY=$(pwd) 23 | PACKAGE_DIRECTORY="$PROJECT_DIRECTORY/packages/lambda" 24 | 25 | if [ -z "$1" ]; then 26 | echo "Missing required channel argument" 27 | exit 1 28 | fi 29 | 30 | build() { 31 | BUILD_NAME=$1 32 | DOCKER_IMAGE=headless-$BUILD_NAME-for-aws-lambda 33 | 34 | if [ -z "$VERSION" ]; then 35 | VERSION=$(./latest.sh "$CHANNEL") 36 | fi 37 | 38 | cd "$PACKAGE_DIRECTORY/builds/$BUILD_NAME" 39 | 40 | if "$PROJECT_DIRECTORY/scripts/docker-image-exists.sh" \ 41 | "$DOCKER_ORG/$DOCKER_IMAGE" "$VERSION" \ 42 | && [ -z "$FORCE_NEW_BUILD" ]; then 43 | echo "$BUILD_NAME version $VERSION was previously built. Skipping build." 44 | else 45 | echo "Building Docker image $BUILD_NAME version $VERSION" 46 | 47 | # Build in Docker 48 | docker build \ 49 | --compress \ 50 | -t "$DOCKER_ORG/$DOCKER_IMAGE-build:$VERSION" \ 51 | --build-arg VERSION="$VERSION" \ 52 | "build" 53 | 54 | mkdir -p dist/ 55 | 56 | # Run the container 57 | docker run -dt --rm \ 58 | --name "$DOCKER_IMAGE-build" \ 59 | -p 9222:9222 \ 60 | "$DOCKER_ORG/$DOCKER_IMAGE-build:$VERSION" 61 | 62 | # Give the container and browser some time to start up 63 | sleep 10 64 | 65 | # Test the build and return if it doesn't run 66 | if ! curl -fs http://localhost:9222/json/version; then 67 | echo "Unable to correctly run and connect to build via Docker." 68 | 69 | # @TODO: this is specific to chromium...... 70 | echo "Here's some output:" 71 | 72 | docker logs "$DOCKER_IMAGE-build" 73 | 74 | docker run --init --rm \ 75 | --entrypoint="/bin/headless-chromium" \ 76 | "$DOCKER_ORG/$DOCKER_IMAGE-build:$VERSION" \ 77 | --no-sandbox --disable-gpu http://google.com/ 78 | return 79 | fi 80 | 81 | # Extract the binary produced in the build 82 | docker cp "$DOCKER_IMAGE-build:/bin/headless-$BUILD_NAME" dist/ 83 | docker stop "$DOCKER_IMAGE-build" 84 | 85 | # Create the public Docker image 86 | # We do this because the image in which be build ends up being huge 87 | # due to the source code and build dependencies 88 | docker build \ 89 | --compress \ 90 | -t "$DOCKER_ORG/$DOCKER_IMAGE:$VERSION" \ 91 | --build-arg VERSION="$VERSION" \ 92 | "." 93 | 94 | if [ -n "$DO_PUSH" ]; then 95 | echo "Pushing image to Docker hub" 96 | 97 | # Only tag stable channel as latest 98 | if [ "$CHANNEL" = "stable" ]; then 99 | docker tag \ 100 | "$DOCKER_ORG/$DOCKER_IMAGE:$VERSION" \ 101 | "$DOCKER_ORG/$DOCKER_IMAGE:latest" 102 | fi 103 | 104 | docker tag \ 105 | "$DOCKER_ORG/$DOCKER_IMAGE:$VERSION" \ 106 | "$DOCKER_ORG/$DOCKER_IMAGE:$CHANNEL" 107 | 108 | docker push "$DOCKER_ORG/$DOCKER_IMAGE" 109 | fi 110 | 111 | # 112 | # Upload a zipped binary to S3 if S3_BUCKET is set 113 | # Prints a presigned S3 URL to the zip file 114 | # 115 | if [ -n "$S3_BUCKET" ]; then 116 | ZIPFILE="headless-$BUILD_NAME-$VERSION-amazonlinux-2017-03.zip" 117 | S3_OBJECT_URI="s3://$S3_BUCKET/$BUILD_NAME/$CHANNEL/$ZIPFILE" 118 | 119 | ( 120 | cd dist 121 | zip -9 -D "$ZIPFILE" "headless-$BUILD_NAME" 122 | ) 123 | 124 | aws s3 \ 125 | cp "dist/$ZIPFILE" \ 126 | "$S3_OBJECT_URI" \ 127 | --region "$AWS_REGION" 128 | 129 | S3_PRESIGNED_URL=$(aws s3 presign \ 130 | "$S3_OBJECT_URI" \ 131 | --region "$AWS_REGION" \ 132 | --expires-in 86400 \ 133 | ) 134 | 135 | printf "\n\nBinary archive URL: %s\n\n" "$S3_PRESIGNED_URL" 136 | fi 137 | fi 138 | } 139 | 140 | # main script 141 | 142 | cd "$PROJECT_DIRECTORY" 143 | 144 | # Docker Login & enable docker push on successful login 145 | if scripts/docker-login.sh; then 146 | DO_PUSH=1 147 | fi 148 | 149 | if [ ! -z "$BROWSER" ]; then 150 | build "$BROWSER" 151 | else 152 | cd "$PACKAGE_DIRECTORY/builds" 153 | 154 | for DOCKER_FILE in */Dockerfile; do 155 | build "${DOCKER_FILE%%/*}" 156 | done 157 | fi 158 | -------------------------------------------------------------------------------- /scripts/docker-image-exists.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # shellcheck shell=dash 3 | 4 | # 5 | # Check if a Docker image and tag exists on Docker Hub 6 | # 7 | # Requires curl 8 | # 9 | # Usage: ./docker-image-exists image [tag] 10 | # 11 | 12 | set -e 13 | 14 | # better implementation here: https://github.com/blueimp/shell-scripts/blob/master/bin/docker-image-exists.sh 15 | # ref: https://stackoverflow.com/a/39731444/845713 16 | 17 | curl --silent -f -L "https://index.docker.io/v1/repositories/$1/tags/$2" > /dev/null 18 | -------------------------------------------------------------------------------- /scripts/docker-login.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # shellcheck shell=dash 3 | 4 | # 5 | # Login to Docker Hub checking for credentials before doing so. 6 | # 7 | # Usage: ./docker-login.sh 8 | # 9 | 10 | set -e 11 | 12 | # Slightly naive and assumes we're not using multiple registries 13 | 14 | if [ -f ~/.docker/config.json ] && \ 15 | [ "$(jq -re '.auths | length' ~/.docker/config.json)" -gt 0 ]; then 16 | echo "Already logged in to Docker Hub" 17 | exit 0 18 | fi 19 | 20 | if [ -z "$DOCKER_USER" ] || [ -z "$DOCKER_PASS" ]; then 21 | echo "Missing required DOCKER_USER and/or DOCKER_PASS environment variables" 22 | exit 1 23 | fi 24 | 25 | # Log in to Docker Hub 26 | docker login -u "$DOCKER_USER" -p "$DOCKER_PASS" 27 | -------------------------------------------------------------------------------- /scripts/docker-pull-image.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # shellcheck shell=dash 3 | 4 | # 5 | # Pull projects' latest Docker images 6 | # 7 | # Requires Docker 8 | # 9 | # Usage: ./docker-pull-image.sh [chromium|firefox] [base] 10 | # 11 | 12 | set -e 13 | 14 | cd "$(dirname "$0")/.." 15 | 16 | PROJECT_DIRECTORY=$(pwd) 17 | PACKAGE_DIRECTORY="$PROJECT_DIRECTORY/packages/lambda" 18 | 19 | # better implementation here: https://github.com/blueimp/shell-scripts/blob/master/bin/docker-image-exists.sh 20 | # ref: https://stackoverflow.com/a/39731444/845713 21 | docker_tag_exists() { 22 | curl --silent -f -L "https://index.docker.io/v1/repositories/$1/tags/$2" > /dev/null 23 | } 24 | 25 | build() { 26 | BUILD_NAME=$1 27 | DO_BASE_BUILD=$2 28 | 29 | cd "$PACKAGE_DIRECTORY/builds/$BUILD_NAME" 30 | 31 | LATEST_VERSION=$(./latest.sh) 32 | 33 | DOCKER_IMAGE=headless-$BUILD_NAME-for-aws-lambda 34 | 35 | if [ -n "$DO_BASE_BUILD" ]; then 36 | DOCKER_IMAGE=$BUILD_NAME-for-amazonlinux-base 37 | fi 38 | 39 | if docker_tag_exists "adieuadieu/$DOCKER_IMAGE" "$LATEST_VERSION"; then 40 | echo "Pulling $BUILD_NAME version $LATEST_VERSION." 41 | docker pull "adieuadieu/$DOCKER_IMAGE:$LATEST_VERSION" 42 | else 43 | echo "Docker image adieuadieu/$DOCKER_IMAGE:$LATEST_VERSION doesn't exist." 44 | exit 1 45 | fi 46 | } 47 | 48 | # main script 49 | 50 | cd "$PROJECT_DIRECTORY" 51 | 52 | if [ ! -z "$1" ]; then 53 | build "$1" "$2" 54 | else 55 | cd "$PACKAGE_DIRECTORY/builds" 56 | 57 | for DOCKER_FILE in */Dockerfile; do 58 | build "${DOCKER_FILE%%/*}" "$2" 59 | done 60 | fi 61 | -------------------------------------------------------------------------------- /scripts/ec2-build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # shellcheck shell=dash 3 | 4 | # 5 | # Perform a build using a AWS EC2 Spot Instance 6 | # 7 | # Usage: ./ec2-build.sh chromium|firefox stable|beta|dev version|tag 8 | # 9 | # 10 | # Further documentation: 11 | # https://github.com/adieuadieu/serverless-chrome/blob/develop/docs/automation.md 12 | # https://github.com/adieuadieu/serverless-chrome/blob/develop/docs/chrome.md 13 | # 14 | 15 | set -e 16 | 17 | cd "$(dirname "$0")/.." 18 | 19 | AWS_REGION=${AWS_REGION:-us-east-1} 20 | 21 | PROJECT_DIRECTORY=$(pwd) 22 | BUILD_NAME=${1:-chromium} 23 | CHANNEL=${2:-stable} 24 | VERSION=${3:-master} 25 | DOCKER_ORG=${DOCKER_ORG:-adieuadieu} 26 | S3_BUCKET=${S3_BUCKET:-} 27 | FORCE_NEW_BUILD=${FORCE_NEW_BUILD:-} 28 | 29 | # 30 | # Check for some required env variables 31 | # 32 | if [ -z "$AWS_ACCESS_KEY_ID" ] || [ -z "$AWS_SECRET_ACCESS_KEY" ]; then 33 | echo "Missing required AWS_ACCESS_KEY_ID and/or AWS_SECRET_ACCESS_KEY environment variables" 34 | exit 1 35 | fi 36 | 37 | if [ -z "$AWS_IAM_INSTANCE_ARN" ]; then 38 | echo "Missing required AWS_IAM_INSTANCE_ARN environment variables" 39 | exit 1 40 | fi 41 | 42 | echo "Launching EC2 spot instance to build $BUILD_NAME version $VERSION ($CHANNEL channel)" 43 | 44 | # 45 | # Pre-process and base64 encode user-data payload (a shell script) 46 | # run on instance startup via cloudinit 47 | # 48 | USER_DATA=$(sed -e "s/INSERT_CHANNEL_HERE/$CHANNEL/g" "$PROJECT_DIRECTORY/aws/user-data.sh" | \ 49 | sed -e "s/INSERT_BROWSER_HERE/$BUILD_NAME/g" | \ 50 | sed -e "s/INSERT_VERSION_HERE/$VERSION/g" | \ 51 | sed -e "s/INSERT_DOCKER_ORG_HERE/$DOCKER_ORG/g" | \ 52 | sed -e "s/INSERT_S3_BUCKET_HERE/$S3_BUCKET/g" | \ 53 | sed -e "s/INSERT_FORCE_NEW_BUILD_HERE/$FORCE_NEW_BUILD/g" | \ 54 | base64 \ 55 | ) 56 | 57 | # 58 | # Setup JSON payload which sets/configures the AWS CLI command 59 | # 60 | JSON=$(jq -c -r \ 61 | ".LaunchSpecification.UserData |= \"$USER_DATA\" | .LaunchSpecification.IamInstanceProfile.Arn |= \"$AWS_IAM_INSTANCE_ARN\"" \ 62 | "$PROJECT_DIRECTORY/aws/ec2-spot-instance-specification.json" 63 | ) 64 | 65 | # 66 | # Request the spot instance / launch 67 | # ref: http://docs.aws.amazon.com/cli/latest/reference/ec2/request-spot-instances.html 68 | # --valid-until "2018-08-22T00:00:00.000Z" \ 69 | # 70 | aws ec2 request-spot-instances \ 71 | --region "$AWS_REGION" \ 72 | --valid-until "$(date -u +%FT%T.000Z -d '6 hours')" \ 73 | --cli-input-json "$JSON" | \ 74 | jq -r ".SpotInstanceRequests[].Status" 75 | -------------------------------------------------------------------------------- /scripts/link-package.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # shellcheck shell=dash 3 | 4 | # 5 | # Link packages 6 | # 7 | # Usage: ./link-packages.sh packageDirectory linkedDependencyDirectory 8 | # 9 | 10 | set -e 11 | 12 | cd "$(dirname "$0")/../" 13 | 14 | PROJECT_DIRECTORY=$(pwd) 15 | PACKAGE_PATH=$1 16 | LINKED_PACKAGE=$2 17 | 18 | # cd packages/ 19 | 20 | # for PACKAGE in */package.json; do 21 | # PACKAGE_NAME="${PACKAGE%%/*}" 22 | # cd "$PROJECT_DIRECTORY/packages/$PACKAGE_NAME" || exit 1 23 | # npm link 24 | # done 25 | 26 | 27 | cd "$PROJECT_DIRECTORY/$PACKAGE_PATH" 28 | npm link "$PROJECT_DIRECTORY/packages/$LINKED_PACKAGE" 29 | -------------------------------------------------------------------------------- /scripts/release.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # shellcheck shell=dash 3 | 4 | # 5 | # Creates a GitHub release or pre-release for tagged commits. 6 | # 7 | # Requires git, curl and jq. 8 | # 9 | # Usage: ./release.sh 10 | # 11 | 12 | set -e 13 | 14 | cd "$(dirname "$0")/../" 15 | 16 | PROJECT_DIRECTORY=$(pwd) 17 | PACKAGE_DIRECTORY="$PROJECT_DIRECTORY/packages/lambda" 18 | 19 | if [ -z "$GITHUB_TOKEN" ]; then 20 | printf "Error: Missing %s environment variable.\n" \ 21 | GITHUB_TOKEN >&2 22 | exit 1 23 | fi 24 | 25 | if ! npm whoami -s && [ -z "$NPM_TOKEN" ]; then 26 | echo "Error: Missing NPM credentials or NPM_TOKEN environment variable." 27 | exit 1 28 | fi 29 | 30 | GITHUB_ORG=adieuadieu 31 | GITHUB_REPO=serverless-chrome 32 | export GITHUB_ORG 33 | export GITHUB_REPO 34 | 35 | # Get the tag for the current commit: 36 | git fetch origin 'refs/tags/*:refs/tags/*' 37 | TAG="$(git describe --exact-match --tags 2> /dev/null || true)" 38 | 39 | if [ -z "$TAG" ]; then 40 | echo "Not a tagged commit. Skipping release.." 41 | exit 42 | fi 43 | 44 | # Check if this is a pre-release version (denoted by a hyphen): 45 | if [ "${TAG#*-}" != "$TAG" ]; then 46 | PRE=true 47 | else 48 | PRE=false 49 | fi 50 | 51 | RELEASE_TEMPLATE='{ 52 | "tag_name": "%s", 53 | "name": "%s", 54 | "prerelease": %s, 55 | "draft": %s 56 | }' 57 | 58 | RELEASE_BODY="This is an automated release.\n\n" 59 | 60 | create_release_draft() { 61 | # shellcheck disable=SC2034 62 | local ouput 63 | # shellcheck disable=SC2059 64 | if output=$(curl \ 65 | --silent \ 66 | --fail \ 67 | --request POST \ 68 | --header "Authorization: token $GITHUB_TOKEN" \ 69 | --header 'Content-Type: application/json' \ 70 | --data "$(printf "$RELEASE_TEMPLATE" "$TAG" "$TAG" "$PRE" true)" \ 71 | "https://api.github.com/repos/$GITHUB_ORG/$GITHUB_REPO/releases"); 72 | then 73 | RELEASE_ID=$(echo "$output" | jq -re '.id') 74 | UPLOAD_URL_TEMPLATE=$(echo "$output" | jq -re '.upload_url') 75 | fi 76 | } 77 | 78 | upload_release_asset() { 79 | # shellcheck disable=SC2059 80 | curl \ 81 | --silent \ 82 | --fail \ 83 | --request POST \ 84 | --header "Authorization: token $GITHUB_TOKEN" \ 85 | --header 'Content-Type: application/zip' \ 86 | --data-binary "@$1" \ 87 | "${UPLOAD_URL_TEMPLATE%\{*}?name=$2&label=$1" \ 88 | > /dev/null 89 | } 90 | 91 | update_release_body() { 92 | # shellcheck disable=SC2059 93 | curl \ 94 | --silent \ 95 | --fail \ 96 | --request PATCH \ 97 | --header "Authorization: token $GITHUB_TOKEN" \ 98 | --header 'Content-Type: application/json' \ 99 | --data "{\"body\":\"$RELEASE_BODY\"}" \ 100 | "https://api.github.com/repos/$GITHUB_ORG/$GITHUB_REPO/releases/$1" \ 101 | > /dev/null 102 | } 103 | 104 | publish_release() { 105 | # shellcheck disable=SC2059 106 | curl \ 107 | --silent \ 108 | --fail \ 109 | --request PATCH \ 110 | --header "Authorization: token $GITHUB_TOKEN" \ 111 | --header 'Content-Type: application/json' \ 112 | --data "{\"draft\":false, \"tag_name\": \"$TAG\"}" \ 113 | "https://api.github.com/repos/$GITHUB_ORG/$GITHUB_REPO/releases/$1" \ 114 | > /dev/null 115 | } 116 | 117 | echo "Creating release draft $TAG" 118 | create_release_draft 119 | 120 | # upload zipped builds 121 | cd "$PACKAGE_DIRECTORY/builds" 122 | 123 | for BUILD in */Dockerfile; do 124 | BUILD_NAME="${BUILD%%/*}" 125 | 126 | cd "$PACKAGE_DIRECTORY/builds/$BUILD_NAME" || exit 1 127 | 128 | CHANNEL_LIST=$(jq -r ". | keys | tostring" ./version.json | sed -e 's/[^A-Za-z,]//g' | tr , '\n') 129 | 130 | while IFS= read -r CHANNEL; do 131 | # Skip empty lines and lines starting with a hash (#): 132 | [ -z "$CHANNEL" ] || [ "${CHANNEL#\#}" != "$CHANNEL" ] && continue 133 | 134 | VERSION=$(jq -r ".$CHANNEL" version.json) 135 | ZIPFILE=$CHANNEL-headless-$BUILD_NAME-$VERSION-amazonlinux-2017-03.zip 136 | 137 | ( 138 | if [ ! -f "dist/$ZIPFILE" ]; then 139 | echo "$BUILD_NAME version $VERSION has not been packaged. Packaging ..." 140 | ../../scripts/package-binaries.sh "$BUILD_NAME" "$CHANNEL" 141 | fi 142 | 143 | cd dist/ 144 | 145 | echo "Uploading $ZIPFILE to GitHub" 146 | 147 | upload_release_asset "$ZIPFILE" "$CHANNEL-headless-$BUILD_NAME-amazonlinux-2.zip" 148 | ) 149 | 150 | RELEASE_BODY="$RELEASE_BODY$BUILD_NAME $VERSION ($CHANNEL channel) for Amazon Linux 2\n" 151 | done << EOL 152 | $CHANNEL_LIST 153 | EOL 154 | 155 | done 156 | 157 | update_release_body "$RELEASE_ID" 158 | 159 | publish_release "$RELEASE_ID" 160 | 161 | 162 | # 163 | # Publish NPM packages 164 | # 165 | 166 | # Add NPM token to .npmrc if not logged in 167 | if [ -n "$NPM_TOKEN" ] && ! npm whoami -s; then 168 | echo "//registry.npmjs.org/:_authToken=$NPM_TOKEN" > "$HOME/.npmrc" 169 | fi 170 | 171 | while IFS= read -r PACKAGE; do 172 | cd "$PROJECT_DIRECTORY/packages/$PACKAGE" 173 | npm publish 174 | done << EOL 175 | lambda 176 | serverless-plugin 177 | EOL 178 | -------------------------------------------------------------------------------- /scripts/sync-package-versions.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # shellcheck shell=dash 3 | 4 | # 5 | # Synchronises package version with the version in the project root. 6 | # Also synchronizes package dependencies 7 | # 8 | # Note: probably would be better to use something like Lerna here......... 9 | # 10 | # Requires jq. 11 | # 12 | # Usage: ./sync-package-versions.sh [version] 13 | # 14 | 15 | set -e 16 | 17 | cd "$(dirname "$0")/../" 18 | 19 | PROJECT_DIRECTORY=$(pwd) 20 | PROJECT_VERSION=$(jq -r ".version" package.json) 21 | VERSION=${1:-"$PROJECT_VERSION"} 22 | 23 | update() { 24 | PACKAGE_NAME="$1" 25 | 26 | cd "$PROJECT_DIRECTORY/$PACKAGE_NAME" || exit 1 27 | 28 | PACKAGE_VERSION=$(jq -r ".version" package.json) 29 | 30 | if [ "$PACKAGE_VERSION" != "$VERSION" ]; then 31 | echo "Updating $PACKAGE_NAME version ..." 32 | 33 | JSON=$(jq -r \ 34 | ".version |= \"$VERSION\"" \ 35 | package.json 36 | ) 37 | 38 | HAS_LAMBDA_DEPENDENCY=$(echo "$JSON" | \ 39 | jq -r \ 40 | ".dependencies | has(\"@serverless-chrome/lambda\")" 41 | ) 42 | 43 | if [ "$HAS_LAMBDA_DEPENDENCY" = "true" ]; then 44 | JSON=$(echo "$JSON" | \ 45 | jq -r \ 46 | ".dependencies.\"@serverless-chrome/lambda\" |= \"$VERSION\"" 47 | ) 48 | fi 49 | 50 | HAS_SERVERLESS_PLUGIN_DEPENDENCY=$(echo "$JSON" | \ 51 | jq -r \ 52 | ".devDependencies | has(\"serverless-plugin-chrome\")" 53 | ) 54 | 55 | if [ "$HAS_SERVERLESS_PLUGIN_DEPENDENCY" = "true" ]; then 56 | JSON=$(echo "$JSON" | \ 57 | jq -r \ 58 | ".devDependencies.\"serverless-plugin-chrome\" |= \"$VERSION\"" 59 | ) 60 | fi 61 | 62 | echo "$JSON" > package.json 63 | 64 | # @TODO: run yarn to update lockfile 65 | # chicken-before-the-egg problem. The following won't work because yarn 66 | # will try to look for the new package version on the npm registry, but 67 | # of course it won't find it because it's not been published yet.. 68 | #yarn --ignore-scripts --non-interactive 69 | else 70 | echo "$PACKAGE_NAME version $VERSION is already latest. Skipping.." 71 | fi 72 | } 73 | 74 | 75 | # 76 | # Synchronize all packages 77 | # 78 | cd packages/ 79 | 80 | for PACKAGE in */package.json; do 81 | PACKAGE_NAME="${PACKAGE%%/*}" 82 | 83 | update "packages/$PACKAGE_NAME" 84 | done 85 | 86 | 87 | # 88 | # Synchronize examples 89 | # 90 | update "examples/serverless-framework/aws" 91 | -------------------------------------------------------------------------------- /scripts/update-browser-versions.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # shellcheck shell=dash 3 | 4 | # 5 | # Checks latest browser versions and updates configurations and commits to repo 6 | # 7 | # Requires git, curl and jq. 8 | # 9 | # Usage: ./update-browser-versions.sh 10 | # 11 | 12 | set -e 13 | 14 | cd "$(dirname "$0")/../" 15 | 16 | PROJECT_DIRECTORY=$(pwd) 17 | PACKAGE_DIRECTORY="$PROJECT_DIRECTORY/packages/lambda" 18 | 19 | UPDATES=0 20 | 21 | 22 | # on CircleCI, setup git user email & name 23 | if [ -z "$(git config user.email)" ] && [ -n "$GIT_USER_EMAIL" ]; then 24 | git config --global user.email "$GIT_USER_EMAIL" 25 | fi 26 | 27 | if [ -z "$(git config user.name)" ] && [ -n "$GIT_USER_NAME" ]; then 28 | git config --global user.name "$GIT_USER_NAME" 29 | fi 30 | 31 | git checkout master 32 | 33 | 34 | cd "$PACKAGE_DIRECTORY/builds" 35 | 36 | for BUILD in */Dockerfile; do 37 | BUILD_NAME="${BUILD%%/*}" 38 | 39 | cd "$BUILD_NAME" || exit 40 | 41 | CHANNEL_LIST=$(jq -r ". | keys | tostring" ./version.json | sed -e 's/[^A-Za-z,]//g' | tr , '\n') 42 | DOCKER_IMAGE=headless-$BUILD_NAME-for-aws-lambda 43 | 44 | # Iterate over the channels: 45 | while IFS= read -r CHANNEL; do 46 | # Skip empty lines and lines starting with a hash (#): 47 | [ -z "$CHANNEL" ] || [ "${CHANNEL#\#}" != "$CHANNEL" ] && continue 48 | 49 | CURRENT_VERSION=$(jq -r ".$CHANNEL" version.json) 50 | LATEST_VERSION=$(./latest.sh "$CHANNEL") 51 | 52 | if [ "$CURRENT_VERSION" != "$LATEST_VERSION" ]; then 53 | if "$PROJECT_DIRECTORY/scripts/docker-image-exists.sh" "adieuadieu/$DOCKER_IMAGE" "$LATEST_VERSION"; then 54 | echo "$BUILD_NAME has new version $LATEST_VERSION. Updating configuration ..." 55 | 56 | JSON=$(jq -r ".$CHANNEL |= \"$LATEST_VERSION\"" version.json) 57 | echo "$JSON" > version.json 58 | 59 | git add version.json 60 | git commit -m "chore ($BUILD_NAME): bump $CHANNEL channel version to $LATEST_VERSION" --no-verify 61 | 62 | # Only create new tag/release when stable channel has new version 63 | if [ "$CHANNEL" = "stable" ]; then 64 | UPDATES=1 65 | fi 66 | else 67 | echo "Docker image for adieuadieu/$DOCKER_IMAGE:$LATEST_VERSION does not exist. Exiting." && exit 1 68 | fi 69 | else 70 | echo "$BUILD_NAME $CHANNEL version $CURRENT_VERSION is already latest. Nothing to update." 71 | fi 72 | 73 | done << EOL 74 | $CHANNEL_LIST 75 | EOL 76 | 77 | cd ../ 78 | done 79 | 80 | cd "$PROJECT_DIRECTORY" 81 | 82 | # If there are new browser versions (on the stable channel) we create a new version 83 | if [ "$UPDATES" -eq 1 ]; then 84 | npm version prerelease --no-git-tag-version # @TODO: change to 'minor' if stable-channel 85 | 86 | PROJECT_VERSION=$(jq -r ".version" package.json) 87 | 88 | ./scripts/sync-package-versions.sh 89 | 90 | git commit -a -m "v$PROJECT_VERSION" 91 | git tag "v$PROJECT_VERSION" 92 | git push --set-upstream origin master 93 | git push --tags 94 | else 95 | git push --set-upstream origin master 96 | fi 97 | --------------------------------------------------------------------------------