├── .github └── workflows │ └── scorecard.yml ├── .gitignore ├── .style.yapf ├── CONTRIBUTING.md ├── LICENSE ├── Makefile ├── README.md ├── SECURITY.md ├── docs ├── SCHEMA.md ├── configuration.md ├── java.md ├── node-js.md ├── python.md └── troubleshooting.md ├── pylintrc ├── samples ├── README.md ├── app_engine_flexible_prerequisites.md ├── app_engine_standard_prerequisites.md ├── gce_samples_prerequisites.md ├── java │ ├── appengine-flexible │ │ ├── README.md │ │ ├── helloworld-springboot │ │ │ ├── README.md │ │ │ ├── pom.xml │ │ │ └── src │ │ │ │ ├── main │ │ │ │ ├── appengine │ │ │ │ │ └── app.yaml │ │ │ │ ├── docker │ │ │ │ │ └── Dockerfile │ │ │ │ ├── java │ │ │ │ │ └── com │ │ │ │ │ │ └── example │ │ │ │ │ │ └── java │ │ │ │ │ │ ├── HelloController.java │ │ │ │ │ │ └── HelloWorldApplication.java │ │ │ │ └── resources │ │ │ │ │ └── application.properties │ │ │ │ └── test │ │ │ │ └── java │ │ │ │ └── com │ │ │ │ └── example │ │ │ │ └── java │ │ │ │ └── HelloControllerTest.java │ │ └── helloworld │ │ │ ├── README.md │ │ │ ├── pom.xml │ │ │ └── src │ │ │ └── main │ │ │ ├── appengine │ │ │ └── app.yaml │ │ │ ├── docker │ │ │ └── Dockerfile │ │ │ └── java │ │ │ └── com │ │ │ └── example │ │ │ └── flexible │ │ │ └── helloworld │ │ │ └── HelloServlet.java │ ├── appengine-java11 │ │ ├── README.md │ │ ├── appengine-simple-jetty-main │ │ │ ├── README.md │ │ │ ├── pom.xml │ │ │ └── src │ │ │ │ └── main │ │ │ │ └── java │ │ │ │ └── com │ │ │ │ └── example │ │ │ │ └── appengine │ │ │ │ └── demo │ │ │ │ └── jettymain │ │ │ │ └── Main.java │ │ ├── helloworld-servlet │ │ │ ├── README.md │ │ │ ├── pom.xml │ │ │ └── src │ │ │ │ └── main │ │ │ │ ├── appengine │ │ │ │ └── app.yaml │ │ │ │ └── java │ │ │ │ └── com │ │ │ │ └── example │ │ │ │ └── appengine │ │ │ │ └── HelloAppEngine.java │ │ └── simple-server │ │ │ ├── .gitignore │ │ │ ├── Main.java │ │ │ ├── README.md │ │ │ └── app.yaml │ ├── appengine-java8 │ │ ├── README.md │ │ └── helloworld │ │ │ ├── README.md │ │ │ ├── pom.xml │ │ │ └── src │ │ │ ├── main │ │ │ ├── java │ │ │ │ └── com │ │ │ │ │ └── example │ │ │ │ │ └── appengine │ │ │ │ │ └── java8 │ │ │ │ │ └── HelloAppEngine.java │ │ │ └── webapp │ │ │ │ ├── WEB-INF │ │ │ │ └── appengine-web.xml │ │ │ │ └── index.jsp │ │ │ └── test │ │ │ └── java │ │ │ └── com │ │ │ └── example │ │ │ └── appengine │ │ │ └── java8 │ │ │ └── HelloAppEngineTest.java │ └── gce │ │ ├── README.md │ │ ├── config │ │ └── base │ │ │ ├── etc │ │ │ └── java-util-logging.properties │ │ │ ├── modules │ │ │ └── gce.mod │ │ │ └── resources │ │ │ └── jetty-logging.properties │ │ ├── deploy.sh │ │ ├── pom.xml │ │ ├── src │ │ ├── main │ │ │ ├── appengine │ │ │ │ └── app.yaml │ │ │ ├── java │ │ │ │ └── com │ │ │ │ │ └── example │ │ │ │ │ └── getstarted │ │ │ │ │ └── basicactions │ │ │ │ │ └── HelloworldController.java │ │ │ └── webapp │ │ │ │ └── WEB-INF │ │ │ │ └── web.xml │ │ └── test │ │ │ └── java │ │ │ └── com │ │ │ └── example │ │ │ └── getstarted │ │ │ └── basicactions │ │ │ └── UserJourneyTestIT.java │ │ ├── startup-script.sh │ │ └── teardown.sh ├── node-js │ ├── appengine-flexible │ │ ├── README.md │ │ ├── app.js │ │ ├── app.yaml │ │ └── package.json │ ├── appengine-standard │ │ ├── README.md │ │ ├── app.js │ │ ├── app.yaml │ │ └── package.json │ └── gce │ │ ├── README.md │ │ ├── app.js │ │ ├── package.json │ │ └── startup-script.sh └── python │ ├── appengine-flexible │ ├── README.md │ ├── app.yaml │ ├── main.py │ └── requirements.txt │ ├── appengine-standard │ ├── README.md │ ├── app.yaml │ ├── main.py │ └── requirements.txt │ └── gce │ ├── README.md │ ├── add-google-cloud-ops-agent-repo.sh │ ├── deploy.sh │ ├── main.py │ ├── procfile │ ├── python-app.conf │ ├── requirements.txt │ ├── startup-script.sh │ └── teardown.sh ├── setup.cfg ├── setup.py ├── snapshot_dbg_cli ├── COMMAND_REFERENCE.md ├── __init__.py ├── __main__.py ├── breakpoint_utils.py ├── cli.py ├── cli_common_arguments.py ├── cli_run.py ├── cli_services.py ├── cli_version.py ├── data_formatter.py ├── debuggee_utils.py ├── delete_breakpoints.py ├── delete_debuggees_command.py ├── delete_logpoints_command.py ├── delete_snapshots_command.py ├── exceptions.py ├── firebase_management_rest_service.py ├── firebase_rtdb_rest_service.py ├── firebase_types.py ├── gcloud_cli_service.py ├── get_logpoint_command.py ├── get_snapshot_command.py ├── http_service.py ├── init_command.py ├── list_debuggees_command.py ├── list_logpoints_command.py ├── list_snapshots_command.py ├── permissions_rest_service.py ├── set_logpoint_command.py ├── set_snapshot_command.py ├── snapshot_debugger_rtdb_service.py ├── snapshot_debugger_schema.py ├── snapshot_parser.py ├── status_message.py ├── time_utils.py ├── user_input.py └── user_output.py ├── snapshot_dbg_cli_tests ├── __init__.py ├── test_breakpoint_utils.py ├── test_cli_common_arguments.py ├── test_cli_run.py ├── test_cli_services.py ├── test_cli_version.py ├── test_data_formatter.py ├── test_debuggee_utils.py ├── test_delete_debuggees_command.py ├── test_delete_logpoints_command.py ├── test_delete_snapshots_command.py ├── test_firebase_management_rest_service.py ├── test_firebase_rtdb_rest_service.py ├── test_firebase_types.py ├── test_gcloud_cli_service.py ├── test_get_logpoint_command.py ├── test_get_snapshot_command.py ├── test_http_service.py ├── test_init.py ├── test_init_command.py ├── test_list_debuggees_command.py ├── test_list_logpoints_command.py ├── test_list_snapshots_command.py ├── test_permissions_rest_service.py ├── test_set_logpoint_command.py ├── test_set_snapshot_command.py ├── test_snapshot_debugger_rtdb_service.py ├── test_snapshot_debugger_schema.py ├── test_snapshot_parser.py ├── test_status_message.py ├── test_time_utils.py ├── test_user_input.py └── test_user_output.py └── snapshot_dbg_extension ├── .gitignore ├── .vscodeignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── media ├── expr.svg └── icon-512.png ├── package.json ├── src ├── adapter.ts ├── breakpoint.ts ├── breakpointManager.ts ├── debugUtil.ts ├── debuggeePicker.ts ├── expressionsPrompter.ts ├── extension.ts ├── gcloudCredential.ts ├── ideBreakpoints.ts ├── initialActiveBreakpoints.ts ├── lineMappings.ts ├── logLevelPicker.ts ├── logpointMessage.ts ├── snapshotPicker.ts ├── statusMessage.ts ├── test │ ├── runTest.ts │ └── suite │ │ ├── extension.test.ts │ │ └── index.ts ├── userPreferences.ts ├── util.ts └── whenClauseContextUtil.ts └── tsconfig.json /.github/workflows/scorecard.yml: -------------------------------------------------------------------------------- 1 | # This workflow uses actions that are not certified by GitHub. They are provided 2 | # by a third-party and are governed by separate terms of service, privacy 3 | # policy, and support documentation. 4 | 5 | name: Scorecard supply-chain security 6 | on: 7 | # For Branch-Protection check. Only the default branch is supported. See 8 | # https://github.com/ossf/scorecard/blob/main/docs/checks.md#branch-protection 9 | branch_protection_rule: 10 | # To guarantee Maintained check is occasionally updated. See 11 | # https://github.com/ossf/scorecard/blob/main/docs/checks.md#maintained 12 | schedule: 13 | - cron: '38 17 * * 4' 14 | push: 15 | branches: [ "main" ] 16 | 17 | # Declare default permissions as read only. 18 | permissions: read-all 19 | 20 | jobs: 21 | analysis: 22 | name: Scorecard analysis 23 | runs-on: ubuntu-latest 24 | permissions: 25 | # Needed to upload the results to code-scanning dashboard. 26 | security-events: write 27 | # Needed to publish results and get a badge (see publish_results below). 28 | id-token: write 29 | # Uncomment the permissions below if installing in a private repository. 30 | # contents: read 31 | # actions: read 32 | 33 | steps: 34 | - name: "Checkout code" 35 | uses: actions/checkout@93ea575cb5d8a053eaa0ac8fa3b40d7e05a33cc8 # v3.1.0 36 | with: 37 | persist-credentials: false 38 | 39 | - name: "Run analysis" 40 | uses: ossf/scorecard-action@e38b1902ae4f44df626f11ba0734b14fb91f8f86 # v2.1.2 41 | with: 42 | results_file: results.sarif 43 | results_format: sarif 44 | # (Optional) "write" PAT token. Uncomment the `repo_token` line below if: 45 | # - you want to enable the Branch-Protection check on a *public* repository, or 46 | # - you are installing Scorecard on a *private* repository 47 | # To create the PAT, follow the steps in https://github.com/ossf/scorecard-action#authentication-with-pat. 48 | # repo_token: ${{ secrets.SCORECARD_TOKEN }} 49 | 50 | # Public repositories: 51 | # - Publish results to OpenSSF REST API for easy access by consumers 52 | # - Allows the repository to include the Scorecard badge. 53 | # - See https://github.com/ossf/scorecard-action#publishing-results. 54 | # For private repositories: 55 | # - `publish_results` will always be set to `false`, regardless 56 | # of the value entered here. 57 | publish_results: true 58 | 59 | # Upload the results as artifacts (optional). Commenting out will disable uploads of run results in SARIF 60 | # format to the repository Actions tab. 61 | - name: "Upload artifact" 62 | uses: actions/upload-artifact@3cea5372237819ed00197afe530f5a7ea3e805c8 # v3.1.0 63 | with: 64 | name: SARIF file 65 | path: results.sarif 66 | retention-days: 5 67 | 68 | # Upload the results to GitHub's code scanning dashboard. 69 | - name: "Upload to code-scanning" 70 | uses: github/codeql-action/upload-sarif@17573ee1cc1b9d061760f3a006fc4aac4f944fd5 # v2.2.4 71 | with: 72 | sarif_file: results.sarif 73 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__/ 2 | *.egg-info/ 3 | dist/ 4 | env/ 5 | *.class 6 | *.jar 7 | *.so 8 | .gcloudignore 9 | 10 | # maven 11 | target/ 12 | 13 | # npm 14 | node_modules/ 15 | package-lock.json 16 | -------------------------------------------------------------------------------- /.style.yapf: -------------------------------------------------------------------------------- 1 | [style] 2 | based_on_style = yapf 3 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # How to Contribute 2 | 3 | We'd love to accept your patches and contributions to this project. There are 4 | just a few small guidelines you need to follow. 5 | 6 | ## Contributor License Agreement 7 | 8 | Contributions to this project must be accompanied by a Contributor License 9 | Agreement (CLA). You (or your employer) retain the copyright to your 10 | contribution; this simply gives us permission to use and redistribute your 11 | contributions as part of the project. Head over to 12 | to see your current agreements on file or 13 | to sign a new one. 14 | 15 | You generally only need to submit a CLA once, so if you've already submitted one 16 | (even if it was for a different project), you probably don't need to do it 17 | again. 18 | 19 | ## Code Reviews 20 | 21 | All submissions, including submissions by project members, require review. We 22 | use GitHub pull requests for this purpose. Consult 23 | [GitHub Help](https://help.github.com/articles/about-pull-requests/) for more 24 | information on using pull requests. 25 | 26 | ## CLI coding style 27 | 28 | The CLI python code follows the [Google Python Style 29 | Guide](https://google.github.io/styleguide/pyguide.html) with one exception: 30 | 31 | - 2 spaces for indentation rather than 4. 32 | 33 | It also uses the [YAPF](https://github.com/google/yapf) formatter, which should 34 | be run on any code changes. 35 | 36 | ## Community Guidelines 37 | 38 | This project follows 39 | [Google's Open Source Community Guidelines](https://opensource.google/conduct/). 40 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: test 2 | 3 | test: 4 | python3 -m unittest discover -s snapshot_dbg_cli_tests -t . 5 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | To report a security issue, please use [https://g.co/vulnz](https://g.co/vulnz). 4 | 5 | We use g.co/vulnz for our intake, and do coordination and disclosure here on 6 | GitHub (including using GitHub Security Advisory). 7 | 8 | The Google Security Team will respond within 5 working days of your report on 9 | g.co/vulnz. 10 | -------------------------------------------------------------------------------- /docs/java.md: -------------------------------------------------------------------------------- 1 | # Java Snapshot Debugger Agent 2 | 3 | This section contains information for integrating and configuring the Java 4 | Snapshot Debugger agent in different environments. Full configuration 5 | information for the agent can be found at [Java Agent Documentation][java-agent] 6 | 7 | ## Samples 8 | 9 | See [samples/java][java-samples] for working examples of installing and 10 | configuring the Java agent across different Google Cloud environments. 11 | 12 | [java-agent]: https://github.com/GoogleCloudPlatform/cloud-debug-java/blob/main/README.md 13 | [java-samples]: https://github.com/GoogleCloudPlatform/snapshot-debugger/tree/main/samples/java 14 | 15 | ## Running locally 16 | 17 | It's also possible to run things outside of a Google Cloud environment, such 18 | as locally. Here are some notes for doing so, which involves generating a 19 | service account key so the agents are able to read/write from the Firebase RTDB 20 | backend. 21 | 22 | ### Download service account credentials from Firebase. 23 | 24 | 1. Navigate to your project in the Firebase console service account page. 25 | Replace `PROJECT_ID` with your project’s ID. 26 | 27 | ``` 28 | https://console.firebase.google.com/project/PROJECT_ID/settings/serviceaccounts/adminsdk 29 | ``` 30 | 31 | 2. Click **Generate new private key** and save the key locally. 32 | 33 | ### Install and configure the agent 34 | 35 | 1. Download the pre-built agent package: 36 | 37 | ``` 38 | # Create a directory for the Debugger. Add and unzip the agent in the directory. 39 | sudo sh -c "mkdir /opt/cdbg && wget -qO- https://github.com/GoogleCloudPlatform/cloud-debug-java/releases/latest/download/cdbg_java_agent_gce.tar.gz | tar xvz -C /opt/cdbg" 40 | ``` 41 | 42 | 2. Add the agent to your Java invocation: 43 | 44 | _(If you are using Tomcat or Jetty, see the [Application 45 | Servers](https://github.com/GoogleCloudPlatform/cloud-debug-java#application-servers) 46 | section of the agent documentation for extra information.)_ 47 | 48 | ``` 49 | # Start the agent when the app is deployed. 50 | java -agentpath:/opt/cdbg/cdbg_java_agent.so \ 51 | -Dcom.google.cdbg.module=MODULE \ 52 | -Dcom.google.cdbg.version=VERSION \ 53 | -Dcom.google.cdbg.agent.use_firebase=True \ 54 | -Dcom.google.cdbg.auth.serviceaccount.jsonfile=PATH-TO-KEY-FILE 55 | -jar PATH_TO_JAR_FILE 56 | ``` 57 | 58 | Where: 59 | * `MODULE` is a name for your app, such as MyApp, Backend, or Frontend. 60 | * `VERSION` is a version, such as v1.0, build_147, or v20170714. 61 | * `PATH-TO-KEY-FILE` is the path to your Firebase private key. 62 | * `PATH_TO_JAR_FILE` is the relative path to the app's JAR file. e.g.,: ~/myapp.jar. 63 | -------------------------------------------------------------------------------- /docs/node-js.md: -------------------------------------------------------------------------------- 1 | # Node.js Snapshot Debugger Agent 2 | 3 | This section contains information for integrating and configuring the Node.js 4 | Snapshot Debugger agent in different environments. Full configuration 5 | information for the agent can be found at [Node.js Agent 6 | Documentation][nodejs-agent] 7 | 8 | ## Samples 9 | 10 | See [samples/node-js][nodejs-samples] for working examples of installing and 11 | configuring the Node.js agent across different Google Cloud environments. 12 | 13 | [nodejs-agent]: https://github.com/googleapis/cloud-debug-nodejs/blob/main/README.md 14 | [nodejs-samples]: https://github.com/GoogleCloudPlatform/snapshot-debugger/tree/main/samples/node-js 15 | 16 | ## Running locally 17 | 18 | It's also possible to run things outside of a Google Cloud environment, such 19 | as locally. Here are some notes for doing so, which involves generating a 20 | service account key so the agents are able to read/write from the Firebase RTDB 21 | backend. 22 | 23 | ### Download service account credentials from Firebase. 24 | 25 | 1. Navigate to your project in the Firebase console service account page. 26 | Replace `PROJECT_ID` with your project’s ID. 27 | 28 | ``` 29 | https://console.firebase.google.com/project/PROJECT_ID/settings/serviceaccounts/adminsdk 30 | ``` 31 | 32 | 2. Click **Generate new private key** and save the key locally. 33 | 34 | ### Install and configure the agent 35 | 36 | 1. Use [npm](https://www.npmjs.com/) to install the package: 37 | 38 | ``` 39 | npm install --save @google-cloud/debug-agent 40 | ``` 41 | 42 | 2. Configure and enable the agent at the top of your app's main script or entry 43 | point (but after @google/cloud-trace if you are also using it). 44 | 45 | ``` 46 | require('@google-cloud/debug-agent').start({ 47 | useFirebase: true, 48 | firebaseKeyPath: 'PATH-TO-KEY-FILE', 49 | // Specify this if you are the Spark billing plan and are using the 50 | // default RTDB instance. 51 | // firebaseDbUrl: 'https://RTDB-NAME-default-rtdb.firebaseio.com', 52 | serviceContext: { 53 | service: 'SERVICE', 54 | version: 'VERSION', 55 | } 56 | }); 57 | ``` 58 | 59 | Where: 60 | * `PATH-TO-KEY-FILE` is the path to your Firebase private key. 61 | * `RTDB-NAME` is the name of your Firebase database. 62 | * `SERVICE` is a name for your app, such as `MyApp`, `Backend`, or `Frontend`. 63 | * `VERSION` is a version, such as `v1.0`, `build_147`, or `v20170714`. 64 | 65 | -------------------------------------------------------------------------------- /docs/python.md: -------------------------------------------------------------------------------- 1 | # Python Snapshot Debugger Agent 2 | 3 | This section contains information for integrating and configuring the Python 4 | Snapshot Debugger agent in different environments. Full configuration 5 | information for the agent can be found at [Python Agent 6 | Documentation][python-agent] 7 | 8 | ## Samples 9 | 10 | See [samples/python][python-samples] for working examples of installing and 11 | configuring the Python agent across different Google Cloud environments. 12 | 13 | [python-agent]: https://github.com/GoogleCloudPlatform/cloud-debug-python/blob/main/README.md 14 | [python-samples]: https://github.com/GoogleCloudPlatform/snapshot-debugger/tree/main/samples/python 15 | 16 | ## Running locally 17 | 18 | It's also possible to run things outside of a Google Cloud environment, such 19 | as locally. Here are some notes for doing so, which involves generating a 20 | service account key so the agents are able to read/write from the Firebase RTDB 21 | backend. 22 | 23 | ### Download service account credentials from Firebase. 24 | 25 | 1. Navigate to your project in the Firebase console service account page. 26 | Replace `PROJECT_ID` with your project’s ID. 27 | 28 | ``` 29 | https://console.firebase.google.com/project/PROJECT_ID/settings/serviceaccounts/adminsdk 30 | ``` 31 | 32 | 2. Click **Generate new private key** and save the key locally. 33 | 34 | ### Install and configure the agent 35 | 36 | 1. Download the Debugger agent. 37 | 38 | The easiest way to install the Python Debugger is with 39 | [pip](https://pypi.org/project/pip/) 40 | 41 | ``` 42 | pip install google-python-cloud-debugger 43 | ``` 44 | 45 | 2. Add the following lines as early as possible in your initialization code, such as in your main function, or in manage.py when using the Django web framework. 46 | 47 | ``` 48 | try: 49 | import googleclouddebugger 50 | googleclouddebugger.enable( 51 | use_firebase=True, 52 | module='[MODULE]', 53 | version='[VERSION]', 54 | service_account_json_file='[PATH-TO-KEY-FILE]' 55 | ) 56 | except ImportError: 57 | pass 58 | ``` 59 | 60 | Where: 61 | * `MODULE` is a name for your app, such as MyApp, Backend, or Frontend. 62 | * `VERSION` is a version, such as v1.0, build_147, or v20170714. 63 | * `PATH-TO-KEY-FILE` is the path to your Firebase private key. 64 | -------------------------------------------------------------------------------- /docs/troubleshooting.md: -------------------------------------------------------------------------------- 1 | # Snapshot Debugger Troubleshooting 2 | 3 | If you run into problems with Snapshot Debugger, file an 4 | [issue](https://github.com/GoogleCloudPlatform/snapshot-debugger/issues). 5 | 6 | ## Your project doesn’t show up when enabling Firebase 7 | 8 | ### Symptom 9 | 10 | Your project id is not auto populated and is not present in the project dropdown 11 | when you try to [Enable Firebase for your GCP Project][enable-firebase] 12 | 13 | ### Resolution 14 | 15 | Check if the Firebase Management API is already enabled, as if it is, that 16 | interferes with the process. Try using the following link to disable the 17 | Firebase Management API, then go back and follow the steps in the [Enable 18 | Firebase for your GCP Project][enable-firebase] 19 | 20 | ``` 21 | https://console.developers.google.com/apis/api/firebase.googleapis.com?project=PROJECT_ID 22 | ``` 23 | 24 | Where PROJECT_ID is your project ID. 25 | 26 | ## Your database in not displayed in the Firebase Console 27 | 28 | ### Symptom 29 | 30 | A blank screen is shown when attempting to view database contents in Firebase 31 | Console's Realtime Database section. The project is using the Blaze pricing 32 | plan. 33 | 34 | ### Resolution 35 | 36 | Rerun the init command to find the database's url. Use that url to view the 37 | database's contents. See [Blaze plan RTDB setup][blaze-plan-setup] for 38 | details, as noted there, it is safe to run the `init` command multiple times to 39 | view your database's information. 40 | 41 | The database's url should resemble `https://DATABASE_NAME.firebaseio.com`, 42 | which should redirect to 43 | `https://console.firebase.google.com/project/PROJECT_ID/database/DATABASE_NAME/data` 44 | for the Firebase Console view. 45 | 46 | [enable-firebase]: https://github.com/GoogleCloudPlatform/snapshot-debugger/blob/main/README.md#enable-firebase-for-your-google-cloud-project 47 | [blaze-plan-setup]: https://github.com/GoogleCloudPlatform/snapshot-debugger/blob/main/README.md#blaze-plan-rtdb-setup 48 | 49 | ## Your Debuggee Is Not Found 50 | 51 | ### Symptom 52 | 53 | When you run the `snapshot-dbg-cli list_debuggees` command, a debuggee you 54 | expect to be present is not shown. 55 | 56 | ### Resolution 57 | 58 | There are a variety of potential causes for this, run through the following: 59 | 60 | #### Wake your application up 61 | 62 | For environments such as App Engine Standard, your application may not actually 63 | be running. This can be the case if one of the following holds: 64 | 65 | * You have just deployed your application and it has not yet received a request 66 | * Enough time has passed since it last serviced a request, as a result the 67 | instance count may have been scaled to 0 68 | 69 | Simply hitting your application's endpoint will cause your application to run, 70 | and should cause the debuggee to be listed by `snapshot-dbg-cli list_debuggees`. 71 | 72 | #### Ensure the required access scopes are in place 73 | 74 | Ensure the required access scopes are configured for your environment so that 75 | the Snapshot Debugger Agent is able to communicate with the Firebase RTDB 76 | backend. See [Required Access Scopes][access-scopes] for more information. 77 | 78 | #### Ensure the service account has the required permissions 79 | 80 | Ensure the service account running your service has the required permissions. 81 | See [Required Permissions][required-permissions] for more information. If you 82 | set the permission, wait a few minutes before checking to see if the debuggee 83 | now appears. 84 | 85 | [access-scopes]: https://github.com/GoogleCloudPlatform/snapshot-debugger/blob/main/docs/configuration.md#access-scopes 86 | [required-permissions]: https://github.com/GoogleCloudPlatform/snapshot-debugger/blob/main/docs/configuration.md#service-account-permissions 87 | -------------------------------------------------------------------------------- /samples/README.md: -------------------------------------------------------------------------------- 1 | # Snapshot Debugger Samples 2 | 3 | This directory contains working examples of using the Snapshot Debugger with 4 | Java, Python and Node.js applications across different Google Cloud 5 | environments. 6 | 7 | The accompanying README files in the subdirectories contain the relevant 8 | information for each sample. 9 | 10 | For more information on the Snapshot Debugger see [Snapshot 11 | Debugger](https://github.com/GoogleCloudPlatform/snapshot-debugger) 12 | 13 | For more information on the agents, as well as how to run them locally, see the 14 | agent specific documentation: 15 | 16 | * [Java](https://github.com/GoogleCloudPlatform/snapshot-debugger/blob/main/docs/java.md) 17 | * [Node.js](https://github.com/GoogleCloudPlatform/snapshot-debugger/blob/main/docs/node-js.md) 18 | * [Python](https://github.com/GoogleCloudPlatform/snapshot-debugger/blob/main/docs/python.md) 19 | -------------------------------------------------------------------------------- /samples/app_engine_flexible_prerequisites.md: -------------------------------------------------------------------------------- 1 | # Prerequisite Steps For The App Engine Flexible Samples 2 | 3 | ## Project Setup 4 | 5 | Follow the instructions 6 | [here](https://cloud.google.com/appengine/docs/flexible/managing-projects-apps-billing) 7 | to create/confgure your App Engine enabled project. 8 | 9 | ## Install the Snapshot Debugger CLI and enable Firebase 10 | 11 | Follow the instructions beginning at [Before you 12 | begin](../README.md#before-you-begin) through to and including [Enable 13 | Firebase for your Google Cloud 14 | Project](../README.md#enable-firebase-for-your-google-cloud-project) to 15 | get the Snapshot Debugger CLI installed and your project configured to use 16 | Firebase. 17 | 18 | ## Configure Service Account Roles 19 | 20 | The application running on App Engine will need to read/write from the Firebase 21 | RTDB instance. For it to do that, the service account running the application 22 | will need the `roles/firebasedatabase.admin` role. In addition, service accounts 23 | running applications in the App Engine Flexible environment require the 24 | `roles/storage.objectViewer` and `roles/appengineflex.serviceAgent` roles. 25 | 26 | * **Determine the Service Account Email Address** 27 | 28 | Here it is assumed the default App Engine service account will be used. If 29 | you have provided special configuration to use a different service account, 30 | use the email address of that service account. 31 | 32 | ``` 33 | gcloud iam service-accounts list 34 | ``` 35 | 36 | Identify the email of the entry with a display name resembling `App Engine 37 | default service account`. 38 | 39 | * **Add the roles** 40 | 41 | ``` 42 | gcloud projects add-iam-policy-binding [YOUR PROJECT ID] \ 43 | --member=serviceAccount:[SERVICE ACCOUNT EMAIL ADDRESS] \ 44 | --role=roles/firebasedatabase.admin 45 | 46 | gcloud projects add-iam-policy-binding [YOUR PROJECT ID] \ 47 | --member=serviceAccount:[SERVICE ACCOUNT EMAIL ADDRESS] \ 48 | --role=roles/storage.objectViewer 49 | 50 | gcloud projects add-iam-policy-binding [YOUR PROJECT ID] \ 51 | --member=serviceAccount:[SERVICE ACCOUNT EMAIL ADDRESS] \ 52 | --role=roles/appengineflex.serviceAgent 53 | ``` 54 | -------------------------------------------------------------------------------- /samples/app_engine_standard_prerequisites.md: -------------------------------------------------------------------------------- 1 | # Prerequisite Steps For The App Engine Standard Samples 2 | 3 | ## Project Setup 4 | 5 | Follow the appropriate instructions for your target language to create/confgure 6 | your App Engine enabled project. 7 | 8 | * [Java](https://cloud.google.com/appengine/docs/standard/java-gen2/building-app/creating-project) 9 | * [Java 8](https://cloud.google.com/appengine/docs/legacy/standard/java/console) 10 | * [Node.js](https://cloud.google.com/appengine/docs/standard/nodejs/building-app/creating-project) 11 | * [Python](https://cloud.google.com/appengine/docs/standard/python3/building-app/creating-gcp-project) 12 | 13 | ## Install the Snapshot Debugger CLI and enable Firebase 14 | 15 | Follow the instructions beginning at [Before you 16 | begin](../README.md#before-you-begin) through to and including [Enable 17 | Firebase for your Google Cloud 18 | Project](../README.md#enable-firebase-for-your-google-cloud-project) to 19 | get the Snapshot Debugger CLI installed and your project configured to use 20 | Firebase. 21 | 22 | ## Configure Service Account Role 23 | 24 | The application running on App Engine will need to read/write from the Firebase 25 | RTDB instance. For it to do that, the service account running the application 26 | will need the `roles/firebasedatabase.admin` role. 27 | 28 | * **Determine the Service Account Email Address** 29 | 30 | Here it is assumed the default App Engine service account will be used. If 31 | you have provided special configuration to use a different service account, 32 | use the email address of that service account. 33 | 34 | ``` 35 | gcloud iam service-accounts list 36 | ``` 37 | 38 | Identify the email of the entry with a display name resembling `App Engine 39 | default service account`. 40 | 41 | * **Add the role** 42 | 43 | ``` 44 | gcloud projects add-iam-policy-binding [YOUR PROJECT ID] \ 45 | --member=serviceAccount:[SERVICE ACCOUNT EMAIL ADDRESS] \ 46 | --role=roles/firebasedatabase.admin 47 | ``` 48 | -------------------------------------------------------------------------------- /samples/gce_samples_prerequisites.md: -------------------------------------------------------------------------------- 1 | # Prerequisite Steps For The GCE Samples 2 | 3 | ## Create a Project in the Google Cloud Platform Console 4 | 5 | If you haven't already created a project, create one now. Projects enable you to 6 | manage all Google Cloud Platform resources for your app, including deployment, 7 | access control, billing, and services. 8 | 9 | 1. Open the [Cloud Platform Console][cloud-console]. 10 | 1. In the drop-down menu at the top, select **Create a project**. 11 | 1. Give your project a name. 12 | 1. Make a note of the project ID, which might be different from the project 13 | name. The project ID is used in commands and in configurations. 14 | 15 | [cloud-console]: https://console.cloud.google.com/ 16 | 17 | ## Enable Compute Engine API 18 | 19 | The Compute Engine API (compute.googleapis.com) must be enabled. 20 | 21 | 1. Check if it's already enabled. 22 | 23 | ``` 24 | gcloud services list --enabled | grep compute.googleapis.com 25 | ``` 26 | 27 | If the entry was found, it is enabled. If not, proceed to step 2 to enable 28 | it. 29 | 30 | 2. Enable the API if it is not already renabled. 31 | 32 | ``` 33 | gcloud services enable compute.googleapis.com 34 | ``` 35 | 36 | ## Install the Snapshot Debugger CLI and enable Firebase 37 | 38 | Follow the instructions beginning at [Before you 39 | begin](../README.md#before-you-begin) through to and including [Enable 40 | Firebase for your Google Cloud 41 | Project](../README.md#enable-firebase-for-your-google-cloud-project) to 42 | get the Snapshot Debugger CLI installed and your project configured to use 43 | Firebase. 44 | 45 | ## Configure Service Account Role 46 | 47 | The application running on GCE will need to read/write from the Firebase RTDB 48 | instance. For it to do that, the service account running the GCE instance will 49 | need the `roles/firebasedatabase.admin` role. 50 | 51 | * **Determine the Service Account Email Address** 52 | 53 | Here it is assumed the default GCE service account will be used when the 54 | instance gets created. 55 | 56 | ``` 57 | gcloud iam service-accounts list 58 | ``` 59 | 60 | Identify the email of the entry with a display name resembling `Compute 61 | Engine default service account`. 62 | 63 | * **Add the role** 64 | 65 | ``` 66 | gcloud projects add-iam-policy-binding [YOUR PROJECT ID] \ 67 | --member=serviceAccount:[SERVICE ACCOUNT EMAIL ADDRESS] \ 68 | --role=roles/firebasedatabase.admin 69 | ``` 70 | -------------------------------------------------------------------------------- /samples/java/appengine-flexible/README.md: -------------------------------------------------------------------------------- 1 | # Snapshot Debugger Examples for Google App Engine Flexible Environment 2 | 3 | > **NOTE** 4 | > This file was copied from 5 | > [java-docs-samples/appengine-flexible/README.md](https://github.com/GoogleCloudPlatform/java-docs-samples/blob/main/flexible/README.md) 6 | > and modified for the Snapshot Debugger samples here. 7 | 8 | ## Prerequisites 9 | 10 | ### Setup the Project and the Snapshot Debugger CLI 11 | 12 | 1. Perform all [Prerequisite Steps](../../app_engine_flexible_prerequisites.md) 13 | 14 | ### Download Maven 15 | 16 | Some of these samples use the [Apache Maven][maven] build system. Before getting 17 | started, be sure to [download][maven-download] and [install][maven-install] it. 18 | When you use Maven as described here, it will automatically download the needed 19 | client libraries. 20 | 21 | [maven]: https://maven.apache.org 22 | [maven-download]: https://maven.apache.org/download.cgi 23 | [maven-install]: https://maven.apache.org/install.html 24 | 25 | ### Install Google Cloud `gcloud` CLI App Engine extension for Java 26 | 27 | 1. `gcloud` should already have been installed as part of an earlier step, if 28 | not [install][install-gcloud] and [initialize][initialize-gcloud] the Google 29 | Cloud CLI. 30 | 1. Install the [gcloud component][managing-components] that includes the App 31 | Engine extension for Java. 32 | 33 | If you used the `apt` or `yum` package managers to install the gcloud CLI, 34 | [use those same package managers to install the gcloud component][external-package-managers]. 35 | 36 | Otherwise, use the following command: 37 | 38 | ``` 39 | gcloud components install app-engine-java 40 | ``` 41 | 42 | [install-gcloud]: https://cloud.google.com/sdk/docs/install 43 | [initialize-gcloud]: https://cloud.google.com/sdk/docs/initializing 44 | [managing-components]: https://cloud.google.com/sdk/docs/managing-components 45 | [external-package-managers]: https://cloud.google.com/sdk/docs/components#external_package_managers 46 | 47 | ### Google Cloud Shell, Open JDK 8 setup: 48 | 49 | If running in the Google Cloud Shell, to switch to an Open JDK 8 you can use: 50 | 51 | ``` 52 | sudo update-alternatives --config java 53 | # And select the usr/lib/jvm/java-8-openjdk-amd64/jre/bin/java version. 54 | # Also, set the JAVA_HOME variable for Maven to pick the correct JDK: 55 | export JAVA_HOME=/usr/lib/jvm/java-8-openjdk-amd64 56 | ``` 57 | 58 | ### Available Runtimes 59 | 60 | There are two Google provided Java runtimes available: 61 | - [The Java 8 / Jetty 9 runtime](https://cloud.google.com/appengine/docs/flexible/java/dev-jetty9) 62 | - Provides OpenJDK 8 and Eclipse Jetty 9 with support for the Java Servlet 3.1 63 | Specification. 64 | - [The Java 8 runtime](https://cloud.google.com/appengine/docs/flexible/java/dev-java-only) 65 | - Does not include any web-serving framework. The only requirement is that 66 | your app should listen and respond on port 8080. 67 | 68 | ### Runtime Customization and JAVA_USER_OPTS 69 | 70 | The runtime customization feature is used in the samples to add the Snapshot 71 | Debugger agent. In addition, the JAVA_USER_OPTS environment variable is used to 72 | set the `-agentpath` JVM option to load the agent. See the links below for more 73 | information on these options: 74 | 75 | - The Java 8 / Jetty 9 Runtime 76 | - [Customization](https://cloud.google.com/appengine/docs/flexible/java/dev-jetty9#customize) 77 | - [Jetty 9 runtime environment variables](https://cloud.google.com/appengine/docs/flexible/java/dev-jetty9#variables) 78 | - [Java 8 runtime environment variables are additionally available](https://cloud.google.com/appengine/docs/flexible/java/dev-java-only#variables) 79 | - The Java 8 Runtime 80 | - [Customization](https://cloud.google.com/appengine/docs/flexible/java/dev-java-only#customizing) 81 | - [Available environment variables](https://cloud.google.com/appengine/docs/flexible/java/dev-java-only#variables) 82 | 83 | **Sample applications**: 84 | - [helloworld](helloworld) uses the Java 8 / Jetty 9 runtime 85 | - [helloworld-springboot](helloworld-springboot) uses the Java 8 Runtime 86 | 87 | -------------------------------------------------------------------------------- /samples/java/appengine-flexible/helloworld-springboot/README.md: -------------------------------------------------------------------------------- 1 | # Spring Boot based Hello World app 2 | 3 | > **Note** 4 | > This example was copied from 5 | > [flexible/helloworld-springboot](https://github.com/GoogleCloudPlatform/java-docs-samples/tree/main/flexible/helloworld-springboot) 6 | > and modified for Snapshot Debugger Java agent use. 7 | 8 | 9 | ## Setup 10 | 11 | See [Prerequisites](../README.md#Prerequisites). 12 | 13 | Ensure your current working directory is 14 | `samples/java/appengine-flexible/helloworld-springboot`, as all following instructions 15 | assumes this. 16 | 17 | ## Examine the `app.yaml` and `Dockerfile` files 18 | 19 | This example customizes the Java 8 / Jetty 9 runtime by explicitly providing a 20 | Dockerfile. The purpose of the Dockerfile is to add the Snapshot Debugger Java 21 | agent to the runtime. The app.yaml file provides configuration information to 22 | specify a custom runtime is being used and to configure the loading of the 23 | agent. 24 | 25 | app.yaml: 26 | 27 | https://github.com/GoogleCloudPlatform/snapshot-debugger/blob/ed767fe00d5b1006ff60ac9c4bf57668b774962f/samples/java/appengine-flexible/helloworld-springboot/src/main/appengine/app.yaml#L15-L34 28 | 29 | Dockerfile: 30 | 31 | https://github.com/GoogleCloudPlatform/snapshot-debugger/blob/ed767fe00d5b1006ff60ac9c4bf57668b774962f/samples/java/appengine-flexible/helloworld-springboot/src/main/docker/Dockerfile#L1-L15 32 | 33 | ## Deploy to App Engine Flexible 34 | 35 | Deploy the app with the following: 36 | 37 | ``` 38 | mvn clean package appengine:deploy 39 | ``` 40 | 41 | Early on in the output, log lines resembling the following will be emitted, make 42 | note of them: 43 | 44 | ``` 45 | [INFO] GCLOUD: target service: [default] 46 | [INFO] GCLOUD: target version: [20221125t224954] 47 | [INFO] GCLOUD: target url: [https://PROJECT_ID.REGION_ID.r.appspot.com] 48 | ``` 49 | 50 | The service and version will be used to identify your debuggee ID. 51 | 52 | ## Navigate To Your App 53 | 54 | This will ensure the app is running correctly. The URL should be provided in the 55 | `target url` output of the previous step. 56 | 57 | ## Determine the Debuggee ID 58 | 59 | Based on the service and version you'll be able to identify your debuggee ID 60 | based on the output from the `list_debuggees` command. 61 | 62 | ``` 63 | snapshot-dbg-cli list_debuggees 64 | ``` 65 | 66 | The output will resemble the following. The first column will contain an entry 67 | ` - `, which in this case is `default - 20221125t224954`. 68 | 69 | ``` 70 | Name ID Description Last Active Status 71 | ------------------------- ---------- ------------------------------------------------ -------------------- ------ 72 | default - 20221125t224954 d-62259477 my-project-id-20221125t224954-448130606220701265 2022-11-25T22:50:00Z ACTIVE 73 | ``` 74 | 75 | The debuggee ID in this case is `d-62259477`. Using this ID you may now run 76 | through an [Example workflow](../../../../README.md#example-workflow). 77 | 78 | E.g. 79 | * Use the `set_snapshot` CLI command to set a snapshot at 80 | `HelloController.java:26`. Note the returned breakpoint ID. 81 | * Navigate to your application using the `target url` shown in the `mvn clean 82 | package appengine:deploy` output. This will trigger the breakpoint and 83 | collect the snapshot. 84 | * Use the `get_snapshot` CLI command to retrieve the snapshot using the 85 | breakpoint ID created with the `set_snapshot` command. 86 | -------------------------------------------------------------------------------- /samples/java/appengine-flexible/helloworld-springboot/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 18 | 4.0.0 19 | com.example.java 20 | helloworld-springboot 21 | 0.0.1-SNAPSHOT 22 | helloworld-springboot 23 | 24 | 28 | 29 | com.google.cloud.samples 30 | shared-configuration 31 | 1.2.0 32 | 33 | 34 | 35 | 1.8 36 | 1.8 37 | 38 | 39 | 40 | 41 | org.springframework.boot 42 | spring-boot-starter-web 43 | 2.6.6 44 | 45 | 46 | org.springframework.boot 47 | spring-boot-starter-actuator 48 | 2.6.6 49 | 50 | 51 | org.springframework.boot 52 | spring-boot-starter-test 53 | 2.6.6 54 | test 55 | 56 | 57 | org.junit.vintage 58 | junit-vintage-engine 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | org.springframework.boot 68 | spring-boot-maven-plugin 69 | 2.6.6 70 | 71 | 72 | 73 | repackage 74 | 75 | 76 | 77 | 78 | 79 | com.google.cloud.tools 80 | appengine-maven-plugin 81 | 2.4.2 82 | 83 | 84 | GCLOUD_CONFIG 85 | 86 | GCLOUD_CONFIG 87 | 88 | 89 | 90 | 91 | 92 | -------------------------------------------------------------------------------- /samples/java/appengine-flexible/helloworld-springboot/src/main/appengine/app.yaml: -------------------------------------------------------------------------------- 1 | # Copyright 2015 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | # 15 | runtime: custom 16 | env: flex 17 | 18 | env_variables: 19 | # Prevents the runtime from loading the current Cloud Debugger agent which is 20 | # present in the base runtime and enabled by default. Loading this agent 21 | # would conflict with the Snapshot Debugger agent. Eventually once the Cloud 22 | # Debugger has been turned down the base image will no longer contain the 23 | # agent and this won't be necessary. 24 | DBG_ENABLE: false 25 | 26 | # The contents of JAVA_USER_OPTS are added to the java command line. To note, 27 | # in this case the Java agent is able to find the class files (enabling it to 28 | # set breakpoints) without any extra configuration. This is because the 29 | # jetty.base property gets set and the root.war file is extracted to 30 | # 'jetty.base'/webapps/root. There are times however you may need to also 31 | # configure cdbg_extra_class_path if the agent cannot find the class files, 32 | # see https://github.com/GoogleCloudPlatform/cloud-debug-java#extra-classpath 33 | # for more information. 34 | JAVA_USER_OPTS: -agentpath:/opt/cdbg/cdbg_java_agent.so=--log_dir=/var/log/app_engine,--use-firebase=true 35 | 36 | handlers: 37 | - url: /.* 38 | script: this field is required, but ignored 39 | 40 | # Set scaling to 1 to minimize resources for the example. This setting should 41 | # be revisited for production use. 42 | manual_scaling: 43 | instances: 1 44 | -------------------------------------------------------------------------------- /samples/java/appengine-flexible/helloworld-springboot/src/main/docker/Dockerfile: -------------------------------------------------------------------------------- 1 | # We are customizing the Java 8 Runtime 2 | # See https://cloud.google.com/appengine/docs/flexible/java/dev-java-only#customizing 3 | # for more information on these first two lines. 4 | FROM gcr.io/google-appengine/openjdk:8 5 | COPY ./helloworld-springboot-0.0.1-SNAPSHOT.jar $APP_DESTINATION 6 | 7 | # Until the Cloud Debugger has been fully turned down, the rutime image will 8 | # continue to contain the current Cloud Debugger Java agent located in 9 | # /opt/cdbg. Here we remove it if it exists. 10 | RUN rm -rf /opt/cdbg 11 | 12 | # Add the Snapshot Debugger Java Agent 13 | ADD https://github.com/GoogleCloudPlatform/cloud-debug-java/releases/latest/download/cdbg_java_agent_gce.tar.gz /opt/cdbg/ 14 | RUN tar Cxfvz /opt/cdbg /opt/cdbg/cdbg_java_agent_gce.tar.gz --no-same-owner \ 15 | && rm /opt/cdbg/cdbg_java_agent_gce.tar.gz 16 | 17 | -------------------------------------------------------------------------------- /samples/java/appengine-flexible/helloworld-springboot/src/main/java/com/example/java/HelloController.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.example.java; 18 | 19 | import org.springframework.web.bind.annotation.RequestMapping; 20 | import org.springframework.web.bind.annotation.RestController; 21 | 22 | @RestController 23 | public class HelloController { 24 | @RequestMapping("/") 25 | String index() { 26 | return "Hello World!"; 27 | } 28 | 29 | /** 30 | * (Optional) App Engine health check endpoint mapping. 31 | * 32 | * @see 34 | * If your app does not handle health checks, a HTTP 404 response is interpreted as a 35 | * successful reply. 36 | */ 37 | @RequestMapping("/_ah/health") 38 | public String healthy() { 39 | // Message body required though ignored 40 | return "Still surviving."; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /samples/java/appengine-flexible/helloworld-springboot/src/main/java/com/example/java/HelloWorldApplication.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.example.java; 18 | 19 | import org.springframework.boot.SpringApplication; 20 | import org.springframework.boot.autoconfigure.SpringBootApplication; 21 | 22 | @SpringBootApplication 23 | public class HelloWorldApplication { 24 | 25 | public static void main(String[] args) { 26 | SpringApplication.run(HelloWorldApplication.class, args); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /samples/java/appengine-flexible/helloworld-springboot/src/main/resources/application.properties: -------------------------------------------------------------------------------- 1 | # Copyright 2015 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | -------------------------------------------------------------------------------- /samples/java/appengine-flexible/helloworld-springboot/src/test/java/com/example/java/HelloControllerTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.example.java; 18 | 19 | import static org.hamcrest.Matchers.equalTo; 20 | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; 21 | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; 22 | 23 | import org.junit.jupiter.api.Test; 24 | import org.springframework.beans.factory.annotation.Autowired; 25 | import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; 26 | import org.springframework.boot.test.context.SpringBootTest; 27 | import org.springframework.http.MediaType; 28 | import org.springframework.test.web.servlet.MockMvc; 29 | import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; 30 | 31 | @SpringBootTest 32 | @AutoConfigureMockMvc 33 | public class HelloControllerTest { 34 | 35 | @Autowired private MockMvc mvc; 36 | 37 | @Test 38 | public void getHello() throws Exception { 39 | mvc.perform(MockMvcRequestBuilders.get("/").accept(MediaType.APPLICATION_JSON)) 40 | .andExpect(status().isOk()) 41 | .andExpect(content().string(equalTo("Hello World!"))); 42 | } 43 | 44 | @Test 45 | public void getHealth() throws Exception { 46 | mvc.perform(MockMvcRequestBuilders.get("/_ah/health").accept(MediaType.APPLICATION_JSON)) 47 | .andExpect(status().isOk()) 48 | .andExpect(content().string(equalTo("Still surviving."))); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /samples/java/appengine-flexible/helloworld/README.md: -------------------------------------------------------------------------------- 1 | # Appengine Helloworld sample for Google App Engine Flexible 2 | 3 | > **Note** 4 | > This example was copied from 5 | > [flexible/helloworld](https://github.com/GoogleCloudPlatform/java-docs-samples/tree/main/flexible/helloworld) 6 | > and modified for Snapshot Debugger Java agent use. 7 | 8 | ## Setup 9 | 10 | See [Prerequisites](../README.md#Prerequisites). 11 | 12 | Ensure your current working directory is 13 | `samples/java/appengine-flexible/helloworld`, as all following instructions 14 | assumes this. 15 | 16 | ## Examine the `app.yaml` and `Dockerfile` files 17 | 18 | This example customizes the Java 8 / Jetty 9 runtime by explicitly providing a 19 | Dockerfile. The purpose of the Dockerfile is to add the Snapshot Debugger Java 20 | agent to the runtime. The app.yaml file provides configuration information to 21 | specify a custom runtime is being used and to configure the loading of the 22 | agent. 23 | 24 | app.yaml: 25 | 26 | https://github.com/GoogleCloudPlatform/snapshot-debugger/blob/ef597d47dadff1921dbf5a00a459d72acf1886c3/samples/java/appengine-flexible/helloworld/src/main/appengine/app.yaml#L15-L40 27 | 28 | Dockerfile: 29 | 30 | https://github.com/GoogleCloudPlatform/snapshot-debugger/blob/0868fe074c9504c9c226c93baa4fa9249afc8c68/samples/java/appengine-flexible/helloworld/src/main/docker/Dockerfile#L1-L15 31 | 32 | ## Deploy to App Engine Flexible 33 | 34 | Deploy the app with the following: 35 | 36 | ``` 37 | mvn clean package appengine:deploy 38 | ``` 39 | 40 | Early on in the output, log lines resembling the following will be emitted, make 41 | note of them: 42 | 43 | ``` 44 | [INFO] GCLOUD: target service: [default] 45 | [INFO] GCLOUD: target version: [20221125t154414] 46 | [INFO] GCLOUD: target url: [https://PROJECT_ID.REGION_ID.r.appspot.com] 47 | ``` 48 | 49 | The service and version will be used to identify your debuggee ID. 50 | 51 | ## Navigate To Your App 52 | 53 | This will ensure the app is running correctly. The URL should be provided in the 54 | `target url` output of the previous step. 55 | 56 | ## Determine the Debuggee ID 57 | 58 | Based on the service and version you'll be able to identify your debuggee ID 59 | based on the output from the `list_debuggees` command. 60 | 61 | ``` 62 | snapshot-dbg-cli list_debuggees 63 | ``` 64 | 65 | The output will resemble the following. The first column will contain an entry 66 | ` - `, which in this case is `default - 20221125t154414`. 67 | 68 | ``` 69 | Name ID Description Last Active Status 70 | -------------------------- --------- ------------------------------------------------ -------------------- ------ 71 | default - 20221125t154414 d-85ad2c65 my-project-id-20221125t154414-448123750866017122 2022-11-25T15:45:00Z ACTIVE 72 | ``` 73 | 74 | The debuggee ID in this case is `d-85ad2c65`. Using this ID you may now run 75 | through an [Example workflow](../../../../README.md#example-workflow). 76 | 77 | E.g. 78 | * Use the `set_snapshot` CLI command to set a snapshot at 79 | `HelloServlet.java:34`. Note the returned breakpoint ID. 80 | * Navigate to your application using the `target url` shown in the `mvn clean 81 | package appengine:deploy` output. This will trigger the breakpoint and 82 | collect the snapshot. 83 | * Use the `get_snapshot` CLI command to retrieve the snapshot using the 84 | breakpoint ID created with the `set_snapshot` command. 85 | -------------------------------------------------------------------------------- /samples/java/appengine-flexible/helloworld/pom.xml: -------------------------------------------------------------------------------- 1 | 16 | 17 | 4.0.0 18 | war 19 | 1.0-SNAPSHOT 20 | com.example.flexible 21 | flexible-helloworld 22 | 23 | 27 | 28 | com.google.cloud.samples 29 | shared-configuration 30 | 1.2.0 31 | 32 | 33 | 34 | 1.8 35 | 1.8 36 | 37 | false 38 | 39 | 2.4.3 40 | 9.4.44.v20210927 41 | 42 | 43 | 44 | 45 | 46 | 47 | javax.servlet 48 | javax.servlet-api 49 | 3.1.0 50 | jar 51 | provided 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | ${project.build.directory}/${project.build.finalName}/WEB-INF/classes 60 | 61 | 62 | 63 | org.apache.maven.plugins 64 | maven-war-plugin 65 | 3.3.2 66 | 67 | 68 | 69 | com.google.cloud.tools 70 | appengine-maven-plugin 71 | ${appengine.maven.plugin} 72 | 73 | GCLOUD_CONFIG 74 | GCLOUD_CONFIG 75 | 76 | 77 | 78 | 79 | org.eclipse.jetty 80 | jetty-maven-plugin 81 | ${jetty} 82 | 83 | 84 | 85 | 86 | 87 | -------------------------------------------------------------------------------- /samples/java/appengine-flexible/helloworld/src/main/appengine/app.yaml: -------------------------------------------------------------------------------- 1 | # Copyright 2016 Google Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | # 'custom` indicates a custom runtime will be used and a Dockerfile will be 16 | # provided to create it. 17 | runtime: custom 18 | env: flex 19 | 20 | # To note, all available environment variables for the Java 8 / Jetty 9 image 21 | # can be found at the following two locations: 22 | # - https://cloud.google.com/appengine/docs/flexible/java/dev-jetty9#variables 23 | # - https://cloud.google.com/appengine/docs/flexible/java/dev-java-only#variables 24 | env_variables: 25 | # Prevents the runtime from loading the current Cloud Debugger agent which is 26 | # present in the base runtime and enabled by default. Loading this agent 27 | # would conflict with the Snapshot Debugger agent. Eventually once the Cloud 28 | # Debugger has been turned down the base image will no longer contain the 29 | # agent and this won't be necessary. 30 | DBG_ENABLE: false 31 | 32 | # The contents of JAVA_USER_OPTS are added to the java command line. To note, 33 | # in this case the Java agent is able to find the class files (enabling it to 34 | # set breakpoints) without any extra configuration. This is because the 35 | # jetty.base property gets set and the root.war file is extracted to 36 | # 'jetty.base'/webapps/root. There are times however you may need to also 37 | # configure cdbg_extra_class_path if the agent cannot find the class files, 38 | # see https://github.com/GoogleCloudPlatform/cloud-debug-java#extra-classpath 39 | # for more information. 40 | JAVA_USER_OPTS: -agentpath:/opt/cdbg/cdbg_java_agent.so=--log_dir=/var/log/app_engine,--use-firebase=true 41 | 42 | handlers: 43 | - url: /.* 44 | script: this field is required, but ignored 45 | 46 | # Set scaling to 1 to minimize resources for the example. This setting should 47 | # be revisited for production use. 48 | manual_scaling: 49 | instances: 1 50 | -------------------------------------------------------------------------------- /samples/java/appengine-flexible/helloworld/src/main/docker/Dockerfile: -------------------------------------------------------------------------------- 1 | # We are customizing the Java 8 / Jetty 9 Runtime 2 | # See https://cloud.google.com/appengine/docs/flexible/java/dev-jetty9#customize 3 | # for more information on these first two lines. 4 | FROM gcr.io/google-appengine/jetty 5 | COPY ./flexible-helloworld-1.0-SNAPSHOT.war $APP_DESTINATION 6 | 7 | # Until the Cloud Debugger has been fully turned down, the rutime image will 8 | # continue to contain the current Cloud Debugger Java agent located in 9 | # /opt/cdbg. Here we remove it if it exists. 10 | RUN rm -rf /opt/cdbg 11 | 12 | # Add the Snapshot Debugger Java Agent 13 | ADD https://github.com/GoogleCloudPlatform/cloud-debug-java/releases/latest/download/cdbg_java_agent_gce.tar.gz /opt/cdbg/ 14 | RUN tar Cxfvz /opt/cdbg /opt/cdbg/cdbg_java_agent_gce.tar.gz --no-same-owner \ 15 | && rm /opt/cdbg/cdbg_java_agent_gce.tar.gz 16 | -------------------------------------------------------------------------------- /samples/java/appengine-flexible/helloworld/src/main/java/com/example/flexible/helloworld/HelloServlet.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 Google Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.example.flexible.helloworld; 18 | 19 | // [START gae_flex_servlet] 20 | import java.io.IOException; 21 | import java.io.PrintWriter; 22 | import javax.servlet.annotation.WebServlet; 23 | import javax.servlet.http.HttpServlet; 24 | import javax.servlet.http.HttpServletRequest; 25 | import javax.servlet.http.HttpServletResponse; 26 | 27 | @WebServlet(name = "helloworld", value = "") 28 | @SuppressWarnings("serial") 29 | public class HelloServlet extends HttpServlet { 30 | 31 | @Override 32 | public void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException { 33 | PrintWriter out = resp.getWriter(); 34 | out.println("Hello, world - App Engine Flexible"); 35 | } 36 | } 37 | // [END gae_flex_servlet] 38 | -------------------------------------------------------------------------------- /samples/java/appengine-java11/appengine-simple-jetty-main/README.md: -------------------------------------------------------------------------------- 1 | # Embedded Jetty Server for Google App Engine Standard with Java 11 2 | 3 | > **Note** 4 | > This directory was copied from [appengine-java11/appengine-simple-jetty-main](https://github.com/GoogleCloudPlatform/java-docs-samples/tree/main/appengine-java11/appengine-simple-jetty-main). 5 | 6 | For the Java 11 runtime, your application must have a `Main` class that starts a 7 | web server. This sample is a shared artifact that provides a `Main` class to 8 | instantiate an HTTP server to run an embedded web application `WAR` file. 9 | 10 | ## Install the dependency 11 | 12 | This sample is used as a dependency and must be installed locally: 13 | 14 | ``` 15 | mvn install 16 | ``` 17 | 18 | ## Using the dependency 19 | 20 | See [`helloworld-servlet`](../helloworld-servlet) for a complete example. 21 | 22 | Your project's `pom.xml` needs to be updated accordingly: 23 | 24 | - Add the `appengine-simple-jetty-main` dependency: 25 | 26 | ``` 27 | 28 | com.example.appengine.demo 29 | simple-jetty-main 30 | 1 31 | provided 32 | 33 | ``` 34 | 35 | - On deployment, the App Engine runtime uploads files located in 36 | `${build.directory}/appengine-staging`. Add the `maven-dependency-plugin` to 37 | the build in order to copy dependencies to the correct folder: 38 | 39 | ``` 40 | 41 | org.apache.maven.plugins 42 | maven-dependency-plugin 43 | 3.1.1 44 | 45 | 46 | copy 47 | prepare-package 48 | 49 | copy-dependencies 50 | 51 | 52 | 53 | ${project.build.directory}/appengine-staging 54 | 55 | 56 | 57 | 58 | 59 | ``` 60 | 61 | See [`helloworld-servlet`](../helloworld-servlet) for how to write your 62 | `app.yaml` file to use the dependency by specifying the entry point. 63 | -------------------------------------------------------------------------------- /samples/java/appengine-java11/appengine-simple-jetty-main/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4.0.0 4 | com.example.appengine.demo 5 | simple-jetty-main 6 | simplejettymain-j11 7 | 1 8 | jar 9 | 10 | 14 | 15 | com.google.cloud.samples 16 | shared-configuration 17 | 1.2.0 18 | 19 | 20 | 21 | UTF-8 22 | 11 23 | 11 24 | 25 | 26 | 27 | 28 | 29 | 30 | org.eclipse.jetty 31 | jetty-server 32 | 9.4.51.v20230217 33 | 34 | 35 | org.eclipse.jetty 36 | jetty-webapp 37 | 9.4.44.v20210927 38 | jar 39 | 40 | 41 | org.eclipse.jetty 42 | jetty-util 43 | 9.4.44.v20210927 44 | 45 | 46 | org.eclipse.jetty 47 | jetty-annotations 48 | 9.4.43.v20210629 49 | jar 50 | 51 | 52 | 53 | org.eclipse.jetty 54 | apache-jsp 55 | 9.4.44.v20210927 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | org.codehaus.mojo 66 | exec-maven-plugin 67 | 3.0.0 68 | 69 | 70 | 71 | java 72 | 73 | 74 | 75 | 76 | com.example.appengine.demo.jettymain.Main 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | -------------------------------------------------------------------------------- /samples/java/appengine-java11/appengine-simple-jetty-main/src/main/java/com/example/appengine/demo/jettymain/Main.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.example.appengine.demo.jettymain; 18 | 19 | // [START gae_java11_server] 20 | import org.eclipse.jetty.server.Server; 21 | import org.eclipse.jetty.webapp.Configuration.ClassList; 22 | import org.eclipse.jetty.webapp.WebAppContext; 23 | 24 | /** 25 | * Simple Jetty Main that can execute a directory containing an exploded webapp 26 | * when passed as an argument. 27 | */ 28 | public class Main { 29 | 30 | public static void main(String[] args) throws Exception { 31 | if (args.length != 1) { 32 | System.err.println("Usage: need a relative path to the war file to execute"); 33 | System.exit(1); 34 | } 35 | System.setProperty("org.eclipse.jetty.util.log.class", "org.eclipse.jetty.util.log.StrErrLog"); 36 | System.setProperty("org.eclipse.jetty.LEVEL", "INFO"); 37 | 38 | // Create a basic Jetty server object that will listen on port defined by 39 | // the PORT environment variable when present, otherwise on 8080. 40 | int port = Integer.parseInt(System.getenv().getOrDefault("PORT", "8080")); 41 | Server server = new Server(port); 42 | 43 | // The WebAppContext is the interface to provide configuration for a web 44 | // application. In this example, the context path is being set to "/" so 45 | // it is suitable for serving root context requests. It is expecting the 46 | // first argument passed in is the directory containing an exploded 47 | // webapp. 48 | WebAppContext webapp = new WebAppContext(args[0], "/"); 49 | ClassList classlist = ClassList.setServerDefault(server); 50 | 51 | // Enable Annotation Scanning. 52 | classlist.addBefore( 53 | "org.eclipse.jetty.webapp.JettyWebXmlConfiguration", 54 | "org.eclipse.jetty.annotations.AnnotationConfiguration"); 55 | 56 | // Set the the WebAppContext as the ContextHandler for the server. 57 | server.setHandler(webapp); 58 | 59 | // Start the server! By using the server.join() the server thread will 60 | // join with the current thread. See 61 | // "http://docs.oracle.com/javase/1.5.0/docs/api/java/lang/Thread.html#join()" 62 | // for more details. 63 | server.start(); 64 | server.join(); 65 | } 66 | } 67 | // [END gae_java11_server] 68 | -------------------------------------------------------------------------------- /samples/java/appengine-java11/helloworld-servlet/src/main/appengine/app.yaml: -------------------------------------------------------------------------------- 1 | # Copyright 2019 Google LLC 2 | # Licensed under the Apache License, Version 2.0 (the "License"); 3 | # you may not use this file except in compliance with the License. 4 | # You may obtain a copy of the License at 5 | # 6 | # http://www.apache.org/licenses/LICENSE-2.0 7 | # 8 | # Unless required by applicable law or agreed to in writing, software 9 | # distributed under the License is distributed on an "AS IS" BASIS, 10 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | # See the License for the specific language governing permissions and 12 | # limitations under the License. 13 | 14 | runtime: java11 15 | entrypoint: 'java -agentpath:/workspace/cdbg/cdbg_java_agent.so=--use-firebase=true,--cdbg_extra_class_path=/workspace/helloworld/WEB-INF/classes:/workspace/helloworld/WEB-INF/lib -cp "*" com.example.appengine.demo.jettymain.Main /workspace/helloworld' 16 | -------------------------------------------------------------------------------- /samples/java/appengine-java11/helloworld-servlet/src/main/java/com/example/appengine/HelloAppEngine.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.example.appengine; 18 | 19 | import java.io.IOException; 20 | import javax.servlet.annotation.WebServlet; 21 | import javax.servlet.http.HttpServlet; 22 | import javax.servlet.http.HttpServletRequest; 23 | import javax.servlet.http.HttpServletResponse; 24 | 25 | @WebServlet(name = "HelloAppEngine", value = "/hello") 26 | public class HelloAppEngine extends HttpServlet { 27 | 28 | @Override 29 | public void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException { 30 | 31 | response.setContentType("text/html"); 32 | response.getWriter().println("

