Repos / gorts / 2c01184f92
commit 2c01184f92f946a725119f6eb3263321faaf7a8e
Author: Nhân <hi@imnhan.com>
Date:   Wed Jun 21 20:58:51 2023 +0700

    migrate to netstring IPC
    
    TODO: stop embedding tcl scripts into go binary

diff --git a/main.go b/main.go
index 9b582cc..2dd7465 100644
--- a/main.go
+++ b/main.go
@@ -3,7 +3,6 @@
 import (
 	"bufio"
 	_ "embed"
-	"encoding/base64"
 	"encoding/json"
 	"fmt"
 	"io"
@@ -14,9 +13,9 @@
 	"os/exec"
 	"runtime"
 	"strconv"
-	"strings"
 	"time"
 
+	"go.imnhan.com/gorts/netstring"
 	"go.imnhan.com/gorts/players"
 	"go.imnhan.com/gorts/startgg"
 )
@@ -94,6 +93,7 @@ func startGUI() {
 	fmt.Fprintln(stdin, "initialize")
 
 	scanner := bufio.NewScanner(stdout)
+	scanner.Split(netstring.SplitFunc)
 
 	next := func() string {
 		scanner.Scan()
@@ -102,90 +102,97 @@ func startGUI() {
 		return v
 	}
 
-	respond := func(s string) {
+	respondOld := func(s string) {
 		debug := "<-- " + s
 		if len(debug) > 35 {
 			debug = debug[:35] + "[...]"
 		}
 		println(debug)
-		io.WriteString(stdin, s+"\n")
+		io.WriteString(stdin, netstring.Encode(s))
+	}
+
+	respond := func(ss ...string) {
+		debug := fmt.Sprintf("<-- %v", ss)
+		if len(debug) > 35 {
+			debug = debug[:35] + "[...]"
+		}
+		println(debug)
+		payload := netstring.EncodeN(ss...)
+		io.WriteString(stdin, payload)
 	}
 
 	for scanner.Scan() {
-		req := scanner.Text()
-		println("--> " + req)
-		switch req {
-		case "readscoreboard":
+		req := netstring.DecodeMultiple(scanner.Text())
+		fmt.Printf("--> %v\n", req)
+		switch req[0] {
+		case "geticon":
+			respond(string(gortsPngIcon))
+
+		case "getstartgg":
+			respond(startggInputs.Token, startggInputs.Slug)
+
+		case "getwebport":
+			respond(WebPort)
+
+		case "getcountrycodes":
+			respond(startgg.CountryCodes...)
+
+		case "getscoreboard":
 			// TODO: there must be a more... civilized way.
-			respond(scoreboard.Description)
-			respond(scoreboard.Subtitle)
-			respond(scoreboard.P1name)
-			respond(scoreboard.P1country)
-			respond(strconv.Itoa(scoreboard.P1score))
-			respond(scoreboard.P1team)
-			respond(scoreboard.P2name)
-			respond(scoreboard.P2country)
-			respond(strconv.Itoa(scoreboard.P2score))
-			respond(scoreboard.P2team)
+			respond(
+				scoreboard.Description,
+				scoreboard.Subtitle,
+				scoreboard.P1name,
+				scoreboard.P1country,
+				strconv.Itoa(scoreboard.P1score),
+				scoreboard.P1team,
+				scoreboard.P2name,
+				scoreboard.P2country,
+				strconv.Itoa(scoreboard.P2score),
+				scoreboard.P2team,
+			)
 
 		case "applyscoreboard":
-			scoreboard.Description = next()
-			scoreboard.Subtitle = next()
-			scoreboard.P1name = next()
-			scoreboard.P1country = next()
-			scoreboard.P1score, _ = strconv.Atoi(next())
-			scoreboard.P1team = next()
-			scoreboard.P2name = next()
-			scoreboard.P2country = next()
-			scoreboard.P2score, _ = strconv.Atoi(next())
-			scoreboard.P2team = next()
+			sb := req[1:]
+			scoreboard.Description = sb[0]
+			scoreboard.Subtitle = sb[1]
+			scoreboard.P1name = sb[2]
+			scoreboard.P1country = sb[3]
+			scoreboard.P1score, _ = strconv.Atoi(sb[4])
+			scoreboard.P1team = sb[5]
+			scoreboard.P2name = sb[6]
+			scoreboard.P2country = sb[7]
+			scoreboard.P2score, _ = strconv.Atoi(sb[8])
+			scoreboard.P2team = sb[9]
 			scoreboard.Write()
-
-		case "readplayernames":
-			for _, player := range allplayers {
-				respond(player.Name)
-			}
-			respond("end")
+			respond("ok")
 
 		case "searchplayers":
-			query := strings.TrimSpace(next())
+			query := req[1]
+			var names []string
 
 			if query == "" {
 				for _, p := range allplayers {
-					respond(p.Name)
+					names = append(names, p.Name)
 				}
-				respond("end")
+				respond(names...)
 				break
 			}
 
 			for _, p := range allplayers {
 				if p.MatchesName(query) {
-					respond(p.Name)
+					names = append(names, p.Name)
 				}
 			}
-			respond("end")
+			respond(names...)
 
-		case "fetchplayers":
+		case "fetchplayers": // FIXME
 			startggInputs.Token = next()
 			startggInputs.Slug = next()
 			time.Sleep(3 * time.Second)
-			respond("fetchplayers__resp")
-			respond("All done.")
+			respondOld("fetchplayers__resp")
+			respondOld("All done.")
 			startggInputs.Write(StartggFile)
-
-		case "readwebport":
-			respond(WebPort)
-
-		case "geticon":
-			b64icon := base64.StdEncoding.EncodeToString(gortsPngIcon)
-			respond(b64icon)
-
-		case "getcountrycodes":
-			respond(strings.Join(startgg.CountryCodes, " "))
-
-		case "readstartgg":
-			respond(startggInputs.Token)
-			respond(startggInputs.Slug)
 		}
 	}
 
diff --git a/netstring/netstring.go b/netstring/netstring.go
index 20c2506..be988e6 100644
--- a/netstring/netstring.go
+++ b/netstring/netstring.go
@@ -5,15 +5,25 @@
 package netstring
 
 import (
+	"bufio"
 	"bytes"
 	"fmt"
 	"strconv"
+	"strings"
 )
 
 func Encode(s string) string {
 	return fmt.Sprintf("%d:%s,", len(s), s)
 }
 
+// Encode multiple strings into a nested netstring
+func EncodeN(strings ...string) (ns string) {
+	for _, s := range strings {
+		ns += Encode(s)
+	}
+	return Encode(ns)
+}
+
 // A SplitFunc to be used in a bufio.Scanner
 func SplitFunc(data []byte, atEOF bool) (advance int, token []byte, err error) {
 	colonIndex := bytes.IndexRune(data, ':')
@@ -36,3 +46,16 @@ func SplitFunc(data []byte, atEOF bool) (advance int, token []byte, err error) {
 	// The whole netstring should now be within the buffer
 	return colonIndex + 1 + length + 1, rest[:length], nil
 }
+
+// Decode multiple concatenated netstrings into plain strings.
+// This is NOT a reverse EncodeN() - the input here is not a nested
+// netstring.
+func DecodeMultiple(nstrings string) (results []string) {
+	r := strings.NewReader(nstrings)
+	s := bufio.NewScanner(r)
+	s.Split(SplitFunc)
+	for s.Scan() {
+		results = append(results, s.Text())
+	}
+	return results
+}
diff --git a/tcl/main.tcl b/tcl/main.tcl
index 616a87a..9944ef1 100644
--- a/tcl/main.tcl
+++ b/tcl/main.tcl
@@ -8,6 +8,8 @@ foreach p {stdin stdout stderr} {
     fconfigure $p -translation lf
 }
 
+source tcl/netstring.tcl
+
 package require Tk
 
 wm title . "Overly Repetitive Tedious Software (in Go)"
@@ -164,86 +166,89 @@ grid .n.s.msg -row 3 -column 1 -stick W
 grid columnconfigure .n.s 1 -weight 1
 grid rowconfigure .n.s 1 -pad 5
 
-# The following procs constitute a very simple line-based IPC system where Tcl
-# client talks to Go server via stdin/stdout.
-
 proc initialize {} {
-    seticon
-    setwebport
-    setcountrycodes
-    setstartgg
-    readscoreboard
+    foreach p {stdin stdout} {
+        fconfigure $p -translation binary
+    }
+    loadicon
+    loadstartgg
+    loadwebmsg
+    loadcountrycodes
+    loadscoreboard
+    loadplayernames
+
     setupdiffcheck
-    readplayernames
-    setupplayersuggestion
+    #setupplayersuggestion
 }
 
-proc setstartgg {} {
-    puts "readstartgg"
-    set ::startgg(token) [gets stdin]
-    set ::startgg(slug) [gets stdin]
+# Very simple IPC system where Tcl client talks to Go server via stdin/stdout
+# using netstrings as wire format.
+proc ipc {method args} {
+    set payload [concat $method $args]
+    puts -nonewline [netstrings $payload]
+    flush stdout
+    return [decodenetstrings [readnetstring stdin]]
 }
 
-proc setwebport {} {
-    puts "readwebport"
-    set webport [gets stdin]
-    set ::mainstatus "Point your OBS browser source to http://localhost:${webport}"
+proc loadicon {} {
+    set resp [ipc "geticon"]
+    set iconblob [lindex $resp 0]
+    image create photo applicationIcon -data $iconblob
+    wm iconphoto . -default applicationIcon
 }
 
-proc seticon {} {
-    puts "geticon"
-    set b64data [gets stdin]
-    image create photo applicationIcon -data [
-        binary decode base64 $b64data
-    ]
-    wm iconphoto . -default applicationIcon
+proc loadstartgg {} {
+    set resp [ipc "getstartgg"]
+    set ::startgg(token) [lindex $resp 0]
+    set ::startgg(slug) [lindex $resp 1]
+}
+
+proc loadwebmsg {} {
+    set resp [ipc "getwebport"]
+    set webport [lindex $resp 0]
+    set ::mainstatus "Point your OBS browser source to http://localhost:${webport}"
 }
 
-proc setcountrycodes {} {
-    puts getcountrycodes
-    set countrycodes [gets stdin]
-    .n.m.players.p1country configure -values $countrycodes
-    .n.m.players.p2country configure -values $countrycodes
+proc loadcountrycodes {} {
+    set codes [ipc "getcountrycodes"]
+    .n.m.players.p1country configure -values $codes
+    .n.m.players.p2country configure -values $codes
 }
 
-proc readscoreboard {} {
-    puts "readscoreboard"
-    set ::scoreboard(description) [gets stdin]
-    set ::scoreboard(subtitle) [gets stdin]
-    set ::scoreboard(p1name) [gets stdin]
-    set ::scoreboard(p1country) [gets stdin]
-    set ::scoreboard(p1score) [gets stdin]
-    set ::scoreboard(p1team) [gets stdin]
-    set ::scoreboard(p2name) [gets stdin]
-    set ::scoreboard(p2country) [gets stdin]
-    set ::scoreboard(p2score) [gets stdin]
-    set ::scoreboard(p2team) [gets stdin]
+proc loadscoreboard {} {
+    set sb [ipc "getscoreboard"]
+    set ::scoreboard(description) [lindex $sb 0]
+    set ::scoreboard(subtitle) [lindex $sb 1]
+    set ::scoreboard(p1name) [lindex $sb 2]
+    set ::scoreboard(p1country) [lindex $sb 3]
+    set ::scoreboard(p1score) [lindex $sb 4]
+    set ::scoreboard(p1team) [lindex $sb 5]
+    set ::scoreboard(p2name) [lindex $sb 6]
+    set ::scoreboard(p2country) [lindex $sb 7]
+    set ::scoreboard(p2score) [lindex $sb 8]
+    set ::scoreboard(p2team) [lindex $sb 9]
     update_applied_scoreboard
 }
 
 proc applyscoreboard {} {
-    puts "applyscoreboard"
-    puts $::scoreboard(description)
-    puts $::scoreboard(subtitle)
-    puts $::scoreboard(p1name)
-    puts $::scoreboard(p1country)
-    puts $::scoreboard(p1score)
-    puts $::scoreboard(p1team)
-    puts $::scoreboard(p2name)
-    puts $::scoreboard(p2country)
-    puts $::scoreboard(p2score)
-    puts $::scoreboard(p2team)
+    set sb [ \
+        ipc "applyscoreboard" \
+        $::scoreboard(description) \
+        $::scoreboard(subtitle) \
+        $::scoreboard(p1name) \
+        $::scoreboard(p1country) \
+        $::scoreboard(p1score) \
+        $::scoreboard(p1team) \
+        $::scoreboard(p2name) \
+        $::scoreboard(p2country) \
+        $::scoreboard(p2score) \
+        $::scoreboard(p2team) \
+    ]
     update_applied_scoreboard
 }
 
-proc readplayernames {} {
-    set playernames {}
-    puts "readplayernames"
-    set line [gets stdin]
-    while {$line != "end"} {
-        lappend playernames $line
-        set line [gets stdin]
-    }
+proc loadplayernames {} {
+    set playernames [ipc "searchplayers" ""]
     .n.m.players.p1name configure -values $playernames
     .n.m.players.p2name configure -values $playernames
 }
diff --git a/tcl/netstring.tcl b/tcl/netstring.tcl
index fa510b1..bfb9bb3 100644
--- a/tcl/netstring.tcl
+++ b/tcl/netstring.tcl
@@ -23,3 +23,19 @@ proc readnetstring {chan} {
     read $chan 1; # consume the trailing ","
     return $nstr
 }
+
+# Assumes input is multiple well formed netstrings concatenated.
+# Returns list of decoded values.
+proc decodenetstrings {ns} {
+    set results {}
+    while {$ns != ""} {
+        set colonIdx [string first : $ns]
+        set len [string range $ns 0 [expr { $colonIdx - 1 }]]
+        set startIdx [expr {$colonIdx + 1}]
+        set endIdx [expr {$startIdx + $len - 1}]
+        set str [string range $ns $startIdx $endIdx]
+        lappend results $str
+        set ns [string range $ns [expr {$endIdx + 2}] end];
+    }
+    return $results
+}