新增 XSLPrintDot 项目,包含打印服务的核心功能和相关配置。实现打印机查询、打印任务处理、远程转发功能,并支持多平台设备ID获取。优化打印数据准备逻辑,增强系统的可维护性和扩展性,同时更新工作区配置以支持新项目。
This commit is contained in:
513
XSLPrintDot/forwarder.go
Normal file
513
XSLPrintDot/forwarder.go
Normal file
@@ -0,0 +1,513 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gorilla/websocket"
|
||||
)
|
||||
|
||||
const (
|
||||
remotePingInterval = 20 * time.Second
|
||||
remoteReportInterval = 10 * time.Second
|
||||
remotePongWait = 70 * time.Second
|
||||
remoteWriteWait = 5 * time.Second
|
||||
)
|
||||
|
||||
type RemoteConfig struct {
|
||||
Server string
|
||||
AuthURL string
|
||||
WsURL string
|
||||
ClientID string
|
||||
SecretKey string
|
||||
ClientName string
|
||||
}
|
||||
|
||||
type RemoteForwarderStatus struct {
|
||||
Connected bool `json:"connected"`
|
||||
LastError string `json:"lastError"`
|
||||
LastChange int64 `json:"lastChange"`
|
||||
AutoReconnect bool `json:"autoReconnect"`
|
||||
}
|
||||
|
||||
type remoteLoginResponse struct {
|
||||
Token string `json:"token"`
|
||||
}
|
||||
|
||||
type remotePrintTask struct {
|
||||
Cmd string `json:"cmd"`
|
||||
TaskID string `json:"task_id"`
|
||||
PrintRequest
|
||||
}
|
||||
|
||||
func (b *Bridge) ConfigureRemoteForwarder(s AppSettings) {
|
||||
b.StartRemoteForwarderWithSettings(s, false)
|
||||
}
|
||||
|
||||
func (b *Bridge) StartRemoteForwarderWithSettings(s AppSettings, force bool) {
|
||||
cfg := RemoteConfig{
|
||||
Server: strings.TrimSpace(s.RemoteServer),
|
||||
AuthURL: strings.TrimSpace(s.RemoteAuthURL),
|
||||
WsURL: strings.TrimSpace(s.RemoteWsURL),
|
||||
ClientID: strings.TrimSpace(firstNonEmpty(s.RemoteClientID, s.RemoteUser)),
|
||||
SecretKey: strings.TrimSpace(firstNonEmpty(s.RemoteSecretKey, s.RemotePassword)),
|
||||
ClientName: strings.TrimSpace(s.RemoteClientName),
|
||||
}
|
||||
|
||||
if !force && !s.RemoteAutoConnect {
|
||||
b.StopRemoteForwarder()
|
||||
return
|
||||
}
|
||||
|
||||
if (cfg.AuthURL == "" && cfg.Server == "") ||
|
||||
(cfg.WsURL == "" && cfg.Server == "") ||
|
||||
cfg.ClientID == "" || cfg.SecretKey == "" {
|
||||
b.StopRemoteForwarder()
|
||||
return
|
||||
}
|
||||
|
||||
b.remoteMu.Lock()
|
||||
same := cfg == b.remoteCfg && b.remoteStop != nil
|
||||
b.remoteMu.Unlock()
|
||||
if same {
|
||||
return
|
||||
}
|
||||
|
||||
b.StopRemoteForwarder()
|
||||
|
||||
b.remoteMu.Lock()
|
||||
b.remoteCfg = cfg
|
||||
b.remoteStop = make(chan struct{})
|
||||
stop := b.remoteStop
|
||||
b.remoteMu.Unlock()
|
||||
|
||||
b.remoteWg.Add(1)
|
||||
go b.runRemoteForwarder(cfg, stop)
|
||||
}
|
||||
|
||||
func (b *Bridge) StopRemoteForwarder() {
|
||||
b.remoteMu.Lock()
|
||||
stop := b.remoteStop
|
||||
conn := b.remoteConn
|
||||
b.remoteStop = nil
|
||||
b.remoteMu.Unlock()
|
||||
|
||||
if stop != nil {
|
||||
close(stop)
|
||||
}
|
||||
if conn != nil {
|
||||
_ = conn.Close()
|
||||
}
|
||||
b.setRemoteConnected(false)
|
||||
b.remoteWg.Wait()
|
||||
}
|
||||
|
||||
func (b *Bridge) runRemoteForwarder(cfg RemoteConfig, stop <-chan struct{}) {
|
||||
defer b.remoteWg.Done()
|
||||
|
||||
for {
|
||||
if err := b.connectAndServeForwarder(cfg, stop); err != nil {
|
||||
b.setRemoteError(err)
|
||||
b.Log(fmt.Sprintf("Remote forwarder error: %v", err))
|
||||
}
|
||||
|
||||
select {
|
||||
case <-stop:
|
||||
return
|
||||
case <-time.After(3 * time.Second):
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (b *Bridge) connectAndServeForwarder(cfg RemoteConfig, stop <-chan struct{}) error {
|
||||
loginURL, wsURL, err := buildRemoteURLsFromConfig(cfg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
token, err := b.remoteLogin(loginURL, cfg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
headers := http.Header{}
|
||||
headers.Set("Authorization", "Bearer "+token)
|
||||
headers.Set("X-Client-Id", cfg.ClientID)
|
||||
if cfg.ClientName != "" {
|
||||
headers.Set("X-Client-Name", cfg.ClientName)
|
||||
}
|
||||
|
||||
conn, _, err := websocket.DefaultDialer.Dial(wsURL.String(), headers)
|
||||
if err != nil {
|
||||
return fmt.Errorf("ws connect failed: %v", err)
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
conn.SetReadLimit(8 * 1024 * 1024)
|
||||
_ = conn.SetReadDeadline(time.Now().Add(remotePongWait))
|
||||
conn.SetPongHandler(func(string) error {
|
||||
return conn.SetReadDeadline(time.Now().Add(remotePongWait))
|
||||
})
|
||||
|
||||
b.setRemoteConn(conn)
|
||||
b.setRemoteConnected(true)
|
||||
defer func() {
|
||||
b.clearRemoteConn(conn)
|
||||
b.setRemoteConnected(false)
|
||||
}()
|
||||
|
||||
b.Log(fmt.Sprintf("Remote forwarder connected: %s", wsURL.String()))
|
||||
|
||||
if err := b.reportPrinters(conn); err != nil {
|
||||
b.Log(fmt.Sprintf("Report printers failed: %v", err))
|
||||
}
|
||||
|
||||
pingStop := make(chan struct{})
|
||||
go b.pingLoop(conn, pingStop)
|
||||
go b.reportLoop(conn, pingStop)
|
||||
defer close(pingStop)
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-stop:
|
||||
return nil
|
||||
default:
|
||||
}
|
||||
|
||||
_ = conn.SetReadDeadline(time.Now().Add(remotePongWait))
|
||||
var rawMsg map[string]interface{}
|
||||
if err := conn.ReadJSON(&rawMsg); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd, _ := rawMsg["cmd"].(string)
|
||||
switch strings.ToLower(strings.TrimSpace(cmd)) {
|
||||
case "print_task":
|
||||
jsonBody, _ := json.Marshal(rawMsg)
|
||||
var task remotePrintTask
|
||||
if err := json.Unmarshal(jsonBody, &task); err != nil {
|
||||
b.Log(fmt.Sprintf("Invalid print task: %v", err))
|
||||
continue
|
||||
}
|
||||
|
||||
message, err := b.processPrintRequest(task.PrintRequest)
|
||||
status := "success"
|
||||
if err != nil {
|
||||
status = "failed"
|
||||
}
|
||||
|
||||
resp := map[string]interface{}{
|
||||
"cmd": "report_result",
|
||||
"task_id": task.TaskID,
|
||||
"status": status,
|
||||
"message": message,
|
||||
}
|
||||
if err := writeJSONWithDeadline(conn, resp); err != nil {
|
||||
b.Log(fmt.Sprintf("Report result failed: %v", err))
|
||||
}
|
||||
case "get_printers":
|
||||
if err := b.reportPrinters(conn); err != nil {
|
||||
b.Log(fmt.Sprintf("Report printers failed: %v", err))
|
||||
}
|
||||
case "auth_resp":
|
||||
b.Log("Remote forwarder auth ok")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (b *Bridge) remoteLogin(loginURL *url.URL, cfg RemoteConfig) (string, error) {
|
||||
payload := map[string]string{
|
||||
"client_id": cfg.ClientID,
|
||||
"secret_key": cfg.SecretKey,
|
||||
}
|
||||
if cfg.ClientName != "" {
|
||||
payload["client_name"] = cfg.ClientName
|
||||
}
|
||||
|
||||
body, err := json.Marshal(payload)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
req, err := http.NewRequest("POST", loginURL.String(), bytes.NewReader(body))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
client := &http.Client{Timeout: 10 * time.Second}
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
||||
return "", fmt.Errorf("login failed: status %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
var loginResp remoteLoginResponse
|
||||
if err := json.NewDecoder(resp.Body).Decode(&loginResp); err != nil {
|
||||
return "", err
|
||||
}
|
||||
if strings.TrimSpace(loginResp.Token) == "" {
|
||||
return "", fmt.Errorf("login failed: empty token")
|
||||
}
|
||||
return loginResp.Token, nil
|
||||
}
|
||||
|
||||
func (b *Bridge) reportPrinters(conn *websocket.Conn) error {
|
||||
printers, err := b.GetPrinters()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
list := make([]map[string]interface{}, 0, len(printers))
|
||||
for _, p := range printers {
|
||||
caps, capsErr := b.GetPrinterCapabilities(p.Name)
|
||||
if capsErr != nil {
|
||||
b.Log(fmt.Sprintf("Get printer capabilities failed: %s: %v", p.Name, capsErr))
|
||||
caps = map[string]interface{}{}
|
||||
}
|
||||
list = append(list, map[string]interface{}{
|
||||
"printer_name": p.Name,
|
||||
"printer_type": "system",
|
||||
"paper_spec": "",
|
||||
"is_ready": true,
|
||||
"supported_format": "pdf",
|
||||
"capabilities": caps,
|
||||
})
|
||||
}
|
||||
|
||||
payload := map[string]interface{}{
|
||||
"cmd": "report_printers",
|
||||
"printers": list,
|
||||
}
|
||||
return writeJSONWithDeadline(conn, payload)
|
||||
}
|
||||
|
||||
func (b *Bridge) pingLoop(conn *websocket.Conn, stop <-chan struct{}) {
|
||||
ticker := time.NewTicker(remotePingInterval)
|
||||
defer ticker.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-stop:
|
||||
return
|
||||
case <-ticker.C:
|
||||
deadline := time.Now().Add(remoteWriteWait)
|
||||
_ = conn.WriteControl(websocket.PingMessage, []byte("ping"), deadline)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (b *Bridge) reportLoop(conn *websocket.Conn, stop <-chan struct{}) {
|
||||
ticker := time.NewTicker(remoteReportInterval)
|
||||
defer ticker.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-stop:
|
||||
return
|
||||
case <-ticker.C:
|
||||
if err := b.reportPrinters(conn); err != nil {
|
||||
b.Log(fmt.Sprintf("Report printers failed: %v", err))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func writeJSONWithDeadline(conn *websocket.Conn, payload interface{}) error {
|
||||
_ = conn.SetWriteDeadline(time.Now().Add(remoteWriteWait))
|
||||
return conn.WriteJSON(payload)
|
||||
}
|
||||
|
||||
func buildRemoteURLs(raw string) (*url.URL, *url.URL, error) {
|
||||
baseURL, err := normalizeRemoteBaseURL(raw)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
loginBase := *baseURL
|
||||
switch loginBase.Scheme {
|
||||
case "ws":
|
||||
loginBase.Scheme = "http"
|
||||
case "wss":
|
||||
loginBase.Scheme = "https"
|
||||
}
|
||||
|
||||
wsBase := *baseURL
|
||||
switch wsBase.Scheme {
|
||||
case "http":
|
||||
wsBase.Scheme = "ws"
|
||||
case "https":
|
||||
wsBase.Scheme = "wss"
|
||||
}
|
||||
|
||||
loginURL := loginBase.ResolveReference(&url.URL{Path: "/api/client/login"})
|
||||
wsURL := wsBase.ResolveReference(&url.URL{Path: "/ws/client"})
|
||||
return loginURL, wsURL, nil
|
||||
}
|
||||
|
||||
func buildRemoteURLsFromConfig(cfg RemoteConfig) (*url.URL, *url.URL, error) {
|
||||
var loginURL *url.URL
|
||||
var wsURL *url.URL
|
||||
|
||||
if cfg.AuthURL != "" {
|
||||
parsed, err := normalizeAuthURL(cfg.AuthURL)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
loginURL = parsed
|
||||
}
|
||||
|
||||
if cfg.WsURL != "" {
|
||||
parsed, err := normalizeWsURL(cfg.WsURL)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
wsURL = parsed
|
||||
}
|
||||
|
||||
if (loginURL == nil || wsURL == nil) && cfg.Server != "" {
|
||||
baseLoginURL, baseWsURL, err := buildRemoteURLs(cfg.Server)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
if loginURL == nil {
|
||||
loginURL = baseLoginURL
|
||||
}
|
||||
if wsURL == nil {
|
||||
wsURL = baseWsURL
|
||||
}
|
||||
}
|
||||
|
||||
if loginURL == nil || wsURL == nil {
|
||||
return nil, nil, fmt.Errorf("auth or ws address is missing")
|
||||
}
|
||||
|
||||
return loginURL, wsURL, nil
|
||||
}
|
||||
|
||||
func normalizeAuthURL(raw string) (*url.URL, error) {
|
||||
trimmed := strings.TrimSpace(raw)
|
||||
if trimmed == "" {
|
||||
return nil, fmt.Errorf("auth address is empty")
|
||||
}
|
||||
if !strings.Contains(trimmed, "://") {
|
||||
trimmed = "http://" + trimmed
|
||||
}
|
||||
parsed, err := url.Parse(trimmed)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if parsed.Host == "" {
|
||||
return nil, fmt.Errorf("invalid auth address")
|
||||
}
|
||||
if parsed.Path == "" || parsed.Path == "/" {
|
||||
parsed.Path = "/api/client/login"
|
||||
}
|
||||
switch parsed.Scheme {
|
||||
case "ws":
|
||||
parsed.Scheme = "http"
|
||||
case "wss":
|
||||
parsed.Scheme = "https"
|
||||
}
|
||||
return parsed, nil
|
||||
}
|
||||
|
||||
func normalizeWsURL(raw string) (*url.URL, error) {
|
||||
trimmed := strings.TrimSpace(raw)
|
||||
if trimmed == "" {
|
||||
return nil, fmt.Errorf("ws address is empty")
|
||||
}
|
||||
if !strings.Contains(trimmed, "://") {
|
||||
trimmed = "ws://" + trimmed
|
||||
}
|
||||
parsed, err := url.Parse(trimmed)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if parsed.Host == "" {
|
||||
return nil, fmt.Errorf("invalid ws address")
|
||||
}
|
||||
if parsed.Path == "" || parsed.Path == "/" {
|
||||
parsed.Path = "/ws/client"
|
||||
}
|
||||
switch parsed.Scheme {
|
||||
case "http":
|
||||
parsed.Scheme = "ws"
|
||||
case "https":
|
||||
parsed.Scheme = "wss"
|
||||
}
|
||||
return parsed, nil
|
||||
}
|
||||
|
||||
func normalizeRemoteBaseURL(raw string) (*url.URL, error) {
|
||||
trimmed := strings.TrimSpace(raw)
|
||||
if trimmed == "" {
|
||||
return nil, fmt.Errorf("remote server is empty")
|
||||
}
|
||||
|
||||
if !strings.Contains(trimmed, "://") {
|
||||
trimmed = "http://" + trimmed
|
||||
}
|
||||
|
||||
parsed, err := url.Parse(trimmed)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if parsed.Host == "" {
|
||||
return nil, fmt.Errorf("invalid remote server address")
|
||||
}
|
||||
parsed.Path = strings.TrimRight(parsed.Path, "/")
|
||||
parsed.RawQuery = ""
|
||||
parsed.Fragment = ""
|
||||
|
||||
return parsed, nil
|
||||
}
|
||||
|
||||
func (b *Bridge) setRemoteConn(conn *websocket.Conn) {
|
||||
b.remoteMu.Lock()
|
||||
b.remoteConn = conn
|
||||
b.remoteMu.Unlock()
|
||||
}
|
||||
|
||||
func (b *Bridge) clearRemoteConn(conn *websocket.Conn) {
|
||||
b.remoteMu.Lock()
|
||||
if b.remoteConn == conn {
|
||||
b.remoteConn = nil
|
||||
}
|
||||
b.remoteMu.Unlock()
|
||||
}
|
||||
|
||||
func (b *Bridge) setRemoteConnected(connected bool) {
|
||||
b.remoteMu.Lock()
|
||||
b.remoteStatus.Connected = connected
|
||||
if connected {
|
||||
b.remoteStatus.LastError = ""
|
||||
}
|
||||
b.remoteStatus.LastChange = time.Now().Unix()
|
||||
b.remoteMu.Unlock()
|
||||
}
|
||||
|
||||
func (b *Bridge) setRemoteError(err error) {
|
||||
if err == nil {
|
||||
return
|
||||
}
|
||||
b.remoteMu.Lock()
|
||||
b.remoteStatus.LastError = err.Error()
|
||||
b.remoteStatus.LastChange = time.Now().Unix()
|
||||
b.remoteMu.Unlock()
|
||||
}
|
||||
|
||||
func (b *Bridge) GetRemoteForwarderStatus() RemoteForwarderStatus {
|
||||
b.remoteMu.Lock()
|
||||
defer b.remoteMu.Unlock()
|
||||
return b.remoteStatus
|
||||
}
|
||||
Reference in New Issue
Block a user