Hello, Google App Engine with Java 11!

"); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /samples/java/appengine-java11/simple-server/.gitignore: -------------------------------------------------------------------------------- 1 | cdbg/* 2 | -------------------------------------------------------------------------------- /samples/java/appengine-java11/simple-server/Main.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import com.sun.net.httpserver.HttpServer; 18 | import java.io.IOException; 19 | import java.io.OutputStream; 20 | import java.net.InetSocketAddress; 21 | 22 | public class Main { 23 | 24 | public static void main(String[] args) throws IOException { 25 | // Create an instance of HttpServer bound to port defined by the 26 | // PORT environment variable when present, otherwise on 8080. 27 | int port = Integer.parseInt(System.getenv().getOrDefault("PORT", "8080")); 28 | HttpServer server = HttpServer.create(new InetSocketAddress(port), 0); 29 | 30 | // Set root URI path. 31 | server.createContext("/", (var t) -> { 32 | byte[] response = "Hello World from Google App Engine Java 11.".getBytes(); 33 | t.sendResponseHeaders(200, response.length); 34 | try (OutputStream os = t.getResponseBody()) { 35 | os.write(response); 36 | } 37 | }); 38 | 39 | // Start the server. 40 | server.start(); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /samples/java/appengine-java11/simple-server/README.md: -------------------------------------------------------------------------------- 1 | # Using the Java Agent on a Simple One File Server App in GAE Standard Java 11 2 | 3 | > **Note** 4 | > This example was copied from 5 | > [appengine-java11/custom-entrypoint](https://github.com/GoogleCloudPlatform/java-docs-samples/blob/main/appengine-java11/custom-entrypoint) 6 | > and modified for Snapshot Debugger Java agent use. 7 | 8 | 9 | ## Setup 10 | See [Prerequisites](../README.md#Prerequisites). 11 | 12 | Ensure your current working directory is 13 | `samples/java/appengine-java11/simple-server`, as all following instructions 14 | assumes this. 15 | 16 | ## Compile the Source With Full Debug Information Enabled 17 | 18 | The `-g` option here is what enables full debug information. Without it 19 | information on method arguments and local variables would not be available. The 20 | `-source` and `-target` options are to ensure it will run with the Java 11 21 | runtime. 22 | 23 | ``` 24 | javac -source 11 -target 11 -g Main.java 25 | ``` 26 | 27 | ## Install the Snapshot Debugger Java Agent 28 | 29 | ``` 30 | mkdir cdbg 31 | wget -qO- https://github.com/GoogleCloudPlatform/cloud-debug-java/releases/latest/download/cdbg_java_agent_gce.tar.gz | tar xvz -C cdbg 32 | ``` 33 | 34 | ## Deploy to App Engine Standard 35 | 36 | Examine the app.yaml contents, which provides a custom entry point that 37 | specifies the `-agentpath` java option to load the agent: 38 | 39 | https://github.com/GoogleCloudPlatform/snapshot-debugger/blob/ef7db1b29937f1f6e140d69214aadf51bddb3770/samples/java/appengine-java11/simple-server/app.yaml#L15-L16 40 | 41 | Deploy the app with the following: 42 | 43 | ``` 44 | gcloud app deploy 45 | ``` 46 | 47 | Make note of the following output entries, which should resemble the following: 48 | 49 | ``` 50 | [...snip] 51 | target service: [default] 52 | target version: [20221122t161333] 53 | target url: [https://.appspot.com] 54 | [...snip] 55 | ``` 56 | 57 | The service and version will be used to identify your debuggee ID. 58 | 59 | ## Navigate To Your App 60 | 61 | This will ensure the app is run and is required as your app will not be 62 | debuggable until after the first request has been received. The URL should be 63 | provided in the `target url` output of the previous step. 64 | 65 | ## Determine the Debuggee ID 66 | 67 | Based on the service and version you'll be able to identify your debuggee ID 68 | based on the output from the `list_debuggees` command. 69 | 70 | ``` 71 | snapshot-dbg-cli list_debuggees 72 | ``` 73 | 74 | The output will resemble the following. The first column will contain an entry 75 | ` - `, which in this case is `default - 20221117t213436`. 76 | 77 | ``` 78 | Name ID Description Last Active Status 79 | ------------------------- ---------- ------------------------------------------------ -------------------- ------ 80 | default - 20221122t161333 d-ad4829f7 my-project-id-20221122t161333-448054658875855015 2022-11-22T16:14:00Z ACTIVE 81 | ``` 82 | 83 | The debuggee ID in this case is `d-ad4829f7`. Using this ID you may now run 84 | through an [Example workflow](../../../../README.md#example-workflow). 85 | 86 | E.g. 87 | * Use the `set_snapshot` CLI command to set a snapshot at `Main.java:33`. Note 88 | the returned breakpoint ID. 89 | * Navigate to your application using the `target url` shown in the `gcloud 90 | app deploy` output. This will trigger the breakpoint and collect the snapshot. 91 | * Use the `get_snapshot` CLI command to retrieve the snapshot using the 92 | breakpoint ID created with the `set_snapshot` command. 93 | 94 | ## Troubleshooting 95 | 96 | ### Can't see Debuggee (via list_debuggees) 97 | 98 | Be sure you navidate to `target url` to wake the application up. In App 99 | Engine Standard the newly deployed application will not actually run until it 100 | receives a request. 101 | -------------------------------------------------------------------------------- /samples/java/appengine-java11/simple-server/app.yaml: -------------------------------------------------------------------------------- 1 | # Copyright 2019 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | runtime: java11 16 | entrypoint: java -agentpath:/workspace/cdbg/cdbg_java_agent.so=--use-firebase=true Main 17 | -------------------------------------------------------------------------------- /samples/java/appengine-java8/helloworld/src/main/java/com/example/appengine/java8/HelloAppEngine.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 Google Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.example.appengine.java8; 18 | 19 | import com.google.appengine.api.utils.SystemProperty; 20 | import java.io.IOException; 21 | import java.util.Properties; 22 | import javax.servlet.annotation.WebServlet; 23 | import javax.servlet.http.HttpServlet; 24 | import javax.servlet.http.HttpServletRequest; 25 | import javax.servlet.http.HttpServletResponse; 26 | 27 | // With @WebServlet annotation the webapp/WEB-INF/web.xml is no longer required. 28 | @WebServlet(name = "HelloAppEngine", value = "/hello") 29 | public class HelloAppEngine extends HttpServlet { 30 | 31 | @Override 32 | public void doGet(HttpServletRequest request, HttpServletResponse response) 33 | throws IOException { 34 | Properties properties = System.getProperties(); 35 | 36 | response.setContentType("text/plain"); 37 | response.getWriter().println("Hello App Engine - Standard using " 38 | + SystemProperty.version.get() + " Java " 39 | + properties.get("java.specification.version")); 40 | } 41 | 42 | public static String getInfo() { 43 | return "Version: " + System.getProperty("java.version") 44 | + " OS: " + System.getProperty("os.name") 45 | + " User: " + System.getProperty("user.name"); 46 | } 47 | 48 | } 49 | -------------------------------------------------------------------------------- /samples/java/appengine-java8/helloworld/src/main/webapp/WEB-INF/appengine-web.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 18 | 19 | java8 20 | true 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /samples/java/appengine-java8/helloworld/src/main/webapp/index.jsp: -------------------------------------------------------------------------------- 1 | 2 | 3 | <%-- 4 | ~ Copyright 2017 Google Inc. 5 | ~ 6 | ~ Licensed under the Apache License, Version 2.0 (the "License"); you 7 | ~ may not use this file except in compliance with the License. You may 8 | ~ obtain a copy of the License at 9 | ~ 10 | ~ http://www.apache.org/licenses/LICENSE-2.0 11 | ~ 12 | ~ Unless required by applicable law or agreed to in writing, software 13 | ~ distributed under the License is distributed on an "AS IS" BASIS, 14 | ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 15 | ~ implied. See the License for the specific language governing 16 | ~ permissions and limitations under the License. 17 | --%> 18 | 19 | <%@ page contentType="text/html;charset=UTF-8" language="java" %> 20 | <%@ page import="com.example.appengine.java8.HelloAppEngine" %> 21 | 22 | 23 | 24 | Hello App Engine Standard Java 8 25 | 26 | 27 |

