feat(rdp): 新增 RDP 远程桌面/SMB/账单同步能力 + 清理编译产物
主要变更: - RDP 远程桌面:新增 backend/pkg/rdp(RDP 引擎 + credssp/display/rle/trace)、 backend/controller/rdp*(连接/凭证/文件/会话/实时)、frontend RDP 组件与 API - Windows SMB 文件传输:backend/pkg/winsmb - 云账单同步:backend/pkg/billingsync + billing_sync/aliyun_billing,前端账单详情 - Agent CLI Kubernetes 工具:kubernetes_action/get、pod_execute/logs - 服务器导入助手:server_import_helpers、ImportCredentialSettings - 国际化与组件重构(cloud/deploy/monitor/server/system 等多模块) - 清理编译产物二进制:移除 backend/server、backend/agent/bin/aiops-agent-* - .gitignore:新增 backend/agent/ 与 .cache/(避免 334MB go-build 缓存误入库) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
1 parent
777d9e4e71
commit
76ec9387f0
202 files changed
+16555
-2543
No files matched your search
@@ -0,0 +1,295 @@
|
||||
package rdp
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"ezops/pkg/rdp/display"
|
||||
"ezops/pkg/rdp/trace"
|
||||
"ezops/pkg/rdp/transport"
|
||||
)
|
||||
|
||||
// SessionEngine 是自研完整 RDP 图形会话引擎 // Logan.Li
|
||||
type SessionEngine struct{}
|
||||
|
||||
func (e *SessionEngine) Run(ctx context.Context, cfg Config, sink FrameSink, events <-chan ClientEvent) error {
|
||||
if cfg.Port == 0 {
|
||||
cfg.Port = 3389
|
||||
}
|
||||
trace.Logf("engine=rdp connect target=%s:%d size=%dx%d security=%s", cfg.Host, cfg.Port, cfg.Width, cfg.Height, cfg.Security)
|
||||
_ = sink.SendFrame(ServerFrame{
|
||||
Type: "status",
|
||||
Message: fmt.Sprintf("正在连接 RDP %s:%d", cfg.Host, cfg.Port),
|
||||
})
|
||||
|
||||
sess, err := OpenSession(ctx, cfg)
|
||||
if err != nil {
|
||||
trace.Logf("engine=rdp open failed target=%s:%d err=%v", cfg.Host, cfg.Port, err)
|
||||
_ = sink.SendFrame(ServerFrame{Type: "error", Message: "RDP 会话建立失败:" + err.Error()})
|
||||
return err
|
||||
}
|
||||
defer func() {
|
||||
if c, ok := sess.Conn.(io.Closer); ok {
|
||||
_ = c.Close()
|
||||
}
|
||||
}()
|
||||
|
||||
_ = sink.SendFrame(ServerFrame{Type: "status", Message: "RDP 图形会话已建立,等待桌面画面..."})
|
||||
trace.Logf("engine=rdp open ok target=%s:%d", cfg.Host, cfg.Port)
|
||||
writeMu := &sync.Mutex{}
|
||||
input := NewInputSenderWithLock(sess.Conn, writeMu)
|
||||
receiver := NewFastPathReceiver()
|
||||
|
||||
readErr := make(chan error, 1)
|
||||
go func() {
|
||||
readErr <- e.readLoop(ctx, sess, receiver, sink, writeMu)
|
||||
}()
|
||||
go e.inputLoop(ctx, input, sink, events)
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
case err := <-readErr:
|
||||
if err != nil {
|
||||
_ = sink.SendFrame(ServerFrame{Type: "error", Message: err.Error()})
|
||||
}
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (e *SessionEngine) inputLoop(ctx context.Context, input *InputSender, sink FrameSink, events <-chan ClientEvent) {
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case event, ok := <-events:
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
if event.Type != "mousemove" && event.Type != "mouse_move" {
|
||||
trace.Logf("engine=rdp input type=%s x=%d y=%d button=%d code=%s down=%t", event.Type, event.X, event.Y, event.Button, event.Code, event.Down)
|
||||
}
|
||||
if err := e.handleEvent(input, sink, event); err != nil {
|
||||
_ = sink.SendFrame(ServerFrame{Type: "status", Message: "输入事件失败:" + err.Error()})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (e *SessionEngine) readLoop(ctx context.Context, sess *Session, receiver *FastPathReceiver, sink FrameSink, writeMu *sync.Mutex) error {
|
||||
framebuffer := display.NewFramebuffer(sess.Width, sess.Height)
|
||||
ticker := time.NewTicker(16 * time.Millisecond)
|
||||
defer ticker.Stop()
|
||||
refreshRequested := false
|
||||
flushFrame := func() {
|
||||
frame, ok := framebuffer.FlushRGBA()
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
_ = sink.SendFrame(ServerFrame{
|
||||
Type: "bitmap",
|
||||
X: frame.X,
|
||||
Y: frame.Y,
|
||||
Width: frame.Width,
|
||||
Height: frame.Height,
|
||||
RGBA: frame.RGBA,
|
||||
})
|
||||
}
|
||||
sendBitmaps := func(rects []BitmapRect) {
|
||||
for _, rect := range rects {
|
||||
if rect.Width <= 0 || rect.Height <= 0 {
|
||||
continue
|
||||
}
|
||||
if !refreshRequested && isBlackFullScreen(rect, sess.Width, sess.Height) {
|
||||
refreshRequested = true
|
||||
go func() {
|
||||
time.Sleep(400 * time.Millisecond)
|
||||
if err := e.requestRefresh(sess, writeMu); err != nil {
|
||||
trace.Logf("engine=rdp refresh request failed err=%v", err)
|
||||
}
|
||||
}()
|
||||
}
|
||||
framebuffer.Draw(display.Bitmap{
|
||||
X: rect.DestLeft,
|
||||
Y: rect.DestTop,
|
||||
Width: rect.Width,
|
||||
Height: rect.Height,
|
||||
BitsPerPixel: rect.BitsPerPixel,
|
||||
Data: rect.Data,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
type packetResult struct {
|
||||
packet WirePacket
|
||||
err error
|
||||
}
|
||||
packetCh := make(chan packetResult, 1)
|
||||
go func() {
|
||||
for {
|
||||
pkt, err := ReadWirePacket(sess.Conn)
|
||||
select {
|
||||
case packetCh <- packetResult{packet: pkt, err: err}:
|
||||
case <-ctx.Done():
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
flushFrame()
|
||||
return ctx.Err()
|
||||
case <-ticker.C:
|
||||
flushFrame()
|
||||
case got := <-packetCh:
|
||||
if got.err != nil {
|
||||
flushFrame()
|
||||
return got.err
|
||||
}
|
||||
var rects []BitmapRect
|
||||
var err error
|
||||
if got.packet.FastPath {
|
||||
rects, err = receiver.Handle(got.packet.SecFlag, got.packet.Payload)
|
||||
} else {
|
||||
rects, err = receiver.HandleSlowPath(got.packet.Payload)
|
||||
}
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
if err := e.ackSurfaceFrames(sess, receiver.TakeFrameIDs(), writeMu); err != nil {
|
||||
trace.Logf("engine=rdp frame ack failed err=%v", err)
|
||||
}
|
||||
if len(rects) > 0 {
|
||||
trace.Logf("engine=rdp bitmap rects=%d fastpath=%t", len(rects), got.packet.FastPath)
|
||||
sendBitmaps(rects)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func isBlackFullScreen(rect BitmapRect, width, height int) bool {
|
||||
if rect.DestLeft != 0 || rect.DestTop != 0 || rect.Width < width || rect.Height < height {
|
||||
return false
|
||||
}
|
||||
limit := len(rect.Data)
|
||||
if limit > 256*1024 {
|
||||
limit = 256 * 1024
|
||||
}
|
||||
for i := 0; i < limit; i++ {
|
||||
if rect.Data[i] > 4 {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return limit > 0
|
||||
}
|
||||
|
||||
func (e *SessionEngine) requestRefresh(sess *Session, writeMu *sync.Mutex) error {
|
||||
body := buildRefreshRectBody(Config{Width: sess.Width, Height: sess.Height})
|
||||
pdu := buildDataPDU(sess.UserID, sess.SharedID, pduType2RefreshRect, body)
|
||||
trace.Logf("engine=rdp refresh rect %dx%d", sess.Width, sess.Height)
|
||||
writeMu.Lock()
|
||||
err := transport.WriteX224Data(sess.Conn, buildMCSSendDataRequest(sess.UserID, mcsGlobalChannelID, pdu))
|
||||
writeMu.Unlock()
|
||||
return err
|
||||
}
|
||||
|
||||
func (e *SessionEngine) ackSurfaceFrames(sess *Session, frameIDs []uint32, writeMu *sync.Mutex) error {
|
||||
for _, frameID := range frameIDs {
|
||||
body := []byte{byte(frameID), byte(frameID >> 8), byte(frameID >> 16), byte(frameID >> 24)}
|
||||
pdu := buildDataPDU(sess.UserID, sess.SharedID, pduType2FrameAcknowledge, body)
|
||||
trace.Logf("engine=rdp frame ack frameID=%d", frameID)
|
||||
writeMu.Lock()
|
||||
err := transport.WriteX224Data(sess.Conn, buildMCSSendDataRequest(sess.UserID, mcsGlobalChannelID, pdu))
|
||||
writeMu.Unlock()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (e *SessionEngine) handleEvent(input *InputSender, sink FrameSink, event ClientEvent) error {
|
||||
switch event.Type {
|
||||
case "mousemove", "mouse_move":
|
||||
return input.MouseMove(event.X, event.Y)
|
||||
case "mousedown", "mouse_down":
|
||||
return input.MouseButton(event.X, event.Y, event.Button, true)
|
||||
case "mouseup", "mouse_up":
|
||||
return input.MouseButton(event.X, event.Y, event.Button, false)
|
||||
case "wheel":
|
||||
return input.MouseWheel(event.X, event.Y, event.DeltaY)
|
||||
case "key":
|
||||
sc, ok := ScanCodeFromJS(event.Code)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
return input.Key(sc, event.Down)
|
||||
case "clipboard":
|
||||
if event.Text != "" {
|
||||
return input.TypeText(event.Text)
|
||||
}
|
||||
return nil
|
||||
case "auto_exec":
|
||||
if event.Text == "" {
|
||||
return nil
|
||||
}
|
||||
_ = sink.SendFrame(ServerFrame{Type: "status", Message: "正在远端执行:" + event.Text})
|
||||
return input.AutoExec(event.Text)
|
||||
case "resize":
|
||||
_ = sink.SendFrame(ServerFrame{Type: "status", Message: "分辨率变更请求已记录,重连后生效"})
|
||||
return nil
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// ProbeSessionFrames 用于诊断:建立会话后读取若干秒位图帧数量 // Logan.Li
|
||||
func ProbeSessionFrames(ctx context.Context, cfg Config, seconds int) (int, error) {
|
||||
sess, err := OpenSession(ctx, cfg)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
defer func() {
|
||||
if c, ok := sess.Conn.(io.Closer); ok {
|
||||
_ = c.Close()
|
||||
}
|
||||
}()
|
||||
receiver := NewFastPathReceiver()
|
||||
deadline := time.Now().Add(time.Duration(seconds) * time.Second)
|
||||
count := 0
|
||||
for time.Now().Before(deadline) {
|
||||
if c, ok := sess.Conn.(interface{ SetReadDeadline(time.Time) error }); ok {
|
||||
_ = c.SetReadDeadline(time.Now().Add(500 * time.Millisecond))
|
||||
}
|
||||
pkt, err := ReadWirePacket(sess.Conn)
|
||||
if err != nil {
|
||||
if time.Now().Before(deadline) {
|
||||
continue
|
||||
}
|
||||
return count, nil
|
||||
}
|
||||
var rects []BitmapRect
|
||||
if pkt.FastPath {
|
||||
rects, err = receiver.Handle(pkt.SecFlag, pkt.Payload)
|
||||
} else {
|
||||
rects, err = receiver.HandleSlowPath(pkt.Payload)
|
||||
}
|
||||
if err == nil {
|
||||
count += len(rects)
|
||||
}
|
||||
}
|
||||
if c, ok := sess.Conn.(interface{ SetReadDeadline(time.Time) error }); ok {
|
||||
_ = c.SetReadDeadline(time.Time{})
|
||||
}
|
||||
return count, nil
|
||||
}
|
||||
Reference in new issue
Block a user