From 351bedd7e25c5725504e96227978aa29d7dda426 Mon Sep 17 00:00:00 2001 From: James Rutherford / creativenucleus Date: Fri, 3 Nov 2023 15:38:26 +0000 Subject: [PATCH 01/11] Unfinished - import cycle --- client-jukebox.go | 10 +- client-panel.go | 34 ++-- client-ws.go | 27 ++-- msg.go => comms/msg.go | 40 ++--- comms/websocket.go | 32 ++++ host-panel.go | 65 ++++---- jam-session-manager.go | 113 -------------- jukebox.go | 9 +- local-jukebox.go | 5 +- machines/machines.go | 3 +- {server => machines}/names.go | 2 +- nusan.go | 25 +-- server/config.go | 18 +++ .../session-config.go | 8 +- jam-session-conn.go => server/session-conn.go | 31 ++-- server/session-manager.go | 113 ++++++++++++++ jam-session.go => server/session.go | 146 ++++++++++-------- websocket-client.go | 7 - 18 files changed, 381 insertions(+), 307 deletions(-) rename msg.go => comms/msg.go (67%) create mode 100644 comms/websocket.go delete mode 100644 jam-session-manager.go rename {server => machines}/names.go (93%) create mode 100644 server/config.go rename jam-session-config.go => server/session-config.go (56%) rename jam-session-conn.go => server/session-conn.go (78%) create mode 100644 server/session-manager.go rename jam-session.go => server/session.go (68%) diff --git a/client-jukebox.go b/client-jukebox.go index 0d5c103..a421975 100644 --- a/client-jukebox.go +++ b/client-jukebox.go @@ -1,9 +1,13 @@ package main -import "log" +import ( + "log" + + "github.com/creativenucleus/bytejammer/comms" +) func startClientJukebox(host string, port int, playlist *Playlist) error { - ch := make(chan Msg) + ch := make(chan comms.Msg) j, err := NewJukebox(playlist, &ch) if err != nil { return err @@ -23,7 +27,7 @@ func startClientJukebox(host string, port int, playlist *Playlist) error { switch msg.Type { case "tic-state": // #TODO: line endings for data? UTF-8? - msg := Msg{Type: "tic-state", TicState: msg.TicState} + msg := comms.Msg{Type: "tic-state", TicState: msg.TicState} err = ws.sendData(msg) if err != nil { // #TODO: soften! diff --git a/client-panel.go b/client-panel.go index 33d694f..34c0ad5 100644 --- a/client-panel.go +++ b/client-panel.go @@ -14,6 +14,7 @@ import ( "github.com/gorilla/websocket" "github.com/tyler-sommer/stick" + "github.com/creativenucleus/bytejammer/comms" "github.com/creativenucleus/bytejammer/embed" ) @@ -27,7 +28,7 @@ const ( type ClientPanel struct { // #TODO: lock down to receiver only - chSendServerStatus chan ClientServerStatus + chSendServerStatus chan comms.DataClientServerStatus wsClient *websocket.Conn wsMutex sync.Mutex } @@ -50,7 +51,7 @@ func startClientPanel(port int) error { fmt.Printf("In a web browser, go to http://localhost:%d/%s\n", port, session) cp := ClientPanel{ - chSendServerStatus: make(chan ClientServerStatus), + chSendServerStatus: make(chan comms.DataClientServerStatus), } http.HandleFunc(fmt.Sprintf("/%s", session), cp.webClientIndex) http.HandleFunc(fmt.Sprintf("/%s/api/identity.json", session), cp.webClientApiIdentityJSON) @@ -112,7 +113,7 @@ func (cp *ClientPanel) webClientApiIdentityJSON(w http.ResponseWriter, r *http.R func (cp *ClientPanel) webClientApiJoinServerJSON(w http.ResponseWriter, r *http.Request) { switch r.Method { case "POST": - cp.chSendServerStatus <- ClientServerStatus{isConnected: false} + cp.chSendServerStatus <- comms.DataClientServerStatus{IsConnected: false} // #TODO: Cleaner way to do this? type reqType struct { @@ -156,25 +157,30 @@ func (cp *ClientPanel) webClientApiJoinServerJSON(w http.ResponseWriter, r *http func (cp *ClientPanel) wsWebClient() func(http.ResponseWriter, *http.Request) { return func(w http.ResponseWriter, r *http.Request) { var err error - cp.wsClient, err = wsUpgrader.Upgrade(w, r, nil) + + comms.WsUpgrade(w, r, func(conn *websocket.Conn) error { + cp.wsClient = conn + defer func() { cp.wsClient = nil }() + + go cp.wsRead() + go cp.wsWrite() + + // #TODO: handle exit + for { + } + + return nil + }) if err != nil { log.Print("upgrade:", err) return } - defer cp.wsClient.Close() - - go cp.wsRead() - go cp.wsWrite() - - // #TODO: handle exit - for { - } } } func (cp *ClientPanel) wsRead() { for { - var msg Msg + var msg comms.Msg err := cp.wsClient.ReadJSON(&msg) if err != nil { log.Println("read:", err) @@ -203,7 +209,7 @@ func (cp *ClientPanel) wsWrite() { // fmt.Println("TICKER!") case status := <-cp.chSendServerStatus: - msg := Msg{Type: "server-status", ServerStatus: status} + msg := comms.Msg{Type: "server-status", ServerStatus: status} err := cp.sendData(&msg) if err != nil { // #TODO: relax diff --git a/client-ws.go b/client-ws.go index 5ea77e3..38fc2d9 100644 --- a/client-ws.go +++ b/client-ws.go @@ -7,6 +7,7 @@ import ( "path/filepath" "time" + "github.com/creativenucleus/bytejammer/comms" "github.com/creativenucleus/bytejammer/config" "github.com/creativenucleus/bytejammer/machines" "github.com/creativenucleus/bytejammer/util" @@ -14,18 +15,14 @@ import ( type ClientWS struct { ws *WebSocketLink - chMsg chan Msg + chMsg chan comms.Msg basepath string } -type ClientServerStatus struct { - isConnected bool -} - -func startClientServerConn(host string, port int, identity *Identity, chServerStatus chan ClientServerStatus) error { - chServerStatus <- ClientServerStatus{isConnected: false} +func startClientServerConn(host string, port int, identity *Identity, chServerStatus chan comms.DataClientServerStatus) error { + chServerStatus <- comms.DataClientServerStatus{IsConnected: false} cws := ClientWS{ - chMsg: make(chan Msg), + chMsg: make(chan comms.Msg), } cws.basepath = filepath.Clean(fmt.Sprintf("%sclient-data/%s", config.WORK_DIR, util.GetSlugFromTime(time.Now()))) @@ -50,7 +47,7 @@ func startClientServerConn(host string, port int, identity *Identity, chServerSt break } defer cws.ws.Close() - chServerStatus <- ClientServerStatus{isConnected: true} + chServerStatus <- comms.DataClientServerStatus{IsConnected: true} m, err := machines.LaunchMachine("TIC-80", true, true, false) if err != nil { @@ -78,7 +75,7 @@ func clientOpenConnection(host string, port int) (*WebSocketLink, error) { func (cws *ClientWS) clientWsReader(tic *machines.Tic) error { for { - var msg Msg + var msg comms.Msg err := cws.ws.conn.ReadJSON(&msg) if err != nil { log.Fatal(err) @@ -95,13 +92,13 @@ func (cws *ClientWS) clientWsReader(tic *machines.Tic) error { cws.handleChallengeRequest(msg.ChallengeRequest.Challenge) case "tic-state": - tic.WriteImportCode(msg.TicState) + tic.WriteImportCode(msg.TicState.State) } } } func (cws *ClientWS) handleChallengeRequest(challenge string) { - cws.chMsg <- Msg{Type: "challenge-response", ChallengeResponse: DataChallengeResponse{Challenge: challenge + " ~ response"}} + cws.chMsg <- comms.Msg{Type: "challenge-response", ChallengeResponse: comms.DataChallengeResponse{Challenge: challenge + " ~ response"}} } // #TODO: fatalErr @@ -112,9 +109,9 @@ func (cws *ClientWS) clientWsWriter(tic *machines.Tic, identity *Identity) { log.Fatal(err) } - msg := Msg{ + msg := comms.Msg{ Type: "identity", - Identity: DataIdentity{ + Identity: comms.DataIdentity{ Uuid: identity.Uuid.String(), DisplayName: identity.DisplayName, PublicKey: publicKeyRaw, @@ -160,7 +157,7 @@ func (cws *ClientWS) clientWsWriter(tic *machines.Tic, identity *Identity) { } // #TODO: line endings for data? UTF-8? - msg := Msg{Type: "tic-state", TicState: *ticState} + msg := comms.Msg{Type: "tic-state", TicState: *ticState} err = cws.ws.sendData(msg) if err != nil { log.Fatal(err) diff --git a/msg.go b/comms/msg.go similarity index 67% rename from msg.go rename to comms/msg.go index 3f00616..81fbfec 100644 --- a/msg.go +++ b/comms/msg.go @@ -1,6 +1,9 @@ -package main +package comms -import "github.com/creativenucleus/bytejammer/machines" +import ( + "github.com/creativenucleus/bytejammer/machines" + "github.com/creativenucleus/bytejammer/server" +) type DataLog struct { Msg string @@ -13,12 +16,18 @@ type MsgTicState struct { CursorY int } +type DataClientServerStatus struct { + IsConnected bool +} + type DataIdentity struct { Uuid string `json:"uuid"` DisplayName string `json:"displayName"` PublicKey []byte `json:"publicKey"` } +type DataTicState machines.TicState + type DataCloseMachine struct { Uuid string `json:"uuid"` } @@ -42,34 +51,15 @@ type DataChallengeResponse struct { } type MsgServerStatus struct { - Type string `json:"type"` - Data struct { - Clients []struct { - Uuid string - DisplayName string - ShortUuid string - Status string - MachineUuid string - LastPingTime string - } - Machines []struct { - Uuid string - MachineName string - ProcessID int - Platform string - Status string - ClientUuid string - JammerDisplayName string - LastSnapshotTime string - } - } `json:"data"` + Type string `json:"type"` + Data server.SessionStatus `json:"data"` } type Msg struct { Type string `json:"type"` Identity DataIdentity `json:"identity,omitempty"` - TicState machines.TicState `json:"tic-state,omitempty"` - ServerStatus ClientServerStatus `json:"server-status,omitempty"` + TicState DataTicState `json:"tic-state,omitempty"` + ServerStatus DataClientServerStatus `json:"server-status,omitempty"` Log DataLog `json:"log,omitempty"` ConnectMachineClient DataConnectMachineClient `json:"connect-machine-client,omitempty"` DisconnectMachineClient DataDisconnectMachineClient `json:"disconnect-machine-client,omitempty"` diff --git a/comms/websocket.go b/comms/websocket.go new file mode 100644 index 0000000..9aa9b6a --- /dev/null +++ b/comms/websocket.go @@ -0,0 +1,32 @@ +package comms + +import ( + "fmt" + "net/http" + + "github.com/gorilla/websocket" +) + +var ( + WS_UPGRADER = websocket.Upgrader{ + ReadBufferSize: 1024, + WriteBufferSize: 1024, + } +) + +type FnWsConn func(*websocket.Conn) error + +func WsUpgrade(w http.ResponseWriter, r *http.Request, fn FnWsConn) error { + conn, err := WS_UPGRADER.Upgrade(w, r, nil) + if err != nil { + return fmt.Errorf("client couldn't upgrade: ", err) + } + defer conn.Close() + + err = fn(conn) + if err != nil { + return fmt.Errorf("client connection raised an error: ", err) + } + + return nil +} diff --git a/host-panel.go b/host-panel.go index 443de68..48012c5 100644 --- a/host-panel.go +++ b/host-panel.go @@ -11,7 +11,9 @@ import ( "sync" "time" + "github.com/creativenucleus/bytejammer/comms" "github.com/creativenucleus/bytejammer/embed" + "github.com/creativenucleus/bytejammer/server" "github.com/google/uuid" "github.com/gorilla/websocket" "github.com/tyler-sommer/stick" @@ -26,7 +28,7 @@ const statusSendPeriod = 5 * time.Second type HostPanel struct { wsOperator *websocket.Conn wsMutex sync.Mutex - session *JamSession + session *server.Session chLog chan string statusTicker *time.Ticker } @@ -83,29 +85,30 @@ func (hp *HostPanel) webOperator(w http.ResponseWriter, r *http.Request) { func (hp *HostPanel) wsWebOperator() func(http.ResponseWriter, *http.Request) { return func(w http.ResponseWriter, r *http.Request) { var err error - hp.wsOperator, err = wsUpgrader.Upgrade(w, r, nil) - if err != nil { - log.Print("upgrade:", err) - return - } - defer func() { - hp.wsOperator.Close() - hp.wsOperator = nil - }() + comms.WsUpgrade(w, r, func(conn *websocket.Conn) error { + hp.wsOperator = conn + defer func() { hp.wsOperator = nil }() + + go hp.wsOperatorRead() + go hp.wsOperatorWrite() - go hp.wsOperatorRead() - go hp.wsOperatorWrite() + // #TODO: handle exit + for { + } - // #TODO: handle exit - for { + return nil + }) + if err != nil { + log.Print("upgrade:", err) + return } } } func (hp *HostPanel) wsOperatorRead() { for { - var msg Msg + var msg comms.Msg err := hp.wsOperator.ReadJSON(&msg) if err != nil { log.Println("read:", err) @@ -177,7 +180,7 @@ func (hp *HostPanel) webApiServer(w http.ResponseWriter, r *http.Request) { return } - hp.session, err = startJamSession(port, req.SessionName, hp.chLog) + hp.session, err = server.CreateSession(port, req.SessionName, hp.chLog) if err != nil { hp.chLog <- fmt.Sprintf("Server failed to launch: %s", err) apiOutErr(w, err, http.StatusInternalServerError) @@ -211,7 +214,7 @@ func (hp *HostPanel) webApiMachine(w http.ResponseWriter, r *http.Request) { switch req.Mode { case "unassigned": - _, err := hp.session.startMachine() + _, err := hp.session.StartMachine() if err != nil { apiOutErr(w, fmt.Errorf("TIC-80 Launch (unassigned): %w", err), http.StatusBadRequest) return @@ -226,7 +229,7 @@ func (hp *HostPanel) webApiMachine(w http.ResponseWriter, r *http.Request) { return } - _, err = hp.session.startMachineForConn(connUuid) + _, err = hp.session.StartMachineForConn(connUuid) if err != nil { apiOutErr(w, fmt.Errorf("TIC-80 Launch (jammer): %w", err), http.StatusBadRequest) return @@ -266,7 +269,7 @@ func (hp *HostPanel) handleStopServer() { return } - hp.session.stop() + hp.session.Stop() hp.sendServerStatus(true) } @@ -276,37 +279,37 @@ func (hp *HostPanel) handleIdentifyMachines() { return } - hp.session.identifyMachines() + hp.session.IdentifyMachines() hp.sendServerStatus(true) } -func (hp *HostPanel) handleConnectMachineClient(data DataConnectMachineClient) { +func (hp *HostPanel) handleConnectMachineClient(data comms.DataConnectMachineClient) { if hp.session == nil { hp.chLog <- "Requested connect, but no server is running" return } - hp.session.connectMachineClient(data) + hp.session.ConnectMachineClient(data) hp.sendServerStatus(true) } -func (hp *HostPanel) handleDisconnectMachineClient(data DataDisconnectMachineClient) { +func (hp *HostPanel) handleDisconnectMachineClient(data comms.DataDisconnectMachineClient) { if hp.session == nil { hp.chLog <- "Requested disconnect, but no server is running" return } - hp.session.disconnectMachineClient(data) + hp.session.DisconnectMachineClient(data) hp.sendServerStatus(true) } -func (hp *HostPanel) handleCloseMachine(data DataCloseMachine) { +func (hp *HostPanel) handleCloseMachine(data comms.DataCloseMachine) { if hp.session == nil { hp.chLog <- "Requested close machine, but no server is running" return } - hp.session.closeMachine(data) + hp.session.CloseMachine(data) hp.sendServerStatus(true) } @@ -321,15 +324,19 @@ func (hp *HostPanel) sendServerStatus(resetTicker bool) { hp.statusTicker.Reset(statusSendPeriod) } - status := hp.session.getStatus() - err := hp.sendData(&status) + msg := comms.MsgServerStatus{ + Type: "server-status", + Data: hp.session.GetStatus(), + } + + err := hp.sendData(&msg) if err != nil { log.Println("read:", err) } } func (hp *HostPanel) sendLog(message string) { - msg := Msg{Type: "log", Log: DataLog{Msg: message}} + msg := comms.Msg{Type: "log", Log: comms.DataLog{Msg: message}} fmt.Printf("-> HOST PANEL: %s\n", message) err := hp.sendData(&msg) diff --git a/jam-session-manager.go b/jam-session-manager.go deleted file mode 100644 index 1f28b9b..0000000 --- a/jam-session-manager.go +++ /dev/null @@ -1,113 +0,0 @@ -package main - -import ( - "errors" - - "github.com/creativenucleus/bytejammer/machines" - "github.com/google/uuid" -) - -type JamSessionManager struct { - machines map[uuid.UUID]*machines.Machine - conns map[uuid.UUID]*JamSessionConn - machineConnMap map[*machines.Machine]*JamSessionConn - - // #TODO: make this work... - // Is this the right level?? - // broadcaster *NusanLauncher -} - -func makeJamSessionManager() *JamSessionManager { - return &JamSessionManager{ - machines: make(map[uuid.UUID]*machines.Machine), - conns: make(map[uuid.UUID]*JamSessionConn), - machineConnMap: make(map[*machines.Machine]*JamSessionConn), - } -} - -// #TODO: Mutexes - -func (m *JamSessionManager) registerMachine(machine *machines.Machine) { - m.machines[machine.Uuid] = machine -} - -func (m *JamSessionManager) unregisterMachine(machine *machines.Machine) { - delete(m.machines, machine.Uuid) -} - -func (m *JamSessionManager) getMachine(uuid uuid.UUID) *machines.Machine { - machine, ok := m.machines[uuid] - if !ok { - return nil - } - return machine -} - -func (m *JamSessionManager) registerConn(conn *JamSessionConn) { - m.conns[conn.connUuid] = conn -} - -func (m *JamSessionManager) unregisterConn(conn *JamSessionConn) { - delete(m.conns, conn.connUuid) -} - -func (m *JamSessionManager) getConn(connUuid uuid.UUID) *JamSessionConn { - conn, ok := m.conns[connUuid] - if !ok { - return nil - } - return conn -} - -// You must register a machine and conn before linking them -func (m *JamSessionManager) linkMachineToConn(machineUuid uuid.UUID, connUuid uuid.UUID) error { - machine := m.getMachine(machineUuid) - if machine == nil { - return errors.New("machine not found") - } - - conn := m.getConn(connUuid) - if conn == nil { - return errors.New("conn not found") - } - - m.machineConnMap[machine] = conn - return nil -} - -// You must unlink a machine and conn before destroying either -func (m *JamSessionManager) unlinkMachineFromConn(machineUuid uuid.UUID, connUuid uuid.UUID) error { - machine := m.getMachine(machineUuid) - if machine == nil { - return errors.New("machine not found") - } - - conn := m.getConn(connUuid) - if conn == nil { - return errors.New("conn not found") - } - - if m.machineConnMap[machine] != conn { - return errors.New("machine does not link to expected conn") - } - - delete(m.machineConnMap, machine) - return nil -} - -func (m *JamSessionManager) getConnForMachine(machine *machines.Machine) *JamSessionConn { - conn, ok := m.machineConnMap[machine] - if !ok { - return nil - } - return conn -} - -func (m *JamSessionManager) getMachineForConn(conn *JamSessionConn) *machines.Machine { - for machine, c := range m.machineConnMap { - if c == conn { - return machine - } - } - return nil -} diff --git a/jukebox.go b/jukebox.go index 0a516eb..b66d658 100644 --- a/jukebox.go +++ b/jukebox.go @@ -5,6 +5,7 @@ import ( "log" "time" + "github.com/creativenucleus/bytejammer/comms" "github.com/creativenucleus/bytejammer/embed" "github.com/creativenucleus/bytejammer/machines" ) @@ -15,10 +16,10 @@ const ( type Jukebox struct { playlist *Playlist - comms *chan Msg + comms *chan comms.Msg } -func NewJukebox(playlist *Playlist, comms *chan Msg) (*Jukebox, error) { +func NewJukebox(playlist *Playlist, comms *chan comms.Msg) (*Jukebox, error) { log.Printf("-> Launching Jukebox for playlist") j := Jukebox{ @@ -38,7 +39,7 @@ func (j *Jukebox) start() { }) tsFirst.SetCode(code) - (*j.comms) <- Msg{Type: "tic-state", TicState: tsFirst} + (*j.comms) <- comms.Msg{Type: "tic-state", TicState: tsFirst} rotateTicker := time.NewTicker(rotatePeriod) defer rotateTicker.Stop() @@ -71,7 +72,7 @@ func (j *Jukebox) start() { */ ts := machines.MakeTicStateRunning(code) - (*j.comms) <- Msg{Type: "tic-state", TicState: ts} + (*j.comms) <- comms.Msg{Type: "tic-state", TicState: ts} } } }() diff --git a/local-jukebox.go b/local-jukebox.go index 7434f3a..d95a314 100644 --- a/local-jukebox.go +++ b/local-jukebox.go @@ -4,13 +4,14 @@ import ( "fmt" "log" + "github.com/creativenucleus/bytejammer/comms" "github.com/creativenucleus/bytejammer/machines" ) func startLocalJukebox(playlist *Playlist) error { fmt.Printf("Starting local jukebox containing %d items\n", len(playlist.items)) - ch := make(chan Msg) + ch := make(chan comms.Msg) j, err := NewJukebox(playlist, &ch) if err != nil { @@ -31,7 +32,7 @@ func startLocalJukebox(playlist *Playlist) error { if ok { switch msg.Type { case "tic-state": - err = m.Tic.WriteImportCode(msg.TicState) + err = m.Tic.WriteImportCode(msg.TicState.TicState) if err != nil { // #TODO: soften! log.Fatal(err) diff --git a/machines/machines.go b/machines/machines.go index 35de74f..51e2aab 100644 --- a/machines/machines.go +++ b/machines/machines.go @@ -5,7 +5,6 @@ import ( "fmt" "math/rand" - "github.com/creativenucleus/bytejammer/server" "github.com/google/uuid" ) @@ -33,7 +32,7 @@ func LaunchMachine(platform string, hasImport bool, hasExport bool, isServer boo m := Machine{ Platform: platform, Uuid: uuid.New(), - MachineName: server.GetFunName(len(MACHINES)), + MachineName: GetFunName(len(MACHINES)), } var err error diff --git a/server/names.go b/machines/names.go similarity index 93% rename from server/names.go rename to machines/names.go index ec7d639..ceee92c 100644 --- a/server/names.go +++ b/machines/names.go @@ -1,4 +1,4 @@ -package server +package machines import ( "math/rand" diff --git a/nusan.go b/nusan.go index cb6ede3..33adb59 100644 --- a/nusan.go +++ b/nusan.go @@ -7,6 +7,7 @@ import ( "sync" "time" + "github.com/creativenucleus/bytejammer/comms" "github.com/gorilla/websocket" ) @@ -43,20 +44,24 @@ func NusanLauncherConnect(port int) (*NusanLauncher, error) { func wsNusan(nl NusanLauncher) func(http.ResponseWriter, *http.Request) { return func(w http.ResponseWriter, r *http.Request) { var err error - nl.wsConn, err = wsUpgrader.Upgrade(w, r, nil) + + comms.WsUpgrade(w, r, func(conn *websocket.Conn) error { + nl.wsConn = conn + defer func() { nl.wsConn = nil }() + + go nl.nusanWsOperatorRead() + go nl.nusanWsOperatorWrite() + + // #TODO: handle exit + for { + } + + return nil + }) if err != nil { log.Print("ERR upgrade:", err) return } - // #TODO: Not great! - defer nl.wsConn.Close() - - go nl.nusanWsOperatorRead() - go nl.nusanWsOperatorWrite() - - // #TODO: handle exit - for { - } } } diff --git a/server/config.go b/server/config.go new file mode 100644 index 0000000..5f8cf04 --- /dev/null +++ b/server/config.go @@ -0,0 +1,18 @@ +package server + +import ( + "github.com/google/uuid" +) + +type ConfigMachines struct { + MachineUuid uuid.UUID `json:"machine_uuid,error"` + UserUuid *uuid.UUID `json:"user_uuid,omitempty"` +} + +type Config struct { + Machines []ConfigMachines `json:"machines"` +} + +func getConfig(m *SessionManager) Config { + panic("not implemented") +} diff --git a/jam-session-config.go b/server/session-config.go similarity index 56% rename from jam-session-config.go rename to server/session-config.go index 1cd235d..075b31d 100644 --- a/jam-session-config.go +++ b/server/session-config.go @@ -1,14 +1,14 @@ -package main +package server -type JamSessionConfig struct { +type SessionConfig struct { Port int Name string Slug string } // JamSessionConfig should be enough to save to disk and restart a JamSession if it crashes -func getJamSessionConfig(js JamSession) JamSessionConfig { - return JamSessionConfig{ +func getSessionConfig(js Session) SessionConfig { + return SessionConfig{ Port: js.port, Name: js.name, Slug: js.slug, diff --git a/jam-session-conn.go b/server/session-conn.go similarity index 78% rename from jam-session-conn.go rename to server/session-conn.go index 03f850e..83b33e1 100644 --- a/jam-session-conn.go +++ b/server/session-conn.go @@ -1,4 +1,4 @@ -package main +package server import ( "fmt" @@ -6,6 +6,7 @@ import ( "sync" "time" + "github.com/creativenucleus/bytejammer/comms" "github.com/creativenucleus/bytejammer/embed" "github.com/creativenucleus/bytejammer/machines" "github.com/creativenucleus/bytejammer/util" @@ -15,26 +16,26 @@ import ( // JamSessionConn is a connection to the JamServer. // It may not not yet be validated, or have an identity. -type JamSessionConn struct { +type SessionConn struct { conn *websocket.Conn connUuid uuid.UUID wsMutex sync.Mutex - identity *JamSessionConnIdentity + identity *SessionConnIdentity lastTicState *machines.TicState serverBasePath string publicKey []byte // This should be a public key type, and we should manage the challenge status signalKick chan bool } -type JamSessionConnIdentity struct { +type SessionConnIdentity struct { uuid uuid.UUID displayName string publicKey []byte isConfirmed bool } -func NewJamSessionConnection(conn *websocket.Conn) *JamSessionConn { - client := JamSessionConn{ +func NewJamSessionConnection(conn *websocket.Conn) *SessionConn { + client := SessionConn{ conn: conn, connUuid: uuid.New(), signalKick: make(chan bool), @@ -43,7 +44,7 @@ func NewJamSessionConnection(conn *websocket.Conn) *JamSessionConn { } // #TODO: make better -func (jc *JamSessionConn) getIdentityShortUuid() string { +func (jc *SessionConn) getIdentityShortUuid() string { if jc.identity == nil { return "(unknown)" } @@ -51,9 +52,9 @@ func (jc *JamSessionConn) getIdentityShortUuid() string { return jc.identity.uuid.String()[0:8] } -func (jc *JamSessionConn) runServerWsConnRead(js *JamSession) { +func (jc *SessionConn) runServerWsConnRead(js *Session) { for { - var msg Msg + var msg comms.Msg err := jc.conn.ReadJSON(&msg) if err != nil { js.chLog <- fmt.Sprintln("read:", err) @@ -69,7 +70,7 @@ func (jc *JamSessionConn) runServerWsConnRead(js *JamSession) { } // #TODO: NB This identity has not yet been challenged - jc.identity = &JamSessionConnIdentity{ + jc.identity = &SessionConnIdentity{ uuid: identityUuid, displayName: msg.Identity.DisplayName, publicKey: msg.Identity.PublicKey, @@ -89,7 +90,7 @@ func (jc *JamSessionConn) runServerWsConnRead(js *JamSession) { } // Send the challenge - msg := Msg{Type: "challenge-request", ChallengeRequest: DataChallengeRequest{Challenge: "This will be a random string!"}} + msg := comms.Msg{Type: "challenge-request", ChallengeRequest: comms.DataChallengeRequest{Challenge: "This will be a random string!"}} err = jc.sendData(msg) if err != nil { js.chLog <- fmt.Sprintln("write:", err) @@ -142,14 +143,14 @@ func (jc *JamSessionConn) runServerWsConnRead(js *JamSession) { } } -func (jc *JamSessionConn) runServerWsConnWrite(js *JamSession) { +func (jc *SessionConn) runServerWsConnWrite(js *Session) { for { select {} } } // TODO: Handle error -func (js *JamSessionConn) sendMachineNameCode(machineName string) error { +func (js *SessionConn) sendMachineNameCode(machineName string) error { fmt.Printf("CLIENT RESET: %d\n", js.connUuid) ts := machines.MakeTicStateRunning(embed.LuaClient) @@ -159,12 +160,12 @@ func (js *JamSessionConn) sendMachineNameCode(machineName string) error { }) ts.SetCode(code) - msg := Msg{Type: "tic-state", TicState: ts} + msg := comms.Msg{Type: "tic-state", TicState: ts} err := js.sendData(msg) return err } -func (jc *JamSessionConn) sendData(data interface{}) error { +func (jc *SessionConn) sendData(data interface{}) error { jc.wsMutex.Lock() defer jc.wsMutex.Unlock() return jc.conn.WriteJSON(data) diff --git a/server/session-manager.go b/server/session-manager.go new file mode 100644 index 0000000..f404764 --- /dev/null +++ b/server/session-manager.go @@ -0,0 +1,113 @@ +package server + +import ( + "errors" + + "github.com/creativenucleus/bytejammer/machines" + "github.com/google/uuid" +) + +type SessionManager struct { + machines map[uuid.UUID]*machines.Machine + conns map[uuid.UUID]*SessionConn + machineConnMap map[*machines.Machine]*SessionConn + + // #TODO: make this work... + // Is this the right level?? + // broadcaster *NusanLauncher +} + +func makeSessionManager() *SessionManager { + return &SessionManager{ + machines: make(map[uuid.UUID]*machines.Machine), + conns: make(map[uuid.UUID]*SessionConn), + machineConnMap: make(map[*machines.Machine]*SessionConn), + } +} + +// #TODO: Mutexes + +func (sm *SessionManager) registerMachine(machine *machines.Machine) { + sm.machines[machine.Uuid] = machine +} + +func (sm *SessionManager) unregisterMachine(machine *machines.Machine) { + delete(sm.machines, machine.Uuid) +} + +func (sm *SessionManager) getMachine(uuid uuid.UUID) *machines.Machine { + machine, ok := sm.machines[uuid] + if !ok { + return nil + } + return machine +} + +func (sm *SessionManager) registerConn(conn *SessionConn) { + sm.conns[conn.connUuid] = conn +} + +func (sm *SessionManager) unregisterConn(conn *SessionConn) { + delete(sm.conns, conn.connUuid) +} + +func (sm *SessionManager) getConn(connUuid uuid.UUID) *SessionConn { + conn, ok := sm.conns[connUuid] + if !ok { + return nil + } + return conn +} + +// You must register a machine and conn before linking them +func (sm *SessionManager) linkMachineToConn(machineUuid uuid.UUID, connUuid uuid.UUID) error { + machine := sm.getMachine(machineUuid) + if machine == nil { + return errors.New("machine not found") + } + + conn := sm.getConn(connUuid) + if conn == nil { + return errors.New("conn not found") + } + + sm.machineConnMap[machine] = conn + return nil +} + +// You must unlink a machine and conn before destroying either +func (sm *SessionManager) unlinkMachineFromConn(machineUuid uuid.UUID, connUuid uuid.UUID) error { + machine := sm.getMachine(machineUuid) + if machine == nil { + return errors.New("machine not found") + } + + conn := sm.getConn(connUuid) + if conn == nil { + return errors.New("conn not found") + } + + if sm.machineConnMap[machine] != conn { + return errors.New("machine does not link to expected conn") + } + + delete(sm.machineConnMap, machine) + return nil +} + +func (sm *SessionManager) getConnForMachine(machine *machines.Machine) *SessionConn { + conn, ok := sm.machineConnMap[machine] + if !ok { + return nil + } + return conn +} + +func (sm *SessionManager) getMachineForConn(conn *SessionConn) *machines.Machine { + for machine, c := range sm.machineConnMap { + if c == conn { + return machine + } + } + return nil +} diff --git a/jam-session.go b/server/session.go similarity index 68% rename from jam-session.go rename to server/session.go index 769ccb8..7970379 100644 --- a/jam-session.go +++ b/server/session.go @@ -1,4 +1,4 @@ -package main +package server import ( "encoding/json" @@ -8,13 +8,15 @@ import ( "os" "time" + "github.com/creativenucleus/bytejammer/comms" "github.com/creativenucleus/bytejammer/config" "github.com/creativenucleus/bytejammer/machines" "github.com/creativenucleus/bytejammer/util" "github.com/google/uuid" + "github.com/gorilla/websocket" ) -type JamSession struct { +type Session struct { port int // Our friendly name... name string @@ -23,24 +25,24 @@ type JamSession struct { startTime time.Time // The connections, machines, and the connections between them - manager *JamSessionManager + manager *SessionManager chLog chan string } -func startJamSession(port int, name string, chLog chan string) (*JamSession, error) { +func CreateSession(port int, name string, chLog chan string) (*Session, error) { nameSlug := util.GetSlug(name) if nameSlug == "" { return nil, errors.New("Invalid session name - unable to make slug") } now := time.Now() - js := JamSession{ + js := Session{ port: port, name: name, slug: fmt.Sprintf("%s_%s", nameSlug, util.GetSlugFromTime(now)), startTime: now, - manager: makeJamSessionManager(), + manager: makeSessionManager(), chLog: chLog, } @@ -51,7 +53,7 @@ func startJamSession(port int, name string, chLog chan string) (*JamSession, err return nil, err } - config := getJamSessionConfig(js) + config := getSessionConfig(js) configData, err := json.Marshal(config) if err != nil { return nil, err @@ -70,7 +72,7 @@ func startJamSession(port int, name string, chLog chan string) (*JamSession, err return &js, nil } -func (js *JamSession) start() error { +func (js *Session) start() error { js.chLog <- fmt.Sprintf("Starting server on port %d", js.port) webServer := &http.Server{ @@ -86,74 +88,92 @@ func (js *JamSession) start() error { return nil } -func (js *JamSession) wsBytejam() func(http.ResponseWriter, *http.Request) { +func (js *Session) wsBytejam() func(http.ResponseWriter, *http.Request) { return func(w http.ResponseWriter, r *http.Request) { js.chLog <- fmt.Sprintf("Client connected") - conn, err := wsUpgrader.Upgrade(w, r, nil) - if err != nil { - js.chLog <- fmt.Sprintf("Client connected but couldn't upgrade: %s", err) - return - } - defer conn.Close() - - /* - var m *machines.Machine - if s.broadcaster != nil { - tic, err = machines.NewNusanServerTic(slug, broadcaster) - if err != nil { - log.Print("ERR new TIC:", err) - return - } - - log.Print("not implemented") - return - } else { - m, err = machines.LaunchMachine("TIC-80", true, false, true) - if err != nil { - log.Print("ERR new TIC:", err) + err := comms.WsUpgrade(w, r, func(conn *websocket.Conn) error { + /* + var m *machines.Machine + if s.broadcaster != nil { + tic, err = machines.NewNusanServerTic(slug, broadcaster) + if err != nil { + log.Print("ERR new TIC:", err) + return + } + + log.Print("not implemented") return + } else { + m, err = machines.LaunchMachine("TIC-80", true, false, true) + if err != nil { + log.Print("ERR new TIC:", err) + return + } + + s.chLog <- "TIC-80 Launched" } + defer m.Shutdown() + */ - s.chLog <- "TIC-80 Launched" - } - defer m.Shutdown() - */ - - jsConn := NewJamSessionConnection(conn) - js.manager.registerConn(jsConn) + jsConn := NewJamSessionConnection(conn) + js.manager.registerConn(jsConn) - go jsConn.runServerWsConnRead(js) - go jsConn.runServerWsConnWrite(js) + go jsConn.runServerWsConnRead(js) + go jsConn.runServerWsConnWrite(js) - // #TODO: send the server status - // hp.sendServerStatus(true) + // #TODO: send the server status + // hp.sendServerStatus(true) - // #TODO: handle exit - for { - select { - case <-jsConn.signalKick: - // #TODO: Close down read and write channels - fmt.Println("KICKED") - return + // #TODO: handle exit + for { + select { + case <-jsConn.signalKick: + // #TODO: Close down read and write channels + fmt.Println("KICKED") + return nil + } } + }) + if err != nil { + js.chLog <- fmt.Sprintf("ws-upgrade: %w", err) + return } } } -func (js *JamSession) stop() error { +func (js *Session) Stop() error { fmt.Println("JamSession->stop not yet implemented") return nil } -func (js *JamSession) getBasePath() string { +func (js *Session) getBasePath() string { return fmt.Sprintf("%sserver-data/%s", config.WORK_DIR, js.slug) } -func (js *JamSession) getStatus() MsgServerStatus { - msg := MsgServerStatus{ - Type: "server-status", +type SessionStatus struct { + Clients []struct { + Uuid string + DisplayName string + ShortUuid string + Status string + MachineUuid string + LastPingTime string } + Machines []struct { + Uuid string + MachineName string + ProcessID int + Platform string + Status string + ClientUuid string + JammerDisplayName string + LastSnapshotTime string + } +} + +func (js *Session) GetStatus() SessionStatus { + ss := SessionStatus{} for _, jc := range js.manager.conns { status := "waiting" @@ -164,7 +184,7 @@ func (js *JamSession) getStatus() MsgServerStatus { machineUuid = machine.Uuid.String() } - msg.Data.Clients = append(msg.Data.Clients, struct { + ss.Clients = append(ss.Clients, struct { Uuid string DisplayName string ShortUuid string @@ -190,7 +210,7 @@ func (js *JamSession) getStatus() MsgServerStatus { clientUuid = client.connUuid.String() } - msg.Data.Machines = append(msg.Data.Machines, struct { + ss.Machines = append(ss.Machines, struct { Uuid string MachineName string ProcessID int @@ -211,10 +231,10 @@ func (js *JamSession) getStatus() MsgServerStatus { }) } - return msg + return ss } -func (js *JamSession) startMachine() (*machines.Machine, error) { +func (js *Session) StartMachine() (*machines.Machine, error) { m, err := machines.LaunchMachine("TIC-80", true, true, false) if err != nil { return nil, err @@ -226,7 +246,7 @@ func (js *JamSession) startMachine() (*machines.Machine, error) { return m, err } -func (js *JamSession) startMachineForConn(connUuid uuid.UUID) (*machines.Machine, error) { +func (js *Session) StartMachineForConn(connUuid uuid.UUID) (*machines.Machine, error) { conn := js.manager.getConn(connUuid) if conn == nil { return nil, errors.New("Unable to find conn") @@ -245,7 +265,7 @@ func (js *JamSession) startMachineForConn(connUuid uuid.UUID) (*machines.Machine return m, err } -func (js *JamSession) identifyMachines() { +func (js *Session) IdentifyMachines() { count := 0 for _, c := range js.manager.conns { m := js.manager.getMachineForConn(c) @@ -261,7 +281,7 @@ func (js *JamSession) identifyMachines() { js.chLog <- fmt.Sprintf("Identification sent to %d machines for 30 seconds", count) } -func (js *JamSession) closeMachine(data DataCloseMachine) { +func (js *Session) CloseMachine(data comms.DataCloseMachine) { // #TODO: unlink and unregister! fmt.Printf("CLOSE: %s\n", data.Uuid) @@ -274,7 +294,7 @@ func (js *JamSession) closeMachine(data DataCloseMachine) { js.chLog <- fmt.Sprintf("Machine %s closed", data.Uuid) } -func (js *JamSession) connectMachineClient(data DataConnectMachineClient) { +func (js *Session) ConnectMachineClient(data comms.DataConnectMachineClient) { fmt.Printf("connect: %s to %s\n", data.ClientUuid, data.MachineUuid) machineUuid, err := uuid.Parse(data.MachineUuid) @@ -306,7 +326,7 @@ func (js *JamSession) connectMachineClient(data DataConnectMachineClient) { js.chLog <- fmt.Sprintf("Connected %s to %s", data.ClientUuid, data.MachineUuid) } -func (js *JamSession) disconnectMachineClient(data DataDisconnectMachineClient) { +func (js *Session) DisconnectMachineClient(data comms.DataDisconnectMachineClient) { fmt.Printf("Disconnect: %s to %s\n", data.ClientUuid, data.MachineUuid) machineUuid, err := uuid.Parse(data.MachineUuid) diff --git a/websocket-client.go b/websocket-client.go index 3873c0b..df54ebe 100644 --- a/websocket-client.go +++ b/websocket-client.go @@ -10,13 +10,6 @@ import ( "github.com/gorilla/websocket" ) -var ( - wsUpgrader = websocket.Upgrader{ - ReadBufferSize: 1024, - WriteBufferSize: 1024, - } -) - // #TODO /* const ( From 7b2fedea0eecca24745ecf04c2674e7501f55db4 Mon Sep 17 00:00:00 2001 From: James Rutherford / creativenucleus Date: Fri, 3 Nov 2023 16:11:20 +0000 Subject: [PATCH 02/11] Merge - broken: import cycle --- client-ws.go | 26 ++++++++--- crypto.go => crypto/crypto.go | 63 ++++++++++++++++---------- crypto/crypto_test.go | 32 +++++++++++++ go.mod | 5 +++ go.sum | 18 ++++++++ identity.go | 9 ++-- server/identity.go | 85 +++++++++++++++++++++++++++++++++++ server/session.go | 2 +- util/util.go | 9 +++- 9 files changed, 214 insertions(+), 35 deletions(-) rename crypto.go => crypto/crypto.go (53%) create mode 100644 crypto/crypto_test.go create mode 100644 server/identity.go diff --git a/client-ws.go b/client-ws.go index 38fc2d9..25488a8 100644 --- a/client-ws.go +++ b/client-ws.go @@ -1,6 +1,7 @@ package main import ( + "encoding/hex" "fmt" "log" "os" @@ -56,7 +57,7 @@ func startClientServerConn(host string, port int, identity *Identity, chServerSt defer m.Shutdown() // #TODO: shift import / export to *Machine? - go cws.clientWsReader(m.Tic) + go cws.clientWsReader(m.Tic, identity) go cws.clientWsWriter(m.Tic, identity) // Lock #TODO: use a channel to escape @@ -73,7 +74,7 @@ func clientOpenConnection(host string, port int) (*WebSocketLink, error) { return ws, nil } -func (cws *ClientWS) clientWsReader(tic *machines.Tic) error { +func (cws *ClientWS) clientWsReader(tic *machines.Tic, identity *Identity) error { for { var msg comms.Msg err := cws.ws.conn.ReadJSON(&msg) @@ -89,7 +90,7 @@ func (cws *ClientWS) clientWsReader(tic *machines.Tic) error { switch msg.Type { case "challenge-request": - cws.handleChallengeRequest(msg.ChallengeRequest.Challenge) + cws.handleChallengeRequest(msg.ChallengeRequest.Challenge, identity) case "tic-state": tic.WriteImportCode(msg.TicState.State) @@ -97,14 +98,27 @@ func (cws *ClientWS) clientWsReader(tic *machines.Tic) error { } } -func (cws *ClientWS) handleChallengeRequest(challenge string) { - cws.chMsg <- comms.Msg{Type: "challenge-response", ChallengeResponse: comms.DataChallengeResponse{Challenge: challenge + " ~ response"}} +func (cws *ClientWS) handleChallengeRequest(challenge string, identity *Identity) error { + data, err := hex.DecodeString(challenge) + if err != nil { + return err + } + + fmt.Printf("%x", data) + signed, err := identity.Crypto.Sign(data) + if err != nil { + return err + } + + cws.chMsg <- comms.Msg{Type: "challenge-response", ChallengeResponse: comms.DataChallengeResponse{Challenge: fmt.Sprintf("%x", signed)}} + + return nil } // #TODO: fatalErr func (cws *ClientWS) clientWsWriter(tic *machines.Tic, identity *Identity) { // Send Identity... - publicKeyRaw, err := identity.Crypto.publicKeyToRaw() + publicKeyRaw, err := identity.Crypto.PublicKeyToPem() if err != nil { log.Fatal(err) } diff --git a/crypto.go b/crypto/crypto.go similarity index 53% rename from crypto.go rename to crypto/crypto.go index 3da7727..926b170 100644 --- a/crypto.go +++ b/crypto/crypto.go @@ -1,4 +1,4 @@ -package main +package crypto import ( "crypto" @@ -9,19 +9,24 @@ import ( "encoding/json" "encoding/pem" "errors" + "fmt" ) type CryptoPrivate struct { privateKey *rsa.PrivateKey } +type CryptoPublic struct { + publicKey *rsa.PublicKey +} + // https://betterprogramming.pub/exploring-cryptography-in-go-signing-vs-encryption-f19534334ad // Returns public key, private key, error -func newCryptoPrivate() (*CryptoPrivate, error) { +func NewCryptoPrivate() (*CryptoPrivate, error) { c := CryptoPrivate{} var err error - c.privateKey, err = rsa.GenerateKey(rand.Reader, 2048) + c.privateKey, err = rsa.GenerateKey(rand.Reader, 4096) if err != nil { return nil, err } @@ -29,19 +34,19 @@ func newCryptoPrivate() (*CryptoPrivate, error) { return &c, nil } -func newCryptoPrivateFromString(keyRaw []byte) (*CryptoPrivate, error) { +func newCryptoPrivateFromString(privKey string) (*CryptoPrivate, error) { c := CryptoPrivate{} var err error - c.privateKey, err = x509.ParsePKCS1PrivateKey(keyRaw) + p, _ := pem.Decode([]byte(privKey)) + c.privateKey, err = x509.ParsePKCS1PrivateKey(p.Bytes) if err != nil { return nil, err } - return &c, nil } -func (c CryptoPrivate) privateKeyToRaw() []byte { +func (c CryptoPrivate) privateKeyToPem() []byte { data := x509.MarshalPKCS1PrivateKey(c.privateKey) privBlock := pem.Block{ @@ -53,20 +58,20 @@ func (c CryptoPrivate) privateKeyToRaw() []byte { return pem.EncodeToMemory(&privBlock) } -func (c CryptoPrivate) publicKeyToRaw() ([]byte, error) { - data, err := x509.MarshalPKIXPublicKey(&c.privateKey.PublicKey) +func (c CryptoPrivate) PublicKeyToPem() ([]byte, error) { + data, err := x509.MarshalPKIXPublicKey(c.privateKey.Public()) if err != nil { return nil, err } - pem := pem.EncodeToMemory( + pemData := pem.EncodeToMemory( &pem.Block{ Type: "RSA PUBLIC KEY", Bytes: data, }, ) - return pem, nil + return pemData, nil } func (c *CryptoPrivate) MarshalJSON() ([]byte, error) { @@ -74,7 +79,7 @@ func (c *CryptoPrivate) MarshalJSON() ([]byte, error) { return json.Marshal(&struct { PrivateKey []byte `json:"privateKey"` }{ - PrivateKey: c.privateKeyToRaw(), + PrivateKey: c.privateKeyToPem(), }) } @@ -107,26 +112,38 @@ func hashData(data []byte) []byte { } // Make a hash of the data, then sign it -func (c CryptoPrivate) sign(data []byte) ([]byte, error) { - return rsa.SignPKCS1v15(rand.Reader, c.privateKey, crypto.SHA256, hashData(data)) -} - -type CryptoPublic struct { - publicKey *rsa.PublicKey +func (c CryptoPrivate) Sign(data []byte) ([]byte, error) { + return rsa.SignPSS(rand.Reader, c.privateKey, crypto.SHA256, hashData(data), nil) } -func newCryptoPublicFromString(keyString string) (*CryptoPublic, error) { +func NewCryptoPublicFromPem(pemData []byte) (*CryptoPublic, error) { c := CryptoPublic{} - var err error - c.publicKey, err = x509.ParsePKCS1PublicKey([]byte(keyString)) + block, _ := pem.Decode(pemData) + if block == nil { + return nil, fmt.Errorf("bad key data: %s", "not PEM-encoded") + } + + if block.Type != "RSA PUBLIC KEY" { + return nil, fmt.Errorf("type was not public key: %s", block.Type) + } + + pubKey, err := x509.ParsePKIXPublicKey(block.Bytes) if err != nil { return nil, err } + switch pubKey.(type) { + case *rsa.PublicKey: + c.publicKey = pubKey.(*rsa.PublicKey) + default: + return nil, fmt.Errorf("type was not known public key type") + } + return &c, nil } -func (c CryptoPublic) verifySigned(data []byte, signature []byte) (bool, error) { - return rsa.VerifyPKCS1v15(c.publicKey, crypto.SHA256, hashData(data), signature) == nil, nil +func (c CryptoPublic) VerifySigned(data []byte, signature []byte) bool { + err := rsa.VerifyPSS(c.publicKey, crypto.SHA256, hashData(data), signature, nil) + return err == nil } diff --git a/crypto/crypto_test.go b/crypto/crypto_test.go new file mode 100644 index 0000000..e4d9998 --- /dev/null +++ b/crypto/crypto_test.go @@ -0,0 +1,32 @@ +package crypto + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestSigning(t *testing.T) { + cryptoPrv, err := NewCryptoPrivate() + require.NoError(t, err, "Failed to create cryptoPrivate") + // require(cryptoPrv != nil, "Private key is nil") + // require(cryptoPrv.privateKey != nil, "Private key is nil") + + publicKeyPem, err := cryptoPrv.PublicKeyToPem() + require.NoError(t, err, "Failed to get public key in PEM format") + // require(publicKey == nil, "Failed to create public key") + + cryptoPub, err := NewCryptoPublicFromPem(publicKeyPem) + require.NoError(t, err, "Failed to create public key from PEM") + //require(cryptoPub == nil, "Failed to create public key") + + challenge := "Some challeng to be signed then verified" + challengeBytes := []byte(challenge) + + signed, err := cryptoPrv.Sign(challengeBytes) + require.NoError(t, err, "Failed to sign") + require.NotEmpty(t, signed, "Empty signed value") + + isVerified := cryptoPub.VerifySigned(challengeBytes, signed) + require.Equal(t, true, isVerified, "Signature did not verify") +} diff --git a/go.mod b/go.mod index 2271ab2..f8422cb 100644 --- a/go.mod +++ b/go.mod @@ -13,8 +13,13 @@ require ( require ( github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect github.com/gosimple/unidecode v1.0.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/shopspring/decimal v1.3.1 // indirect + github.com/stretchr/objx v0.5.0 // indirect + github.com/stretchr/testify v1.8.4 // indirect github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 6c770f0..5e19ef1 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,8 @@ github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w= github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/google/uuid v1.3.1 h1:KjJaJ9iWZ3jOFZIf1Lqf4laDRCasjl0BCmnEGxkdLb4= github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= @@ -8,11 +11,22 @@ github.com/gosimple/slug v1.13.1 h1:bQ+kpX9Qa6tHRaK+fZR0A0M2Kd7Pa5eHPPsb1JpHD+Q= github.com/gosimple/slug v1.13.1/go.mod h1:UiRaFH+GEilHstLUmcBgWcI42viBN7mAb818JrYOeFQ= github.com/gosimple/unidecode v1.0.1 h1:hZzFTMMqSswvf0LBJZCZgThIZrpDHFXux9KeGmn6T/o= github.com/gosimple/unidecode v1.0.1/go.mod h1:CP0Cr1Y1kogOtx0bJblKzsVWrqYaqfNOnHzpgWw4Awc= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4= github.com/shopspring/decimal v1.3.1 h1:2Usl1nmF/WZucqkFZhnfFYxxxu8LG21F6nPQBE5gKV8= github.com/shopspring/decimal v1.3.1/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.1/go.mod h1:/iHQpkQwBD6DLUmQ4pE+s1TXdob1mORJ4/UFdrifcy0= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/tyler-sommer/stick v1.0.6 h1:LLdJ8oGotXCsAuVx2BRZZT6s3bYXHe0ImQ+azF4HtJg= github.com/tyler-sommer/stick v1.0.6/go.mod h1:rjBy3zi6GwoxExa6OSRPPPaLqUEKNsBxTeWckhIX1us= github.com/urfave/cli/v2 v2.25.7 h1:VAzn5oq403l5pHjc4OhD54+XGO9cdKVL/7lDjF+iKUs= @@ -21,3 +35,7 @@ github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRT github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8= golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM= golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/identity.go b/identity.go index c978097..9610ad7 100644 --- a/identity.go +++ b/identity.go @@ -7,14 +7,15 @@ import ( "path/filepath" "github.com/creativenucleus/bytejammer/config" + "github.com/creativenucleus/bytejammer/crypto" "github.com/creativenucleus/bytejammer/util" "github.com/google/uuid" ) type Identity struct { - Uuid uuid.UUID `json:"uuid"` - DisplayName string `json:"displayName"` - Crypto *CryptoPrivate `json:"crypto"` + Uuid uuid.UUID `json:"uuid"` + DisplayName string `json:"displayName"` + Crypto *crypto.CryptoPrivate `json:"crypto"` } // For storage?: hex.EncodeString, hex.DecodeString @@ -26,7 +27,7 @@ func makeIdentity(displayName string) error { return err } - c, err := newCryptoPrivate() + c, err := crypto.NewCryptoPrivate() if err != nil { return err } diff --git a/server/identity.go b/server/identity.go new file mode 100644 index 0000000..1b23469 --- /dev/null +++ b/server/identity.go @@ -0,0 +1,85 @@ +package server + +import ( + "encoding/json" + "fmt" + "os" + "path/filepath" + "time" + + "github.com/creativenucleus/bytejammer/config" + "github.com/creativenucleus/bytejammer/crypto" + "github.com/google/uuid" +) + +type JammerIdentity struct { + Uuid uuid.UUID `json:"uuid"` + DisplayName string `json:"displayName"` + CryptoPublic *crypto.CryptoPublic `json:"cryptoPublic"` + CreatedAt time.Time `json:"createdAt"` +} + +type Identities struct { + identities map[string]JammerIdentity +} + +// Loads in the known identities from disk... +func NewIdentities() (*Identities, error) { + i := Identities{ + identities: make(map[string]JammerIdentity), + } + + filematches, err := filepath.Glob(fmt.Sprintf("%sserver-data/identity/identity-*.json", config.WORK_DIR)) + if err != nil { + return nil, err + } + + for _, filematch := range filematches { + identity, err := readIdentityFile(filematch) + if err != nil { + return nil, err + } + + // #TODO: This is a bit hacky + strlen := len(filematch) + key := filematch[strlen-41 : strlen-5] + i.identities[key] = *identity + } + + return &i, nil +} + +func (i *Identities) getIdentityById(id string) *JammerIdentity { + identity, ok := i.identities[id] + if !ok { + return nil + } + + return &identity +} + +func (i *Identities) addIdentity(identity JammerIdentity) error { + data, err := json.Marshal(identity) + if err != nil { + return err + } + + filepath := filepath.Clean(fmt.Sprintf("%sserver-data/identity/identity-%s.json", config.WORK_DIR, identity.Uuid.String())) + + return os.WriteFile(filepath, data, 0644) +} + +func readIdentityFile(filepath string) (*JammerIdentity, error) { + data, err := os.ReadFile(filepath) + if err != nil { + return nil, err + } + + var identity JammerIdentity + err = json.Unmarshal(data, &identity) + if err != nil { + return nil, err + } + + return &identity, nil +} diff --git a/server/session.go b/server/session.go index 7970379..5e87774 100644 --- a/server/session.go +++ b/server/session.go @@ -40,7 +40,7 @@ func CreateSession(port int, name string, chLog chan string) (*Session, error) { js := Session{ port: port, name: name, - slug: fmt.Sprintf("%s_%s", nameSlug, util.GetSlugFromTime(now)), + slug: fmt.Sprintf("%s_%s", util.GetSlugFromTime(now), nameSlug), startTime: now, manager: makeSessionManager(), chLog: chLog, diff --git a/util/util.go b/util/util.go index c54b5b2..c1c2341 100644 --- a/util/util.go +++ b/util/util.go @@ -1,6 +1,7 @@ package util import ( + "crypto/rand" "errors" "fmt" "io/fs" @@ -38,5 +39,11 @@ func GetSlug(in string) string { } func GetSlugFromTime(t time.Time) string { - return fmt.Sprintf(t.Format("2006-01-02_15-04-05")) + return fmt.Sprintf(t.Format("20060102_1504")) +} + +func GetRandomBytes(length int) []byte { + b := make([]byte, length) + rand.Read(b) + return b } From 0d2fe5345b936fe617f3412c41c2fc5f844d523a Mon Sep 17 00:00:00 2001 From: James / creativenucleus Date: Sun, 5 Nov 2023 23:08:00 +0000 Subject: [PATCH 03/11] Comms -> getting back to fixed! --- client-panel.go | 12 ++-- client-ws.go | 10 +-- comms/msg.go | 39 ++++++++---- comms/websocket.go | 4 +- embed/web/client/index.html | 2 +- embed/web/server/operator.html | 5 +- go.mod | 1 - host-panel.go | 6 +- jukebox.go | 8 ++- local-jukebox.go | 2 +- server/config.go | 18 ------ server/session-config.go | 43 ++++++++++--- server/session-conn.go | 12 ++-- server/session-manager.go | 113 --------------------------------- server/session.go | 105 +++++++++++++++--------------- server/switchboard.go | 113 +++++++++++++++++++++++++++++++++ 16 files changed, 261 insertions(+), 232 deletions(-) delete mode 100644 server/config.go delete mode 100644 server/session-manager.go create mode 100644 server/switchboard.go diff --git a/client-panel.go b/client-panel.go index 34c0ad5..f78f573 100644 --- a/client-panel.go +++ b/client-panel.go @@ -28,7 +28,7 @@ const ( type ClientPanel struct { // #TODO: lock down to receiver only - chSendServerStatus chan comms.DataClientServerStatus + chSendClientStatus chan comms.DataClientStatus wsClient *websocket.Conn wsMutex sync.Mutex } @@ -51,7 +51,7 @@ func startClientPanel(port int) error { fmt.Printf("In a web browser, go to http://localhost:%d/%s\n", port, session) cp := ClientPanel{ - chSendServerStatus: make(chan comms.DataClientServerStatus), + chSendClientStatus: make(chan comms.DataClientStatus), } http.HandleFunc(fmt.Sprintf("/%s", session), cp.webClientIndex) http.HandleFunc(fmt.Sprintf("/%s/api/identity.json", session), cp.webClientApiIdentityJSON) @@ -113,7 +113,7 @@ func (cp *ClientPanel) webClientApiIdentityJSON(w http.ResponseWriter, r *http.R func (cp *ClientPanel) webClientApiJoinServerJSON(w http.ResponseWriter, r *http.Request) { switch r.Method { case "POST": - cp.chSendServerStatus <- comms.DataClientServerStatus{IsConnected: false} + cp.chSendClientStatus <- comms.DataClientStatus{IsConnected: false} // #TODO: Cleaner way to do this? type reqType struct { @@ -142,7 +142,7 @@ func (cp *ClientPanel) webClientApiJoinServerJSON(w http.ResponseWriter, r *http return } - err = startClientServerConn(req.Host, port, identity, cp.chSendServerStatus) + err = startClientServerConn(req.Host, port, identity, cp.chSendClientStatus) if err != nil { apiOutErr(w, err, http.StatusInternalServerError) return @@ -208,8 +208,8 @@ func (cp *ClientPanel) wsWrite() { // case <-statusTicker.C: // fmt.Println("TICKER!") - case status := <-cp.chSendServerStatus: - msg := comms.Msg{Type: "server-status", ServerStatus: status} + case status := <-cp.chSendClientStatus: + msg := comms.Msg{Type: "client-status", ClientStatus: status} err := cp.sendData(&msg) if err != nil { // #TODO: relax diff --git a/client-ws.go b/client-ws.go index 25488a8..6969968 100644 --- a/client-ws.go +++ b/client-ws.go @@ -20,8 +20,8 @@ type ClientWS struct { basepath string } -func startClientServerConn(host string, port int, identity *Identity, chServerStatus chan comms.DataClientServerStatus) error { - chServerStatus <- comms.DataClientServerStatus{IsConnected: false} +func startClientServerConn(host string, port int, identity *Identity, chServerStatus chan comms.DataClientStatus) error { + chServerStatus <- comms.DataClientStatus{IsConnected: false} cws := ClientWS{ chMsg: make(chan comms.Msg), } @@ -48,7 +48,7 @@ func startClientServerConn(host string, port int, identity *Identity, chServerSt break } defer cws.ws.Close() - chServerStatus <- comms.DataClientServerStatus{IsConnected: true} + chServerStatus <- comms.DataClientStatus{IsConnected: true} m, err := machines.LaunchMachine("TIC-80", true, true, false) if err != nil { @@ -171,7 +171,9 @@ func (cws *ClientWS) clientWsWriter(tic *machines.Tic, identity *Identity) { } // #TODO: line endings for data? UTF-8? - msg := comms.Msg{Type: "tic-state", TicState: *ticState} + msg := comms.Msg{Type: "tic-state", TicState: comms.DataTicState{ + State: *ticState, + }} err = cws.ws.sendData(msg) if err != nil { log.Fatal(err) diff --git a/comms/msg.go b/comms/msg.go index 81fbfec..55624ef 100644 --- a/comms/msg.go +++ b/comms/msg.go @@ -2,21 +2,13 @@ package comms import ( "github.com/creativenucleus/bytejammer/machines" - "github.com/creativenucleus/bytejammer/server" ) type DataLog struct { Msg string } -type MsgTicState struct { - Code []byte - IsRunning bool - CursorX int - CursorY int -} - -type DataClientServerStatus struct { +type DataClientStatus struct { IsConnected bool } @@ -26,7 +18,9 @@ type DataIdentity struct { PublicKey []byte `json:"publicKey"` } -type DataTicState machines.TicState +type DataTicState struct { + State machines.TicState +} type DataCloseMachine struct { Uuid string `json:"uuid"` @@ -50,16 +44,33 @@ type DataChallengeResponse struct { Challenge string `json:"challenge"` } -type MsgServerStatus struct { - Type string `json:"type"` - Data server.SessionStatus `json:"data"` +type DataSessionStatus struct { + Clients []struct { + Uuid string + DisplayName string + ShortUuid string + Status string + MachineUuid string + LastPingTime string + } + Machines []struct { + Uuid string + MachineName string + ProcessID int + Platform string + Status string + ClientUuid string + JammerDisplayName string + LastSnapshotTime string + } } type Msg struct { Type string `json:"type"` Identity DataIdentity `json:"identity,omitempty"` TicState DataTicState `json:"tic-state,omitempty"` - ServerStatus DataClientServerStatus `json:"server-status,omitempty"` + ClientStatus DataClientStatus `json:"client-status,omitempty"` + SessionStatus DataSessionStatus `json:"session-status,omitempty"` Log DataLog `json:"log,omitempty"` ConnectMachineClient DataConnectMachineClient `json:"connect-machine-client,omitempty"` DisconnectMachineClient DataDisconnectMachineClient `json:"disconnect-machine-client,omitempty"` diff --git a/comms/websocket.go b/comms/websocket.go index 9aa9b6a..af70b87 100644 --- a/comms/websocket.go +++ b/comms/websocket.go @@ -19,13 +19,13 @@ type FnWsConn func(*websocket.Conn) error func WsUpgrade(w http.ResponseWriter, r *http.Request, fn FnWsConn) error { conn, err := WS_UPGRADER.Upgrade(w, r, nil) if err != nil { - return fmt.Errorf("client couldn't upgrade: ", err) + return fmt.Errorf("client couldn't upgrade: %w", err) } defer conn.Close() err = fn(conn) if err != nil { - return fmt.Errorf("client connection raised an error: ", err) + return fmt.Errorf("client connection raised an error: %w", err) } return nil diff --git a/embed/web/client/index.html b/embed/web/client/index.html index 50f4b41..67a095e 100644 --- a/embed/web/client/index.html +++ b/embed/web/client/index.html @@ -108,7 +108,7 @@ const msg = JSON.parse(evt.data); console.log(msg) switch(msg.type) { - case "server-status": + case "client-status": // handleMsgServerStatus(msg.data) break; diff --git a/embed/web/server/operator.html b/embed/web/server/operator.html index a5d59dc..909a1e9 100644 --- a/embed/web/server/operator.html +++ b/embed/web/server/operator.html @@ -189,8 +189,9 @@ conn.onmessage = (evt) => { const msg = JSON.parse(evt.data); switch(msg.type) { - case "server-status": - handleMsgServerStatus(msg.data); + case "session-status": + console.log(msg) + handleMsgServerStatus(msg['session-status']); break; case "log": diff --git a/go.mod b/go.mod index f8422cb..d3c2763 100644 --- a/go.mod +++ b/go.mod @@ -18,7 +18,6 @@ require ( github.com/pmezard/go-difflib v1.0.0 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/shopspring/decimal v1.3.1 // indirect - github.com/stretchr/objx v0.5.0 // indirect github.com/stretchr/testify v1.8.4 // indirect github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/host-panel.go b/host-panel.go index 48012c5..a563e12 100644 --- a/host-panel.go +++ b/host-panel.go @@ -324,9 +324,9 @@ func (hp *HostPanel) sendServerStatus(resetTicker bool) { hp.statusTicker.Reset(statusSendPeriod) } - msg := comms.MsgServerStatus{ - Type: "server-status", - Data: hp.session.GetStatus(), + msg := comms.Msg{ + Type: "session-status", + SessionStatus: hp.session.GetStatus(), } err := hp.sendData(&msg) diff --git a/jukebox.go b/jukebox.go index b66d658..0b3a9e1 100644 --- a/jukebox.go +++ b/jukebox.go @@ -39,7 +39,9 @@ func (j *Jukebox) start() { }) tsFirst.SetCode(code) - (*j.comms) <- comms.Msg{Type: "tic-state", TicState: tsFirst} + (*j.comms) <- comms.Msg{Type: "tic-state", TicState: comms.DataTicState{ + State: tsFirst, + }} rotateTicker := time.NewTicker(rotatePeriod) defer rotateTicker.Stop() @@ -72,7 +74,9 @@ func (j *Jukebox) start() { */ ts := machines.MakeTicStateRunning(code) - (*j.comms) <- comms.Msg{Type: "tic-state", TicState: ts} + (*j.comms) <- comms.Msg{Type: "tic-state", TicState: comms.DataTicState{ + State: ts, + }} } } }() diff --git a/local-jukebox.go b/local-jukebox.go index d95a314..6f6b9c3 100644 --- a/local-jukebox.go +++ b/local-jukebox.go @@ -32,7 +32,7 @@ func startLocalJukebox(playlist *Playlist) error { if ok { switch msg.Type { case "tic-state": - err = m.Tic.WriteImportCode(msg.TicState.TicState) + err = m.Tic.WriteImportCode(msg.TicState.State) if err != nil { // #TODO: soften! log.Fatal(err) diff --git a/server/config.go b/server/config.go deleted file mode 100644 index 5f8cf04..0000000 --- a/server/config.go +++ /dev/null @@ -1,18 +0,0 @@ -package server - -import ( - "github.com/google/uuid" -) - -type ConfigMachines struct { - MachineUuid uuid.UUID `json:"machine_uuid,error"` - UserUuid *uuid.UUID `json:"user_uuid,omitempty"` -} - -type Config struct { - Machines []ConfigMachines `json:"machines"` -} - -func getConfig(m *SessionManager) Config { - panic("not implemented") -} diff --git a/server/session-config.go b/server/session-config.go index 075b31d..1d52b32 100644 --- a/server/session-config.go +++ b/server/session-config.go @@ -1,16 +1,43 @@ package server +import "github.com/google/uuid" + +type ConfigMachine struct { + Name string `json:"name"` + Uuid uuid.UUID `json:"uuid"` + JammerIdentity *uuid.UUID `json:"jammer_identity,omitempty"` +} + type SessionConfig struct { - Port int - Name string - Slug string + Port int `json:"port"` + Name string `json:"name"` + Slug string `json:"slug"` + Machines []ConfigMachine `json:"machines"` } // JamSessionConfig should be enough to save to disk and restart a JamSession if it crashes -func getSessionConfig(js Session) SessionConfig { - return SessionConfig{ - Port: js.port, - Name: js.name, - Slug: js.slug, +func getSessionConfig(s Session) SessionConfig { + sc := SessionConfig{ + Port: s.port, + Name: s.name, + Slug: s.slug, } + + for _, machine := range s.switchboard.machines { + var jammerIdentity *uuid.UUID + machineConn, ok := s.switchboard.machineConnMap[machine] + if ok { + if machineConn.identity != nil { + jammerIdentity = &machineConn.identity.uuid + } + } + + sc.Machines = append(sc.Machines, ConfigMachine{ + Name: machine.MachineName, + Uuid: machine.Uuid, + JammerIdentity: jammerIdentity, + }) + } + + return sc } diff --git a/server/session-conn.go b/server/session-conn.go index 83b33e1..c50f64f 100644 --- a/server/session-conn.go +++ b/server/session-conn.go @@ -82,10 +82,10 @@ func (jc *SessionConn) runServerWsConnRead(js *Session) { // #TODO: Refactor this placeholder! // Kick an existing connection off if it has the same identity - for _, c := range js.manager.conns { + for _, c := range js.switchboard.conns { if c != jc && c.identity != nil && c.identity.uuid.String() == jc.identity.uuid.String() { c.signalKick <- true - js.manager.unregisterConn(c) + js.switchboard.unregisterConn(c) } } @@ -104,7 +104,7 @@ func (jc *SessionConn) runServerWsConnRead(js *Session) { fmt.Println(msg.ChallengeResponse.Challenge) case "tic-state": - ts := msg.TicState + ts := msg.TicState.State if jc.lastTicState != nil && ts.IsEqual(*jc.lastTicState) { // We already sent this state @@ -118,7 +118,7 @@ func (jc *SessionConn) runServerWsConnRead(js *Session) { os.WriteFile(path, []byte(ts.GetCode()), 0644) } - machine := js.manager.getMachineForConn(jc) + machine := js.switchboard.getMachineForConn(jc) if machine != nil && machine.Tic != nil { // Output to Tic // Don't shim for now... @@ -160,7 +160,9 @@ func (js *SessionConn) sendMachineNameCode(machineName string) error { }) ts.SetCode(code) - msg := comms.Msg{Type: "tic-state", TicState: ts} + msg := comms.Msg{Type: "tic-state", TicState: comms.DataTicState{ + State: ts, + }} err := js.sendData(msg) return err } diff --git a/server/session-manager.go b/server/session-manager.go deleted file mode 100644 index f404764..0000000 --- a/server/session-manager.go +++ /dev/null @@ -1,113 +0,0 @@ -package server - -import ( - "errors" - - "github.com/creativenucleus/bytejammer/machines" - "github.com/google/uuid" -) - -type SessionManager struct { - machines map[uuid.UUID]*machines.Machine - conns map[uuid.UUID]*SessionConn - machineConnMap map[*machines.Machine]*SessionConn - - // #TODO: make this work... - // Is this the right level?? - // broadcaster *NusanLauncher -} - -func makeSessionManager() *SessionManager { - return &SessionManager{ - machines: make(map[uuid.UUID]*machines.Machine), - conns: make(map[uuid.UUID]*SessionConn), - machineConnMap: make(map[*machines.Machine]*SessionConn), - } -} - -// #TODO: Mutexes - -func (sm *SessionManager) registerMachine(machine *machines.Machine) { - sm.machines[machine.Uuid] = machine -} - -func (sm *SessionManager) unregisterMachine(machine *machines.Machine) { - delete(sm.machines, machine.Uuid) -} - -func (sm *SessionManager) getMachine(uuid uuid.UUID) *machines.Machine { - machine, ok := sm.machines[uuid] - if !ok { - return nil - } - return machine -} - -func (sm *SessionManager) registerConn(conn *SessionConn) { - sm.conns[conn.connUuid] = conn -} - -func (sm *SessionManager) unregisterConn(conn *SessionConn) { - delete(sm.conns, conn.connUuid) -} - -func (sm *SessionManager) getConn(connUuid uuid.UUID) *SessionConn { - conn, ok := sm.conns[connUuid] - if !ok { - return nil - } - return conn -} - -// You must register a machine and conn before linking them -func (sm *SessionManager) linkMachineToConn(machineUuid uuid.UUID, connUuid uuid.UUID) error { - machine := sm.getMachine(machineUuid) - if machine == nil { - return errors.New("machine not found") - } - - conn := sm.getConn(connUuid) - if conn == nil { - return errors.New("conn not found") - } - - sm.machineConnMap[machine] = conn - return nil -} - -// You must unlink a machine and conn before destroying either -func (sm *SessionManager) unlinkMachineFromConn(machineUuid uuid.UUID, connUuid uuid.UUID) error { - machine := sm.getMachine(machineUuid) - if machine == nil { - return errors.New("machine not found") - } - - conn := sm.getConn(connUuid) - if conn == nil { - return errors.New("conn not found") - } - - if sm.machineConnMap[machine] != conn { - return errors.New("machine does not link to expected conn") - } - - delete(sm.machineConnMap, machine) - return nil -} - -func (sm *SessionManager) getConnForMachine(machine *machines.Machine) *SessionConn { - conn, ok := sm.machineConnMap[machine] - if !ok { - return nil - } - return conn -} - -func (sm *SessionManager) getMachineForConn(conn *SessionConn) *machines.Machine { - for machine, c := range sm.machineConnMap { - if c == conn { - return machine - } - } - return nil -} diff --git a/server/session.go b/server/session.go index 5e87774..48b5309 100644 --- a/server/session.go +++ b/server/session.go @@ -25,7 +25,7 @@ type Session struct { startTime time.Time // The connections, machines, and the connections between them - manager *SessionManager + switchboard *Switchboard chLog chan string } @@ -38,12 +38,12 @@ func CreateSession(port int, name string, chLog chan string) (*Session, error) { now := time.Now() js := Session{ - port: port, - name: name, - slug: fmt.Sprintf("%s_%s", util.GetSlugFromTime(now), nameSlug), - startTime: now, - manager: makeSessionManager(), - chLog: chLog, + port: port, + name: name, + slug: fmt.Sprintf("%s_%s", util.GetSlugFromTime(now), nameSlug), + startTime: now, + switchboard: makeSwitchboard(), + chLog: chLog, } basePath := js.getBasePath() @@ -53,23 +53,45 @@ func CreateSession(port int, name string, chLog chan string) (*Session, error) { return nil, err } - config := getSessionConfig(js) - configData, err := json.Marshal(config) + err = js.writeConfig() if err != nil { return nil, err } - err = os.WriteFile(fmt.Sprintf("%s/config.json", basePath), configData, 0644) + err = js.start() if err != nil { return nil, err } - err = js.start() + // Periodic saving... + // #TODO: (There must be a bad pattern!) + go func() { + for { + time.Sleep(10 * time.Second) + err := js.writeConfig() + if err != nil { + chLog <- fmt.Sprintf("ERR save config: %s", err) + } + } + }() + + return &js, nil +} + +func (js *Session) writeConfig() error { + config := getSessionConfig(*js) + configData, err := json.Marshal(config) if err != nil { - return nil, err + return err } - return &js, nil + filepath := fmt.Sprintf("%s/config.json", js.getBasePath()) + err = os.WriteFile(filepath, configData, 0644) + if err != nil { + return err + } + + return nil } func (js *Session) start() error { @@ -117,7 +139,7 @@ func (js *Session) wsBytejam() func(http.ResponseWriter, *http.Request) { */ jsConn := NewJamSessionConnection(conn) - js.manager.registerConn(jsConn) + js.switchboard.registerConn(jsConn) go jsConn.runServerWsConnRead(js) go jsConn.runServerWsConnWrite(js) @@ -151,34 +173,13 @@ func (js *Session) getBasePath() string { return fmt.Sprintf("%sserver-data/%s", config.WORK_DIR, js.slug) } -type SessionStatus struct { - Clients []struct { - Uuid string - DisplayName string - ShortUuid string - Status string - MachineUuid string - LastPingTime string - } - Machines []struct { - Uuid string - MachineName string - ProcessID int - Platform string - Status string - ClientUuid string - JammerDisplayName string - LastSnapshotTime string - } -} - -func (js *Session) GetStatus() SessionStatus { - ss := SessionStatus{} +func (js *Session) GetStatus() comms.DataSessionStatus { + ss := comms.DataSessionStatus{} - for _, jc := range js.manager.conns { + for _, jc := range js.switchboard.conns { status := "waiting" machineUuid := "" - machine := js.manager.getMachineForConn(jc) + machine := js.switchboard.getMachineForConn(jc) if machine != nil { status = fmt.Sprintf("Connected: %s", machine.MachineName) machineUuid = machine.Uuid.String() @@ -201,10 +202,10 @@ func (js *Session) GetStatus() SessionStatus { }) } - for _, m := range js.manager.machines { + for _, m := range js.switchboard.machines { name := "(unassigned)" clientUuid := "" - client := js.manager.getConnForMachine(m) + client := js.switchboard.getConnForMachine(m) if client != nil { name = client.identity.displayName clientUuid = client.connUuid.String() @@ -240,14 +241,14 @@ func (js *Session) StartMachine() (*machines.Machine, error) { return nil, err } - js.manager.registerMachine(m) + js.switchboard.registerMachine(m) js.chLog <- fmt.Sprintf("TIC-80 Launched: %s", m.MachineName) return m, err } func (js *Session) StartMachineForConn(connUuid uuid.UUID) (*machines.Machine, error) { - conn := js.manager.getConn(connUuid) + conn := js.switchboard.getConn(connUuid) if conn == nil { return nil, errors.New("Unable to find conn") } @@ -257,8 +258,8 @@ func (js *Session) StartMachineForConn(connUuid uuid.UUID) (*machines.Machine, e return nil, err } - js.manager.registerMachine(m) - js.manager.linkMachineToConn(m.Uuid, conn.connUuid) + js.switchboard.registerMachine(m) + js.switchboard.linkMachineToConn(m.Uuid, conn.connUuid) // TODO: May have identity? js.chLog <- fmt.Sprintf("TIC-80 Launched: %s for %s", m.MachineName, conn.connUuid) @@ -267,8 +268,8 @@ func (js *Session) StartMachineForConn(connUuid uuid.UUID) (*machines.Machine, e func (js *Session) IdentifyMachines() { count := 0 - for _, c := range js.manager.conns { - m := js.manager.getMachineForConn(c) + for _, c := range js.switchboard.conns { + m := js.switchboard.getMachineForConn(c) if m != nil { err := c.sendMachineNameCode(m.MachineName) if err != nil { @@ -315,13 +316,13 @@ func (js *Session) ConnectMachineClient(data comms.DataConnectMachineClient) { return } - conn := js.manager.getConn(connUuid) + conn := js.switchboard.getConn(connUuid) if conn == nil { js.chLog <- fmt.Sprintf("ERR connect: Could not find Jammer ID") return } - js.manager.linkMachineToConn(machineUuid, connUuid) + js.switchboard.linkMachineToConn(machineUuid, connUuid) js.chLog <- fmt.Sprintf("Connected %s to %s", data.ClientUuid, data.MachineUuid) } @@ -341,13 +342,13 @@ func (js *Session) DisconnectMachineClient(data comms.DataDisconnectMachineClien return } - conn := js.manager.getConn(connUuid) + conn := js.switchboard.getConn(connUuid) if conn == nil { js.chLog <- fmt.Sprintf("ERR connect: Could not find Jammer ID") return } - machine := js.manager.getMachineForConn(conn) + machine := js.switchboard.getMachineForConn(conn) if machine == nil { js.chLog <- fmt.Sprintf("ERR connect: Jammer does not have a machine") } @@ -356,7 +357,7 @@ func (js *Session) DisconnectMachineClient(data comms.DataDisconnectMachineClien js.chLog <- fmt.Sprintf("ERR connect: Jammer's machine ID does not match the requested one") } - js.manager.unlinkMachineFromConn(machineUuid, connUuid) + js.switchboard.unlinkMachineFromConn(machineUuid, connUuid) js.chLog <- fmt.Sprintf("Disconnected %s from %s", data.ClientUuid, data.MachineUuid) } diff --git a/server/switchboard.go b/server/switchboard.go new file mode 100644 index 0000000..765bd39 --- /dev/null +++ b/server/switchboard.go @@ -0,0 +1,113 @@ +package server + +import ( + "errors" + + "github.com/creativenucleus/bytejammer/machines" + "github.com/google/uuid" +) + +type Switchboard struct { + machines map[uuid.UUID]*machines.Machine + conns map[uuid.UUID]*SessionConn + machineConnMap map[*machines.Machine]*SessionConn + + // #TODO: make this work... + // Is this the right level?? + // broadcaster *NusanLauncher +} + +func makeSwitchboard() *Switchboard { + return &Switchboard{ + machines: make(map[uuid.UUID]*machines.Machine), + conns: make(map[uuid.UUID]*SessionConn), + machineConnMap: make(map[*machines.Machine]*SessionConn), + } +} + +// #TODO: Mutexes + +func (s *Switchboard) registerMachine(machine *machines.Machine) { + s.machines[machine.Uuid] = machine +} + +func (s *Switchboard) unregisterMachine(machine *machines.Machine) { + delete(s.machines, machine.Uuid) +} + +func (s *Switchboard) getMachine(uuid uuid.UUID) *machines.Machine { + machine, ok := s.machines[uuid] + if !ok { + return nil + } + return machine +} + +func (s *Switchboard) registerConn(conn *SessionConn) { + s.conns[conn.connUuid] = conn +} + +func (s *Switchboard) unregisterConn(conn *SessionConn) { + delete(s.conns, conn.connUuid) +} + +func (sm *Switchboard) getConn(connUuid uuid.UUID) *SessionConn { + conn, ok := sm.conns[connUuid] + if !ok { + return nil + } + return conn +} + +// You must register a machine and conn before linking them +func (s *Switchboard) linkMachineToConn(machineUuid uuid.UUID, connUuid uuid.UUID) error { + machine := s.getMachine(machineUuid) + if machine == nil { + return errors.New("machine not found") + } + + conn := s.getConn(connUuid) + if conn == nil { + return errors.New("conn not found") + } + + s.machineConnMap[machine] = conn + return nil +} + +// You must unlink a machine and conn before destroying either +func (s *Switchboard) unlinkMachineFromConn(machineUuid uuid.UUID, connUuid uuid.UUID) error { + machine := s.getMachine(machineUuid) + if machine == nil { + return errors.New("machine not found") + } + + conn := s.getConn(connUuid) + if conn == nil { + return errors.New("conn not found") + } + + if s.machineConnMap[machine] != conn { + return errors.New("machine does not link to expected conn") + } + + delete(s.machineConnMap, machine) + return nil +} + +func (s *Switchboard) getConnForMachine(machine *machines.Machine) *SessionConn { + conn, ok := s.machineConnMap[machine] + if !ok { + return nil + } + return conn +} + +func (s *Switchboard) getMachineForConn(conn *SessionConn) *machines.Machine { + for machine, c := range s.machineConnMap { + if c == conn { + return machine + } + } + return nil +} From 9105d82995a813f8525224e2adff01e0568dd6b0 Mon Sep 17 00:00:00 2001 From: Dave Borghuis Date: Mon, 6 Nov 2023 00:25:53 +0100 Subject: [PATCH 04/11] Juxebox playtime (#6) * Implement option playtime in commandline option and json * Cleanup and documentation --------- Co-authored-by: Dave Borghuis --- README.md | 19 +++++++++++++++++++ client-jukebox.go | 6 +++--- host-panel.go | 4 +++- jukebox.go | 20 +++++++++++++++++++- local-jukebox.go | 5 +++-- main.go | 23 +++++++++++++++++++---- playlist.go | 1 + playlist_json.go | 2 ++ 8 files changed, 69 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index e7d60ff..bdae537 100644 --- a/README.md +++ b/README.md @@ -42,6 +42,25 @@ Make sure you place it in a folder of its own. It will create a subfolder to hol Default (no arguments) mode will launch into jukebox mode, playing random Bytejams from LCDZ. It can be provided with a JSON file playlist (from remote and local) or .zip file. +JSON file should be formated like +```JSON +{ + "title": "TIC-80 selectec works", + "description": "(with FFT)", + "items": [ + { + "location": "https://livecode.demozoo.org/shader_file_sources/2023_05_15_byte_jam_monday_night_bytes/nusan.lua", + "author": "Nusan ", + "description": "FieldFX Byte Jam - 15/05/2023", + "playtime": 30 + } + ] +} +``` + +A JSON file can be included in a zip file with the name 'index.json' + + Applications: - To project onto a wall at parties to preach the good TIC and Bytejam words. diff --git a/client-jukebox.go b/client-jukebox.go index a421975..b948a33 100644 --- a/client-jukebox.go +++ b/client-jukebox.go @@ -2,13 +2,13 @@ package main import ( "log" - + "time" "github.com/creativenucleus/bytejammer/comms" ) -func startClientJukebox(host string, port int, playlist *Playlist) error { +func startClientJukebox(host string, port int, playtime time.Duration, playlist *Playlist) error { ch := make(chan comms.Msg) - j, err := NewJukebox(playlist, &ch) + j, err := NewJukebox(playlist, playtime, &ch) if err != nil { return err } diff --git a/host-panel.go b/host-panel.go index a563e12..fd3c19a 100644 --- a/host-panel.go +++ b/host-panel.go @@ -244,7 +244,9 @@ func (hp *HostPanel) webApiMachine(w http.ResponseWriter, r *http.Request) { return } - err = startLocalJukebox(playlist) + playtime := 7 * time.Second //TODO: playtime + + err = startLocalJukebox(playlist, playtime) if err != nil { apiOutErr(w, err, http.StatusInternalServerError) return diff --git a/jukebox.go b/jukebox.go index 0b3a9e1..8c2734e 100644 --- a/jukebox.go +++ b/jukebox.go @@ -16,14 +16,16 @@ const ( type Jukebox struct { playlist *Playlist + playtime time.Duration comms *chan comms.Msg } -func NewJukebox(playlist *Playlist, comms *chan comms.Msg) (*Jukebox, error) { +func NewJukebox(playlist *Playlist, playtime time.Duration, comms *chan comms.Msg) (*Jukebox, error) { log.Printf("-> Launching Jukebox for playlist") j := Jukebox{ comms: comms, + playtime: playtime, playlist: playlist, } @@ -64,6 +66,22 @@ func (j *Jukebox) start() { fmt.Printf("Description: %s\n", playlistItem.description) } + var timeoutduration time.Duration + //from json file + if playlistItem.playtime > 0 { + timeoutduration = time.Duration(playlistItem.playtime) * time.Second + } + //from command prompt + if j.playtime > 0 { + timeoutduration = time.Duration(j.playtime) + } + //internal default + if timeoutduration == 0 { + timeoutduration = rotatePeriod + } + fmt.Printf("Playtime: %s\n", timeoutduration) + rotateTicker.Reset(timeoutduration) + code := playlistItem.code /* diff --git a/local-jukebox.go b/local-jukebox.go index 6f6b9c3..521930b 100644 --- a/local-jukebox.go +++ b/local-jukebox.go @@ -3,17 +3,18 @@ package main import ( "fmt" "log" + "time" "github.com/creativenucleus/bytejammer/comms" "github.com/creativenucleus/bytejammer/machines" ) -func startLocalJukebox(playlist *Playlist) error { +func startLocalJukebox(playlist *Playlist, playtime time.Duration) error { fmt.Printf("Starting local jukebox containing %d items\n", len(playlist.items)) ch := make(chan comms.Msg) - j, err := NewJukebox(playlist, &ch) + j, err := NewJukebox(playlist, playtime, &ch) if err != nil { return err } diff --git a/main.go b/main.go index 170a6e8..da8d42b 100644 --- a/main.go +++ b/main.go @@ -5,6 +5,7 @@ import ( "log" "os" "path/filepath" + "time" "github.com/urfave/cli/v2" @@ -57,15 +58,22 @@ func main() { Name: "playlist", Usage: "Playlist file (empty for LCDZ playlist)", }, + &cli.StringFlag{ + Name: "playtime", + Usage: "Playtime for each item, default 7 sec", + }, }, Action: func(cCtx *cli.Context) error { + var_playtime := cCtx.Uint("playtime") + fmt.Printf("default playtime is %d\n", var_playtime) + var playtime = time.Duration(var_playtime) * time.Second + playlistFilename := cCtx.String("playlist") playlist, err := readPlaylist(playlistFilename) if err != nil { log.Fatal(err) } - - err = startLocalJukebox(playlist) + err = startLocalJukebox(playlist, playtime) if err != nil { log.Fatal(err) } @@ -106,7 +114,6 @@ func main() { if err != nil { log.Fatal(err) } - return nil }, }, { @@ -146,8 +153,16 @@ func main() { Name: "playlist", Usage: "Playlist file (empty for LCDZ playlist)", }, + &cli.StringFlag{ + Name: "playtime", + Usage: "Playtime for each item, default 7 sec", + }, }, Action: func(cCtx *cli.Context) error { + var_playtime := cCtx.Uint("playtime") + fmt.Printf("default playtime is %d\n", var_playtime) + var playtime = time.Duration(var_playtime) * time.Second + host := cCtx.String("host") port := cCtx.Int("port") playlistFilename := cCtx.String("playlist") @@ -156,7 +171,7 @@ func main() { log.Fatal(err) } - err = startClientJukebox(host, port, playlist) + err = startClientJukebox(host, port, playtime, playlist) if err != nil { log.Fatal(err) } diff --git a/playlist.go b/playlist.go index f9e3a75..f98dda9 100644 --- a/playlist.go +++ b/playlist.go @@ -17,6 +17,7 @@ type PlaylistItem struct { location string author string description string + playtime uint32 code []byte } diff --git a/playlist_json.go b/playlist_json.go index a782ac9..3ccc8b8 100644 --- a/playlist_json.go +++ b/playlist_json.go @@ -9,6 +9,7 @@ type PlaylistJSON struct { Location string `json:"location"` Author string `json:"author"` Description string `json:"description"` + PlayTime uint32 `json:"playtime"` } `json:"items"` } @@ -27,6 +28,7 @@ func NewPlaylistFromJSON(bytesIn []byte) (*Playlist, error) { location: item.Location, author: item.Author, description: item.Description, + playtime: item.PlayTime, }) } From 5ce85b5aabb11f27483bc1d9fa84ed2d96961d2c Mon Sep 17 00:00:00 2001 From: James / creativenucleus Date: Sun, 5 Nov 2023 23:59:58 +0000 Subject: [PATCH 05/11] Shifted some numbers around for the jukebox playtime. Did some cleanup (from vet) --- README.md | 4 +-- client-jukebox.go | 23 ++++++------- client-panel.go | 25 +++++--------- go.mod | 2 +- go.sum | 10 +----- host-panel.go | 18 ++++------ identity.go | 4 +-- jukebox.go | 78 ++++++++++++++++++------------------------ main.go | 31 ++++++++++------- nusan.go | 18 +++++----- playlist.go | 2 +- playlist/maths-af.json | 9 +++-- playlist_zip.go | 3 +- server/session.go | 28 +++++++-------- 14 files changed, 113 insertions(+), 142 deletions(-) diff --git a/README.md b/README.md index bdae537..6528c9f 100644 --- a/README.md +++ b/README.md @@ -45,12 +45,12 @@ It can be provided with a JSON file playlist (from remote and local) or .zip fil JSON file should be formated like ```JSON { - "title": "TIC-80 selectec works", + "title": "TIC-80 selected works", "description": "(with FFT)", "items": [ { "location": "https://livecode.demozoo.org/shader_file_sources/2023_05_15_byte_jam_monday_night_bytes/nusan.lua", - "author": "Nusan ", + "author": "Nusan", "description": "FieldFX Byte Jam - 15/05/2023", "playtime": 30 } diff --git a/client-jukebox.go b/client-jukebox.go index b948a33..156fe61 100644 --- a/client-jukebox.go +++ b/client-jukebox.go @@ -3,6 +3,7 @@ package main import ( "log" "time" + "github.com/creativenucleus/bytejammer/comms" ) @@ -21,18 +22,16 @@ func startClientJukebox(host string, port int, playtime time.Duration, playlist go func() { for { - select { - case msg, ok := <-ch: - if ok { - switch msg.Type { - case "tic-state": - // #TODO: line endings for data? UTF-8? - msg := comms.Msg{Type: "tic-state", TicState: msg.TicState} - err = ws.sendData(msg) - if err != nil { - // #TODO: soften! - log.Fatal(err) - } + msg, ok := <-ch + if ok { + switch msg.Type { + case "tic-state": + // #TODO: line endings for data? UTF-8? + msg := comms.Msg{Type: "tic-state", TicState: msg.TicState} + err = ws.sendData(msg) + if err != nil { + // #TODO: soften! + log.Fatal(err) } } } diff --git a/client-panel.go b/client-panel.go index f78f573..d1344db 100644 --- a/client-panel.go +++ b/client-panel.go @@ -106,7 +106,7 @@ func (cp *ClientPanel) webClientApiIdentityJSON(w http.ResponseWriter, r *http.R apiOutResponse(w, nil, http.StatusCreated) default: - apiOutErr(w, errors.New("Method not allowed"), http.StatusMethodNotAllowed) + apiOutErr(w, fmt.Errorf("method not allowed"), http.StatusMethodNotAllowed) } } @@ -150,7 +150,7 @@ func (cp *ClientPanel) webClientApiJoinServerJSON(w http.ResponseWriter, r *http apiOutResponse(w, nil, http.StatusCreated) default: - apiOutErr(w, errors.New("Method not allowed"), http.StatusMethodNotAllowed) + apiOutErr(w, errors.New("method not allowed"), http.StatusMethodNotAllowed) } } @@ -168,8 +168,6 @@ func (cp *ClientPanel) wsWebClient() func(http.ResponseWriter, *http.Request) { // #TODO: handle exit for { } - - return nil }) if err != nil { log.Print("upgrade:", err) @@ -202,19 +200,12 @@ func (cp *ClientPanel) wsWrite() { }() */ for { - select { - // case <-done: - // return - // case <-statusTicker.C: - // fmt.Println("TICKER!") - - case status := <-cp.chSendClientStatus: - msg := comms.Msg{Type: "client-status", ClientStatus: status} - err := cp.sendData(&msg) - if err != nil { - // #TODO: relax - log.Fatal(err) - } + status := <-cp.chSendClientStatus + msg := comms.Msg{Type: "client-status", ClientStatus: status} + err := cp.sendData(&msg) + if err != nil { + // #TODO: relax + log.Fatal(err) } } } diff --git a/go.mod b/go.mod index d3c2763..ad659e5 100644 --- a/go.mod +++ b/go.mod @@ -6,6 +6,7 @@ require ( github.com/google/uuid v1.3.1 github.com/gorilla/websocket v1.5.0 github.com/gosimple/slug v1.13.1 + github.com/stretchr/testify v1.8.4 github.com/tyler-sommer/stick v1.0.6 github.com/urfave/cli/v2 v2.25.7 golang.org/x/net v0.17.0 @@ -18,7 +19,6 @@ require ( github.com/pmezard/go-difflib v1.0.0 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/shopspring/decimal v1.3.1 // indirect - github.com/stretchr/testify v1.8.4 // indirect github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 5e19ef1..436d2d2 100644 --- a/go.sum +++ b/go.sum @@ -1,6 +1,5 @@ github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w= github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= -github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/google/uuid v1.3.1 h1:KjJaJ9iWZ3jOFZIf1Lqf4laDRCasjl0BCmnEGxkdLb4= @@ -18,13 +17,6 @@ github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQD github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4= github.com/shopspring/decimal v1.3.1 h1:2Usl1nmF/WZucqkFZhnfFYxxxu8LG21F6nPQBE5gKV8= github.com/shopspring/decimal v1.3.1/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= -github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= -github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c= -github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= -github.com/stretchr/objx v0.5.1/go.mod h1:/iHQpkQwBD6DLUmQ4pE+s1TXdob1mORJ4/UFdrifcy0= -github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/tyler-sommer/stick v1.0.6 h1:LLdJ8oGotXCsAuVx2BRZZT6s3bYXHe0ImQ+azF4HtJg= @@ -35,7 +27,7 @@ github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRT github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8= golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM= golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/host-panel.go b/host-panel.go index fd3c19a..2c3df2f 100644 --- a/host-panel.go +++ b/host-panel.go @@ -96,8 +96,6 @@ func (hp *HostPanel) wsWebOperator() func(http.ResponseWriter, *http.Request) { // #TODO: handle exit for { } - - return nil }) if err != nil { log.Print("upgrade:", err) @@ -176,22 +174,22 @@ func (hp *HostPanel) webApiServer(w http.ResponseWriter, r *http.Request) { // #TODO: This is not great - return some detail if hp.session != nil { - apiOutErr(w, errors.New("Server already running"), http.StatusBadRequest) + apiOutErr(w, errors.New("server already running"), http.StatusBadRequest) return } hp.session, err = server.CreateSession(port, req.SessionName, hp.chLog) if err != nil { - hp.chLog <- fmt.Sprintf("Server failed to launch: %s", err) + hp.chLog <- fmt.Sprintf("server failed to launch: %s", err) apiOutErr(w, err, http.StatusInternalServerError) return } - hp.sendLog(fmt.Sprintf("Server launched")) + hp.sendLog("server launched") hp.sendServerStatus(true) default: - apiOutErr(w, errors.New("Method not allowed"), http.StatusMethodNotAllowed) + apiOutErr(w, errors.New("method not allowed"), http.StatusMethodNotAllowed) } } @@ -244,9 +242,7 @@ func (hp *HostPanel) webApiMachine(w http.ResponseWriter, r *http.Request) { return } - playtime := 7 * time.Second //TODO: playtime - - err = startLocalJukebox(playlist, playtime) + err = startLocalJukebox(playlist, time.Duration(JUKEBOX_PLAYTIME_SECS)*time.Second) if err != nil { apiOutErr(w, err, http.StatusInternalServerError) return @@ -255,14 +251,14 @@ func (hp *HostPanel) webApiMachine(w http.ResponseWriter, r *http.Request) { hp.sendLog("TIC-80 Launched for (playlist)") default: - apiOutErr(w, errors.New("Unexpected mode (should be jammer or jukebox)"), http.StatusBadRequest) + apiOutErr(w, errors.New("unexpected mode (should be jammer or jukebox)"), http.StatusBadRequest) } hp.sendServerStatus(true) apiOutResponse(w, nil, http.StatusCreated) default: - apiOutErr(w, errors.New("Method not allowed"), http.StatusMethodNotAllowed) + apiOutErr(w, errors.New("method not allowed"), http.StatusMethodNotAllowed) } } func (hp *HostPanel) handleStopServer() { diff --git a/identity.go b/identity.go index 9610ad7..22bb7ef 100644 --- a/identity.go +++ b/identity.go @@ -81,11 +81,11 @@ func getIdentity(uuid string) (*Identity, error) { } if len(filematches) == 0 { - return nil, fmt.Errorf("No identity file found - ensure you've run this program with make-identity first") + return nil, fmt.Errorf("no identity file found - ensure you've run this program with make-identity first") } if len(filematches) > 1 { - return nil, fmt.Errorf("Multiple identity files found - please specify") + return nil, fmt.Errorf("multiple identity files found - please specify") } identityFilePath = filematches[0] diff --git a/jukebox.go b/jukebox.go index 8c2734e..26e1354 100644 --- a/jukebox.go +++ b/jukebox.go @@ -21,7 +21,7 @@ type Jukebox struct { } func NewJukebox(playlist *Playlist, playtime time.Duration, comms *chan comms.Msg) (*Jukebox, error) { - log.Printf("-> Launching Jukebox for playlist") + log.Printf("-> Launching Jukebox for playlist, with default playtime of %s", playtime) j := Jukebox{ comms: comms, @@ -49,53 +49,43 @@ func (j *Jukebox) start() { defer rotateTicker.Stop() for { - select { - case <-rotateTicker.C: - playlistItem, err := j.playlist.getNext() - if err != nil { - log.Println("ERR get code:", err) - break - } + <-rotateTicker.C + playlistItem, err := j.playlist.getNext() + if err != nil { + log.Println("ERR get code:", err) + break + } - fmt.Printf("Playing (TIC):\nLocation: %s\n", playlistItem.location) - if playlistItem.author != "" { - fmt.Printf("Author: %s\n", playlistItem.author) - } + fmt.Printf("Playing (TIC):\nLocation: %s\n", playlistItem.location) + if playlistItem.author != "" { + fmt.Printf("Author: %s\n", playlistItem.author) + } - if playlistItem.description != "" { - fmt.Printf("Description: %s\n", playlistItem.description) - } + if playlistItem.description != "" { + fmt.Printf("Description: %s\n", playlistItem.description) + } - var timeoutduration time.Duration - //from json file - if playlistItem.playtime > 0 { - timeoutduration = time.Duration(playlistItem.playtime) * time.Second - } - //from command prompt - if j.playtime > 0 { - timeoutduration = time.Duration(j.playtime) - } - //internal default - if timeoutduration == 0 { - timeoutduration = rotatePeriod - } - fmt.Printf("Playtime: %s\n", timeoutduration) - rotateTicker.Reset(timeoutduration) - - code := playlistItem.code - - /* - -- Removes vbank 1, so nerfed for now! - if playlistItem.author != "" { - code = machines.CodeAddAuthorShim(code, playlistItem.author) - } - */ - - ts := machines.MakeTicStateRunning(code) - (*j.comms) <- comms.Msg{Type: "tic-state", TicState: comms.DataTicState{ - State: ts, - }} + playtime := time.Duration(j.playtime) + if playlistItem.playtime > 0 { + // Prefer the value from the playlist item in the JSON file if it is set... + playtime = time.Duration(playlistItem.playtime) * time.Second } + + rotateTicker.Reset(playtime) + + code := playlistItem.code + + /* + -- Removes vbank 1, so nerfed for now! + if playlistItem.author != "" { + code = machines.CodeAddAuthorShim(code, playlistItem.author) + } + */ + + ts := machines.MakeTicStateRunning(code) + (*j.comms) <- comms.Msg{Type: "tic-state", TicState: comms.DataTicState{ + State: ts, + }} } }() } diff --git a/main.go b/main.go index da8d42b..d743ae5 100644 --- a/main.go +++ b/main.go @@ -14,7 +14,8 @@ import ( ) const ( - RELEASE_TITLE = "Appealing Apricot" + RELEASE_TITLE = "Appealing Apricot" + JUKEBOX_PLAYTIME_SECS = 10 ) func main() { @@ -58,22 +59,24 @@ func main() { Name: "playlist", Usage: "Playlist file (empty for LCDZ playlist)", }, - &cli.StringFlag{ + &cli.UintFlag{ Name: "playtime", - Usage: "Playtime for each item, default 7 sec", + Usage: "Playtime for each item (in seconds)", + Value: JUKEBOX_PLAYTIME_SECS, }, }, Action: func(cCtx *cli.Context) error { - var_playtime := cCtx.Uint("playtime") - fmt.Printf("default playtime is %d\n", var_playtime) - var playtime = time.Duration(var_playtime) * time.Second + defaultPlaytime := cCtx.Uint("playtime") + if defaultPlaytime == 0 { + defaultPlaytime = JUKEBOX_PLAYTIME_SECS + } playlistFilename := cCtx.String("playlist") playlist, err := readPlaylist(playlistFilename) if err != nil { log.Fatal(err) } - err = startLocalJukebox(playlist, playtime) + err = startLocalJukebox(playlist, time.Duration(defaultPlaytime)*time.Second) if err != nil { log.Fatal(err) } @@ -153,15 +156,17 @@ func main() { Name: "playlist", Usage: "Playlist file (empty for LCDZ playlist)", }, - &cli.StringFlag{ + &cli.UintFlag{ Name: "playtime", - Usage: "Playtime for each item, default 7 sec", + Usage: "Playtime for each item (in seconds)", + Value: JUKEBOX_PLAYTIME_SECS, }, }, Action: func(cCtx *cli.Context) error { - var_playtime := cCtx.Uint("playtime") - fmt.Printf("default playtime is %d\n", var_playtime) - var playtime = time.Duration(var_playtime) * time.Second + defaultPlaytime := cCtx.Uint("playtime") + if defaultPlaytime == 0 { + defaultPlaytime = JUKEBOX_PLAYTIME_SECS + } host := cCtx.String("host") port := cCtx.Int("port") @@ -171,7 +176,7 @@ func main() { log.Fatal(err) } - err = startClientJukebox(host, port, playtime, playlist) + err = startClientJukebox(host, port, time.Duration(defaultPlaytime)*time.Second, playlist) if err != nil { log.Fatal(err) } diff --git a/nusan.go b/nusan.go index 33adb59..fd5da4a 100644 --- a/nusan.go +++ b/nusan.go @@ -90,16 +90,14 @@ type NusanLauncherMsg struct { func (nl *NusanLauncher) nusanWsOperatorWrite() { for { - select { - case msg := <-(*nl.ch): - fmt.Printf("-> NUSAN TOSEND: %v\n", msg) - nlMsg := NusanLauncherMsg{} - nlMsg.Data.RoomName = "bytejammer" - nlMsg.Data.NickName = msg - err := nl.sendData(&nlMsg) - if err != nil { - log.Fatal(err) - } + msg := <-(*nl.ch) + fmt.Printf("-> NUSAN TOSEND: %v\n", msg) + nlMsg := NusanLauncherMsg{} + nlMsg.Data.RoomName = "bytejammer" + nlMsg.Data.NickName = msg + err := nl.sendData(&nlMsg) + if err != nil { + log.Fatal(err) } } } diff --git a/playlist.go b/playlist.go index f98dda9..c5e70dd 100644 --- a/playlist.go +++ b/playlist.go @@ -65,7 +65,7 @@ func (p *Playlist) getNext() (*PlaylistItem, error) { defer respLua.Body.Close() if respLua.StatusCode != http.StatusOK { - return nil, errors.New(fmt.Sprintf("ERR write: Status Code = %d", respLua.StatusCode)) + return nil, fmt.Errorf("write: Status Code = %d", respLua.StatusCode) } data, err := io.ReadAll(respLua.Body) diff --git a/playlist/maths-af.json b/playlist/maths-af.json index 2234017..8bed6de 100644 --- a/playlist/maths-af.json +++ b/playlist/maths-af.json @@ -1,11 +1,14 @@ { "title": "Maths A.F. Playlist", "items": [{ - "location": "https://livecode.demozoo.org/shader_file_sources/2023_09_04_byte_jam_monday_night_bytes/alia.lua" + "location": "https://livecode.demozoo.org/shader_file_sources/2023_09_04_byte_jam_monday_night_bytes/alia.lua", + "playtime": 3 }, { - "location": "https://livecode.demozoo.org/shader_file_sources/2023_06_12_byte_jam_monday_night_bytes/gasman.lua" + "location": "https://livecode.demozoo.org/shader_file_sources/2023_06_12_byte_jam_monday_night_bytes/gasman.lua", + "playtime": 30 }, { - "location": "https://livecode.demozoo.org/shader_file_sources/2022_12_05_byte_jam_fieldfx_casual/gasman.lua" + "location": "https://livecode.demozoo.org/shader_file_sources/2022_12_05_byte_jam_fieldfx_casual/gasman.lua", + "playtime": 3 }, { "location": "https://livecode.demozoo.org/shader_file_sources/2023_07_28_byte_jam_evoke/superogue_jam.lua" }] diff --git a/playlist_zip.go b/playlist_zip.go index 331018a..78da5a9 100644 --- a/playlist_zip.go +++ b/playlist_zip.go @@ -3,7 +3,6 @@ package main import ( "archive/zip" "bytes" - "errors" "fmt" "io" "log" @@ -61,7 +60,7 @@ func NewPlaylistFromZip(zipFilename string) (*Playlist, error) { for key, item := range playlist.items { codeData, ok := files[item.location] if !ok { - return nil, errors.New(fmt.Sprintf("File not found (%s)", item.location)) + return nil, fmt.Errorf("File not found (%s)", item.location) } playlist.items[key].code = codeData diff --git a/server/session.go b/server/session.go index 44a0ce4..0a6a632 100644 --- a/server/session.go +++ b/server/session.go @@ -33,7 +33,7 @@ type Session struct { func CreateSession(port int, name string, chLog chan string) (*Session, error) { nameSlug := util.GetSlug(name) if nameSlug == "" { - return nil, errors.New("Invalid session name - unable to make slug") + return nil, errors.New("invalid session name - unable to make slug") } now := time.Now() @@ -113,7 +113,7 @@ func (js *Session) start() error { func (js *Session) wsBytejam() func(http.ResponseWriter, *http.Request) { return func(w http.ResponseWriter, r *http.Request) { - js.chLog <- fmt.Sprintf("Client connected") + js.chLog <- "Client connected" err := comms.WsUpgrade(w, r, func(conn *websocket.Conn) error { /* @@ -150,16 +150,14 @@ func (js *Session) wsBytejam() func(http.ResponseWriter, *http.Request) { // #TODO: handle exit for { - select { - case <-jsConn.signalKick: - // #TODO: Close down read and write channels - fmt.Println("KICKED") - return nil - } + <-jsConn.signalKick + // #TODO: Close down read and write channels + fmt.Println("KICKED") + return nil } }) if err != nil { - js.chLog <- fmt.Sprintf("ws-upgrade: %w", err) + js.chLog <- fmt.Sprintf("ws-upgrade: %s", err) return } } @@ -251,7 +249,7 @@ func (js *Session) StartMachine() (*machines.Machine, error) { func (js *Session) StartMachineForConn(connUuid uuid.UUID) (*machines.Machine, error) { conn := js.switchboard.getConn(connUuid) if conn == nil { - return nil, errors.New("Unable to find conn") + return nil, errors.New("unable to find conn") } m, err := machines.LaunchMachine("TIC-80", true, true, false) @@ -313,13 +311,13 @@ func (js *Session) ConnectMachineClient(data comms.DataConnectMachineClient) { machine := machines.GetMachine(machineUuid) if machine == nil { - js.chLog <- fmt.Sprintf("ERR connect: Could not find Machine ID") + js.chLog <- "ERR connect: Could not find Machine ID" return } conn := js.switchboard.getConn(connUuid) if conn == nil { - js.chLog <- fmt.Sprintf("ERR connect: Could not find Jammer ID") + js.chLog <- "ERR connect: Could not find Jammer ID" return } @@ -345,17 +343,17 @@ func (js *Session) DisconnectMachineClient(data comms.DataDisconnectMachineClien conn := js.switchboard.getConn(connUuid) if conn == nil { - js.chLog <- fmt.Sprintf("ERR connect: Could not find Jammer ID") + js.chLog <- "ERR connect: Could not find Jammer ID" return } machine := js.switchboard.getMachineForConn(conn) if machine == nil { - js.chLog <- fmt.Sprintf("ERR connect: Jammer does not have a machine") + js.chLog <- "ERR connect: Jammer does not have a machine" } if machine.Uuid != machineUuid { - js.chLog <- fmt.Sprintf("ERR connect: Jammer's machine ID does not match the requested one") + js.chLog <- "ERR connect: Jammer's machine ID does not match the requested one" } js.switchboard.unlinkMachineFromConn(machineUuid, connUuid) From 3720766f44d4a73033f7d0c9cc760a5335a55c0c Mon Sep 17 00:00:00 2001 From: James / creativenucleus Date: Mon, 6 Nov 2023 00:04:15 +0000 Subject: [PATCH 06/11] Did some cleanup (from vet) --- client-jukebox.go | 3 +++ client-panel.go | 2 ++ client-ws.go | 2 ++ host-panel.go | 2 ++ local-jukebox.go | 20 ++++++++++---------- nusan.go | 6 ++++-- playlist/maths-af.json | 9 +++------ playlist_lcdz.go | 3 +++ playlist_zip.go | 2 +- 9 files changed, 30 insertions(+), 19 deletions(-) diff --git a/client-jukebox.go b/client-jukebox.go index 156fe61..66b80db 100644 --- a/client-jukebox.go +++ b/client-jukebox.go @@ -39,6 +39,9 @@ func startClientJukebox(host string, port int, playtime time.Duration, playlist }() j.start() + for { + // Removes 100% CPU warning - but this should really be restructured + time.Sleep(10 * time.Second) } } diff --git a/client-panel.go b/client-panel.go index d1344db..af2eb76 100644 --- a/client-panel.go +++ b/client-panel.go @@ -167,6 +167,8 @@ func (cp *ClientPanel) wsWebClient() func(http.ResponseWriter, *http.Request) { // #TODO: handle exit for { + // Removes 100% CPU warning - but this should really be restructured + time.Sleep(10 * time.Second) } }) if err != nil { diff --git a/client-ws.go b/client-ws.go index 5ae03a8..1392d1f 100644 --- a/client-ws.go +++ b/client-ws.go @@ -62,6 +62,8 @@ func startClientServerConn(host string, port int, identity *Identity, chServerSt // Lock #TODO: use a channel to escape for { + // Removes 100% CPU warning - but this should really be restructured + time.Sleep(10 * time.Second) } } diff --git a/host-panel.go b/host-panel.go index 2c3df2f..bdb5c56 100644 --- a/host-panel.go +++ b/host-panel.go @@ -95,6 +95,8 @@ func (hp *HostPanel) wsWebOperator() func(http.ResponseWriter, *http.Request) { // #TODO: handle exit for { + // Removes 100% CPU warning - but this should really be restructured + time.Sleep(10 * time.Second) } }) if err != nil { diff --git a/local-jukebox.go b/local-jukebox.go index 521930b..5c40ab1 100644 --- a/local-jukebox.go +++ b/local-jukebox.go @@ -28,16 +28,14 @@ func startLocalJukebox(playlist *Playlist, playtime time.Duration) error { go func() { for { - select { - case msg, ok := <-ch: - if ok { - switch msg.Type { - case "tic-state": - err = m.Tic.WriteImportCode(msg.TicState.State) - if err != nil { - // #TODO: soften! - log.Fatal(err) - } + msg, ok := <-ch + if ok { + switch msg.Type { + case "tic-state": + err = m.Tic.WriteImportCode(msg.TicState.State) + if err != nil { + // #TODO: soften! + log.Fatal(err) } } } @@ -46,5 +44,7 @@ func startLocalJukebox(playlist *Playlist, playtime time.Duration) error { j.start() for { + // Removes 100% CPU warning - but this should really be restructured + time.Sleep(10 * time.Second) } } diff --git a/nusan.go b/nusan.go index fd5da4a..e4e5d28 100644 --- a/nusan.go +++ b/nusan.go @@ -54,9 +54,9 @@ func wsNusan(nl NusanLauncher) func(http.ResponseWriter, *http.Request) { // #TODO: handle exit for { + // Removes 100% CPU warning - but this should really be restructured + time.Sleep(10 * time.Second) } - - return nil }) if err != nil { log.Print("ERR upgrade:", err) @@ -68,6 +68,8 @@ func wsNusan(nl NusanLauncher) func(http.ResponseWriter, *http.Request) { // #TODO: Is this used? func (nl *NusanLauncher) nusanWsOperatorRead() { for { + // Removes 100% CPU warning - but this should really be restructured + time.Sleep(10 * time.Second) /* var msg interface{} err := nl.conn.ReadJSON(&msg) diff --git a/playlist/maths-af.json b/playlist/maths-af.json index 8bed6de..2234017 100644 --- a/playlist/maths-af.json +++ b/playlist/maths-af.json @@ -1,14 +1,11 @@ { "title": "Maths A.F. Playlist", "items": [{ - "location": "https://livecode.demozoo.org/shader_file_sources/2023_09_04_byte_jam_monday_night_bytes/alia.lua", - "playtime": 3 + "location": "https://livecode.demozoo.org/shader_file_sources/2023_09_04_byte_jam_monday_night_bytes/alia.lua" }, { - "location": "https://livecode.demozoo.org/shader_file_sources/2023_06_12_byte_jam_monday_night_bytes/gasman.lua", - "playtime": 30 + "location": "https://livecode.demozoo.org/shader_file_sources/2023_06_12_byte_jam_monday_night_bytes/gasman.lua" }, { - "location": "https://livecode.demozoo.org/shader_file_sources/2022_12_05_byte_jam_fieldfx_casual/gasman.lua", - "playtime": 3 + "location": "https://livecode.demozoo.org/shader_file_sources/2022_12_05_byte_jam_fieldfx_casual/gasman.lua" }, { "location": "https://livecode.demozoo.org/shader_file_sources/2023_07_28_byte_jam_evoke/superogue_jam.lua" }] diff --git a/playlist_lcdz.go b/playlist_lcdz.go index fbd5110..4518fdb 100644 --- a/playlist_lcdz.go +++ b/playlist_lcdz.go @@ -22,6 +22,9 @@ func NewPlaylistLCDZ() (*Playlist, error) { } doc, err := html.Parse(resp.Body) + if err != nil { + log.Fatal(err) + } p := NewPlaylist() links := findAllLuaLinks(doc) diff --git a/playlist_zip.go b/playlist_zip.go index 78da5a9..a6a4c2b 100644 --- a/playlist_zip.go +++ b/playlist_zip.go @@ -60,7 +60,7 @@ func NewPlaylistFromZip(zipFilename string) (*Playlist, error) { for key, item := range playlist.items { codeData, ok := files[item.location] if !ok { - return nil, fmt.Errorf("File not found (%s)", item.location) + return nil, fmt.Errorf("file not found (%s)", item.location) } playlist.items[key].code = codeData From 09ff9475c9a83ada4e5581c715ef92e527bb75d8 Mon Sep 17 00:00:00 2001 From: James / creativenucleus Date: Mon, 6 Nov 2023 00:09:55 +0000 Subject: [PATCH 07/11] Cleanup readme --- README.md | 14 +++++++++----- playlist/scenes.json | 2 +- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 6528c9f..772c94b 100644 --- a/README.md +++ b/README.md @@ -42,17 +42,17 @@ Make sure you place it in a folder of its own. It will create a subfolder to hol Default (no arguments) mode will launch into jukebox mode, playing random Bytejams from LCDZ. It can be provided with a JSON file playlist (from remote and local) or .zip file. -JSON file should be formated like +The JSON file format looks like: ```JSON { "title": "TIC-80 selected works", - "description": "(with FFT)", + "description": "(with FFT)", // (optional) "items": [ { "location": "https://livecode.demozoo.org/shader_file_sources/2023_05_15_byte_jam_monday_night_bytes/nusan.lua", - "author": "Nusan", - "description": "FieldFX Byte Jam - 15/05/2023", - "playtime": 30 + "author": "Nusan", // (optional) + "description": "FieldFX Byte Jam - 15/05/2023", // (optional) + "playtime": 30 // (optional) } ] } @@ -161,6 +161,10 @@ For the previous work ByteJammer builds on, testing, good-will, and support: Aldroid, Gasman, Lex Bailey, Mantratronic, NesBox, NuSan, PS, Raccoon Violet, Superogue, Totetmatt. +Additional development: + +zeno4ever. + Thanks to those whose work features on the [walkthrough video](https://youtube.com/watch?v=erhyvrGxwZY): Alia, Aldroid, Dave84, Gasman, Lex Bailey, Gigabates, Mantratronic, NuSan, PS, Superogue, Suule, Synesthesia, TôBach. diff --git a/playlist/scenes.json b/playlist/scenes.json index c8aa757..e3a767a 100644 --- a/playlist/scenes.json +++ b/playlist/scenes.json @@ -31,7 +31,7 @@ },{ "location": "https://livecode.demozoo.org/shader_file_sources/2023_09_18_byte_jam_monday_night_bytes/suule.lua" },{ - "location": "https://livecode.demozoo.org/shader_file_sources/2023_08_28_byte_jam_monday_night_bytes/2023_08_28_jtruk.lua" + "location": "https://livecode.demozoo.org/shader_file_sources/2023_08_28_byte_jam_monday_night_bytes/2023_08_28_jtruk.lua" },{ "location": "https://livecode.demozoo.org/shader_file_sources/2022_12_05_byte_jam_fieldfx_casual/tobach.lua" }] From 49dde8bec67c610807f799657d43fa5eb825c7a9 Mon Sep 17 00:00:00 2001 From: James / creativenucleus Date: Mon, 6 Nov 2023 00:14:39 +0000 Subject: [PATCH 08/11] Fixed isRunning flag bug --- machines/tic_state.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/machines/tic_state.go b/machines/tic_state.go index cadc616..d0e7760 100644 --- a/machines/tic_state.go +++ b/machines/tic_state.go @@ -37,7 +37,7 @@ func MakeTicStateFromExportData(data []byte) (*TicState, error) { r := regexp.MustCompile(`(?s)^-- pos: (\d+),(\d+)\n(.*)$`) matches := r.FindStringSubmatch(string(data)) - ts.IsRunning = matches[1] == "0" && matches[1] == "0" + ts.IsRunning = matches[1] == "0" && matches[2] == "0" if !ts.IsRunning { var err error ts.CursorX, err = strconv.Atoi(matches[1]) From 3a65cd9841b5b457742c02ba517117d93ba67135 Mon Sep 17 00:00:00 2001 From: James / creativenucleus Date: Tue, 7 Nov 2023 21:05:19 +0000 Subject: [PATCH 09/11] identify works (with non-critical issues) --- client-ws.go | 3 +- embed/web/server/operator.html | 2 +- local-jukebox.go | 2 +- machines/machines.go | 10 ++++-- machines/names.go | 1 - machines/tic.go | 64 ++++++++++++++++++++++++++++++---- server/session-conn.go | 7 ++-- server/session.go | 57 +++++++++++++++++++++++++----- 8 files changed, 121 insertions(+), 25 deletions(-) diff --git a/client-ws.go b/client-ws.go index 1392d1f..97a069f 100644 --- a/client-ws.go +++ b/client-ws.go @@ -95,7 +95,7 @@ func (cws *ClientWS) clientWsReader(tic *machines.Tic, identity *Identity) error cws.handleChallengeRequest(msg.ChallengeRequest.Challenge, identity) case "tic-state": - tic.WriteImportCode(msg.TicState.State) + tic.WriteImportCode(msg.TicState.State, true) } } } @@ -106,7 +106,6 @@ func (cws *ClientWS) handleChallengeRequest(challenge string, identity *Identity return err } - fmt.Printf("%x", data) signed, err := identity.Crypto.Sign(data) if err != nil { return err diff --git a/embed/web/server/operator.html b/embed/web/server/operator.html index 909a1e9..109bf54 100644 --- a/embed/web/server/operator.html +++ b/embed/web/server/operator.html @@ -320,7 +320,7 @@

Machines

  • TIC (playlist: LCDZ)
  • - + diff --git a/local-jukebox.go b/local-jukebox.go index 5c40ab1..293a16a 100644 --- a/local-jukebox.go +++ b/local-jukebox.go @@ -32,7 +32,7 @@ func startLocalJukebox(playlist *Playlist, playtime time.Duration) error { if ok { switch msg.Type { case "tic-state": - err = m.Tic.WriteImportCode(msg.TicState.State) + err = m.Tic.WriteImportCode(msg.TicState.State, true) if err != nil { // #TODO: soften! log.Fatal(err) diff --git a/machines/machines.go b/machines/machines.go index 51e2aab..7ff4c9f 100644 --- a/machines/machines.go +++ b/machines/machines.go @@ -8,6 +8,10 @@ import ( "github.com/google/uuid" ) +const ( + PlatformTIC80 = "TIC-80" +) + type Machine struct { MachineName string Platform string @@ -37,9 +41,10 @@ func LaunchMachine(platform string, hasImport bool, hasExport bool, isServer boo var err error switch m.Platform { - case "TIC-80": + case PlatformTIC80: slug := fmt.Sprint(rand.Intn(100000000)) - m.Tic, err = newTic(slug, hasImport, hasExport, isServer) + chClosedErr := make(chan error) + m.Tic, err = newTic(slug, hasImport, hasExport, isServer, chClosedErr) if err != nil { return nil, err } @@ -49,6 +54,7 @@ func LaunchMachine(platform string, hasImport bool, hasExport bool, isServer boo } MACHINES = append(MACHINES, &m) + fmt.Printf("%v\n", m) return &m, nil } diff --git a/machines/names.go b/machines/names.go index ceee92c..000a73d 100644 --- a/machines/names.go +++ b/machines/names.go @@ -16,7 +16,6 @@ func GetFunName(index int) string { "Citrus Lump", "Dusk Pustules", "Heroine's Tear", - "Pocked Airhead", "Crimson Banquet", "Delectable Bouquet", "Lesser Mock Bottom", diff --git a/machines/tic.go b/machines/tic.go index 5202e30..8070ad0 100644 --- a/machines/tic.go +++ b/machines/tic.go @@ -1,6 +1,7 @@ package machines import ( + "errors" "fmt" "log" "os" @@ -17,10 +18,17 @@ import ( type Tic struct { cmd *exec.Cmd ticFilename string - // Add latestImport - for server to read + + latestImport TicState // #TODO: propagate this to the server panel importFullpath string + // Add latestExport - for server to read exportFullpath string + + // This receives nil for normal shutdown (i.e. by TIC exit, user clicking the close button etc) + chClosedErr chan error + + codeOverride bool } func (t *Tic) GetExportFullpath() string { @@ -37,14 +45,14 @@ func (t *Tic) GetProcessID() int { } */ -func newTic(slug string, hasImportFile bool, hasExportFile bool, isServer bool /*, broadcaster *NusanLauncher*/) (*Tic, error) { - tic := Tic{} +func newTic(slug string, hasImportFile bool, hasExportFile bool, isServer bool, chClosedError chan error /*, broadcaster *NusanLauncher*/) (*Tic, error) { + tic := Tic{ + chClosedErr: chClosedError, + } args := []string{ "--skip", } - fmt.Println(slug) - exchangefileBasePath := fmt.Sprintf("%s_temp", config.WORK_DIR) err := util.EnsurePathExists(exchangefileBasePath, os.ModePerm) if err != nil { @@ -107,9 +115,11 @@ func newTic(slug string, hasImportFile bool, hasExportFile bool, isServer bool / // use goroutine waiting, manage process // this is important, otherwise the process becomes in S mode + // This may be error or nil go func() { err = tic.cmd.Wait() fmt.Printf("TIC (%d) finished with error: %v", tic.cmd.Process.Pid, err) + tic.chClosedErr <- err // #TODO: cleanup }() /* @@ -150,11 +160,53 @@ func (t *Tic) shutdown() { } } -func (t Tic) WriteImportCode(ts TicState) error { +// This pushes the supplied code to this TIC and prevents regular import for the specified duration +// 1) Grabs the current TIC state to holding +// 2) Writes the override to the import file +func (t *Tic) SetCodeOverride(tsOverride TicState, d time.Duration) error { + // We ought to handle when someone sets multiple concurrent overrides - for the moment, just block! + if t.codeOverride { + return errors.New("we already have a code override - request ignored") + } + + err := t.WriteImportCode(tsOverride, false) + if err != nil { + fmt.Println(err) + return err + } + + t.codeOverride = true + + // Remove the override struct when the timer expires + timer := time.NewTimer(d) + go func() error { // (NB error ignored) + <-timer.C + err := t.WriteImportCode(t.latestImport, true) + if err != nil { + return err + } + + t.codeOverride = false + return nil + }() + + return nil +} + +// If we have overide code, then put the supplied update in a placeholder, ready for when the override completes +func (t *Tic) WriteImportCode(ts TicState, saveAsLatest bool) error { if t.importFullpath == "" { log.Fatal("Tried to import code - but file is not set up") } + if saveAsLatest { + t.latestImport = ts + } + + if t.codeOverride { + return nil + } + data, err := ts.MakeDataToImport() if err != nil { return err diff --git a/server/session-conn.go b/server/session-conn.go index a87c212..429558a 100644 --- a/server/session-conn.go +++ b/server/session-conn.go @@ -9,7 +9,6 @@ import ( "github.com/creativenucleus/bytejammer/comms" "github.com/creativenucleus/bytejammer/crypto" - "github.com/creativenucleus/bytejammer/embed" "github.com/creativenucleus/bytejammer/machines" "github.com/creativenucleus/bytejammer/util" "github.com/google/uuid" @@ -157,7 +156,7 @@ func (jc *SessionConn) runServerWsConnRead(js *Session) { } */ - err = machine.Tic.WriteImportCode(ts) + err = machine.Tic.WriteImportCode(ts, true) if err != nil { js.chLog <- fmt.Sprintln("read:", err) break @@ -178,7 +177,8 @@ func (jc *SessionConn) runServerWsConnWrite(js *Session) { } } -// TODO: Handle error +// #TODO: Adapt this to send code to remote machines +/* func (js *SessionConn) sendMachineNameCode(machineName string) error { ts := machines.MakeTicStateRunning(embed.LuaClient) code := machines.CodeReplace(ts.GetCode(), map[string]string{ @@ -193,6 +193,7 @@ func (js *SessionConn) sendMachineNameCode(machineName string) error { err := js.sendData(msg) return err } +*/ func (jc *SessionConn) sendData(data interface{}) error { jc.wsMutex.Lock() diff --git a/server/session.go b/server/session.go index 0a6a632..a901984 100644 --- a/server/session.go +++ b/server/session.go @@ -10,6 +10,7 @@ import ( "github.com/creativenucleus/bytejammer/comms" "github.com/creativenucleus/bytejammer/config" + "github.com/creativenucleus/bytejammer/embed" "github.com/creativenucleus/bytejammer/machines" "github.com/creativenucleus/bytejammer/util" "github.com/google/uuid" @@ -270,24 +271,62 @@ func (js *Session) IdentifyMachines() { for _, c := range js.switchboard.conns { m := js.switchboard.getMachineForConn(c) if m != nil { - err := c.sendMachineNameCode(m.MachineName) - if err != nil { - js.chLog <- fmt.Sprintln("ERR write:", err) - } + fmt.Printf("Machine platform: %s\n", m.Platform) + if m.Platform == machines.PlatformTIC80 { + jammerDisplayName := "-" + conn := js.switchboard.getConnForMachine(m) + if conn != nil { + jammerDisplayName = conn.identity.displayName + } + ts := machines.MakeTicStateRunning(embed.LuaClient) + code := machines.CodeReplace(ts.GetCode(), map[string]string{ + "CLIENT_ID": m.MachineName, + "DISPLAY_NAME": jammerDisplayName, + }) + ts.SetCode(code) + + err := m.Tic.SetCodeOverride(ts, 10*time.Second) + if err != nil { + js.chLog <- fmt.Sprintln("ERR write:", err) + } + } count++ } } - js.chLog <- fmt.Sprintf("Identification sent to %d machines for 30 seconds", count) + js.chLog <- fmt.Sprintf("Identification sent to %d machines for 10 seconds", count) } func (js *Session) CloseMachine(data comms.DataCloseMachine) { - // #TODO: unlink and unregister! + // #TODO: This is quite verbose. Maybe the switchboard interface should be simpler? + fmt.Printf("Closeing Machine: %s\n", data.Uuid) + + machineUuid, err := uuid.Parse(data.Uuid) + if err != nil { + js.chLog <- fmt.Sprintf("ERR close machine: %s", err) + return + } + + machine := js.switchboard.getMachine(machineUuid) + if machine == nil { + js.chLog <- "ERR close machine: could not get machine" + return + } + + conn := js.switchboard.getConnForMachine(machine) + if conn != nil { + err = js.switchboard.unlinkMachineFromConn(machineUuid, conn.connUuid) + if err != nil { + js.chLog <- fmt.Sprintf("ERR close machine: %s", err) + return + } + } + + js.switchboard.unregisterMachine(machine) - fmt.Printf("CLOSE: %s\n", data.Uuid) - err := machines.ShutdownMachine(data.Uuid) + err = machines.ShutdownMachine(machineUuid.String()) if err != nil { - js.chLog <- fmt.Sprintf("ERR shutdown: %s", err) + js.chLog <- fmt.Sprintf("ERR close machine: %s", err) return } From 0edaa69a33a5aa6a62953d4f499f42f3075613f1 Mon Sep 17 00:00:00 2001 From: James / creativenucleus Date: Tue, 7 Nov 2023 21:45:02 +0000 Subject: [PATCH 10/11] Updated server panel now hides sections --- client-panel.go | 24 ++++++++++- comms/msg.go | 1 + embed/web/client/index.html | 39 ++++++++++++----- embed/web/server/operator.html | 79 +++++++++++++++++++++++----------- host-panel.go | 1 + server/session.go | 6 ++- 6 files changed, 111 insertions(+), 39 deletions(-) diff --git a/client-panel.go b/client-panel.go index af2eb76..d4185c7 100644 --- a/client-panel.go +++ b/client-panel.go @@ -31,6 +31,7 @@ type ClientPanel struct { chSendClientStatus chan comms.DataClientStatus wsClient *websocket.Conn wsMutex sync.Mutex + chLog chan string } func startClientPanel(port int) error { @@ -52,12 +53,22 @@ func startClientPanel(port int) error { cp := ClientPanel{ chSendClientStatus: make(chan comms.DataClientStatus), + chLog: make(chan string), } + + go func() { + for { + logMsg := <-cp.chLog + cp.sendLog(logMsg) + } + }() + http.HandleFunc(fmt.Sprintf("/%s", session), cp.webClientIndex) http.HandleFunc(fmt.Sprintf("/%s/api/identity.json", session), cp.webClientApiIdentityJSON) http.HandleFunc(fmt.Sprintf("/%s/api/join-server.json", session), cp.webClientApiJoinServerJSON) http.HandleFunc(fmt.Sprintf("/%s/ws-client", session), cp.wsWebClient()) - if err := webServer.ListenAndServe(); err != nil { + err = webServer.ListenAndServe() + if err != nil { return err } @@ -113,6 +124,8 @@ func (cp *ClientPanel) webClientApiIdentityJSON(w http.ResponseWriter, r *http.R func (cp *ClientPanel) webClientApiJoinServerJSON(w http.ResponseWriter, r *http.Request) { switch r.Method { case "POST": + cp.chLog <- "Request: Join Server" + cp.chSendClientStatus <- comms.DataClientStatus{IsConnected: false} // #TODO: Cleaner way to do this? @@ -212,6 +225,15 @@ func (cp *ClientPanel) wsWrite() { } } +func (cp *ClientPanel) sendLog(message string) { + msg := comms.Msg{Type: "log", Log: comms.DataLog{Msg: message}} + + err := cp.sendData(&msg) + if err != nil { + log.Println("read:", err) + } +} + func (cp *ClientPanel) sendData(data interface{}) error { cp.wsMutex.Lock() defer cp.wsMutex.Unlock() diff --git a/comms/msg.go b/comms/msg.go index 55624ef..bc886cf 100644 --- a/comms/msg.go +++ b/comms/msg.go @@ -45,6 +45,7 @@ type DataChallengeResponse struct { } type DataSessionStatus struct { + Port int Clients []struct { Uuid string DisplayName string diff --git a/embed/web/client/index.html b/embed/web/client/index.html index 67a095e..b3c55ec 100644 --- a/embed/web/client/index.html +++ b/embed/web/client/index.html @@ -17,7 +17,7 @@ const ws = new BjmrWebSocket('{{session_key}}') const ajax = new BjmrAjax('{{session_key}}') - let IDENTITIES = {}; + let IDENTITIES = null; const refreshViewIdentities = () => { const elSelect = document.getElementById("identity-id"); @@ -40,10 +40,27 @@ if (res.ok) { IDENTITIES = res.data; refreshViewIdentities(); + refreshVisibility(); + } + } + + const refreshVisibility = () => { + if (IDENTITIES===null) { + // Not yet initialised + document.getElementById("section-create-identity").style.display = "none"; + document.getElementById("section-join-server").style.display = "none"; + } else if (Object.keys(IDENTITIES).length == 0) { + // Currently, only one identity is supported for clients + document.getElementById("section-create-identity").style.display = "block"; + document.getElementById("section-join-server").style.display = "none"; + } else { + document.getElementById("section-create-identity").style.display = "none"; + document.getElementById("section-join-server").style.display = "block"; } } window.onload = () => { + refreshVisibility(); fetchIdentites(); document.getElementById("create-identity").addEventListener("submit", (e) => { @@ -113,7 +130,7 @@ break; case "log": - handleMsgLog(msg.data); + handleMsgLog(msg.log); break; default: @@ -128,7 +145,7 @@

    ByteJammer icon ByteJammer

    -
    +

    Create an Identity

    @@ -152,7 +169,7 @@

    Create an Identity

    -
    +

    Join a Server

    @@ -211,14 +228,14 @@

    Join a Server

    +
    -
    -
    -

    Log

    -
    -
    -
    -
    +
    +
    +

    Log

    +
    +
    +
    diff --git a/embed/web/server/operator.html b/embed/web/server/operator.html index 109bf54..b616edf 100644 --- a/embed/web/server/operator.html +++ b/embed/web/server/operator.html @@ -17,9 +17,20 @@ const ws = new BjmrWebSocket('{{session_key}}') const ajax = new BjmrAjax('{{session_key}}') + let SERVER = null; let CLIENTS = {}; let MACHINES = {}; + const refreshVisibility = () => { + if (!SERVER) { + document.getElementById("section-create-server").style.display = "block"; + document.getElementById("section-server-info").style.display = "none"; + } else { + document.getElementById("section-create-server").style.display = "none"; + document.getElementById("section-server-info").style.display = "block"; + } + } + const onClickConnectMachine = (machineUuid, clientUuid) => { if (!ws.isOpen()) { return false; @@ -69,7 +80,13 @@ }; const handleMsgServerStatus = (data) => { - let html = ''; + SERVER = true; // (echo settings) + refreshVisibility(); + + let html = `

    Port: ${data.Port}

    `; + document.getElementById("server-details").innerHTML = html; + + html = ''; if(!data.Clients || data.Clients.length == 0) { html = "
    No clients connected
    "; } else { @@ -159,6 +176,7 @@ } window.onload = () => { + refreshVisibility(); const conn = ws.open("ws://" + document.location.host + "/{{session_key}}/ws-operator"); if(!conn) { setWsLocalStatusText('error', "Your browser does not support WebSockets"); @@ -257,7 +275,7 @@

    ByteJammer icon ByteJammer

    -
    +

    Server

    @@ -294,33 +312,44 @@

    Server

    -
    -
    -

    Jammers

    -
    -
    -
    +
    +
    +
    +

    Server

    +
    +
    +
    +
    -
    -
    -
    -

    Machines

    -
    -
    -
    +
    +
    +

    Jammers

    +
    +
    +
    +
    -