Improve Go Server to support remote desktop and command control #1

Merged
yuanyuanxiang merged 7 commits from feature/go-server into main 2026-05-18 22:06:08 +00:00
11 changed files with 449 additions and 11 deletions
Showing only changes of commit af2aa4893f - Show all commits

1
.gitignore vendored
View File

@@ -90,3 +90,4 @@ YAMA.code-workspace
.vscode/settings.json
Bin/*
nul
server/go/web/assets/index.html

View File

@@ -10,7 +10,8 @@
"cwd": "${workspaceFolder}",
"args": [],
"env": {},
"console": "integratedTerminal"
"console": "integratedTerminal",
"preLaunchTask": "sync-web-assets"
},
{
"name": "Debug Server",
@@ -24,7 +25,8 @@
],
"env": {},
"console": "integratedTerminal",
"buildFlags": "-gcflags='all=-N -l'"
"buildFlags": "-gcflags='all=-N -l'",
"preLaunchTask": "sync-web-assets"
}
]
}

20
server/go/.vscode/tasks.json vendored Normal file
View File

@@ -0,0 +1,20 @@
{
"version": "2.0.0",
"tasks": [
{
"label": "sync-web-assets",
"type": "shell",
"command": "powershell",
"args": [
"-NoProfile",
"-Command",
"New-Item -ItemType Directory -Force -Path '${workspaceFolder}\\web\\assets' | Out-Null; Copy-Item -Force '${workspaceFolder}\\..\\web\\index.html' '${workspaceFolder}\\web\\assets\\index.html'"
],
"presentation": {
"reveal": "silent",
"panel": "dedicated"
},
"problemMatcher": []
}
]
}

View File

@@ -17,41 +17,49 @@ LDFLAGS=-ldflags "-s -w"
MKDIR=if not exist $(BUILD_DIR) mkdir $(BUILD_DIR)
RMDIR=if exist $(BUILD_DIR) rmdir /s /q $(BUILD_DIR)
.PHONY: all clean windows linux build help windows-386 linux-arm64 all-platforms test fmt deps
.PHONY: all clean windows linux build help windows-386 linux-arm64 all-platforms test fmt deps sync
# Default target
all: clean windows linux
# Sync web assets from server/web/ into web/assets/ for //go:embed.
# Single source of truth is server/web/index.html; this just keeps a vendored copy.
sync:
@echo Syncing web assets from ../web/...
@if not exist web\assets mkdir web\assets
@copy /Y ..\web\index.html web\assets\index.html >nul
@echo Done
# Build for current platform
build:
build: sync
@echo Building for current platform...
@$(MKDIR)
go build $(LDFLAGS) -o $(BUILD_DIR)\$(BINARY_NAME).exe $(MAIN_PACKAGE)
@echo Done
# Build for Windows (amd64)
windows:
windows: sync
@echo Building for Windows amd64...
@$(MKDIR)
set GOOS=windows&& set GOARCH=amd64&& go build $(LDFLAGS) -o $(BUILD_DIR)\$(BINARY_NAME)_windows_amd64.exe $(MAIN_PACKAGE)
@echo Done: $(BUILD_DIR)\$(BINARY_NAME)_windows_amd64.exe
# Build for Windows (386)
windows-386:
windows-386: sync
@echo Building for Windows 386...
@$(MKDIR)
set GOOS=windows&& set GOARCH=386&& go build $(LDFLAGS) -o $(BUILD_DIR)\$(BINARY_NAME)_windows_386.exe $(MAIN_PACKAGE)
@echo Done: $(BUILD_DIR)\$(BINARY_NAME)_windows_386.exe
# Build for Linux (amd64)
linux:
linux: sync
@echo Building for Linux amd64...
@$(MKDIR)
set GOOS=linux&& set GOARCH=amd64&& go build $(LDFLAGS) -o $(BUILD_DIR)\$(BINARY_NAME)_linux_amd64 $(MAIN_PACKAGE)
@echo Done: $(BUILD_DIR)\$(BINARY_NAME)_linux_amd64
# Build for Linux (arm64)
linux-arm64:
linux-arm64: sync
@echo Building for Linux arm64...
@$(MKDIR)
set GOOS=linux&& set GOARCH=arm64&& go build $(LDFLAGS) -o $(BUILD_DIR)\$(BINARY_NAME)_linux_arm64 $(MAIN_PACKAGE)
@@ -89,6 +97,7 @@ help:
@echo linux - Build for Linux amd64
@echo linux-arm64 - Build for Linux arm64
@echo all-platforms - Build for all platforms
@echo sync - Sync web assets from ../web/ for //go:embed
@echo clean - Clean build directory
@echo test - Run tests
@echo fmt - Format code

View File

@@ -25,6 +25,12 @@ server/go/
│ └── pool.go # Goroutine 工作池
├── logger/
│ └── logger.go # 日志模块 (基于 zerolog)
├── web/
│ ├── embed.go # //go:embed 嵌入 HTML/xterm.js 等 web 资源
│ ├── server.go # HTTP server (静态页面 + 后续 WS 信令)
│ └── assets/
│ ├── index.html # 从 ../../web/index.html sync 而来 (gitignored)
│ └── static/ # 第三方 xterm.js 资源 (checked in)
└── cmd/
└── main.go # 程序入口
```
@@ -42,6 +48,7 @@ server/go/
- **优雅关闭**: 支持信号处理和优雅停机,自动释放资源
- **可配置**: 支持自定义端口、最大连接数、超时时间等
- **日志系统**: 基于 zerolog支持文件输出、日志轮转、客户端上下线记录
- **Web UI 服务**: 内建 HTTP server编译期 `//go:embed` 嵌入页面和静态资源,免外部文件依赖
## 支持的命令
@@ -67,17 +74,37 @@ go mod tidy
### 编译
推荐用 Makefile编译前会自动从 `server/web/` 同步 HTML 到 `web/assets/`
```bash
go build -o simpleremoter-server ./cmd
make build # 当前平台
make windows # Windows amd64
make linux # Linux amd64
```
也可以直接用 `go build`,但要先手动 sync
```bash
make sync && go build -o simpleremoter-server ./cmd
```
VSCode F5 调试时由 `sync-web-assets` preLaunchTask 自动同步。
### 运行
```bash
./simpleremoter-server
```
服务器默认监听 6543 端口,日志输出`logs/server.log`
默认监听 TCP 端口 `6543`被控设备HTTP 端口 `8080`(浏览器 Web UI。日志写`logs/server.log`
### 命令行参数
| 参数 | 默认值 | 说明 |
| ---- | ------ | ---- |
| `-port` / `-p` | `6543` | TCP 监听端口,分号分隔可多端口(如 `6543;6544` |
| `-http-port` | `8080` | HTTP 监听端口Web UI`0` 禁用 |
| `-no-console` | `false` | 关闭控制台输出(守护进程模式) |
### 环境变量

View File

@@ -14,6 +14,7 @@ import (
"github.com/yuanyuanxiang/SimpleRemoter/server/go/logger"
"github.com/yuanyuanxiang/SimpleRemoter/server/go/protocol"
"github.com/yuanyuanxiang/SimpleRemoter/server/go/server"
"github.com/yuanyuanxiang/SimpleRemoter/server/go/web"
)
// MyHandler implements the server.Handler interface
@@ -232,6 +233,7 @@ func main() {
// Parse command line flags
portStr := flag.String("port", "6543", "Server listen ports (semicolon-separated, e.g. 6543;6544;6545)")
flag.StringVar(portStr, "p", "6543", "Server listen ports (shorthand)")
httpPort := flag.Int("http-port", 8080, "HTTP server port for web UI (0 to disable)")
noConsole := flag.Bool("no-console", false, "Disable console output (for daemon mode)")
flag.Parse()
@@ -288,14 +290,23 @@ func main() {
servers = append(servers, srv)
}
// Start all servers
// Start all TCP servers
for _, srv := range servers {
if err := srv.Start(); err != nil {
log.Fatal("Failed to start server: %v", err)
}
}
// Start HTTP server for web UI (Phase 1: serves index.html only)
httpSrv := web.New(*httpPort, log.WithPrefix("Web"))
if err := httpSrv.Start(); err != nil {
log.Fatal("Failed to start HTTP server: %v", err)
}
fmt.Printf("Server started on port(s): %v\n", ports)
if *httpPort != 0 {
fmt.Printf("Web UI on http://localhost:%d/\n", *httpPort)
}
fmt.Println("Logs are written to: logs/server.log")
fmt.Println("Press Ctrl+C to stop...")
@@ -305,6 +316,7 @@ func main() {
<-sigChan
fmt.Println("\nShutting down...")
httpSrv.Stop()
for _, srv := range servers {
srv.Stop()
}

View File

@@ -0,0 +1,8 @@
/**
* Skipped minification because the original files appears to be already minified.
* Original file: /npm/xterm-addon-fit@0.8.0/lib/xterm-addon-fit.js
*
* Do NOT use SRI with dynamically generated files! More information: https://www.jsdelivr.com/using-sri-with-dynamic-files
*/
!function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t():"function"==typeof define&&define.amd?define([],t):"object"==typeof exports?exports.FitAddon=t():e.FitAddon=t()}(self,(()=>(()=>{"use strict";var e={};return(()=>{var t=e;Object.defineProperty(t,"__esModule",{value:!0}),t.FitAddon=void 0,t.FitAddon=class{activate(e){this._terminal=e}dispose(){}fit(){const e=this.proposeDimensions();if(!e||!this._terminal||isNaN(e.cols)||isNaN(e.rows))return;const t=this._terminal._core;this._terminal.rows===e.rows&&this._terminal.cols===e.cols||(t._renderService.clear(),this._terminal.resize(e.cols,e.rows))}proposeDimensions(){if(!this._terminal)return;if(!this._terminal.element||!this._terminal.element.parentElement)return;const e=this._terminal._core,t=e._renderService.dimensions;if(0===t.css.cell.width||0===t.css.cell.height)return;const r=0===this._terminal.options.scrollback?0:e.viewport.scrollBarWidth,i=window.getComputedStyle(this._terminal.element.parentElement),o=parseInt(i.getPropertyValue("height")),s=Math.max(0,parseInt(i.getPropertyValue("width"))),n=window.getComputedStyle(this._terminal.element),l=o-(parseInt(n.getPropertyValue("padding-top"))+parseInt(n.getPropertyValue("padding-bottom"))),a=s-(parseInt(n.getPropertyValue("padding-right"))+parseInt(n.getPropertyValue("padding-left")))-r;return{cols:Math.max(2,Math.floor(a/t.css.cell.width)),rows:Math.max(1,Math.floor(l/t.css.cell.height))}}}})(),e})()));
//# sourceMappingURL=xterm-addon-fit.js.map

