diff --git a/connection/server.go b/connection/server.go index 3540ca2..dfdc722 100644 --- a/connection/server.go +++ b/connection/server.go @@ -3,7 +3,6 @@ package connection import ( "bytes" "fmt" - "log" "net" "os" "reflect" @@ -65,21 +64,21 @@ func (server *Server) RemoveClient(client *Client) { delete(server.Clients, client.FD) } -func (server *Server) Run() { +func (server *Server) Run() error { var err error BelfastInstance = server if server.SocketFD, err = syscall.Socket(syscall.AF_INET, syscall.SOCK_STREAM|syscall.O_NONBLOCK, 0); err != nil { - log.Fatalf("failed to create socket : %v", err) + return fmt.Errorf("failed to create socket : %v", err) } defer syscall.Close(server.SocketFD) logger.LogEvent("Server", "Listen", fmt.Sprintf("Listening on %s:%d", server.BindAddress, server.Port), logger.LOG_LEVEL_AUTO) if err = syscall.SetsockoptInt(server.SocketFD, syscall.SOL_SOCKET, syscall.SO_REUSEADDR, 1); err != nil { - log.Fatalf("setsockopt error: %v", err) + return fmt.Errorf("setsockopt error: %v", err) } if err = syscall.SetNonblock(server.SocketFD, true); err != nil { - log.Fatalf("setnonblock error: %v", err) + return fmt.Errorf("setnonblock error: %v", err) } var ip [4]byte @@ -90,11 +89,11 @@ func (server *Server) Run() { } if err = syscall.Bind(server.SocketFD, &addr); err != nil { - log.Fatalf("bind error: %v", err) + return fmt.Errorf("bind error: %v", err) } if err = syscall.Listen(server.SocketFD, syscall.SOMAXCONN); err != nil { - log.Fatalf("listen error: %v", err) + return fmt.Errorf("listen error: %v", err) } if server.EpollFD, err = syscall.EpollCreate1(0); err != nil { diff --git a/debug/adb_watcher.go b/debug/adb_watcher.go new file mode 100644 index 0000000..312909b --- /dev/null +++ b/debug/adb_watcher.go @@ -0,0 +1,243 @@ +package debug + +import ( + "bufio" + "fmt" + "os" + "os/exec" + "runtime" + "strings" + "time" + + "github.com/ggmolly/belfast/logger" + "github.com/mattn/go-tty" +) + +var handlers = map[string]func(){ + "?": help, + "c": clear, + "l": listDevices, + "s": toggleLogcat, + "f": flushLogcat, + "d": dumpLogcat, + "+": increaseSleep, + "-": decreaseSleep, + "=": printDelay, +} + +// a list of needles to search for in the process list +// to find Azur Lane's PID -- these should be lowercase +// to make the search case-insensitive +const grepRegex = "'(azurlane|blhx|manjuu|yostar)'" + +// Filter to remove Azur Lane's uninteresting logs (FacebookSDK, ...) -- regex for -e parameter +// see https://developer.android.com/studio/command-line/logcat#filteringOutput +const defaultLogcatFilter = "(System|Unity)" + +var logcatProcess *exec.Cmd +var azurLanePID int +var psDelay time.Duration = 3 * time.Second + +func help() { + fmt.Println("belfast -- adb watcher help") + fmt.Println("?: print this help") + fmt.Println("l: list connected devices") + fmt.Println("c: clear terminal") + fmt.Println("s: start/stop logcat parsing") + fmt.Println("f: flush logcat") + fmt.Println("d: dump logcat buffer to a file") + fmt.Println("+: increase delay between ps commands (default: 3s)") + fmt.Println("-: decrease delay between ps commands (default: 3s)") + fmt.Println("=: print current delay between ps commands") + fmt.Println("x: exit adb watcher") +} + +// stupid way to clear the terminal, calls 'clear' on non-windows and 'cls' on windows +func clear() { + if runtime.GOOS == "windows" { + cmd := exec.Command("cmd", "/c", "cls") + cmd.Stdout = os.Stdout + cmd.Run() + } else { + cmd := exec.Command("clear") + cmd.Stdout = os.Stdout + cmd.Run() + } +} + +// runs 'adb devices' and prints the output +func listDevices() { + cmd := exec.Command("adb", "devices") + out, err := cmd.Output() + if err != nil { + logger.LogEvent("ADB", "ListDevices", "Failed to list devices", logger.LOG_LEVEL_ERROR) + return + } + fmt.Print(string(out)) +} + +// runs 'adb logcat -c' to flush logcat's buffer +func flushLogcat() { + cmd := exec.Command("adb", "logcat", "-c") + if err := cmd.Run(); err != nil { + logger.LogEvent("ADB", "Flush", "Failed to flush logcat", logger.LOG_LEVEL_ERROR) + return + } + logger.LogEvent("ADB", "FlushLogcat", "Logcat flushed", logger.LOG_LEVEL_INFO) +} + +// wrapper function to print logcat lines +func echoLog(line *string) { + fmt.Println(*line) +} + +func stopLogcat() { + if logcatProcess == nil { + return + } + pid := logcatProcess.Process.Pid + logcatProcess.Process.Kill() + logcatProcess = nil + logger.LogEvent("ADB", "Logcat", fmt.Sprintf("Logcat stopped (PID: %d)", pid), logger.LOG_LEVEL_INFO) +} + +// starts/stops logcat parsing +func toggleLogcat() { + if logcatProcess != nil { + stopLogcat() + return + } + go func() { + args := []string{"logcat"} + if azurLanePID != 0 { + args = append(args, "--pid", fmt.Sprintf("%d", azurLanePID), "-e", defaultLogcatFilter) + } else { + logger.LogEvent("ADB", "Logcat", fmt.Sprintf("Azur Lane PID not found, waiting %v to retry", psDelay), logger.LOG_LEVEL_INFO) + return + } + logcatProcess = exec.Command("adb", args...) + processStdout, err := logcatProcess.StdoutPipe() + if err != nil { + logger.LogEvent("ADB", "Logcat", fmt.Sprintf("Failed to get logcat stdout: %v", err), logger.LOG_LEVEL_ERROR) + return + } + + if err := logcatProcess.Start(); err != nil { + logger.LogEvent("ADB", "Logcat", fmt.Sprintf("Failed to start logcat: %v", err), logger.LOG_LEVEL_ERROR) + return + } + logger.LogEvent("ADB", "Logcat", fmt.Sprintf("Logcat started (PID: %d)", logcatProcess.Process.Pid), logger.LOG_LEVEL_INFO) + + // Parse logcat output in background + go func() { + scanner := bufio.NewScanner(processStdout) + for scanner.Scan() { + line := scanner.Text() + echoLog(&line) + } + if err := scanner.Err(); err != nil { + logger.LogEvent("ADB", "Logcat", fmt.Sprintf("Error reading logcat stdout: %v", err), logger.LOG_LEVEL_INFO) + } + }() + logcatProcess.Wait() + exitCode := logcatProcess.ProcessState.ExitCode() + if exitCode != 0 { + logger.LogEvent("ADB", "Logcat", fmt.Sprintf("Logcat process (PID: %d) exited with code %d", logcatProcess.Process.Pid, exitCode), logger.LOG_LEVEL_ERROR) + } + }() +} + +// increases by 1s the delay between ps commands +func increaseSleep() { + psDelay += 1 * time.Second + logger.LogEvent("Watcher", "Delay", fmt.Sprintf("Delay increased to %v", psDelay), logger.LOG_LEVEL_INFO) +} + +// decreases by 1s the delay between ps commands +func decreaseSleep() { + if psDelay > 1*time.Second { + psDelay -= 1 * time.Second + logger.LogEvent("Watcher", "Delay", fmt.Sprintf("Delay decreased to %v", psDelay), logger.LOG_LEVEL_INFO) + } else { + logger.LogEvent("Watcher", "Delay", "Delay cannot be decreased further, minimum is 1s", logger.LOG_LEVEL_INFO) + } +} + +// prints the current delay between ps commands +func printDelay() { + logger.LogEvent("ADB", "PrintDelay", fmt.Sprintf("Current delay: %v", psDelay), logger.LOG_LEVEL_INFO) +} + +// dump logcat buffer to a file +func dumpLogcat() { + cmd := exec.Command("adb", "logcat", "-d") + filename := time.Now().Format("2006-01-02_15-04-05") + "_belfast_logcat.log" + file, err := os.Create(filename) + if err != nil { + logger.LogEvent("ADB", "DumpLogcat", "Failed to create file", logger.LOG_LEVEL_ERROR) + return + } + cmd.Stdout = file + cmd.Stderr = file + if err := cmd.Run(); err != nil { + logger.LogEvent("ADB", "DumpLogcat", "Failed to dump logcat", logger.LOG_LEVEL_ERROR) + return + } + defer file.Close() + logger.LogEvent("ADB", "DumpLogcat", fmt.Sprintf("Logcat dumped to %s", filename), logger.LOG_LEVEL_INFO) +} + +// main routine for ADB watcher, listens for key presses and executes commands +func ADBRoutine(tty *tty.TTY, flush bool) { + if tty == nil { + return // silently return, main function will handle the error + } + // Checking if adb is installed / in PATH + _, err := exec.LookPath("adb") + if err != nil { + logger.LogEvent("ADB", "Init", "ADB not found in PATH", logger.LOG_LEVEL_ERROR) + return + } + logger.LogEvent("ADB", "Init", "ADB watcher started", logger.LOG_LEVEL_INFO) + if flush { + flushLogcat() + } + help() + + // Goroutine to find Azur Lane's PID + go func() { + for { + cmd := exec.Command("adb", "shell", "ps", "-A", "-o", "PID,NAME", "|", "grep", "-iE", grepRegex) + if out, err := cmd.Output(); err == nil { + for _, line := range strings.Split(string(out), "\n") { + newPid := 0 + fmt.Sscanf(line, "%d", &newPid) + if newPid != 0 && newPid != azurLanePID { + azurLanePID = newPid + logger.LogEvent("ADB", "Shell", fmt.Sprintf("Azur Lane PID: %d", azurLanePID), logger.LOG_LEVEL_INFO) + stopLogcat() // force stop logcat to restart with new PID + toggleLogcat() // restart logcat with new PID + } + } + } + time.Sleep(psDelay) + } + }() + + for { + // Read key from stdin + // If key is '?' print hello world + r, err := tty.ReadRune() + if err != nil { + logger.LogEvent("ADB", "ReadRune", fmt.Sprintf("Failed to read rune: %v", err), logger.LOG_LEVEL_ERROR) + break + } + if r == 'x' { + logger.LogEvent("ADB", "Exit", "ADB watcher exited", logger.LOG_LEVEL_INFO) + break + } + if handler, ok := handlers[string(r)]; ok { + handler() + } + } +} diff --git a/main.go b/main.go index bd5b6fc..9016ef9 100644 --- a/main.go +++ b/main.go @@ -9,6 +9,7 @@ import ( "github.com/akamensky/argparse" "github.com/ggmolly/belfast/answer" "github.com/ggmolly/belfast/connection" + "github.com/ggmolly/belfast/debug" "github.com/ggmolly/belfast/logger" "github.com/ggmolly/belfast/misc" "github.com/ggmolly/belfast/orm" @@ -16,6 +17,7 @@ import ( "github.com/ggmolly/belfast/protobuf" "github.com/ggmolly/belfast/web" "github.com/joho/godotenv" + "github.com/mattn/go-tty" "google.golang.org/protobuf/proto" ) @@ -34,6 +36,16 @@ func main() { Help: "Forces the reseed of the database with the latest data", Default: false, }) + adb := parser.Flag("a", "adb", &argparse.Options{ + Required: false, + Help: "Parse ADB logs for debugging purposes (experimental -- tested on Linux only)", + Default: false, + }) + flushLogcat := parser.Flag("f", "flush-logcat", &argparse.Options{ + Required: false, + Help: "Flush the logcat buffer upon starting the ADB watcher", + Default: false, + }) if err := parser.Parse(os.Args); err != nil { fmt.Print(parser.Usage(err)) os.Exit(1) @@ -43,6 +55,14 @@ func main() { misc.UpdateAllData(os.Getenv("AL_REGION")) } server := connection.NewServer("0.0.0.0", 80, packets.Dispatch) + + // Open TTY for adb controls + tty, err := tty.Open() + if err != nil { + log.Println("failed to open tty:", err) + log.Println("adb background routine will be disabled.") + return + } // wait for SIGINT sigChannel := make(chan os.Signal, 1) signal.Notify(sigChannel, os.Interrupt) @@ -50,6 +70,7 @@ func main() { <-sigChannel fmt.Printf("\r") // trick to avoid ^C in the terminal, could use low-level RawMode() but why bother server.Kill() + tty.Close() os.Exit(0) }() // Prepare web server @@ -57,7 +78,15 @@ func main() { web.StartWeb() }() - server.Run() + // Prepare adb background task + if *adb { + go debug.ADBRoutine(tty, *flushLogcat) + } + if err := server.Run(); err != nil { + logger.LogEvent("Server", "Run", fmt.Sprintf("%v", err), logger.LOG_LEVEL_ERROR) + tty.Close() + os.Exit(1) + } } func init() { @@ -88,19 +117,19 @@ func init() { packets.RegisterPacketHandler(10022, []packets.PacketHandler{answer.JoinServer}) packets.RegisterPacketHandler(10026, []packets.PacketHandler{answer.PlayerExist}) packets.RegisterPacketHandler(11001, []packets.PacketHandler{ - answer.LastLogin, // SC_11000 - answer.PlayerInfo, // SC_11003 - answer.PlayerBuffs, // SC_11015 - answer.GetMetaProgress, // SC_63315 - answer.LastOnlineInfo, // SC_11752 - answer.ResourcesInfo, // SC_22001 - answer.EventData, // SC_26120 - answer.Meowfficers, // SC_25001 - answer.CommanderCollection, // SC_17001 - answer.OngoingBuilds, // SC_12024 - answer.PlayerDock, // SC_12001 - answer.CommanderFleetA, // SC_12010 - // answer.UNK_12101, // SC_12101 + answer.LastLogin, // SC_11000 + answer.PlayerInfo, // SC_11003 + answer.PlayerBuffs, // SC_11015 + answer.GetMetaProgress, // SC_63315 + answer.LastOnlineInfo, // SC_11752 + answer.ResourcesInfo, // SC_22001 + answer.EventData, // SC_26120 + answer.Meowfficers, // SC_25001 + answer.CommanderCollection, // SC_17001 + answer.OngoingBuilds, // SC_12024 + answer.PlayerDock, // SC_12001 + answer.CommanderFleetA, // SC_12010 + answer.UNK_12101, // SC_12101 answer.CommanderOwnedSkins, // SC_12201 answer.UNK_63000, // SC_63000 answer.ShipyardData, // SC_63100