A lightweight, cross-platform application installer and updater for GitHub projects.
Built with C++ (Qt/QML) for the frontend and Go for the backend, AstralInstall provides an elegant interface for discovering, installing, and managing GitHub-hosted applications.
- π¨ Modern Qt/QML-based user interface
- β‘ Fast Go backend for network operations
- π¦ Automatic GitHub release detection and updates
- π§ JSON-based IPC for seamless frontend-backend communication
- π Game/application library management
AstralInstall uses a process-based IPC (Inter-Process Communication) architecture:
βββββββββββββββββββββββββββ
β C++ Frontend (Qt/QML) β
β User Interface Layer β
ββββββββββββββ¬βββββββββββββ
β stdin/stdout (JSON)
βΌ
βββββββββββββββββββββββββββ
β Go Backend Server β
β Network & Logic Layer β
βββββββββββββββββββββββββββ
- Frontend (C++): Handles UI rendering, user interactions, and game library management using Qt and QML
- Backend (Go): Manages network requests, GitHub API interactions, and release checking
The frontend and backend communicate exclusively through JSON messages over standard input/output streams. This creates a clean separation of concerns and allows the two components to run as independent processes while maintaining synchronous request-response communication.
The C++ frontend sends JSON-encoded requests to the backend through stdin:
{"action": "check_release", "repo": "https://github.com/hannes-swd/code-miner"}The Go backend runs an infinite loop (FrontendLineReader()) that:
- Reads each line from
stdinusing abufio.Scanner - Validates the JSON format using
json.Unmarshal() - Routes the request to appropriate handlers based on the
actionfield - Processes the request (e.g., fetching release info from GitHub)
- Encodes the response as JSON
- Writes the response to
stdoutusingfmt.Println()
The backend sends JSON-encoded responses back to the frontend through stdout:
{"success": true, "version": "v1.2.3"}or in case of error:
{"success": false, "error": "repo field is required"}type Request struct {
Action string `json:"action"` // Action to perform (e.g., "check_release")
Repo string `json:"repo"` // Repository URL or identifier
}type Response struct {
Success bool `json:"success"` // True if request succeeded
Version string `json:"version,omitempty"` // Release version (if applicable)
Error string `json:"error,omitempty"` // Error message (if failed)
}Purpose: Check the latest release version of a GitHub repository
Request Example:
{"action": "check_release", "repo": "https://github.com/hannes-swd/code-miner"}Response (Success):
{"success": true, "version": "v1.2.3"}Response (Failure - Missing Repo):
{"success": false, "error": "repo field is required"}Response (Failure - Network Error):
{"success": false, "error": "failed to fetch release data"}Here's a step-by-step trace of a typical IPC interaction:
FRONTEND (C++) BACKEND (Go)
β β
ββ Construct Request βββββββββββββββββΊ β
β {"action":"check_release", β
β "repo":"github.com/user/project"} β
β β
β βββββββββββββ€ Read from stdin
β βββββββββββββ€ Unmarshal JSON
β βββββββββββββ€ Route to handler
β βββββββββββββ€ Call Network.GetLatest()
β βββββββββββββ€ Marshal Response
β βββββββββββββ€ Write to stdout
β {"success":true, β
β "version":"v2.0.1"}βββββββββββββββββ€
β β
ββ Parse Response β
Update UI β
The IPC protocol handles errors gracefully:
-
Invalid JSON: If the frontend sends malformed JSON, the backend responds with:
{"success": false, "error": "Invalid JSON: <detailed error>"} -
Empty Lines: The backend skips empty lines sent by the frontend (defensive programming)
-
Unknown Actions: Unrecognized actions receive:
{"success": false, "error": "Unknown action: <action_name>"} -
Missing Required Fields: Each handler validates required fields and responds with specific error messages
-
Network Errors: Network-level errors (API failures, timeouts, etc.) are caught and reported:
{"success": false, "error": "<network error details>"} -
JSON Encoding Failures: Fallback error response if the response itself fails to marshal:
{"success": false, "error": "Failed to encode response JSON"}
Located in src/Backend/Protocol/Protocol.go:
func FrontendLineReader() {
scanner := bufio.NewScanner(os.Stdin) // Create scanner for stdin
for scanner.Scan() { // Infinite loop
var request Request
rawBytes := scanner.Bytes()
// Skip empty lines
if len(rawBytes) == 0 {
continue
}
// Unmarshal JSON
if err := json.Unmarshal(rawBytes, &request); err != nil {
writeResponse(Response{
Success: false,
Error: "Invalid JSON: " + err.Error(),
})
continue
}
// Route request
switch request.Action {
case "check_release":
handleCheckRelease(request)
default:
writeResponse(Response{
Success: false,
Error: "Unknown action: " + request.Action,
})
}
}
// Error handling if scanner stops
if err := scanner.Err(); err != nil {
fmt.Fprintf(os.Stderr, "Backend stdin reading error: %v\n", err)
}
}The switch statement routes each request to its handler based on the action field. Currently implemented:
"check_release"βhandleCheckRelease()
New actions can be easily added by implementing a new handler and adding a case to the switch statement.
All responses are written through a single function to ensure consistency:
func writeResponse(response Response) {
data, err := json.Marshal(response)
if err != nil {
// Fallback if JSON encoding fails
fmt.Println(`{"success":false,"error":"Failed to encode response JSON"}`)
return
}
fmt.Println(string(data)) // Each response is one line
}Important: Each response is printed on a single line followed by a newline. This allows the frontend to use readline() or equivalent to read complete messages.
func handleCheckRelease(request Request) {
// Validate required fields
if request.Repo == "" {
writeResponse(Response{Success: false, Error: "repo field is required"})
return
}
// Call backend network layer
release, err := network.GetLatest(request.Repo)
if err != nil {
writeResponse(Response{
Success: false,
Error: err.Error(),
})
return
}
// Success response
writeResponse(Response{
Success: true,
Version: release.Version,
})
}β
Simple: JSON is human-readable and language-agnostic
β
Reliable: One message per line makes parsing predictable
β
Decoupled: Frontend and backend run independently
β
Debuggable: Easy to log and inspect communication
β
Portable: Works across different operating systems
β
Extensible: New actions and response fields can be added without breaking existing code
cd src/Frontend
mkdir build && cd build
cmake ..
makecd src/Backend
go build -o MyAppBackend-
src/Frontend/: Qt/QML based user interface
main.cpp,MainWindow.cpp/h: Main application entry and windowqml/: QML files for UI (Library.qml, Main.qml, MyGamesPage.qml)resources.qrc: Qt resource file
-
src/Backend/: Go backend server
main.go: Entry pointProtocol/Protocol.go: IPC implementation and request routingNetwork/: GitHub API integration and release fetchingCommon/: Shared utilities
Once built, run the frontend application. It will spawn the backend process and communicate via IPC to fetch and display game/application information from GitHub.