mirror of
https://git.jami.net/savoirfairelinux/jami-client-qt.git
synced 2025-09-10 03:53:23 +02:00
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
This commit is contained in:
parent
cba84f2cb7
commit
4152700848
4 changed files with 290 additions and 13 deletions
|
@ -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:
|
||||
|
|
|
@ -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)
|
||||
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)
|
|
@ -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)
|
|
@ -2,4 +2,5 @@ Flask==3.0.3
|
|||
requests==2.24.0
|
||||
markupsafe==2.1.1
|
||||
itsdangerous==2.1.2
|
||||
werkzeug==3.0.0
|
||||
werkzeug==3.0.0
|
||||
waitress==3.0.2
|
||||
|
|
Loading…
Add table
Reference in a new issue