新增 XSLPrintDot 项目,包含打印服务的核心功能和相关配置。实现打印机查询、打印任务处理、远程转发功能,并支持多平台设备ID获取。优化打印数据准备逻辑,增强系统的可维护性和扩展性,同时更新工作区配置以支持新项目。
This commit is contained in:
950
XSLPrintDot/backend.go
Normal file
950
XSLPrintDot/backend.go
Normal file
@@ -0,0 +1,950 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"math"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/gorilla/websocket"
|
||||
)
|
||||
|
||||
type Bridge struct {
|
||||
server *http.Server
|
||||
port string
|
||||
key string
|
||||
mu sync.Mutex
|
||||
log func(string) // Callback to log to frontend
|
||||
|
||||
// Log related
|
||||
logFileMu sync.Mutex
|
||||
logServer *http.Server
|
||||
logPort int
|
||||
|
||||
// Connection tracking
|
||||
clientCount int
|
||||
countMu sync.Mutex
|
||||
onCountChange func(int) // Callback to update frontend
|
||||
conns map[*websocket.Conn]bool
|
||||
|
||||
// Restart callback
|
||||
onRestart func()
|
||||
onReload func()
|
||||
onClientConnect func(string)
|
||||
|
||||
// Remote forwarding
|
||||
remoteMu sync.Mutex
|
||||
remoteStop chan struct{}
|
||||
remoteWg sync.WaitGroup
|
||||
remoteCfg RemoteConfig
|
||||
remoteConn *websocket.Conn
|
||||
remoteStatus RemoteForwarderStatus
|
||||
forwarderStatusProvider func() RemoteForwarderStatus
|
||||
forwarderConnect func()
|
||||
forwarderDisconnect func()
|
||||
}
|
||||
|
||||
func NewBridge() *Bridge {
|
||||
b := &Bridge{
|
||||
port: "1122",
|
||||
key: "",
|
||||
conns: make(map[*websocket.Conn]bool),
|
||||
}
|
||||
b.ensureLogDir()
|
||||
return b
|
||||
}
|
||||
|
||||
func (b *Bridge) SetLogger(logger func(string)) {
|
||||
b.log = logger
|
||||
}
|
||||
|
||||
func (b *Bridge) SetCountCallback(cb func(int)) {
|
||||
b.onCountChange = cb
|
||||
}
|
||||
|
||||
func (b *Bridge) SetRestartCallback(cb func()) {
|
||||
b.onRestart = cb
|
||||
}
|
||||
|
||||
func (b *Bridge) SetReloadCallback(cb func()) {
|
||||
b.onReload = cb
|
||||
}
|
||||
|
||||
func (b *Bridge) SetClientConnectCallback(cb func(string)) {
|
||||
b.onClientConnect = cb
|
||||
}
|
||||
|
||||
func (b *Bridge) SetForwarderStatusProvider(cb func() RemoteForwarderStatus) {
|
||||
b.forwarderStatusProvider = cb
|
||||
}
|
||||
|
||||
func (b *Bridge) SetForwarderConnectHandler(cb func()) {
|
||||
b.forwarderConnect = cb
|
||||
}
|
||||
|
||||
func (b *Bridge) SetForwarderDisconnectHandler(cb func()) {
|
||||
b.forwarderDisconnect = cb
|
||||
}
|
||||
|
||||
func (b *Bridge) updateClientCount(delta int) {
|
||||
b.countMu.Lock()
|
||||
b.clientCount += delta
|
||||
count := b.clientCount
|
||||
b.countMu.Unlock()
|
||||
|
||||
if b.onCountChange != nil {
|
||||
b.onCountChange(count)
|
||||
}
|
||||
}
|
||||
|
||||
func (b *Bridge) Log(msg string) {
|
||||
timestamp := time.Now().Format("15:04:05")
|
||||
entry := fmt.Sprintf("[%s] %s", timestamp, msg)
|
||||
if err := b.appendLogLine(entry); err != nil {
|
||||
log.Println(entry)
|
||||
} else if b.log != nil {
|
||||
b.log(msg)
|
||||
}
|
||||
}
|
||||
|
||||
func (b *Bridge) GetPrinters() ([]PrinterInfo, error) {
|
||||
return b.getPrintersPlatform()
|
||||
}
|
||||
|
||||
type PrinterInfo struct {
|
||||
Name string `json:"name"`
|
||||
IsDefault bool `json:"isDefault"`
|
||||
}
|
||||
|
||||
func (b *Bridge) GetPrinterCapabilities(printerName string) (map[string]interface{}, error) {
|
||||
return b.getPrinterCapabilitiesPlatform(printerName)
|
||||
}
|
||||
|
||||
func (b *Bridge) StartServer(port string, key string) error {
|
||||
b.mu.Lock()
|
||||
defer b.mu.Unlock()
|
||||
|
||||
if b.server != nil {
|
||||
return fmt.Errorf("server already running")
|
||||
}
|
||||
|
||||
b.port = port
|
||||
b.key = key
|
||||
|
||||
mux := http.NewServeMux()
|
||||
mux.HandleFunc("/ws", b.handleWebSocket)
|
||||
|
||||
b.server = &http.Server{
|
||||
Addr: ":" + port,
|
||||
Handler: mux,
|
||||
}
|
||||
|
||||
go func() {
|
||||
b.Log(fmt.Sprintf("Starting server on port %s...", port))
|
||||
if err := b.server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
|
||||
b.Log(fmt.Sprintf("Server error: %v", err))
|
||||
}
|
||||
b.Log("Server stopped")
|
||||
}()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b *Bridge) StopServer() error {
|
||||
b.mu.Lock()
|
||||
defer b.mu.Unlock()
|
||||
|
||||
if b.server == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Close all active connections first
|
||||
b.countMu.Lock()
|
||||
for conn := range b.conns {
|
||||
conn.Close()
|
||||
}
|
||||
// Clear the map
|
||||
b.conns = make(map[*websocket.Conn]bool)
|
||||
b.countMu.Unlock()
|
||||
|
||||
if err := b.server.Shutdown(context.Background()); err != nil {
|
||||
return err
|
||||
}
|
||||
b.server = nil
|
||||
return nil
|
||||
}
|
||||
|
||||
var upgrader = websocket.Upgrader{
|
||||
CheckOrigin: func(r *http.Request) bool {
|
||||
return true // Allow all origins
|
||||
},
|
||||
}
|
||||
|
||||
type PrintRequest struct {
|
||||
Printer string `json:"printer"`
|
||||
Content string `json:"content"` // Base64 encoded PDF
|
||||
Key string `json:"key"`
|
||||
|
||||
Job struct {
|
||||
Name string `json:"name"`
|
||||
Copies int `json:"copies"`
|
||||
IntervalMs int `json:"intervalMs"`
|
||||
} `json:"job"`
|
||||
Pages struct {
|
||||
Range string `json:"range"`
|
||||
Set string `json:"set"`
|
||||
} `json:"pages"`
|
||||
Layout struct {
|
||||
Scale string `json:"scale"`
|
||||
Orientation string `json:"orientation"`
|
||||
} `json:"layout"`
|
||||
Color struct {
|
||||
Mode string `json:"mode"`
|
||||
} `json:"color"`
|
||||
Sides struct {
|
||||
Mode string `json:"mode"`
|
||||
} `json:"sides"`
|
||||
Paper PaperSpec `json:"paper"`
|
||||
Tray struct {
|
||||
Bin string `json:"bin"`
|
||||
} `json:"tray"`
|
||||
}
|
||||
|
||||
type PaperSpec struct {
|
||||
Size string `json:"size"`
|
||||
}
|
||||
|
||||
type PrintOptions struct {
|
||||
PageRange string
|
||||
PageSet string
|
||||
Duplex string
|
||||
ColorMode string
|
||||
Paper string
|
||||
Scale string
|
||||
Orientation string
|
||||
TrayBin string
|
||||
Copies int
|
||||
}
|
||||
|
||||
type Response struct {
|
||||
Status string `json:"status"`
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
func (b *Bridge) handleWebSocket(w http.ResponseWriter, r *http.Request) {
|
||||
// Authentication check
|
||||
if b.key != "" {
|
||||
pass := r.URL.Query().Get("key")
|
||||
if pass == "" {
|
||||
pass = r.URL.Query().Get("password")
|
||||
}
|
||||
|
||||
if pass != b.key {
|
||||
b.Log(fmt.Sprintf("Authentication failed for %s", r.RemoteAddr))
|
||||
http.Error(w, "Unauthorized", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
c, err := upgrader.Upgrade(w, r, nil)
|
||||
if err != nil {
|
||||
b.Log(fmt.Sprintf("Upgrade error: %v", err))
|
||||
return
|
||||
}
|
||||
defer c.Close()
|
||||
|
||||
b.countMu.Lock()
|
||||
b.conns[c] = true
|
||||
b.countMu.Unlock()
|
||||
b.updateClientCount(1)
|
||||
defer func() {
|
||||
b.countMu.Lock()
|
||||
delete(b.conns, c)
|
||||
b.countMu.Unlock()
|
||||
b.updateClientCount(-1)
|
||||
}()
|
||||
|
||||
b.Log(fmt.Sprintf("Client connected from %s", c.RemoteAddr()))
|
||||
|
||||
if b.onClientConnect != nil {
|
||||
b.onClientConnect(c.RemoteAddr().String())
|
||||
}
|
||||
|
||||
// Send printer list immediately upon connection
|
||||
printers, err := b.GetPrinters()
|
||||
if err == nil {
|
||||
msg := map[string]interface{}{
|
||||
"type": "printer_list",
|
||||
"data": printers,
|
||||
}
|
||||
if jsonBytes, err := json.Marshal(msg); err == nil {
|
||||
b.Log(fmt.Sprintf("Sent WS message: %s", string(jsonBytes)))
|
||||
}
|
||||
c.WriteJSON(msg)
|
||||
} else {
|
||||
b.Log(fmt.Sprintf("Failed to get printers on connect: %v", err))
|
||||
errMsg := fmt.Sprintf("Failed to get printer list: %v", err)
|
||||
c.WriteJSON(Response{Status: "error", Message: errMsg})
|
||||
}
|
||||
|
||||
for {
|
||||
// Read message as raw JSON map first to check type
|
||||
var rawMsg map[string]interface{}
|
||||
err := c.ReadJSON(&rawMsg)
|
||||
if err != nil {
|
||||
if websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway, websocket.CloseAbnormalClosure) {
|
||||
b.Log(fmt.Sprintf("Client disconnected: %v", err))
|
||||
} else {
|
||||
// Normal closure or other error
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
if jsonBytes, err := json.Marshal(rawMsg); err == nil {
|
||||
b.Log(fmt.Sprintf("Received WS message: %s", string(jsonBytes)))
|
||||
}
|
||||
|
||||
// Check message type
|
||||
if msgType, ok := rawMsg["type"].(string); ok && msgType == "get_printers" {
|
||||
printers, err := b.GetPrinters()
|
||||
if err == nil {
|
||||
msg := map[string]interface{}{
|
||||
"type": "printer_list",
|
||||
"data": printers,
|
||||
}
|
||||
if jsonBytes, err := json.Marshal(msg); err == nil {
|
||||
b.Log(fmt.Sprintf("Sent WS message: %s", string(jsonBytes)))
|
||||
}
|
||||
c.WriteJSON(msg)
|
||||
} else {
|
||||
resp := Response{Status: "error", Message: fmt.Sprintf("Failed to get printer list: %v", err)}
|
||||
if jsonBytes, err := json.Marshal(resp); err == nil {
|
||||
b.Log(fmt.Sprintf("Sent WS message: %s", string(jsonBytes)))
|
||||
}
|
||||
c.WriteJSON(resp)
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if msgType, ok := rawMsg["type"].(string); ok && msgType == "get_printer_caps" {
|
||||
printer, _ := rawMsg["printer"].(string)
|
||||
printer = strings.TrimSpace(printer)
|
||||
if printer == "" {
|
||||
resp := Response{Status: "error", Message: "printer is required"}
|
||||
c.WriteJSON(resp)
|
||||
continue
|
||||
}
|
||||
|
||||
caps, err := b.GetPrinterCapabilities(printer)
|
||||
if err != nil {
|
||||
b.Log(fmt.Sprintf("Failed to get printer capabilities for '%s': %v", printer, err))
|
||||
resp := Response{Status: "error", Message: err.Error()}
|
||||
c.WriteJSON(resp)
|
||||
continue
|
||||
}
|
||||
|
||||
msg := map[string]interface{}{
|
||||
"type": "printer_caps",
|
||||
"printer": printer,
|
||||
"data": caps,
|
||||
}
|
||||
c.WriteJSON(msg)
|
||||
continue
|
||||
}
|
||||
|
||||
// Handle as PrintRequest (default)
|
||||
jsonBody, _ := json.Marshal(rawMsg)
|
||||
var req PrintRequest
|
||||
if err := json.Unmarshal(jsonBody, &req); err != nil {
|
||||
b.Log(fmt.Sprintf("Invalid print request: %v", err))
|
||||
resp := Response{Status: "error", Message: "Invalid request format"}
|
||||
c.WriteJSON(resp)
|
||||
continue
|
||||
}
|
||||
|
||||
msg, err := b.processPrintRequest(req)
|
||||
if err == nil {
|
||||
resp := Response{Status: "success", Message: msg}
|
||||
c.WriteJSON(resp)
|
||||
} else {
|
||||
c.WriteJSON(Response{Status: "error", Message: msg})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (b *Bridge) processPrintRequest(req PrintRequest) (string, error) {
|
||||
jobName := strings.TrimSpace(req.Job.Name)
|
||||
|
||||
copies := req.Job.Copies
|
||||
if copies <= 0 {
|
||||
copies = 1
|
||||
}
|
||||
|
||||
intervalMs := req.Job.IntervalMs
|
||||
|
||||
contentToDecode := req.Content
|
||||
if strings.HasPrefix(contentToDecode, "data:") {
|
||||
if idx := strings.Index(contentToDecode, ","); idx != -1 {
|
||||
contentToDecode = contentToDecode[idx+1:]
|
||||
}
|
||||
}
|
||||
decoded, err := base64.StdEncoding.DecodeString(contentToDecode)
|
||||
if err != nil {
|
||||
b.Log("Error decoding Base64 content")
|
||||
return "Invalid Base64 content", fmt.Errorf("invalid base64 content")
|
||||
}
|
||||
|
||||
if len(decoded) < 4 || string(decoded[0:4]) != "%PDF" {
|
||||
b.Log("Content is not a valid PDF (missing %PDF header)")
|
||||
return "Content must be a PDF file", fmt.Errorf("invalid pdf")
|
||||
}
|
||||
|
||||
autoPaper := ""
|
||||
if strings.TrimSpace(req.Paper.Size) == "" {
|
||||
if name, ok := detectPaperFromPDF(decoded); ok {
|
||||
autoPaper = name
|
||||
b.Log(fmt.Sprintf("Auto paper size detected: %s", name))
|
||||
}
|
||||
}
|
||||
|
||||
options := PrintOptions{
|
||||
PageRange: strings.TrimSpace(req.Pages.Range),
|
||||
PageSet: strings.TrimSpace(req.Pages.Set),
|
||||
Duplex: strings.TrimSpace(req.Sides.Mode),
|
||||
ColorMode: strings.TrimSpace(req.Color.Mode),
|
||||
Paper: strings.TrimSpace(firstNonEmpty(req.Paper.Size, autoPaper)),
|
||||
Scale: strings.TrimSpace(req.Layout.Scale),
|
||||
Orientation: strings.TrimSpace(req.Layout.Orientation),
|
||||
TrayBin: strings.TrimSpace(req.Tray.Bin),
|
||||
}
|
||||
|
||||
runCount := 1
|
||||
perRunCopies := copies
|
||||
if intervalMs > 0 {
|
||||
runCount = copies
|
||||
perRunCopies = 1
|
||||
}
|
||||
options.Copies = perRunCopies
|
||||
|
||||
successCount := 0
|
||||
var lastErr error
|
||||
|
||||
for i := 0; i < runCount; i++ {
|
||||
if i > 0 && intervalMs > 0 {
|
||||
time.Sleep(time.Duration(intervalMs) * time.Millisecond)
|
||||
}
|
||||
|
||||
err = b.printPDF(req.Printer, jobName, decoded, options)
|
||||
if err != nil {
|
||||
lastErr = err
|
||||
b.Log(fmt.Sprintf("Print error (copy %d): %v", i+1, err))
|
||||
break
|
||||
} else {
|
||||
successCount += perRunCopies
|
||||
}
|
||||
}
|
||||
|
||||
if successCount == copies {
|
||||
b.Log("Print success")
|
||||
return "Printed successfully", nil
|
||||
}
|
||||
|
||||
msg := fmt.Sprintf("Printed %d/%d copies. Error: %v", successCount, copies, lastErr)
|
||||
b.Log(msg)
|
||||
return msg, fmt.Errorf("print failed")
|
||||
}
|
||||
|
||||
func firstNonEmpty(values ...string) string {
|
||||
for _, value := range values {
|
||||
if strings.TrimSpace(value) != "" {
|
||||
return value
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
var (
|
||||
mediaBoxRegex = regexp.MustCompile(`/MediaBox\s*\[\s*([-0-9.]+)\s+([-0-9.]+)\s+([-0-9.]+)\s+([-0-9.]+)\s*\]`)
|
||||
cropBoxRegex = regexp.MustCompile(`/CropBox\s*\[\s*([-0-9.]+)\s+([-0-9.]+)\s+([-0-9.]+)\s+([-0-9.]+)\s*\]`)
|
||||
)
|
||||
|
||||
func detectPaperFromPDF(pdfData []byte) (string, bool) {
|
||||
limit := len(pdfData)
|
||||
if limit > 5*1024*1024 {
|
||||
limit = 5 * 1024 * 1024
|
||||
}
|
||||
chunk := string(pdfData[:limit])
|
||||
match := cropBoxRegex.FindStringSubmatch(chunk)
|
||||
if len(match) != 5 {
|
||||
match = mediaBoxRegex.FindStringSubmatch(chunk)
|
||||
}
|
||||
if len(match) != 5 {
|
||||
return "", false
|
||||
}
|
||||
|
||||
llx, err1 := strconv.ParseFloat(match[1], 64)
|
||||
lly, err2 := strconv.ParseFloat(match[2], 64)
|
||||
urx, err3 := strconv.ParseFloat(match[3], 64)
|
||||
ury, err4 := strconv.ParseFloat(match[4], 64)
|
||||
if err1 != nil || err2 != nil || err3 != nil || err4 != nil {
|
||||
return "", false
|
||||
}
|
||||
|
||||
widthPt := math.Abs(urx - llx)
|
||||
heightPt := math.Abs(ury - lly)
|
||||
return matchStandardPaper(widthPt, heightPt)
|
||||
}
|
||||
|
||||
type paperSize struct {
|
||||
Name string
|
||||
Wmm float64
|
||||
Hmm float64
|
||||
}
|
||||
|
||||
func matchStandardPaper(widthPt, heightPt float64) (string, bool) {
|
||||
mmPerPt := 25.4 / 72.0
|
||||
widthMm := widthPt * mmPerPt
|
||||
heightMm := heightPt * mmPerPt
|
||||
|
||||
if widthMm <= 0 || heightMm <= 0 {
|
||||
return "", false
|
||||
}
|
||||
|
||||
if widthMm > heightMm {
|
||||
widthMm, heightMm = heightMm, widthMm
|
||||
}
|
||||
|
||||
standard := []paperSize{
|
||||
{Name: "A2", Wmm: 420, Hmm: 594},
|
||||
{Name: "A3", Wmm: 297, Hmm: 420},
|
||||
{Name: "A4", Wmm: 210, Hmm: 297},
|
||||
{Name: "A5", Wmm: 148, Hmm: 210},
|
||||
{Name: "A6", Wmm: 105, Hmm: 148},
|
||||
{Name: "letter", Wmm: 216, Hmm: 279},
|
||||
{Name: "legal", Wmm: 216, Hmm: 356},
|
||||
{Name: "tabloid", Wmm: 279, Hmm: 432},
|
||||
{Name: "statement", Wmm: 140, Hmm: 216},
|
||||
}
|
||||
|
||||
const tolerance = 2.0
|
||||
for _, size := range standard {
|
||||
if math.Abs(widthMm-size.Wmm) <= tolerance && math.Abs(heightMm-size.Hmm) <= tolerance {
|
||||
return size.Name, true
|
||||
}
|
||||
}
|
||||
|
||||
return "", false
|
||||
}
|
||||
|
||||
func (b *Bridge) printPDF(printerName string, jobName string, pdfData []byte, options PrintOptions) error {
|
||||
// 1. Write to temp file
|
||||
tmpFile, err := ioutil.TempFile("", "print-dot-*.pdf")
|
||||
if err != nil {
|
||||
return fmt.Errorf("create temp file failed: %v", err)
|
||||
}
|
||||
defer os.Remove(tmpFile.Name()) // Clean up on exit
|
||||
|
||||
if _, err := tmpFile.Write(pdfData); err != nil {
|
||||
tmpFile.Close()
|
||||
return fmt.Errorf("write temp file failed: %v", err)
|
||||
}
|
||||
tmpFile.Close()
|
||||
absPath, _ := filepath.Abs(tmpFile.Name())
|
||||
|
||||
if jobName != "" {
|
||||
b.Log(fmt.Sprintf("Printing job '%s': %s to %s", jobName, absPath, printerName))
|
||||
} else {
|
||||
b.Log(fmt.Sprintf("Printing PDF file: %s to %s", absPath, printerName))
|
||||
}
|
||||
|
||||
return b.printPDFPlatform(printerName, absPath, options)
|
||||
}
|
||||
|
||||
func (b *Bridge) StartLogServer() error {
|
||||
b.ensureLogDir()
|
||||
listener, err := net.Listen("tcp", "localhost:0")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
b.logPort = listener.Addr().(*net.TCPAddr).Port
|
||||
|
||||
mux := http.NewServeMux()
|
||||
mux.HandleFunc("/logs", b.handleLogs)
|
||||
mux.HandleFunc("/api/logs", b.handleLogsJSON)
|
||||
mux.HandleFunc("/api/logs/clear", b.handleClearLogs)
|
||||
mux.HandleFunc("/api/restart", b.handleRestartRequest)
|
||||
mux.HandleFunc("/api/reload", b.handleReloadRequest)
|
||||
mux.HandleFunc("/api/forwarder/status", b.handleForwarderStatus)
|
||||
mux.HandleFunc("/api/forwarder/connect", b.handleForwarderConnect)
|
||||
mux.HandleFunc("/api/forwarder/disconnect", b.handleForwarderDisconnect)
|
||||
mux.HandleFunc("/api/forwarder/stream", b.handleForwarderStream)
|
||||
|
||||
b.logServer = &http.Server{
|
||||
Handler: mux,
|
||||
}
|
||||
|
||||
go func() {
|
||||
if err := b.logServer.Serve(listener); err != nil && err != http.ErrServerClosed {
|
||||
b.Log(fmt.Sprintf("Log server error: %v", err))
|
||||
}
|
||||
}()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b *Bridge) ensureLogDir() {
|
||||
logDir, err := b.logDirPath()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
_ = os.MkdirAll(logDir, 0755)
|
||||
path := filepath.Join(logDir, time.Now().Format("20060102")+".txt")
|
||||
if f, err := os.OpenFile(path, os.O_CREATE, 0644); err == nil {
|
||||
f.Close()
|
||||
}
|
||||
}
|
||||
|
||||
func (b *Bridge) StopLogServer() error {
|
||||
if b.logServer == nil {
|
||||
return nil
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
|
||||
defer cancel()
|
||||
if err := b.logServer.Shutdown(ctx); err != nil {
|
||||
_ = b.logServer.Close()
|
||||
return err
|
||||
}
|
||||
b.logServer = nil
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b *Bridge) handleLogs(w http.ResponseWriter, r *http.Request) {
|
||||
currentLogs, _ := b.readLogLines()
|
||||
|
||||
html := `<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>System Logs - XSL-PrintDot Client</title>
|
||||
<meta http-equiv="refresh" content="2">
|
||||
<style>
|
||||
body {
|
||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||
background: #ffffff;
|
||||
color: #333;
|
||||
padding: 20px;
|
||||
margin: 0;
|
||||
}
|
||||
h2 { border-bottom: 2px solid #eee; padding-bottom: 10px; margin-top: 0; color: #444; }
|
||||
.log-entry {
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
padding: 8px 0;
|
||||
font-family: Consolas, 'Courier New', monospace;
|
||||
font-size: 13px;
|
||||
}
|
||||
.log-entry:hover { background-color: #f9f9f9; }
|
||||
.empty { color: #999; font-style: italic; padding: 20px 0; }
|
||||
.status { font-size: 12px; color: #888; margin-top: 5px; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h2>System Logs</h2>
|
||||
<div class="status">Auto-refreshing every 2 seconds</div>
|
||||
<div id="logs">
|
||||
{{range .}}
|
||||
<div class="log-entry">{{.}}</div>
|
||||
{{else}}
|
||||
<div class="empty">No logs available.</div>
|
||||
{{end}}
|
||||
</div>
|
||||
</body>
|
||||
</html>`
|
||||
|
||||
t, err := template.New("logs").Parse(html)
|
||||
if err != nil {
|
||||
http.Error(w, "Template error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
t.Execute(w, currentLogs)
|
||||
}
|
||||
|
||||
func (b *Bridge) handleLogsJSON(w http.ResponseWriter, r *http.Request) {
|
||||
currentLogs, _ := b.readLogLines()
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.Header().Set("Access-Control-Allow-Origin", "*") // Allow child process to fetch
|
||||
json.NewEncoder(w).Encode(currentLogs)
|
||||
}
|
||||
|
||||
func (b *Bridge) handleClearLogs(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Access-Control-Allow-Origin", "*")
|
||||
w.Header().Set("Access-Control-Allow-Methods", "POST, OPTIONS")
|
||||
|
||||
if r.Method == http.MethodOptions {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
return
|
||||
}
|
||||
|
||||
if r.Method != http.MethodPost {
|
||||
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
b.clearLogFile()
|
||||
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}
|
||||
|
||||
func (b *Bridge) logFilePath() (string, error) {
|
||||
logDir, err := b.logDirPath()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
fileName := time.Now().Format("20060102") + ".txt"
|
||||
return filepath.Join(logDir, fileName), nil
|
||||
}
|
||||
|
||||
func (b *Bridge) logDirPath() (string, error) {
|
||||
baseDir, err := dataDirPath()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return filepath.Join(baseDir, "logs"), nil
|
||||
}
|
||||
|
||||
func dataDirPath() (string, error) {
|
||||
if programData := strings.TrimSpace(os.Getenv("ProgramData")); programData != "" {
|
||||
return filepath.Join(programData, "PrintDot"), nil
|
||||
}
|
||||
if runtime.GOOS == "darwin" {
|
||||
if home, err := os.UserHomeDir(); err == nil {
|
||||
return filepath.Join(home, "Library", "Application Support", "PrintDot"), nil
|
||||
}
|
||||
}
|
||||
if runtime.GOOS == "linux" {
|
||||
if dataHome := strings.TrimSpace(os.Getenv("XDG_DATA_HOME")); dataHome != "" {
|
||||
return filepath.Join(dataHome, "PrintDot"), nil
|
||||
}
|
||||
if home, err := os.UserHomeDir(); err == nil {
|
||||
return filepath.Join(home, ".local", "share", "PrintDot"), nil
|
||||
}
|
||||
}
|
||||
if wd, err := os.Getwd(); err == nil {
|
||||
return filepath.Join(wd, "PrintDot"), nil
|
||||
}
|
||||
return "", fmt.Errorf("failed to resolve data directory")
|
||||
}
|
||||
|
||||
func (b *Bridge) appendLogLine(line string) error {
|
||||
b.ensureLogDir()
|
||||
path, err := b.logFilePath()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !strings.HasSuffix(line, "\n") {
|
||||
line += "\n"
|
||||
}
|
||||
|
||||
b.logFileMu.Lock()
|
||||
defer b.logFileMu.Unlock()
|
||||
|
||||
f, err := os.OpenFile(path, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0644)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
_, err = f.WriteString(line)
|
||||
return err
|
||||
}
|
||||
|
||||
func (b *Bridge) readLogLines() ([]string, error) {
|
||||
b.ensureLogDir()
|
||||
path, err := b.logFilePath()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
b.logFileMu.Lock()
|
||||
defer b.logFileMu.Unlock()
|
||||
|
||||
f, err := os.Open(path)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return []string{}, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
lines := []string{}
|
||||
scanner := bufio.NewScanner(f)
|
||||
for scanner.Scan() {
|
||||
text := strings.TrimSpace(scanner.Text())
|
||||
if text == "" {
|
||||
continue
|
||||
}
|
||||
lines = append(lines, text)
|
||||
}
|
||||
|
||||
if err := scanner.Err(); err != nil {
|
||||
return lines, err
|
||||
}
|
||||
|
||||
// Reverse logs for display (newest top)
|
||||
for i, j := 0, len(lines)-1; i < j; i, j = i+1, j-1 {
|
||||
lines[i], lines[j] = lines[j], lines[i]
|
||||
}
|
||||
return lines, nil
|
||||
}
|
||||
|
||||
func (b *Bridge) clearLogFile() {
|
||||
path, err := b.logFilePath()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
b.logFileMu.Lock()
|
||||
defer b.logFileMu.Unlock()
|
||||
_ = os.Remove(path)
|
||||
}
|
||||
|
||||
func (b *Bridge) handleReloadRequest(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Access-Control-Allow-Origin", "*")
|
||||
w.Header().Set("Access-Control-Allow-Methods", "POST, OPTIONS")
|
||||
|
||||
if r.Method == http.MethodOptions {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
return
|
||||
}
|
||||
|
||||
if r.Method != http.MethodPost {
|
||||
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte("Reload initiated"))
|
||||
|
||||
// Trigger reload on main thread
|
||||
if b.onReload != nil {
|
||||
go func() {
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
b.onReload()
|
||||
}()
|
||||
}
|
||||
}
|
||||
|
||||
func (b *Bridge) handleForwarderStatus(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Access-Control-Allow-Origin", "*")
|
||||
if r.Method != http.MethodGet {
|
||||
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
status := RemoteForwarderStatus{}
|
||||
if b.forwarderStatusProvider != nil {
|
||||
status = b.forwarderStatusProvider()
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(status)
|
||||
}
|
||||
|
||||
func (b *Bridge) handleForwarderConnect(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Access-Control-Allow-Origin", "*")
|
||||
w.Header().Set("Access-Control-Allow-Methods", "POST, OPTIONS")
|
||||
|
||||
if r.Method == http.MethodOptions {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
return
|
||||
}
|
||||
if r.Method != http.MethodPost {
|
||||
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
if b.forwarderConnect != nil {
|
||||
b.forwarderConnect()
|
||||
}
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}
|
||||
|
||||
func (b *Bridge) handleForwarderDisconnect(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Access-Control-Allow-Origin", "*")
|
||||
w.Header().Set("Access-Control-Allow-Methods", "POST, OPTIONS")
|
||||
|
||||
if r.Method == http.MethodOptions {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
return
|
||||
}
|
||||
if r.Method != http.MethodPost {
|
||||
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
if b.forwarderDisconnect != nil {
|
||||
b.forwarderDisconnect()
|
||||
}
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}
|
||||
|
||||
func (b *Bridge) handleForwarderStream(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Access-Control-Allow-Origin", "*")
|
||||
w.Header().Set("Content-Type", "text/event-stream")
|
||||
w.Header().Set("Cache-Control", "no-cache")
|
||||
w.Header().Set("Connection", "keep-alive")
|
||||
|
||||
flusher, ok := w.(http.Flusher)
|
||||
if !ok {
|
||||
http.Error(w, "streaming unsupported", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
status := RemoteForwarderStatus{}
|
||||
if b.forwarderStatusProvider != nil {
|
||||
status = b.forwarderStatusProvider()
|
||||
}
|
||||
if data, err := json.Marshal(status); err == nil {
|
||||
fmt.Fprintf(w, "data: %s\n\n", data)
|
||||
flusher.Flush()
|
||||
}
|
||||
|
||||
last := status
|
||||
ctx := r.Context()
|
||||
ticker := time.NewTicker(2 * time.Second)
|
||||
defer ticker.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case <-ticker.C:
|
||||
current := RemoteForwarderStatus{}
|
||||
if b.forwarderStatusProvider != nil {
|
||||
current = b.forwarderStatusProvider()
|
||||
}
|
||||
if current != last {
|
||||
if data, err := json.Marshal(current); err == nil {
|
||||
fmt.Fprintf(w, "data: %s\n\n", data)
|
||||
flusher.Flush()
|
||||
last = current
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user