Rewrite and update the code for the modern world.

rewrite
Icedream 2024-05-05 09:39:54 +02:00
parent 3a42381727
commit 9bdc64ae6e
Signed by: icedream
GPG Key ID: 468BBEEBB9EC6AEA
23 changed files with 648 additions and 465 deletions

View File

@ -0,0 +1,5 @@
package main
import (
_ "github.com/icedream/icecon/internal/ui/windows"
)

105
cmd/icecon/main.go Normal file
View File

@ -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
}
}

4
cmd/icecon/rsrc.go Normal file
View File

@ -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
View File

@ -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
View File

@ -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=

110
internal/rcon/client.go Normal file
View File

@ -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
}
}

View File

@ -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
}

63
internal/ui/ui.go Normal file
View File

@ -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
}

View File

@ -1,6 +1,7 @@
//+build windows
//go:build windows
// +build windows
package main
package windows
import "github.com/lxn/walk"

View File

@ -3,7 +3,7 @@
//+build windows
package main
package windows
import (
"github.com/lxn/walk"

View File

@ -1,6 +1,7 @@
//+build windows
//go:build windows
// +build windows
package main
package windows
import "github.com/lxn/walk"

View File

@ -3,7 +3,7 @@
//+build windows
package main
package windows
import (
"github.com/lxn/walk"

View File

@ -0,0 +1,3 @@
package windows
//go:generate go run -mod=mod github.com/icedream/ui2walk

View File

@ -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
View File

@ -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))
}
}
}

View File

@ -1,12 +0,0 @@
//+build !windows
package main
var flagGuiIncompatible = false
var flagGui = &flagGuiIncompatible
var hasGraphicalUI = false
func runGraphicalUi() error {
return errNotSupported
}

View File

@ -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
}

View File

@ -1,5 +0,0 @@
// +build windows,386
package main
//go:generate goversioninfo -manifest "rsrc/app.manifest" -icon "rsrc/app.ico" -o "rsrc_windows.syso"

View File

@ -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

9
tools.go Normal file
View File

@ -0,0 +1,9 @@
//go:build tools
// +build tools
package main
import (
_ "github.com/icedream/ui2walk"
_ "github.com/josephspurrier/goversioninfo/cmd/goversioninfo"
)