diff --git a/lib/coherentui.go b/lib/coherentui.go new file mode 100644 index 0000000..a6f9029 --- /dev/null +++ b/lib/coherentui.go @@ -0,0 +1,33 @@ +package lib + +import ( + "github.com/mitchellh/go-ps" + "log" + "os" +) + +func killCoherentUI() { + // Find process(es) + chp, err := ps.Processes() + if err != nil { + log.Fatal(err) + } + + // Find PID and kill + for _, v := range chp { + if v.Executable() == "CoherentUI_Host.exe" { + proc, err := os.FindProcess(v.Pid()) + + if err != nil { + log.Println(err) + } + + defer func() { + recover() // 1 + return + }() + // Kill the process + proc.Kill() + } + } +} diff --git a/lib/config.go b/lib/config.go new file mode 100644 index 0000000..d1a0766 --- /dev/null +++ b/lib/config.go @@ -0,0 +1,66 @@ +package lib + +import ( + "strings" + "io/ioutil" + "os" + "gopkg.in/yaml.v2" +) + +// Telegram and program settings (config.yml) +// Guide: http://sweetohm.net/article/go-yaml-parsers.en.html +type Config struct { + Token string + Botid string + Chatid string + Message string + StayAlive bool + Process string + TimeBetweenChecksInS int + KillOnDC bool + ShutdownOnDC bool + KillCoherentUI bool +} + +func Read_Settings(ex string, err error) Config { + + //// SETTINGS + //-------------------------------------------------------------------------------------------------------------- + // YAML PARSING + newex := strings.Replace(ex, "BDO-Watchdog.exe", "config.yml", -1) + // This is necessary for dynamic builds in Jetbrains Gogland IDE + //newex = strings.Replace(ex, "Application.exe", "config.yml", -1) + + var config Config + source, err := ioutil.ReadFile(newex) + + if err != nil { + // in theory, using yml.Marshal() would be more elegant, but we want to preserve the yaml comments + // as well as set some default values/hints + defconf := + "## Get updates here: https://github.com/tzerk/BDO-Watchdog/releases/\r\n" + + "## Telegram Bot Settings\r\n" + + "token: \r\n" + + "botid: \r\n" + + "chatid: \r\n" + + "message: BDO disconnected \r\n" + + "\r\n" + + "## Program Settings\r\n" + + "stayalive: false\r\n" + + "process: BlackDesert64.exe\r\n" + + "timebetweenchecksins: 60\r\n" + + "\r\n" + + "# These settings require the .exe to be run with admin rights! \r\n" + + "killondc: true\r\n" + + "shutdownondc: false\r\n" + + "killcoherentui: false" + ioutil.WriteFile("config.yml", []byte(defconf), os.FileMode(int(0777))) + panic(err) + } + err = yaml.Unmarshal(source, &config) + if err != nil { + panic(err) + } + + return(config) +} \ No newline at end of file diff --git a/lib/telegram.go b/lib/telegram.go new file mode 100644 index 0000000..8c54865 --- /dev/null +++ b/lib/telegram.go @@ -0,0 +1,14 @@ +package lib + +import "net/http" + +// --------------------------------------------------------------------------------------------------------------------- +// Send a telegram message using a query URL +func Send_TelegramMessage(config Config) { + // Learn how to setup a telegram bot: https://core.telegram.org/bots + resp, _ := http.Get("https://api.telegram.org/bot" + config.Botid + + ":" + config.Token + + "/sendMessage?chat_id=" + config.Chatid + + "&text=" + config.Message) + defer resp.Body.Close() +} diff --git a/lib/wait.go b/lib/wait.go new file mode 100644 index 0000000..f732528 --- /dev/null +++ b/lib/wait.go @@ -0,0 +1,28 @@ +package lib + +import ( + "github.com/andlabs/ui" + "strconv" + "time" +) + +// --------------------------------------------------------------------------------------------------------------------- +// A wrapper for time.Sleep() that also updates the UI label and progressbar +func wait(config Config, label_Update *ui.Label, pb *ui.ProgressBar) { + tstep := config.TimeBetweenChecksInS + var pbVal int + + if tstep <= 0 { + tstep = 1 + } // otherwise division by 0 + for i := 0; i <= tstep; i++ { + pbVal = int(100/float32(tstep) * float32(i)) + if pbVal > 100 { + pbVal = 100 + } + pb.SetValue(pbVal) + label_Update.SetText(" Next update in... " + strconv.Itoa(tstep - i) + " s") + time.Sleep(1 * time.Second) + } + pb.SetValue(0) +} \ No newline at end of file diff --git a/lib/watchdog.go b/lib/watchdog.go new file mode 100644 index 0000000..b9022ba --- /dev/null +++ b/lib/watchdog.go @@ -0,0 +1,146 @@ +package lib + +import ( + "os/exec" + "syscall" + "log" + "regexp" + "strconv" + "github.com/andlabs/ui" + "github.com/mitchellh/go-ps" + "os" + "time" +) + +var STATUS bool = false +var CONNECTION bool = false +var PID int + +//-------------------------------------------------------------------------------------------------------------- +// PROCESS +//-------------------------------------------------------------------------------------------------------------- +func Watchdog( + config Config, + label_Status *ui.Label, + label_PID *ui.Label, + label_Connection *ui.Label, + label_Update *ui.Label, + pb *ui.ProgressBar) { + + // KILL CoherentUI_Host.exe + if config.KillCoherentUI { + killCoherentUI() + } + + // INFINITE MAIN LOOP + for { + label_Update.SetText("") + + //// EXIT CONDITION + //----------------- + // If the process is running, but no longer connected we trigger the following actions + if STATUS && !CONNECTION { + + // Use the Telegram API to send a message + Send_TelegramMessage(config) + + // Optional: shutdown the computer if the monitored process is disconnected + if config.ShutdownOnDC { + exec.Command("cmd", "/C", "shutdown", "/s").Run() + } + + // Optional: kill the monitored process if it is disconnected + // requires elevated rights --> start .exe as administrator + if config.KillOnDC { + + proc, err := os.FindProcess(PID) + + if err != nil { + log.Println(err) + } + + defer func() { + recover() // 1 + return + }() + // Kill the process + proc.Kill() + + time.Sleep(5 * time.Second) + } + + // Optional (YAML file, default: false): keep ts program open even if + // the process is disconnected + if !config.StayAlive { + os.Exit(1) + } + } + + //// PROCESS + //---------- + p, err := ps.Processes() + if err != nil { + log.Fatal(err) + } + + //// PID + //------ + PID = 0 + for _, v := range p { + if v.Executable() == config.Process { + PID = v.Pid() + } + } + if (PID == 0) { + ui.QueueMain(func () { + STATUS = false + label_Status.SetText(" Status: not running") + label_PID.SetText(" PID: -") + label_Connection.SetText(" Connection: -" ) + }) + + wait(config, label_Update, pb) + continue + } else { + + ui.QueueMain(func () { + STATUS = true + label_Status.SetText(" Status: running") + label_PID.SetText(" PID: " + strconv.Itoa(PID)) + }) + } + + //// CONNECTION STATUS + //-------------------- + // NETSTAT + // the syscall.SysProcAttr trick found here: + // https://www.reddit.com/r/golang/comments/2c1g3x/build_golang_app_reverse_shell_to_run_in_windows/ + cmd := exec.Command("cmd.exe", "/C netstat -aon") + cmd.SysProcAttr = &syscall.SysProcAttr{HideWindow: true} + out, err := cmd.Output() + if err != nil { + log.Fatal(err) + } + + + // RegEx matching; try to find the PID in the netstat output + re := regexp.MustCompile(strconv.Itoa(PID)) + byteIndex := re.FindIndex([]byte(out)) + + if (len(byteIndex) == 0) { + ui.QueueMain(func () { + CONNECTION = false + label_Connection.SetText(" Connection: Offline" ) + }) + } else { + // Update labels + ui.QueueMain(func () { + CONNECTION = true + label_Connection.SetText(" Connection: online") + }) + } + + // Wait x seconds before next iteration + wait(config, label_Update, pb) + } +} \ No newline at end of file diff --git a/main.go b/main.go index f7750df..f6be999 100644 --- a/main.go +++ b/main.go @@ -1,86 +1,20 @@ package main import ( - ps "github.com/mitchellh/go-ps" - "log" - "os/exec" - "regexp" "strconv" "github.com/andlabs/ui" - "time" - "net/http" "os" - "gopkg.in/yaml.v2" - "io/ioutil" - "syscall" - "strings" + . "BDO-Watchdog/lib" ) -// Telegram and program settings (config.yml) -// Guide: http://sweetohm.net/article/go-yaml-parsers.en.html -type Config struct { - Token string - Botid string - Chatid string - Message string - StayAlive bool - Process string - TimeBetweenChecksInS int - KillOnDC bool - ShutdownOnDC bool - KillCoherentUI bool -} - // Variables const VERSION = "0.1.5" -var STATUS bool = false -var CONNECTION bool = false -var PID int func main() { - //// SETTINGS - //-------------------------------------------------------------------------------------------------------------- - // YAML PARSING - ex, err := os.Executable() - if err != nil { - panic(err) - } - newex := strings.Replace(ex, "BDO-Watchdog.exe", "config.yml", -1) - // This is necessary for dynamic builds in Jetbrains Gogland IDE - //newex = strings.Replace(ex, "Application.exe", "config.yml", -1) - - var config Config - source, err := ioutil.ReadFile(newex) - - if err != nil { - // in theory, using yml.Marshal() would be more elegant, but we want to preserve the yaml comments - // as well as set some default values/hints - defconf := - "## Get updates here: https://github.com/tzerk/BDO-Watchdog/releases/\r\n" + - "## Telegram Bot Settings\r\n" + - "token: \r\n" + - "botid: \r\n" + - "chatid: \r\n" + - "message: BDO disconnected \r\n" + - "\r\n" + - "## Program Settings\r\n" + - "stayalive: false\r\n" + - "process: BlackDesert64.exe\r\n" + - "timebetweenchecksins: 60\r\n" + - "\r\n" + - "# These settings require the .exe to be run with admin rights! \r\n" + - "killondc: true\r\n" + - "shutdownondc: false\r\n" + - "killcoherentui: false" - ioutil.WriteFile("config.yml", []byte(defconf), os.FileMode(int(0777))) - panic(err) - } - err = yaml.Unmarshal(source, &config) - if err != nil { - panic(err) - } + // SETTINGS + config := Read_Settings(os.Executable()) //// GUI //-------------------------------------------------------------------------------------------------------------- @@ -101,7 +35,7 @@ func main() { tbtn := ui.NewButton("Send Telegram test message") tbtn.OnClicked(func(*ui.Button) { - send_TelegramMessage(config) + Send_TelegramMessage(config) }) sep := ui.NewHorizontalSeparator() @@ -154,194 +88,11 @@ func main() { return true }) window.Show() - go observer(config, label_Status, label_PID, label_Connection, label_Update, pb) + go Watchdog(config, label_Status, label_PID, label_Connection, label_Update, pb) }) if ui != nil { panic(ui) } } -//-------------------------------------------------------------------------------------------------------------- -// PROCESS -//-------------------------------------------------------------------------------------------------------------- -func observer( - config Config, - label_Status *ui.Label, - label_PID *ui.Label, - label_Connection *ui.Label, - label_Update *ui.Label, - pb *ui.ProgressBar) { - - // KILL CoherentUI_Host.exe - if config.KillCoherentUI { - - // Find process(es) - chp, err := ps.Processes() - if err != nil { - log.Fatal(err) - } - - // Find PID and kill - for _, v := range chp { - if v.Executable() == "CoherentUI_Host.exe" { - proc, err := os.FindProcess(v.Pid()) - - if err != nil { - log.Println(err) - } - - defer func() { - recover() // 1 - return - }() - // Kill the process - proc.Kill() - } - } - } - - // INFINITE MAIN LOOP - for { - label_Update.SetText("") - - //// EXIT CONDITION - //----------------- - // If the process is running, but no longer connected we trigger the following actions - if STATUS && !CONNECTION { - - // Use the Telegram API to send a message - send_TelegramMessage(config) - - // Optional: shutdown the computer if the monitored process is disconnected - if config.ShutdownOnDC { - exec.Command("cmd", "/C", "shutdown", "/s").Run() - } - - // Optional: kill the monitored process if it is disconnected - // requires elevated rights --> start .exe as administrator - if config.KillOnDC { - - proc, err := os.FindProcess(PID) - - if err != nil { - log.Println(err) - } - - defer func() { - recover() // 1 - return - }() - // Kill the process - proc.Kill() - - time.Sleep(5 * time.Second) - } - - // Optional (YAML file, default: false): keep ts program open even if - // the process is disconnected - if !config.StayAlive { - os.Exit(1) - } - } - - //// PROCESS - //---------- - p, err := ps.Processes() - if err != nil { - log.Fatal(err) - } - - //// PID - //------ - PID = 0 - for _, v := range p { - if v.Executable() == config.Process { - PID = v.Pid() - } - } - if (PID == 0) { - ui.QueueMain(func () { - STATUS = false - label_Status.SetText(" Status: not running") - label_PID.SetText(" PID: -") - label_Connection.SetText(" Connection: -" ) - }) - - wait(config, label_Update, pb) - continue - } else { - - ui.QueueMain(func () { - STATUS = true - label_Status.SetText(" Status: running") - label_PID.SetText(" PID: " + strconv.Itoa(PID)) - }) - } - - //// CONNECTION STATUS - //-------------------- - // NETSTAT - // the syscall.SysProcAttr trick found here: - // https://www.reddit.com/r/golang/comments/2c1g3x/build_golang_app_reverse_shell_to_run_in_windows/ - cmd := exec.Command("cmd.exe", "/C netstat -aon") - cmd.SysProcAttr = &syscall.SysProcAttr{HideWindow: true} - out, err := cmd.Output() - if err != nil { - log.Fatal(err) - } - - - // RegEx matching; try to find the PID in the netstat output - re := regexp.MustCompile(strconv.Itoa(PID)) - byteIndex := re.FindIndex([]byte(out)) - - if (len(byteIndex) == 0) { - ui.QueueMain(func () { - CONNECTION = false - label_Connection.SetText(" Connection: Offline" ) - }) - } else { - // Update labels - ui.QueueMain(func () { - CONNECTION = true - label_Connection.SetText(" Connection: online") - }) - } - - // Wait x seconds before next iteration - wait(config, label_Update, pb) - } -} - -// --------------------------------------------------------------------------------------------------------------------- -// A wrapper for time.Sleep() that also updates the UI label and progressbar -func wait(config Config, label_Update *ui.Label, pb *ui.ProgressBar) { - tstep := config.TimeBetweenChecksInS - var pbVal int - - if tstep <= 0 { - tstep = 1 - } // otherwise division by 0 - for i := 0; i <= tstep; i++ { - pbVal = int(100/float32(tstep) * float32(i)) - if pbVal > 100 { - pbVal = 100 - } - pb.SetValue(pbVal) - label_Update.SetText(" Next update in... " + strconv.Itoa(tstep - i) + " s") - time.Sleep(1 * time.Second) - } - pb.SetValue(0) -} - -// --------------------------------------------------------------------------------------------------------------------- -// Send a telegram message using a query URL -func send_TelegramMessage(config Config) { - // Learn how to setup a telegram bot: https://core.telegram.org/bots - resp, _ := http.Get("https://api.telegram.org/bot" + config.Botid + - ":" + config.Token + - "/sendMessage?chat_id=" + config.Chatid + - "&text=" + config.Message) - defer resp.Body.Close() -}