feat(muxterm): wire protocol types and parser
This commit is contained in:
@@ -0,0 +1,94 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import "encoding/json"
|
||||||
|
|
||||||
|
// --- Client-to-server message types ---
|
||||||
|
|
||||||
|
// AttachMsg requests attaching to a named tmux session.
|
||||||
|
type AttachMsg struct {
|
||||||
|
Attach string `json:"attach"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ResizeMsg requests a PTY resize.
|
||||||
|
type ResizeMsg struct {
|
||||||
|
Resize struct {
|
||||||
|
Cols int `json:"cols"`
|
||||||
|
Rows int `json:"rows"`
|
||||||
|
} `json:"resize"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// DetachMsg requests detaching from the current session.
|
||||||
|
type DetachMsg struct {
|
||||||
|
Detach bool `json:"detach"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Server-to-client message types ---
|
||||||
|
|
||||||
|
// AttachedMsg confirms a successful attach.
|
||||||
|
type AttachedMsg struct {
|
||||||
|
Attached string `json:"attached"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ErrorMsg reports an error to the client.
|
||||||
|
type ErrorMsg struct {
|
||||||
|
Error string `json:"error"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExitedMsg reports that a session's process has exited.
|
||||||
|
type ExitedMsg struct {
|
||||||
|
Exited string `json:"exited"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ParseControlMessage unmarshals a JSON control message and returns the
|
||||||
|
// message kind and a pointer to the typed value. Returns ("invalid", nil)
|
||||||
|
// for unparseable JSON and ("unknown", nil) for unrecognised keys.
|
||||||
|
func ParseControlMessage(data []byte) (string, interface{}) {
|
||||||
|
var raw map[string]json.RawMessage
|
||||||
|
if err := json.Unmarshal(data, &raw); err != nil {
|
||||||
|
return "invalid", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, ok := raw["attach"]; ok {
|
||||||
|
var msg AttachMsg
|
||||||
|
if err := json.Unmarshal(data, &msg); err != nil {
|
||||||
|
return "invalid", nil
|
||||||
|
}
|
||||||
|
return "attach", &msg
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, ok := raw["resize"]; ok {
|
||||||
|
var msg ResizeMsg
|
||||||
|
if err := json.Unmarshal(data, &msg); err != nil {
|
||||||
|
return "invalid", nil
|
||||||
|
}
|
||||||
|
return "resize", &msg
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, ok := raw["detach"]; ok {
|
||||||
|
var msg DetachMsg
|
||||||
|
if err := json.Unmarshal(data, &msg); err != nil {
|
||||||
|
return "invalid", nil
|
||||||
|
}
|
||||||
|
return "detach", &msg
|
||||||
|
}
|
||||||
|
|
||||||
|
return "unknown", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarshalAttached returns JSON for an AttachedMsg.
|
||||||
|
func MarshalAttached(session string) []byte {
|
||||||
|
b, _ := json.Marshal(AttachedMsg{Attached: session})
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarshalError returns JSON for an ErrorMsg.
|
||||||
|
func MarshalError(msg string) []byte {
|
||||||
|
b, _ := json.Marshal(ErrorMsg{Error: msg})
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarshalExited returns JSON for an ExitedMsg.
|
||||||
|
func MarshalExited(session string) []byte {
|
||||||
|
b, _ := json.Marshal(ExitedMsg{Exited: session})
|
||||||
|
return b
|
||||||
|
}
|
||||||
@@ -0,0 +1,202 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
// --- Client-to-server message type tests ---
|
||||||
|
|
||||||
|
func TestAttachMsgJSON(t *testing.T) {
|
||||||
|
msg := AttachMsg{Attach: "my-session"}
|
||||||
|
data, err := json.Marshal(msg)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("marshal AttachMsg: %v", err)
|
||||||
|
}
|
||||||
|
var m map[string]interface{}
|
||||||
|
json.Unmarshal(data, &m)
|
||||||
|
if m["attach"] != "my-session" {
|
||||||
|
t.Errorf("expected attach=my-session, got %v", m["attach"])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestResizeMsgJSON(t *testing.T) {
|
||||||
|
msg := ResizeMsg{Resize: struct {
|
||||||
|
Cols int `json:"cols"`
|
||||||
|
Rows int `json:"rows"`
|
||||||
|
}{Cols: 80, Rows: 24}}
|
||||||
|
data, err := json.Marshal(msg)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("marshal ResizeMsg: %v", err)
|
||||||
|
}
|
||||||
|
var m map[string]interface{}
|
||||||
|
json.Unmarshal(data, &m)
|
||||||
|
resize, ok := m["resize"].(map[string]interface{})
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("expected resize to be a map, got %T", m["resize"])
|
||||||
|
}
|
||||||
|
if resize["cols"].(float64) != 80 {
|
||||||
|
t.Errorf("expected cols=80, got %v", resize["cols"])
|
||||||
|
}
|
||||||
|
if resize["rows"].(float64) != 24 {
|
||||||
|
t.Errorf("expected rows=24, got %v", resize["rows"])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDetachMsgJSON(t *testing.T) {
|
||||||
|
msg := DetachMsg{Detach: true}
|
||||||
|
data, err := json.Marshal(msg)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("marshal DetachMsg: %v", err)
|
||||||
|
}
|
||||||
|
var m map[string]interface{}
|
||||||
|
json.Unmarshal(data, &m)
|
||||||
|
if m["detach"] != true {
|
||||||
|
t.Errorf("expected detach=true, got %v", m["detach"])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Server-to-client message type tests ---
|
||||||
|
|
||||||
|
func TestAttachedMsgJSON(t *testing.T) {
|
||||||
|
msg := AttachedMsg{Attached: "sess1"}
|
||||||
|
data, err := json.Marshal(msg)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("marshal AttachedMsg: %v", err)
|
||||||
|
}
|
||||||
|
var m map[string]interface{}
|
||||||
|
json.Unmarshal(data, &m)
|
||||||
|
if m["attached"] != "sess1" {
|
||||||
|
t.Errorf("expected attached=sess1, got %v", m["attached"])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestErrorMsgJSON(t *testing.T) {
|
||||||
|
msg := ErrorMsg{Error: "something broke"}
|
||||||
|
data, err := json.Marshal(msg)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("marshal ErrorMsg: %v", err)
|
||||||
|
}
|
||||||
|
var m map[string]interface{}
|
||||||
|
json.Unmarshal(data, &m)
|
||||||
|
if m["error"] != "something broke" {
|
||||||
|
t.Errorf("expected error='something broke', got %v", m["error"])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestExitedMsgJSON(t *testing.T) {
|
||||||
|
msg := ExitedMsg{Exited: "sess1"}
|
||||||
|
data, err := json.Marshal(msg)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("marshal ExitedMsg: %v", err)
|
||||||
|
}
|
||||||
|
var m map[string]interface{}
|
||||||
|
json.Unmarshal(data, &m)
|
||||||
|
if m["exited"] != "sess1" {
|
||||||
|
t.Errorf("expected exited=sess1, got %v", m["exited"])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- ParseControlMessage tests ---
|
||||||
|
|
||||||
|
func TestParseControlMessage_Attach(t *testing.T) {
|
||||||
|
data := []byte(`{"attach":"dev"}`)
|
||||||
|
kind, val := ParseControlMessage(data)
|
||||||
|
if kind != "attach" {
|
||||||
|
t.Fatalf("expected kind=attach, got %q", kind)
|
||||||
|
}
|
||||||
|
msg, ok := val.(*AttachMsg)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("expected *AttachMsg, got %T", val)
|
||||||
|
}
|
||||||
|
if msg.Attach != "dev" {
|
||||||
|
t.Errorf("expected Attach=dev, got %q", msg.Attach)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParseControlMessage_Resize(t *testing.T) {
|
||||||
|
data := []byte(`{"resize":{"cols":120,"rows":40}}`)
|
||||||
|
kind, val := ParseControlMessage(data)
|
||||||
|
if kind != "resize" {
|
||||||
|
t.Fatalf("expected kind=resize, got %q", kind)
|
||||||
|
}
|
||||||
|
msg, ok := val.(*ResizeMsg)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("expected *ResizeMsg, got %T", val)
|
||||||
|
}
|
||||||
|
if msg.Resize.Cols != 120 || msg.Resize.Rows != 40 {
|
||||||
|
t.Errorf("expected 120x40, got %dx%d", msg.Resize.Cols, msg.Resize.Rows)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParseControlMessage_Detach(t *testing.T) {
|
||||||
|
data := []byte(`{"detach":true}`)
|
||||||
|
kind, val := ParseControlMessage(data)
|
||||||
|
if kind != "detach" {
|
||||||
|
t.Fatalf("expected kind=detach, got %q", kind)
|
||||||
|
}
|
||||||
|
msg, ok := val.(*DetachMsg)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("expected *DetachMsg, got %T", val)
|
||||||
|
}
|
||||||
|
if !msg.Detach {
|
||||||
|
t.Errorf("expected Detach=true")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParseControlMessage_Unknown(t *testing.T) {
|
||||||
|
data := []byte(`{"foo":"bar"}`)
|
||||||
|
kind, val := ParseControlMessage(data)
|
||||||
|
if kind != "unknown" {
|
||||||
|
t.Errorf("expected kind=unknown, got %q", kind)
|
||||||
|
}
|
||||||
|
if val != nil {
|
||||||
|
t.Errorf("expected nil val for unknown, got %v", val)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParseControlMessage_Invalid(t *testing.T) {
|
||||||
|
data := []byte(`not json at all`)
|
||||||
|
kind, val := ParseControlMessage(data)
|
||||||
|
if kind != "invalid" {
|
||||||
|
t.Errorf("expected kind=invalid, got %q", kind)
|
||||||
|
}
|
||||||
|
if val != nil {
|
||||||
|
t.Errorf("expected nil val for invalid, got %v", val)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Marshal function tests ---
|
||||||
|
|
||||||
|
func TestMarshalAttached(t *testing.T) {
|
||||||
|
data := MarshalAttached("sess1")
|
||||||
|
var m map[string]interface{}
|
||||||
|
if err := json.Unmarshal(data, &m); err != nil {
|
||||||
|
t.Fatalf("unmarshal: %v", err)
|
||||||
|
}
|
||||||
|
if m["attached"] != "sess1" {
|
||||||
|
t.Errorf("expected attached=sess1, got %v", m["attached"])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMarshalError(t *testing.T) {
|
||||||
|
data := MarshalError("bad thing")
|
||||||
|
var m map[string]interface{}
|
||||||
|
if err := json.Unmarshal(data, &m); err != nil {
|
||||||
|
t.Fatalf("unmarshal: %v", err)
|
||||||
|
}
|
||||||
|
if m["error"] != "bad thing" {
|
||||||
|
t.Errorf("expected error='bad thing', got %v", m["error"])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMarshalExited(t *testing.T) {
|
||||||
|
data := MarshalExited("sess1")
|
||||||
|
var m map[string]interface{}
|
||||||
|
if err := json.Unmarshal(data, &m); err != nil {
|
||||||
|
t.Fatalf("unmarshal: %v", err)
|
||||||
|
}
|
||||||
|
if m["exited"] != "sess1" {
|
||||||
|
t.Errorf("expected exited=sess1, got %v", m["exited"])
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user