Hello App Engine -- Java 8!

28 | 29 |

This is <%= HelloAppEngine.getInfo() %>.

30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 |
Available Servlets:
Hello App Engine
38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /samples/java/appengine-java8/helloworld/src/test/java/com/example/appengine/java8/HelloAppEngineTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 Google Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.example.appengine.java8; 18 | 19 | import static com.google.common.truth.Truth.assertThat; 20 | import static org.mockito.Mockito.when; 21 | 22 | import com.google.appengine.tools.development.testing.LocalServiceTestHelper; 23 | import java.io.PrintWriter; 24 | import java.io.StringWriter; 25 | import javax.servlet.http.HttpServletRequest; 26 | import javax.servlet.http.HttpServletResponse; 27 | import org.junit.After; 28 | import org.junit.Before; 29 | import org.junit.Test; 30 | import org.junit.runner.RunWith; 31 | import org.junit.runners.JUnit4; 32 | import org.mockito.Mock; 33 | import org.mockito.MockitoAnnotations; 34 | 35 | /** 36 | * Unit tests for {@link HelloAppEngine}. 37 | */ 38 | @RunWith(JUnit4.class) 39 | public class HelloAppEngineTest { 40 | private static final String FAKE_URL = "fake.fk/hello"; 41 | // Set up a helper so that the ApiProxy returns a valid environment for local testing. 42 | private final LocalServiceTestHelper helper = new LocalServiceTestHelper(); 43 | 44 | @Mock private HttpServletRequest mockRequest; 45 | @Mock private HttpServletResponse mockResponse; 46 | private StringWriter responseWriter; 47 | private HelloAppEngine servletUnderTest; 48 | 49 | @Before 50 | public void setUp() throws Exception { 51 | MockitoAnnotations.initMocks(this); 52 | helper.setUp(); 53 | 54 | // Set up some fake HTTP requests 55 | when(mockRequest.getRequestURI()).thenReturn(FAKE_URL); 56 | 57 | // Set up a fake HTTP response. 58 | responseWriter = new StringWriter(); 59 | when(mockResponse.getWriter()).thenReturn(new PrintWriter(responseWriter)); 60 | 61 | servletUnderTest = new HelloAppEngine(); 62 | } 63 | 64 | @After public void tearDown() { 65 | helper.tearDown(); 66 | } 67 | 68 | @Test 69 | public void doGetWritesResponse() throws Exception { 70 | servletUnderTest.doGet(mockRequest, mockResponse); 71 | 72 | // We expect our hello world response. 73 | assertThat(responseWriter.toString()) 74 | .contains("Hello App Engine - Standard "); 75 | } 76 | 77 | @Test 78 | public void helloInfoTest() { 79 | String result = HelloAppEngine.getInfo(); 80 | assertThat(result) 81 | .containsMatch("^Version:\\s+.+OS:\\s+.+User:\\s"); 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /samples/java/gce/config/base/etc/java-util-logging.properties: -------------------------------------------------------------------------------- 1 | # Copyright 2019 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | # A default java.util.logging configuration. 16 | # 17 | 18 | handlers=java.util.logging.ConsoleHandler,java.util.logging.FileHandler 19 | 20 | java.util.logging.FileHandler.level=INFO 21 | #java.util.logging.FileHandler.formatter= 22 | #java.util.logging.FileHandler.pattern=/opt/jetty/logs/app.%g.log.json 23 | java.util.logging.FileHandler.limit=104857600 24 | java.util.logging.FileHandler.count=3 25 | 26 | # Set the default logging level for all loggers to INFO 27 | .level=INFO 28 | 29 | # Override root level 30 | com.foo.level=FINE 31 | 32 | # Override parent's level 33 | com.foo.bar.level=SEVERE 34 | -------------------------------------------------------------------------------- /samples/java/gce/config/base/modules/gce.mod: -------------------------------------------------------------------------------- 1 | # Copyright 2019 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | # 15 | # GCE Module 16 | # 17 | 18 | [depend] 19 | resources 20 | server 21 | 22 | [optional] 23 | 24 | [ini-template] 25 | 26 | ## Google Defaults 27 | jetty.httpConfig.outputAggregationSize=32768 28 | jetty.httpConfig.headerCacheSize=512 29 | 30 | jetty.httpConfig.sendServerVersion=true 31 | jetty.httpConfig.sendDateHeader=false 32 | 33 | #gae.httpPort=80 34 | #gae.httpsPort=443 35 | -------------------------------------------------------------------------------- /samples/java/gce/config/base/resources/jetty-logging.properties: -------------------------------------------------------------------------------- 1 | # Copyright 2019 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | # Direct Jetty logging to JavaUtilLog 16 | # see etc/java-util-logging.properties 17 | org.eclipse.jetty.util.log.class=org.eclipse.jetty.util.log.JavaUtilLog 18 | -------------------------------------------------------------------------------- /samples/java/gce/deploy.sh: -------------------------------------------------------------------------------- 1 | # Copyright 2019 Google LLC All Rights Reserved. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | set -ex 16 | 17 | MY_INSTANCE_NAME="my-app-instance" 18 | ZONE=us-central1-a 19 | 20 | gcloud compute instances create $MY_INSTANCE_NAME \ 21 | --image-family=debian-10 \ 22 | --image-project=debian-cloud \ 23 | --machine-type=g1-small \ 24 | --scopes userinfo-email,cloud-platform,https://www.googleapis.com/auth/firebase.database \ 25 | --metadata-from-file startup-script=startup-script.sh \ 26 | --zone $ZONE \ 27 | --tags http-server 28 | 29 | gcloud compute firewall-rules create default-allow-http-80 \ 30 | --allow tcp:80 \ 31 | --source-ranges 0.0.0.0/0 \ 32 | --target-tags http-server \ 33 | --description "Allow port 80 access to http-server" 34 | -------------------------------------------------------------------------------- /samples/java/gce/src/main/appengine/app.yaml: -------------------------------------------------------------------------------- 1 | # Copyright 2019 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | # [START runtime] 16 | runtime: java 17 | env: flex 18 | 19 | handlers: 20 | - url: /.* 21 | script: this field is required, but ignored 22 | 23 | # [START env_variables] 24 | env_variables: # Logging options 25 | JAVA_OPTS: >- 26 | -D.level=INFO 27 | # [END env_variables] 28 | # [END runtime] 29 | 30 | runtime_config: # Optional 31 | jdk: openjdk8 32 | server: jetty9 33 | -------------------------------------------------------------------------------- /samples/java/gce/src/main/java/com/example/getstarted/basicactions/HelloworldController.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.example.getstarted.basicactions; 18 | 19 | import java.io.IOException; 20 | import javax.servlet.annotation.WebServlet; 21 | import javax.servlet.http.HttpServlet; 22 | import javax.servlet.http.HttpServletRequest; 23 | import javax.servlet.http.HttpServletResponse; 24 | 25 | @WebServlet(value = "/") 26 | public class HelloworldController extends HttpServlet { 27 | 28 | @Override 29 | public void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException { 30 | resp.getWriter().write("Hello world - GCE!"); 31 | resp.setStatus(HttpServletResponse.SC_OK); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /samples/java/gce/src/main/webapp/WEB-INF/web.xml: -------------------------------------------------------------------------------- 1 | 2 | 14 | 15 | 16 | 20 | 21 | 22 | / 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /samples/java/gce/src/test/java/com/example/getstarted/basicactions/UserJourneyTestIT.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.example.getstarted.basicactions; 18 | 19 | import static org.junit.Assert.assertTrue; 20 | 21 | import org.junit.After; 22 | import org.junit.Before; 23 | import org.junit.BeforeClass; 24 | import org.junit.Ignore; 25 | import org.junit.Test; 26 | import org.junit.runner.RunWith; 27 | import org.junit.runners.JUnit4; 28 | import org.openqa.selenium.WebDriver; 29 | import org.openqa.selenium.chrome.ChromeDriverService; 30 | import org.openqa.selenium.chrome.ChromeOptions; 31 | import org.openqa.selenium.remote.RemoteWebDriver; 32 | import org.openqa.selenium.remote.service.DriverService; 33 | 34 | @RunWith(JUnit4.class) 35 | @SuppressWarnings("checkstyle:AbbreviationAsWordInName") 36 | public class UserJourneyTestIT { 37 | private static DriverService service; 38 | private WebDriver driver; 39 | 40 | @BeforeClass 41 | public static void setupClass() throws Exception { 42 | service = ChromeDriverService.createDefaultService(); 43 | service.start(); 44 | } 45 | 46 | @Before 47 | public void setup() { 48 | driver = new RemoteWebDriver(service.getUrl(), new ChromeOptions()); 49 | } 50 | 51 | @After 52 | public void tearDown() { 53 | driver.quit(); 54 | } 55 | 56 | @Test 57 | @Ignore("b/138123046") 58 | public void userJourney() { 59 | driver.get("http://localhost:8080"); 60 | 61 | try { 62 | assertTrue(driver.getPageSource().contains("Hello world - GCE!")); 63 | } catch (Exception e) { 64 | System.err.println(driver.getPageSource()); 65 | throw e; 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /samples/java/gce/teardown.sh: -------------------------------------------------------------------------------- 1 | #! /bin/bash 2 | 3 | # Copyright 2019 Google LLC All Rights Reserved. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | set -x 18 | 19 | MY_INSTANCE_NAME="my-app-instance" 20 | ZONE=us-central1-a 21 | 22 | gcloud compute instances delete $MY_INSTANCE_NAME \ 23 | --zone=$ZONE --delete-disks=all 24 | 25 | gcloud compute firewall-rules delete default-allow-http-80 26 | -------------------------------------------------------------------------------- /samples/node-js/appengine-flexible/README.md: -------------------------------------------------------------------------------- 1 | # Snapshot Debugger Examples for Node.js in the App Engine flexible environment 2 | 3 | NOTE: This sample application was copied from 4 | [nodejs-docs-samples/appengine/hello-world/flexible][sample-source] 5 | and modified for the Snapshot Debugger samples here. 6 | 7 | 8 | This is the sample application for the 9 | [Quickstart for Node.js in the App Engine flexible environment](https://cloud.google.com/appengine/docs/flexible/nodejs/quickstart) 10 | tutorial found in the [Google App Engine Node.js flexible environment](https://cloud.google.com/appengine/docs/flexible/nodejs) 11 | documentation. 12 | 13 | * [Setup](#setup) 14 | * [Deploying to App Engine](#deploying-to-app-engine) 15 | 16 | ## Setup 17 | 18 | Before you can run or deploy the sample, you need to do the following: 19 | 20 | 1. Perform all [Prerequisite Steps](../../app_engine_flexible_prerequisites.md) 21 | 1. Install dependencies: 22 | 23 | npm install 24 | 25 | ## Deploying to App Engine 26 | 27 | The following code changes have been made to enable the Snapshot Debugger: 28 | 29 | https://github.com/GoogleCloudPlatform/snapshot-debugger/blob/43428608e9fd262cd3a4d48c151b6577a8abd5e1/samples/node-js/appengine-flexible/package.json#L18-L21 30 | 31 | https://github.com/GoogleCloudPlatform/snapshot-debugger/blob/43428608e9fd262cd3a4d48c151b6577a8abd5e1/samples/node-js/appengine-flexible/app.js#L17-L20 32 | 33 | Then deploy to App Engine as usual: 34 | 35 | gcloud app deploy 36 | 37 | ## Navigate To Your App 38 | 39 | This will ensure the app is run and is required as your app will not be 40 | debuggable until after the first request has been received. The URL should be 41 | provided in the `target url` output of the previous step. 42 | 43 | ## Determine the Debuggee ID 44 | 45 | Based on the service and version you'll be able to identify your debuggee ID 46 | based on the output from the `list_debuggees` command. 47 | 48 | ``` 49 | snapshot-dbg-cli list_debuggees 50 | ``` 51 | 52 | The output will resemble the following. The first column will contain an entry 53 | ` - `, which in this case is `default - 20221117t213436`. 54 | 55 | ``` 56 | Name ID Description Last Active Status 57 | ------------------------- ---------- ----------------------------------- -------------------- ------ 58 | default - 20221122t161333 d-ad4829f7 node app.js version:20221122t161333 2022-11-22T16:15:00Z ACTIVE 59 | ``` 60 | 61 | The debuggee ID in this case is `d-ad4829f7`. Using this ID you may now run 62 | through an [Example workflow](../../../README.md#example-workflow). 63 | 64 | E.g. 65 | * Use the `set_snapshot` CLI command to set a snapshot at `app.js:28`. 66 | Note the returned breakpoint ID. 67 | * Navigate to your application using the `target url` shown in the 68 | `gcloud app deploy` output. This will trigger the breakpoint and 69 | collect the snapshot. 70 | * Use the `get_snapshot` CLI command to retrieve the snapshot using the 71 | breakpoint ID created with the `set_snapshot` command. 72 | 73 | [sample-source]: https://github.com/GoogleCloudPlatform/nodejs-docs-samples/blob/main/appengine/hello-world/flexible 74 | 75 | -------------------------------------------------------------------------------- /samples/node-js/appengine-flexible/app.js: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | 'use strict'; 16 | 17 | require('@google-cloud/debug-agent').start({ 18 | useFirebase: true, 19 | allowExpressions: true, 20 | }); 21 | 22 | const express = require('express'); 23 | 24 | const app = express(); 25 | 26 | app.get('/', (req, res) => { 27 | res.status(200).send('Hello, world!').end(); 28 | }); 29 | 30 | // Start the server 31 | const PORT = parseInt(process.env.PORT) || 8080; 32 | app.listen(PORT, () => { 33 | console.log(`App listening on port ${PORT}`); 34 | console.log('Press Ctrl+C to quit.'); 35 | }); 36 | 37 | module.exports = app; 38 | -------------------------------------------------------------------------------- /samples/node-js/appengine-flexible/app.yaml: -------------------------------------------------------------------------------- 1 | # Copyright 2017, Google, Inc. 2 | # Licensed under the Apache License, Version 2.0 (the "License"); 3 | # you may not use this file except in compliance with the License. 4 | # You may obtain a copy of the License at 5 | # 6 | # http://www.apache.org/licenses/LICENSE-2.0 7 | # 8 | # Unless required by applicable law or agreed to in writing, software 9 | # distributed under the License is distributed on an "AS IS" BASIS, 10 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | # See the License for the specific language governing permissions and 12 | # limitations under the License. 13 | 14 | runtime: nodejs 15 | env: flex 16 | 17 | # This sample incurs costs to run on the App Engine flexible environment. 18 | # The settings below are to reduce costs during testing and are not appropriate 19 | # for production use. For more information, see: 20 | # https://cloud.google.com/appengine/docs/flexible/nodejs/configuring-your-app-with-app-yaml 21 | manual_scaling: 22 | instances: 1 23 | resources: 24 | cpu: 1 25 | memory_gb: 0.5 26 | disk_size_gb: 10 27 | 28 | -------------------------------------------------------------------------------- /samples/node-js/appengine-flexible/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "appengine-hello-world", 3 | "description": "Simple Hello World Node.js sample for Google App Engine Flexible Environment.", 4 | "version": "0.0.2", 5 | "private": true, 6 | "license": "Apache-2.0", 7 | "author": "Google Inc.", 8 | "repository": { 9 | "type": "git", 10 | "url": "https://github.com/GoogleCloudPlatform/nodejs-docs-samples.git" 11 | }, 12 | "engines": { 13 | "node": "=v16.19.0" 14 | }, 15 | "scripts": { 16 | "start": "node app.js" 17 | }, 18 | "dependencies": { 19 | "express": "^4.17.1", 20 | "@google-cloud/debug-agent": "^7.2.1" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /samples/node-js/appengine-standard/README.md: -------------------------------------------------------------------------------- 1 | # Snapshot Debugger Examples for Node.js in the App Engine standard environment 2 | 3 | NOTE: This sample application was copied from 4 | [nodejs-docs-samples/appengine/hello-world/standard][sample-source] 5 | and modified for the Snapshot Debugger samples here. 6 | 7 | 8 | This is the sample application for the 9 | [Quickstart for Node.js in the App Engine standard environment](https://cloud.google.com/appengine/docs/standard/nodejs/quickstart) 10 | tutorial found in the 11 | [Google App Engine Node.js standard environment](https://cloud.google.com/appengine/docs/standard/nodejs) 12 | documentation. 13 | 14 | * [Setup](#setup) 15 | * [Deploying to App Engine](#deploying-to-app-engine) 16 | 17 | ## Setup 18 | 19 | Before you can run or deploy the sample, you need to do the following: 20 | 21 | 1. Perform all [Prerequisite Steps](../../app_engine_standard_prerequisites.md) 22 | 1. Install dependencies: 23 | 24 | npm install 25 | 26 | ## Deploying to App Engine 27 | 28 | The following code changes have been made to enable the Snapshot Debugger: 29 | 30 | https://github.com/GoogleCloudPlatform/snapshot-debugger/blob/c7cc477b9a66541f9b6804f0b6ba215547901afc/samples/node-js/appengine-standard/package.json#L18-L21 31 | 32 | https://github.com/GoogleCloudPlatform/snapshot-debugger/blob/c7cc477b9a66541f9b6804f0b6ba215547901afc/samples/node-js/appengine-standard/app.js#L17-L20 33 | 34 | Then deploy to App Engine as usual: 35 | 36 | gcloud app deploy 37 | 38 | 39 | ## Navigate To Your App 40 | 41 | This will ensure the app is run and is required as your app will not be 42 | debuggable until after the first request has been received. The URL should be 43 | provided in the `target url` output of the previous step. 44 | 45 | ## Determine the Debuggee ID 46 | 47 | Based on the service and version you'll be able to identify your debuggee ID 48 | based on the output from the `list_debuggees` command. 49 | 50 | ``` 51 | snapshot-dbg-cli list_debuggees 52 | ``` 53 | 54 | The output will resemble the following. The first column will contain an entry 55 | ` - `, which in this case is `default - 20221117t213436`. 56 | 57 | ``` 58 | Name ID Description Last Active Status 59 | ------------------------- ---------- ----------------------------------- -------------------- ------ 60 | default - 20221122t161333 d-ad4829f7 node app.js version:20221122t161333 2022-11-22T16:15:00Z ACTIVE 61 | ``` 62 | 63 | The debuggee ID in this case is `d-ad4829f7`. Using this ID you may now run 64 | through an [Example workflow](../../../README.md#example-workflow). 65 | 66 | E.g. 67 | * Use the `set_snapshot` CLI command to set a snapshot at `app.js:28`. 68 | Note the returned breakpoint ID. 69 | * Navigate to your application using the `target url` shown in the 70 | `gcloud app deploy` output. This will trigger the breakpoint and 71 | collect the snapshot. 72 | * Use the `get_snapshot` CLI command to retrieve the snapshot using the 73 | breakpoint ID created with the `set_snapshot` command. 74 | 75 | [sample-source]: https://github.com/GoogleCloudPlatform/nodejs-docs-samples/tree/main/appengine/hello-world/standard 76 | 77 | -------------------------------------------------------------------------------- /samples/node-js/appengine-standard/app.js: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | 'use strict'; 16 | 17 | require('@google-cloud/debug-agent').start({ 18 | useFirebase: true, 19 | allowExpressions: true, 20 | }); 21 | 22 | const express = require('express'); 23 | 24 | const app = express(); 25 | 26 | app.get('/', (req, res) => { 27 | res.status(200).send('Hello, world!').end(); 28 | }); 29 | 30 | // Start the server 31 | const PORT = parseInt(process.env.PORT) || 8080; 32 | app.listen(PORT, () => { 33 | console.log(`App listening on port ${PORT}`); 34 | console.log('Press Ctrl+C to quit.'); 35 | }); 36 | 37 | module.exports = app; 38 | -------------------------------------------------------------------------------- /samples/node-js/appengine-standard/app.yaml: -------------------------------------------------------------------------------- 1 | # Copyright 2017, Google, Inc. 2 | # Licensed under the Apache License, Version 2.0 (the "License"); 3 | # you may not use this file except in compliance with the License. 4 | # You may obtain a copy of the License at 5 | # 6 | # http://www.apache.org/licenses/LICENSE-2.0 7 | # 8 | # Unless required by applicable law or agreed to in writing, software 9 | # distributed under the License is distributed on an "AS IS" BASIS, 10 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | # See the License for the specific language governing permissions and 12 | # limitations under the License. 13 | 14 | runtime: nodejs16 15 | -------------------------------------------------------------------------------- /samples/node-js/appengine-standard/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "appengine-hello-world", 3 | "description": "Simple Hello World Node.js sample for Google App Engine Standard Environment.", 4 | "version": "0.0.2", 5 | "private": true, 6 | "license": "Apache-2.0", 7 | "author": "Google Inc.", 8 | "repository": { 9 | "type": "git", 10 | "url": "https://github.com/GoogleCloudPlatform/nodejs-docs-samples.git" 11 | }, 12 | "engines": { 13 | "node": ">=14.0.0" 14 | }, 15 | "scripts": { 16 | "start": "node app.js" 17 | }, 18 | "dependencies": { 19 | "express": "^4.17.1", 20 | "@google-cloud/debug-agent": "^7.2.1" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /samples/node-js/gce/app.js: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | 'use strict'; 16 | 17 | require('@google-cloud/debug-agent').start({ 18 | useFirebase: true, 19 | allowExpressions: true, 20 | serviceContext: { 21 | service: 'gce-nodejs-sample', 22 | version: 'v1' 23 | }, 24 | }); 25 | 26 | const express = require('express'); 27 | 28 | const app = express(); 29 | 30 | app.get('/', (req, res) => { 31 | res.status(200).send('Hello, world!').end(); 32 | }); 33 | 34 | // Start the server 35 | const PORT = process.env.PORT || 8080; 36 | app.listen(PORT, () => { 37 | console.log(`App listening on port ${PORT}`); 38 | console.log('Press Ctrl+C to quit.'); 39 | }); 40 | 41 | module.exports = app; 42 | -------------------------------------------------------------------------------- /samples/node-js/gce/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "gce-hello-world", 3 | "description": "Simple Hello World Node.js sample for Google Compute Engine.", 4 | "version": "0.0.1", 5 | "private": true, 6 | "license": "Apache-2.0", 7 | "author": "Google Inc.", 8 | "repository": { 9 | "type": "git", 10 | "url": "https://github.com/GoogleCloudPlatform/nodejs-getting-started.git" 11 | }, 12 | "engines": { 13 | "node": ">=12" 14 | }, 15 | "scripts": { 16 | "start": "node app.js" 17 | }, 18 | "dependencies": { 19 | "express": "^4.16.3", 20 | "@google-cloud/debug-agent": "^7.2.1" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /samples/node-js/gce/startup-script.sh: -------------------------------------------------------------------------------- 1 | #! /bin/bash 2 | # Copyright 2019, Google, Inc. 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | set -e 16 | set -v 17 | 18 | 19 | # Talk to the metadata server to get the project id 20 | PROJECTID=$(curl -s "http://metadata.google.internal/computeMetadata/v1/project/project-id" -H "Metadata-Flavor: Google") 21 | echo ${PROJECTID} 22 | 23 | # Install logging monitor. The monitor will automatically pick up logs sent to 24 | # syslog. 25 | curl -s "https://storage.googleapis.com/signals-agents/logging/google-fluentd-install.sh" | bash 26 | service google-fluentd restart & 27 | 28 | # Install dependencies from apt 29 | apt-get update 30 | apt-get install -yq ca-certificates git build-essential supervisor 31 | 32 | # Install nodejs 33 | mkdir /opt/nodejs 34 | curl https://nodejs.org/dist/v16.15.0/node-v16.15.0-linux-x64.tar.gz | tar xzf - -C /opt/nodejs --strip-components=1 35 | ln -s /opt/nodejs/bin/node /usr/bin/node 36 | ln -s /opt/nodejs/bin/npm /usr/bin/npm 37 | 38 | # Get the application source code from the Github Repository. 39 | # git requires $HOME and it's not set during the startup script. 40 | export HOME=/root 41 | git config --global credential.helper gcloud.sh 42 | git clone https://github.com/GoogleCloudPlatform/snapshot-debugger /opt/app/new-repo 43 | 44 | # Install app dependencies 45 | cd /opt/app/new-repo/samples/node-js/gce 46 | echo "about to install application dependencies" 47 | npm install 48 | # FAILING HERE? 49 | 50 | # Create a nodeapp user. The application will run as this user. 51 | useradd -m -d /home/nodeapp nodeapp 52 | chown -R nodeapp:nodeapp /opt/app 53 | 54 | # Configure supervisor to run the node app. 55 | cat >/etc/supervisor/conf.d/node-app.conf << EOF 56 | [program:nodeapp] 57 | directory=/opt/app/new-repo/samples/node-js/gce 58 | command=npm start 59 | autostart=true 60 | autorestart=true 61 | user=nodeapp 62 | environment=HOME="/home/nodeapp",USER="nodeapp",NODE_ENV="production" 63 | stdout_logfile=syslog 64 | stderr_logfile=syslog 65 | EOF 66 | 67 | supervisorctl reread 68 | supervisorctl update 69 | 70 | # Application should now be running under supervisor 71 | # [END startup] 72 | -------------------------------------------------------------------------------- /samples/python/appengine-flexible/README.md: -------------------------------------------------------------------------------- 1 | # Snapshot Debugger Examples for Python in the App Engine flexible environment 2 | 3 | NOTE: This sample application was copied from 4 | [python-docs-samples/appengine/hello-world/flexible][sample-source] 5 | and modified for the Snapshot Debugger samples here. 6 | 7 | 8 | This is the sample application for the 9 | [Quickstart for Python in the App Engine flexible environment](https://cloud.google.com/appengine/docs/flexible/python/quickstart) 10 | tutorial found in the [Google App Engine Python flexible environment](https://cloud.google.com/appengine/docs/flexible/python) 11 | documentation. 12 | 13 | * [Setup](#setup) 14 | * [Deploying to App Engine](#deploying-to-app-engine) 15 | 16 | ## Setup 17 | 18 | Before you can run or deploy the sample, you need to do the following: 19 | 20 | 1. Perform all [Prerequisite Steps](../../app_engine_flexible_prerequisites.md) 21 | 22 | ## Deploying to App Engine 23 | 24 | The following code changes have been made to enable the Snapshot Debugger: 25 | 26 | https://github.com/GoogleCloudPlatform/snapshot-debugger/blob/ae8358fb69ae8f9df10e4675d59671d950e0d6c1/samples/python/appengine-flexible/requirements.txt#L4 27 | 28 | https://github.com/GoogleCloudPlatform/snapshot-debugger/blob/ae8358fb69ae8f9df10e4675d59671d950e0d6c1/samples/python/appengine-flexible/main.py#L17-L21 29 | 30 | Then deploy to App Engine as usual: 31 | 32 | gcloud app deploy 33 | 34 | ## Determine the Debuggee ID 35 | 36 | Based on the service and version you'll be able to identify your debuggee ID 37 | based on the output from the `list_debuggees` command. 38 | 39 | ``` 40 | snapshot-dbg-cli list_debuggees 41 | ``` 42 | 43 | The output will resemble the following. The first column will contain an entry 44 | ` - `, which in this case is `default - 20221122t161333`. 45 | 46 | ``` 47 | Name ID Description Last Active Status 48 | ------------------------- ---------- ----------------------------- -------------------- ------ 49 | default - 20221122t161333 d-ad4829f7 my-project-id-20221122t161333 2022-11-22T16:15:00Z ACTIVE 50 | ``` 51 | 52 | The debuggee ID in this case is `d-ad4829f7`. Using this ID you may now run 53 | through an [Example workflow](../../../README.md#example-workflow). 54 | 55 | E.g. 56 | * Use the `set_snapshot` CLI command to set a snapshot at `main.py:30`. 57 | Note the returned breakpoint ID. 58 | * Navigate to your application using the `target url` shown in the 59 | `gcloud app deploy` output. This will trigger the breakpoint and 60 | collect the snapshot. 61 | * Use the `get_snapshot` CLI command to retrieve the snapshot using the 62 | breakpoint ID created with the `set_snapshot` command. 63 | 64 | [sample-source]: https://github.com/GoogleCloudPlatform/python-docs-samples/tree/main/appengine/flexible/hello_world 65 | -------------------------------------------------------------------------------- /samples/python/appengine-flexible/app.yaml: -------------------------------------------------------------------------------- 1 | # Copyright 2021, Google, Inc. 2 | # Licensed under the Apache License, Version 2.0 (the "License"); 3 | # you may not use this file except in compliance with the License. 4 | # You may obtain a copy of the License at 5 | # 6 | # http://www.apache.org/licenses/LICENSE-2.0 7 | # 8 | # Unless required by applicable law or agreed to in writing, software 9 | # distributed under the License is distributed on an "AS IS" BASIS, 10 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | # See the License for the specific language governing permissions and 12 | # limitations under the License. 13 | 14 | runtime: python 15 | env: flex 16 | entrypoint: gunicorn -b :$PORT main:app 17 | 18 | runtime_config: 19 | python_version: 3 20 | 21 | # This sample incurs costs to run on the App Engine flexible environment. 22 | # The settings below are to reduce costs during testing and are not appropriate 23 | # for production use. For more information, see: 24 | # https://cloud.google.com/appengine/docs/flexible/python/configuring-your-app-with-app-yaml 25 | manual_scaling: 26 | instances: 1 27 | resources: 28 | cpu: 1 29 | memory_gb: 0.5 30 | disk_size_gb: 10 31 | 32 | -------------------------------------------------------------------------------- /samples/python/appengine-flexible/main.py: -------------------------------------------------------------------------------- 1 | # Copyright 2015 Google Inc. All Rights Reserved. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | from flask import Flask 16 | 17 | try: 18 | import googleclouddebugger 19 | googleclouddebugger.enable(use_firebase=True) 20 | except ImportError: 21 | pass 22 | 23 | 24 | app = Flask(__name__) 25 | 26 | 27 | @app.route('/') 28 | def hello(): 29 | """Return a friendly HTTP greeting.""" 30 | return 'Hello World!' 31 | 32 | 33 | if __name__ == '__main__': 34 | # This is used when running locally only. When deploying to Google App 35 | # Engine, a webserver process such as Gunicorn will serve the app. 36 | app.run(host='127.0.0.1', port=8080, debug=True) 37 | -------------------------------------------------------------------------------- /samples/python/appengine-flexible/requirements.txt: -------------------------------------------------------------------------------- 1 | Flask==2.2.5; python_version > '3.6' 2 | Flask==2.0.3; python_version < '3.7' 3 | gunicorn==20.1.0 4 | google-python-cloud-debugger 5 | -------------------------------------------------------------------------------- /samples/python/appengine-standard/README.md: -------------------------------------------------------------------------------- 1 | # Snapshot Debugger Examples for Python in the App Engine standard environment 2 | 3 | NOTE: This sample application was copied from 4 | [python-docs-samples/appengine/hello-world/standard][sample-source] 5 | and modified for the Snapshot Debugger samples here. 6 | 7 | 8 | This is the hello-world application for the 9 | [Google App Engine Python standard environment](https://cloud.google.com/appengine/docs/standard/python3). 10 | More complete documentation about getting started in App Engine can be found in 11 | [this tutorial](https://cloud.google.com/appengine/docs/standard/python3/building-app). 12 | 13 | * [Setup](#setup) 14 | * [Deploying to App Engine](#deploying-to-app-engine) 15 | 16 | ## Setup 17 | 18 | Before you can run or deploy the sample, you need to do the following: 19 | 20 | 1. Perform all [Prerequisite Steps](../../app_engine_standard_prerequisites.md) 21 | 1. Refer to the [getting started documentation][create-cloud-project] 22 | for instructions on setting up your Cloud project. 23 | 24 | ## Deploying to App Engine 25 | 26 | The following code changes have been made to enable the Snapshot Debugger: 27 | 28 | https://github.com/GoogleCloudPlatform/snapshot-debugger/blob/33f8756c5adb7684650adbe0d81ebbc6f5051c4c/samples/python/appengine-standard/requirements.txt#L2 29 | 30 | https://github.com/GoogleCloudPlatform/snapshot-debugger/blob/33f8756c5adb7684650adbe0d81ebbc6f5051c4c/samples/python/appengine-standard/main.py#L17-L21 31 | 32 | Then deploy to App Engine as usual: 33 | 34 | gcloud app deploy 35 | 36 | 37 | ## Navigate To Your App 38 | 39 | This will ensure the app is run and is required as your app will not be 40 | debuggable until after the first request has been received. The URL should be 41 | provided in the `target url` output of the previous step. 42 | 43 | ## Determine the Debuggee ID 44 | 45 | Based on the service and version you'll be able to identify your debuggee ID 46 | based on the output from the `list_debuggees` command. 47 | 48 | ``` 49 | snapshot-dbg-cli list_debuggees 50 | ``` 51 | 52 | The output will resemble the following. The first column will contain an entry 53 | ` - `, which in this case is `default - 20221122t161333`. 54 | 55 | ``` 56 | Name ID Description Last Active Status 57 | ------------------------- ---------- ----------------------------- -------------------- ------ 58 | default - 20221122t161333 d-ad4829f7 my-project-id-20221122t161333 2022-11-22T16:15:00Z ACTIVE 59 | ``` 60 | 61 | The debuggee ID in this case is `d-ad4829f7`. Using this ID you may now run 62 | through an [Example workflow](../../../README.md#example-workflow). 63 | 64 | E.g. 65 | * Use the `set_snapshot` CLI command to set a snapshot at `main.py:32`. 66 | Note the returned breakpoint ID. 67 | * Navigate to your application using the `target url` shown in the 68 | `gcloud app deploy` output. This will trigger the breakpoint and 69 | collect the snapshot. 70 | * Use the `get_snapshot` CLI command to retrieve the snapshot using the 71 | breakpoint ID created with the `set_snapshot` command. 72 | 73 | [sample-source]: https://github.com/GoogleCloudPlatform/python-docs-samples/tree/main/appengine/standard_python3/hello_world 74 | [create-cloud-project]: https://cloud.google.com/appengine/docs/standard/python3/building-app/creating-gcp-project 75 | -------------------------------------------------------------------------------- /samples/python/appengine-standard/app.yaml: -------------------------------------------------------------------------------- 1 | # Copyright 2017, Google, Inc. 2 | # Licensed under the Apache License, Version 2.0 (the "License"); 3 | # you may not use this file except in compliance with the License. 4 | # You may obtain a copy of the License at 5 | # 6 | # http://www.apache.org/licenses/LICENSE-2.0 7 | # 8 | # Unless required by applicable law or agreed to in writing, software 9 | # distributed under the License is distributed on an "AS IS" BASIS, 10 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | # See the License for the specific language governing permissions and 12 | # limitations under the License. 13 | 14 | runtime: python39 15 | -------------------------------------------------------------------------------- /samples/python/appengine-standard/main.py: -------------------------------------------------------------------------------- 1 | # Copyright 2018 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | from flask import Flask 16 | 17 | try: 18 | import googleclouddebugger 19 | googleclouddebugger.enable(use_firebase=True) 20 | except ImportError: 21 | pass 22 | 23 | 24 | # If `entrypoint` is not defined in app.yaml, App Engine will look for an app 25 | # called `app` in `main.py`. 26 | app = Flask(__name__) 27 | 28 | 29 | @app.route('/') 30 | def hello(): 31 | """Return a friendly HTTP greeting.""" 32 | return 'Hello World!' 33 | 34 | 35 | if __name__ == '__main__': 36 | # This is used when running locally only. When deploying to Google App 37 | # Engine, a webserver process such as Gunicorn will serve the app. You 38 | # can configure startup instructions by adding `entrypoint` to app.yaml. 39 | app.run(host='127.0.0.1', port=8080, debug=True) 40 | -------------------------------------------------------------------------------- /samples/python/appengine-standard/requirements.txt: -------------------------------------------------------------------------------- 1 | Flask==2.2.5 2 | google-python-cloud-debugger 3 | -------------------------------------------------------------------------------- /samples/python/gce/deploy.sh: -------------------------------------------------------------------------------- 1 | # Copyright 2019 Google LLC All Rights Reserved. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | set -ex 16 | 17 | MY_INSTANCE_NAME="my-app-instance" 18 | ZONE=us-central1-a 19 | 20 | gcloud compute instances create $MY_INSTANCE_NAME \ 21 | --image-family=debian-10 \ 22 | --image-project=debian-cloud \ 23 | --machine-type=g1-small \ 24 | --scopes userinfo-email,cloud-platform,https://www.googleapis.com/auth/firebase.database \ 25 | --metadata-from-file startup-script=startup-script.sh \ 26 | --zone $ZONE \ 27 | --tags http-server 28 | 29 | gcloud compute firewall-rules create default-allow-http-8080 \ 30 | --allow tcp:8080 \ 31 | --source-ranges 0.0.0.0/0 \ 32 | --target-tags http-server \ 33 | --description "Allow port 8080 access to http-server" 34 | -------------------------------------------------------------------------------- /samples/python/gce/main.py: -------------------------------------------------------------------------------- 1 | # Copyright 2019 Google LLC All Rights Reserved. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | from flask import Flask 16 | app = Flask(__name__) 17 | 18 | try: 19 | import googleclouddebugger 20 | googleclouddebugger.enable( 21 | use_firebase=True, 22 | module='gce-python-sample', 23 | version='v1') 24 | except ImportError: 25 | pass 26 | 27 | 28 | @app.route('/', methods=['GET']) 29 | def say_hello(): 30 | return "Hello, world!" 31 | 32 | 33 | if __name__ == '__main__': 34 | app.run(host='127.0.0.1', port=8080, debug=True) 35 | -------------------------------------------------------------------------------- /samples/python/gce/procfile: -------------------------------------------------------------------------------- 1 | hello: /opt/app/gce/env/bin/gunicorn -b 0.0.0.0:8080 main:app 2 | -------------------------------------------------------------------------------- /samples/python/gce/python-app.conf: -------------------------------------------------------------------------------- 1 | [program:pythonapp] 2 | directory=/opt/app/gce 3 | command=/opt/app/gce/env/bin/honcho start -f ./procfile hello 4 | autostart=true 5 | autorestart=true 6 | user=pythonapp 7 | # Environment variables ensure that the application runs inside of the 8 | # configured virtualenv. 9 | environment=VIRTUAL_ENV="/opt/app/gce/env",PATH="/opt/app/gce/env/bin",HOME="/home/pythonapp",USER="pythonapp" 10 | stdout_logfile=syslog 11 | stderr_logfile=syslog 12 | -------------------------------------------------------------------------------- /samples/python/gce/requirements.txt: -------------------------------------------------------------------------------- 1 | flask==2.2.5 2 | honcho==1.1.0 3 | gunicorn==20.1.0 4 | google-python-cloud-debugger 5 | -------------------------------------------------------------------------------- /samples/python/gce/startup-script.sh: -------------------------------------------------------------------------------- 1 | # Copyright 2019 Google LLC All Rights Reserved. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | # Echo commands and fail on error 16 | set -ev 17 | 18 | # Install or update needed software 19 | apt-get update 20 | apt-get install -yq git supervisor python python-pip python3-distutils 21 | pip install --upgrade pip virtualenv 22 | 23 | # Fetch source code 24 | export HOME=/root 25 | git clone https://github.com/GoogleCloudPlatform/snapshot-debugger.git /tmp/app 26 | mkdir /opt/app 27 | cp -r /tmp/app/samples/python/gce /opt/app 28 | 29 | # Install Cloud Ops Agent 30 | sudo bash /opt/app/gce/add-google-cloud-ops-agent-repo.sh --also-install 31 | 32 | # Account to own server process 33 | useradd -m -d /home/pythonapp pythonapp 34 | 35 | # Python environment setup 36 | virtualenv -p python3 /opt/app/gce/env 37 | /bin/bash -c "source /opt/app/gce/env/bin/activate" 38 | /opt/app/gce/env/bin/pip install -r /opt/app/gce/requirements.txt 39 | 40 | # Set ownership to newly created account 41 | chown -R pythonapp:pythonapp /opt/app 42 | 43 | # Put supervisor configuration in proper place 44 | cp /opt/app/gce/python-app.conf /etc/supervisor/conf.d/python-app.conf 45 | 46 | # Start service via supervisorctl 47 | supervisorctl reread 48 | supervisorctl update 49 | -------------------------------------------------------------------------------- /samples/python/gce/teardown.sh: -------------------------------------------------------------------------------- 1 | #! /bin/bash 2 | 3 | # Copyright 2019 Google LLC All Rights Reserved. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | set -x 18 | 19 | MY_INSTANCE_NAME="my-app-instance" 20 | ZONE=us-central1-a 21 | 22 | gcloud compute instances delete $MY_INSTANCE_NAME \ 23 | --zone=$ZONE --delete-disks=all 24 | 25 | gcloud compute firewall-rules delete default-allow-http-8080 26 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | name= snapshot-dbg-cli 3 | version = attr: snapshot_dbg_cli.__version__ 4 | description='Snapshot Debugger CLI tool.' 5 | long_description = file: README.md 6 | long_description_content_type = text/markdown 7 | license = Apache License, Version 2.0 8 | author = Google Inc. 9 | keywords = cloud, snapshot, debugger 10 | url = https://github.com/GoogleCloudPlatform/snapshot-debugger 11 | python_requires= >=3.6 12 | project_urls = 13 | CLI Source = https://github.com/GoogleCloudPlatform/snapshot-debugger 14 | Java Agent Source = https://github.com/GoogleCloudPlatform/cloud-debug-java 15 | Node.js Agent Source = https://github.com/googleapis/cloud-debug-nodejs 16 | Python Agent Source = https://github.com/GoogleCloudPlatform/cloud-debug-python 17 | 18 | classifiers = 19 | Development Status :: 4 - Beta 20 | Intended Audience :: Developers 21 | Programming Language :: Python 22 | Programming Language :: Python :: 3 23 | Programming Language :: Python :: 3.6 24 | Programming Language :: Python :: 3.7 25 | Programming Language :: Python :: 3.8 26 | Programming Language :: Python :: 3.9 27 | Programming Language :: Python :: 3.10 28 | Topic :: Software Development :: Debuggers 29 | 30 | [options] 31 | packages = snapshot_dbg_cli 32 | 33 | [options.entry_points] 34 | console_scripts = 35 | snapshot-dbg-cli = snapshot_dbg_cli:run_main 36 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # Copyright 2022 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | """Minimal setup.py file for packaging snapshot-dbg-cli. 15 | 16 | All packaging configuriation can be found in setup.cfg 17 | """ 18 | 19 | from setuptools import setup 20 | 21 | if __name__ == '__main__': 22 | setup() 23 | -------------------------------------------------------------------------------- /snapshot_dbg_cli/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright 2022 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | """ Snapshot Debugger CLI. 15 | 16 | This package provides the CLI of the Snapshot Debugger. 17 | """ 18 | 19 | import sys 20 | 21 | import snapshot_dbg_cli.cli_run 22 | import snapshot_dbg_cli.cli_version 23 | from snapshot_dbg_cli.exceptions import SilentlyExitError 24 | 25 | __version__ = snapshot_dbg_cli.cli_version.running_version() 26 | 27 | 28 | def main(): 29 | snapshot_dbg_cli.cli_version.check_for_newer_version() 30 | snapshot_dbg_cli.cli_run.run() 31 | 32 | 33 | def run_main(): 34 | try: 35 | main() 36 | except SilentlyExitError: 37 | sys.exit(1) 38 | 39 | sys.exit(0) 40 | 41 | 42 | if __name__ == '__main__': 43 | run_main() 44 | -------------------------------------------------------------------------------- /snapshot_dbg_cli/__main__.py: -------------------------------------------------------------------------------- 1 | # Copyright 2022 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | """Main entry point for the cli package.""" 15 | # pylint: disable=invalid-name 16 | import snapshot_dbg_cli 17 | 18 | snapshot_dbg_cli.run_main() 19 | -------------------------------------------------------------------------------- /snapshot_dbg_cli/cli.py: -------------------------------------------------------------------------------- 1 | # Copyright 2022 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | """Old CLI startup code, no longer in use. 15 | 16 | This file is no longer in use but is being kept to help any users attempting to 17 | run the cli via the old mechanism learn how to run it the new way. 18 | """ 19 | import sys 20 | 21 | if __name__ == '__main__': 22 | print("Please run 'python3 -m snapshot_dbg_cli ...' from the root of the " 23 | 'repository.') 24 | sys.exit(1) 25 | -------------------------------------------------------------------------------- /snapshot_dbg_cli/cli_run.py: -------------------------------------------------------------------------------- 1 | # Copyright 2022 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | """ The main Snapshot Debugger CLI startup code. 15 | 16 | This module provides the run() function, which sets up the CLI commands, parses 17 | the command line arguments and runs the specified command. 18 | """ 19 | 20 | import argparse 21 | import sys 22 | 23 | from snapshot_dbg_cli.cli_services import CliServices 24 | from snapshot_dbg_cli import cli_common_arguments 25 | from snapshot_dbg_cli.exceptions import SilentlyExitError 26 | from snapshot_dbg_cli.delete_debuggees_command import DeleteDebuggeesCommand 27 | from snapshot_dbg_cli.delete_logpoints_command import DeleteLogpointsCommand 28 | from snapshot_dbg_cli.delete_snapshots_command import DeleteSnapshotsCommand 29 | from snapshot_dbg_cli.get_logpoint_command import GetLogpointCommand 30 | from snapshot_dbg_cli.get_snapshot_command import GetSnapshotCommand 31 | from snapshot_dbg_cli.init_command import InitCommand 32 | from snapshot_dbg_cli.list_debuggees_command import ListDebuggeesCommand 33 | from snapshot_dbg_cli.list_logpoints_command import ListLogpointsCommand 34 | from snapshot_dbg_cli.list_snapshots_command import ListSnapshotsCommand 35 | from snapshot_dbg_cli.set_logpoint_command import SetLogpointCommand 36 | from snapshot_dbg_cli.set_snapshot_command import SetSnapshotCommand 37 | 38 | 39 | def run(cli_services=None): 40 | cli_commands = [ 41 | DeleteDebuggeesCommand(), 42 | DeleteLogpointsCommand(), 43 | DeleteSnapshotsCommand(), 44 | GetLogpointCommand(), 45 | GetSnapshotCommand(), 46 | InitCommand(), 47 | ListLogpointsCommand(), 48 | ListSnapshotsCommand(), 49 | ListDebuggeesCommand(), 50 | SetLogpointCommand(), 51 | SetSnapshotCommand() 52 | ] 53 | 54 | args_parser = argparse.ArgumentParser() 55 | common_parsers = cli_common_arguments.CommonArgumentParsers() 56 | required_parsers = cli_common_arguments.RequiredArgumentParsers().parsers 57 | 58 | args_subparsers = args_parser.add_subparsers() 59 | 60 | for cmd in cli_commands: 61 | cmd.register( 62 | args_subparsers, 63 | required_parsers=required_parsers, 64 | common_parsers=common_parsers) 65 | 66 | args = args_parser.parse_args() 67 | 68 | if 'func' not in args: 69 | print( 70 | 'Missing required argument, please specify --help for more information', 71 | file=sys.stderr) 72 | 73 | raise SilentlyExitError 74 | 75 | if cli_services is None: 76 | cli_services = CliServices(args) 77 | 78 | # This will run the appropriate command. 79 | args.func(args=args, cli_services=cli_services) 80 | -------------------------------------------------------------------------------- /snapshot_dbg_cli/cli_version.py: -------------------------------------------------------------------------------- 1 | # Copyright 2022 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | """This module provides the current version of the package. 15 | """ 16 | 17 | import re 18 | import sys 19 | 20 | from snapshot_dbg_cli.data_formatter import DataFormatter 21 | from snapshot_dbg_cli.exceptions import SilentlyExitError 22 | from snapshot_dbg_cli.http_service import HttpService 23 | from snapshot_dbg_cli.user_output import UserOutput 24 | 25 | VERSION = 'SNAPSHOT_DEBUGGER_CLI_VERSION_0_3_8' 26 | 27 | VERSION_PATTERN = 'SNAPSHOT_DEBUGGER_CLI_VERSION_[0-9]+_[0-9]+_[0-9]+' 28 | 29 | VERSION_URL = ('https://raw.githubusercontent.com/GoogleCloudPlatform' 30 | '/snapshot-debugger/main/snapshot_dbg_cli/cli_version.py') 31 | 32 | NEWER_VERSION_MESSAGE = """ 33 | A newer version of the CLI is available ({latest} vs {running}). To install it please run: 34 | $ pip install --upgrade snapshot-dbg-cli 35 | """ 36 | 37 | 38 | class SuppressedUserOutput(UserOutput): 39 | """A UserOutput subclass that prevents any user output from being emitted. 40 | """ 41 | 42 | def __init__(self): 43 | # Just need to intialize it, the local_print override is the important bit 44 | # that will ensure no data is written to stdout/stderr. 45 | super().__init__(is_debug_enabled=False, data_formatter=DataFormatter()) 46 | 47 | def local_print(self, *args, **kwargs): 48 | """Override the local_print of UserOutput to suppress all output. 49 | """ 50 | pass 51 | 52 | 53 | def extract_version_number(version_string): 54 | # Expressly not using str.removeprefix as that was added in Python 3.9 55 | # and we want to support older versions. 56 | prefix = 'SNAPSHOT_DEBUGGER_CLI_VERSION_' 57 | if version_string.startswith(prefix): 58 | version_string = version_string[len(prefix):] 59 | 60 | return version_string.replace('_', '.') 61 | 62 | 63 | def running_version(): 64 | return extract_version_number(VERSION) 65 | 66 | 67 | def latest_version(): 68 | # Send in SuppressedUserOutput, we don't want any error messages emitted on a 69 | # failure to get the version file, as it's not critical for it to succeed. 70 | http_service = HttpService( 71 | project_id=None, access_token=None, user_output=SuppressedUserOutput()) 72 | version = None 73 | 74 | try: 75 | # Make a quick call with a low timeout, and don't care if it fails. If we 76 | # can't get the latest version it's not critical, we simply can't warn the 77 | # user if they happen to be running an older version. 78 | content = http_service.send_request( 79 | 'GET', VERSION_URL, max_retries=0, timeout_sec=5) 80 | match = re.search(VERSION_PATTERN, content) 81 | if match is not None: 82 | version = extract_version_number(match.group(0)) 83 | except SilentlyExitError: 84 | # Simply swallow the error, it is not critical to retrieve the latest, we'll 85 | # simply fall through and None will be returned in this case. 86 | pass 87 | 88 | return version 89 | 90 | 91 | def check_for_newer_version(): 92 | 93 | def version(version_string): 94 | return tuple(map(int, version_string.split('.'))) 95 | 96 | running_version_string = running_version() 97 | latest_version_string = latest_version() 98 | 99 | if latest_version_string is None: 100 | return 101 | 102 | if version(latest_version_string) > version(running_version_string): 103 | print( 104 | NEWER_VERSION_MESSAGE.format( 105 | latest=latest_version_string, running=running_version_string), 106 | file=sys.stderr) 107 | -------------------------------------------------------------------------------- /snapshot_dbg_cli/data_formatter.py: -------------------------------------------------------------------------------- 1 | # Copyright 2022 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | """This module provides utilities for formatting text. 15 | 16 | The DataFormatter class provides utility functions such as formatting tabular 17 | data and json data. 18 | """ 19 | 20 | import json 21 | 22 | 23 | class DataFormatter: 24 | """Provides utilities for formatting text. 25 | 26 | The DataFormatter class provides utility functions such as formatting tabular 27 | data and json data. 28 | """ 29 | 30 | def build_table(self, headers, values): 31 | """Builds a human friendly string of the data in table form. 32 | 33 | Args: 34 | headers: Array of strings to be used as the top row of the table, 35 | representing the column names. 36 | values: Array of tuples containing one row of output (1 value for each 37 | column). Each tuple must have the same number of elements, which much 38 | also match the length of the headers tuple. 39 | """ 40 | widths = [ 41 | max(len(headers[i]), max((len(v[i]) 42 | for v in values), default=0)) 43 | for i in range(len(headers)) 44 | ] 45 | 46 | separator_row = list(map(lambda w: '-' * w, widths)) 47 | 48 | rows = [] 49 | rows.append(headers) 50 | rows.append(separator_row) 51 | rows += values 52 | 53 | field_buffer = ' ' 54 | table = '' 55 | 56 | for i in range(len(rows)): 57 | fields = [f'{rows[i][j]:{widths[j]}}' for j in range(len(rows[i]))] 58 | table += field_buffer.join(fields) 59 | table += '\n' 60 | 61 | return table 62 | 63 | def to_json_string(self, data, pretty): 64 | """Transforms the data to a JSON string representation. 65 | 66 | Args: 67 | data: The data to convert to a JSON string. It is expected to be a Python 68 | object representation of the data. 69 | pretty: Boolean flag that when True will be cause the json string to be 70 | human readable, otherwise it will be in a compact representation. 71 | """ 72 | if pretty: 73 | return json.dumps(data, indent=2) 74 | else: 75 | return json.dumps(data) 76 | -------------------------------------------------------------------------------- /snapshot_dbg_cli/delete_logpoints_command.py: -------------------------------------------------------------------------------- 1 | # Copyright 2022 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | """This module contains the support for the delete_logpoints command. 15 | 16 | The delete_logpoints command is used to delete logpoints from a debug target 17 | (debuggee). 18 | """ 19 | 20 | from snapshot_dbg_cli import breakpoint_utils 21 | from snapshot_dbg_cli import delete_breakpoints 22 | 23 | DESCRIPTION = """ 24 | Used to delete logpoints from a debug target (debuggee). You are prompted for 25 | confirmation before any logpoints are deleted. To suppress confirmation, use the 26 | --quiet option. 27 | """ 28 | 29 | ID_HELP = """ 30 | Zero or more logpoint IDs. The specified logpoints will be deleted. By default, 31 | if no logpoint IDs are specified, all active logpoints created by the user are 32 | selected for deletion. 33 | """ 34 | 35 | ALL_USERS_HELP = """ 36 | If set, logpoints from all users will be deleted, rather than only logpoints 37 | created by the current user. This flag is not required when specifying the exact 38 | ID of a logpoint. 39 | """ 40 | 41 | INCLUDE_INACTIVE_HELP = """ 42 | If set, also delete logpoints which have been completed. By default, only 43 | pending logpoints will be deleted. This flag is not required when specifying the 44 | exact ID of an inactive logpoint. 45 | """ 46 | 47 | QUIET_HELP = 'If set, suppresses user confirmation of the command.' 48 | 49 | SUMMARY_HEADERS = [ 50 | 'Location', 'Condition', 'Log Level', 'Log Message Format', 'ID' 51 | ] 52 | 53 | 54 | def transform_to_logpoint_summary(logpoint): 55 | # Match the fields from SUMMARY_HEADERS 56 | return [ 57 | breakpoint_utils.transform_location_to_file_line(logpoint['location']), 58 | logpoint['condition'] if 'condition' in logpoint else '', 59 | logpoint['logLevel'], 60 | logpoint['logMessageFormatString'], 61 | logpoint['id'], 62 | ] 63 | 64 | 65 | class DeleteLogpointsCommand: 66 | """This class implements the delete_logpoints command. 67 | 68 | The register() method is called by the CLI startup code to install the 69 | delete_logpoints command information, and the cmd() function will be invoked 70 | if the delete_logpoints command was specified by the user. 71 | """ 72 | 73 | def __init__(self): 74 | pass 75 | 76 | def register(self, args_subparsers, required_parsers, common_parsers): 77 | parent_parsers = [ 78 | common_parsers.database_url, common_parsers.format, 79 | common_parsers.debuggee_id 80 | ] 81 | parent_parsers += required_parsers 82 | parser = args_subparsers.add_parser( 83 | 'delete_logpoints', description=DESCRIPTION, parents=parent_parsers) 84 | parser.add_argument('ID', help=ID_HELP, nargs='*') 85 | parser.add_argument('--all-users', help=ALL_USERS_HELP, action='store_true') 86 | parser.add_argument( 87 | '--include-inactive', help=INCLUDE_INACTIVE_HELP, action='store_true') 88 | parser.add_argument('--quiet', help=QUIET_HELP, action='store_true') 89 | parser.set_defaults(func=self.cmd) 90 | 91 | def cmd(self, args, cli_services): 92 | delete_breakpoints.run_cmd('LOG', args, cli_services, SUMMARY_HEADERS, 93 | transform_to_logpoint_summary) 94 | -------------------------------------------------------------------------------- /snapshot_dbg_cli/delete_snapshots_command.py: -------------------------------------------------------------------------------- 1 | # Copyright 2022 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | """This module contains the support for the delete_snapshots command. 15 | 16 | The delete_snapshots command is used to delete snapshots from a debug target 17 | (debuggee). 18 | """ 19 | 20 | from snapshot_dbg_cli import breakpoint_utils 21 | from snapshot_dbg_cli import delete_breakpoints 22 | 23 | DESCRIPTION = """ 24 | Used to delete snapshots from a debug target (debuggee). You are prompted for 25 | confirmation before any snapshots are deleted. To suppress confirmation, use the 26 | --quiet option. 27 | """ 28 | 29 | ID_HELP = """ 30 | Zero or more snapshot IDs. The specified snapshots will be deleted. By default, 31 | if no snapshot IDs are specified, all active snapshots created by the user are 32 | selected for deletion. 33 | """ 34 | 35 | ALL_USERS_HELP = """ 36 | If set, snapshots from all users will be deleted, rather than only snapshots 37 | created by the current user. This flag is not required when specifying the exact 38 | ID of a snapshot. 39 | """ 40 | 41 | INCLUDE_INACTIVE_HELP = """ 42 | If set, also delete snapshots which have been completed. By default, only 43 | pending snapshots will be deleted. This flag is not required when specifying the 44 | exact ID of an inactive snapshot. 45 | """ 46 | 47 | QUIET_HELP = 'If set, suppresses user confirmation of the command.' 48 | 49 | SUMMARY_HEADERS = ['Status', 'Location', 'Condition', 'ID'] 50 | 51 | 52 | def transform_to_snapshot_summary(snapshot): 53 | # Match the fields from SUMMARY_HEADERS 54 | return [ 55 | 'COMPLETED' if snapshot['isFinalState'] else 'ACTIVE', 56 | breakpoint_utils.transform_location_to_file_line(snapshot['location']), 57 | snapshot['condition'] if 'condition' in snapshot else '', snapshot['id'] 58 | ] 59 | 60 | 61 | class DeleteSnapshotsCommand: 62 | """This class implements the delete_snapshots command. 63 | 64 | The register() method is called by the CLI startup code to install the 65 | delete_snapshots command information, and the cmd() function will be invoked 66 | if the delete_snapshots command was specified by the user. 67 | """ 68 | 69 | def __init__(self): 70 | pass 71 | 72 | def register(self, args_subparsers, required_parsers, common_parsers): 73 | parent_parsers = [ 74 | common_parsers.database_url, common_parsers.format, 75 | common_parsers.debuggee_id 76 | ] 77 | parent_parsers += required_parsers 78 | parser = args_subparsers.add_parser( 79 | 'delete_snapshots', description=DESCRIPTION, parents=parent_parsers) 80 | parser.add_argument('ID', help=ID_HELP, nargs='*') 81 | parser.add_argument('--all-users', help=ALL_USERS_HELP, action='store_true') 82 | parser.add_argument( 83 | '--include-inactive', help=INCLUDE_INACTIVE_HELP, action='store_true') 84 | parser.add_argument('--quiet', help=QUIET_HELP, action='store_true') 85 | parser.set_defaults(func=self.cmd) 86 | 87 | def cmd(self, args, cli_services): 88 | delete_breakpoints.run_cmd('CAPTURE', args, cli_services, SUMMARY_HEADERS, 89 | transform_to_snapshot_summary) 90 | -------------------------------------------------------------------------------- /snapshot_dbg_cli/exceptions.py: -------------------------------------------------------------------------------- 1 | # Copyright 2022 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | """Module to hold custom exceptions for the CLI. 15 | """ 16 | 17 | 18 | class SilentlyExitError(Exception): 19 | """Exception to cause the CLI to silently exit with an error. 20 | 21 | Should be thrown when an error occured and the code needs to exit. It's 22 | expected any user error messages have already been emitted. The top level 23 | CLI code will catch this and exit with an error without any further console 24 | output. 25 | """ 26 | pass 27 | -------------------------------------------------------------------------------- /snapshot_dbg_cli/firebase_rtdb_rest_service.py: -------------------------------------------------------------------------------- 1 | # Copyright 2022 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | """Service for making Firebase RTDB REST requests. 15 | 16 | This service is for making read/write requests to a Firebase RTDB instance 17 | using the REST interface, which is documented at 18 | https://firebase.google.com/docs/reference/rest/database. 19 | """ 20 | 21 | FULL_REQUEST_URL = "{database_url}/{db_path}.json" 22 | 23 | 24 | class FirebaseRtdbRestService: 25 | """This class implements a service for making Firebase RTDB REST requests. 26 | 27 | This service is for making read/write requests to a Firebase RTDB instance 28 | using the REST interface which is documented at 29 | https://firebase.google.com/docs/reference/rest/database. 30 | """ 31 | 32 | def __init__(self, http_service, database_url, user_output): 33 | self._http_service = http_service 34 | self._database_url = database_url 35 | self._user_output = user_output 36 | 37 | def get(self, db_path, shallow=None, extra_retry_codes=None): 38 | """Gets the value at the specified path. 39 | 40 | Args: 41 | db_path: The database path to retrieve the value on. 42 | shallow: If specified will just get the values at the top level of the 43 | node, nothing further down. 44 | extra_retry_codes: A list of extra HTTP error codes that will be retried 45 | if the request fails in addition to the default error codes that are 46 | retried. 47 | 48 | Returns: 49 | The value at the specified path if it exists, None otherwise. 50 | """ 51 | url = self.build_rtdb_url(db_path) 52 | parameters = ["shallow=true"] if shallow else [] 53 | return self._http_service.send_request( 54 | "GET", url, extra_retry_codes=extra_retry_codes, parameters=parameters) 55 | 56 | def set(self, db_path, data): 57 | url = self.build_rtdb_url(db_path) 58 | return self._http_service.send_request("PUT", url, data=data, max_retries=0) 59 | 60 | def delete(self, db_path): 61 | url = self.build_rtdb_url(db_path) 62 | 63 | # From 64 | # https://firebase.google.com/docs/reference/rest/database#section-delete 65 | # 66 | # A successful DELETE request is indicated by a 200 OK HTTP status code with 67 | # a response containing JSON null. 68 | # Based on experimentation, even if the node didn't exist, this will be the 69 | # response. It seems the response is success if after the call the specified 70 | # path does not exist, as long as the database the call referenced did 71 | # exist. 72 | return self._http_service.send_request("DELETE", url, max_retries=5) 73 | 74 | def build_rtdb_url(self, db_path): 75 | return FULL_REQUEST_URL.format( 76 | database_url=self._database_url, db_path=db_path) 77 | -------------------------------------------------------------------------------- /snapshot_dbg_cli/get_logpoint_command.py: -------------------------------------------------------------------------------- 1 | # Copyright 2022 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | """This module contains the support for the get_logpoint command. 15 | 16 | The get_logpoint command is used to create a logpoint on a debug target 17 | (Debuggee). 18 | """ 19 | 20 | from snapshot_dbg_cli.exceptions import SilentlyExitError 21 | from snapshot_dbg_cli import breakpoint_utils 22 | 23 | DESCRIPTION = """ 24 | Used to retrieve a debug logpoint from a debug target (debuggee). 25 | """ 26 | 27 | ID_HELP = 'Specify the logpoint ID to retrieve.' 28 | 29 | 30 | class GetLogpointCommand: 31 | """This class implements the get_logpoint command. 32 | 33 | The register() method is called by the CLI startup code to install the 34 | get_logpoint command information, and the cmd() function will be invoked if 35 | the get_logpoint command was specified by the user. 36 | """ 37 | 38 | def __init__(self): 39 | pass 40 | 41 | def register(self, args_subparsers, required_parsers, common_parsers): 42 | parent_parsers = [ 43 | common_parsers.database_url, common_parsers.format, 44 | common_parsers.debuggee_id 45 | ] 46 | parent_parsers += required_parsers 47 | parser = args_subparsers.add_parser( 48 | 'get_logpoint', description=DESCRIPTION, parents=parent_parsers) 49 | parser.add_argument('logpoint_id', metavar='ID', help=ID_HELP) 50 | parser.set_defaults(func=self.cmd) 51 | 52 | def display_summary(self, bp): 53 | # To note we're being defensive here with respect to the condition field the 54 | # that its absence is treated to the same way as it being present but empty. 55 | logpoint_id = bp['id'] 56 | location = breakpoint_utils.transform_location_to_file_line(bp['location']) 57 | condition = bp.get('condition', '') 58 | condition = condition if condition != '' else 'No condition set' 59 | status = breakpoint_utils.get_logpoint_short_status(bp) 60 | log_message_format = bp['logMessageFormatString'] 61 | create_time = bp['createTime'] 62 | final_time = bp.get('finalTime', '') 63 | user_email = bp.get('userEmail', '') 64 | 65 | self.user_output.normal(f'Logpoint ID: {logpoint_id}') 66 | self.user_output.normal(f'Log Message Format: {log_message_format}') 67 | self.user_output.normal(f'Location: {location}') 68 | self.user_output.normal(f'Condition: {condition}') 69 | self.user_output.normal(f'Status: {status}') 70 | self.user_output.normal(f'Create Time: {create_time}') 71 | self.user_output.normal(f'Final Time: {final_time}') 72 | self.user_output.normal(f'User Email: {user_email}') 73 | 74 | def cmd(self, args, cli_services): 75 | self.data_formatter = cli_services.data_formatter 76 | self.user_output = cli_services.user_output 77 | debugger_rtdb_service = cli_services.get_snapshot_debugger_rtdb_service() 78 | 79 | debugger_rtdb_service.validate_debuggee_id(args.debuggee_id) 80 | logpoint = debugger_rtdb_service.get_breakpoint(args.debuggee_id, 81 | args.logpoint_id) 82 | 83 | if logpoint is None or logpoint['action'] != 'LOG': 84 | self.user_output.error(f'Logpoint ID not found: {args.logpoint_id}') 85 | raise SilentlyExitError 86 | 87 | if args.format.is_a_json_value(): 88 | self.user_output.json_format( 89 | logpoint, pretty=args.format.is_pretty_json()) 90 | return 91 | 92 | self.display_summary(logpoint) 93 | -------------------------------------------------------------------------------- /snapshot_dbg_cli/list_debuggees_command.py: -------------------------------------------------------------------------------- 1 | # Copyright 2022 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | """This module contains the support for the list_debuggees command. 15 | 16 | The list_debugees command is used to display a list of the debug targets 17 | (debuggees) registered with the Snapshot Debugger. 18 | """ 19 | 20 | from snapshot_dbg_cli.debuggee_utils import get_debuggee_status 21 | from snapshot_dbg_cli.debuggee_utils import sort_debuggees 22 | from snapshot_dbg_cli.time_utils import get_current_time_unix_msec 23 | 24 | DESCRIPTION = """ 25 | Used to display a list of the debug targets (debuggees) registered with the 26 | Snapshot Debugger. By default all active debuggees are returned. To also obtain 27 | inactive debuggees specify the --include-inactive option. A debuggee is 28 | considered to be active if it currently running or last ran in the past 5-6 29 | hours. 30 | """ 31 | 32 | INCLUDE_INACTIVE_HELP = 'Include inactive debuggees.' 33 | 34 | SUMMARY_HEADERS = headers = [ 35 | 'Name', 'ID', 'Description', 'Last Active', 'Status' 36 | ] 37 | 38 | 39 | def transform_to_debuggee_summary(debuggee): 40 | # Match the fields from SUMMARY_HEADERS 41 | return [ 42 | debuggee['displayName'], 43 | debuggee['id'], 44 | debuggee['description'], 45 | debuggee['lastUpdateTime'], 46 | get_debuggee_status(debuggee), 47 | ] 48 | 49 | 50 | class ListDebuggeesCommand: 51 | """This class implements the list_debuggees command. 52 | 53 | The register() method is called by the CLI startup code to install the 54 | list_debuggees command information, and the cmd() function will be invoked if 55 | the list_debuggees command was specified by the user. 56 | """ 57 | 58 | def __init__(self): 59 | pass 60 | 61 | def register(self, args_subparsers, required_parsers, common_parsers): 62 | parent_parsers = [common_parsers.database_url, common_parsers.format] 63 | parent_parsers += required_parsers 64 | parser = args_subparsers.add_parser( 65 | 'list_debuggees', description=DESCRIPTION, parents=parent_parsers) 66 | parser.add_argument( 67 | '--include-inactive', help=INCLUDE_INACTIVE_HELP, action='store_true') 68 | parser.set_defaults(func=self.cmd) 69 | 70 | def cmd(self, args, cli_services): 71 | user_output = cli_services.user_output 72 | 73 | current_time_unix_msec = get_current_time_unix_msec() 74 | debugger_rtdb_service = cli_services.get_snapshot_debugger_rtdb_service() 75 | debuggees = debugger_rtdb_service.get_debuggees(current_time_unix_msec) 76 | 77 | if not args.include_inactive: 78 | # If there are any debuggees that support the 'active debuggee' feature, 79 | # then we go ahead and apply isActive filter. Any debuggees that don't 80 | # support it will be filtered out, but given there are debuggees from 81 | # newer agents present odds then that debugees without this feature are 82 | # inactive. 83 | if any(d['activeDebuggeeEnabled'] for d in debuggees): 84 | debuggees = list(filter(lambda d: d['isActive'], debuggees)) 85 | 86 | debuggees = sort_debuggees(debuggees) 87 | 88 | if args.format.is_a_json_value(): 89 | user_output.json_format(debuggees, pretty=args.format.is_pretty_json()) 90 | else: 91 | values = list(map(transform_to_debuggee_summary, debuggees)) 92 | user_output.tabular(SUMMARY_HEADERS, values) 93 | -------------------------------------------------------------------------------- /snapshot_dbg_cli/list_logpoints_command.py: -------------------------------------------------------------------------------- 1 | # Copyright 2022 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | """This module contains the support for the list_logpoints command. 15 | 16 | The list_logpoints command is Used to display the debug logpoints for a debug 17 | target (debuggee). 18 | """ 19 | 20 | from snapshot_dbg_cli import breakpoint_utils 21 | 22 | DESCRIPTION = """ 23 | Used to display the debug logpoints for a debug target (debuggee). By default 24 | all active logpoints are returned. To obtain older, expired logpoints, specify 25 | the --include-inactive option. 26 | """ 27 | 28 | INCLUDE_INACTIVE_HELP = 'Include all logpoints which have completed.' 29 | 30 | ALL_USERS_HELP = """ 31 | If false, display only logpoints created by the current user. Enabled by 32 | default, use --no-all-users to disable. 33 | """ 34 | 35 | NO_ALL_USERS_HELP = """ 36 | Disables --all-users, which is enabled by default. 37 | """ 38 | 39 | SUMMARY_HEADERS = [ 40 | 'User Email', 'Location', 'Condition', 'Log Level', 'Log Message Format', 41 | 'ID', 'Status' 42 | ] 43 | 44 | 45 | def transform_to_logpoint_summary(logpoint): 46 | # Match the fields from SUMMARY_HEADERS 47 | return [ 48 | logpoint['userEmail'], 49 | breakpoint_utils.transform_location_to_file_line(logpoint['location']), 50 | logpoint['condition'] if 'condition' in logpoint else '', 51 | logpoint['logLevel'], 52 | logpoint['logMessageFormatString'], 53 | logpoint['id'], 54 | breakpoint_utils.get_logpoint_short_status(logpoint), 55 | ] 56 | 57 | 58 | class ListLogpointsCommand: 59 | """This class implements the list_logpoints command. 60 | 61 | The register() method is called by the CLI startup code to install the 62 | list_logpoints command information, and the cmd() function will be invoked if 63 | the list_logpoints command was specified by the user. 64 | """ 65 | 66 | def __init__(self): 67 | pass 68 | 69 | def register(self, args_subparsers, required_parsers, common_parsers): 70 | parent_parsers = [ 71 | common_parsers.database_url, common_parsers.format, 72 | common_parsers.debuggee_id 73 | ] 74 | parent_parsers += required_parsers 75 | parser = args_subparsers.add_parser( 76 | 'list_logpoints', description=DESCRIPTION, parents=parent_parsers) 77 | parser.add_argument( 78 | '--include-inactive', help=INCLUDE_INACTIVE_HELP, action='store_true') 79 | parser.add_argument( 80 | '--all-users', 81 | help=ALL_USERS_HELP, 82 | default=True, 83 | action='store_true', 84 | dest='all_users') 85 | parser.add_argument( 86 | '--no-all-users', 87 | help=NO_ALL_USERS_HELP, 88 | action='store_false', 89 | dest='all_users') 90 | parser.set_defaults(func=self.cmd) 91 | 92 | def cmd(self, args, cli_services): 93 | user_output = cli_services.user_output 94 | debugger_rtdb_service = cli_services.get_snapshot_debugger_rtdb_service() 95 | 96 | debugger_rtdb_service.validate_debuggee_id(args.debuggee_id) 97 | 98 | user_email = None if args.all_users is True else cli_services.account 99 | 100 | logpoints = debugger_rtdb_service.get_logpoints( 101 | debuggee_id=args.debuggee_id, 102 | include_inactive=args.include_inactive, 103 | user_email=user_email) 104 | 105 | if args.format.is_a_json_value(): 106 | user_output.json_format(logpoints, pretty=args.format.is_pretty_json()) 107 | else: 108 | values = list(map(transform_to_logpoint_summary, logpoints)) 109 | user_output.tabular(SUMMARY_HEADERS, values) 110 | -------------------------------------------------------------------------------- /snapshot_dbg_cli/list_snapshots_command.py: -------------------------------------------------------------------------------- 1 | # Copyright 2022 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | """This module contains the support for the list_snapshots command. 15 | 16 | The list_snapshots command is Used to display the debug snapshots for a debug 17 | target (debuggee). 18 | """ 19 | 20 | from snapshot_dbg_cli import breakpoint_utils 21 | from snapshot_dbg_cli.status_message import StatusMessage 22 | 23 | DESCRIPTION = """ 24 | Used to display the debug snapshots for a debug target (debuggee). By default 25 | all active snapshots are returned. To obtain completed snapshots specify the 26 | --include-inactive option. 27 | """ 28 | 29 | INCLUDE_INACTIVE_HELP = 'Include all snapshots which have completed.' 30 | 31 | ALL_USERS_HELP = """ 32 | If set, display snapshots from all users, rather than only the current user. 33 | """ 34 | 35 | SUMMARY_HEADERS = ['Status', 'Location', 'Condition', 'CompletedTime', 'ID'] 36 | 37 | 38 | def get_snapshot_state(snapshot): 39 | if not snapshot['isFinalState']: 40 | return 'ACTIVE' 41 | 42 | status_message = StatusMessage(snapshot) 43 | 44 | if not status_message.is_error: 45 | return 'COMPLETED' 46 | 47 | refers_to = status_message.refers_to 48 | if refers_to == 'BREAKPOINT_AGE': 49 | return 'EXPIRED' 50 | 51 | return 'FAILED' 52 | 53 | 54 | def transform_to_snapshot_summary(snapshot): 55 | # Match the fields from SUMMARY_HEADERS 56 | return [ 57 | get_snapshot_state(snapshot), 58 | breakpoint_utils.transform_location_to_file_line(snapshot['location']), 59 | snapshot['condition'] if 'condition' in snapshot else '', 60 | snapshot['finalTime'] if 'finalTime' in snapshot else '', snapshot['id'] 61 | ] 62 | 63 | 64 | class ListSnapshotsCommand: 65 | """This class implements the list_snapshots command. 66 | 67 | The register() method is called by the CLI startup code to install the 68 | list_snapshots command information, and the cmd() function will be invoked if 69 | the list_snapshots command was specified by the user. 70 | """ 71 | 72 | def __init__(self): 73 | pass 74 | 75 | def register(self, args_subparsers, required_parsers, common_parsers): 76 | parent_parsers = [ 77 | common_parsers.database_url, common_parsers.format, 78 | common_parsers.debuggee_id 79 | ] 80 | parent_parsers += required_parsers 81 | parser = args_subparsers.add_parser( 82 | 'list_snapshots', description=DESCRIPTION, parents=parent_parsers) 83 | parser.add_argument( 84 | '--include-inactive', help=INCLUDE_INACTIVE_HELP, action='store_true') 85 | parser.add_argument('--all-users', help=ALL_USERS_HELP, action='store_true') 86 | parser.set_defaults(func=self.cmd) 87 | 88 | def cmd(self, args, cli_services): 89 | user_output = cli_services.user_output 90 | debugger_rtdb_service = cli_services.get_snapshot_debugger_rtdb_service() 91 | 92 | debugger_rtdb_service.validate_debuggee_id(args.debuggee_id) 93 | 94 | user_email = None if args.all_users is True else cli_services.account 95 | 96 | snapshots = debugger_rtdb_service.get_snapshots( 97 | debuggee_id=args.debuggee_id, 98 | include_inactive=args.include_inactive, 99 | user_email=user_email) 100 | 101 | if args.format.is_a_json_value(): 102 | user_output.json_format(snapshots, pretty=args.format.is_pretty_json()) 103 | else: 104 | values = list(map(transform_to_snapshot_summary, snapshots)) 105 | user_output.tabular(SUMMARY_HEADERS, values) 106 | -------------------------------------------------------------------------------- /snapshot_dbg_cli/snapshot_debugger_schema.py: -------------------------------------------------------------------------------- 1 | # Copyright 2022 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | """Provides a service to retrieve the paths entities in the database. 15 | """ 16 | 17 | SNAPSHOT_DEBUGGER_ROOT_PATH = 'cdbg' 18 | 19 | DEBUGGEES_PATH = '{root_path}/debuggees' 20 | DEBUGGEES_PATH_FOR_ID = '{root_path}/debuggees/{debuggee_id}' 21 | 22 | SCHEMA_VERSION_PATH = '{root_path}/schema_version' 23 | 24 | BREAKPOINTS_PATH = '{root_path}/breakpoints/{debuggee_id}' 25 | 26 | BREAKPOINTS_ACTIVE_PATH = '{root_path}/breakpoints/{debuggee_id}/active' 27 | BREAKPOINTS_ACTIVE_PATH_FOR_ID = ('{root_path}/breakpoints/{debuggee_id}' 28 | '/active/{breakpoint_id}') 29 | 30 | BREAKPOINTS_FINAL_PATH = '{root_path}/breakpoints/{debuggee_id}/final' 31 | BREAKPOINTS_FINAL_PATH_FOR_ID = ('{root_path}/breakpoints/{debuggee_id}' 32 | '/final/{breakpoint_id}') 33 | 34 | BREAKPOINTS_SNAPSHOT_PATH = '{root_path}/breakpoints/{debuggee_id}/snapshot' 35 | BREAKPOINTS_SNAPSHOT_PATH_FOR_ID = ('{root_path}/breakpoints/{debuggee_id}' 36 | '/snapshot/{breakpoint_id}') 37 | 38 | 39 | class SnapshotDebuggerSchema: 40 | """This class provides methods for retrieving database paths. 41 | 42 | The purpose of this class is to keep all the information about the database 43 | paths in one location and provide utility methods for retrieving them. 44 | """ 45 | 46 | def get_path_schema_version(self): 47 | return SCHEMA_VERSION_PATH.format(root_path=SNAPSHOT_DEBUGGER_ROOT_PATH) 48 | 49 | def get_path_debuggees(self): 50 | return DEBUGGEES_PATH.format(root_path=SNAPSHOT_DEBUGGER_ROOT_PATH) 51 | 52 | def get_path_debuggees_for_id(self, debuggee_id): 53 | return DEBUGGEES_PATH_FOR_ID.format( 54 | root_path=SNAPSHOT_DEBUGGER_ROOT_PATH, debuggee_id=debuggee_id) 55 | 56 | def get_path_breakpoints(self, debuggee_id): 57 | return self._get_path_breakpoints(BREAKPOINTS_PATH, debuggee_id) 58 | 59 | def get_path_breakpoints_active(self, debuggee_id): 60 | return self._get_path_breakpoints(BREAKPOINTS_ACTIVE_PATH, debuggee_id) 61 | 62 | def get_path_breakpoints_active_for_id(self, debuggee_id, breakpoint_id): 63 | return self._get_path_breakpoints_for_id(BREAKPOINTS_ACTIVE_PATH_FOR_ID, 64 | debuggee_id, breakpoint_id) 65 | 66 | def get_path_breakpoints_final(self, debuggee_id): 67 | return self._get_path_breakpoints(BREAKPOINTS_FINAL_PATH, debuggee_id) 68 | 69 | def get_path_breakpoints_final_for_id(self, debuggee_id, breakpoint_id): 70 | return self._get_path_breakpoints_for_id(BREAKPOINTS_FINAL_PATH_FOR_ID, 71 | debuggee_id, breakpoint_id) 72 | 73 | def get_path_breakpoints_snapshot(self, debuggee_id): 74 | return self._get_path_breakpoints(BREAKPOINTS_SNAPSHOT_PATH, debuggee_id) 75 | 76 | def get_path_breakpoints_snapshot_for_id(self, debuggee_id, breakpoint_id): 77 | return self._get_path_breakpoints_for_id(BREAKPOINTS_SNAPSHOT_PATH_FOR_ID, 78 | debuggee_id, breakpoint_id) 79 | 80 | def _get_path_breakpoints(self, base_breakpoints_format_string, debuggee_id): 81 | return base_breakpoints_format_string.format( 82 | root_path=SNAPSHOT_DEBUGGER_ROOT_PATH, debuggee_id=debuggee_id) 83 | 84 | def _get_path_breakpoints_for_id(self, base_breakpoints_format_string, 85 | debuggee_id, breakpoint_id): 86 | return base_breakpoints_format_string.format( 87 | root_path=SNAPSHOT_DEBUGGER_ROOT_PATH, 88 | debuggee_id=debuggee_id, 89 | breakpoint_id=breakpoint_id) 90 | -------------------------------------------------------------------------------- /snapshot_dbg_cli/time_utils.py: -------------------------------------------------------------------------------- 1 | # Copyright 2022 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | """This module provides utilities related to timestamps. 15 | """ 16 | 17 | import datetime 18 | import time 19 | 20 | 21 | def get_current_time_unix_msec(): 22 | return int(time.time() * 1000) 23 | 24 | 25 | def convert_unix_msec_to_rfc3339(unix_msec): 26 | """Converts a Unix timestamp represented in milliseconds since the epoch to an 27 | 28 | RFC3339 string representation. 29 | 30 | Args: 31 | unix_msec: The Unix timestamp, represented in milliseconds since the epoch. 32 | 33 | Returns: 34 | An RFC3339 encoded timestamp string in format: "%Y-%m-%dT%H:%M:%SZ". 35 | """ 36 | try: 37 | seconds = unix_msec / 1000 38 | timestamp = seconds 39 | dt = datetime.datetime.fromtimestamp(timestamp, tz=datetime.timezone.utc) 40 | return dt.strftime('%Y-%m-%dT%H:%M:%S') + 'Z' 41 | except (OverflowError, OSError, TypeError, ValueError): 42 | # By using 0, we'll still get the expected formatted string, and the value 43 | # will be '1970-01-01...', which visually will be recognizable as beginning 44 | # of epoch and that the value was not known. 45 | return convert_unix_msec_to_rfc3339(0) 46 | 47 | 48 | def set_converted_timestamps(data, field_mappings): 49 | """Converts raw unix timestamp fields to human readable versions. 50 | 51 | If the destination field already exists it won't be overwritten. 52 | 53 | Example: 54 | data = { 55 | fooTimeUnixMsec: 1649962215000 56 | barTimeUnixMsec: 1649962216000 57 | } 58 | 59 | field_mappings = [ 60 | ('fooTime', 'fooTimeUnixMsec'), 61 | ('barTIme, 'barTimeUnixMsec') 62 | ] 63 | 64 | set_converted_timestamps(data, field_mappings) 65 | data = { 66 | fooTimeUnixMsec: 1649962215000 67 | fooTime: '2022-04-14T18:50:15Z' 68 | barTimeUnixMsec = 1649962216000 69 | barTime: '2022-04-14T18:50:16Z' 70 | } 71 | 72 | 73 | Args: 74 | data: The container dict, expected to represent either a breakpoint a 75 | debuggee. 76 | field_mappings: [(str, str)] List of field mappings. For each entry the 77 | first member is the destination field to be set with a human readable 78 | timestamp. The second member is the source field and should map to a unix 79 | timestamp in data. 80 | 81 | Returns: 82 | The data dict that was passed in. 83 | """ 84 | 85 | for m in field_mappings: 86 | if m[0] not in data and m[1] in data: 87 | data[m[0]] = convert_unix_msec_to_rfc3339( 88 | data[m[1]]) if data[m[1]] != 0 else 'not set' 89 | 90 | return data 91 | -------------------------------------------------------------------------------- /snapshot_dbg_cli/user_input.py: -------------------------------------------------------------------------------- 1 | # Copyright 2022 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | """This module provides a simple user input utility class. 15 | 16 | This class should be used by the CLI anytime it requires user input. 17 | """ 18 | 19 | 20 | class UserInput: 21 | """This class handles prompting the user for input. 22 | 23 | This provides the CLI with the ability to prompt the user for input. 24 | """ 25 | 26 | def prompt_user_to_continue(self): 27 | answer = input('Do you want to continue (Y/n)? ').lower() 28 | 29 | while answer and answer not in ['y', 'n']: 30 | answer = input("Please enter 'y' or 'n': ").lower() 31 | 32 | return answer != 'n' 33 | -------------------------------------------------------------------------------- /snapshot_dbg_cli_tests/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright 2022 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | -------------------------------------------------------------------------------- /snapshot_dbg_cli_tests/test_cli_run.py: -------------------------------------------------------------------------------- 1 | # Copyright 2022 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | """ Unit test file for the cli_run module. 15 | """ 16 | 17 | import io 18 | import sys 19 | import unittest 20 | 21 | import snapshot_dbg_cli.cli_run 22 | import snapshot_dbg_cli.cli_services 23 | import snapshot_dbg_cli.exceptions 24 | 25 | from unittest.mock import MagicMock 26 | from unittest.mock import patch 27 | 28 | 29 | class CliRunTests(unittest.TestCase): 30 | """ Contains the unit tests for the cli_run module. 31 | """ 32 | 33 | def setUp(self): 34 | self.cli_services_mock = MagicMock( 35 | spec=snapshot_dbg_cli.cli_services.CliServices) 36 | 37 | def test_expected_commands_are_registered(self): 38 | testcases = [ 39 | ('delete_debuggees', 'Used to delete debuggees'), 40 | ('delete_logpoints', 'Used to delete logpoints'), 41 | ('delete_snapshots', 'Used to delete snapshots'), 42 | ('get_logpoint', 'Used to retrieve a debug logpoint'), 43 | ('get_snapshot', 'Used to retrieve a debug snapshot'), 44 | ('init', 'Initializes a GCP project with the required'), 45 | ('list_debuggees', 'Used to display a list of the debug targets'), 46 | ('list_logpoints', 'Used to display the debug logpoints'), 47 | ('list_snapshots', 'Used to display the debug snapshots'), 48 | ('set_logpoint', 'Adds a debug logpoint to a debug target'), 49 | ('set_snapshot', 'Creates a snapshot on a debug target'), 50 | ] 51 | 52 | for command, expected_description_substring in testcases: 53 | with self.subTest(command): 54 | argv = ['prog', command, '--help'] 55 | with self.assertRaises(SystemExit), \ 56 | patch.object(sys, 'argv', argv), \ 57 | patch('sys.stdout', new_callable=io.StringIO) as out: 58 | snapshot_dbg_cli.cli_run.run(self.cli_services_mock) 59 | 60 | self.assertIn(expected_description_substring, out.getvalue()) 61 | 62 | def test_command_missing_works_as_expected(self): 63 | argv = ['prog'] 64 | with self.assertRaises(snapshot_dbg_cli.exceptions.SilentlyExitError), \ 65 | patch.object(sys, 'argv', argv), \ 66 | patch('sys.stderr', new_callable=io.StringIO) as err: 67 | snapshot_dbg_cli.cli_run.run(self.cli_services_mock) 68 | 69 | self.assertIn( 70 | 'Missing required argument, please specify --help for more ' 71 | 'information', err.getvalue()) 72 | -------------------------------------------------------------------------------- /snapshot_dbg_cli_tests/test_firebase_types.py: -------------------------------------------------------------------------------- 1 | # Copyright 2023 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | """ Unit test file for the firebase_types module. 15 | """ 16 | 17 | import unittest 18 | from snapshot_dbg_cli.firebase_types import DatabaseInstance 19 | 20 | 21 | class DatabaseInstanceTests(unittest.TestCase): 22 | """ Contains the unit tests for the DatabaseInstance class. 23 | """ 24 | 25 | def test_location_success(self): 26 | locations = ['us-central1', 'europe-west1'] 27 | for location in locations: 28 | with self.subTest(location): 29 | db_instance = DatabaseInstance({ 30 | 'name': 31 | f'projects/1111111111/locations/{location}/instances/foo-cdbg', 32 | 'project': 33 | 'projects/1111111111', 34 | 'databaseUrl': 35 | 'https://foo-cdbg.firebaseio.com', 36 | 'type': 37 | 'USER_DATABASE', 38 | 'state': 39 | 'ACTIVE' 40 | }) 41 | self.assertEqual(location, db_instance.location) 42 | 43 | def test_location_could_not_be_found(self): 44 | invalid_names = [ 45 | '', 46 | 'foo', 47 | # missing the /locations/ 48 | 'projects/1111111111/us-central1/instances/foo-cdbg', 49 | 'projects/1111111111/locations', 50 | 'projects/1111111111/locations/' 51 | ] 52 | 53 | for full_db_name in invalid_names: 54 | with self.subTest(full_db_name): 55 | with self.assertRaises(ValueError) as ctxt: 56 | DatabaseInstance({ 57 | 'name': full_db_name, 58 | 'project': 'projects/1111111111', 59 | 'databaseUrl': 'https://foo-cdbg.firebaseio.com', 60 | 'type': 'USER_DATABASE', 61 | 'state': 'ACTIVE' 62 | }) 63 | self.assertEqual( 64 | f"Failed to extract location from project name '{full_db_name}'", 65 | str(ctxt.exception)) 66 | -------------------------------------------------------------------------------- /snapshot_dbg_cli_tests/test_init.py: -------------------------------------------------------------------------------- 1 | # Copyright 2022 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | """ Unit test __init__. 15 | """ 16 | 17 | import unittest 18 | import snapshot_dbg_cli 19 | 20 | 21 | class CliInitTests(unittest.TestCase): 22 | """ Contains the unit tests for __init__.py 23 | """ 24 | 25 | def test_version_is_expected_value(self): 26 | # Yes, this will need to be updated for each new version. 27 | self.assertEqual('0.3.8', snapshot_dbg_cli.__version__) 28 | -------------------------------------------------------------------------------- /snapshot_dbg_cli_tests/test_snapshot_debugger_schema.py: -------------------------------------------------------------------------------- 1 | # Copyright 2022 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | """ Unit test file for the SnapshotDebuggerSchema class. 15 | """ 16 | 17 | import unittest 18 | from snapshot_dbg_cli import snapshot_debugger_schema 19 | 20 | 21 | class SnapshotDebuggerSchemaTests(unittest.TestCase): 22 | """ Contains the unit tests for the SnapshotDebuggerSchema class. 23 | """ 24 | 25 | def setUp(self): 26 | self.schema = snapshot_debugger_schema.SnapshotDebuggerSchema() 27 | 28 | def test_path_schema_version_is_correct(self): 29 | self.assertEqual(self.schema.get_path_schema_version(), 30 | 'cdbg/schema_version') 31 | 32 | def test_path_debuggees_is_correct(self): 33 | self.assertEqual(self.schema.get_path_debuggees(), 'cdbg/debuggees') 34 | 35 | def test_path_debuggees_for_id_is_correct(self): 36 | self.assertEqual( 37 | self.schema.get_path_debuggees_for_id(debuggee_id='123'), 38 | 'cdbg/debuggees/123') 39 | 40 | def test_get_path_breakpoints_is_correct(self): 41 | self.assertEqual( 42 | self.schema.get_path_breakpoints(debuggee_id='123'), 43 | 'cdbg/breakpoints/123') 44 | 45 | def test_path_breakpoints_active_is_correct(self): 46 | self.assertEqual( 47 | self.schema.get_path_breakpoints_active(debuggee_id='123'), 48 | 'cdbg/breakpoints/123/active') 49 | 50 | def test_path_breakpoints_active_for_id_is_correct(self): 51 | self.assertEqual( 52 | self.schema.get_path_breakpoints_active_for_id( 53 | debuggee_id='123', breakpoint_id='b-1653408119'), 54 | 'cdbg/breakpoints/123/active/b-1653408119') 55 | 56 | def test_path_breakpoints_final_is_correct(self): 57 | self.assertEqual( 58 | self.schema.get_path_breakpoints_final(debuggee_id='123'), 59 | 'cdbg/breakpoints/123/final') 60 | 61 | def test_path_breakpoints_final_for_id_is_correct(self): 62 | self.assertEqual( 63 | self.schema.get_path_breakpoints_final_for_id( 64 | debuggee_id='123', breakpoint_id='b-1653408119'), 65 | 'cdbg/breakpoints/123/final/b-1653408119') 66 | 67 | def test_path_breakpoints_snapshot_is_correct(self): 68 | self.assertEqual( 69 | self.schema.get_path_breakpoints_snapshot(debuggee_id='123'), 70 | 'cdbg/breakpoints/123/snapshot') 71 | 72 | def test_path_breakpoints_snapshot_for_id_is_correct(self): 73 | self.assertEqual( 74 | self.schema.get_path_breakpoints_snapshot_for_id( 75 | debuggee_id='123', breakpoint_id='b-1653408119'), 76 | 'cdbg/breakpoints/123/snapshot/b-1653408119') 77 | 78 | 79 | if __name__ == '__main__': 80 | unittest.main() 81 | -------------------------------------------------------------------------------- /snapshot_dbg_cli_tests/test_user_input.py: -------------------------------------------------------------------------------- 1 | # Copyright 2022 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | """ Unit test file for the user_input module. 15 | """ 16 | 17 | import unittest 18 | 19 | from snapshot_dbg_cli.user_input import UserInput 20 | from unittest.mock import MagicMock 21 | from unittest.mock import call 22 | from unittest.mock import patch 23 | 24 | 25 | class SnapshotDebuggerSchemaTests(unittest.TestCase): 26 | """Contains the unit tests for the user_input module. 27 | """ 28 | 29 | def test_prompt_user_to_continue(self): 30 | testcases = [ 31 | ([''], True), 32 | (['o', ''], True), 33 | (['foo', ''], True), 34 | (['o', 'f', ''], True), 35 | (['y'], True), 36 | (['o', 'y'], True), 37 | (['Y'], True), 38 | (['foo', 'n'], False), 39 | (['n'], False), 40 | (['o', 'n'], False), 41 | (['N'], False), 42 | (['o', 'N'], False), 43 | ] 44 | 45 | for input_sequence, expected_response in testcases: 46 | with self.subTest(input_sequence): 47 | with patch('builtins.input', 48 | MagicMock(side_effect=input_sequence)) as input_mock: 49 | expected_calls = [call('Do you want to continue (Y/n)? ')] 50 | for _ in range(0, len(input_sequence) - 1): 51 | expected_calls.append(call("Please enter 'y' or 'n': ")) 52 | 53 | self.assertEqual(expected_response, 54 | UserInput().prompt_user_to_continue()) 55 | self.assertEqual(expected_calls, input_mock.mock_calls) 56 | -------------------------------------------------------------------------------- /snapshot_dbg_extension/.gitignore: -------------------------------------------------------------------------------- 1 | .vscode 2 | .eslintrc.json 3 | node_modules/ 4 | out/ 5 | *.vsix 6 | -------------------------------------------------------------------------------- /snapshot_dbg_extension/.vscodeignore: -------------------------------------------------------------------------------- 1 | .vscode/** 2 | .vscode-test/** 3 | out/test/** 4 | src/** 5 | .gitignore 6 | .yarnrc 7 | vsc-extension-quickstart.md 8 | **/tsconfig.json 9 | **/.eslintrc.json 10 | **/*.map 11 | **/*.ts 12 | node_modules/** 13 | 14 | -------------------------------------------------------------------------------- /snapshot_dbg_extension/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # What's New? 2 | 3 | ## 0.1.0 4 | 5 | - Initial release 6 | -------------------------------------------------------------------------------- /snapshot_dbg_extension/media/expr.svg: -------------------------------------------------------------------------------- 1 | EX 2 | PR 3 | EXPR -------------------------------------------------------------------------------- /snapshot_dbg_extension/media/icon-512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GoogleCloudPlatform/snapshot-debugger/50a5b3edcb20c4744d10116022277b2db50338dc/snapshot_dbg_extension/media/icon-512.png -------------------------------------------------------------------------------- /snapshot_dbg_extension/src/debugUtil.ts: -------------------------------------------------------------------------------- 1 | 2 | let debugLogEnabled = true; 3 | export function setDebugLogEnabled(enabled: boolean): void { 4 | debugLogEnabled = enabled; 5 | } 6 | 7 | export function debugLog(message?: any, ...optionalParams: any[]): void { 8 | if (debugLogEnabled) { 9 | console.log(message, ...optionalParams); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /snapshot_dbg_extension/src/debuggeePicker.ts: -------------------------------------------------------------------------------- 1 | import { Database } from 'firebase-admin/database'; 2 | import * as vscode from 'vscode'; 3 | import { debugLog } from './debugUtil'; 4 | 5 | const NO_DEBUGGEES = 'No debuggees found'; 6 | const NO_DEBUGGEES_DETAIL = 'Please check your configuration'; 7 | 8 | class DebuggeeItem implements vscode.QuickPickItem { 9 | label: string; 10 | detail: string; 11 | kind?: vscode.QuickPickItemKind | undefined; 12 | 13 | constructor(public debuggeeId: string, description: string, public timestamp: Date) { 14 | if (this.debuggeeId === NO_DEBUGGEES) { 15 | this.label = NO_DEBUGGEES; 16 | this.detail = NO_DEBUGGEES_DETAIL; 17 | } else { 18 | this.label = description; 19 | this.detail = `${debuggeeId} | ${duractionToLastActiveString(new Date().getTime() - timestamp.getTime())}`; 20 | } 21 | } 22 | } 23 | 24 | function duractionToLastActiveString(d: number) { 25 | const seconds = d / 1000; 26 | const minutes = seconds / 60; 27 | 28 | if (minutes < 60) { 29 | return 'Recently active'; 30 | } 31 | 32 | return `Last seen ${durationToFriendlyString(d)} ago`; 33 | } 34 | 35 | function durationToFriendlyString(d: number) { 36 | const seconds = d / 1000; 37 | const minutes = seconds / 60; 38 | const hours = minutes / 60; 39 | const days = hours / 24; 40 | 41 | if (seconds == 1) { 42 | return 'a second'; 43 | } else if (seconds < 45) { 44 | return `${Math.round(seconds)} seconds`; 45 | } else if (seconds < 90) { 46 | return 'a minute'; 47 | } else if (minutes < 45) { 48 | return `${Math.round(minutes)} minutes`; 49 | } else if (minutes < 90) { 50 | return 'an hour'; 51 | } else if (hours < 24) { 52 | return `${Math.round(hours)} hours`; 53 | } else if (hours < 42) { 54 | return 'a day'; 55 | } else if (days < 30) { 56 | return `${Math.round(days)} days`; 57 | } else if (days < 45) { 58 | return 'a month'; 59 | } else { 60 | return `${Math.round(days / 30)} months`; 61 | } 62 | } 63 | 64 | async function fetchDebuggees(db: Database): Promise { 65 | const debuggees: DebuggeeItem[] = []; 66 | 67 | const snapshot = await db.ref('/cdbg/debuggees').get(); 68 | const savedDebuggees = snapshot.val(); 69 | if (savedDebuggees) { 70 | for (const debuggeeId in savedDebuggees) { 71 | const timestamp = new Date(savedDebuggees[debuggeeId].lastUpdateTimeUnixMsec); 72 | const labels = savedDebuggees[debuggeeId].labels; 73 | debuggees.push( 74 | new DebuggeeItem( 75 | debuggeeId, 76 | `${labels.module || 'default'} - ${labels.version}`, 77 | timestamp)); 78 | } 79 | debuggees.sort((a: DebuggeeItem, b: DebuggeeItem) => b.timestamp.getTime() - a.timestamp.getTime()); 80 | } else { 81 | const noDebuggees = new DebuggeeItem(NO_DEBUGGEES, "", new Date()); 82 | noDebuggees.debuggeeId = ""; 83 | debuggees.push(); 84 | } 85 | return debuggees; 86 | } 87 | 88 | export async function pickDebuggeeId(db: Database): Promise { 89 | const selection = await vscode.window.showQuickPick(fetchDebuggees(db), {'title': 'Select Debuggee'}); 90 | if (selection) { 91 | debugLog(`Selected Debuggee: ${selection.debuggeeId}`); 92 | return selection.debuggeeId; 93 | } else { 94 | return undefined; 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /snapshot_dbg_extension/src/expressionsPrompter.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | 3 | export async function promptUserForExpressions(): Promise { 4 | let expressions: string[] = []; 5 | 6 | while (true) { 7 | const title = (expressions.length == 0) ? 8 | "Add an Expression to the Breakpoint" : "Add Another Expression to the Breakpoint"; 9 | 10 | const prompt = (expressions.length == 0) ? 11 | "Press 'Escape' or enter an empty line if you do not wish to add an expression." : 12 | "Press 'Escape' or enter an empty line if you do not wish to add any more expressions."; 13 | 14 | let expression = await vscode.window.showInputBox({title, prompt}); 15 | 16 | expression = expression?.trim(); 17 | 18 | if (!expression) { 19 | break; 20 | } 21 | 22 | expressions.push(expression); 23 | }; 24 | 25 | return expressions.length ? expressions : undefined; 26 | } 27 | -------------------------------------------------------------------------------- /snapshot_dbg_extension/src/extension.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | import { WorkspaceFolder, DebugConfiguration, DebugSession, ProviderResult, CancellationToken } from 'vscode'; 3 | import { IsActiveWhenClauseContext } from './whenClauseContextUtil'; 4 | 5 | import { CustomRequest, SnapshotDebuggerSession } from './adapter'; 6 | import { UserPreferences } from './userPreferences'; 7 | import { debugLog, setDebugLogEnabled } from './debugUtil'; 8 | 9 | // This method is called when the extension is activated. 10 | // The extension is activated the very first time the command is executed 11 | export function activate(context: vscode.ExtensionContext) { 12 | IsActiveWhenClauseContext.create(); 13 | 14 | const userPreferences: UserPreferences = {"isExpressionsPromptEnabled": true}; 15 | 16 | context.subscriptions.push(vscode.commands.registerCommand('extension.snapshotdbg.viewHistoricalSnapshot', async args => { 17 | const activeDebugSession: DebugSession | undefined = vscode.debug.activeDebugSession; 18 | if (activeDebugSession) { 19 | activeDebugSession.customRequest(CustomRequest.RUN_HISTORICAL_SNAPSHOT_PICKER); 20 | } else { 21 | debugLog("Unexpected no active SnapshotDebugger session.") 22 | } 23 | })); 24 | 25 | context.subscriptions.push(vscode.commands.registerCommand('extension.snapshotdbg.toggleExpressions', async args => { 26 | const action = userPreferences.isExpressionsPromptEnabled ? "Disabled" : "Enabled"; 27 | const message = `${action} expresssions prompt when creating a breakpoint.`; 28 | const reverseAction = userPreferences.isExpressionsPromptEnabled ? "re-enable" : "disable"; 29 | const reverseMessage = `Click again to ${reverseAction} it.`; 30 | 31 | userPreferences.isExpressionsPromptEnabled = !userPreferences.isExpressionsPromptEnabled; 32 | await vscode.window.showInformationMessage(message, {"detail": reverseMessage, "modal": true}); 33 | })); 34 | 35 | const provider = new SnapshotDebuggerConfigurationProvider(); 36 | context.subscriptions.push(vscode.debug.registerDebugConfigurationProvider('snapshotdbg', provider)); 37 | 38 | const factory = new DebugAdapterFactory(userPreferences); 39 | context.subscriptions.push(vscode.debug.registerDebugAdapterDescriptorFactory('snapshotdbg', factory)); 40 | } 41 | 42 | // This method is called when the extension is deactivated 43 | export function deactivate() { 44 | } 45 | 46 | 47 | class SnapshotDebuggerConfigurationProvider implements vscode.DebugConfigurationProvider { 48 | resolveDebugConfiguration(folder: WorkspaceFolder | undefined, config: DebugConfiguration, token?: CancellationToken): ProviderResult { 49 | // Just accept all configuration. 50 | return config; 51 | } 52 | } 53 | 54 | class DebugAdapterFactory implements vscode.DebugAdapterDescriptorFactory { 55 | private userPreferences: UserPreferences; 56 | 57 | constructor(userPreferences: UserPreferences) { 58 | this.userPreferences = userPreferences; 59 | } 60 | 61 | createDebugAdapterDescriptor(session: vscode.DebugSession, executable: vscode.DebugAdapterExecutable | undefined): vscode.ProviderResult { 62 | return new vscode.DebugAdapterInlineImplementation(new SnapshotDebuggerSession(this.userPreferences)); 63 | } 64 | 65 | } 66 | -------------------------------------------------------------------------------- /snapshot_dbg_extension/src/gcloudCredential.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | 3 | import { exec } from 'child_process'; 4 | import { Credential, GoogleOAuthAccessToken } from 'firebase-admin/app'; 5 | import { debugLog } from './debugUtil'; 6 | 7 | const NO_TOKEN_MESSAGE = 'Could not fetch access token from gcloud. Are you logged in?'; 8 | 9 | export class GcloudCredential implements Credential { 10 | public initialized = false; 11 | private showingModal = false; 12 | 13 | async getAccessToken(): Promise { 14 | return new Promise((resolve, reject) => { 15 | exec('gcloud auth print-access-token', (err, stdout, stderr) => { 16 | if (stderr) { 17 | debugLog(stderr); 18 | } 19 | if (err) { 20 | reject(err); 21 | } 22 | if (!stdout.trim()) { 23 | // Only show the dialog if we've previously been connected and are not already showing the error. 24 | if (this.initialized && !this.showingModal) { 25 | this.showingModal = true; 26 | vscode.window.showErrorMessage(NO_TOKEN_MESSAGE, {"modal": true}).then(() => this.showingModal = false); 27 | } 28 | reject(new Error(NO_TOKEN_MESSAGE)); 29 | } 30 | resolve({ 31 | access_token: stdout.trim(), 32 | expires_in: 3600 33 | }); 34 | }); 35 | }); 36 | } 37 | 38 | async getProjectId(): Promise { 39 | return new Promise((resolve, reject) => { 40 | exec('gcloud config get-value project', (err, stdout, stderr) => { 41 | if (stderr) { 42 | debugLog(stderr); 43 | } 44 | if (err) { 45 | reject(err); 46 | } 47 | if (!stdout.trim()) { 48 | reject(new Error('Unable to fetch project id')); 49 | } 50 | resolve(stdout.trim()); 51 | }); 52 | }); 53 | } 54 | 55 | async getAccount(): Promise { 56 | return new Promise((resolve, reject) => { 57 | exec('gcloud config get-value account', (err, stdout, stderr) => { 58 | if (stderr) { 59 | debugLog(stderr); 60 | } 61 | if (err) { 62 | reject(err); 63 | } 64 | if (!stdout.trim()) { 65 | reject(new Error('Unable to fetch account')); 66 | } 67 | resolve(stdout.trim()); 68 | }); 69 | }); 70 | } 71 | } -------------------------------------------------------------------------------- /snapshot_dbg_extension/src/ideBreakpoints.ts: -------------------------------------------------------------------------------- 1 | import { DebugProtocol } from '@vscode/debugprotocol'; 2 | import { sourceBreakpointToString, stringToSourceBreakpoint } from './util'; 3 | 4 | export interface IdeBreakpointsDiff { 5 | added: DebugProtocol.SourceBreakpoint[]; 6 | deleted: DebugProtocol.SourceBreakpoint[]; 7 | } 8 | 9 | /** 10 | * Class for the adaptor to track the current breakpoints the IDE has. 11 | */ 12 | export class IdeBreakpoints { 13 | private breakpoints: Map = new Map(); 14 | 15 | public applyNewIdeSnapshot(path: string, newSnapshot: DebugProtocol.SourceBreakpoint[]): IdeBreakpointsDiff { 16 | const prevBPs: DebugProtocol.SourceBreakpoint[] = this.breakpoints.get(path) ?? []; 17 | const currBPs: DebugProtocol.SourceBreakpoint[] = newSnapshot; 18 | const prevBPSet = new Set(prevBPs.map(bp => sourceBreakpointToString(bp))); 19 | const currBPSet = new Set(currBPs.map(bp => sourceBreakpointToString(bp))); 20 | 21 | const newBPs = [...currBPSet].filter(bp => !prevBPSet.has(bp)); 22 | const delBPs = [...prevBPSet].filter(bp => !currBPSet.has(bp)); 23 | 24 | const bpDiff = { 25 | added: newBPs.map(bp => stringToSourceBreakpoint(bp)), 26 | deleted: delBPs.map(bp => stringToSourceBreakpoint(bp)), 27 | } 28 | 29 | this.breakpoints.set(path, newSnapshot); 30 | return bpDiff; 31 | } 32 | 33 | public add(path: string, breakpoint: DebugProtocol.SourceBreakpoint): void { 34 | const bps: DebugProtocol.SourceBreakpoint[] = this.breakpoints.get(path) ?? []; 35 | bps.push(breakpoint); 36 | this.breakpoints.set(path, bps); 37 | } 38 | 39 | public remove(path: string, breakpoint: DebugProtocol.SourceBreakpoint): void { 40 | const bps = this.breakpoints.get(path) ?? []; 41 | const bpString = sourceBreakpointToString(breakpoint); 42 | this.breakpoints.set(path, bps.filter(b => bpString !== sourceBreakpointToString(b))); 43 | } 44 | 45 | public updateLine(path: string, originalLine: number, newLine: number) : void { 46 | const bps: DebugProtocol.SourceBreakpoint[] = this.breakpoints.get(path) ?? []; 47 | const bp = bps?.find(b => b.line === originalLine); 48 | if (bp) { bp.line = newLine; } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /snapshot_dbg_extension/src/lineMappings.ts: -------------------------------------------------------------------------------- 1 | import { DebugProtocol } from '@vscode/debugprotocol'; 2 | import { sourceBreakpointToString, stringToSourceBreakpoint } from './util'; 3 | 4 | export interface IdeBreakpointsDiff { 5 | added: DebugProtocol.SourceBreakpoint[]; 6 | deleted: DebugProtocol.SourceBreakpoint[]; 7 | } 8 | 9 | /** 10 | * Class to track line number that got remapped by the agent. 11 | * 12 | * When a BP gets set on a line the agent doesn't find code on, it may find the 13 | * closest line with code, set the BP and report back the updated line number. 14 | * This class tracks thes remappings per path. 15 | */ 16 | export class LineMappings { 17 | // Key: File path. 18 | // Value: Map of requested line to actual line agent reported back with code. 19 | private paths: Map> = new Map(); 20 | 21 | public add(path: string, requestedLine: number, actualLine: number): void { 22 | let lineMappings = this.paths.get(path); 23 | if (lineMappings === undefined) { 24 | lineMappings = new Map(); 25 | this.paths.set(path, lineMappings); 26 | } 27 | 28 | lineMappings.set(requestedLine, actualLine); 29 | } 30 | 31 | public get(path: string, line: number): number|undefined { 32 | return this.paths.get(path)?.get(line); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /snapshot_dbg_extension/src/logLevelPicker.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | 3 | import { sleep } from "./util"; 4 | 5 | let gPickPending = false; 6 | 7 | async function pickLogLevel(title: string): Promise { 8 | /** 9 | * The lable field supports icons via the $()-syntax. 10 | * https://code.visualstudio.com/api/references/vscode-api#ThemeIcon 11 | * 12 | * A list of available icons can be found here: 13 | * https://code.visualstudio.com/api/references/icons-in-labels 14 | * 15 | * Here we're able to leverage the fact that icons with the names 'info', 16 | * 'warning' and 'error' exist and we can convert our log level to lowercase 17 | * to obtain a relevant icon. 18 | */ 19 | const items: vscode.QuickPickItem[] = 20 | ['INFO', 'WARNING', 'ERROR'].map(level => ( 21 | {'label': `${level} $(${level.toLowerCase()})`} 22 | )); 23 | 24 | // If we have one call to showQuickPick outstanding, any other calls to it 25 | // during this time will immediately return with a value of undefined. 26 | // During initialization time it's possible we'll be prompting the user 27 | // to select a log level for logpoints found in the IDE in multiple 28 | // different files. This can lead to this situation occurring, so here 29 | // we serialize the calls. 30 | while (gPickPending) { 31 | await (sleep(250)); 32 | } 33 | 34 | gPickPending = true; 35 | const selection = await vscode.window.showQuickPick(items, {title}); 36 | gPickPending = false; 37 | 38 | return selection?.label.split(' ')[0].trim(); 39 | } 40 | 41 | export async function pickLogLevelNewlyCreated(): Promise { 42 | return pickLogLevel('Select Log Level'); 43 | } 44 | 45 | export async function pickLogLevelSyncedFromIDE(path: string, line: number): Promise { 46 | return pickLogLevel(`Select Log Level For Logpoint: ${path}:${line}`); 47 | } 48 | -------------------------------------------------------------------------------- /snapshot_dbg_extension/src/snapshotPicker.ts: -------------------------------------------------------------------------------- 1 | import { Database, DataSnapshot } from 'firebase-admin/database'; 2 | import { CdbgBreakpoint } from './breakpoint'; 3 | import * as vscode from 'vscode'; 4 | import { debugLog } from './debugUtil'; 5 | 6 | class SnapshotItem implements vscode.QuickPickItem { 7 | label: string; 8 | description: string | undefined; 9 | kind?: vscode.QuickPickItemKind | undefined; 10 | 11 | constructor(public cdbgBreakpoint: CdbgBreakpoint | undefined) { 12 | if (cdbgBreakpoint) { 13 | this.label = cdbgBreakpoint.id; 14 | this.description = ` ${cdbgBreakpoint.shortPath}:${cdbgBreakpoint.line} ${cdbgBreakpoint.finalTime}` 15 | } else { 16 | this.label = "No snapshots found"; 17 | } 18 | } 19 | } 20 | 21 | async function fetchSnapshots(db: Database, debuggeeId: string): Promise { 22 | const snapshotItems: SnapshotItem[] = []; 23 | 24 | const snapshots: DataSnapshot= await db.ref(`cdbg/breakpoints/${debuggeeId}/final`).get(); 25 | snapshots.forEach((breakpoint): void => { 26 | const cdbgBreakpoint: CdbgBreakpoint = CdbgBreakpoint.fromSnapshot(breakpoint); 27 | if (cdbgBreakpoint.isSnapshot()) { 28 | snapshotItems.push(new SnapshotItem(cdbgBreakpoint)); 29 | } 30 | }); 31 | return snapshotItems; 32 | } 33 | 34 | export async function pickSnapshot(db: Database, debuggeeId: string): Promise { 35 | const selection = await vscode.window.showQuickPick(fetchSnapshots(db, debuggeeId), {'title': 'Select Previously Captured Snapshot'}); 36 | if (selection?.cdbgBreakpoint) { 37 | debugLog(`Selected Snapshot: ${selection.cdbgBreakpoint.id}`); 38 | return selection.cdbgBreakpoint; 39 | } else { 40 | return undefined; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /snapshot_dbg_extension/src/statusMessage.ts: -------------------------------------------------------------------------------- 1 | import {ServerBreakpoint, Variable} from './breakpoint' 2 | 3 | /** 4 | * Convenience class that represents a StatusMessage. It provides convenience 5 | * functions for interpreting and using the message. 6 | */ 7 | export class StatusMessage { 8 | /** 9 | * The parsed message or undefined if the status is not present or the message 10 | * could not be parsed. 11 | */ 12 | message: string|undefined = undefined; 13 | 14 | constructor(private readonly parent: ServerBreakpoint|Variable) { 15 | this.message = this.parseMessage(); 16 | } 17 | 18 | private parseMessage(): string|undefined { 19 | if (!this.parent.status?.description?.format) { 20 | return undefined; 21 | } 22 | 23 | // Get the formatting string such as "Failed to load '$0' which 24 | // helps debug $1" 25 | const formatMessage = this.parent.status.description; 26 | let formatString = formatMessage.format!; 27 | 28 | // Get the number of parameters to replace '$' prefixed vars. 29 | let totalParameters = 0; 30 | if (formatMessage.parameters) { 31 | totalParameters = formatMessage.parameters.length; 32 | } 33 | let dollarIndex; 34 | let outputString = ''; 35 | 36 | // While we have remaining '$' place holders keep traversing the format 37 | // string. 38 | while ((dollarIndex = formatString.indexOf('$')) > -1) { 39 | // Add the first portion of the format string to the output. 40 | outputString += formatString.substr(0, dollarIndex); 41 | if (formatString.length > dollarIndex + 1) { 42 | // Get the parameters index value in the parameters list or a '$' if 43 | // the value is escaped. 44 | const nextChar = formatString.substr(dollarIndex + 1, 1); 45 | 46 | // Assumption is there are not more than 10 parameters. 47 | const intVal = Number(nextChar); 48 | if (nextChar === '$') { 49 | // The next character is a '$' this is an escaped character, be sure 50 | // to maintain the next '$'. 51 | outputString += '$'; 52 | formatString = formatString.substr(dollarIndex + 2); 53 | } else if ( 54 | !isNaN(intVal) && intVal < totalParameters && 55 | formatMessage.parameters) { 56 | // Get the proper parameter for the index. 57 | outputString += formatMessage.parameters[intVal]; 58 | formatString = formatString.substr(dollarIndex + 2); 59 | } else { 60 | // FormatMessage with too many arguments, unclear what to do from 61 | // the spec, will just keep it verbatim. 62 | outputString += '$'; 63 | formatString = formatString.substr(dollarIndex + 1); 64 | } 65 | 66 | } else { 67 | // A '$' was the last value add it back. 68 | outputString += '$'; 69 | formatString = ''; 70 | } 71 | } 72 | 73 | // Add the remainder of the format string to the returned value. 74 | outputString += formatString; 75 | return outputString; 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /snapshot_dbg_extension/src/test/runTest.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path'; 2 | 3 | import { runTests } from '@vscode/test-electron'; 4 | 5 | async function main() { 6 | try { 7 | // The folder containing the Extension Manifest package.json 8 | // Passed to `--extensionDevelopmentPath` 9 | const extensionDevelopmentPath = path.resolve(__dirname, '../../'); 10 | 11 | // The path to test runner 12 | // Passed to --extensionTestsPath 13 | const extensionTestsPath = path.resolve(__dirname, './suite/index'); 14 | 15 | // Download VS Code, unzip it and run the integration test 16 | await runTests({ extensionDevelopmentPath, extensionTestsPath }); 17 | } catch (err) { 18 | console.error('Failed to run tests', err); 19 | process.exit(1); 20 | } 21 | } 22 | 23 | main(); 24 | -------------------------------------------------------------------------------- /snapshot_dbg_extension/src/test/suite/extension.test.ts: -------------------------------------------------------------------------------- 1 | import * as assert from 'assert'; 2 | 3 | // You can import and use all API from the 'vscode' module 4 | // as well as import your extension to test it 5 | import * as vscode from 'vscode'; 6 | // import * as myExtension from '../../extension'; 7 | 8 | suite('Extension Test Suite', () => { 9 | vscode.window.showInformationMessage('Start all tests.'); 10 | 11 | test('Sample test', () => { 12 | assert.strictEqual(-1, [1, 2, 3].indexOf(5)); 13 | assert.strictEqual(-1, [1, 2, 3].indexOf(0)); 14 | }); 15 | }); 16 | -------------------------------------------------------------------------------- /snapshot_dbg_extension/src/test/suite/index.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path'; 2 | import * as Mocha from 'mocha'; 3 | import * as glob from 'glob'; 4 | 5 | export function run(): Promise { 6 | // Create the mocha test 7 | const mocha = new Mocha({ 8 | ui: 'tdd', 9 | color: true 10 | }); 11 | 12 | const testsRoot = path.resolve(__dirname, '..'); 13 | 14 | return new Promise((c, e) => { 15 | glob('**/**.test.js', { cwd: testsRoot }, (err, files) => { 16 | if (err) { 17 | return e(err); 18 | } 19 | 20 | // Add files to the test suite 21 | files.forEach(f => mocha.addFile(path.resolve(testsRoot, f))); 22 | 23 | try { 24 | // Run the mocha test 25 | mocha.run(failures => { 26 | if (failures > 0) { 27 | e(new Error(`${failures} tests failed.`)); 28 | } else { 29 | c(); 30 | } 31 | }); 32 | } catch (err) { 33 | console.error(err); 34 | e(err); 35 | } 36 | }); 37 | }); 38 | } 39 | -------------------------------------------------------------------------------- /snapshot_dbg_extension/src/userPreferences.ts: -------------------------------------------------------------------------------- 1 | export interface UserPreferences { 2 | isExpressionsPromptEnabled: boolean; 3 | }; 4 | -------------------------------------------------------------------------------- /snapshot_dbg_extension/src/util.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | import { DebugProtocol } from '@vscode/debugprotocol'; 3 | 4 | /** 5 | * Returns a promise that will resolve in ms milliseconds. 6 | * @param ms 7 | * @returns 8 | */ 9 | export function sleep(ms: number) { 10 | return new Promise((resolve) => { setTimeout(resolve, ms) }); 11 | } 12 | 13 | /** 14 | * Waits ms milliseconds for the promise to resolve, or rejects with a timeout. 15 | * @param ms 16 | * @param promise 17 | * @returns Promise wrapped in a timeout. 18 | */ 19 | export function withTimeout(ms: number, promise: Promise) { 20 | const timeout = new Promise((_, reject) => 21 | setTimeout(() => reject(`Timed out after ${ms} ms.`), ms) 22 | ); 23 | return Promise.race([promise, timeout]); 24 | }; 25 | 26 | // TODO: Plumb this through from outside this file if possible. 27 | // FIXME: Use a path library instead of this nonsense. 28 | function pwd() { 29 | if (vscode.workspace.workspaceFolders !== undefined) { 30 | return vscode.workspace.workspaceFolders[0].uri.fsPath + '/'; 31 | } else { 32 | return ''; 33 | } 34 | } 35 | 36 | export function stripPwd(path: string): string { 37 | const prefix = pwd(); 38 | if (path.startsWith(prefix)) { 39 | return path.substring(prefix.length); 40 | } 41 | return path; 42 | } 43 | 44 | export function addPwd(path: string): string { 45 | const prefix = pwd(); 46 | return `${prefix}${path}`; 47 | } 48 | 49 | export function sourceBreakpointToString(bp: DebugProtocol.SourceBreakpoint): string { 50 | return `${bp.line}\0${bp.condition}\0${bp.logMessage}`; 51 | } 52 | 53 | export function stringToSourceBreakpoint(bp: string): DebugProtocol.SourceBreakpoint { 54 | const parts = bp.split('\0'); 55 | return { 56 | line: parseInt(parts[0]), 57 | ...(parts[1] !== 'undefined' && {condition: parts[1]}), 58 | ...(parts[2] !== 'undefined' && {logMessage: parts[2]}) 59 | }; 60 | } 61 | 62 | -------------------------------------------------------------------------------- /snapshot_dbg_extension/src/whenClauseContextUtil.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | 3 | /** 4 | * We use a custom when clause context for tracking when the Snapshot Debugger 5 | * extension is actually active. The adapter will enable it when it receives an 6 | * attach request, and then disable it when it receives a disonnect request. 7 | * This is then available for use in when clauses in the package.json file. One 8 | * use is for controlling when the icons are displayed in the debug toolbar. 9 | * Without this the Snapshot Debugger icons would be visible in the debug 10 | * toolbar for any debugger in use. With this we can ensure it is only visible 11 | * when the Snapshot Debugger extension is actively being used. 12 | * 13 | * https://code.visualstudio.com/api/references/when-clause-contexts#add-a-custom-when-clause-context 14 | */ 15 | export class IsActiveWhenClauseContext { 16 | public static CONTEXT_NAME: string = 'extension.snapshotdbg.isActive'; 17 | 18 | private static setContext(value: boolean): void { 19 | vscode.commands.executeCommand('setContext', IsActiveWhenClauseContext.CONTEXT_NAME, value); 20 | } 21 | 22 | public static create(): void { 23 | IsActiveWhenClauseContext.setContext(false); 24 | } 25 | 26 | public static enable(): void { 27 | IsActiveWhenClauseContext.setContext(true); 28 | } 29 | 30 | public static disable(): void { 31 | IsActiveWhenClauseContext.setContext(false); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /snapshot_dbg_extension/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "ES2020", 5 | "outDir": "out", 6 | "lib": [ 7 | "ES2020" 8 | ], 9 | "sourceMap": true, 10 | "rootDir": "src", 11 | "strict": true /* enable all strict type-checking options */ 12 | /* Additional Checks */ 13 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ 14 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ 15 | // "noUnusedParameters": true, /* Report errors on unused parameters. */ 16 | } 17 | } 18 | --------------------------------------------------------------------------------