Skip to content

Commit a2afc45

Browse files
committed
Initial Alpha for GroovyMAME commands.
1 parent 5a4f97c commit a2afc45

17 files changed

Lines changed: 970 additions & 86 deletions

File tree

.gitignore

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,3 +20,9 @@
2020
# Go workspace file
2121
go.work
2222
go.work.sum
23+
24+
cmd/cmdtest
25+
cmd/romparser
26+
dev-notes.txt
27+
config.json
28+
printargs.sh

README.md

Lines changed: 79 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -1,68 +1,112 @@
11
# GroovyMiSTerHTTP
22
An HTTP client to allow remote GroovyMAME execution from MiSTer
33

4-
## Draft Data Contracts
4+
Actually it's going to be done over UDP but I'm not going to rename/readdress everything yet.
55

6-
### Groovy MiSTer
6+
## Alpha Setup Guide
7+
Assumes Go is installed, and I am using WSL2 on windows so my pathing and UDP is wonky but works.
8+
9+
```
10+
git clone git@github.com:BossRighteous/GroovyMiSTerHTTP.git
11+
cd GroovyMiSTerHTTP
12+
cp ./spec/GmcServerConfig.json ./config.json
13+
// Edit JSON for mame bin paths and MiSTer IP
14+
go run cmd/groovymisterhttp/main.go "/path/to/GroovyMiSTerHTTP/config.json"
15+
```
16+
17+
In new terminal screen, send UDP commands to server port
18+
```
19+
echo -n '{"cmd":"mame","vars":{"MACHINE_NAME":"circus"}}' >/dev/udp/localhost/32105
20+
21+
echo -n '{"cmd":"unload"}' >/dev/udp/localhost/32105
22+
```
23+
24+
### Groovy MiSTer Command (GMC) spec
725

826
The Groovy MiSTer core does not currently support file browsing as there are no direct roms to load.
927
The following data type is a proposed file structure the Groovy core can open as a rom-like and perform
10-
an HTTP RPC command against.
28+
UDP RPC commands against.
1129

1230
```
1331
Filename:
14-
{Text Title}.{GroovyMisterCommand}
15-
Contents: UTF-8 Text
16-
{UtilKeyword | MameSlug}
32+
{Human ReadableText Title}.gmc
33+
Contents: UTF-8 JSON Text
34+
{
35+
"cmd": "mame",
36+
"vars": {
37+
"MACHINE_NAME": "circus"
38+
}
39+
}
40+
```
1741

18-
Examples:
19-
// Game Slug
20-
Hanabi de Doon! - Don-chan Puzzle.gmc
21-
doncdoon
42+
When the core opens the GMC file, it's contents will be streamed over UDP to the server.
43+
It is assumed this will be plain text, but the core will send the binary data as it. It is up
44+
to the server software to read and handle appropriately.
2245

23-
// Util Keywords
24-
Test Connection.gmc
25-
connect
46+
This configuration is managed from the server bin via a settings JSON
47+
```
48+
{
49+
"mister_host": "192.168.1.255",
50+
"commands": [
51+
{
52+
"cmd": "mame",
53+
"work_dir": "/path/to/groovymame",
54+
"exec_bin": "./mame",
55+
"exec_args": ["${MACHINE_NAME}"]
56+
}
57+
]
58+
}
59+
```
2660

27-
Unload Rom.gmc
28-
unload
61+
This allows the server to easily control it's command whitelist and intented variable replacements in args.
62+
Since the command will be executed outside of a shell context ENV and command line variable substitution isn't possible.
63+
Arg/Var replacement is done by the server. This also makes it much more difficult to inject escape sequences.
2964

30-
Kill Server Process.gmc
31-
kill
65+
```
66+
/path/to/mybin positional -tf --foo="complex"
3267
33-
Reboot Host Machine.gmc
34-
reboot
68+
... might become
69+
"cmd": "mycmd"
70+
"work_dir": "/path/to/",
71+
"exec_bin": "./mybin",
72+
"exec_args": ["positional", "-tf", "--foo=\"${FOO_VAL}\""]
3573
36-
Shutdown Host Machine.gmc
37-
shutdown
74+
... and be executed by
75+
{"cmd":"mycmd", "vars":{"FOO_VAL":"custom"}}
3876
```
3977

