diff --git a/Makefile b/Makefile index 5cbc143..97f9b00 100644 --- a/Makefile +++ b/Makefile @@ -10,11 +10,27 @@ all: stat run: go run \ - nethack-launcher.go + nethack-launcher/check_dir.go \ + nethack-launcher/create_initial_files.go \ + nethack-launcher/create_user_files.go \ + nethack-launcher/janitor.go \ + nethack-launcher/nethack-launcher.go \ + nethack-launcher/print_change_password_screen.go \ + nethack-launcher/print_high_scores.go \ + nethack-launcher/print_login_screen.go \ + nethack-launcher/print_progress_screen.go \ + nethack-launcher/print_register_screen.go \ + nethack-launcher/print_user_screen.go \ + nethack-launcher/print_welcome_screen.go \ + nethack-launcher/recover_save.go \ + nethack-launcher/run_game.go \ + nethack-launcher/start_watcher.go + + stat: mkdir -p bin/ - $(CGOR) $(GOC) $(GOFLAGS) -o bin/nethack-launcher nethack-launcher.go + $(CGOR) $(GOC) $(GOFLAGS) -o bin/nethack-launcher nethack-launcher/*.go dependencies: go get github.com/gorilla/mux diff --git a/nethack-launcher.go b/nethack-launcher.go deleted file mode 100644 index dbf3a2d..0000000 --- a/nethack-launcher.go +++ /dev/null @@ -1,809 +0,0 @@ -package main - -// TODO split up into multiple files - -import ( - "bufio" - "fmt" - "io" - "io/ioutil" - "os" - "os/exec" - "path/filepath" - "strconv" - "strings" - "sync" - "time" - - "golang.org/x/crypto/sha3" - "gopkg.in/gcfg.v1" - "gopkg.in/redis.v5" -) - -type Config struct { - NethackLauncher struct { - Loglevel string - ServerDisplay string - NethackVersion string - HackDir string - NhdatLocation string - RecoverBinary string - BootstrapDelay time.Duration - } - - Redis struct { - Host string - Password string - } -} - -var ( - config = Config{} - wg sync.WaitGroup -) - -func main() { - // read conf file - readConf() - - // init redis connection - redisClient, redisErr := initRedisConnection() - if redisErr != nil { - time.Sleep(config.NethackLauncher.BootstrapDelay * time.Second) - redisClient, redisErr = initRedisConnection() - if redisErr != nil { - panic(redisErr) - } - } else { - } - - // create initial files needed by nethack - createInitialFiles() - - // start janitor - go janitor(redisClient) - - // start homescreen - screenFunction := printWelcomeScreen(redisClient) - fmt.Printf("screen %s recieved\n", screenFunction) -} - -func readConf() { - // init config file - err := gcfg.ReadFileInto(&config, "config.gcfg") - if err != nil { - panic(fmt.Sprintf("Could not load config.gcfg, error: %s\n", err)) - } -} - -func initRedisConnection() (*redis.Client, error) { - // init redis connection - redisClient := redis.NewClient(&redis.Options{ - Addr: config.Redis.Host, - Password: config.Redis.Password, - DB: 0, - }) - - _, redisErr := redisClient.Ping().Result() - return redisClient, redisErr -} - -func checkFiles() { - // make sure record file exists - if _, err := os.Stat(fmt.Sprintf("%s/record", config.NethackLauncher.HackDir)); os.IsNotExist(err) { - fmt.Printf("record file not found in %s/record\n", config.NethackLauncher.HackDir) - fmt.Printf("%s\n", err) - os.Exit(1) - } - // make sure initial rcfile exists - hackRCLoc := fmt.Sprintf("%s/.nethackrc", config.NethackLauncher.HackDir) - if _, err := os.Stat(hackRCLoc); os.IsNotExist(err) { - fmt.Printf("initial config file not found at: %s\n", hackRCLoc) - fmt.Printf("%s\n", err) - os.Exit(1) - } -} - -func clearScreen() { - cmd := exec.Command("clear") - cmd.Stdout = os.Stdout - cmd.Run() -} - -func printWelcomeScreen(redisClient *redis.Client) string { - clearScreen() - fmt.Printf(" %s\n", config.NethackLauncher.ServerDisplay) - println("") - println(" Not logged in.") - println("") - println(" l) Login") - println(" r) Register new user") - println(" w) Watch games in progress") - println(" h) View highscores") - println(" q) Quit") - println("") - fmt.Printf(">> ") - - // disable input buffering - exec.Command("stty", "-F", "/dev/tty", "cbreak", "min", "1").Run() - // do not display entered characters on the screen - exec.Command("stty", "-F", "/dev/tty", "-echo").Run() - - var b []byte = make([]byte, 1) - for { - os.Stdin.Read(b) - switch string(b) { - case "l": - // restart display - exec.Command("stty", "-F", "/dev/tty", "echo", "-cbreak").Run() - clearScreen() - printLoginScreen(redisClient) - case "r": - // restart display - exec.Command("stty", "-F", "/dev/tty", "echo", "-cbreak").Run() - clearScreen() - printRegisterScreen(redisClient) - case "w": - clearScreen() - printProgressScreen(redisClient, "") - case "h": - clearScreen() - printHighScores(redisClient, "") - case "q": - exec.Command("stty", "-F", "/dev/tty", "echo", "-cbreak").Run() - clearScreen() - os.Exit(0) - default: - } - } -} - -func printUserScreen(redisClient *redis.Client, username string) string { - clearScreen() - fmt.Printf(" %s\n", config.NethackLauncher.ServerDisplay) - println("") - fmt.Printf(" Logged in as: %s\n", username) - println("") - println(" l) Logout") - println(" c) Change password") - println(" w) Watch games in progress") - println(" h) View highscores") - println(" e) Edit config") - println(" r) Recover from crash") - fmt.Printf(" p) Play NetHack %s\n", config.NethackLauncher.NethackVersion) - println(" q) Quit") - println("") - fmt.Printf(">> ") - - // disable input buffering - exec.Command("stty", "-F", "/dev/tty", "cbreak", "min", "1").Run() - // do not display entered characters on the screen - exec.Command("stty", "-F", "/dev/tty", "-echo").Run() - - var b []byte = make([]byte, 1) - for { - os.Stdin.Read(b) - switch string(b) { - case "l": - // restart display - exec.Command("stty", "-F", "/dev/tty", "echo", "-cbreak").Run() - clearScreen() - printWelcomeScreen(redisClient) - case "e": - hackRCLoc := fmt.Sprintf("%s/user/%s/.nethackrc", config.NethackLauncher.HackDir, username) - exec.Command("stty", "-F", "/dev/tty", "echo", "-cbreak").Run() - clearScreen() - nh := exec.Command("vim", "-Z", hackRCLoc) - nh.Stdout = os.Stdout - nh.Stdin = os.Stdin - nh.Stderr = os.Stderr - nh.Run() - clearScreen() - printUserScreen(redisClient, username) - case "c": - printChangePasswordScreen(redisClient, username) - clearScreen() - case "w": - clearScreen() - printProgressScreen(redisClient, username) - case "h": - clearScreen() - printHighScores(redisClient, username) - case "p": - wg.Add(1) - currentTime := time.Now().UTC() - fulltime := currentTime.Format("2006-01-02.03:04:05") - go runGame(username, fulltime) - watcher := startWatcher(username, fulltime, redisClient) - wg.Wait() - close(watcher) - printUserScreen(redisClient, username) - case "r": - clearScreen() - recoverSave(redisClient, username) - case "q": - exec.Command("stty", "-F", "/dev/tty", "echo", "-cbreak").Run() - clearScreen() - os.Exit(0) - default: - } - } -} - -func printLoginScreen(redisClient *redis.Client) { - fmt.Printf(" %s\n", config.NethackLauncher.ServerDisplay) - println("") - println(" Please enter your username. (blank entry aborts)") - println("") - fmt.Printf(">> ") - - scanner := bufio.NewScanner(os.Stdin) - for scanner.Scan() { - if scanner.Text() == "" { - printWelcomeScreen(redisClient) - } - - // check redis for user - username := scanner.Text() - storedHash, err := redisClient.Get(fmt.Sprintf("user:%s", username)).Result() - if err != nil { - // user does not exist - fmt.Printf(" There was a problem with your last entry.\n>> ") - } else { - // get password from user and compare - // turn off echo display - exec.Command("stty", "-F", "/dev/tty", "-echo").Run() - - noPass := true - for noPass { - fmt.Printf("\n Please enter your password (blank entry aborts).\n>> ") - reader := bufio.NewReader(os.Stdin) - - typedAuth, _ := reader.ReadString('\n') - typedAuth = strings.Replace(typedAuth, "\n", "", -1) - - if typedAuth == "" { - // exit to main menu - printWelcomeScreen(redisClient) - } - - // get hash of typedAuth - typedHash := sha3.Sum512([]byte(typedAuth)) - if fmt.Sprintf("%x", typedHash) == storedHash { - // user authed - printUserScreen(redisClient, username) - noPass = false - } else { - fmt.Printf("\n There was a problem with your last entry.") - } - } - } - } - if err := scanner.Err(); err != nil { - fmt.Printf("%s\n", err) - } -} - -func printRegisterScreen(redisClient *redis.Client) { - // TODO : configure password restriction checking - fmt.Printf(" %s\n", config.NethackLauncher.ServerDisplay) - println("") - println(" Welcome new user. Please enter a username") - println(" Only characters and numbers are allowed, with no spaces.") - println(" 20 characters max. (blank entry aborts)") - println("") - fmt.Printf(">> ") - - scanner := bufio.NewScanner(os.Stdin) - for scanner.Scan() { - if scanner.Text() == "" { - printWelcomeScreen(redisClient) - } - - // check redis for user - username := scanner.Text() - _, err := redisClient.Get(fmt.Sprintf("user:%s", username)).Result() - if err != redis.Nil { - // user already exists - fmt.Printf("There was a problem with your last entry.\n>> ") - } else { - // set up user - - // turn off echo display - exec.Command("stty", "-F", "/dev/tty", "-echo").Run() - reader := bufio.NewReader(os.Stdin) - - noPass := true - sec := "" - for noPass { - // pull pass the first time - fmt.Printf(" Please enter your password (blank entry aborts).\n>> ") - sec0, _ := reader.ReadString('\n') - sec0 = strings.Replace(sec0, "\n", "", -1) - if sec0 == "" { - printWelcomeScreen(redisClient) - } - - // pull pass the second time - fmt.Printf("\n Please enter your password again.\n>> ") - sec1, _ := reader.ReadString('\n') - sec1 = strings.Replace(sec1, "\n", "", -1) - - // make sure passwords match - if sec0 == sec1 { - sec = sec0 - noPass = false - } else { - fmt.Println("Lets try that again.") - } - } - - // reset display - exec.Command("stty", "-F", "/dev/tty", "echo", "-cbreak").Run() - - // set user in redis - secHash := sha3.Sum512([]byte(sec)) - redisClient.Set(fmt.Sprintf("user:%s", username), fmt.Sprintf("%x", secHash), 0).Err() - - // create user directories - userPath := fmt.Sprintf("%s/user/%s/ttyrec/", config.NethackLauncher.HackDir, username) - exec.Command("mkdir", "-p", userPath).Run() - - // copy in rc file - hackRCLoc := fmt.Sprintf("%s/.nethackrc", config.NethackLauncher.HackDir) - hackRCDest := fmt.Sprintf("%s/user/%s/.nethackrc", config.NethackLauncher.HackDir, username) - exec.Command("cp", hackRCLoc, hackRCDest).Run() - - // TODO: move the above creation code into the createUserFiles() function - createUserFiles(username) - - // back to main screen - printUserScreen(redisClient, username) - } - } - if err := scanner.Err(); err != nil { - fmt.Printf("%s\n", err) - } -} - -func printChangePasswordScreen(redisClient *redis.Client, username string) { - // TODO : configure password restriction checking - clearScreen() - fmt.Printf(" %s\n", config.NethackLauncher.ServerDisplay) - println("") - fmt.Printf(" Welcome %s\n", username) - println(" Only characters and numbers are allowed, with no spaces.") - println(" 20 characters max. (blank entry aborts)") - println("") - //fmt.Printf(">> ") - - scanner := bufio.NewScanner(os.Stdin) - //for scanner.Scan() { - // if scanner.Text() == "" { - // printUserScreen(redisClient, username) - // } - - // turn off echo display - exec.Command("stty", "-F", "/dev/tty", "-echo").Run() - reader := bufio.NewReader(os.Stdin) - - noPass := true - sec := "" - for noPass { - // pull pass the first time - fmt.Printf(" Please enter your password (blank entry aborts).\n>> ") - sec0, _ := reader.ReadString('\n') - sec0 = strings.Replace(sec0, "\n", "", -1) - if sec0 == "" { - printWelcomeScreen(redisClient) - } - - // pull pass the second time - fmt.Printf("\n Please enter your password again.\n>> ") - sec1, _ := reader.ReadString('\n') - sec1 = strings.Replace(sec1, "\n", "", -1) - - // make sure passwords match - if sec0 == sec1 { - sec = sec0 - noPass = false - } else { - fmt.Println("Lets try that again.") - } - } - - // reset display - exec.Command("stty", "-F", "/dev/tty", "echo", "-cbreak").Run() - - // set user in redis - secHash := sha3.Sum512([]byte(sec)) - redisClient.Set(fmt.Sprintf("user:%s", username), fmt.Sprintf("%x", secHash), 0).Err() - - // back to main screen - printUserScreen(redisClient, username) - //} - if err := scanner.Err(); err != nil { - fmt.Printf("%s\n", err) - } -} - -func printProgressScreen(redisClient *redis.Client, username string) { - // print header - fmt.Printf(" %s\n", config.NethackLauncher.ServerDisplay) - println("") - - // check directory for live players - isNotEmpty, err := redisClient.Exists("inprogress").Result() - if err != nil { - panic(err) - } - if isNotEmpty { - println(" Choose a player to spectate ('enter' without selection returns)") - } else { - println(" No live players currently (blank entry returns)") - } - println("") - - inProg := make(map[int]string) - inProgTimer := 1 - inprogress, err := redisClient.SMembers("inprogress").Result() - if err != nil { - panic(err) - } - for _, i := range inprogress { - // loop through users - fmt.Printf(" %d) %s\n", inProgTimer, i) - - // add user to line map - inProg[inProgTimer] = i - inProgTimer++ - } - - println("") - fmt.Printf(">> ") - // start user input - - // disable input buffering - exec.Command("stty", "-F", "/dev/tty", "cbreak", "min", "1").Run() - // do not display entered characters on the screen - exec.Command("stty", "-F", "/dev/tty", "-echo").Run() - - var b []byte = make([]byte, 1) - for { - os.Stdin.Read(b) - s, _ := strconv.Atoi(string(b)) - - // check if user is trying to navigate - if string(b) == "\n" { - exec.Command("stty", "-F", "/dev/tty", "echo", "-cbreak").Run() - clearScreen() - if username == "" { - printWelcomeScreen(redisClient) - } else { - printUserScreen(redisClient, username) - } - } - - // check if selection is in out map - if inProg[s] != "" { - user := strings.Split(inProg[s], ":") - fmt.Printf("going to spectate '%s'\n", user[0]) - - // set ttyrec path - ttyName, _ := redisClient.Get(fmt.Sprintf("inprogress:%s", user[0])).Result() - ttyrecPath := fmt.Sprintf("%s/user/%s/ttyrec/%s.ttyrec", config.NethackLauncher.HackDir, user[0], ttyName) - - // restart display - exec.Command("stty", "-F", "/dev/tty", "echo", "-cbreak").Run() - clearScreen() - nh := exec.Command("ttyplay", "-p", ttyrecPath) - //nh := exec.Command("termplay", "-f", "live", ttyrecPath) - nh.Stdout = os.Stdout - nh.Stdin = os.Stdin - nh.Stderr = os.Stderr - nh.Run() - if username == "" { - printWelcomeScreen(redisClient) - } else { - printUserScreen(redisClient, username) - } - // TODO fix bug where user has to when game exists - } - } -} - -func runGame(username, timestamp string) { - exec.Command("stty", "-F", "/dev/tty", "echo", "-cbreak").Run() - clearScreen() - - // put together users home dir - homeDir := fmt.Sprintf("%s/user/%s/", config.NethackLauncher.HackDir, username) - ttyrecPath := fmt.Sprintf("%s/user/%s/ttyrec/%s.ttyrec", config.NethackLauncher.HackDir, username, timestamp) - - nh := exec.Command("ttyrec", "-f", ttyrecPath, "--", "nethack") - nh.Env = os.Environ() - nh.Env = append(nh.Env, fmt.Sprintf("HOME=%s", homeDir)) - nh.Env = append(nh.Env, fmt.Sprintf("USER=%s", username)) - nh.Env = append(nh.Env, fmt.Sprintf("NETHACKDIR=%s/user/%s", config.NethackLauncher.HackDir, username)) - nh.Stdout = os.Stdout - nh.Stdin = os.Stdin - nh.Stderr = os.Stderr - err := nh.Run() - if err != nil { - fmt.Print(err) - } - exec.Command("exit").Run() - wg.Done() -} - -func startWatcher(username, timestamp string, redisClient *redis.Client) chan struct{} { - // create initial keys - redisClient.SAdd("inprogress", username) - redisClient.Set(fmt.Sprintf("inprogress:%s", username), timestamp, 0) - - ch := make(chan struct{}) - // enter inital inprogress yet - go func() { - for { - select { - case <-ch: - return - default: - redisClient.Expire(fmt.Sprintf("inprogress:%s", username), 10*time.Second) - time.Sleep(8 * time.Second) - } - } - }() - return ch -} - -func createInitialFiles() { - // create necessary directories if they dont exist - if _, err := os.Stat(config.NethackLauncher.HackDir); os.IsNotExist(err) { - os.Mkdir(config.NethackLauncher.HackDir, os.ModeDir) - } - if _, err := os.Stat(fmt.Sprintf("%s/dumps/", config.NethackLauncher.HackDir)); os.IsNotExist(err) { - os.Mkdir(fmt.Sprintf("%s/dumps/", config.NethackLauncher.HackDir), os.ModeDir) - } - if _, err := os.Stat(fmt.Sprintf("%s/save/", config.NethackLauncher.HackDir)); os.IsNotExist(err) { - os.Mkdir(fmt.Sprintf("%s/save/", config.NethackLauncher.HackDir), os.ModeDir) - } - - // create necessary files if they dont exist - os.OpenFile(fmt.Sprintf("%s/logfile", config.NethackLauncher.HackDir), os.O_RDONLY|os.O_CREATE, 0666) - os.OpenFile(fmt.Sprintf("%s/perm", config.NethackLauncher.HackDir), os.O_RDONLY|os.O_CREATE, 0666) - os.OpenFile(fmt.Sprintf("%s/record", config.NethackLauncher.HackDir), os.O_RDONLY|os.O_CREATE, 0666) - os.OpenFile(fmt.Sprintf("%s/xlogfile", config.NethackLauncher.HackDir), os.O_RDONLY|os.O_CREATE, 0666) - - // move in nhdat file if it does not exist - if _, err := os.Stat(fmt.Sprintf("%s/nhdat", config.NethackLauncher.HackDir)); os.IsNotExist(err) { - exec.Command("cp", config.NethackLauncher.NhdatLocation, config.NethackLauncher.HackDir).Run() - } - - // make sure initial rcfile exists - hackRCLoc := fmt.Sprintf("%s/.nethackrc", config.NethackLauncher.HackDir) - if _, err := os.Stat(hackRCLoc); os.IsNotExist(err) { - fmt.Printf("initial config file not found at: %s\n", hackRCLoc) - fmt.Printf("%s\n", err) - - // check root and move in if applicable - if _, err := os.Stat("/.nethackrc"); os.IsNotExist(err) { - fmt.Printf("initial config file not found at root\n") - fmt.Printf("%s\n", err) - os.Exit(1) - } else { - // move nethackrc file to proper location - exec.Command("cp", "/.nethackrc", config.NethackLauncher.HackDir).Run() - } - - } -} - -func createUserFiles(username string) { - // create necessary directories if they dont exist - userpath := fmt.Sprintf("%s/user/%s", config.NethackLauncher.HackDir, username) - - if _, err := os.Stat(fmt.Sprintf("%s/dumps/", userpath)); os.IsNotExist(err) { - os.Mkdir(fmt.Sprintf("%s/dumps/", userpath), os.ModeDir) - } - if _, err := os.Stat(fmt.Sprintf("%s/save/", userpath)); os.IsNotExist(err) { - os.Mkdir(fmt.Sprintf("%s/save/", userpath), os.ModeDir) - } - - // create necessary files if they dont exist - os.OpenFile(fmt.Sprintf("%s/logfile", userpath), os.O_RDONLY|os.O_CREATE, 0666) - os.OpenFile(fmt.Sprintf("%s/perm", userpath), os.O_RDONLY|os.O_CREATE, 0666) - os.OpenFile(fmt.Sprintf("%s/record", userpath), os.O_RDONLY|os.O_CREATE, 0666) - os.OpenFile(fmt.Sprintf("%s/xlogfile", userpath), os.O_RDONLY|os.O_CREATE, 0666) - - // move in nhdat file if it does not exist - exec.Command("cp", config.NethackLauncher.NhdatLocation, userpath).Run() -} - -func recoverSave(redisClient *redis.Client, username string) { - // if no statefile exist, exit early as we cannot recover - matches, _ := filepath.Glob(fmt.Sprintf("%s/user/%s/*lock*", config.NethackLauncher.HackDir, username)) - if matches == nil { - fmt.Printf(" %s\n", config.NethackLauncher.ServerDisplay) - println("") - println(" The dungeon crumbles around you, darkness remains...") - println(" Godspeed adventurer..") - println("") - println(" No statefiles exist. (press enter to return)") - println("") - fmt.Printf(">> ") - - scanner := bufio.NewScanner(os.Stdin) - for scanner.Scan() { - if scanner.Text() == "" { - printUserScreen(redisClient, username) - } - } - } - - fmt.Printf(" %s\n", config.NethackLauncher.ServerDisplay) - println("") - println(" Recover utility.") - println("") - // check if dir is empty - isEmpty, _ := checkDir(fmt.Sprintf("%s/user/%s/save/", config.NethackLauncher.HackDir, username)) - if !isEmpty { - // save file exists, overwrite? - println(" Save file already exists!") - println(" Overwrite? (y/n)") - println("") - fmt.Printf(">> ") - } else { - println(" Attempt to recover statefile? (y/n)") - println("") - fmt.Printf(">> ") - } - - // disable input buffering - exec.Command("stty", "-F", "/dev/tty", "cbreak", "min", "1").Run() - // do not display entered characters on the screen - exec.Command("stty", "-F", "/dev/tty", "-echo").Run() - - var b []byte = make([]byte, 1) - for { - os.Stdin.Read(b) - switch string(b) { - case "n": - clearScreen() - printUserScreen(redisClient, username) - case "y": - // set user path to save file candidates - userPath := fmt.Sprintf("%s/user/%s/", config.NethackLauncher.HackDir, username) - - // read in all files - candidates, err := ioutil.ReadDir(userPath) - if err != nil { - fmt.Println(err) - } - var newestFile string - var newestTime int64 = 0 - for _, f := range candidates { - // loop through save file candidates - if strings.Contains(f.Name(), "lock") { - fi, err := os.Stat(userPath + f.Name()) - if err != nil { - fmt.Println(err) - } - currTime := fi.ModTime().Unix() - if currTime > newestTime { - newestTime = currTime - newestFile = f.Name() - } - } - } - - // get prefix for latest file - latestFile := strings.Split(newestFile, ".") - //fmt.Printf("recover string: %s -d %s/user/%s %s", config.NethackLauncher.RecoverBinary, config.NethackLauncher.HackDir, username, latestFile[0]) - - // run recover on file - out, err := exec.Command(config.NethackLauncher.RecoverBinary, "-d", fmt.Sprintf("%s/user/%s", config.NethackLauncher.HackDir, username), latestFile[0]).Output() - if err != nil { - fmt.Println(out) - panic(err) - } - - // make sure save file exists before removing the extra locks - isEmpty, _ = checkDir(fmt.Sprintf("%s/user/%s/save/", config.NethackLauncher.HackDir, username)) - if !isEmpty { - // save file made it, clean up old cruft - files, err := filepath.Glob(fmt.Sprintf("%s/*lock*", userPath)) - if err != nil { - panic(err) - } - for _, f := range files { - if err := os.Remove(f); err != nil { - panic(err) - } - } - // alert user that the save file was recovered - println("") - println(" Statefile was recovered successfully! (press enter to return)") - fmt.Printf(">> ") - } - case "\n": - clearScreen() - printUserScreen(redisClient, username) - default: - } - } -} - -func janitor(redisClient *redis.Client) { - // loop through the set - for { - inprogress, err := redisClient.SMembers("inprogress").Result() - if err != nil { - panic(err) - } - for _, i := range inprogress { - // for each user, make sure the expire key exists - exists, err := redisClient.Exists(fmt.Sprintf("inprogress:%s", i)).Result() - if err != nil { - panic(err) - } - if !exists { - redisClient.SRem("inprogress", i) - } - } - - time.Sleep(10 * time.Second) - } -} - -func printHighScores(redisClient *redis.Client, username string) { - exec.Command("stty", "-F", "/dev/tty", "echo", "-cbreak").Run() - clearScreen() - - nh := exec.Command("nethack", "-d", config.NethackLauncher.HackDir, "-s") - nh.Stdout = os.Stdout - nh.Stdin = os.Stdin - nh.Stderr = os.Stderr - nh.Run() - - println("") - println(" Press enter to return to menu") - println("") - fmt.Printf(">> ") - - // allow user back to home screen - // disable input buffering - exec.Command("stty", "-F", "/dev/tty", "cbreak", "min", "1").Run() - // do not display entered characters on the screen - exec.Command("stty", "-F", "/dev/tty", "-echo").Run() - - var b []byte = make([]byte, 1) - for { - os.Stdin.Read(b) - - // check if user is trying to navigate - if string(b) == "\n" { - exec.Command("stty", "-F", "/dev/tty", "echo", "-cbreak").Run() - clearScreen() - if username == "" { - printWelcomeScreen(redisClient) - } else { - printUserScreen(redisClient, username) - } - } - } -} - -func checkDir(dirName string) (bool, error) { - f, err := os.Open(dirName) - if err != nil { - return false, err - } - defer f.Close() - - _, err = f.Readdirnames(1) - if err == io.EOF { - return true, nil - } - return false, err -} diff --git a/nethack-launcher/check_dir.go b/nethack-launcher/check_dir.go new file mode 100644 index 0000000..70f1c42 --- /dev/null +++ b/nethack-launcher/check_dir.go @@ -0,0 +1,20 @@ +package main + +import ( + "io" + "os" +) + +func checkDir(dirName string) (bool, error) { + f, err := os.Open(dirName) + if err != nil { + return false, err + } + defer f.Close() + + _, err = f.Readdirnames(1) + if err == io.EOF { + return true, nil + } + return false, err +} diff --git a/nethack-launcher/create_initial_files.go b/nethack-launcher/create_initial_files.go new file mode 100644 index 0000000..c352d1e --- /dev/null +++ b/nethack-launcher/create_initial_files.go @@ -0,0 +1,49 @@ +package main + +import ( + "fmt" + "os" + "os/exec" +) + +func createInitialFiles() { + // create necessary directories if they dont exist + if _, err := os.Stat(config.NethackLauncher.HackDir); os.IsNotExist(err) { + os.Mkdir(config.NethackLauncher.HackDir, os.ModeDir) + } + if _, err := os.Stat(fmt.Sprintf("%s/dumps/", config.NethackLauncher.HackDir)); os.IsNotExist(err) { + os.Mkdir(fmt.Sprintf("%s/dumps/", config.NethackLauncher.HackDir), os.ModeDir) + } + if _, err := os.Stat(fmt.Sprintf("%s/save/", config.NethackLauncher.HackDir)); os.IsNotExist(err) { + os.Mkdir(fmt.Sprintf("%s/save/", config.NethackLauncher.HackDir), os.ModeDir) + } + + // create necessary files if they dont exist + os.OpenFile(fmt.Sprintf("%s/logfile", config.NethackLauncher.HackDir), os.O_RDONLY|os.O_CREATE, 0666) + os.OpenFile(fmt.Sprintf("%s/perm", config.NethackLauncher.HackDir), os.O_RDONLY|os.O_CREATE, 0666) + os.OpenFile(fmt.Sprintf("%s/record", config.NethackLauncher.HackDir), os.O_RDONLY|os.O_CREATE, 0666) + os.OpenFile(fmt.Sprintf("%s/xlogfile", config.NethackLauncher.HackDir), os.O_RDONLY|os.O_CREATE, 0666) + + // move in nhdat file if it does not exist + if _, err := os.Stat(fmt.Sprintf("%s/nhdat", config.NethackLauncher.HackDir)); os.IsNotExist(err) { + exec.Command("cp", config.NethackLauncher.NhdatLocation, config.NethackLauncher.HackDir).Run() + } + + // make sure initial rcfile exists + hackRCLoc := fmt.Sprintf("%s/.nethackrc", config.NethackLauncher.HackDir) + if _, err := os.Stat(hackRCLoc); os.IsNotExist(err) { + fmt.Printf("initial config file not found at: %s\n", hackRCLoc) + fmt.Printf("%s\n", err) + + // check root and move in if applicable + if _, err := os.Stat("/.nethackrc"); os.IsNotExist(err) { + fmt.Printf("initial config file not found at root\n") + fmt.Printf("%s\n", err) + os.Exit(1) + } else { + // move nethackrc file to proper location + exec.Command("cp", "/.nethackrc", config.NethackLauncher.HackDir).Run() + } + + } +} diff --git a/nethack-launcher/create_user_files.go b/nethack-launcher/create_user_files.go new file mode 100644 index 0000000..c50c02d --- /dev/null +++ b/nethack-launcher/create_user_files.go @@ -0,0 +1,28 @@ +package main + +import ( + "fmt" + "os" + "os/exec" +) + +func createUserFiles(username string) { + // create necessary directories if they dont exist + userpath := fmt.Sprintf("%s/user/%s", config.NethackLauncher.HackDir, username) + + if _, err := os.Stat(fmt.Sprintf("%s/dumps/", userpath)); os.IsNotExist(err) { + os.Mkdir(fmt.Sprintf("%s/dumps/", userpath), os.ModeDir) + } + if _, err := os.Stat(fmt.Sprintf("%s/save/", userpath)); os.IsNotExist(err) { + os.Mkdir(fmt.Sprintf("%s/save/", userpath), os.ModeDir) + } + + // create necessary files if they dont exist + os.OpenFile(fmt.Sprintf("%s/logfile", userpath), os.O_RDONLY|os.O_CREATE, 0666) + os.OpenFile(fmt.Sprintf("%s/perm", userpath), os.O_RDONLY|os.O_CREATE, 0666) + os.OpenFile(fmt.Sprintf("%s/record", userpath), os.O_RDONLY|os.O_CREATE, 0666) + os.OpenFile(fmt.Sprintf("%s/xlogfile", userpath), os.O_RDONLY|os.O_CREATE, 0666) + + // move in nhdat file if it does not exist + exec.Command("cp", config.NethackLauncher.NhdatLocation, userpath).Run() +} diff --git a/nethack-launcher/janitor.go b/nethack-launcher/janitor.go new file mode 100644 index 0000000..f102080 --- /dev/null +++ b/nethack-launcher/janitor.go @@ -0,0 +1,30 @@ +package main + +import ( + "fmt" + "time" + + "gopkg.in/redis.v5" +) + +func janitor(redisClient *redis.Client) { + // loop through the set + for { + inprogress, err := redisClient.SMembers("inprogress").Result() + if err != nil { + panic(err) + } + for _, i := range inprogress { + // for each user, make sure the expire key exists + exists, err := redisClient.Exists(fmt.Sprintf("inprogress:%s", i)).Result() + if err != nil { + panic(err) + } + if !exists { + redisClient.SRem("inprogress", i) + } + } + + time.Sleep(10 * time.Second) + } +} diff --git a/nethack-launcher/nethack-launcher.go b/nethack-launcher/nethack-launcher.go new file mode 100644 index 0000000..2ef8eff --- /dev/null +++ b/nethack-launcher/nethack-launcher.go @@ -0,0 +1,104 @@ +package main + +// TODO on runtime "Old lockfile found. Recover/Delete? (r/d) + +import ( + "fmt" + "os" + "os/exec" + "sync" + "time" + + "gopkg.in/gcfg.v1" + "gopkg.in/redis.v5" +) + +type Config struct { + NethackLauncher struct { + Loglevel string + ServerDisplay string + NethackVersion string + HackDir string + NhdatLocation string + RecoverBinary string + BootstrapDelay time.Duration + } + + Redis struct { + Host string + Password string + } +} + +var ( + config = Config{} + wg sync.WaitGroup +) + +func main() { + // read conf file + readConf() + + // init redis connection + redisClient, redisErr := initRedisConnection() + if redisErr != nil { + time.Sleep(config.NethackLauncher.BootstrapDelay * time.Second) + redisClient, redisErr = initRedisConnection() + if redisErr != nil { + panic(redisErr) + } + } else { + } + + // create initial files needed by nethack + createInitialFiles() + + // start janitor + go janitor(redisClient) + + // start homescreen + screenFunction := printWelcomeScreen(redisClient) + fmt.Printf("screen %s recieved\n", screenFunction) +} + +func readConf() { + // init config file + err := gcfg.ReadFileInto(&config, "config.gcfg") + if err != nil { + panic(fmt.Sprintf("Could not load config.gcfg, error: %s\n", err)) + } +} + +func initRedisConnection() (*redis.Client, error) { + // init redis connection + redisClient := redis.NewClient(&redis.Options{ + Addr: config.Redis.Host, + Password: config.Redis.Password, + DB: 0, + }) + + _, redisErr := redisClient.Ping().Result() + return redisClient, redisErr +} + +func checkFiles() { + // make sure record file exists + if _, err := os.Stat(fmt.Sprintf("%s/record", config.NethackLauncher.HackDir)); os.IsNotExist(err) { + fmt.Printf("record file not found in %s/record\n", config.NethackLauncher.HackDir) + fmt.Printf("%s\n", err) + os.Exit(1) + } + // make sure initial rcfile exists + hackRCLoc := fmt.Sprintf("%s/.nethackrc", config.NethackLauncher.HackDir) + if _, err := os.Stat(hackRCLoc); os.IsNotExist(err) { + fmt.Printf("initial config file not found at: %s\n", hackRCLoc) + fmt.Printf("%s\n", err) + os.Exit(1) + } +} + +func clearScreen() { + cmd := exec.Command("clear") + cmd.Stdout = os.Stdout + cmd.Run() +} diff --git a/nethack-launcher/print_change_password_screen.go b/nethack-launcher/print_change_password_screen.go new file mode 100644 index 0000000..6b58a05 --- /dev/null +++ b/nethack-launcher/print_change_password_screen.go @@ -0,0 +1,73 @@ +package main + +import ( + "bufio" + "fmt" + "os" + "os/exec" + "strings" + + "golang.org/x/crypto/sha3" + "gopkg.in/redis.v5" +) + +func printChangePasswordScreen(redisClient *redis.Client, username string) { + // TODO : configure password restriction checking + clearScreen() + fmt.Printf(" %s\n", config.NethackLauncher.ServerDisplay) + println("") + fmt.Printf(" Welcome %s\n", username) + println(" Only characters and numbers are allowed, with no spaces.") + println(" 20 characters max. (blank entry aborts)") + println("") + //fmt.Printf(">> ") + + scanner := bufio.NewScanner(os.Stdin) + //for scanner.Scan() { + // if scanner.Text() == "" { + // printUserScreen(redisClient, username) + // } + + // turn off echo display + exec.Command("stty", "-F", "/dev/tty", "-echo").Run() + reader := bufio.NewReader(os.Stdin) + + noPass := true + sec := "" + for noPass { + // pull pass the first time + fmt.Printf(" Please enter your password (blank entry aborts).\n>> ") + sec0, _ := reader.ReadString('\n') + sec0 = strings.Replace(sec0, "\n", "", -1) + if sec0 == "" { + printWelcomeScreen(redisClient) + } + + // pull pass the second time + fmt.Printf("\n Please enter your password again.\n>> ") + sec1, _ := reader.ReadString('\n') + sec1 = strings.Replace(sec1, "\n", "", -1) + + // make sure passwords match + if sec0 == sec1 { + sec = sec0 + noPass = false + } else { + fmt.Println("Lets try that again.") + } + } + + // reset display + exec.Command("stty", "-F", "/dev/tty", "echo", "-cbreak").Run() + + // set user in redis + secHash := sha3.Sum512([]byte(sec)) + redisClient.Set(fmt.Sprintf("user:%s", username), fmt.Sprintf("%x", secHash), 0).Err() + + // back to main screen + printUserScreen(redisClient, username) + //} + if err := scanner.Err(); err != nil { + fmt.Printf("%s\n", err) + } +} diff --git a/nethack-launcher/print_high_scores.go b/nethack-launcher/print_high_scores.go new file mode 100644 index 0000000..75872b5 --- /dev/null +++ b/nethack-launcher/print_high_scores.go @@ -0,0 +1,47 @@ +package main + +import ( + "fmt" + "os" + "os/exec" + + "gopkg.in/redis.v5" +) + +func printHighScores(redisClient *redis.Client, username string) { + exec.Command("stty", "-F", "/dev/tty", "echo", "-cbreak").Run() + clearScreen() + + nh := exec.Command("nethack", "-d", config.NethackLauncher.HackDir, "-s") + nh.Stdout = os.Stdout + nh.Stdin = os.Stdin + nh.Stderr = os.Stderr + nh.Run() + + println("") + println(" Press enter to return to menu") + println("") + fmt.Printf(">> ") + + // allow user back to home screen + // disable input buffering + exec.Command("stty", "-F", "/dev/tty", "cbreak", "min", "1").Run() + // do not display entered characters on the screen + exec.Command("stty", "-F", "/dev/tty", "-echo").Run() + + var b []byte = make([]byte, 1) + for { + os.Stdin.Read(b) + + // check if user is trying to navigate + if string(b) == "\n" { + exec.Command("stty", "-F", "/dev/tty", "echo", "-cbreak").Run() + clearScreen() + if username == "" { + printWelcomeScreen(redisClient) + } else { + printUserScreen(redisClient, username) + } + } + } +} diff --git a/nethack-launcher/print_login_screen.go b/nethack-launcher/print_login_screen.go new file mode 100644 index 0000000..f82e33f --- /dev/null +++ b/nethack-launcher/print_login_screen.go @@ -0,0 +1,66 @@ +package main + +import ( + "bufio" + "fmt" + "os" + "os/exec" + "strings" + + "golang.org/x/crypto/sha3" + "gopkg.in/redis.v5" +) + +func printLoginScreen(redisClient *redis.Client) { + fmt.Printf(" %s\n", config.NethackLauncher.ServerDisplay) + println("") + println(" Please enter your username. (blank entry aborts)") + println("") + fmt.Printf(">> ") + + scanner := bufio.NewScanner(os.Stdin) + for scanner.Scan() { + if scanner.Text() == "" { + printWelcomeScreen(redisClient) + } + + // check redis for user + username := scanner.Text() + storedHash, err := redisClient.Get(fmt.Sprintf("user:%s", username)).Result() + if err != nil { + // user does not exist + fmt.Printf(" There was a problem with your last entry.\n>> ") + } else { + // get password from user and compare + // turn off echo display + exec.Command("stty", "-F", "/dev/tty", "-echo").Run() + + noPass := true + for noPass { + fmt.Printf("\n Please enter your password (blank entry aborts).\n>> ") + reader := bufio.NewReader(os.Stdin) + + typedAuth, _ := reader.ReadString('\n') + typedAuth = strings.Replace(typedAuth, "\n", "", -1) + + if typedAuth == "" { + // exit to main menu + printWelcomeScreen(redisClient) + } + + // get hash of typedAuth + typedHash := sha3.Sum512([]byte(typedAuth)) + if fmt.Sprintf("%x", typedHash) == storedHash { + // user authed + printUserScreen(redisClient, username) + noPass = false + } else { + fmt.Printf("\n There was a problem with your last entry.") + } + } + } + } + if err := scanner.Err(); err != nil { + fmt.Printf("%s\n", err) + } +} diff --git a/nethack-launcher/print_progress_screen.go b/nethack-launcher/print_progress_screen.go new file mode 100644 index 0000000..e88f3c7 --- /dev/null +++ b/nethack-launcher/print_progress_screen.go @@ -0,0 +1,96 @@ +package main + +import ( + "fmt" + "os" + "os/exec" + "strconv" + "strings" + + "gopkg.in/redis.v5" +) + +func printProgressScreen(redisClient *redis.Client, username string) { + // print header + fmt.Printf(" %s\n", config.NethackLauncher.ServerDisplay) + println("") + + // check directory for live players + isNotEmpty, err := redisClient.Exists("inprogress").Result() + if err != nil { + panic(err) + } + if isNotEmpty { + println(" Choose a player to spectate ('enter' without selection returns)") + } else { + println(" No live players currently (blank entry returns)") + } + println("") + + inProg := make(map[int]string) + inProgTimer := 1 + inprogress, err := redisClient.SMembers("inprogress").Result() + if err != nil { + panic(err) + } + for _, i := range inprogress { + // loop through users + fmt.Printf(" %d) %s\n", inProgTimer, i) + + // add user to line map + inProg[inProgTimer] = i + inProgTimer++ + } + + println("") + fmt.Printf(">> ") + // start user input + + // disable input buffering + exec.Command("stty", "-F", "/dev/tty", "cbreak", "min", "1").Run() + // do not display entered characters on the screen + exec.Command("stty", "-F", "/dev/tty", "-echo").Run() + + var b []byte = make([]byte, 1) + for { + os.Stdin.Read(b) + s, _ := strconv.Atoi(string(b)) + + // check if user is trying to navigate + if string(b) == "\n" { + exec.Command("stty", "-F", "/dev/tty", "echo", "-cbreak").Run() + clearScreen() + if username == "" { + printWelcomeScreen(redisClient) + } else { + printUserScreen(redisClient, username) + } + } + + // check if selection is in out map + if inProg[s] != "" { + user := strings.Split(inProg[s], ":") + fmt.Printf("going to spectate '%s'\n", user[0]) + + // set ttyrec path + ttyName, _ := redisClient.Get(fmt.Sprintf("inprogress:%s", user[0])).Result() + ttyrecPath := fmt.Sprintf("%s/user/%s/ttyrec/%s.ttyrec", config.NethackLauncher.HackDir, user[0], ttyName) + + // restart display + exec.Command("stty", "-F", "/dev/tty", "echo", "-cbreak").Run() + clearScreen() + nh := exec.Command("ttyplay", "-p", ttyrecPath) + //nh := exec.Command("termplay", "-f", "live", ttyrecPath) + nh.Stdout = os.Stdout + nh.Stdin = os.Stdin + nh.Stderr = os.Stderr + nh.Run() + if username == "" { + printWelcomeScreen(redisClient) + } else { + printUserScreen(redisClient, username) + } + // TODO fix bug where user has to when game exists + } + } +} diff --git a/nethack-launcher/print_register_screen.go b/nethack-launcher/print_register_screen.go new file mode 100644 index 0000000..5b527c9 --- /dev/null +++ b/nethack-launcher/print_register_screen.go @@ -0,0 +1,94 @@ +package main + +import ( + "bufio" + "fmt" + "os" + "os/exec" + "strings" + + "golang.org/x/crypto/sha3" + "gopkg.in/redis.v5" +) + +func printRegisterScreen(redisClient *redis.Client) { + // TODO : configure password restriction checking + fmt.Printf(" %s\n", config.NethackLauncher.ServerDisplay) + println("") + println(" Welcome new user. Please enter a username") + println(" Only characters and numbers are allowed, with no spaces.") + println(" 20 characters max. (blank entry aborts)") + println("") + fmt.Printf(">> ") + + scanner := bufio.NewScanner(os.Stdin) + for scanner.Scan() { + if scanner.Text() == "" { + printWelcomeScreen(redisClient) + } + + // check redis for user + username := scanner.Text() + _, err := redisClient.Get(fmt.Sprintf("user:%s", username)).Result() + if err != redis.Nil { + // user already exists + fmt.Printf("There was a problem with your last entry.\n>> ") + } else { + // set up user + + // turn off echo display + exec.Command("stty", "-F", "/dev/tty", "-echo").Run() + reader := bufio.NewReader(os.Stdin) + + noPass := true + sec := "" + for noPass { + // pull pass the first time + fmt.Printf(" Please enter your password (blank entry aborts).\n>> ") + sec0, _ := reader.ReadString('\n') + sec0 = strings.Replace(sec0, "\n", "", -1) + if sec0 == "" { + printWelcomeScreen(redisClient) + } + + // pull pass the second time + fmt.Printf("\n Please enter your password again.\n>> ") + sec1, _ := reader.ReadString('\n') + sec1 = strings.Replace(sec1, "\n", "", -1) + + // make sure passwords match + if sec0 == sec1 { + sec = sec0 + noPass = false + } else { + fmt.Println("Lets try that again.") + } + } + + // reset display + exec.Command("stty", "-F", "/dev/tty", "echo", "-cbreak").Run() + + // set user in redis + secHash := sha3.Sum512([]byte(sec)) + redisClient.Set(fmt.Sprintf("user:%s", username), fmt.Sprintf("%x", secHash), 0).Err() + + // create user directories + userPath := fmt.Sprintf("%s/user/%s/ttyrec/", config.NethackLauncher.HackDir, username) + exec.Command("mkdir", "-p", userPath).Run() + + // copy in rc file + hackRCLoc := fmt.Sprintf("%s/.nethackrc", config.NethackLauncher.HackDir) + hackRCDest := fmt.Sprintf("%s/user/%s/.nethackrc", config.NethackLauncher.HackDir, username) + exec.Command("cp", hackRCLoc, hackRCDest).Run() + + // TODO: move the above creation code into the createUserFiles() function + createUserFiles(username) + + // back to main screen + printUserScreen(redisClient, username) + } + } + if err := scanner.Err(); err != nil { + fmt.Printf("%s\n", err) + } +} diff --git a/nethack-launcher/print_user_screen.go b/nethack-launcher/print_user_screen.go new file mode 100644 index 0000000..ddf9038 --- /dev/null +++ b/nethack-launcher/print_user_screen.go @@ -0,0 +1,82 @@ +package main + +import ( + "fmt" + "os" + "os/exec" + "time" + + "gopkg.in/redis.v5" +) + +func printUserScreen(redisClient *redis.Client, username string) string { + clearScreen() + fmt.Printf(" %s\n", config.NethackLauncher.ServerDisplay) + println("") + fmt.Printf(" Logged in as: %s\n", username) + println("") + println(" l) Logout") + println(" c) Change password") + println(" w) Watch games in progress") + println(" h) View highscores") + println(" e) Edit config") + println(" r) Recover from crash") + fmt.Printf(" p) Play NetHack %s\n", config.NethackLauncher.NethackVersion) + println(" q) Quit") + println("") + fmt.Printf(">> ") + + // disable input buffering + exec.Command("stty", "-F", "/dev/tty", "cbreak", "min", "1").Run() + // do not display entered characters on the screen + exec.Command("stty", "-F", "/dev/tty", "-echo").Run() + + var b []byte = make([]byte, 1) + for { + os.Stdin.Read(b) + switch string(b) { + case "l": + // restart display + exec.Command("stty", "-F", "/dev/tty", "echo", "-cbreak").Run() + clearScreen() + printWelcomeScreen(redisClient) + case "e": + hackRCLoc := fmt.Sprintf("%s/user/%s/.nethackrc", config.NethackLauncher.HackDir, username) + exec.Command("stty", "-F", "/dev/tty", "echo", "-cbreak").Run() + clearScreen() + nh := exec.Command("vim", "-Z", hackRCLoc) + nh.Stdout = os.Stdout + nh.Stdin = os.Stdin + nh.Stderr = os.Stderr + nh.Run() + clearScreen() + printUserScreen(redisClient, username) + case "c": + printChangePasswordScreen(redisClient, username) + clearScreen() + case "w": + clearScreen() + printProgressScreen(redisClient, username) + case "h": + clearScreen() + printHighScores(redisClient, username) + case "p": + wg.Add(1) + currentTime := time.Now().UTC() + fulltime := currentTime.Format("2006-01-02.03:04:05") + go runGame(username, fulltime) + watcher := startWatcher(username, fulltime, redisClient) + wg.Wait() + close(watcher) + printUserScreen(redisClient, username) + case "r": + clearScreen() + recoverSave(redisClient, username) + case "q": + exec.Command("stty", "-F", "/dev/tty", "echo", "-cbreak").Run() + clearScreen() + os.Exit(0) + default: + } + } +} diff --git a/nethack-launcher/print_welcome_screen.go b/nethack-launcher/print_welcome_screen.go new file mode 100644 index 0000000..0cc5c58 --- /dev/null +++ b/nethack-launcher/print_welcome_screen.go @@ -0,0 +1,57 @@ +package main + +import ( + "fmt" + "os" + "os/exec" + + "gopkg.in/redis.v5" +) + +func printWelcomeScreen(redisClient *redis.Client) string { + clearScreen() + fmt.Printf(" %s\n", config.NethackLauncher.ServerDisplay) + println("") + println(" Not logged in.") + println("") + println(" l) Login") + println(" r) Register new user") + println(" w) Watch games in progress") + println(" h) View highscores") + println(" q) Quit") + println("") + fmt.Printf(">> ") + + // disable input buffering + exec.Command("stty", "-F", "/dev/tty", "cbreak", "min", "1").Run() + // do not display entered characters on the screen + exec.Command("stty", "-F", "/dev/tty", "-echo").Run() + + var b []byte = make([]byte, 1) + for { + os.Stdin.Read(b) + switch string(b) { + case "l": + // restart display + exec.Command("stty", "-F", "/dev/tty", "echo", "-cbreak").Run() + clearScreen() + printLoginScreen(redisClient) + case "r": + // restart display + exec.Command("stty", "-F", "/dev/tty", "echo", "-cbreak").Run() + clearScreen() + printRegisterScreen(redisClient) + case "w": + clearScreen() + printProgressScreen(redisClient, "") + case "h": + clearScreen() + printHighScores(redisClient, "") + case "q": + exec.Command("stty", "-F", "/dev/tty", "echo", "-cbreak").Run() + clearScreen() + os.Exit(0) + default: + } + } +} diff --git a/nethack-launcher/recover_save.go b/nethack-launcher/recover_save.go new file mode 100644 index 0000000..663aa94 --- /dev/null +++ b/nethack-launcher/recover_save.go @@ -0,0 +1,126 @@ +package main + +import ( + "bufio" + "fmt" + "io/ioutil" + "os" + "os/exec" + "path/filepath" + "strings" + + "gopkg.in/redis.v5" +) + +func recoverSave(redisClient *redis.Client, username string) { + // if no statefile exist, exit early as we cannot recover + matches, _ := filepath.Glob(fmt.Sprintf("%s/user/%s/*lock*", config.NethackLauncher.HackDir, username)) + if matches == nil { + fmt.Printf(" %s\n", config.NethackLauncher.ServerDisplay) + println("") + println(" The dungeon crumbles around you, darkness remains...") + println(" Godspeed adventurer..") + println("") + println(" No statefiles exist. (press enter to return)") + println("") + fmt.Printf(">> ") + + scanner := bufio.NewScanner(os.Stdin) + for scanner.Scan() { + if scanner.Text() == "" { + printUserScreen(redisClient, username) + } + } + } + + fmt.Printf(" %s\n", config.NethackLauncher.ServerDisplay) + println("") + println(" Recover utility.") + println("") + // check if dir is empty + isEmpty, _ := checkDir(fmt.Sprintf("%s/user/%s/save/", config.NethackLauncher.HackDir, username)) + if !isEmpty { + // save file exists, overwrite? + println(" Save file already exists!") + println(" Overwrite? (y/n)") + println("") + fmt.Printf(">> ") + } else { + println(" Attempt to recover statefile? (y/n)") + println("") + fmt.Printf(">> ") + } + + // disable input buffering + exec.Command("stty", "-F", "/dev/tty", "cbreak", "min", "1").Run() + // do not display entered characters on the screen + exec.Command("stty", "-F", "/dev/tty", "-echo").Run() + + var b []byte = make([]byte, 1) + for { + os.Stdin.Read(b) + switch string(b) { + case "n": + clearScreen() + printUserScreen(redisClient, username) + case "y": + // set user path to save file candidates + userPath := fmt.Sprintf("%s/user/%s/", config.NethackLauncher.HackDir, username) + + // read in all files + candidates, err := ioutil.ReadDir(userPath) + if err != nil { + fmt.Println(err) + } + var newestFile string + var newestTime int64 = 0 + for _, f := range candidates { + // loop through save file candidates + if strings.Contains(f.Name(), "lock") { + fi, err := os.Stat(userPath + f.Name()) + if err != nil { + fmt.Println(err) + } + currTime := fi.ModTime().Unix() + if currTime > newestTime { + newestTime = currTime + newestFile = f.Name() + } + } + } + + // get prefix for latest file + latestFile := strings.Split(newestFile, ".") + + // run recover on file + out, err := exec.Command(config.NethackLauncher.RecoverBinary, "-d", fmt.Sprintf("%s/user/%s", config.NethackLauncher.HackDir, username), latestFile[0]).Output() + if err != nil { + fmt.Println(out) + panic(err) + } + + // make sure save file exists before removing the extra locks + isEmpty, _ = checkDir(fmt.Sprintf("%s/user/%s/save/", config.NethackLauncher.HackDir, username)) + if !isEmpty { + // save file made it, clean up old cruft + files, err := filepath.Glob(fmt.Sprintf("%s/*lock*", userPath)) + if err != nil { + panic(err) + } + for _, f := range files { + if err := os.Remove(f); err != nil { + panic(err) + } + } + // alert user that the save file was recovered + println("") + println(" Statefile was recovered successfully! (press enter to return)") + fmt.Printf(">> ") + } + case "\n": + clearScreen() + printUserScreen(redisClient, username) + default: + } + } +} diff --git a/nethack-launcher/run_game.go b/nethack-launcher/run_game.go new file mode 100644 index 0000000..0bf7c73 --- /dev/null +++ b/nethack-launcher/run_game.go @@ -0,0 +1,31 @@ +package main + +import ( + "fmt" + "os" + "os/exec" +) + +func runGame(username, timestamp string) { + exec.Command("stty", "-F", "/dev/tty", "echo", "-cbreak").Run() + clearScreen() + + // put together users home dir + homeDir := fmt.Sprintf("%s/user/%s/", config.NethackLauncher.HackDir, username) + ttyrecPath := fmt.Sprintf("%s/user/%s/ttyrec/%s.ttyrec", config.NethackLauncher.HackDir, username, timestamp) + + nh := exec.Command("ttyrec", "-f", ttyrecPath, "--", "nethack") + nh.Env = os.Environ() + nh.Env = append(nh.Env, fmt.Sprintf("HOME=%s", homeDir)) + nh.Env = append(nh.Env, fmt.Sprintf("USER=%s", username)) + nh.Env = append(nh.Env, fmt.Sprintf("NETHACKDIR=%s/user/%s", config.NethackLauncher.HackDir, username)) + nh.Stdout = os.Stdout + nh.Stdin = os.Stdin + nh.Stderr = os.Stderr + err := nh.Run() + if err != nil { + fmt.Print(err) + } + exec.Command("exit").Run() + wg.Done() +} diff --git a/nethack-launcher/start_watcher.go b/nethack-launcher/start_watcher.go new file mode 100644 index 0000000..4b713e5 --- /dev/null +++ b/nethack-launcher/start_watcher.go @@ -0,0 +1,29 @@ +package main + +import ( + "fmt" + "time" + + "gopkg.in/redis.v5" +) + +func startWatcher(username, timestamp string, redisClient *redis.Client) chan struct{} { + // create initial keys + redisClient.SAdd("inprogress", username) + redisClient.Set(fmt.Sprintf("inprogress:%s", username), timestamp, 0) + + ch := make(chan struct{}) + // enter inital inprogress yet + go func() { + for { + select { + case <-ch: + return + default: + redisClient.Expire(fmt.Sprintf("inprogress:%s", username), 10*time.Second) + time.Sleep(8 * time.Second) + } + } + }() + return ch +}