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