From 4152700848a26841c716a737d07738ae57d60e74 Mon Sep 17 00:00:00 2001 From: Andreas Traczyk <andreas.traczyk@savoirfairelinux.com> Date: Mon, 13 Jan 2025 14:59:08 -0500 Subject: [PATCH] crash-reporting: improve submission services Provide general improvements to the crash reporting submission services by adding a separate report access server with a simple UI and updating the crashpad submission server to use waitress. - changes crashpad.py to crashpad_submit_server.py - adds report_access_server.py - updates README.md Gitlab: #1454 Change-Id: I4e97f77cf2e2c0bb405064b0187ed3dfc2ee703e --- .../example-submission-servers/README.md | 23 +- ...{crashpad.py => crashpad_submit_server.py} | 25 +- .../report_access_server.py | 252 ++++++++++++++++++ .../requirements.txt | 3 +- 4 files changed, 290 insertions(+), 13 deletions(-) rename extras/crash-reports/example-submission-servers/{crashpad.py => crashpad_submit_server.py} (68%) create mode 100644 extras/crash-reports/example-submission-servers/report_access_server.py diff --git a/extras/crash-reports/example-submission-servers/README.md b/extras/crash-reports/example-submission-servers/README.md index 407c577b..960a2b27 100644 --- a/extras/crash-reports/example-submission-servers/README.md +++ b/extras/crash-reports/example-submission-servers/README.md @@ -2,7 +2,9 @@ ## Overview -This directory contains examples of crash report submission servers. These servers are responsible for receiving crash reports from clients and storing them. The examples are written in Python and use the Flask web framework. +This directory contains an example of a crash report submission server. This server is responsible for receiving crash reports from clients and storing them. The example is written in Python and uses the Flask web framework with Waitress as the WSGI server. It exposes one endpoint for submitting crash reports on the `/submit` path using the POST method on port `8080`. + +It also contains an example of a crash report access server. This server is responsible for displaying the crash reports. It uses port `8081` and provides a simple HTML page that lists crash reports by page. ## Running the examples @@ -11,15 +13,28 @@ To run the examples, you need to have Python 3 installed. You can just use the v ``` python3 -m venv venv source venv/bin/activate -pip install -r requirements.txt +python3 -m pip install -r requirements.txt ``` -After activating the virtual environment, you can should be able to execute the example submission servers. To run the example submission server that uses the Crashpad format, run the following command: + +> ⚠️ On Windows, you need to use `venv\Scripts\activate` instead of `source venv/bin/activate`. + +After activating the virtual environment, you can should be able to execute the example submission server. To run the example submission server that uses the Crashpad format, run the following command: ``` -python crashpad.py +python3 crashpad_submit_server.py ``` +To run a server that displays the crash reports, run the following command: + +``` +python3 report_access_server.py +``` + +> ⚠️ It is recommended to run the report access server in a way that is not publicly accessible. + +Either server can be run on the same machine or on different machines, and each can be run using the `--debug` flag to enable debugging. + ## Metadata The crash report submission servers expect the crash reports to contain a JSON object. The JSON object should contain the following basic metadata: diff --git a/extras/crash-reports/example-submission-servers/crashpad.py b/extras/crash-reports/example-submission-servers/crashpad_submit_server.py similarity index 68% rename from extras/crash-reports/example-submission-servers/crashpad.py rename to extras/crash-reports/example-submission-servers/crashpad_submit_server.py index fed0c45a..6335c03a 100644 --- a/extras/crash-reports/example-submission-servers/crashpad.py +++ b/extras/crash-reports/example-submission-servers/crashpad_submit_server.py @@ -1,10 +1,12 @@ #!/usr/bin/env python3 import os -from flask import Flask, request +from flask import Flask, request, jsonify import json +import argparse app = Flask(__name__) +BASE_PATH = 'crash_reports' @app.route('/submit', methods=['POST']) def submit(): @@ -16,11 +18,10 @@ def submit(): dump_id = file_storage.filename # Create a directory to store the crash reports if it doesn't exist - base_path = 'crash_reports' - if not os.path.exists(base_path): - os.makedirs(base_path) + if not os.path.exists(BASE_PATH): + os.makedirs(BASE_PATH) - filepath = os.path.join(base_path, dump_id) + filepath = os.path.join(BASE_PATH, dump_id) # Attempt to write the file, fail gracefully if it already exists if os.path.exists(filepath): @@ -31,8 +32,7 @@ def submit(): print(f"File saved successfully at {filepath}") # Now save the metadata in {request.form} as separate filename <UID>.info. - # We assume the data is a JSON string. - metadata_filepath = os.path.join(base_path, f"{dump_id}.info") + metadata_filepath = os.path.join(BASE_PATH, f"{dump_id}.info") with open(metadata_filepath, 'w') as f: f.write(str(json.dumps(dict(request.form), indent=4))) else: @@ -48,4 +48,13 @@ def submit(): return 'Internal Server Error', 500 if __name__ == '__main__': - app.run(port=8080, debug=True) \ No newline at end of file + parser = argparse.ArgumentParser(description='Crash report submission server') + parser.add_argument('--debug', action='store_true', help='Run in debug mode') + args = parser.parse_args() + + if args.debug: + app.run(port=8080, debug=True) + else: + from waitress import serve + print("Starting production server on port 8080...") + serve(app, host='0.0.0.0', port=8080) \ No newline at end of file diff --git a/extras/crash-reports/example-submission-servers/report_access_server.py b/extras/crash-reports/example-submission-servers/report_access_server.py new file mode 100644 index 00000000..1a4cbb05 --- /dev/null +++ b/extras/crash-reports/example-submission-servers/report_access_server.py @@ -0,0 +1,252 @@ +#!/usr/bin/env python3 + +import os +from flask import Flask, request, jsonify, render_template_string, send_file +import json +from datetime import datetime +import argparse + +app = Flask(__name__) +BASE_PATH = 'crash_reports' + +@app.route('/', methods=['GET']) +def list_reports(): + try: + if not os.path.exists(BASE_PATH): + return jsonify({"error": "No reports directory found"}), 404 + + # Get page number from query parameters, default to 1 + page = int(request.args.get('page', 1)) + per_page = 10 + + reports = os.listdir(BASE_PATH) + if not reports: + return render_template_string(""" + <h1>Crash Reports</h1> + <p>No crash reports found.</p> + """) + + # Build report pairs with metadata + report_pairs = [] + for report in reports: + if not report.endswith('.info'): + info_file = f"{report}.info" + if info_file in reports: + try: + dump_path = os.path.join(BASE_PATH, report) + timestamp = os.path.getctime(dump_path) + upload_time = datetime.fromtimestamp(timestamp) + + with open(os.path.join(BASE_PATH, info_file), 'r') as f: + metadata = json.load(f) + report_pairs.append({ + 'dump_file': report, + 'info_file': info_file, + 'metadata': metadata, + 'sort_key': f"{metadata.get('client_sha', '')}-{metadata.get('jamicore_sha', '')}", + 'download_name': f"{metadata.get('client_sha', 'unknown')}-{metadata.get('jamicore_sha', 'unknown')}-{metadata.get('platform', 'unknown').replace(' ', '_')}", + 'upload_time': upload_time + }) + except json.JSONDecodeError: + print(f"Error parsing metadata file: {info_file}") + continue + + # Sort reports by upload time (most recent first), then by SHA + report_pairs.sort(key=lambda x: (-x['upload_time'].timestamp(), x['sort_key'])) + + # Calculate pagination values + total_reports = len(report_pairs) + total_pages = (total_reports + per_page - 1) // per_page + page = min(max(1, page), total_pages or 1) # Handle case when total_pages is 0 + start_idx = (page - 1) * per_page + end_idx = start_idx + per_page + + # Get current page's reports + current_page_reports = report_pairs[start_idx:end_idx] + + return render_template_string(""" + <!DOCTYPE html> + <html lang="en"> + <head> + <meta charset="UTF-8"> + <title>Crash Reports</title> + <style> + body { font-family: Arial, sans-serif; margin: 2em; } + .header { + margin-bottom: 2em; + } + .header h1 { + margin-bottom: 0.5em; + } + .report-list { list-style: none; padding: 0; } + .report-item { margin: 1em 0; padding: 1em; border: 1px solid #ddd; border-radius: 4px; } + .download-link { + display: inline-block; + padding: 8px 16px; + background-color: #0066cc; + color: white; + text-decoration: none; + border-radius: 4px; + margin: 8px 0; + } + .download-link:hover { background-color: #0052a3; } + .metadata-table { + border-collapse: collapse; + width: 100%; + margin: 8px 0; + } + .metadata-table td { + padding: 4px 8px; + border-bottom: 1px solid #ddd; + } + .metadata-table td:first-child { + font-weight: bold; + width: 150px; + } + .upload-time { + color: #666; + font-size: 0.9em; + margin-bottom: 8px; + } + .pagination { + margin: 1em 0; + text-align: center; + } + .pagination a, .pagination span { + display: inline-block; + padding: 8px 16px; + margin: 0 4px; + border: 1px solid #ddd; + border-radius: 4px; + text-decoration: none; + color: #0066cc; + } + .pagination .current { + background-color: #0066cc; + color: white; + border-color: #0066cc; + } + .pagination a:hover { + background-color: #f5f5f5; + } + .pagination-info { + text-align: center; + color: #666; + margin-bottom: 10px; + } + </style> + </head> + <body> + <div class="header"> + <h1>Crash Reports</h1> + <div class="pagination-info"> + Showing {{ start_idx + 1 }}-{{ [end_idx, total_reports] | min }} of {{ total_reports }} reports + </div> + <div class="pagination"> + {% if page > 1 %} + <a href="{{ url_for('list_reports', page=1) }}">« First</a> + <a href="{{ url_for('list_reports', page=page-1) }}">‹ Previous</a> + {% endif %} + + {% for p in range([1, page-2] | max, [total_pages + 1, page + 3] | min) %} + {% if p == page %} + <span class="current">{{ p }}</span> + {% else %} + <a href="{{ url_for('list_reports', page=p) }}">{{ p }}</a> + {% endif %} + {% endfor %} + + {% if page < total_pages %} + <a href="{{ url_for('list_reports', page=page+1) }}">Next ›</a> + <a href="{{ url_for('list_reports', page=total_pages) }}">Last »</a> + {% endif %} + </div> + </div> + + <div class="report-list"> + {% for report in reports %} + <div class="report-item"> + <h3>Report: {{ report['sort_key'] }}</h3> + <div class="upload-time"> + Uploaded: {{ report['upload_time'].strftime('%Y-%m-%d %H:%M:%S') }} + </div> + <table class="metadata-table"> + <tr> + <td>Platform:</td> + <td>{{ report['metadata']['platform'] }}</td> + </tr> + <tr> + <td>Client SHA:</td> + <td>{{ report['metadata']['client_sha'] }}</td> + </tr> + <tr> + <td>Jami Core SHA:</td> + <td>{{ report['metadata']['jamicore_sha'] }}</td> + </tr> + <tr> + <td>Build ID:</td> + <td>{{ report['metadata']['build_id'] }}</td> + </tr> + <tr> + <td>GUID:</td> + <td>{{ report['metadata']['guid'] }}</td> + </tr> + </table> + <a class="download-link" href="{{ url_for('download_report_bundle', dump_file=report['dump_file'], info_file=report['info_file'], download_name=report['download_name']) }}"> + Download Report Bundle + </a> + </div> + {% endfor %} + </div> + </body> + </html> + """, reports=current_page_reports, page=page, total_pages=total_pages, + start_idx=start_idx, end_idx=end_idx, total_reports=total_reports) + + except Exception as e: + print(f"Error listing reports: {e}") + return 'Internal Server Error', 500 + +@app.route('/download-bundle/<path:dump_file>/<path:info_file>/<path:download_name>') +def download_report_bundle(dump_file, info_file, download_name): + try: + import zipfile + from io import BytesIO + + # Create a memory file for the zip + memory_file = BytesIO() + + # Create the zip file + with zipfile.ZipFile(memory_file, 'w', zipfile.ZIP_DEFLATED) as zf: + # Add the dump file + dump_path = os.path.join(BASE_PATH, dump_file) + zf.write(dump_path, f"{download_name}.dmp") + + # Add the info file + info_path = os.path.join(BASE_PATH, info_file) + zf.write(info_path, f"{download_name}.info") + + # Seek to the beginning of the memory file + memory_file.seek(0) + + return send_file( + memory_file, + mimetype='application/zip', + as_attachment=True, + download_name=f"{download_name}.zip" + ) + except Exception as e: + print(f"Error creating zip bundle: {e}") + return 'Internal Server Error', 500 + +if __name__ == '__main__': + parser = argparse.ArgumentParser(description='Crash reports viewing server') + parser.add_argument('--debug', action='store_true', help='Run in debug mode') + args = parser.parse_args() + + if args.debug: + app.run(port=8081, debug=True) + else: + from waitress import serve + print("Starting production server on port 8081...") + serve(app, host='0.0.0.0', port=8081) \ No newline at end of file diff --git a/extras/crash-reports/example-submission-servers/requirements.txt b/extras/crash-reports/example-submission-servers/requirements.txt index dce4dce8..d823d19b 100644 --- a/extras/crash-reports/example-submission-servers/requirements.txt +++ b/extras/crash-reports/example-submission-servers/requirements.txt @@ -2,4 +2,5 @@ Flask==3.0.3 requests==2.24.0 markupsafe==2.1.1 itsdangerous==2.1.2 -werkzeug==3.0.0 \ No newline at end of file +werkzeug==3.0.0 +waitress==3.0.2 -- GitLab