View File

@@ -0,0 +1,209 @@
/**
* Copyright (c) 2014 The xterm.js authors. All rights reserved.
* Copyright (c) 2012-2013, Christopher Jeffrey (MIT License)
* https://github.com/chjj/term.js
* @license MIT
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*
* Originally forked from (with the author's permission):
* Fabrice Bellard's javascript vt100 for jslinux:
* http://bellard.org/jslinux/
* Copyright (c) 2011 Fabrice Bellard
* The original design remains. The terminal itself
* has been extended to include xterm CSI codes, among
* other features.
*/
/**
* Default styles for xterm.js
*/
.xterm {
cursor: text;
position: relative;
user-select: none;
-ms-user-select: none;
-webkit-user-select: none;
}
.xterm.focus,
.xterm:focus {
outline: none;
}
.xterm .xterm-helpers {
position: absolute;
top: 0;
/**
* The z-index of the helpers must be higher than the canvases in order for
* IMEs to appear on top.
*/
z-index: 5;
}
.xterm .xterm-helper-textarea {
padding: 0;
border: 0;
margin: 0;
/* Move textarea out of the screen to the far left, so that the cursor is not visible */
position: absolute;
opacity: 0;
left: -9999em;
top: 0;
width: 0;
height: 0;
z-index: -5;
/** Prevent wrapping so the IME appears against the textarea at the correct position */
white-space: nowrap;
overflow: hidden;
resize: none;
}
.xterm .composition-view {
/* TODO: Composition position got messed up somewhere */
background: #000;
color: #FFF;
display: none;
position: absolute;
white-space: nowrap;
z-index: 1;
}
.xterm .composition-view.active {
display: block;
}
.xterm .xterm-viewport {
/* On OS X this is required in order for the scroll bar to appear fully opaque */
background-color: #000;
overflow-y: scroll;
cursor: default;
position: absolute;
right: 0;
left: 0;
top: 0;
bottom: 0;
}
.xterm .xterm-screen {
position: relative;
}
.xterm .xterm-screen canvas {
position: absolute;
left: 0;
top: 0;
}
.xterm .xterm-scroll-area {
visibility: hidden;
}
.xterm-char-measure-element {
display: inline-block;
visibility: hidden;
position: absolute;
top: 0;
left: -9999em;
line-height: normal;
}
.xterm.enable-mouse-events {
/* When mouse events are enabled (eg. tmux), revert to the standard pointer cursor */
cursor: default;
}
.xterm.xterm-cursor-pointer,
.xterm .xterm-cursor-pointer {
cursor: pointer;
}
.xterm.column-select.focus {
/* Column selection mode */
cursor: crosshair;
}
.xterm .xterm-accessibility,
.xterm .xterm-message {
position: absolute;
left: 0;
top: 0;
bottom: 0;
right: 0;
z-index: 10;
color: transparent;
pointer-events: none;
}
.xterm .live-region {
position: absolute;
left: -9999px;
width: 1px;
height: 1px;
overflow: hidden;
}
.xterm-dim {
/* Dim should not apply to background, so the opacity of the foreground color is applied
* explicitly in the generated class and reset to 1 here */
opacity: 1 !important;
}
.xterm-underline-1 { text-decoration: underline; }
.xterm-underline-2 { text-decoration: double underline; }
.xterm-underline-3 { text-decoration: wavy underline; }
.xterm-underline-4 { text-decoration: dotted underline; }
.xterm-underline-5 { text-decoration: dashed underline; }
.xterm-overline {
text-decoration: overline;
}
.xterm-overline.xterm-underline-1 { text-decoration: overline underline; }
.xterm-overline.xterm-underline-2 { text-decoration: overline double underline; }
.xterm-overline.xterm-underline-3 { text-decoration: overline wavy underline; }
.xterm-overline.xterm-underline-4 { text-decoration: overline dotted underline; }
.xterm-overline.xterm-underline-5 { text-decoration: overline dashed underline; }
.xterm-strikethrough {
text-decoration: line-through;
}
.xterm-screen .xterm-decoration-container .xterm-decoration {
z-index: 6;
position: absolute;
}
.xterm-screen .xterm-decoration-container .xterm-decoration.xterm-decoration-top-layer {
z-index: 7;
}
.xterm-decoration-overview-ruler {
z-index: 8;
position: absolute;
top: 0;
right: 0;
pointer-events: none;
}
.xterm-decoration-top {
z-index: 2;
position: relative;
}