40-
This is a minimal amount of information and should provide just enough data for the server to execute tasks without security concerns.
78+
GroovyMiSTer will know of the server by way of init beacon. Upon server boot, and every n seconds therafter,
79+
an init handshake will be sent to the Groovy core over UDP until the Core acknoledges.
80+
81+
This enables the MiSTer to reply even in cases the server machine amy have a dynamic IP.
82+
Your GroovyMister enabled emulator will need to have it's own configuration including host/IP.
83+
84+
The server will attempt to run asyncronous (long running processes) until exit or error.
85+
Any running processes will be terminated on receipt of the next GMC command over UDP.
4186

42-
I initially considered a {command, args} schema but limiting args to single 'word' will make verification much easier.
87+
Messages including a stderr tail will be displayed on the MiSTer. This is under development for messaging data sources.
4388

4489
#### Groovy MiSTer UX
4590

91+
psakhis has conceptually reviewed the following and may go forward with something like the following
92+
4693
Similar to many other cores a load rom menu setting may be implemented:
4794
```
48-
Load Remote GroovyMAME *.gmc
95+
Load Remote Command *.gmc
4996
```
5097

5198
This will open a file browser interface, pointing to rom-like directory
5299
```
53-
/media/fat/games/GroovyMAME/**.gmc
100+
/media/fat/games/Groovy/**.gmc
54101
```
55102

56103
Upon GMC file selection the core may
57-
- load the contents into string/chars CMD_STR
58-
- Check new ini/env key GROOVY_MAME_HOST (Example: "192.168.1.128")
59-
- Execute syncronous HTTP POST
60-
- Address: "http://{GROOVY_MAME_HOST}:32105/cmd"
61-
- Body: "{CMD_STR}"
62-
- For 200/ok response, close OSD, wait for game broadcast from PC server
63-
- ? For non-200 response, display error message from body or http status code
104+
- load binary contents up to 1024 bytes
105+
- Send bytes as UDP packet to acked Server socket
106+
- Server will manage process state and errors via text blitting back to the MiSTer
107+
108+
#### File Browsing
64109

65-
This could trivially be a GET request with query parameters as well. Depends on what the Core's build would best support.
66110

