Nethack Launcher
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 

809 lines
22 KiB

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 <ctrl-c> 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
}