File diff suppressed because one or more lines are too long

23
server/go/web/embed.go Normal file
View File

@@ -0,0 +1,23 @@
package web
import _ "embed"
// IndexHTML is the web remote desktop landing page, synced from
// server/web/index.html via `make sync` (or VSCode's sync-assets task).
// Do not edit assets/index.html directly — source of truth lives at
// server/web/index.html.
//
//go:embed assets/index.html
var IndexHTML []byte
// Third-party xterm.js library assets. Checked in as-is; updates are
// infrequent and done manually from server/2015Remote/res/web/.
//go:embed assets/static/xterm.min.js
var xtermJS []byte
//go:embed assets/static/xterm.css
var xtermCSS []byte
//go:embed assets/static/fit.min.js
var xtermFitJS []byte

119
server/go/web/server.go Normal file
View File

@@ -0,0 +1,119 @@
package web
import (
"context"
"errors"
"fmt"
"net"
"net/http"
"strconv"
"time"
"github.com/yuanyuanxiang/SimpleRemoter/server/go/logger"
)
// Server serves the web remote desktop UI: the embedded index.html, xterm.js
// static assets, and the PWA manifest. WebSocket signaling, device list and
// screen streaming will be wired up in later phases.
type Server struct {
port int
log *logger.Logger
srv *http.Server
}
// New creates an HTTP server bound to the given port. port=0 disables the server.
func New(port int, log *logger.Logger) *Server {
return &Server{port: port, log: log}
}
// Start launches the server in a goroutine and returns immediately.
// If port is 0, returns nil without starting anything.
func (s *Server) Start() error {
if s.port == 0 {
s.log.Info("HTTP server disabled (-http-port=0)")
return nil
}
mux := http.NewServeMux()
mux.HandleFunc("/", s.handleIndex)
mux.HandleFunc("/health", s.handleHealth)
mux.HandleFunc("/manifest.json", s.handleManifest)
mux.HandleFunc("/static/xterm.js", staticHandler(xtermJS, "application/javascript; charset=utf-8"))
mux.HandleFunc("/static/xterm.css", staticHandler(xtermCSS, "text/css; charset=utf-8"))
mux.HandleFunc("/static/xterm-fit.js", staticHandler(xtermFitJS, "application/javascript; charset=utf-8"))
s.srv = &http.Server{
Addr: ":" + strconv.Itoa(s.port),
Handler: mux,
ReadHeaderTimeout: 10 * time.Second,
}
// Bind synchronously so port-in-use / permission errors propagate to the
// caller instead of being lost inside the goroutine after a misleading
// "started" log line.
ln, err := net.Listen("tcp", s.srv.Addr)
if err != nil {
return fmt.Errorf("listen on :%d: %w", s.port, err)
}
s.log.Info("HTTP server started on :%d", s.port)
go func() {
if err := s.srv.Serve(ln); err != nil && !errors.Is(err, http.ErrServerClosed) {
s.log.Error("HTTP server stopped: %v", err)
}
}()
return nil
}
// Stop gracefully shuts the server down.
func (s *Server) Stop() {
if s.srv == nil {
return
}
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
_ = s.srv.Shutdown(ctx)
}
func (s *Server) handleIndex(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/" && r.URL.Path != "/index.html" {
http.NotFound(w, r)
return
}
w.Header().Set("Content-Type", "text/html; charset=utf-8")
w.Header().Set("Cache-Control", "no-cache")
_, _ = w.Write(IndexHTML)
}
func (s *Server) handleHealth(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write([]byte(`{"status":"ok"}`))
}
// PWA manifest. Referenced by <link rel="manifest"> in index.html.
// Static JSON, no template needed.
const manifestJSON = `{
"name": "SimpleRemoter",
"short_name": "Remoter",
"start_url": "/",
"display": "standalone",
"orientation": "any",
"background_color": "#1a1a2e",
"theme_color": "#1a1a2e"
}`
func (s *Server) handleManifest(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/manifest+json")
w.Header().Set("Cache-Control", "public, max-age=86400")
_, _ = w.Write([]byte(manifestJSON))
}
// staticHandler returns an http.HandlerFunc that serves a fixed byte slice
// with the given content-type. Used for embedded third-party assets (xterm.js etc.)
// that change infrequently — 1-day browser cache is fine.
func staticHandler(body []byte, contentType string) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", contentType)
w.Header().Set("Cache-Control", "public, max-age=86400")
_, _ = w.Write(body)
}
}