mirror of https://github.com/icedream/icecon.git
Rewrite and update the code for the modern world.
parent
3a42381727
commit
9bdc64ae6e
|
@ -0,0 +1,5 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
_ "github.com/icedream/icecon/internal/ui/windows"
|
||||||
|
)
|
|
@ -0,0 +1,105 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/alecthomas/kingpin/v2"
|
||||||
|
"github.com/icedream/icecon/internal/rcon"
|
||||||
|
"github.com/icedream/icecon/internal/ui"
|
||||||
|
|
||||||
|
_ "github.com/icedream/icecon/internal/ui/console"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
flagCommand = kingpin.Flag("command",
|
||||||
|
"Run a one-off command and then exit.").
|
||||||
|
Short('c').String()
|
||||||
|
argAddress = kingpin.Arg("address",
|
||||||
|
"Server IP/hostname and port, written as \"server:port\".")
|
||||||
|
argPassword = kingpin.Arg("password", "The RCON password.")
|
||||||
|
|
||||||
|
password string
|
||||||
|
)
|
||||||
|
|
||||||
|
func usage() {
|
||||||
|
kingpin.Usage()
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
hasGraphicalUI = ui.HasGraphicalUI()
|
||||||
|
flagGui *bool
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
// only provide -gui/-g flag if there is a graphical user interface available
|
||||||
|
if hasGraphicalUI {
|
||||||
|
flagGui = kingpin.
|
||||||
|
Flag("gui", "Run as GUI (runs automatically as GUI if no arguments given, ignored if command flag used)").
|
||||||
|
Short('g').Bool()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
fmt.Println("IceCon - Icedream's RCON Client")
|
||||||
|
fmt.Println("\t\u00A9 2016-2024 Carl Kittelberger/Icedream")
|
||||||
|
fmt.Println()
|
||||||
|
|
||||||
|
argAddressTCP := argAddress.TCP()
|
||||||
|
argPasswordStr := argPassword.String()
|
||||||
|
|
||||||
|
kingpin.Parse()
|
||||||
|
|
||||||
|
// If no arguments, fall back to running the shell
|
||||||
|
wantGui := (*argAddressTCP == nil && *flagCommand == "") || *flagGui
|
||||||
|
|
||||||
|
// Command-line shell doesn't support starting up without arguments
|
||||||
|
// but graphical Windows UI does
|
||||||
|
if !(hasGraphicalUI && wantGui) {
|
||||||
|
argAddress = argAddress.Required()
|
||||||
|
argPassword = argPassword.Required()
|
||||||
|
kingpin.Parse()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize socket
|
||||||
|
rconClient := rcon.NewRconClient()
|
||||||
|
rconClient.InitSocket()
|
||||||
|
defer rconClient.Release()
|
||||||
|
|
||||||
|
// Set target address if given
|
||||||
|
if *argAddressTCP != nil {
|
||||||
|
rconClient.SetSocketAddr((*argAddressTCP).String())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get password
|
||||||
|
password = *argPasswordStr
|
||||||
|
|
||||||
|
// Run one-off command?
|
||||||
|
if *flagCommand != "" {
|
||||||
|
// Send
|
||||||
|
err := rconClient.Send(*flagCommand)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Receive
|
||||||
|
msg, err := rconClient.Receive()
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
switch strings.ToLower(msg.Name) {
|
||||||
|
case "print":
|
||||||
|
fmt.Println(string(msg.Data))
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Which UI should be run?
|
||||||
|
if err := ui.Run(rconClient, wantGui); err != nil {
|
||||||
|
log.Fatal("User interface failed:", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,4 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
//go:generate go run -mod=mod github.com/josephspurrier/goversioninfo/cmd/goversioninfo -manifest "../../rsrc/app.manifest" -icon "../../rsrc/app.ico" -o "rsrc_windows_386.syso"
|
||||||
|
//go:generate go run -mod=mod github.com/josephspurrier/goversioninfo/cmd/goversioninfo -manifest "../../rsrc/app.manifest" -icon "../../rsrc/app.ico" -o "rsrc_windows_amd64.syso" -64
|
10
go.mod
10
go.mod
|
@ -1,12 +1,20 @@
|
||||||
module github.com/icedream/icecon
|
module github.com/icedream/icecon
|
||||||
|
|
||||||
go 1.12
|
go 1.22
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/alecthomas/kingpin/v2 v2.4.0
|
github.com/alecthomas/kingpin/v2 v2.4.0
|
||||||
github.com/icedream/go-q3net v0.1.0
|
github.com/icedream/go-q3net v0.1.0
|
||||||
|
github.com/icedream/ui2walk v0.0.0-20160513005918-6f3bcb07a270
|
||||||
|
github.com/josephspurrier/goversioninfo v1.4.0
|
||||||
github.com/lxn/walk v0.0.0-20210112085537-c389da54e794
|
github.com/lxn/walk v0.0.0-20210112085537-c389da54e794
|
||||||
|
)
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/akavel/rsrc v0.10.2 // indirect
|
||||||
|
github.com/alecthomas/units v0.0.0-20211218093645-b94a6e3cc137 // indirect
|
||||||
github.com/lxn/win v0.0.0-20210218163916-a377121e959e // indirect
|
github.com/lxn/win v0.0.0-20210218163916-a377121e959e // indirect
|
||||||
|
github.com/xhit/go-str2duration/v2 v2.1.0 // indirect
|
||||||
golang.org/x/sys v0.12.0 // indirect
|
golang.org/x/sys v0.12.0 // indirect
|
||||||
gopkg.in/Knetic/govaluate.v3 v3.0.0 // indirect
|
gopkg.in/Knetic/govaluate.v3 v3.0.0 // indirect
|
||||||
)
|
)
|
||||||
|
|
12
go.sum
12
go.sum
|
@ -1,3 +1,5 @@
|
||||||
|
github.com/akavel/rsrc v0.10.2 h1:Zxm8V5eI1hW4gGaYsJQUhxpjkENuG91ki8B4zCrvEsw=
|
||||||
|
github.com/akavel/rsrc v0.10.2/go.mod h1:uLoCtb9J+EyAqh+26kdrTgmzRBFPGOolLWKpdxkKq+c=
|
||||||
github.com/alecthomas/kingpin/v2 v2.4.0 h1:f48lwail6p8zpO1bC4TxtqACaGqHYA22qkHjHpqDjYY=
|
github.com/alecthomas/kingpin/v2 v2.4.0 h1:f48lwail6p8zpO1bC4TxtqACaGqHYA22qkHjHpqDjYY=
|
||||||
github.com/alecthomas/kingpin/v2 v2.4.0/go.mod h1:0gyi0zQnjuFk8xrkNKamJoyUo382HRL7ATRpFZCw6tE=
|
github.com/alecthomas/kingpin/v2 v2.4.0/go.mod h1:0gyi0zQnjuFk8xrkNKamJoyUo382HRL7ATRpFZCw6tE=
|
||||||
github.com/alecthomas/units v0.0.0-20211218093645-b94a6e3cc137 h1:s6gZFSlWYmbqAuRjVTiNNhvNRfY2Wxp9nhfyel4rklc=
|
github.com/alecthomas/units v0.0.0-20211218093645-b94a6e3cc137 h1:s6gZFSlWYmbqAuRjVTiNNhvNRfY2Wxp9nhfyel4rklc=
|
||||||
|
@ -7,6 +9,10 @@ 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/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/icedream/go-q3net v0.1.0 h1:ly5QS55sXAs7HunlCPDsUmS6QLYqP6kGBdupwufaiC4=
|
github.com/icedream/go-q3net v0.1.0 h1:ly5QS55sXAs7HunlCPDsUmS6QLYqP6kGBdupwufaiC4=
|
||||||
github.com/icedream/go-q3net v0.1.0/go.mod h1:2Y0epYeaR6uWXDMvapfsUkLDqAXhI8mp/J5LxO86eUU=
|
github.com/icedream/go-q3net v0.1.0/go.mod h1:2Y0epYeaR6uWXDMvapfsUkLDqAXhI8mp/J5LxO86eUU=
|
||||||
|
github.com/icedream/ui2walk v0.0.0-20160513005918-6f3bcb07a270 h1:tjLVsfoFJxX30ny02EEOjg3VXdoZA0uH8x3gw9YUM4U=
|
||||||
|
github.com/icedream/ui2walk v0.0.0-20160513005918-6f3bcb07a270/go.mod h1:6wS3BNtTpx4//e4hNWPUegvMQ9qT7iZ9RyvB8HmCtzQ=
|
||||||
|
github.com/josephspurrier/goversioninfo v1.4.0 h1:Puhl12NSHUSALHSuzYwPYQkqa2E1+7SrtAPJorKK0C8=
|
||||||
|
github.com/josephspurrier/goversioninfo v1.4.0/go.mod h1:JWzv5rKQr+MmW+LvM412ToT/IkYDZjaclF2pKDss8IY=
|
||||||
github.com/lxn/walk v0.0.0-20210112085537-c389da54e794 h1:NVRJ0Uy0SOFcXSKLsS65OmI1sgCCfiDUPj+cwnH7GZw=
|
github.com/lxn/walk v0.0.0-20210112085537-c389da54e794 h1:NVRJ0Uy0SOFcXSKLsS65OmI1sgCCfiDUPj+cwnH7GZw=
|
||||||
github.com/lxn/walk v0.0.0-20210112085537-c389da54e794/go.mod h1:E23UucZGqpuUANJooIbHWCufXvOcT6E7Stq81gU+CSQ=
|
github.com/lxn/walk v0.0.0-20210112085537-c389da54e794/go.mod h1:E23UucZGqpuUANJooIbHWCufXvOcT6E7Stq81gU+CSQ=
|
||||||
github.com/lxn/win v0.0.0-20210218163916-a377121e959e h1:H+t6A/QJMbhCSEH5rAuRxh+CtW96g0Or0Fxa9IKr4uc=
|
github.com/lxn/win v0.0.0-20210218163916-a377121e959e h1:H+t6A/QJMbhCSEH5rAuRxh+CtW96g0Or0Fxa9IKr4uc=
|
||||||
|
@ -14,12 +20,9 @@ github.com/lxn/win v0.0.0-20210218163916-a377121e959e/go.mod h1:KxxjdtRkfNoYDCUP
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
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/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
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/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
|
||||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
github.com/stretchr/testify v1.6.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.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8=
|
github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8=
|
||||||
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||||
github.com/xhit/go-str2duration/v2 v2.1.0 h1:lxklc02Drh6ynqX+DdPyp5pCKLUQpRT8bp8Ydu2Bstc=
|
github.com/xhit/go-str2duration/v2 v2.1.0 h1:lxklc02Drh6ynqX+DdPyp5pCKLUQpRT8bp8Ydu2Bstc=
|
||||||
|
@ -29,7 +32,6 @@ golang.org/x/sys v0.12.0 h1:CM0HF96J0hcLAwsHPJZjfdNzs0gftsLfgKt57wWHJ0o=
|
||||||
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
gopkg.in/Knetic/govaluate.v3 v3.0.0 h1:18mUyIt4ZlRlFZAAfVetz4/rzlJs9yhN+U02F4u1AOc=
|
gopkg.in/Knetic/govaluate.v3 v3.0.0 h1:18mUyIt4ZlRlFZAAfVetz4/rzlJs9yhN+U02F4u1AOc=
|
||||||
gopkg.in/Knetic/govaluate.v3 v3.0.0/go.mod h1:csKLBORsPbafmSCGTEh3U7Ozmsuq8ZSIlKk1bcqph0E=
|
gopkg.in/Knetic/govaluate.v3 v3.0.0/go.mod h1:csKLBORsPbafmSCGTEh3U7Ozmsuq8ZSIlKk1bcqph0E=
|
||||||
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/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
|
|
@ -0,0 +1,110 @@
|
||||||
|
package rcon
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
quake "github.com/icedream/go-q3net"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Client struct {
|
||||||
|
address *net.UDPAddr
|
||||||
|
addressStr string
|
||||||
|
password string
|
||||||
|
|
||||||
|
socket *net.UDPConn
|
||||||
|
socketBuffer []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewRconClient() *Client {
|
||||||
|
socketBuffer := make([]byte, 64*1024)
|
||||||
|
return &Client{
|
||||||
|
socketBuffer: socketBuffer,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) SetSocketAddr(addr string) (err error) {
|
||||||
|
newAddr, err := net.ResolveUDPAddr("udp", addr)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.address, c.addressStr = newAddr, addr
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) SetPassword(pw string) {
|
||||||
|
c.password = pw
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) Address() *net.UDPAddr {
|
||||||
|
return c.address
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) AddressString() string {
|
||||||
|
return c.addressStr
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) Password() string {
|
||||||
|
return c.password
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) InitSocket() (err error) {
|
||||||
|
c.socket, err = net.ListenUDP("udp", nil)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) udpReceiveAndUnmarshal() (msg *quake.Message, err error) {
|
||||||
|
length, _, err := c.socket.ReadFromUDP(c.socketBuffer)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
msg, err = quake.UnmarshalMessage(c.socketBuffer[0:length])
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) Receive() (msg *quake.Message, err error) {
|
||||||
|
msg, err = c.udpReceiveAndUnmarshal()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !strings.EqualFold(msg.Name, "print") {
|
||||||
|
err = errors.New("rcon: Unexpected response from server: " + msg.Name)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) Send(input string) (err error) {
|
||||||
|
buf := new(bytes.Buffer)
|
||||||
|
msg := &quake.Message{
|
||||||
|
Header: quake.OOBHeader,
|
||||||
|
Name: "rcon",
|
||||||
|
Data: []byte(fmt.Sprintf("%s %s", c.password, input)),
|
||||||
|
}
|
||||||
|
if err = msg.Marshal(buf); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if _, err = c.socket.WriteToUDP(buf.Bytes(), c.address); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) Release() {
|
||||||
|
if c.socket != nil {
|
||||||
|
c.socket.Close()
|
||||||
|
c.socket = nil
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,74 @@
|
||||||
|
package ui
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/icedream/icecon/internal/rcon"
|
||||||
|
"github.com/icedream/icecon/internal/ui"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
ui.RegisterUserInterface(ui.UserInterfaceProvider{
|
||||||
|
New: NewConsoleUserInterface,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
type consoleUserInterface struct {
|
||||||
|
rcon *rcon.Client
|
||||||
|
bufferedStdin *bufio.Reader
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewConsoleUserInterface(rconClient *rcon.Client) (ui.UserInterface, error) {
|
||||||
|
return &consoleUserInterface{
|
||||||
|
rcon: rconClient,
|
||||||
|
bufferedStdin: bufio.NewReader(os.Stdin),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ui *consoleUserInterface) readLineFromInput() (input string, err error) {
|
||||||
|
for {
|
||||||
|
if line, hasMoreInLine, err := ui.bufferedStdin.ReadLine(); err != nil {
|
||||||
|
return input, err
|
||||||
|
} else {
|
||||||
|
input += string(line)
|
||||||
|
if !hasMoreInLine {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ui *consoleUserInterface) Run() error {
|
||||||
|
for {
|
||||||
|
input, err := ui.readLineFromInput()
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// "quit" => exit shell
|
||||||
|
if strings.EqualFold(strings.TrimSpace(input), "quit") {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
err = ui.rcon.Send(input)
|
||||||
|
if err != nil {
|
||||||
|
log.Println(err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
msg, err := ui.rcon.Receive()
|
||||||
|
if err != nil {
|
||||||
|
log.Println(err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
switch strings.ToLower(msg.Name) {
|
||||||
|
case "print":
|
||||||
|
log.Println(string(msg.Data))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -0,0 +1,63 @@
|
||||||
|
package ui
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"slices"
|
||||||
|
|
||||||
|
"github.com/icedream/icecon/internal/rcon"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ErrNotSupported is returned on calls to graphical user interface routines
|
||||||
|
// when none provide one.
|
||||||
|
var ErrNotSupported = errors.New("not supported")
|
||||||
|
|
||||||
|
// All registered user interface providers.
|
||||||
|
var userInterfaces = []*UserInterfaceProvider{}
|
||||||
|
|
||||||
|
// UserInterface describes the methods implemented by user interfaces.
|
||||||
|
type UserInterface interface {
|
||||||
|
Run() error
|
||||||
|
}
|
||||||
|
|
||||||
|
// UserInterfaceProvider contains metadata and an entrypoint for a provided
|
||||||
|
// user interface.
|
||||||
|
type UserInterfaceProvider struct {
|
||||||
|
// Whether the user interface renders as a graphical user interface.
|
||||||
|
IsGraphical bool
|
||||||
|
New func(
|
||||||
|
rconClient *rcon.Client,
|
||||||
|
) (UserInterface, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// RegisterUserInterface must be called on init by any user interface provider
|
||||||
|
// loaded in to be discoverable by other methods in this package.
|
||||||
|
func RegisterUserInterface(uiDesc UserInterfaceProvider) {
|
||||||
|
userInterfaces = append(userInterfaces, &uiDesc)
|
||||||
|
}
|
||||||
|
|
||||||
|
// HasGraphicalUI returns true if at least one user interface provider provides
|
||||||
|
// a graphical user interface.
|
||||||
|
func HasGraphicalUI() bool {
|
||||||
|
return slices.IndexFunc(
|
||||||
|
userInterfaces,
|
||||||
|
func(ui *UserInterfaceProvider) bool {
|
||||||
|
return ui.IsGraphical
|
||||||
|
}) >= 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run scans for a matchin user interface provider. If it finds one, it will run
|
||||||
|
// the user interface through it, otherwise it will return nil.
|
||||||
|
func Run(rconClient *rcon.Client, wantGraphical bool) error {
|
||||||
|
for _, uiDesc := range userInterfaces {
|
||||||
|
if uiDesc.IsGraphical != wantGraphical {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
ui, err := uiDesc.New(rconClient)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to instantiate user interface: %w", err)
|
||||||
|
}
|
||||||
|
return ui.Run()
|
||||||
|
}
|
||||||
|
return ErrNotSupported
|
||||||
|
}
|
|
@ -1,6 +1,7 @@
|
||||||
//+build windows
|
//go:build windows
|
||||||
|
// +build windows
|
||||||
|
|
||||||
package main
|
package windows
|
||||||
|
|
||||||
import "github.com/lxn/walk"
|
import "github.com/lxn/walk"
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
|
|
||||||
//+build windows
|
//+build windows
|
||||||
|
|
||||||
package main
|
package windows
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/lxn/walk"
|
"github.com/lxn/walk"
|
|
@ -1,6 +1,7 @@
|
||||||
//+build windows
|
//go:build windows
|
||||||
|
// +build windows
|
||||||
|
|
||||||
package main
|
package windows
|
||||||
|
|
||||||
import "github.com/lxn/walk"
|
import "github.com/lxn/walk"
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
|
|
||||||
//+build windows
|
//+build windows
|
||||||
|
|
||||||
package main
|
package windows
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/lxn/walk"
|
"github.com/lxn/walk"
|
|
@ -0,0 +1,3 @@
|
||||||
|
package windows
|
||||||
|
|
||||||
|
//go:generate go run -mod=mod github.com/icedream/ui2walk
|
|
@ -0,0 +1,251 @@
|
||||||
|
//go:build windows
|
||||||
|
// +build windows
|
||||||
|
|
||||||
|
package windows
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"strings"
|
||||||
|
"syscall"
|
||||||
|
|
||||||
|
"github.com/icedream/icecon/internal/rcon"
|
||||||
|
"github.com/icedream/icecon/internal/ui"
|
||||||
|
walk "github.com/lxn/walk"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
kernel32 *syscall.DLL
|
||||||
|
freeConsole *syscall.Proc
|
||||||
|
|
||||||
|
initErr error
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
kernel32, initErr = syscall.LoadDLL("kernel32")
|
||||||
|
if initErr != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
freeConsole, initErr = kernel32.FindProc("FreeConsole")
|
||||||
|
if initErr != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ui.RegisterUserInterface(ui.UserInterfaceProvider{
|
||||||
|
IsGraphical: true,
|
||||||
|
New: NewWindowsUserInterface,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
type windowsUserInterface struct {
|
||||||
|
rcon *rcon.Client
|
||||||
|
|
||||||
|
mainDialog *mainDialog
|
||||||
|
originalDialogTitle string
|
||||||
|
|
||||||
|
history []string
|
||||||
|
historyIndex int
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ui *windowsUserInterface) logError(text string) {
|
||||||
|
text = normalizeTextForUI(text)
|
||||||
|
ui.mainDialog.Synchronize(func() {
|
||||||
|
ui.mainDialog.ui.rconOutput.AppendText("ERROR: " + text + "\r\n")
|
||||||
|
walk.MsgBox(ui.mainDialog, "Error",
|
||||||
|
text,
|
||||||
|
walk.MsgBoxIconError)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ui *windowsUserInterface) log(text string) {
|
||||||
|
text = normalizeTextForUI(text)
|
||||||
|
ui.mainDialog.Synchronize(func() {
|
||||||
|
ui.mainDialog.ui.rconOutput.AppendText(text + "\r\n")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func normalizeTextForUI(text string) string {
|
||||||
|
text = strings.Replace(text, "\r", "", -1)
|
||||||
|
text = strings.Replace(text, "\n", "\r\n", -1)
|
||||||
|
|
||||||
|
return text
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ui *windowsUserInterface) updateAddress() {
|
||||||
|
if len(ui.originalDialogTitle) <= 0 {
|
||||||
|
ui.originalDialogTitle = ui.mainDialog.Title()
|
||||||
|
}
|
||||||
|
if len(ui.rcon.AddressString()) > 0 {
|
||||||
|
ui.mainDialog.SetTitle(ui.originalDialogTitle + " - " + ui.rcon.AddressString())
|
||||||
|
} else {
|
||||||
|
ui.mainDialog.SetTitle(ui.originalDialogTitle)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ui *windowsUserInterface) addToHistory(command string) {
|
||||||
|
// limit history to 20 items
|
||||||
|
if len(ui.history) > 20 {
|
||||||
|
ui.history = append(ui.history[:0], ui.history[0+1:]...)
|
||||||
|
}
|
||||||
|
|
||||||
|
ui.history = append(ui.history, command)
|
||||||
|
ui.historyIndex = len(ui.history)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ui *windowsUserInterface) Dispose() {
|
||||||
|
if ui.mainDialog != nil {
|
||||||
|
ui.mainDialog.Dispose()
|
||||||
|
ui.mainDialog = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewWindowsUserInterface(rconClient *rcon.Client) (ui.UserInterface, error) {
|
||||||
|
if initErr != nil {
|
||||||
|
return nil, initErr
|
||||||
|
}
|
||||||
|
|
||||||
|
return &windowsUserInterface{
|
||||||
|
rcon: rconClient,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ui *windowsUserInterface) Run() error {
|
||||||
|
var err error
|
||||||
|
defer func() {
|
||||||
|
if err != nil {
|
||||||
|
ui.Dispose()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
ui.mainDialog = new(mainDialog)
|
||||||
|
if err := ui.mainDialog.init(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Window icon
|
||||||
|
// TODO - Do this more intelligently
|
||||||
|
for i := 0; i < 128; i++ {
|
||||||
|
if icon, err := walk.NewIconFromResourceId(i); err == nil {
|
||||||
|
ui.mainDialog.SetIcon(icon)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Quit button
|
||||||
|
quitAction := walk.NewAction()
|
||||||
|
if err = quitAction.SetText("&Quit"); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
quitAction.Triggered().Attach(func() { ui.mainDialog.Close() })
|
||||||
|
if err = ui.mainDialog.Menu().Actions().Add(quitAction); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Connect button
|
||||||
|
connectAction := walk.NewAction()
|
||||||
|
if err = connectAction.SetText("&Connect"); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
connectAction.Triggered().Attach(func() {
|
||||||
|
result, addr, pw, err := runConnectDialog(
|
||||||
|
ui.rcon.AddressString(),
|
||||||
|
ui.rcon.Password(),
|
||||||
|
ui.mainDialog)
|
||||||
|
if err != nil {
|
||||||
|
ui.logError(fmt.Sprintf("Failed to run connect dialog: %s", err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if result {
|
||||||
|
if err = ui.rcon.SetSocketAddr(addr); err != nil {
|
||||||
|
ui.logError(fmt.Sprintf("Couldn't use that address: %s", err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ui.rcon.SetPassword(pw)
|
||||||
|
ui.mainDialog.ui.rconOutput.SetText("")
|
||||||
|
ui.updateAddress()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
if err = ui.mainDialog.Menu().Actions().Add(connectAction); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle input
|
||||||
|
ui.mainDialog.ui.rconInput.KeyPress().Attach(func(key walk.Key) {
|
||||||
|
// handle history (arrow up/down)
|
||||||
|
if key == walk.KeyUp || key == walk.KeyDown {
|
||||||
|
if len(ui.history) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if key == walk.KeyUp {
|
||||||
|
if ui.historyIndex == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ui.historyIndex -= 1
|
||||||
|
ui.mainDialog.ui.rconInput.SetText(ui.history[ui.historyIndex])
|
||||||
|
} else {
|
||||||
|
if (ui.historyIndex + 1) >= len(ui.history) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ui.historyIndex += 1
|
||||||
|
ui.mainDialog.ui.rconInput.SetText(ui.history[ui.historyIndex])
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if key != walk.KeyReturn {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if ui.rcon.Address() == nil {
|
||||||
|
ui.logError("No server configured.")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd := ui.mainDialog.ui.rconInput.Text()
|
||||||
|
ui.mainDialog.ui.rconInput.SetText("")
|
||||||
|
|
||||||
|
ui.log(ui.rcon.Address().String() + "> " + cmd)
|
||||||
|
ui.rcon.Send(cmd)
|
||||||
|
|
||||||
|
// add to history
|
||||||
|
ui.addToHistory(cmd)
|
||||||
|
})
|
||||||
|
|
||||||
|
// When window is initialized we can let a secondary routine print all
|
||||||
|
// output received
|
||||||
|
ui.mainDialog.Synchronize(func() {
|
||||||
|
ui.updateAddress()
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
for {
|
||||||
|
msg, err := ui.rcon.Receive()
|
||||||
|
if err != nil {
|
||||||
|
ui.logError(err.Error())
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
switch strings.ToLower(msg.Name) {
|
||||||
|
case "print":
|
||||||
|
ui.log(string(msg.Data))
|
||||||
|
default:
|
||||||
|
log.Println(msg.Name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
})
|
||||||
|
|
||||||
|
// Get rid of the console window
|
||||||
|
// freeConsole.Call()
|
||||||
|
|
||||||
|
ui.mainDialog.Show()
|
||||||
|
|
||||||
|
// Message loop starts here and will block the main goroutine!
|
||||||
|
if retval := ui.mainDialog.Run(); retval != 0 {
|
||||||
|
err = syscall.Errno(retval)
|
||||||
|
}
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
215
main.go
215
main.go
|
@ -1,215 +0,0 @@
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bufio"
|
|
||||||
"bytes"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"log"
|
|
||||||
"net"
|
|
||||||
"os"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/alecthomas/kingpin/v2"
|
|
||||||
quake "github.com/icedream/go-q3net"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
flagCommand = kingpin.Flag("command",
|
|
||||||
"Run a one-off command and then exit.").
|
|
||||||
Short('c').String()
|
|
||||||
argAddress = kingpin.Arg("address",
|
|
||||||
"Server IP/hostname and port, written as \"server:port\".")
|
|
||||||
argPassword = kingpin.Arg("password", "The RCON password.")
|
|
||||||
|
|
||||||
address *net.UDPAddr
|
|
||||||
addressStr string
|
|
||||||
password string
|
|
||||||
|
|
||||||
socket *net.UDPConn
|
|
||||||
socketBuffer = make([]byte, 64*1024)
|
|
||||||
|
|
||||||
bufferedStdin *bufio.Reader
|
|
||||||
|
|
||||||
errNotSupported = errors.New("Not supported")
|
|
||||||
)
|
|
||||||
|
|
||||||
func initSocketAddr(addr string) (err error) {
|
|
||||||
newAddr, err := net.ResolveUDPAddr("udp", addr)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
address, addressStr = newAddr, addr
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func initSocket() (err error) {
|
|
||||||
socket, err = net.ListenUDP("udp", nil)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func receive() (msg *quake.Message, err error) {
|
|
||||||
length, _, err := socket.ReadFromUDP(socketBuffer)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
msg, err = quake.UnmarshalMessage(socketBuffer[0:length])
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func receiveRcon() (msg *quake.Message, err error) {
|
|
||||||
msg, err = receive()
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if !strings.EqualFold(msg.Name, "print") {
|
|
||||||
err = errors.New("rcon: Unexpected response from server: " + msg.Name)
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func sendRcon(input string) (err error) {
|
|
||||||
buf := new(bytes.Buffer)
|
|
||||||
msg := &quake.Message{
|
|
||||||
Header: quake.OOBHeader,
|
|
||||||
Name: "rcon",
|
|
||||||
Data: []byte(fmt.Sprintf("%s %s", password, input)),
|
|
||||||
}
|
|
||||||
if err = msg.Marshal(buf); err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if _, err = socket.WriteToUDP(buf.Bytes(), address); err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func readLineFromInput() (input string, err error) {
|
|
||||||
if bufferedStdin == nil {
|
|
||||||
bufferedStdin = bufio.NewReader(os.Stdin)
|
|
||||||
}
|
|
||||||
for {
|
|
||||||
if line, hasMoreInLine, err := bufferedStdin.ReadLine(); err != nil {
|
|
||||||
return input, err
|
|
||||||
} else {
|
|
||||||
input += string(line)
|
|
||||||
if !hasMoreInLine {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func usage() {
|
|
||||||
kingpin.Usage()
|
|
||||||
}
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
fmt.Println("IceCon - Icedream's RCON Client")
|
|
||||||
fmt.Println("\t\u00A9 2016-2024 Carl Kittelberger/Icedream")
|
|
||||||
fmt.Println()
|
|
||||||
|
|
||||||
argAddressTCP := argAddress.TCP()
|
|
||||||
argPasswordStr := argPassword.String()
|
|
||||||
|
|
||||||
kingpin.Parse()
|
|
||||||
|
|
||||||
// If no arguments, fall back to running the shell
|
|
||||||
wantGui := (*argAddressTCP == nil && *flagCommand == "") || *flagGui
|
|
||||||
|
|
||||||
// Command-line shell doesn't support starting up without arguments
|
|
||||||
// but graphical Windows UI does
|
|
||||||
if !(hasGraphicalUI && wantGui) {
|
|
||||||
argAddress = argAddress.Required()
|
|
||||||
argPassword = argPassword.Required()
|
|
||||||
kingpin.Parse()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initialize socket
|
|
||||||
initSocket()
|
|
||||||
|
|
||||||
// Set target address if given
|
|
||||||
if *argAddressTCP != nil {
|
|
||||||
initSocketAddr((*argAddressTCP).String())
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get password
|
|
||||||
password = *argPasswordStr
|
|
||||||
|
|
||||||
// Run one-off command?
|
|
||||||
if *flagCommand != "" {
|
|
||||||
// Send
|
|
||||||
err := sendRcon(*flagCommand)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Receive
|
|
||||||
msg, err := receiveRcon()
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
switch strings.ToLower(msg.Name) {
|
|
||||||
case "print":
|
|
||||||
fmt.Println(string(msg.Data))
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Which UI should be run?
|
|
||||||
if wantGui {
|
|
||||||
if err := runGraphicalUi(); err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
runConsoleShell()
|
|
||||||
}
|
|
||||||
|
|
||||||
if socket != nil {
|
|
||||||
socket.Close()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func runConsoleShell() {
|
|
||||||
for {
|
|
||||||
input, err := readLineFromInput()
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// "quit" => exit shell
|
|
||||||
if strings.EqualFold(strings.TrimSpace(input), "quit") {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
err = sendRcon(input)
|
|
||||||
if err != nil {
|
|
||||||
log.Println(err)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
msg, err := receiveRcon()
|
|
||||||
if err != nil {
|
|
||||||
log.Println(err)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
switch strings.ToLower(msg.Name) {
|
|
||||||
case "print":
|
|
||||||
log.Println(string(msg.Data))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,12 +0,0 @@
|
||||||
//+build !windows
|
|
||||||
|
|
||||||
package main
|
|
||||||
|
|
||||||
var flagGuiIncompatible = false
|
|
||||||
var flagGui = &flagGuiIncompatible
|
|
||||||
|
|
||||||
var hasGraphicalUI = false
|
|
||||||
|
|
||||||
func runGraphicalUi() error {
|
|
||||||
return errNotSupported
|
|
||||||
}
|
|
216
main_windows.go
216
main_windows.go
|
@ -1,216 +0,0 @@
|
||||||
//go:build windows
|
|
||||||
// +build windows
|
|
||||||
|
|
||||||
package main
|
|
||||||
|
|
||||||
//go:generate ui2walk
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"log"
|
|
||||||
"strings"
|
|
||||||
"syscall"
|
|
||||||
|
|
||||||
"github.com/alecthomas/kingpin/v2"
|
|
||||||
"github.com/lxn/walk"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
hasGraphicalUI = true
|
|
||||||
|
|
||||||
flagGui = kingpin.
|
|
||||||
Flag("gui", "Run as GUI (runs automatically as GUI if no arguments given, ignored if command flag used)").
|
|
||||||
Short('g').Bool()
|
|
||||||
|
|
||||||
guiInitErr error
|
|
||||||
kernel32 *syscall.DLL
|
|
||||||
freeConsole *syscall.Proc
|
|
||||||
|
|
||||||
dlg *mainDialog
|
|
||||||
dlgOriginalTitle string
|
|
||||||
|
|
||||||
history []string
|
|
||||||
historyIndex = 0
|
|
||||||
)
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
kernel32, guiInitErr = syscall.LoadDLL("kernel32.dll")
|
|
||||||
freeConsole, guiInitErr = kernel32.FindProc("FreeConsole")
|
|
||||||
}
|
|
||||||
|
|
||||||
func uiLogError(text string) {
|
|
||||||
uiNormalize(&text)
|
|
||||||
dlg.Synchronize(func() {
|
|
||||||
dlg.ui.rconOutput.AppendText("ERROR: " + text + "\r\n")
|
|
||||||
walk.MsgBox(dlg, "Error",
|
|
||||||
text,
|
|
||||||
walk.MsgBoxIconError)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func uiLog(text string) {
|
|
||||||
uiNormalize(&text)
|
|
||||||
dlg.Synchronize(func() {
|
|
||||||
dlg.ui.rconOutput.AppendText(text + "\r\n")
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func uiNormalize(textRef *string) {
|
|
||||||
text := *textRef
|
|
||||||
|
|
||||||
text = strings.Replace(text, "\r", "", -1)
|
|
||||||
text = strings.Replace(text, "\n", "\r\n", -1)
|
|
||||||
|
|
||||||
*textRef = text
|
|
||||||
}
|
|
||||||
|
|
||||||
func uiUpdateAddress() {
|
|
||||||
if len(dlgOriginalTitle) <= 0 {
|
|
||||||
dlgOriginalTitle = dlg.Title()
|
|
||||||
}
|
|
||||||
if len(addressStr) > 0 {
|
|
||||||
dlg.SetTitle(dlgOriginalTitle + " - " + addressStr)
|
|
||||||
} else {
|
|
||||||
dlg.SetTitle(dlgOriginalTitle)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func addToHistory(command string) {
|
|
||||||
// limit history to 20 items
|
|
||||||
if len(history) > 20 {
|
|
||||||
history = append(history[:0], history[0+1:]...)
|
|
||||||
}
|
|
||||||
|
|
||||||
history = append(history, command)
|
|
||||||
historyIndex = len(history)
|
|
||||||
}
|
|
||||||
|
|
||||||
func runGraphicalUi() (err error) {
|
|
||||||
dlg = new(mainDialog)
|
|
||||||
if err := dlg.init(); err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
defer dlg.Dispose()
|
|
||||||
|
|
||||||
// Window icon
|
|
||||||
// TODO - Do this more intelligently
|
|
||||||
for i := 0; i < 128; i++ {
|
|
||||||
if icon, err := walk.NewIconFromResourceId(i); err == nil {
|
|
||||||
dlg.SetIcon(icon)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Quit button
|
|
||||||
quitAction := walk.NewAction()
|
|
||||||
if err = quitAction.SetText("&Quit"); err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
quitAction.Triggered().Attach(func() { dlg.Close() })
|
|
||||||
if err = dlg.Menu().Actions().Add(quitAction); err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Connect button
|
|
||||||
connectAction := walk.NewAction()
|
|
||||||
if err = connectAction.SetText("&Connect"); err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
connectAction.Triggered().Attach(func() {
|
|
||||||
result, addr, pw, err := runConnectDialog(addressStr, password, dlg)
|
|
||||||
if err != nil {
|
|
||||||
uiLogError(fmt.Sprintf("Failed to run connect dialog: %s", err))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if result {
|
|
||||||
if err = initSocketAddr(addr); err != nil {
|
|
||||||
uiLogError(fmt.Sprintf("Couldn't use that address: %s", err))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
password = pw
|
|
||||||
dlg.ui.rconOutput.SetText("")
|
|
||||||
uiUpdateAddress()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
if err = dlg.Menu().Actions().Add(connectAction); err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle input
|
|
||||||
dlg.ui.rconInput.KeyPress().Attach(func(key walk.Key) {
|
|
||||||
// handle history (arrow up/down)
|
|
||||||
if key == walk.KeyUp || key == walk.KeyDown {
|
|
||||||
if len(history) == 0 {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if key == walk.KeyUp {
|
|
||||||
if historyIndex == 0 {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
historyIndex -= 1
|
|
||||||
dlg.ui.rconInput.SetText(history[historyIndex])
|
|
||||||
} else {
|
|
||||||
if (historyIndex + 1) >= len(history) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
historyIndex += 1
|
|
||||||
dlg.ui.rconInput.SetText(history[historyIndex])
|
|
||||||
}
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if key != walk.KeyReturn {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if address == nil {
|
|
||||||
uiLogError("No server configured.")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
cmd := dlg.ui.rconInput.Text()
|
|
||||||
dlg.ui.rconInput.SetText("")
|
|
||||||
|
|
||||||
uiLog(address.String() + "> " + cmd)
|
|
||||||
sendRcon(cmd)
|
|
||||||
|
|
||||||
// add to history
|
|
||||||
addToHistory(cmd)
|
|
||||||
})
|
|
||||||
|
|
||||||
// When window is initialized we can let a secondary routine print all
|
|
||||||
// output received
|
|
||||||
dlg.Synchronize(func() {
|
|
||||||
uiUpdateAddress()
|
|
||||||
|
|
||||||
go func() {
|
|
||||||
for {
|
|
||||||
msg, err := receiveRcon()
|
|
||||||
if err != nil {
|
|
||||||
uiLogError(err.Error())
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
switch strings.ToLower(msg.Name) {
|
|
||||||
case "print":
|
|
||||||
uiLog(string(msg.Data))
|
|
||||||
default:
|
|
||||||
log.Println(msg.Name)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
})
|
|
||||||
|
|
||||||
// Get rid of the console window
|
|
||||||
freeConsole.Call()
|
|
||||||
|
|
||||||
dlg.Show()
|
|
||||||
|
|
||||||
// Message loop starts here and will block the main goroutine!
|
|
||||||
dlg.Run()
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
|
@ -1,5 +0,0 @@
|
||||||
// +build windows,386
|
|
||||||
|
|
||||||
package main
|
|
||||||
|
|
||||||
//go:generate goversioninfo -manifest "rsrc/app.manifest" -icon "rsrc/app.ico" -o "rsrc_windows.syso"
|
|
|
@ -1,5 +0,0 @@
|
||||||
// +build windows,amd64
|
|
||||||
|
|
||||||
package main
|
|
||||||
|
|
||||||
//go:generate goversioninfo -manifest "rsrc/app.manifest" -icon "rsrc/app.ico" -o "rsrc_windows.syso" -64
|
|
Loading…
Reference in New Issue