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
|
||||
|
||||
go 1.12
|
||||
go 1.22
|
||||
|
||||
require (
|
||||
github.com/alecthomas/kingpin/v2 v2.4.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
|
||||
)
|
||||
|
||||
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/xhit/go-str2duration/v2 v2.1.0 // indirect
|
||||
golang.org/x/sys v0.12.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/go.mod h1:0gyi0zQnjuFk8xrkNKamJoyUo382HRL7ATRpFZCw6tE=
|
||||
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/icedream/go-q3net v0.1.0 h1:ly5QS55sXAs7HunlCPDsUmS6QLYqP6kGBdupwufaiC4=
|
||||
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/go.mod h1:E23UucZGqpuUANJooIbHWCufXvOcT6E7Stq81gU+CSQ=
|
||||
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/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
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.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.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8=
|
||||
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
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=
|
||||
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/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.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=
|
||||
|
|
|
@ -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"
|
||||
|
|
@ -3,7 +3,7 @@
|
|||
|
||||
//+build windows
|
||||
|
||||
package main
|
||||
package windows
|
||||
|
||||
import (
|
||||
"github.com/lxn/walk"
|
|
@ -1,6 +1,7 @@
|
|||
//+build windows
|
||||
//go:build windows
|
||||
// +build windows
|
||||
|
||||
package main
|
||||
package windows
|
||||
|
||||
import "github.com/lxn/walk"
|
||||
|
|
@ -3,7 +3,7 @@
|
|||
|
||||
//+build windows
|
||||
|
||||
package main
|
||||
package windows
|
||||
|
||||
import (
|
||||
"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