mirror of
https://github.com/NohamR/RMHook.git
synced 2026-04-08 07:59:58 +00:00
Add native HTTP server, CLI scripts and docs
Implement a native macOS HTTP server for RMHook and wire it into the app. Adds HttpServer.h/.mm (CFSocket-based) with start/stop/isRunning APIs, broadcasts incoming POST /exportFile and /documentAccepted requests to the QML MessageBroker, and starts the server on port 8080 from reMarkable.m. Update CMakeLists to include HttpServer.mm and link Qt Qml when building qmlrebuild mode. Add documentation (docs/HTTP_SERVER.md) and QML snippets for MessageBroker integration, plus Python helper scripts (scripts/http_server.py and scripts/test_http_server.py) for invoking the endpoints. Also add small MessageBroker examples (src/utils/mb.m, src/utils/mb.qml) and update ResourceUtils to include additional QML resources.
This commit is contained in:
@@ -114,13 +114,24 @@ endif()
|
||||
if(BUILD_MODE_QMLREBUILD)
|
||||
target_compile_definitions(reMarkable PRIVATE BUILD_MODE_QMLREBUILD=1)
|
||||
|
||||
# Enable Qt MOC for MessageBroker
|
||||
# Enable Qt MOC for MessageBroker (HttpServer is pure Obj-C/Foundation, no MOC needed)
|
||||
set_target_properties(reMarkable PROPERTIES AUTOMOC ON)
|
||||
|
||||
# Add MessageBroker source (needs MOC processing)
|
||||
# Add MessageBroker (needs MOC) and HttpServer (native macOS)
|
||||
target_sources(reMarkable PRIVATE
|
||||
${PROJECT_ROOT_DIR}/src/utils/MessageBroker.mm
|
||||
${PROJECT_ROOT_DIR}/src/utils/HttpServer.mm
|
||||
)
|
||||
|
||||
find_package(Qt6 COMPONENTS Qml QUIET)
|
||||
if(Qt6Qml_FOUND)
|
||||
target_link_libraries(reMarkable PRIVATE Qt6::Qml)
|
||||
else()
|
||||
find_package(Qt5 COMPONENTS Qml QUIET)
|
||||
if(Qt5Qml_FOUND)
|
||||
target_link_libraries(reMarkable PRIVATE Qt5::Qml)
|
||||
endif()
|
||||
endif()
|
||||
message(STATUS "Build mode: qmlrebuild (resource hooking)")
|
||||
endif()
|
||||
|
||||
|
||||
54
docs/DocumentAccepted_MessageBroker_snippet.qml
Normal file
54
docs/DocumentAccepted_MessageBroker_snippet.qml
Normal file
@@ -0,0 +1,54 @@
|
||||
// Add this MessageBroker to a QML component that has access to PlatformHelpers
|
||||
// This listens for documentAccepted signals from the HTTP server
|
||||
// and calls PlatformHelpers.documentAccepted()
|
||||
|
||||
import net.noham.MessageBroker
|
||||
|
||||
MessageBroker {
|
||||
id: documentAcceptedBroker
|
||||
listeningFor: ["documentAccepted"]
|
||||
|
||||
onSignalReceived: (signal, message) => {
|
||||
console.log("[DocumentAccepted.MessageBroker] Received signal:", signal);
|
||||
console.log("[DocumentAccepted.MessageBroker] Message data:", message);
|
||||
|
||||
try {
|
||||
// Parse JSON message from HTTP server
|
||||
const data = JSON.parse(message);
|
||||
console.log("[DocumentAccepted.MessageBroker] Parsed request:", JSON.stringify(data));
|
||||
|
||||
// Extract parameters with defaults
|
||||
const url = data.url || "";
|
||||
const password = data.password || "";
|
||||
const directoryId = data.directoryId || "";
|
||||
const flag1 = data.flag1 !== undefined ? data.flag1 : false;
|
||||
const flag2 = data.flag2 !== undefined ? data.flag2 : false;
|
||||
|
||||
console.log("[DocumentAccepted.MessageBroker] Parameters:");
|
||||
console.log("[DocumentAccepted.MessageBroker] url:", url);
|
||||
console.log("[DocumentAccepted.MessageBroker] password:", password ? "(set)" : "(empty)");
|
||||
console.log("[DocumentAccepted.MessageBroker] directoryId:", directoryId);
|
||||
console.log("[DocumentAccepted.MessageBroker] flag1:", flag1);
|
||||
console.log("[DocumentAccepted.MessageBroker] flag2:", flag2);
|
||||
|
||||
// Validate required parameters
|
||||
if (!url) {
|
||||
console.error("[DocumentAccepted.MessageBroker] ERROR: Missing 'url' parameter");
|
||||
return;
|
||||
}
|
||||
if (!directoryId) {
|
||||
console.error("[DocumentAccepted.MessageBroker] ERROR: Missing 'directoryId' parameter");
|
||||
return;
|
||||
}
|
||||
|
||||
// Call PlatformHelpers.documentAccepted
|
||||
console.log("[DocumentAccepted.MessageBroker] Calling PlatformHelpers.documentAccepted...");
|
||||
PlatformHelpers.documentAccepted(url, password, directoryId, flag1, flag2);
|
||||
console.log("[DocumentAccepted.MessageBroker] Document accepted successfully");
|
||||
|
||||
} catch (error) {
|
||||
console.error("[DocumentAccepted.MessageBroker] ERROR parsing request:", error);
|
||||
console.error("[DocumentAccepted.MessageBroker] Message was:", message);
|
||||
}
|
||||
}
|
||||
}
|
||||
67
docs/ExportDialog_MessageBroker_snippet.qml
Normal file
67
docs/ExportDialog_MessageBroker_snippet.qml
Normal file
@@ -0,0 +1,67 @@
|
||||
// Add this MessageBroker to ExportDialog.qml after the PopupDialog definition
|
||||
// This should be added near the top of the component, after property definitions
|
||||
|
||||
import net.noham.MessageBroker
|
||||
|
||||
// ... existing properties ...
|
||||
|
||||
// MessageBroker for HTTP server export requests
|
||||
MessageBroker {
|
||||
id: exportBroker
|
||||
listeningFor: ["exportFile"]
|
||||
|
||||
onSignalReceived: (signal, message) => {
|
||||
console.log("[ExportDialog.MessageBroker] Received signal:", signal);
|
||||
console.log("[ExportDialog.MessageBroker] Message data:", message);
|
||||
|
||||
try {
|
||||
// Parse JSON message from HTTP server
|
||||
const data = JSON.parse(message);
|
||||
console.log("[ExportDialog.MessageBroker] Parsed export request:", JSON.stringify(data));
|
||||
|
||||
// Extract parameters
|
||||
const target = data.target || "";
|
||||
const documentId = data.id || data.documentId || "";
|
||||
const format = data.format !== undefined ? data.format : PlatformHelpers.ExportPdf;
|
||||
const password = data.password || "";
|
||||
const keepPassword = data.keepPassword !== undefined ? data.keepPassword : true;
|
||||
const grayscale = data.grayscale !== undefined ? data.grayscale : false;
|
||||
const pageSelection = data.pageSelection || [];
|
||||
|
||||
console.log("[ExportDialog.MessageBroker] Export parameters:");
|
||||
console.log("[ExportDialog.MessageBroker] target:", target);
|
||||
console.log("[ExportDialog.MessageBroker] documentId:", documentId);
|
||||
console.log("[ExportDialog.MessageBroker] format:", format);
|
||||
console.log("[ExportDialog.MessageBroker] keepPassword:", keepPassword);
|
||||
console.log("[ExportDialog.MessageBroker] grayscale:", grayscale);
|
||||
console.log("[ExportDialog.MessageBroker] pageSelection:", JSON.stringify(pageSelection));
|
||||
|
||||
// Validate required parameters
|
||||
if (!target) {
|
||||
console.error("[ExportDialog.MessageBroker] ERROR: Missing 'target' parameter");
|
||||
return;
|
||||
}
|
||||
if (!documentId) {
|
||||
console.error("[ExportDialog.MessageBroker] ERROR: Missing 'id' or 'documentId' parameter");
|
||||
return;
|
||||
}
|
||||
|
||||
// Call PlatformHelpers.exportFile
|
||||
console.log("[ExportDialog.MessageBroker] Calling PlatformHelpers.exportFile...");
|
||||
|
||||
if (pageSelection && pageSelection.length > 0) {
|
||||
console.log("[ExportDialog.MessageBroker] Exporting with page selection");
|
||||
PlatformHelpers.exportFile(target, documentId, format, password, keepPassword, grayscale, pageSelection);
|
||||
} else {
|
||||
console.log("[ExportDialog.MessageBroker] Exporting full document");
|
||||
PlatformHelpers.exportFile(target, documentId, format, password, keepPassword, grayscale);
|
||||
}
|
||||
|
||||
console.log("[ExportDialog.MessageBroker] Export completed successfully");
|
||||
|
||||
} catch (error) {
|
||||
console.error("[ExportDialog.MessageBroker] ERROR parsing export request:", error);
|
||||
console.error("[ExportDialog.MessageBroker] Message was:", message);
|
||||
}
|
||||
}
|
||||
}
|
||||
293
docs/HTTP_SERVER.md
Normal file
293
docs/HTTP_SERVER.md
Normal file
@@ -0,0 +1,293 @@
|
||||
# HTTP Server for Export Requests
|
||||
|
||||
The RMHook dylib includes an HTTP server that accepts export requests and forwards them to the reMarkable application via MessageBroker.
|
||||
|
||||
## Server Details
|
||||
|
||||
- **Host**: `localhost`
|
||||
- **Port**: `8080`
|
||||
- **Base URL**: `http://localhost:8080`
|
||||
|
||||
## Endpoints
|
||||
|
||||
### `POST /exportFile`
|
||||
|
||||
Trigger a document export from the reMarkable application.
|
||||
|
||||
**Request Body** (JSON):
|
||||
```json
|
||||
{
|
||||
"target": "file:///Users/username/Desktop/output.pdf",
|
||||
"id": "document-uuid-here",
|
||||
"format": 0,
|
||||
"password": "",
|
||||
"keepPassword": true,
|
||||
"grayscale": false,
|
||||
"pageSelection": []
|
||||
}
|
||||
```
|
||||
|
||||
**Parameters**:
|
||||
- `target` (string, required): File path or folder URL for the export. Use `file://` prefix for local paths.
|
||||
- `id` or `documentId` (string, required): The UUID of the document to export.
|
||||
- `format` (integer, optional): Export format. Default: `0` (PDF)
|
||||
- `0`: PDF
|
||||
- `1`: PNG
|
||||
- `2`: SVG
|
||||
- `3`: RmBundle
|
||||
- `4`: RmHtml
|
||||
- `password` (string, optional): Password for password-protected documents. Default: `""`
|
||||
- `keepPassword` (boolean, optional): Whether to keep password protection on PDF exports. Default: `true`
|
||||
- `grayscale` (boolean, optional): Export with grayscale pens. Default: `false`
|
||||
- `pageSelection` (array, optional): Array of page indices to export. If empty or omitted, exports all pages. Example: `[0, 1, 2]`
|
||||
|
||||
**Response**:
|
||||
```json
|
||||
{
|
||||
"status": "success",
|
||||
"message": "Export request sent to application"
|
||||
}
|
||||
```
|
||||
|
||||
**Error Response**:
|
||||
```json
|
||||
{
|
||||
"error": "Error description"
|
||||
}
|
||||
```
|
||||
|
||||
### `POST /documentAccepted`
|
||||
|
||||
Import/accept a document into the reMarkable application.
|
||||
|
||||
**Request Body** (JSON):
|
||||
```json
|
||||
{
|
||||
"url": "file:///Users/username/Desktop/test.pdf",
|
||||
"password": "",
|
||||
"directoryId": "2166c19d-d2cc-456c-9f0e-49482031092a",
|
||||
"flag1": false,
|
||||
"flag2": false
|
||||
}
|
||||
```
|
||||
|
||||
**Parameters**:
|
||||
- `url` (string, required): File URL to import. Use `file://` prefix for local paths.
|
||||
- `password` (string, optional): Password for password-protected documents. Default: `""`
|
||||
- `directoryId` (string, required): The UUID of the target directory/folder where the document should be imported.
|
||||
- `flag1` (boolean, optional): Purpose unclear. Default: `false`
|
||||
- `flag2` (boolean, optional): Purpose unclear. Default: `false`
|
||||
|
||||
**Response**:
|
||||
```json
|
||||
{
|
||||
"status": "success",
|
||||
"message": "Document accepted request sent to application"
|
||||
}
|
||||
```
|
||||
|
||||
**Error Response**:
|
||||
```json
|
||||
{
|
||||
"error": "Error description"
|
||||
}
|
||||
```
|
||||
|
||||
### `GET /health`
|
||||
|
||||
Health check endpoint.
|
||||
|
||||
**Response**:
|
||||
```json
|
||||
{
|
||||
"status": "ok",
|
||||
"service": "RMHook HTTP Server"
|
||||
}
|
||||
```
|
||||
|
||||
## Example Requests
|
||||
|
||||
### Export a document to PDF
|
||||
|
||||
```bash
|
||||
curl -X POST http://localhost:8080/exportFile \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"target": "file:///Users/noham/Desktop/export.pdf",
|
||||
"id": "12345678-1234-1234-1234-123456789abc",
|
||||
"format": 0,
|
||||
"grayscale": false,
|
||||
"keepPassword": true
|
||||
}'
|
||||
```
|
||||
|
||||
### Export specific pages as PNG
|
||||
|
||||
```bash
|
||||
curl -X POST http://localhost:8080/exportFile \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"target": "file:///Users/noham/Desktop/pages",
|
||||
"id": "12345678-1234-1234-1234-123456789abc",
|
||||
"format": 1,
|
||||
"pageSelection": [0, 1, 2],
|
||||
"grayscale": true
|
||||
}'
|
||||
```
|
||||
|
||||
### Export to RmBundle format
|
||||
|
||||
```bash
|
||||
curl -X POST http://localhost:8080/exportFile \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"target": "file:///Users/noham/Desktop/MyDocument",
|
||||
"id": "12345678-1234-1234-1234-123456789abc",
|
||||
"format": 3
|
||||
}'
|
||||
```
|
||||
|
||||
### Import/Accept a document
|
||||
|
||||
```bash
|
||||
curl -X POST http://localhost:8080/documentAccepted \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"url": "file:///Users/noham/Desktop/test.pdf",
|
||||
"password": "",
|
||||
"directoryId": "2166c19d-d2cc-456c-9f0e-49482031092a",
|
||||
"flag1": false,
|
||||
"flag2": false
|
||||
}'
|
||||
```
|
||||
|
||||
### Python Example - Export
|
||||
|
||||
```python
|
||||
import requests
|
||||
import json
|
||||
|
||||
# Export configuration
|
||||
export_data = {
|
||||
"target": "file:///Users/noham/Desktop/output.pdf",
|
||||
"id": "12345678-1234-1234-1234-123456789abc",
|
||||
"format": 0, # PDF
|
||||
"grayscale": False,
|
||||
"keepPassword": True
|
||||
}
|
||||
|
||||
# Send request
|
||||
response = requests.post(
|
||||
"http://localhost:8080/exportFile",
|
||||
json=export_data
|
||||
)
|
||||
|
||||
print(f"Status: {response.status_code}")
|
||||
print(f"Response: {response.json()}")
|
||||
```
|
||||
|
||||
### Python Example - Import Document
|
||||
|
||||
```python
|
||||
import requests
|
||||
|
||||
# Import configuration
|
||||
import_data = {
|
||||
"url": "file:///Users/noham/Desktop/test.pdf",
|
||||
"password": "",
|
||||
"directoryId": "2166c19d-d2cc-456c-9f0e-49482031092a",
|
||||
"flag1": False,
|
||||
"flag2": False
|
||||
}
|
||||
|
||||
# Send request
|
||||
response = requests.post(
|
||||
"http://localhost:8080/documentAccepted",
|
||||
json=import_data
|
||||
)
|
||||
|
||||
print(f"Status: {response.status_code}")
|
||||
print(f"Response: {response.json()}")
|
||||
```
|
||||
|
||||
### JavaScript Example - Export
|
||||
|
||||
```javascript
|
||||
// Export configuration
|
||||
const exportData = {
|
||||
target: "file:///Users/noham/Desktop/output.pdf",
|
||||
id: "12345678-1234-1234-1234-123456789abc",
|
||||
format: 0, // PDF
|
||||
grayscale: false,
|
||||
keepPassword: true
|
||||
};
|
||||
|
||||
// Send request
|
||||
fetch("http://localhost:8080/exportFile", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
body: JSON.stringify(exportData)
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => console.log("Success:", data))
|
||||
.catch(error => console.error("Error:", error));
|
||||
```
|
||||
|
||||
### JavaScript Example - Import Document
|
||||
|
||||
```javascript
|
||||
// Import configuration
|
||||
const importData = {
|
||||
url: "file:///Users/noham/Desktop/test.pdf",
|
||||
password: "",
|
||||
directoryId: "2166c19d-d2cc-456c-9f0e-49482031092a",
|
||||
flag1: false,
|
||||
flag2: false
|
||||
};
|
||||
|
||||
// Send request
|
||||
fetch("http://localhost:8080/documentAccepted", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
body: JSON.stringify(importData)
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => console.log("Success:", data))
|
||||
.catch(error => console.error("Error:", error));
|
||||
```
|
||||
|
||||
## Integration with QML
|
||||
|
||||
### Export Dialog Integration
|
||||
|
||||
Add the MessageBroker snippet from `docs/ExportDialog_MessageBroker_snippet.qml` to your ExportDialog.qml replacement file. This will enable the QML side to receive export requests from the HTTP server.
|
||||
|
||||
The MessageBroker listens for "exportFile" signals and automatically calls `PlatformHelpers.exportFile()` with the provided parameters.
|
||||
|
||||
### Document Import Integration
|
||||
|
||||
Add the MessageBroker snippet from `docs/DocumentAccepted_MessageBroker_snippet.qml` to a QML component (such as GeneralSettings.qml) that has access to PlatformHelpers. This will enable the QML side to receive document import requests from the HTTP server.
|
||||
|
||||
The MessageBroker listens for "documentAccepted" signals and automatically calls `PlatformHelpers.documentAccepted()` with the provided parameters.
|
||||
|
||||
## Document ID and Directory ID Discovery
|
||||
|
||||
To find document and directory IDs, you can:
|
||||
|
||||
1. Check the reMarkable application logs when opening documents or folders
|
||||
2. Use the reMarkable Cloud API
|
||||
3. Access the local database at `~/Library/Application Support/remarkable/desktop-app/`
|
||||
4. For the root directory ID, check the logs when navigating to "My Files"
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
- Ensure the HTTP server started successfully by checking the logs: `2025-12-08 17:32:22.288 reMarkable[19574:1316287] [HttpServer] HTTP server started successfully on http://localhost:8080`
|
||||
- Test the health endpoint: `curl http://localhost:8080/health`
|
||||
- Check the Console.app for detailed logging from the MessageBroker and HttpServer
|
||||
- Verify the document ID and directory ID are correct UUIDs
|
||||
- Ensure the target/url path is accessible and uses the `file://` prefix
|
||||
- For imports, verify the target directory exists and is accessible
|
||||
200
scripts/http_server.py
Normal file
200
scripts/http_server.py
Normal file
@@ -0,0 +1,200 @@
|
||||
#!/usr/bin/env python3
|
||||
import requests
|
||||
import json
|
||||
import sys
|
||||
import argparse
|
||||
|
||||
BASE_URL = "http://localhost:8080"
|
||||
|
||||
def export_document(document_id, target_path, format_type=0, grayscale=False,
|
||||
keep_password=True, password="", page_selection=None):
|
||||
"""
|
||||
Export a document via HTTP API
|
||||
|
||||
Args:
|
||||
document_id: UUID of the document to export
|
||||
target_path: Target path for the export (use file:// prefix)
|
||||
format_type: Export format (0=PDF, 1=PNG, 2=SVG, 3=RmBundle, 4=RmHtml)
|
||||
grayscale: Export with grayscale pens
|
||||
keep_password: Keep password protection (for PDFs)
|
||||
password: Password for protected documents
|
||||
page_selection: List of page indices to export (None = all pages)
|
||||
"""
|
||||
print(f"\nExporting document {document_id}...")
|
||||
print(f"Target: {target_path}")
|
||||
print(f"Format: {format_type}")
|
||||
|
||||
data = {
|
||||
"target": target_path,
|
||||
"id": document_id,
|
||||
"format": format_type,
|
||||
"grayscale": grayscale,
|
||||
"keepPassword": keep_password,
|
||||
"password": password
|
||||
}
|
||||
|
||||
if page_selection:
|
||||
data["pageSelection"] = page_selection
|
||||
print(f"Pages: {page_selection}")
|
||||
|
||||
try:
|
||||
response = requests.post(
|
||||
f"{BASE_URL}/exportFile",
|
||||
json=data,
|
||||
timeout=10
|
||||
)
|
||||
print(f"Status: {response.status_code}")
|
||||
print(f"Response: {json.dumps(response.json(), indent=2)}")
|
||||
return response.status_code == 200
|
||||
except Exception as e:
|
||||
print(f"Error: {e}")
|
||||
return False
|
||||
|
||||
def import_document(file_url, directory_id, password="", flag1=False, flag2=False):
|
||||
"""
|
||||
Import a document via HTTP API
|
||||
|
||||
Args:
|
||||
file_url: File URL to import (use file:// prefix)
|
||||
directory_id: UUID of the target directory
|
||||
password: Password for protected documents (optional)
|
||||
flag1: Additional flag parameter
|
||||
flag2: Additional flag parameter
|
||||
"""
|
||||
print(f"\nImporting document from {file_url}...")
|
||||
print(f"Target directory: {directory_id}")
|
||||
|
||||
data = {
|
||||
"url": file_url,
|
||||
"password": password,
|
||||
"directoryId": directory_id,
|
||||
"flag1": flag1,
|
||||
"flag2": flag2
|
||||
}
|
||||
|
||||
try:
|
||||
response = requests.post(
|
||||
f"{BASE_URL}/documentAccepted",
|
||||
json=data,
|
||||
timeout=10
|
||||
)
|
||||
print(f"Status: {response.status_code}")
|
||||
print(f"Response: {json.dumps(response.json(), indent=2)}")
|
||||
return response.status_code == 200
|
||||
except Exception as e:
|
||||
print(f"Error: {e}")
|
||||
return False
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
description='reMarkable HTTP Server API Client',
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
epilog='''
|
||||
Examples:
|
||||
Export a document as PDF:
|
||||
%(prog)s export 12345678-1234-1234-1234-123456789abc file:///Users/noham/Desktop/test.pdf
|
||||
|
||||
Export as PNG with grayscale:
|
||||
%(prog)s export <doc-id> <target> --format 1 --grayscale
|
||||
|
||||
Export specific pages:
|
||||
%(prog)s export <doc-id> <target> --pages 0 1 2
|
||||
|
||||
Import a document:
|
||||
%(prog)s import file:///Users/noham/Desktop/test.pdf 2166c19d-d2cc-456c-9f0e-49482031092a
|
||||
|
||||
Import with password:
|
||||
%(prog)s import <file-url> <directory-id> --password mypassword
|
||||
'''
|
||||
)
|
||||
|
||||
subparsers = parser.add_subparsers(dest='command', help='Available commands')
|
||||
|
||||
# Export command
|
||||
export_parser = subparsers.add_parser('export', help='Export a document')
|
||||
export_parser.add_argument('document_id', help='UUID of the document to export')
|
||||
export_parser.add_argument('target_path', help='Target path for export (use file:// prefix)')
|
||||
export_parser.add_argument(
|
||||
'--format', '-f',
|
||||
type=int,
|
||||
default=0,
|
||||
choices=[0, 1, 2, 3, 4],
|
||||
help='Export format: 0=PDF, 1=PNG, 2=SVG, 3=RmBundle, 4=RmHtml (default: 0)'
|
||||
)
|
||||
export_parser.add_argument(
|
||||
'--grayscale', '-g',
|
||||
action='store_true',
|
||||
help='Export with grayscale pens'
|
||||
)
|
||||
export_parser.add_argument(
|
||||
'--no-keep-password',
|
||||
action='store_true',
|
||||
help='Do not keep password protection (for PDFs)'
|
||||
)
|
||||
export_parser.add_argument(
|
||||
'--password', '-p',
|
||||
default='',
|
||||
help='Password for protected documents'
|
||||
)
|
||||
export_parser.add_argument(
|
||||
'--pages',
|
||||
type=int,
|
||||
nargs='+',
|
||||
help='List of page indices to export (default: all pages)'
|
||||
)
|
||||
|
||||
# Import command
|
||||
import_parser = subparsers.add_parser('import', help='Import a document')
|
||||
import_parser.add_argument('file_url', help='File URL to import (use file:// prefix)')
|
||||
import_parser.add_argument('directory_id', help='UUID of the target directory')
|
||||
import_parser.add_argument(
|
||||
'--password', '-p',
|
||||
default='',
|
||||
help='Password for protected documents'
|
||||
)
|
||||
import_parser.add_argument(
|
||||
'--flag1',
|
||||
action='store_true',
|
||||
help='Additional flag parameter'
|
||||
)
|
||||
import_parser.add_argument(
|
||||
'--flag2',
|
||||
action='store_true',
|
||||
help='Additional flag parameter'
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.command == 'export':
|
||||
success = export_document(
|
||||
document_id=args.document_id,
|
||||
target_path=args.target_path,
|
||||
format_type=args.format,
|
||||
grayscale=args.grayscale,
|
||||
keep_password=not args.no_keep_password,
|
||||
password=args.password,
|
||||
page_selection=args.pages
|
||||
)
|
||||
if success:
|
||||
print("\n✅ Export request sent successfully!")
|
||||
else:
|
||||
print("\n❌ Export request failed!")
|
||||
sys.exit(1)
|
||||
elif args.command == 'import':
|
||||
success = import_document(
|
||||
file_url=args.file_url,
|
||||
directory_id=args.directory_id,
|
||||
password=args.password,
|
||||
flag1=args.flag1,
|
||||
flag2=args.flag2
|
||||
)
|
||||
if success:
|
||||
print("\n✅ Import request sent successfully!")
|
||||
else:
|
||||
print("\n❌ Import request failed!")
|
||||
sys.exit(1)
|
||||
else:
|
||||
parser.print_help()
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
165
scripts/test_http_server.py
Normal file
165
scripts/test_http_server.py
Normal file
@@ -0,0 +1,165 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Test script for RMHook HTTP Server
|
||||
Demonstrates how to trigger exports and imports via HTTP API
|
||||
"""
|
||||
|
||||
import requests
|
||||
import json
|
||||
import sys
|
||||
|
||||
BASE_URL = "http://localhost:8080"
|
||||
|
||||
def test_health():
|
||||
"""Test the health endpoint"""
|
||||
print("Testing /health endpoint...")
|
||||
try:
|
||||
response = requests.get(f"{BASE_URL}/health", timeout=5)
|
||||
print(f"Status: {response.status_code}")
|
||||
print(f"Response: {json.dumps(response.json(), indent=2)}")
|
||||
return response.status_code == 200
|
||||
except Exception as e:
|
||||
print(f"Error: {e}")
|
||||
return False
|
||||
|
||||
def export_document(document_id, target_path, format_type=0, grayscale=False,
|
||||
keep_password=True, password="", page_selection=None):
|
||||
"""
|
||||
Export a document via HTTP API
|
||||
|
||||
Args:
|
||||
document_id: UUID of the document to export
|
||||
target_path: Target path for the export (use file:// prefix)
|
||||
format_type: Export format (0=PDF, 1=PNG, 2=SVG, 3=RmBundle, 4=RmHtml)
|
||||
grayscale: Export with grayscale pens
|
||||
keep_password: Keep password protection (for PDFs)
|
||||
password: Password for protected documents
|
||||
page_selection: List of page indices to export (None = all pages)
|
||||
"""
|
||||
print(f"\nExporting document {document_id}...")
|
||||
print(f"Target: {target_path}")
|
||||
print(f"Format: {format_type}")
|
||||
|
||||
data = {
|
||||
"target": target_path,
|
||||
"id": document_id,
|
||||
"format": format_type,
|
||||
"grayscale": grayscale,
|
||||
"keepPassword": keep_password,
|
||||
"password": password
|
||||
}
|
||||
|
||||
if page_selection:
|
||||
data["pageSelection"] = page_selection
|
||||
print(f"Pages: {page_selection}")
|
||||
|
||||
try:
|
||||
response = requests.post(
|
||||
f"{BASE_URL}/exportFile",
|
||||
json=data,
|
||||
timeout=10
|
||||
)
|
||||
print(f"Status: {response.status_code}")
|
||||
print(f"Response: {json.dumps(response.json(), indent=2)}")
|
||||
return response.status_code == 200
|
||||
except Exception as e:
|
||||
print(f"Error: {e}")
|
||||
return False
|
||||
|
||||
def import_document(file_url, directory_id, password=""):
|
||||
"""
|
||||
Import a document via HTTP API
|
||||
|
||||
Args:
|
||||
file_url: File URL to import (use file:// prefix)
|
||||
directory_id: UUID of the target directory
|
||||
password: Password for protected documents (optional)
|
||||
"""
|
||||
print(f"\nImporting document from {file_url}...")
|
||||
print(f"Target directory: {directory_id}")
|
||||
|
||||
data = {
|
||||
"url": file_url,
|
||||
"password": password,
|
||||
"directoryId": directory_id,
|
||||
"flag1": False,
|
||||
"flag2": False
|
||||
}
|
||||
|
||||
try:
|
||||
response = requests.post(
|
||||
f"{BASE_URL}/documentAccepted",
|
||||
json=data,
|
||||
timeout=10
|
||||
)
|
||||
print(f"Status: {response.status_code}")
|
||||
print(f"Response: {json.dumps(response.json(), indent=2)}")
|
||||
return response.status_code == 200
|
||||
except Exception as e:
|
||||
print(f"Error: {e}")
|
||||
return False
|
||||
|
||||
def main():
|
||||
print("=" * 60)
|
||||
print("RMHook HTTP Server Test Script")
|
||||
print("=" * 60)
|
||||
|
||||
# Test health endpoint
|
||||
if not test_health():
|
||||
print("\n❌ Health check failed. Is the server running?")
|
||||
print("Make sure reMarkable app is running with the dylib injected.")
|
||||
sys.exit(1)
|
||||
|
||||
print("\n✅ Server is running!")
|
||||
|
||||
# Command line interface
|
||||
if len(sys.argv) < 2:
|
||||
print("\n" + "=" * 60)
|
||||
print("Usage Examples")
|
||||
print("=" * 60)
|
||||
print("\n1. Export a document:")
|
||||
print(' python3 test_http_server.py export <doc-id> <target-path> [format] [grayscale]')
|
||||
print("\n Example:")
|
||||
print(' python3 test_http_server.py export "abc-123" "file:///Users/noham/Desktop/test.pdf" 0 false')
|
||||
|
||||
print("\n2. Import a document:")
|
||||
print(' python3 test_http_server.py import <file-url> <directory-id>')
|
||||
print("\n Example:")
|
||||
print(' python3 test_http_server.py import "file:///Users/noham/Desktop/test.pdf" "2166c19d-d2cc-456c-9f0e-49482031092a"')
|
||||
|
||||
sys.exit(0)
|
||||
|
||||
command = sys.argv[1].lower()
|
||||
|
||||
if command == "export" and len(sys.argv) >= 4:
|
||||
doc_id = sys.argv[2]
|
||||
target = sys.argv[3]
|
||||
format_type = int(sys.argv[4]) if len(sys.argv) > 4 else 0
|
||||
grayscale = sys.argv[5].lower() == "true" if len(sys.argv) > 5 else False
|
||||
|
||||
success = export_document(doc_id, target, format_type, grayscale)
|
||||
if success:
|
||||
print("\n✅ Export request sent successfully!")
|
||||
else:
|
||||
print("\n❌ Export request failed!")
|
||||
sys.exit(1)
|
||||
|
||||
elif command == "import" and len(sys.argv) >= 4:
|
||||
file_url = sys.argv[2]
|
||||
directory_id = sys.argv[3]
|
||||
password = sys.argv[4] if len(sys.argv) > 4 else ""
|
||||
|
||||
success = import_document(file_url, directory_id, password)
|
||||
if success:
|
||||
print("\n✅ Import request sent successfully!")
|
||||
else:
|
||||
print("\n❌ Import request failed!")
|
||||
sys.exit(1)
|
||||
|
||||
else:
|
||||
print(f"\n❌ Invalid command or missing arguments: {' '.join(sys.argv[1:])}")
|
||||
print("Run without arguments to see usage examples.")
|
||||
sys.exit(1)
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -9,6 +9,7 @@
|
||||
#endif
|
||||
#ifdef BUILD_MODE_QMLREBUILD
|
||||
#import "MessageBroker.h"
|
||||
#import "HttpServer.h"
|
||||
#endif
|
||||
#import <objc/runtime.h>
|
||||
#import <Cocoa/Cocoa.h>
|
||||
@@ -314,6 +315,13 @@ static inline bool shouldPatchURL(const QString &host) {
|
||||
NSLogger(@"[reMarkable] Native callback received signal '%s' with value '%s'", signal, value);
|
||||
});
|
||||
|
||||
// Start HTTP server for export requests
|
||||
if (httpserver::start(8080)) {
|
||||
NSLogger(@"[reMarkable] HTTP server started on http://localhost:8080");
|
||||
} else {
|
||||
NSLogger(@"[reMarkable] Failed to start HTTP server");
|
||||
}
|
||||
|
||||
[MemoryUtils hookSymbol:@"QtCore"
|
||||
symbolName:@"__Z21qRegisterResourceDataiPKhS0_S0_"
|
||||
hookFunction:(void *)hooked_qRegisterResourceData
|
||||
|
||||
23
src/utils/HttpServer.h
Normal file
23
src/utils/HttpServer.h
Normal file
@@ -0,0 +1,23 @@
|
||||
// HTTP Server for RMHook - native macOS implementation
|
||||
#pragma once
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
namespace httpserver {
|
||||
// Start HTTP server on specified port
|
||||
bool start(uint16_t port = 8080);
|
||||
|
||||
// Stop HTTP server
|
||||
void stop();
|
||||
|
||||
// Check if server is running
|
||||
bool isRunning();
|
||||
}
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
305
src/utils/HttpServer.mm
Normal file
305
src/utils/HttpServer.mm
Normal file
@@ -0,0 +1,305 @@
|
||||
// HTTP Server for RMHook - native macOS implementation using CFSocket
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
#import <CoreFoundation/CoreFoundation.h>
|
||||
#include <sys/socket.h>
|
||||
#include <netinet/in.h>
|
||||
#include <arpa/inet.h>
|
||||
#include <unistd.h>
|
||||
#include "HttpServer.h"
|
||||
#include "MessageBroker.h"
|
||||
#include "Logger.h"
|
||||
|
||||
static CFSocketRef g_serverSocket = NULL;
|
||||
static uint16_t g_serverPort = 0;
|
||||
static bool g_isRunning = false;
|
||||
|
||||
// Forward declarations
|
||||
static void handleClientConnection(int clientSocket);
|
||||
static void sendResponse(int clientSocket, int statusCode, NSString *body, NSString *contentType);
|
||||
static void handleExportFileRequest(int clientSocket, NSDictionary *jsonData);
|
||||
static void handleDocumentAcceptedRequest(int clientSocket, NSDictionary *jsonData);
|
||||
|
||||
// Socket callback
|
||||
static void socketCallback(CFSocketRef socket, CFSocketCallBackType type,
|
||||
CFDataRef address, const void *data, void *info)
|
||||
{
|
||||
if (type == kCFSocketAcceptCallBack) {
|
||||
CFSocketNativeHandle clientSocket = *(CFSocketNativeHandle *)data;
|
||||
NSLogger(@"[HttpServer] New connection accepted, socket: %d", clientSocket);
|
||||
|
||||
// Handle client in background
|
||||
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
|
||||
handleClientConnection(clientSocket);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
static void handleClientConnection(int clientSocket)
|
||||
{
|
||||
@autoreleasepool {
|
||||
// Read request
|
||||
char buffer[4096];
|
||||
ssize_t bytesRead = recv(clientSocket, buffer, sizeof(buffer) - 1, 0);
|
||||
|
||||
if (bytesRead <= 0) {
|
||||
close(clientSocket);
|
||||
return;
|
||||
}
|
||||
|
||||
buffer[bytesRead] = '\0';
|
||||
NSString *request = [NSString stringWithUTF8String:buffer];
|
||||
|
||||
NSLogger(@"[HttpServer] Received request (%ld bytes)", (long)bytesRead);
|
||||
|
||||
// Parse request line
|
||||
NSArray *lines = [request componentsSeparatedByString:@"\r\n"];
|
||||
if (lines.count == 0) {
|
||||
sendResponse(clientSocket, 400, @"{\"error\": \"Invalid request\"}", @"application/json");
|
||||
close(clientSocket);
|
||||
return;
|
||||
}
|
||||
|
||||
NSArray *requestLine = [lines[0] componentsSeparatedByString:@" "];
|
||||
if (requestLine.count < 3) {
|
||||
sendResponse(clientSocket, 400, @"{\"error\": \"Invalid request line\"}", @"application/json");
|
||||
close(clientSocket);
|
||||
return;
|
||||
}
|
||||
|
||||
NSString *method = requestLine[0];
|
||||
NSString *path = requestLine[1];
|
||||
|
||||
NSLogger(@"[HttpServer] %@ %@", method, path);
|
||||
|
||||
// Find body (after \r\n\r\n)
|
||||
NSRange bodyRange = [request rangeOfString:@"\r\n\r\n"];
|
||||
NSString *body = nil;
|
||||
if (bodyRange.location != NSNotFound) {
|
||||
body = [request substringFromIndex:bodyRange.location + 4];
|
||||
}
|
||||
|
||||
// Route requests
|
||||
if ([path isEqualToString:@"/exportFile"] && [method isEqualToString:@"POST"]) {
|
||||
if (!body || body.length == 0) {
|
||||
sendResponse(clientSocket, 400, @"{\"error\": \"Missing request body\"}", @"application/json");
|
||||
close(clientSocket);
|
||||
return;
|
||||
}
|
||||
|
||||
NSData *jsonData = [body dataUsingEncoding:NSUTF8StringEncoding];
|
||||
NSError *error = nil;
|
||||
id json = [NSJSONSerialization JSONObjectWithData:jsonData options:0 error:&error];
|
||||
|
||||
if (error || ![json isKindOfClass:[NSDictionary class]]) {
|
||||
NSString *errorMsg = [NSString stringWithFormat:@"{\"error\": \"Invalid JSON: %@\"}",
|
||||
error ? error.localizedDescription : @"Not an object"];
|
||||
sendResponse(clientSocket, 400, errorMsg, @"application/json");
|
||||
close(clientSocket);
|
||||
return;
|
||||
}
|
||||
|
||||
handleExportFileRequest(clientSocket, (NSDictionary *)json);
|
||||
|
||||
} else if ([path isEqualToString:@"/documentAccepted"] && [method isEqualToString:@"POST"]) {
|
||||
if (!body || body.length == 0) {
|
||||
sendResponse(clientSocket, 400, @"{\"error\": \"Missing request body\"}", @"application/json");
|
||||
close(clientSocket);
|
||||
return;
|
||||
}
|
||||
|
||||
NSData *jsonData = [body dataUsingEncoding:NSUTF8StringEncoding];
|
||||
NSError *error = nil;
|
||||
id json = [NSJSONSerialization JSONObjectWithData:jsonData options:0 error:&error];
|
||||
|
||||
if (error || ![json isKindOfClass:[NSDictionary class]]) {
|
||||
NSString *errorMsg = [NSString stringWithFormat:@"{\"error\": \"Invalid JSON: %@\"}",
|
||||
error ? error.localizedDescription : @"Not an object"];
|
||||
sendResponse(clientSocket, 400, errorMsg, @"application/json");
|
||||
close(clientSocket);
|
||||
return;
|
||||
}
|
||||
|
||||
handleDocumentAcceptedRequest(clientSocket, (NSDictionary *)json);
|
||||
|
||||
} else if ([path isEqualToString:@"/health"] || [path isEqualToString:@"/"]) {
|
||||
sendResponse(clientSocket, 200, @"{\"status\": \"ok\", \"service\": \"RMHook HTTP Server\"}", @"application/json");
|
||||
|
||||
} else {
|
||||
sendResponse(clientSocket, 404, @"{\"error\": \"Endpoint not found\"}", @"application/json");
|
||||
}
|
||||
|
||||
close(clientSocket);
|
||||
}
|
||||
}
|
||||
|
||||
static void handleExportFileRequest(int clientSocket, NSDictionary *jsonData)
|
||||
{
|
||||
NSLogger(@"[HttpServer] Processing /exportFile request");
|
||||
|
||||
// Convert to JSON string for MessageBroker
|
||||
NSError *error = nil;
|
||||
NSData *jsonDataEncoded = [NSJSONSerialization dataWithJSONObject:jsonData
|
||||
options:0
|
||||
error:&error];
|
||||
|
||||
if (error) {
|
||||
NSString *errorMsg = [NSString stringWithFormat:@"{\"error\": \"Failed to encode JSON: %@\"}",
|
||||
error.localizedDescription];
|
||||
sendResponse(clientSocket, 500, errorMsg, @"application/json");
|
||||
return;
|
||||
}
|
||||
|
||||
NSString *jsonStr = [[NSString alloc] initWithData:jsonDataEncoded encoding:NSUTF8StringEncoding];
|
||||
NSLogger(@"[HttpServer] Broadcasting exportFile signal with data: %@", jsonStr);
|
||||
|
||||
// Broadcast to MessageBroker
|
||||
messagebroker::broadcast("exportFile", [jsonStr UTF8String]);
|
||||
|
||||
// Send success response
|
||||
sendResponse(clientSocket, 200,
|
||||
@"{\"status\": \"success\", \"message\": \"Export request sent to application\"}",
|
||||
@"application/json");
|
||||
}
|
||||
|
||||
static void handleDocumentAcceptedRequest(int clientSocket, NSDictionary *jsonData)
|
||||
{
|
||||
NSLogger(@"[HttpServer] Processing /documentAccepted request");
|
||||
|
||||
// Convert to JSON string for MessageBroker
|
||||
NSError *error = nil;
|
||||
NSData *jsonDataEncoded = [NSJSONSerialization dataWithJSONObject:jsonData
|
||||
options:0
|
||||
error:&error];
|
||||
|
||||
if (error) {
|
||||
NSString *errorMsg = [NSString stringWithFormat:@"{\"error\": \"Failed to encode JSON: %@\"}",
|
||||
error.localizedDescription];
|
||||
sendResponse(clientSocket, 500, errorMsg, @"application/json");
|
||||
return;
|
||||
}
|
||||
|
||||
NSString *jsonStr = [[NSString alloc] initWithData:jsonDataEncoded encoding:NSUTF8StringEncoding];
|
||||
NSLogger(@"[HttpServer] Broadcasting documentAccepted signal with data: %@", jsonStr);
|
||||
|
||||
// Broadcast to MessageBroker
|
||||
messagebroker::broadcast("documentAccepted", [jsonStr UTF8String]);
|
||||
|
||||
// Send success response
|
||||
sendResponse(clientSocket, 200,
|
||||
@"{\"status\": \"success\", \"message\": \"Document accepted request sent to application\"}",
|
||||
@"application/json");
|
||||
}
|
||||
|
||||
static void sendResponse(int clientSocket, int statusCode, NSString *body, NSString *contentType)
|
||||
{
|
||||
NSString *statusText;
|
||||
switch (statusCode) {
|
||||
case 200: statusText = @"OK"; break;
|
||||
case 400: statusText = @"Bad Request"; break;
|
||||
case 404: statusText = @"Not Found"; break;
|
||||
case 500: statusText = @"Internal Server Error"; break;
|
||||
default: statusText = @"Unknown"; break;
|
||||
}
|
||||
|
||||
NSData *bodyData = [body dataUsingEncoding:NSUTF8StringEncoding];
|
||||
|
||||
NSString *response = [NSString stringWithFormat:
|
||||
@"HTTP/1.1 %d %@\r\n"
|
||||
@"Content-Type: %@; charset=utf-8\r\n"
|
||||
@"Content-Length: %lu\r\n"
|
||||
@"Access-Control-Allow-Origin: *\r\n"
|
||||
@"Connection: close\r\n"
|
||||
@"\r\n"
|
||||
@"%@",
|
||||
statusCode, statusText, contentType, (unsigned long)bodyData.length, body
|
||||
];
|
||||
|
||||
NSData *responseData = [response dataUsingEncoding:NSUTF8StringEncoding];
|
||||
send(clientSocket, responseData.bytes, responseData.length, 0);
|
||||
}
|
||||
|
||||
namespace httpserver {
|
||||
|
||||
bool start(uint16_t port)
|
||||
{
|
||||
if (g_isRunning) {
|
||||
NSLogger(@"[HttpServer] Server already running on port %d", g_serverPort);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Create socket
|
||||
CFSocketContext context = {0, NULL, NULL, NULL, NULL};
|
||||
g_serverSocket = CFSocketCreate(kCFAllocatorDefault,
|
||||
PF_INET,
|
||||
SOCK_STREAM,
|
||||
IPPROTO_TCP,
|
||||
kCFSocketAcceptCallBack,
|
||||
socketCallback,
|
||||
&context);
|
||||
|
||||
if (!g_serverSocket) {
|
||||
NSLogger(@"[HttpServer] Failed to create socket");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Set socket options
|
||||
int yes = 1;
|
||||
setsockopt(CFSocketGetNative(g_serverSocket), SOL_SOCKET, SO_REUSEADDR, &yes, sizeof(yes));
|
||||
|
||||
// Bind to address
|
||||
struct sockaddr_in addr;
|
||||
memset(&addr, 0, sizeof(addr));
|
||||
addr.sin_len = sizeof(addr);
|
||||
addr.sin_family = AF_INET;
|
||||
addr.sin_port = htons(port);
|
||||
addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK); // localhost only
|
||||
|
||||
CFDataRef addressData = CFDataCreate(kCFAllocatorDefault,
|
||||
(const UInt8 *)&addr,
|
||||
sizeof(addr));
|
||||
|
||||
CFSocketError error = CFSocketSetAddress(g_serverSocket, addressData);
|
||||
CFRelease(addressData);
|
||||
|
||||
if (error != kCFSocketSuccess) {
|
||||
NSLogger(@"[HttpServer] Failed to bind to port %d (error: %ld)", port, (long)error);
|
||||
CFRelease(g_serverSocket);
|
||||
g_serverSocket = NULL;
|
||||
return false;
|
||||
}
|
||||
|
||||
// Add to run loop
|
||||
CFRunLoopSourceRef source = CFSocketCreateRunLoopSource(kCFAllocatorDefault, g_serverSocket, 0);
|
||||
CFRunLoopAddSource(CFRunLoopGetMain(), source, kCFRunLoopCommonModes);
|
||||
CFRelease(source);
|
||||
|
||||
g_serverPort = port;
|
||||
g_isRunning = true;
|
||||
|
||||
NSLogger(@"[HttpServer] HTTP server started successfully on http://localhost:%d", port);
|
||||
return true;
|
||||
}
|
||||
|
||||
void stop()
|
||||
{
|
||||
if (!g_isRunning) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (g_serverSocket) {
|
||||
CFSocketInvalidate(g_serverSocket);
|
||||
CFRelease(g_serverSocket);
|
||||
g_serverSocket = NULL;
|
||||
}
|
||||
|
||||
g_isRunning = false;
|
||||
NSLogger(@"[HttpServer] HTTP server stopped");
|
||||
}
|
||||
|
||||
bool isRunning()
|
||||
{
|
||||
return g_isRunning;
|
||||
}
|
||||
|
||||
} // namespace httpserver
|
||||
@@ -348,6 +348,8 @@ void ReMarkableDumpResourceFile(struct ResourceRoot *root, int node, const char
|
||||
static const char *kFilesToReplace[] = {
|
||||
"/qml/client/dialogs/ExportDialog.qml",
|
||||
"/qml/client/settings/GeneralSettings.qml",
|
||||
"/qml/client/dialogs/ExportUtils.js",
|
||||
"/qml/client/desktop/FileImportDialog.qml",
|
||||
NULL // Sentinel to mark end of list
|
||||
};
|
||||
|
||||
|
||||
17
src/utils/mb.m
Normal file
17
src/utils/mb.m
Normal file
@@ -0,0 +1,17 @@
|
||||
// Example usage of MessageBroker from C++/Objective-C
|
||||
|
||||
#include <QObject>
|
||||
#include <QProcess>
|
||||
#include <QString>
|
||||
#include <QQmlApplicationEngine>
|
||||
#include "MessageBroker.h"
|
||||
|
||||
// Example: Register MessageBroker QML type (called from dylib init)
|
||||
void initMessageBroker() {
|
||||
messagebroker::registerQmlType();
|
||||
}
|
||||
|
||||
// Example: Send a signal from C++ to QML
|
||||
void sendSignal() {
|
||||
messagebroker::broadcast("demoSignal", "Hello from C!");
|
||||
}
|
||||
16
src/utils/mb.qml
Normal file
16
src/utils/mb.qml
Normal file
@@ -0,0 +1,16 @@
|
||||
import net.noham.MessageBroker
|
||||
|
||||
MessageBroker {
|
||||
id: demoBroker
|
||||
listeningFor: ["demoSignal"]
|
||||
|
||||
onSignalReceived: (signal, message) => {
|
||||
console.log("Got message", signal, message);
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
onClicked: () => {
|
||||
demoBroker.sendSignal("mySignalName", "Hello from QML!");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user