improved rev console
This commit is contained in:
@@ -1,175 +0,0 @@
|
|||||||
package console
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"errors"
|
|
||||||
"io"
|
|
||||||
"os"
|
|
||||||
"os/exec"
|
|
||||||
"runtime"
|
|
||||||
"sync"
|
|
||||||
|
|
||||||
rt "overlord-client/cmd/agent/runtime"
|
|
||||||
"overlord-client/cmd/agent/wire"
|
|
||||||
)
|
|
||||||
|
|
||||||
type session struct {
|
|
||||||
id string
|
|
||||||
cmd *exec.Cmd
|
|
||||||
stdin io.WriteCloser
|
|
||||||
cancel context.CancelFunc
|
|
||||||
env *rt.Env
|
|
||||||
closed chan struct{}
|
|
||||||
once sync.Once
|
|
||||||
}
|
|
||||||
|
|
||||||
var sessions sync.Map
|
|
||||||
|
|
||||||
func shellCommand() []string {
|
|
||||||
if runtime.GOOS == "windows" {
|
|
||||||
return []string{"cmd.exe", "/d"}
|
|
||||||
}
|
|
||||||
|
|
||||||
return []string{"/bin/sh"}
|
|
||||||
}
|
|
||||||
|
|
||||||
func Start(ctx context.Context, env *rt.Env, sessionID string, _ int, _ int) error {
|
|
||||||
//garble:controlflow block_splits=10 junk_jumps=10 flatten_passes=2
|
|
||||||
if sessionID == "" {
|
|
||||||
return errors.New("session id required")
|
|
||||||
}
|
|
||||||
|
|
||||||
Stop(sessionID)
|
|
||||||
|
|
||||||
parts := shellCommand()
|
|
||||||
cctx, cancel := context.WithCancel(ctx)
|
|
||||||
cmd := exec.CommandContext(cctx, parts[0], parts[1:]...)
|
|
||||||
if attr := platformSysProcAttr(); attr != nil {
|
|
||||||
cmd.SysProcAttr = attr
|
|
||||||
}
|
|
||||||
cmd.Env = append(os.Environ(), "CLINK_NOAUTORUN=1")
|
|
||||||
|
|
||||||
stdin, err := cmd.StdinPipe()
|
|
||||||
if err != nil {
|
|
||||||
cancel()
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
stdout, err := cmd.StdoutPipe()
|
|
||||||
if err != nil {
|
|
||||||
cancel()
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
stderr, err := cmd.StderrPipe()
|
|
||||||
if err != nil {
|
|
||||||
cancel()
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := cmd.Start(); err != nil {
|
|
||||||
cancel()
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
sess := &session{id: sessionID, cmd: cmd, stdin: stdin, cancel: cancel, env: env, closed: make(chan struct{})}
|
|
||||||
sessions.Store(sessionID, sess)
|
|
||||||
|
|
||||||
go sess.forwardOutput(stdout)
|
|
||||||
go sess.forwardOutput(stderr)
|
|
||||||
go sess.waitExit()
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func Stop(sessionID string) {
|
|
||||||
val, ok := sessions.LoadAndDelete(sessionID)
|
|
||||||
if !ok {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
sess := val.(*session)
|
|
||||||
sess.cancel()
|
|
||||||
if sess.cmd.Process != nil {
|
|
||||||
_ = sess.cmd.Process.Kill()
|
|
||||||
}
|
|
||||||
sess.once.Do(func() { close(sess.closed) })
|
|
||||||
}
|
|
||||||
|
|
||||||
func Input(sessionID string, data string) error {
|
|
||||||
val, ok := sessions.Load(sessionID)
|
|
||||||
if !ok {
|
|
||||||
return errors.New("session not found")
|
|
||||||
}
|
|
||||||
sess := val.(*session)
|
|
||||||
if sess.stdin == nil {
|
|
||||||
return errors.New("stdin missing")
|
|
||||||
}
|
|
||||||
_, err := sess.stdin.Write([]byte(data))
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func Resize(_ string, _ int, _ int) {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *session) forwardOutput(r io.Reader) {
|
|
||||||
buf := make([]byte, 4096)
|
|
||||||
for {
|
|
||||||
n, err := r.Read(buf)
|
|
||||||
if n > 0 {
|
|
||||||
clean := stripBackspaces(buf[:n])
|
|
||||||
if len(clean) > 0 {
|
|
||||||
_ = sendOutput(s.env, s.id, clean, nil, "")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *session) waitExit() {
|
|
||||||
err := s.cmd.Wait()
|
|
||||||
var codePtr *int
|
|
||||||
msg := ""
|
|
||||||
if err != nil {
|
|
||||||
if exitErr, ok := err.(*exec.ExitError); ok {
|
|
||||||
code := exitErr.ExitCode()
|
|
||||||
codePtr = &code
|
|
||||||
} else {
|
|
||||||
msg = err.Error()
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
code := 0
|
|
||||||
codePtr = &code
|
|
||||||
}
|
|
||||||
_ = sendOutput(s.env, s.id, nil, codePtr, msg)
|
|
||||||
sessions.Delete(s.id)
|
|
||||||
s.once.Do(func() { close(s.closed) })
|
|
||||||
}
|
|
||||||
|
|
||||||
func sendOutput(env *rt.Env, sessionID string, data []byte, exitCode *int, errMsg string) error {
|
|
||||||
payload := wire.ConsoleOutput{Type: "console_output", SessionID: sessionID}
|
|
||||||
if len(data) > 0 {
|
|
||||||
payload.Data = data
|
|
||||||
}
|
|
||||||
if errMsg != "" {
|
|
||||||
payload.Error = errMsg
|
|
||||||
}
|
|
||||||
if exitCode != nil {
|
|
||||||
payload.ExitCode = exitCode
|
|
||||||
}
|
|
||||||
return wire.WriteMsg(context.Background(), env.Conn, payload)
|
|
||||||
}
|
|
||||||
|
|
||||||
func stripBackspaces(src []byte) []byte {
|
|
||||||
out := make([]byte, 0, len(src))
|
|
||||||
for _, b := range src {
|
|
||||||
if b == '\b' {
|
|
||||||
if len(out) > 0 {
|
|
||||||
out = out[:len(out)-1]
|
|
||||||
}
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
out = append(out, b)
|
|
||||||
}
|
|
||||||
return out
|
|
||||||
}
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
//go:build !windows
|
|
||||||
|
|
||||||
package console
|
|
||||||
|
|
||||||
import "syscall"
|
|
||||||
|
|
||||||
func platformSysProcAttr() *syscall.SysProcAttr {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
//go:build windows
|
|
||||||
|
|
||||||
package console
|
|
||||||
|
|
||||||
import "syscall"
|
|
||||||
|
|
||||||
func platformSysProcAttr() *syscall.SysProcAttr {
|
|
||||||
return &syscall.SysProcAttr{HideWindow: true}
|
|
||||||
}
|
|
||||||
@@ -16,7 +16,6 @@ import (
|
|||||||
|
|
||||||
"overlord-client/cmd/agent/audio"
|
"overlord-client/cmd/agent/audio"
|
||||||
"overlord-client/cmd/agent/capture"
|
"overlord-client/cmd/agent/capture"
|
||||||
"overlord-client/cmd/agent/console"
|
|
||||||
"overlord-client/cmd/agent/criticalproc"
|
"overlord-client/cmd/agent/criticalproc"
|
||||||
"overlord-client/cmd/agent/filesearch"
|
"overlord-client/cmd/agent/filesearch"
|
||||||
"overlord-client/cmd/agent/persistence"
|
"overlord-client/cmd/agent/persistence"
|
||||||
@@ -1856,7 +1855,7 @@ func HandleCommand(ctx context.Context, env *runtime.Env, envelope map[string]in
|
|||||||
if sessionID == "" {
|
if sessionID == "" {
|
||||||
return wire.WriteMsg(ctx, env.Conn, wire.CommandResult{Type: "command_result", CommandID: cmdID, OK: false, Message: "missing session id"})
|
return wire.WriteMsg(ctx, env.Conn, wire.CommandResult{Type: "command_result", CommandID: cmdID, OK: false, Message: "missing session id"})
|
||||||
}
|
}
|
||||||
if err := console.Start(ctx, env, sessionID, cols, rows); err != nil {
|
if err := env.Console.Start(ctx, runtime.ConsoleStartRequest{SessionID: sessionID, Cols: cols, Rows: rows}); err != nil {
|
||||||
_ = wire.WriteMsg(ctx, env.Conn, wire.CommandResult{Type: "command_result", CommandID: cmdID, OK: false, Message: err.Error()})
|
_ = wire.WriteMsg(ctx, env.Conn, wire.CommandResult{Type: "command_result", CommandID: cmdID, OK: false, Message: err.Error()})
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -1865,21 +1864,21 @@ func HandleCommand(ctx context.Context, env *runtime.Env, envelope map[string]in
|
|||||||
sessionID, _ := envelopePayloadString(envelope, "sessionId")
|
sessionID, _ := envelopePayloadString(envelope, "sessionId")
|
||||||
data, _ := envelopePayloadString(envelope, "data")
|
data, _ := envelopePayloadString(envelope, "data")
|
||||||
if sessionID != "" && data != "" {
|
if sessionID != "" && data != "" {
|
||||||
_ = console.Input(sessionID, data)
|
_ = env.Console.Write(ctx, sessionID, data)
|
||||||
}
|
}
|
||||||
return wire.WriteMsg(ctx, env.Conn, wire.CommandResult{Type: "command_result", CommandID: cmdID, OK: true})
|
return wire.WriteMsg(ctx, env.Conn, wire.CommandResult{Type: "command_result", CommandID: cmdID, OK: true})
|
||||||
case "console_stop":
|
case "console_stop":
|
||||||
sessionID, _ := envelopePayloadString(envelope, "sessionId")
|
sessionID, _ := envelopePayloadString(envelope, "sessionId")
|
||||||
if sessionID != "" {
|
if sessionID != "" {
|
||||||
console.Stop(sessionID)
|
env.Console.Stop(sessionID)
|
||||||
}
|
}
|
||||||
return wire.WriteMsg(ctx, env.Conn, wire.CommandResult{Type: "command_result", CommandID: cmdID, OK: true})
|
return wire.WriteMsg(ctx, env.Conn, wire.CommandResult{Type: "command_result", CommandID: cmdID, OK: true})
|
||||||
case "console_resize":
|
case "console_resize":
|
||||||
|
|
||||||
sessionID, _ := envelopePayloadString(envelope, "sessionId")
|
sessionID, _ := envelopePayloadString(envelope, "sessionId")
|
||||||
cols, rows := envelopePayloadInts(envelope)
|
cols, rows := envelopePayloadInts(envelope)
|
||||||
_ = sessionID
|
if sessionID != "" {
|
||||||
console.Resize(sessionID, cols, rows)
|
_ = env.Console.Resize(sessionID, cols, rows)
|
||||||
|
}
|
||||||
return wire.WriteMsg(ctx, env.Conn, wire.CommandResult{Type: "command_result", CommandID: cmdID, OK: true})
|
return wire.WriteMsg(ctx, env.Conn, wire.CommandResult{Type: "command_result", CommandID: cmdID, OK: true})
|
||||||
case "voice_session_start":
|
case "voice_session_start":
|
||||||
sessionID, _ := envelopePayloadString(envelope, "sessionId")
|
sessionID, _ := envelopePayloadString(envelope, "sessionId")
|
||||||
|
|||||||
@@ -5,14 +5,10 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
|
||||||
"runtime"
|
"runtime"
|
||||||
"sync"
|
"sync"
|
||||||
"syscall"
|
|
||||||
|
|
||||||
"overlord-client/cmd/agent/wire"
|
"overlord-client/cmd/agent/wire"
|
||||||
|
|
||||||
"github.com/creack/pty"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type ConsoleHub struct {
|
type ConsoleHub struct {
|
||||||
@@ -23,8 +19,7 @@ type ConsoleHub struct {
|
|||||||
|
|
||||||
type ConsoleSession struct {
|
type ConsoleSession struct {
|
||||||
id string
|
id string
|
||||||
cmd *exec.Cmd
|
pty ptyHandle
|
||||||
pty *os.File
|
|
||||||
cancel context.CancelFunc
|
cancel context.CancelFunc
|
||||||
once sync.Once
|
once sync.Once
|
||||||
}
|
}
|
||||||
@@ -35,6 +30,12 @@ type ConsoleStartRequest struct {
|
|||||||
Rows int
|
Rows int
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ptyHandle interface {
|
||||||
|
io.ReadWriteCloser
|
||||||
|
Resize(cols, rows uint16) error
|
||||||
|
Wait() (int, error)
|
||||||
|
}
|
||||||
|
|
||||||
func NewConsoleHub(env *Env) *ConsoleHub {
|
func NewConsoleHub(env *Env) *ConsoleHub {
|
||||||
return &ConsoleHub{env: env, sessions: make(map[string]*ConsoleSession)}
|
return &ConsoleHub{env: env, sessions: make(map[string]*ConsoleSession)}
|
||||||
}
|
}
|
||||||
@@ -62,22 +63,19 @@ func (h *ConsoleHub) Start(ctx context.Context, req ConsoleStartRequest) error {
|
|||||||
|
|
||||||
shell := detectShell()
|
shell := detectShell()
|
||||||
sessionCtx, cancel := context.WithCancel(ctx)
|
sessionCtx, cancel := context.WithCancel(ctx)
|
||||||
cmd := exec.CommandContext(sessionCtx, shell[0], shell[1:]...)
|
handle, err := startPty(sessionCtx, shell, cols, rows)
|
||||||
cmd.Env = os.Environ()
|
|
||||||
|
|
||||||
ptmx, err := pty.StartWithSize(cmd, &pty.Winsize{Cols: cols, Rows: rows})
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
cancel()
|
cancel()
|
||||||
h.emitError(ctx, req.SessionID, err)
|
h.emitError(req.SessionID, err)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
sess := &ConsoleSession{id: req.SessionID, cmd: cmd, pty: ptmx, cancel: cancel}
|
sess := &ConsoleSession{id: req.SessionID, pty: handle, cancel: cancel}
|
||||||
h.mu.Lock()
|
h.mu.Lock()
|
||||||
h.sessions[req.SessionID] = sess
|
h.sessions[req.SessionID] = sess
|
||||||
h.mu.Unlock()
|
h.mu.Unlock()
|
||||||
|
|
||||||
go h.forwardOutput(sessionCtx, sess)
|
go h.forwardOutput(sess)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -101,7 +99,7 @@ func (h *ConsoleHub) Resize(sessionID string, cols, rows int) error {
|
|||||||
if rows <= 0 {
|
if rows <= 0 {
|
||||||
rows = 36
|
rows = 36
|
||||||
}
|
}
|
||||||
return pty.Setsize(sess.pty, &pty.Winsize{Cols: uint16(cols), Rows: uint16(rows)})
|
return sess.pty.Resize(uint16(cols), uint16(rows))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *ConsoleHub) Stop(sessionID string) {
|
func (h *ConsoleHub) Stop(sessionID string) {
|
||||||
@@ -129,28 +127,27 @@ func (h *ConsoleHub) StopAll() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *ConsoleHub) forwardOutput(ctx context.Context, sess *ConsoleSession) {
|
func (h *ConsoleHub) forwardOutput(sess *ConsoleSession) {
|
||||||
|
writeCtx := context.Background()
|
||||||
|
|
||||||
buf := make([]byte, 4096)
|
buf := make([]byte, 4096)
|
||||||
for {
|
for {
|
||||||
n, err := sess.pty.Read(buf)
|
n, err := sess.pty.Read(buf)
|
||||||
if n > 0 {
|
if n > 0 {
|
||||||
chunk := make([]byte, n)
|
chunk := make([]byte, n)
|
||||||
copy(chunk, buf[:n])
|
copy(chunk, buf[:n])
|
||||||
_ = wire.WriteMsg(ctx, h.env.Conn, wire.ConsoleOutput{Type: "console_output", SessionID: sess.id, Data: chunk})
|
_ = wire.WriteMsg(writeCtx, h.env.Conn, wire.ConsoleOutput{Type: "console_output", SessionID: sess.id, Data: chunk})
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if err != io.EOF {
|
if err != io.EOF {
|
||||||
h.emitError(ctx, sess.id, err)
|
h.emitError(sess.id, err)
|
||||||
}
|
}
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if sess.cmd != nil {
|
exitCode, _ := sess.pty.Wait()
|
||||||
_ = sess.cmd.Wait()
|
_ = wire.WriteMsg(writeCtx, h.env.Conn, wire.ConsoleOutput{Type: "console_output", SessionID: sess.id, ExitCode: &exitCode})
|
||||||
}
|
|
||||||
exitCode := extractExitCode(sess.cmd)
|
|
||||||
_ = wire.WriteMsg(ctx, h.env.Conn, wire.ConsoleOutput{Type: "console_output", SessionID: sess.id, ExitCode: &exitCode})
|
|
||||||
|
|
||||||
h.mu.Lock()
|
h.mu.Lock()
|
||||||
delete(h.sessions, sess.id)
|
delete(h.sessions, sess.id)
|
||||||
@@ -158,8 +155,8 @@ func (h *ConsoleHub) forwardOutput(ctx context.Context, sess *ConsoleSession) {
|
|||||||
sess.close()
|
sess.close()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *ConsoleHub) emitError(ctx context.Context, sessionID string, err error) {
|
func (h *ConsoleHub) emitError(sessionID string, err error) {
|
||||||
_ = wire.WriteMsg(ctx, h.env.Conn, wire.ConsoleOutput{Type: "console_output", SessionID: sessionID, Error: err.Error()})
|
_ = wire.WriteMsg(context.Background(), h.env.Conn, wire.ConsoleOutput{Type: "console_output", SessionID: sessionID, Error: err.Error()})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *ConsoleHub) get(sessionID string) *ConsoleSession {
|
func (h *ConsoleHub) get(sessionID string) *ConsoleSession {
|
||||||
@@ -173,9 +170,6 @@ func (sess *ConsoleSession) close() {
|
|||||||
if sess.cancel != nil {
|
if sess.cancel != nil {
|
||||||
sess.cancel()
|
sess.cancel()
|
||||||
}
|
}
|
||||||
if sess.cmd != nil && sess.cmd.Process != nil {
|
|
||||||
_ = sess.cmd.Process.Kill()
|
|
||||||
}
|
|
||||||
if sess.pty != nil {
|
if sess.pty != nil {
|
||||||
_ = sess.pty.Close()
|
_ = sess.pty.Close()
|
||||||
}
|
}
|
||||||
@@ -197,13 +191,3 @@ func detectShell() []string {
|
|||||||
}
|
}
|
||||||
return []string{"/bin/bash", "-l"}
|
return []string{"/bin/bash", "-l"}
|
||||||
}
|
}
|
||||||
|
|
||||||
func extractExitCode(cmd *exec.Cmd) int {
|
|
||||||
if cmd == nil || cmd.ProcessState == nil {
|
|
||||||
return -1
|
|
||||||
}
|
|
||||||
if status, ok := cmd.ProcessState.Sys().(syscall.WaitStatus); ok {
|
|
||||||
return status.ExitStatus()
|
|
||||||
}
|
|
||||||
return -1
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -0,0 +1,59 @@
|
|||||||
|
//go:build !windows
|
||||||
|
|
||||||
|
package runtime
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"syscall"
|
||||||
|
|
||||||
|
"github.com/creack/pty"
|
||||||
|
)
|
||||||
|
|
||||||
|
type unixPty struct {
|
||||||
|
f *os.File
|
||||||
|
cmd *exec.Cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *unixPty) Read(p []byte) (int, error) { return u.f.Read(p) }
|
||||||
|
func (u *unixPty) Write(p []byte) (int, error) { return u.f.Write(p) }
|
||||||
|
|
||||||
|
func (u *unixPty) Close() error {
|
||||||
|
if u.cmd != nil && u.cmd.Process != nil {
|
||||||
|
_ = u.cmd.Process.Kill()
|
||||||
|
}
|
||||||
|
return u.f.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *unixPty) Resize(cols, rows uint16) error {
|
||||||
|
return pty.Setsize(u.f, &pty.Winsize{Cols: cols, Rows: rows})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *unixPty) Wait() (int, error) {
|
||||||
|
if u.cmd == nil {
|
||||||
|
return -1, nil
|
||||||
|
}
|
||||||
|
err := u.cmd.Wait()
|
||||||
|
if err != nil {
|
||||||
|
if ee, ok := err.(*exec.ExitError); ok {
|
||||||
|
if status, ok := ee.Sys().(syscall.WaitStatus); ok {
|
||||||
|
return status.ExitStatus(), nil
|
||||||
|
}
|
||||||
|
return ee.ExitCode(), nil
|
||||||
|
}
|
||||||
|
return -1, err
|
||||||
|
}
|
||||||
|
return 0, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func startPty(ctx context.Context, shell []string, cols, rows uint16) (ptyHandle, error) {
|
||||||
|
cmd := exec.CommandContext(ctx, shell[0], shell[1:]...)
|
||||||
|
cmd.Env = append(os.Environ(), "TERM=xterm-256color")
|
||||||
|
|
||||||
|
f, err := pty.StartWithSize(cmd, &pty.Winsize{Cols: cols, Rows: rows})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &unixPty{f: f, cmd: cmd}, nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
//go:build windows
|
||||||
|
|
||||||
|
package runtime
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/UserExistsError/conpty"
|
||||||
|
)
|
||||||
|
|
||||||
|
type winPty struct {
|
||||||
|
cp *conpty.ConPty
|
||||||
|
waitCh chan waitResult
|
||||||
|
}
|
||||||
|
|
||||||
|
type waitResult struct {
|
||||||
|
code int
|
||||||
|
err error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *winPty) Read(p []byte) (int, error) { return w.cp.Read(p) }
|
||||||
|
func (w *winPty) Write(p []byte) (int, error) { return w.cp.Write(p) }
|
||||||
|
func (w *winPty) Close() error { return w.cp.Close() }
|
||||||
|
|
||||||
|
func (w *winPty) Resize(cols, rows uint16) error {
|
||||||
|
return w.cp.Resize(int(cols), int(rows))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *winPty) Wait() (int, error) {
|
||||||
|
res := <-w.waitCh
|
||||||
|
return res.code, res.err
|
||||||
|
}
|
||||||
|
|
||||||
|
func startPty(ctx context.Context, shell []string, cols, rows uint16) (ptyHandle, error) {
|
||||||
|
cmdline := strings.Join(shell, " ")
|
||||||
|
|
||||||
|
cp, err := conpty.Start(cmdline, conpty.ConPtyDimensions(int(cols), int(rows)))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
w := &winPty{cp: cp, waitCh: make(chan waitResult, 1)}
|
||||||
|
go func() {
|
||||||
|
code, werr := cp.Wait(context.Background())
|
||||||
|
w.waitCh <- waitResult{code: int(code), err: werr}
|
||||||
|
}()
|
||||||
|
return w, nil
|
||||||
|
}
|
||||||
@@ -13,6 +13,7 @@ require (
|
|||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/Kirizu-Official/windows-camera-go v0.2.0
|
github.com/Kirizu-Official/windows-camera-go v0.2.0
|
||||||
|
github.com/UserExistsError/conpty v0.1.4
|
||||||
github.com/gen2brain/malgo v0.11.24
|
github.com/gen2brain/malgo v0.11.24
|
||||||
github.com/gen2brain/shm v0.1.0
|
github.com/gen2brain/shm v0.1.0
|
||||||
github.com/jezek/xgb v1.1.1
|
github.com/jezek/xgb v1.1.1
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
github.com/Kirizu-Official/windows-camera-go v0.2.0 h1:wF8s6US/me/d5WaBeoHGVKE8mBOl83kKTfMV38YYQ3g=
|
github.com/Kirizu-Official/windows-camera-go v0.2.0 h1:wF8s6US/me/d5WaBeoHGVKE8mBOl83kKTfMV38YYQ3g=
|
||||||
github.com/Kirizu-Official/windows-camera-go v0.2.0/go.mod h1:zMP4SmblRxD6zQHe4X07CHO4lnNcWo6zeZhQavO2aLQ=
|
github.com/Kirizu-Official/windows-camera-go v0.2.0/go.mod h1:zMP4SmblRxD6zQHe4X07CHO4lnNcWo6zeZhQavO2aLQ=
|
||||||
|
github.com/UserExistsError/conpty v0.1.4 h1:+3FhJhiqhyEJa+K5qaK3/w6w+sN3Nh9O9VbJyBS02to=
|
||||||
|
github.com/UserExistsError/conpty v0.1.4/go.mod h1:PDglKIkX3O/2xVk0MV9a6bCWxRmPVfxqZoTG/5sSd9I=
|
||||||
github.com/creack/pty v1.1.21 h1:1/QdRyBaHHJP61QkWMXlOIBfsgdDeeKfK8SYVUWJKf0=
|
github.com/creack/pty v1.1.21 h1:1/QdRyBaHHJP61QkWMXlOIBfsgdDeeKfK8SYVUWJKf0=
|
||||||
github.com/creack/pty v1.1.21/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4=
|
github.com/creack/pty v1.1.21/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4=
|
||||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
@@ -86,6 +88,7 @@ golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtC
|
|||||||
golang.org/x/net v0.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo=
|
golang.org/x/net v0.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo=
|
||||||
golang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y=
|
golang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y=
|
||||||
golang.org/x/sys v0.0.0-20201018230417-eeed37f84f13/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20201018230417-eeed37f84f13/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
|
golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
|
||||||
golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
|||||||
+49
-11
@@ -1,26 +1,15 @@
|
|||||||
{
|
{
|
||||||
"lockfileVersion": 1,
|
"lockfileVersion": 1,
|
||||||
"configVersion": 1,
|
|
||||||
"workspaces": {
|
"workspaces": {
|
||||||
"": {
|
"": {
|
||||||
"name": "overlord-server",
|
"name": "overlord-server",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fontsource/inter": "^5.2.8",
|
|
||||||
"@fontsource/jetbrains-mono": "^5.2.8",
|
|
||||||
"@fortawesome/fontawesome-free": "6.5.2",
|
|
||||||
"@msgpack/msgpack": "^3.0.0",
|
"@msgpack/msgpack": "^3.0.0",
|
||||||
"@solana/web3.js": "^1.98.4",
|
"@solana/web3.js": "^1.98.4",
|
||||||
"ace-builds": "1.36.5",
|
|
||||||
"adm-zip": "^0.5.12",
|
"adm-zip": "^0.5.12",
|
||||||
"animejs": "3.2.2",
|
|
||||||
"ansi-to-html": "0.7.2",
|
"ansi-to-html": "0.7.2",
|
||||||
"chart.js": "4.4.0",
|
|
||||||
"codemirror": "5.65.16",
|
|
||||||
"flag-icons": "latest",
|
|
||||||
"geoip-lite": "latest",
|
"geoip-lite": "latest",
|
||||||
"highlight.js": "11.9.0",
|
|
||||||
"jose": "^6.1.3",
|
"jose": "^6.1.3",
|
||||||
"leaflet": "1.9.4",
|
|
||||||
"msgpackr": "1.11.8",
|
"msgpackr": "1.11.8",
|
||||||
"sharp": "^0.34.5",
|
"sharp": "^0.34.5",
|
||||||
"uuid": "^9.0.1",
|
"uuid": "^9.0.1",
|
||||||
@@ -28,13 +17,26 @@
|
|||||||
"winston": "^3.11.0",
|
"winston": "^3.11.0",
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@fontsource/inter": "^5.2.8",
|
||||||
|
"@fontsource/jetbrains-mono": "^5.2.8",
|
||||||
|
"@fortawesome/fontawesome-free": "6.5.2",
|
||||||
"@tailwindcss/cli": "^4.1.18",
|
"@tailwindcss/cli": "^4.1.18",
|
||||||
"@types/bun": "^1.3.9",
|
"@types/bun": "^1.3.9",
|
||||||
"@types/geoip-lite": "latest",
|
"@types/geoip-lite": "latest",
|
||||||
"@types/uuid": "^9.0.7",
|
"@types/uuid": "^9.0.7",
|
||||||
|
"@xterm/addon-fit": "^0.11.0",
|
||||||
|
"@xterm/addon-web-links": "^0.12.0",
|
||||||
|
"@xterm/xterm": "^6.0.0",
|
||||||
|
"ace-builds": "1.36.5",
|
||||||
|
"animejs": "3.2.2",
|
||||||
"autoprefixer": "^10.4.23",
|
"autoprefixer": "^10.4.23",
|
||||||
|
"chart.js": "4.4.0",
|
||||||
"clean-css": "^5.3.3",
|
"clean-css": "^5.3.3",
|
||||||
|
"codemirror": "5.65.16",
|
||||||
|
"flag-icons": "latest",
|
||||||
|
"highlight.js": "11.9.0",
|
||||||
"html-minifier-terser": "^7.2.0",
|
"html-minifier-terser": "^7.2.0",
|
||||||
|
"leaflet": "1.9.4",
|
||||||
"postcss": "^8.5.6",
|
"postcss": "^8.5.6",
|
||||||
"tailwindcss": "^4.1.18",
|
"tailwindcss": "^4.1.18",
|
||||||
"terser": "^5.46.1",
|
"terser": "^5.46.1",
|
||||||
@@ -225,6 +227,12 @@
|
|||||||
|
|
||||||
"@types/ws": ["@types/ws@7.4.7", "", { "dependencies": { "@types/node": "*" } }, "sha512-JQbbmxZTZehdc2iszGKs5oC3NFnjeay7mtAWrdt7qNtAVK0g19muApzAy4bm9byz79xa2ZnO/BOBC2R8RC5Lww=="],
|
"@types/ws": ["@types/ws@7.4.7", "", { "dependencies": { "@types/node": "*" } }, "sha512-JQbbmxZTZehdc2iszGKs5oC3NFnjeay7mtAWrdt7qNtAVK0g19muApzAy4bm9byz79xa2ZnO/BOBC2R8RC5Lww=="],
|
||||||
|
|
||||||
|
"@xterm/addon-fit": ["@xterm/addon-fit@0.11.0", "", {}, "sha512-jYcgT6xtVYhnhgxh3QgYDnnNMYTcf8ElbxxFzX0IZo+vabQqSPAjC3c1wJrKB5E19VwQei89QCiZZP86DCPF7g=="],
|
||||||
|
|
||||||
|
"@xterm/addon-web-links": ["@xterm/addon-web-links@0.12.0", "", {}, "sha512-4Smom3RPyVp7ZMYOYDoC/9eGJJJqYhnPLGGqJ6wOBfB8VxPViJNSKdgRYb8NpaM6YSelEKbA2SStD7lGyqaobw=="],
|
||||||
|
|
||||||
|
"@xterm/xterm": ["@xterm/xterm@6.0.0", "", {}, "sha512-TQwDdQGtwwDt+2cgKDLn0IRaSxYu1tSUjgKarSDkUM0ZNiSRXFpjxEsvc/Zgc5kq5omJ+V0a8/kIM2WD3sMOYg=="],
|
||||||
|
|
||||||
"ace-builds": ["ace-builds@1.36.5", "", {}, "sha512-mZ5KVanRT6nLRDLqtG/1YQQLX/gZVC/v526cm1Ru/MTSlrbweSmqv2ZT0d2GaHpJq035MwCMIrj+LgDAUnDXrg=="],
|
"ace-builds": ["ace-builds@1.36.5", "", {}, "sha512-mZ5KVanRT6nLRDLqtG/1YQQLX/gZVC/v526cm1Ru/MTSlrbweSmqv2ZT0d2GaHpJq035MwCMIrj+LgDAUnDXrg=="],
|
||||||
|
|
||||||
"acorn": ["acorn@8.16.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw=="],
|
"acorn": ["acorn@8.16.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw=="],
|
||||||
@@ -537,6 +545,8 @@
|
|||||||
|
|
||||||
"yauzl": ["yauzl@3.2.1", "", { "dependencies": { "buffer-crc32": "~0.2.3", "pend": "~1.2.0" } }, "sha512-k1isifdbpNSFEHFJ1ZY4YDewv0IH9FR61lDetaRMD3j2ae3bIXGV+7c+LHCqtQGofSd8PIyV4X6+dHMAnSr60A=="],
|
"yauzl": ["yauzl@3.2.1", "", { "dependencies": { "buffer-crc32": "~0.2.3", "pend": "~1.2.0" } }, "sha512-k1isifdbpNSFEHFJ1ZY4YDewv0IH9FR61lDetaRMD3j2ae3bIXGV+7c+LHCqtQGofSd8PIyV4X6+dHMAnSr60A=="],
|
||||||
|
|
||||||
|
"@emnapi/runtime/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
|
||||||
|
|
||||||
"@solana/errors/chalk": ["chalk@5.6.2", "", {}, "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA=="],
|
"@solana/errors/chalk": ["chalk@5.6.2", "", {}, "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA=="],
|
||||||
|
|
||||||
"@solana/errors/commander": ["commander@14.0.3", "", {}, "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw=="],
|
"@solana/errors/commander": ["commander@14.0.3", "", {}, "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw=="],
|
||||||
@@ -577,6 +587,34 @@
|
|||||||
|
|
||||||
"terser/commander": ["commander@2.20.3", "", {}, "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ=="],
|
"terser/commander": ["commander@2.20.3", "", {}, "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ=="],
|
||||||
|
|
||||||
|
"@tailwindcss/oxide-wasm32-wasi/@emnapi/core/@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.2.0", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-N10dEJNSsUx41Z6pZsXU8FjPjpBEplgH24sfkmITrBED1/U2Esum9F3lfLrMjKHHjmi557zQn7kR9R+XWXu5Rg=="],
|
||||||
|
|
||||||
|
"@tailwindcss/oxide-wasm32-wasi/@emnapi/core/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
|
||||||
|
|
||||||
|
"@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
|
||||||
|
|
||||||
|
"@tailwindcss/oxide-wasm32-wasi/@emnapi/wasi-threads/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
|
||||||
|
|
||||||
|
"@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime/@emnapi/core": ["@emnapi/core@1.9.1", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.0", "tslib": "^2.4.0" }, "bundled": true }, "sha512-mukuNALVsoix/w1BJwFzwXBN/dHeejQtuVzcDsfOEsdpCumXb/E9j8w11h5S54tT1xhifGfbbSm/ICrObRb3KA=="],
|
||||||
|
|
||||||
|
"@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime/@emnapi/runtime": ["@emnapi/runtime@1.9.1", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-VYi5+ZVLhpgK4hQ0TAjiQiZ6ol0oe4mBx7mVv7IflsiEp0OWoVsp/+f9Vc1hOhE0TtkORVrI1GvzyreqpgWtkA=="],
|
||||||
|
|
||||||
|
"@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime/@tybys/wasm-util": ["@tybys/wasm-util@0.10.1", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg=="],
|
||||||
|
|
||||||
|
"@tailwindcss/oxide-wasm32-wasi/@tybys/wasm-util/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
|
||||||
|
|
||||||
"color/color-convert/color-name": ["color-name@2.1.0", "", {}, "sha512-1bPaDNFm0axzE4MEAzKPuqKWeRaT43U/hyxKPBdqTfmPF+d6n7FSoTFxLVULUJOmiLp01KjhIPPH+HrXZJN4Rg=="],
|
"color/color-convert/color-name": ["color-name@2.1.0", "", {}, "sha512-1bPaDNFm0axzE4MEAzKPuqKWeRaT43U/hyxKPBdqTfmPF+d6n7FSoTFxLVULUJOmiLp01KjhIPPH+HrXZJN4Rg=="],
|
||||||
|
|
||||||
|
"@tailwindcss/oxide-wasm32-wasi/@emnapi/core/@emnapi/wasi-threads/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
|
||||||
|
|
||||||
|
"@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime/@emnapi/core/@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.2.0", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-N10dEJNSsUx41Z6pZsXU8FjPjpBEplgH24sfkmITrBED1/U2Esum9F3lfLrMjKHHjmi557zQn7kR9R+XWXu5Rg=="],
|
||||||
|
|
||||||
|
"@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime/@emnapi/core/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
|
||||||
|
|
||||||
|
"@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime/@emnapi/runtime/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
|
||||||
|
|
||||||
|
"@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime/@tybys/wasm-util/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
|
||||||
|
|
||||||
|
"@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime/@emnapi/core/@emnapi/wasi-threads/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,6 +35,9 @@
|
|||||||
"@types/bun": "^1.3.9",
|
"@types/bun": "^1.3.9",
|
||||||
"@types/geoip-lite": "latest",
|
"@types/geoip-lite": "latest",
|
||||||
"@types/uuid": "^9.0.7",
|
"@types/uuid": "^9.0.7",
|
||||||
|
"@xterm/addon-fit": "^0.11.0",
|
||||||
|
"@xterm/addon-web-links": "^0.12.0",
|
||||||
|
"@xterm/xterm": "^6.0.0",
|
||||||
"ace-builds": "1.36.5",
|
"ace-builds": "1.36.5",
|
||||||
"animejs": "3.2.2",
|
"animejs": "3.2.2",
|
||||||
"autoprefixer": "^10.4.23",
|
"autoprefixer": "^10.4.23",
|
||||||
|
|||||||
@@ -10,58 +10,39 @@
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
.console-topbar {
|
|
||||||
position: sticky;
|
|
||||||
top: 0;
|
|
||||||
z-index: 10;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
gap: 10px;
|
|
||||||
padding: 14px 18px;
|
|
||||||
background: rgba(7, 12, 22, 0.9);
|
|
||||||
border-bottom: 1px solid rgba(255, 255, 255, 0.06);
|
|
||||||
backdrop-filter: blur(10px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.console-topbar .brand {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 10px;
|
|
||||||
font-weight: 600;
|
|
||||||
letter-spacing: 0.3px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.console-topbar .console-actions {
|
|
||||||
display: flex;
|
|
||||||
gap: 8px;
|
|
||||||
align-items: center;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.console-main {
|
.console-main {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
padding: 18px;
|
padding: 18px;
|
||||||
display: grid;
|
box-sizing: border-box;
|
||||||
place-items: center;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.console-card {
|
.console-card {
|
||||||
width: min(1100px, 96vw);
|
max-width: 1280px;
|
||||||
|
width: 100%;
|
||||||
|
margin: 0 auto;
|
||||||
|
box-sizing: border-box;
|
||||||
background: rgba(8, 12, 22, 0.8);
|
background: rgba(8, 12, 22, 0.8);
|
||||||
border: 1px solid rgba(255, 255, 255, 0.05);
|
border: 1px solid rgba(255, 255, 255, 0.05);
|
||||||
border-radius: 14px;
|
border-radius: 14px;
|
||||||
box-shadow: 0 20px 50px rgba(0, 0, 0, 0.4);
|
box-shadow: 0 20px 50px rgba(0, 0, 0, 0.4);
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 14px;
|
gap: 12px;
|
||||||
padding: 16px;
|
padding: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.console-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 12px;
|
||||||
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.console-meta {
|
.console-meta {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
|
grid-template-columns: repeat(auto-fit, minmax(120px, max-content));
|
||||||
gap: 10px;
|
gap: 14px 24px;
|
||||||
color: #a9b8d4;
|
color: #a9b8d4;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -70,67 +51,37 @@
|
|||||||
color: #e5edff;
|
color: #e5edff;
|
||||||
}
|
}
|
||||||
|
|
||||||
.console-output {
|
.console-toolbar {
|
||||||
min-height: 420px;
|
display: flex;
|
||||||
max-height: 70vh;
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.console-terminal {
|
||||||
|
min-height: 65vh;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
max-width: 100%;
|
|
||||||
min-width: 0;
|
|
||||||
background: #050913;
|
background: #050913;
|
||||||
border: 1px solid rgba(255, 255, 255, 0.06);
|
border: 1px solid rgba(255, 255, 255, 0.06);
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
padding: 14px;
|
|
||||||
overflow: auto;
|
|
||||||
overflow-x: auto;
|
|
||||||
box-sizing: border-box;
|
|
||||||
white-space: normal;
|
|
||||||
text-align: left;
|
|
||||||
overflow-wrap: normal;
|
|
||||||
word-break: normal;
|
|
||||||
line-height: 1.5;
|
|
||||||
font-size: 14px;
|
|
||||||
color: #e8edf2;
|
|
||||||
box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.02);
|
|
||||||
}
|
|
||||||
|
|
||||||
.console-pre {
|
|
||||||
margin: 0;
|
|
||||||
white-space: pre;
|
|
||||||
display: block;
|
|
||||||
width: max-content;
|
|
||||||
min-width: 0;
|
|
||||||
text-align: left;
|
|
||||||
}
|
|
||||||
|
|
||||||
.console-input-row {
|
|
||||||
display: flex;
|
|
||||||
gap: 10px;
|
|
||||||
align-items: center;
|
|
||||||
background: rgba(255, 255, 255, 0.02);
|
|
||||||
border: 1px solid rgba(255, 255, 255, 0.06);
|
|
||||||
border-radius: 10px;
|
|
||||||
padding: 10px 12px;
|
padding: 10px 12px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.02);
|
||||||
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.console-input-row .prompt {
|
.console-terminal .xterm,
|
||||||
color: #6ee7b7;
|
.console-terminal .xterm-viewport,
|
||||||
|
.console-terminal .xterm-screen {
|
||||||
|
background: transparent !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.console-input-row textarea {
|
.console-terminal .xterm-viewport::-webkit-scrollbar {
|
||||||
flex: 1;
|
width: 10px;
|
||||||
resize: vertical;
|
|
||||||
min-height: 44px;
|
|
||||||
max-height: 160px;
|
|
||||||
background: transparent;
|
|
||||||
border: none;
|
|
||||||
color: #e8edf2;
|
|
||||||
font-size: 14px;
|
|
||||||
outline: none;
|
|
||||||
}
|
}
|
||||||
|
.console-terminal .xterm-viewport::-webkit-scrollbar-thumb {
|
||||||
.console-buttons {
|
background: rgba(255, 255, 255, 0.12);
|
||||||
display: flex;
|
border-radius: 8px;
|
||||||
gap: 8px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn,
|
.btn,
|
||||||
@@ -160,12 +111,35 @@
|
|||||||
background: rgba(255, 255, 255, 0.06);
|
background: rgba(255, 255, 255, 0.06);
|
||||||
}
|
}
|
||||||
|
|
||||||
.status-pill {
|
.pill {
|
||||||
padding: 8px 12px;
|
padding: 6px 10px;
|
||||||
border-radius: 999px;
|
border-radius: 999px;
|
||||||
font-size: 13px;
|
font-size: 12px;
|
||||||
border: 1px solid rgba(255, 255, 255, 0.14);
|
border: 1px solid rgba(255, 255, 255, 0.14);
|
||||||
background: rgba(255, 255, 255, 0.04);
|
background: rgba(255, 255, 255, 0.04);
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pill i {
|
||||||
|
font-size: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pill.pill-online {
|
||||||
|
color: #6ee7b7;
|
||||||
|
border-color: rgba(110, 231, 183, 0.35);
|
||||||
|
background: rgba(110, 231, 183, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pill.pill-offline {
|
||||||
|
color: #fca5a5;
|
||||||
|
border-color: rgba(252, 165, 165, 0.35);
|
||||||
|
background: rgba(252, 165, 165, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pill.pill-ghost {
|
||||||
|
color: #cbd5e1;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 720px) {
|
@media (max-width: 720px) {
|
||||||
@@ -173,21 +147,12 @@
|
|||||||
background: #050913;
|
background: #050913;
|
||||||
}
|
}
|
||||||
.console-main {
|
.console-main {
|
||||||
padding: 12px;
|
padding: 10px;
|
||||||
}
|
}
|
||||||
.console-card {
|
.console-card {
|
||||||
padding: 12px;
|
padding: 10px;
|
||||||
}
|
}
|
||||||
.console-input-row {
|
.console-terminal {
|
||||||
flex-direction: column;
|
min-height: 70vh;
|
||||||
align-items: stretch;
|
|
||||||
}
|
|
||||||
.console-buttons {
|
|
||||||
width: 100%;
|
|
||||||
justify-content: flex-end;
|
|
||||||
}
|
|
||||||
.console-buttons .btn,
|
|
||||||
.console-buttons .button {
|
|
||||||
width: auto;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,33 +1,72 @@
|
|||||||
import { ansiToHtml } from "./ansi.js";
|
import { Terminal } from "/vendor/xterm/xterm.mjs";
|
||||||
|
import { FitAddon } from "/vendor/xterm/addon-fit.mjs";
|
||||||
|
import { WebLinksAddon } from "/vendor/xterm/addon-web-links.mjs";
|
||||||
import { encodeMsgpack, decodeMsgpack } from "./msgpack-helpers.js";
|
import { encodeMsgpack, decodeMsgpack } from "./msgpack-helpers.js";
|
||||||
import { checkFeatureAccess } from "./feature-gate.js";
|
import { checkFeatureAccess } from "./feature-gate.js";
|
||||||
const outputEl = document.getElementById("console-output");
|
|
||||||
|
const containerEl = document.getElementById("terminal");
|
||||||
const statusPill = document.getElementById("status-pill");
|
const statusPill = document.getElementById("status-pill");
|
||||||
const clientLabel = document.getElementById("client-label");
|
const clientLabel = document.getElementById("client-label");
|
||||||
const hostLabel = document.getElementById("host-label");
|
const hostLabel = document.getElementById("host-label");
|
||||||
const userLabel = document.getElementById("user-label");
|
const userLabel = document.getElementById("user-label");
|
||||||
const osLabel = document.getElementById("os-label");
|
const osLabel = document.getElementById("os-label");
|
||||||
const form = document.getElementById("console-form");
|
const reconnectBtn = document.getElementById("reconnect-btn");
|
||||||
const input = document.getElementById("console-input");
|
const clearBtn = document.getElementById("clear-btn");
|
||||||
const ctrlcBtn = document.getElementById("ctrlc-btn");
|
|
||||||
|
|
||||||
const clientId = decodeURIComponent(
|
const clientId = decodeURIComponent(
|
||||||
location.pathname.split("/").filter(Boolean)[0] || "",
|
location.pathname.split("/").filter(Boolean)[0] || "",
|
||||||
);
|
);
|
||||||
const wsProto = location.protocol === "https:" ? "wss" : "ws";
|
const wsProto = location.protocol === "https:" ? "wss" : "ws";
|
||||||
const wsUrl = `${wsProto}://${location.host}/api/clients/${encodeURIComponent(clientId)}/console/ws`;
|
const wsUrl = `${wsProto}://${location.host}/api/clients/${encodeURIComponent(clientId)}/console/ws`;
|
||||||
|
|
||||||
|
const term = new Terminal({
|
||||||
|
cursorBlink: true,
|
||||||
|
fontFamily: '"JetBrains Mono", ui-monospace, Menlo, Consolas, monospace',
|
||||||
|
fontSize: 13,
|
||||||
|
scrollback: 10000,
|
||||||
|
allowProposedApi: true,
|
||||||
|
theme: {
|
||||||
|
background: "#050913",
|
||||||
|
foreground: "#e8edf2",
|
||||||
|
cursor: "#6ee7b7",
|
||||||
|
cursorAccent: "#050913",
|
||||||
|
selectionBackground: "rgba(110, 231, 183, 0.25)",
|
||||||
|
black: "#1a1d29",
|
||||||
|
red: "#ef4444",
|
||||||
|
green: "#22c55e",
|
||||||
|
yellow: "#eab308",
|
||||||
|
blue: "#60a5fa",
|
||||||
|
magenta: "#c084fc",
|
||||||
|
cyan: "#22d3ee",
|
||||||
|
white: "#e8edf2",
|
||||||
|
brightBlack: "#475569",
|
||||||
|
brightRed: "#f87171",
|
||||||
|
brightGreen: "#4ade80",
|
||||||
|
brightYellow: "#facc15",
|
||||||
|
brightBlue: "#93c5fd",
|
||||||
|
brightMagenta: "#d8b4fe",
|
||||||
|
brightCyan: "#67e8f9",
|
||||||
|
brightWhite: "#f8fafc",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const fit = new FitAddon();
|
||||||
|
term.loadAddon(fit);
|
||||||
|
term.loadAddon(new WebLinksAddon());
|
||||||
|
term.open(containerEl);
|
||||||
|
|
||||||
let ws = null;
|
let ws = null;
|
||||||
let connected = false;
|
let connected = false;
|
||||||
let outputBuffer = "";
|
let sessionClosed = false;
|
||||||
let hasOutput = false;
|
let offlineNotified = false;
|
||||||
|
let lastSize = { cols: 0, rows: 0 };
|
||||||
|
let resizeTimer = 0;
|
||||||
|
|
||||||
const urlParams = new URLSearchParams(window.location.search);
|
function tryFit() {
|
||||||
const prefilledCommand = urlParams.get("cmd");
|
try {
|
||||||
if (prefilledCommand && input) {
|
fit.fit();
|
||||||
setTimeout(() => {
|
} catch {
|
||||||
input.value = prefilledCommand;
|
/* ignore: container not yet measured */
|
||||||
input.focus();
|
}
|
||||||
}, 1000);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function setStatus(label, tone = "pill-offline") {
|
function setStatus(label, tone = "pill-offline") {
|
||||||
@@ -36,32 +75,46 @@ function setStatus(label, tone = "pill-offline") {
|
|||||||
statusPill.innerHTML = `<i class="fa-solid fa-circle"></i> ${label}`;
|
statusPill.innerHTML = `<i class="fa-solid fa-circle"></i> ${label}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function appendSystem(text) {
|
function writeSystem(msg) {
|
||||||
outputBuffer += `\n[system] ${text}\n`;
|
term.writeln(`\x1b[2m[${msg}]\x1b[0m`);
|
||||||
renderOutput();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function appendOutput(text) {
|
function sendResize() {
|
||||||
outputBuffer += text;
|
if (sessionClosed) return;
|
||||||
renderOutput();
|
if (!ws || ws.readyState !== WebSocket.OPEN) return;
|
||||||
|
const cols = term.cols;
|
||||||
|
const rows = term.rows;
|
||||||
|
if (!cols || !rows) return;
|
||||||
|
if (cols === lastSize.cols && rows === lastSize.rows) return;
|
||||||
|
lastSize = { cols, rows };
|
||||||
|
ws.send(encodeMsgpack({ type: "resize", cols, rows }));
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderOutput() {
|
function applyOutput(data) {
|
||||||
if (!outputEl) return;
|
if (!data) return;
|
||||||
const normalized = outputBuffer.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
|
if (data instanceof Uint8Array) {
|
||||||
outputEl.innerHTML = `<pre class="console-pre">${ansiToHtml(normalized)}</pre>`;
|
term.write(data);
|
||||||
outputEl.scrollTop = outputEl.scrollHeight;
|
} else if (typeof data === "string") {
|
||||||
|
term.write(data);
|
||||||
|
} else if (data?.buffer instanceof ArrayBuffer) {
|
||||||
|
term.write(new Uint8Array(data.buffer, data.byteOffset ?? 0, data.byteLength));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function connect() {
|
function connect() {
|
||||||
appendSystem("Connecting to console...");
|
sessionClosed = false;
|
||||||
setStatus("Connecting...", "pill-offline");
|
setStatus("Connecting...", "pill-ghost");
|
||||||
|
writeSystem("Connecting...");
|
||||||
|
|
||||||
ws = new WebSocket(wsUrl);
|
ws = new WebSocket(wsUrl);
|
||||||
ws.binaryType = "arraybuffer";
|
ws.binaryType = "arraybuffer";
|
||||||
|
|
||||||
ws.addEventListener("open", () => {
|
ws.addEventListener("open", () => {
|
||||||
connected = true;
|
connected = true;
|
||||||
setStatus("Connected", "pill-online");
|
setStatus("Connected", "pill-online");
|
||||||
|
term.options.disableStdin = false;
|
||||||
|
tryFit();
|
||||||
|
sendResize();
|
||||||
});
|
});
|
||||||
|
|
||||||
ws.addEventListener("message", (event) => {
|
ws.addEventListener("message", (event) => {
|
||||||
@@ -74,32 +127,36 @@ function connect() {
|
|||||||
if (hostLabel) hostLabel.textContent = payload.host || "unknown";
|
if (hostLabel) hostLabel.textContent = payload.host || "unknown";
|
||||||
if (userLabel) userLabel.textContent = payload.user || "unknown";
|
if (userLabel) userLabel.textContent = payload.user || "unknown";
|
||||||
if (osLabel) osLabel.textContent = payload.os || "unknown";
|
if (osLabel) osLabel.textContent = payload.os || "unknown";
|
||||||
setStatus("Connected", "pill-online");
|
setStatus("Live", "pill-online");
|
||||||
break;
|
break;
|
||||||
case "status":
|
case "status":
|
||||||
if (payload.status === "offline") {
|
if (payload.status === "offline") {
|
||||||
setStatus("Offline", "pill-offline");
|
setStatus("Offline", "pill-offline");
|
||||||
appendSystem(payload.reason || "Client offline");
|
if (!offlineNotified) {
|
||||||
} else if (payload.status === "connecting" && !hasOutput) {
|
offlineNotified = true;
|
||||||
setStatus("Connecting...", "pill-ghost");
|
writeSystem(payload.reason || "Client offline");
|
||||||
|
}
|
||||||
|
sessionClosed = true;
|
||||||
|
term.options.disableStdin = true;
|
||||||
} else if (payload.status === "closed") {
|
} else if (payload.status === "closed") {
|
||||||
setStatus("Closed", "pill-offline");
|
setStatus("Closed", "pill-offline");
|
||||||
appendSystem(payload.reason || "Console closed");
|
writeSystem(payload.reason || "Console closed");
|
||||||
|
sessionClosed = true;
|
||||||
|
term.options.disableStdin = true;
|
||||||
|
} else if (payload.status === "online") {
|
||||||
|
offlineNotified = false;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case "output": {
|
case "output":
|
||||||
if (payload.data) {
|
applyOutput(payload.data);
|
||||||
hasOutput = true;
|
if (payload.error) writeSystem(payload.error);
|
||||||
setStatus("Live", "pill-online");
|
|
||||||
appendOutput(payload.data);
|
|
||||||
}
|
|
||||||
if (payload.error) appendSystem(payload.error);
|
|
||||||
if (typeof payload.exitCode === "number") {
|
if (typeof payload.exitCode === "number") {
|
||||||
appendSystem(`Process exited (${payload.exitCode})`);
|
term.writeln(`\r\n\x1b[33m[Process exited (${payload.exitCode})]\x1b[0m`);
|
||||||
setStatus("Closed", "pill-offline");
|
setStatus("Closed", "pill-offline");
|
||||||
|
sessionClosed = true;
|
||||||
|
term.options.disableStdin = true;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -109,7 +166,8 @@ function connect() {
|
|||||||
if (!connected) return;
|
if (!connected) return;
|
||||||
connected = false;
|
connected = false;
|
||||||
setStatus("Disconnected", "pill-offline");
|
setStatus("Disconnected", "pill-offline");
|
||||||
appendSystem("Connection closed");
|
if (!sessionClosed) writeSystem("Connection closed");
|
||||||
|
term.options.disableStdin = true;
|
||||||
});
|
});
|
||||||
|
|
||||||
ws.addEventListener("error", () => {
|
ws.addEventListener("error", () => {
|
||||||
@@ -117,40 +175,41 @@ function connect() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function sendInput(value) {
|
term.onData((d) => {
|
||||||
if (!ws || ws.readyState !== WebSocket.OPEN) {
|
if (!ws || ws.readyState !== WebSocket.OPEN) return;
|
||||||
appendSystem("Socket not ready");
|
if (sessionClosed) return;
|
||||||
return;
|
ws.send(encodeMsgpack({ type: "input", data: d }));
|
||||||
}
|
});
|
||||||
ws.send(encodeMsgpack({ type: "input", data: value }));
|
|
||||||
}
|
|
||||||
|
|
||||||
function wireInput() {
|
term.onResize(() => {
|
||||||
if (!form || !input) return;
|
sendResize();
|
||||||
|
});
|
||||||
|
|
||||||
form.addEventListener("submit", (e) => {
|
window.addEventListener("resize", () => {
|
||||||
e.preventDefault();
|
clearTimeout(resizeTimer);
|
||||||
const value = input.value;
|
resizeTimer = setTimeout(tryFit, 120);
|
||||||
if (!value.trim()) {
|
});
|
||||||
input.value = "";
|
|
||||||
return;
|
reconnectBtn?.addEventListener("click", () => {
|
||||||
|
try { ws?.close(); } catch { /* ignore */ }
|
||||||
|
term.reset();
|
||||||
|
connect();
|
||||||
|
});
|
||||||
|
|
||||||
|
clearBtn?.addEventListener("click", () => {
|
||||||
|
term.clear();
|
||||||
|
});
|
||||||
|
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
tryFit();
|
||||||
|
checkFeatureAccess("console", clientId).then((ok) => ok && connect());
|
||||||
|
});
|
||||||
|
|
||||||
|
const prefilledCommand = new URLSearchParams(window.location.search).get("cmd");
|
||||||
|
if (prefilledCommand) {
|
||||||
|
setTimeout(() => {
|
||||||
|
if (ws?.readyState === WebSocket.OPEN && !sessionClosed) {
|
||||||
|
ws.send(encodeMsgpack({ type: "input", data: prefilledCommand + "\r" }));
|
||||||
}
|
}
|
||||||
const text = value.endsWith("\n") ? value : `${value}\n`;
|
}, 1200);
|
||||||
sendInput(text);
|
|
||||||
input.value = "";
|
|
||||||
input.focus();
|
|
||||||
});
|
|
||||||
|
|
||||||
input.addEventListener("keydown", (e) => {
|
|
||||||
if (e.key === "Enter" && !e.shiftKey) {
|
|
||||||
e.preventDefault();
|
|
||||||
form.dispatchEvent(new Event("submit"));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
ctrlcBtn?.addEventListener("click", () => {
|
|
||||||
sendInput("\u0003");
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
wireInput();
|
|
||||||
checkFeatureAccess("console", clientId).then(ok => ok && connect());
|
|
||||||
|
|||||||
@@ -11,8 +11,9 @@
|
|||||||
<link rel="stylesheet" href="/vendor/inter/700.css" />
|
<link rel="stylesheet" href="/vendor/inter/700.css" />
|
||||||
<link rel="stylesheet" href="/vendor/jetbrains-mono/400.css" />
|
<link rel="stylesheet" href="/vendor/jetbrains-mono/400.css" />
|
||||||
<link rel="stylesheet" href="/vendor/jetbrains-mono/600.css" />
|
<link rel="stylesheet" href="/vendor/jetbrains-mono/600.css" />
|
||||||
<link rel="stylesheet" href="/assets/tailwind.css" />
|
<link rel="stylesheet" href="/assets/tailwind.css" />
|
||||||
<link rel="stylesheet" href="/vendor/fontawesome/css/all.min.css" />
|
<link rel="stylesheet" href="/vendor/fontawesome/css/all.min.css" />
|
||||||
|
<link rel="stylesheet" href="/vendor/xterm/xterm.css" />
|
||||||
<link rel="icon" type="image/x-icon" href="/assets/favicon.ico" />
|
<link rel="icon" type="image/x-icon" href="/assets/favicon.ico" />
|
||||||
<link rel="stylesheet" href="/assets/main.css" />
|
<link rel="stylesheet" href="/assets/main.css" />
|
||||||
<link rel="stylesheet" href="/assets/custom.css" />
|
<link rel="stylesheet" href="/assets/custom.css" />
|
||||||
@@ -22,49 +23,37 @@
|
|||||||
<header id="top-nav"></header>
|
<header id="top-nav"></header>
|
||||||
<main class="console-main">
|
<main class="console-main">
|
||||||
<div class="console-card">
|
<div class="console-card">
|
||||||
<div class="console-meta">
|
<div class="console-header">
|
||||||
<div class="console-target">
|
<div class="console-meta">
|
||||||
<div class="muted text-sm">Client</div>
|
<div class="console-target">
|
||||||
<div class="console-target-id" id="client-label">-</div>
|
<div class="muted text-sm">Client</div>
|
||||||
|
<div class="console-target-id" id="client-label">-</div>
|
||||||
|
</div>
|
||||||
|
<div class="console-target">
|
||||||
|
<div class="muted text-sm">Host</div>
|
||||||
|
<div class="console-target-id" id="host-label">-</div>
|
||||||
|
</div>
|
||||||
|
<div class="console-target">
|
||||||
|
<div class="muted text-sm">User</div>
|
||||||
|
<div class="console-target-id" id="user-label">-</div>
|
||||||
|
</div>
|
||||||
|
<div class="console-target">
|
||||||
|
<div class="muted text-sm">OS</div>
|
||||||
|
<div class="console-target-id" id="os-label">-</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="console-target">
|
<div class="console-toolbar">
|
||||||
<div class="muted text-sm">Host</div>
|
<span id="status-pill" class="pill pill-offline"><i class="fa-solid fa-circle"></i> Offline</span>
|
||||||
<div class="console-target-id" id="host-label">-</div>
|
<button type="button" id="reconnect-btn" class="button ghost" title="Reconnect">
|
||||||
</div>
|
<i class="fa-solid fa-rotate-right"></i><span>Reconnect</span>
|
||||||
<div class="console-target">
|
</button>
|
||||||
<div class="muted text-sm">User</div>
|
<button type="button" id="clear-btn" class="button ghost" title="Clear screen">
|
||||||
<div class="console-target-id" id="user-label">-</div>
|
<i class="fa-solid fa-eraser"></i><span>Clear</span>
|
||||||
</div>
|
</button>
|
||||||
<div class="console-target">
|
|
||||||
<div class="muted text-sm">OS</div>
|
|
||||||
<div class="console-target-id" id="os-label">-</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div id="terminal" class="console-terminal"></div>
|
||||||
id="console-output"
|
|
||||||
class="console-output"
|
|
||||||
aria-live="polite"
|
|
||||||
></div>
|
|
||||||
|
|
||||||
<form id="console-form" class="console-input-row">
|
|
||||||
<div class="prompt">$</div>
|
|
||||||
<textarea
|
|
||||||
id="console-input"
|
|
||||||
rows="1"
|
|
||||||
autocomplete="off"
|
|
||||||
spellcheck="false"
|
|
||||||
placeholder="Type a command and press Enter"
|
|
||||||
></textarea>
|
|
||||||
<div class="console-buttons">
|
|
||||||
<button type="submit" class="button primary">
|
|
||||||
<i class="fa-solid fa-paper-plane"></i><span>Send</span>
|
|
||||||
</button>
|
|
||||||
<button type="button" id="ctrlc-btn" class="button ghost">
|
|
||||||
<i class="fa-solid fa-ban"></i><span>Ctrl+C</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
<script defer src="/vendor/msgpackr/msgpackr.js"></script>
|
<script defer src="/vendor/msgpackr/msgpackr.js"></script>
|
||||||
|
|||||||
@@ -207,6 +207,27 @@ if (hljsBuild.success) {
|
|||||||
}
|
}
|
||||||
rmSync(hljsTmp, { force: true });
|
rmSync(hljsTmp, { force: true });
|
||||||
|
|
||||||
|
/* ── xterm.js ────────────────────────────────────────────────────── */
|
||||||
|
|
||||||
|
console.log("Copying xterm.js ...");
|
||||||
|
const xtermRoot = path.join(NM, "@xterm");
|
||||||
|
copyFile(
|
||||||
|
path.join(xtermRoot, "xterm", "lib", "xterm.mjs"),
|
||||||
|
path.join(VENDOR, "xterm", "xterm.mjs"),
|
||||||
|
);
|
||||||
|
copyFile(
|
||||||
|
path.join(xtermRoot, "xterm", "css", "xterm.css"),
|
||||||
|
path.join(VENDOR, "xterm", "xterm.css"),
|
||||||
|
);
|
||||||
|
copyFile(
|
||||||
|
path.join(xtermRoot, "addon-fit", "lib", "addon-fit.mjs"),
|
||||||
|
path.join(VENDOR, "xterm", "addon-fit.mjs"),
|
||||||
|
);
|
||||||
|
copyFile(
|
||||||
|
path.join(xtermRoot, "addon-web-links", "lib", "addon-web-links.mjs"),
|
||||||
|
path.join(VENDOR, "xterm", "addon-web-links.mjs"),
|
||||||
|
);
|
||||||
|
|
||||||
/* ── ansi-to-html (bundle for ESM import) ────────────────────────── */
|
/* ── ansi-to-html (bundle for ESM import) ────────────────────────── */
|
||||||
|
|
||||||
console.log("Bundling ansi-to-html ...");
|
console.log("Bundling ansi-to-html ...");
|
||||||
|
|||||||
@@ -19,6 +19,8 @@ const MIME_TYPES: Record<string, string> = {
|
|||||||
".html": "text/html; charset=utf-8",
|
".html": "text/html; charset=utf-8",
|
||||||
".css": "text/css; charset=utf-8",
|
".css": "text/css; charset=utf-8",
|
||||||
".js": "text/javascript; charset=utf-8",
|
".js": "text/javascript; charset=utf-8",
|
||||||
|
".mjs": "text/javascript; charset=utf-8",
|
||||||
|
".map": "application/json",
|
||||||
".json": "application/json",
|
".json": "application/json",
|
||||||
".png": "image/png",
|
".png": "image/png",
|
||||||
".jpg": "image/jpeg",
|
".jpg": "image/jpeg",
|
||||||
|
|||||||
@@ -1181,8 +1181,6 @@ export function sendHVNCCommand(target: ClientInfo, commandType: string, payload
|
|||||||
return broadcastFrameToViewers(sessionManager.getWebcamSessionsForClient(clientId), buf, header);
|
return broadcastFrameToViewers(sessionManager.getWebcamSessionsForClient(clientId), buf, header);
|
||||||
};
|
};
|
||||||
|
|
||||||
const textDecoder = new TextDecoder();
|
|
||||||
|
|
||||||
export function handleConsoleViewerMessage(ws: ServerWebSocket<SocketData>, raw: string | ArrayBuffer | Uint8Array) {
|
export function handleConsoleViewerMessage(ws: ServerWebSocket<SocketData>, raw: string | ArrayBuffer | Uint8Array) {
|
||||||
const payload = decodeViewerPayload(raw);
|
const payload = decodeViewerPayload(raw);
|
||||||
if (!payload) return;
|
if (!payload) return;
|
||||||
@@ -1193,7 +1191,6 @@ export function handleConsoleViewerMessage(ws: ServerWebSocket<SocketData>, raw:
|
|||||||
const { clientId, sessionId } = ws.data;
|
const { clientId, sessionId } = ws.data;
|
||||||
const target = clientManager.getClient(clientId);
|
const target = clientManager.getClient(clientId);
|
||||||
if (!target) {
|
if (!target) {
|
||||||
safeSendViewer(ws, { type: "status", status: "offline", reason: "Client is offline", sessionId });
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1225,11 +1222,10 @@ export function handleConsoleOutput(clientId: string, payload: any) {
|
|||||||
const session = sessionManager.getConsoleSession(sessionId);
|
const session = sessionManager.getConsoleSession(sessionId);
|
||||||
if (!session) return;
|
if (!session) return;
|
||||||
if (session.clientId !== clientId) return;
|
if (session.clientId !== clientId) return;
|
||||||
const data = payload.data ? textDecoder.decode(payload.data as Uint8Array) : "";
|
|
||||||
safeSendViewer(session.viewer, {
|
safeSendViewer(session.viewer, {
|
||||||
type: "output",
|
type: "output",
|
||||||
sessionId,
|
sessionId,
|
||||||
data,
|
data: payload.data ?? null,
|
||||||
exitCode: payload.exitCode,
|
exitCode: payload.exitCode,
|
||||||
error: payload.error,
|
error: payload.error,
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user