67111
#### File Browsing
68112
I am making some assumptions here that there is a file browsing interface module the core
@@ -100,7 +144,7 @@ To keep the Core's command logic simplified, companion server-side scripts can b
100144
...
101145
```
102146

103-
I've noted that .zip files are browseable as filesystem, so a single portable zip may reduce FS block fill and make the build/transfer simpler.
147+
I've noted that .zip files are browseable as filesystem, so a single portable zip may reduce FS block fill and make the build/transfer simpler. I don't know the limits to file density in a zip but could test this.
104148

105149
This would allow the server binary to run a CLI command routine that
106150
- Asks for category/filter preferences
@@ -110,37 +154,4 @@ This would allow the server binary to run a CLI command routine that
110154
- Dupes into category hierarchies selected.
111155
- zips directory
112156

113-
This could then be trivially SCPed and extracted.
114-
115-
This could also be exposed as a special HTTP endpoint for a MiSTer Script to process against the remote
116-
117-
### Groovy MiSTer HTTP Server
118-
119-
This is a binary CLI app that will live on the GroovyMAME host machine.
120-
121-
The IP address or hostname of the server machine should be defined in the MiSTer ini for GroovyMiSTer settings.
122-
123-
The server binary itself should be placed in the GroovyMAME application directory for easier cross-platform build pathing. May include .ini configuration for the server's paths and ports to decouple.
124-
125-
On execution the server will listen generically to any host, but specifically on port 32105. This port allows for additional GroovyMiSTer UDP expansion but occupies similar range.
126-
127-
The server listens for commands on the ` POST /cmd` endpoint. It expects basic single word text data as defined and generated for the .gmc spec.
128-
Based on the command, a single subprocess may be terminated or replaced.
129-
130-
For example: a command will
131-
- Compare POST body command to known UtilKeywords
132-
- Execute Utility routine on match
133-
- Else pattern match for MAME game slug
134-
- Check GroovyMAME executable for game slug verification
135-
- Fail if not verifiable
136-
- Check for existing GroovyMAME subprocess, exit if operational
137-
- Attempt to create new GroovyMAME subprocess based on game slug
138-
- Runs as goroutine/async
139-
- Else fail
140-
- Return HTTP response
141-
142-
The CLI app must be executed/ran on the Server before the GroovyMiSTer core can access it.
143-
144-
For a delegated mini-pc this could be run on startup. The server process is extremely light weight and can safely run in the background without significant resource drain.
145-
146-
PC, OSX, and Linux builds are possible from the single go repository.
157+
This could then be trivially SCPed and extracted.

cmd/groovymisterhttp/main.go

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
package main
2+
3+
import (
4+
"fmt"
5+
"log"
6+
"os"
7+
8+
"github.com/BossRighteous/GroovyMiSTerHTTP/pkg/command"
9+
"github.com/BossRighteous/GroovyMiSTerHTTP/pkg/display"
10+
"github.com/BossRighteous/GroovyMiSTerHTTP/pkg/server"
11+
)
12+
13+
func main() {
14+
configPath := "./config.json"
15+
if len(os.Args) > 1 {
16+
configPath = os.Args[1]
17+
}
18+
19+
config, err := command.LoadConfigFromPath(configPath)
20+
if err != nil {
21+
log.Fatal("Config could not be loaded from path argument")
22+
}
23+
24+
misterHost := config.MisterHost
25+
cmdChan := make(chan command.GroovyMiSTerCommand)
26+
27+
cmdr := &command.CommandRunner{
28+
Config: config,
29+
ResultChan: make(chan command.RunResult),
30+
}
31+
32+
server.StartUdpClient(misterHost, cmdChan)
33+
34+
disp := display.NewMiSTerDisplay(misterHost)
35+
36+
for {
37+
select {
38+
case res := <-cmdr.ResultChan:
39+
fmt.Println(res)
40+
if res.BlitMessage {
41+
if len(res.MessageLines) > 0 {
42+
disp.BlitText(res.MessageLines)
43+
} else if res.Message != "" {
44+
disp.BlitText(display.ReflowText(res.Message))
45+
} else {
46+
disp.BlitText([]string{"Process Completed"})
47+
}
48+
}
49+
case cmd := <-cmdChan:
50+
disp.SafeClose()
51+
fmt.Println(cmd.Raw)
52+
res := cmdr.Run(cmd)
53+
if res.BlitMessage {
54+
if len(res.MessageLines) > 0 {
55+
disp.BlitText(res.MessageLines)
56+
} else {
57+
disp.BlitText(display.ReflowText(res.Message))
58+
}
59+
}
60+
}
61+
}
62+
}

go.mod

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
module github.com/BossRighteous/GroovyMiSTerHTTP
2+
3+
go 1.18
4+
5+
require (
6+
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect
7+
golang.org/x/image v0.18.0 // indirect
8+
)

go.sum

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g=
2+
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k=
3+
golang.org/x/image v0.18.0 h1:jGzIakQa/ZXI1I0Fxvaa9W7yP25TqT6cHIHn+6CqvSQ=
4+
golang.org/x/image v0.18.0/go.mod h1:4yyo5vMFQjVjUcVk4jEQcU9MGy/rulF5WvUILseCM2E=

pkg/command/config.go

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
package command
2+
3+
import (
4+
"encoding/json"
5+
"os"
6+
)
7+
8+
type GMCConfigCommand struct {
9+
Cmd string `json:"cmd"`
10+
WorkDir string `json:"work_dir"`
11+
ExecBin string `json:"exec_bin"`
12+
ExecArgs []string `json:"exec_args"`
13+
}
14+
15+
type GMCConfig struct {
16+
MisterHost string `json:"mister_host"`
17+
Commands []GMCConfigCommand `json:"commands"`
18+
CmdMap map[string]GMCConfigCommand
19+
}
20+
21+
func LoadConfigFromPath(path string) (*GMCConfig, error) {
22+
var config GMCConfig
23+
dat, err := os.ReadFile(path)
24+
if err != nil {
25+
return nil, err
26+
}
27+
err = json.Unmarshal(dat, &config)
28+
if err != nil {
29+
return nil, err
30+
}
31+
32+
for i := range config.Commands {
33+
cmd := config.Commands[i]
34+
config.CmdMap = make(map[string]GMCConfigCommand)
35+
config.CmdMap[cmd.Cmd] = cmd
36+
}
37+
38+
// Add built in commands
39+
config.CmdMap["unload"] = GMCConfigCommand{
40+
Cmd: "unload",
41+
ExecBin: "echo",
42+
ExecArgs: []string{""},
43+
}
44+
45+
return &config, nil
46+
}

pkg/command/gmc.go

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
package command
2+
3+
import "encoding/json"
4+
5+
type GroovyMiSTerCommand struct {
6+
Cmd string `json:"cmd"`
7+
Vars map[string]string `json:"vars"`
8+
Raw []byte
9+
}
10+
11+
func ParseGMC(cmdBytes []byte) (GroovyMiSTerCommand, error) {
12+
cmd := GroovyMiSTerCommand{
13+
Raw: cmdBytes,
14+
}
15+
err := json.Unmarshal(cmdBytes, &cmd)
16+
return cmd, err
17+
}

0 commit comments

Comments
 (0)