diff --git a/.gitignore b/.gitignore index bb6d0c54..d5a1d1aa 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ .DS_Store tile38-* +!cmd/tile38-* data*/ diff --git a/cmd/tile38-cli/main.go b/cmd/tile38-cli/main.go new file mode 100644 index 00000000..2960796e --- /dev/null +++ b/cmd/tile38-cli/main.go @@ -0,0 +1,356 @@ +package main + +import ( + "encoding/json" + "fmt" + "io" + "net" + "os" + "path/filepath" + "runtime" + "sort" + "strconv" + "strings" + + "github.com/peterh/liner" + "github.com/tidwall/tile38/client" + "github.com/tidwall/tile38/core" + "github.com/tidwall/tile38/kml" +) + +func userHomeDir() string { + if runtime.GOOS == "windows" { + home := os.Getenv("HOMEDRIVE") + os.Getenv("HOMEPATH") + if home == "" { + home = os.Getenv("USERPROFILE") + } + return home + } + return os.Getenv("HOME") +} + +var ( + historyFile = filepath.Join(userHomeDir(), ".liner_example_history") +) + +type connError struct { + OK bool `json:"ok"` + Err string `json:"err"` +} + +var ( + hostname = "127.0.0.1" + port = 9851 + oneCommand string + tokml bool +) + +func showHelp() bool { + fmt.Fprintf(os.Stdout, "tile38-cli %s (git:%s)\n\n", core.Version, core.GitSHA) + fmt.Fprintf(os.Stdout, "Usage: tile38-cli [OPTIONS] [cmd [arg [arg ...]]]\n") + fmt.Fprintf(os.Stdout, " -h Server hostname (default: %s).\n", hostname) + fmt.Fprintf(os.Stdout, " -p Server port (default: %d).\n", port) + fmt.Fprintf(os.Stdout, "\n") + return false +} + +func parseArgs() bool { + defer func() { + if v := recover(); v != nil { + if v, ok := v.(string); ok && v == "bad arg" { + showHelp() + } + } + }() + + args := os.Args[1:] + readArg := func(arg string) string { + if len(args) == 0 { + panic("bad arg") + } + var narg = args[0] + args = args[1:] + return narg + } + badArg := func(arg string) bool { + fmt.Fprintf(os.Stderr, "Unrecognized option or bad number of args for: '%s'\n", arg) + return false + } + for len(args) > 0 { + arg := readArg("") + if arg == "--help" { + return showHelp() + } + if !strings.HasPrefix(arg, "-") { + args = append([]string{arg}, args...) + break + } + switch arg { + default: + return badArg(arg) + case "-kml": + tokml = true + case "-h": + hostname = readArg(arg) + case "-p": + n, err := strconv.ParseUint(readArg(arg), 10, 16) + if err != nil { + return badArg(arg) + } + port = int(n) + } + } + oneCommand = strings.Join(args, " ") + return true +} + +func refusedErrorString(addr string) string { + return fmt.Sprintf("Could not connect to Tile38 at %s: Connection refused", addr) +} + +var groupsM = make(map[string][]string) + +func main() { + if !parseArgs() { + return + } + + addr := fmt.Sprintf("%s:%d", hostname, port) + conn, err := client.Dial(addr) + if err != nil { + if _, ok := err.(net.Error); ok { + fmt.Fprintln(os.Stderr, refusedErrorString(addr)) + } else { + fmt.Fprintln(os.Stderr, err.Error()) + } + return + } + defer conn.Close() + livemode := false + aof := false + defer func() { + if livemode { + var err error + if aof { + _, err = io.Copy(os.Stdout, conn.Reader()) + fmt.Fprintln(os.Stderr, "") + } else { + var msg []byte + for { + msg, err = conn.ReadMessage() + if err != nil { + break + } + fmt.Fprintln(os.Stderr, string(msg)) + } + } + if err != nil && err != io.EOF { + fmt.Fprintln(os.Stderr, err.Error()) + } + } + }() + + line := liner.NewLiner() + defer line.Close() + + var commands []string + for name, command := range core.Commands { + commands = append(commands, name) + groupsM[command.Group] = append(groupsM[command.Group], name) + } + sort.Strings(commands) + var groups []string + for group, arr := range groupsM { + groups = append(groups, "@"+group) + sort.Strings(arr) + groupsM[group] = arr + } + sort.Strings(groups) + + line.SetMultiLineMode(false) + line.SetCtrlCAborts(true) + line.SetCompleter(func(line string) (c []string) { + if strings.HasPrefix(strings.ToLower(line), "help ") { + var nitems []string + nline := strings.TrimSpace(line[5:]) + if nline == "" || nline[0] == '@' { + for _, n := range groups { + if strings.HasPrefix(strings.ToLower(n), strings.ToLower(nline)) { + nitems = append(nitems, line[:len(line)-len(nline)]+strings.ToLower(n)) + } + } + } else { + for _, n := range commands { + if strings.HasPrefix(strings.ToLower(n), strings.ToLower(nline)) { + nitems = append(nitems, line[:len(line)-len(nline)]+strings.ToUpper(n)) + } + } + } + for _, n := range nitems { + if strings.HasPrefix(strings.ToLower(n), strings.ToLower(line)) { + c = append(c, n) + } + } + } else { + for _, n := range commands { + if strings.HasPrefix(strings.ToLower(n), strings.ToLower(line)) { + c = append(c, n) + } + } + } + return + }) + if f, err := os.Open(historyFile); err == nil { + line.ReadHistory(f) + f.Close() + } + defer func() { + if f, err := os.Create(historyFile); err != nil { + fmt.Fprintln(os.Stderr, err.Error()) + } else { + line.WriteHistory(f) + f.Close() + } + }() + var raw bool + for { + var command string + var err error + if oneCommand == "" { + command, err = line.Prompt(addr + "> ") + } else { + command = oneCommand + } + if err == nil { + nohist := strings.HasPrefix(command, " ") + command = strings.TrimSpace(command) + if command == "" { + _, err := conn.Do("pInG") + if err != nil { + if err != io.EOF { + fmt.Fprintln(os.Stderr, err.Error()) + } else { + fmt.Fprintln(os.Stderr, refusedErrorString(addr)) + } + return + } + } else { + if !nohist { + line.AppendHistory(command) + } + if (command[0] == 'e' || command[0] == 'E') && strings.ToLower(command) == "exit" { + return + } + if (command[0] == 'q' || command[0] == 'Q') && strings.ToLower(command) == "quit" { + return + } + if (command[0] == 'r' || command[0] == 'R') && strings.ToLower(command) == "raw" { + raw = true + fmt.Fprintln(os.Stderr, "raw mode is ON") + continue + } + if (command[0] == 'h' || command[0] == 'H') && (strings.ToLower(command) == "help" || strings.HasPrefix(strings.ToLower(command), "help")) { + err = help(strings.TrimSpace(command[4:])) + if err != nil { + return + } + continue + } + if (command[0] == 'p' || command[0] == 'P') && strings.ToLower(command) == "pretty" { + raw = false + fmt.Fprintln(os.Stderr, "raw mode is OFF") + continue + } + aof = (command[0] == 'a' || command[0] == 'A') && strings.HasPrefix(strings.ToLower(command), "aof ") + msg, err := conn.Do(command) + if err != nil { + if err != io.EOF { + fmt.Fprintln(os.Stderr, err.Error()) + } else { + fmt.Fprintln(os.Stderr, refusedErrorString(addr)) + } + return + } + mustOutput := true + if oneCommand == "" && !strings.HasPrefix(string(msg), `{"ok":true`) { + var cerr connError + if err := json.Unmarshal(msg, &cerr); err == nil { + fmt.Fprintln(os.Stderr, "(error) "+cerr.Err) + mustOutput = false + } + } else if string(msg) == client.LiveJSON { + fmt.Fprintln(os.Stderr, string(msg)) + livemode = true + break // break out of prompt and just feed data to screen + } + if mustOutput { + if tokml { + msg = convert2kml(msg) + } + if raw { + fmt.Fprintln(os.Stdout, string(msg)) + } else { + fmt.Fprintln(os.Stdout, string(msg)) + } + } + } + } else if err == liner.ErrPromptAborted { + return + } else { + fmt.Fprintf(os.Stderr, "Error reading line: %s", err.Error()) + } + if oneCommand != "" { + return + } + } +} + +func convert2kml(msg []byte) []byte { + k := kml.New() + var m map[string]interface{} + if err := json.Unmarshal(msg, &m); err == nil { + if v, ok := m["points"].([]interface{}); ok { + for _, v := range v { + if v, ok := v.(map[string]interface{}); ok { + if v, ok := v["point"].(map[string]interface{}); ok { + var name string + var lat, lon float64 + if v, ok := v["id"].(string); ok { + name = v + } + if v, ok := v["lat"].(float64); ok { + lat = v + } + if v, ok := v["lon"].(float64); ok { + lon = v + } + k.AddPoint(name, lat, lon) + } + } + } + } + return k.Bytes() + } + return []byte(`{"ok":false,"err":"results must contain points"}`) +} + +func help(arg string) error { + if arg == "" { + fmt.Fprintf(os.Stderr, "tile38-cli %s (git:%s)\n", core.Version, core.GitSHA) + fmt.Fprintf(os.Stderr, `Type: "help @" to get a list of commands in `+"\n") + fmt.Fprintf(os.Stderr, ` "help " for help on `+"\n") + fmt.Fprintf(os.Stderr, ` "help " to get a list of possible help topics`+"\n") + fmt.Fprintf(os.Stderr, ` "quit" to exit`+"\n") + return nil + } + if strings.HasPrefix(arg, "@") { + for _, command := range groupsM[arg[1:]] { + fmt.Fprintf(os.Stderr, "%s\n", core.Commands[command].TermOutput(" ")) + } + } else { + if command, ok := core.Commands[strings.ToUpper(arg)]; ok { + fmt.Fprintf(os.Stderr, "%s\n", command.TermOutput(" ")) + } + } + return nil +} diff --git a/cmd/tile38-server/main.go b/cmd/tile38-server/main.go new file mode 100644 index 00000000..fd221dfc --- /dev/null +++ b/cmd/tile38-server/main.go @@ -0,0 +1,65 @@ +package main + +import ( + "flag" + "fmt" + "io" + "io/ioutil" + "os" + "runtime" + "strconv" + + "github.com/tidwall/tile38/controller" + "github.com/tidwall/tile38/core" + "github.com/tidwall/tile38/log" + "github.com/tidwall/tile38/server" +) + +var ( + dir string + port int + verbose bool + veryVerbose bool + devMode bool + quiet bool +) + +func main() { + flag.IntVar(&port, "p", 9851, "The listening port for communication.") + flag.StringVar(&dir, "d", "data", "The data directory.") + flag.BoolVar(&verbose, "v", false, "Enable verbose logging.") + flag.BoolVar(&quiet, "q", false, "Quiet logging. Totally silent.") + flag.BoolVar(&veryVerbose, "vv", false, "Enable very verbose logging.") + flag.BoolVar(&devMode, "dev", false, "Activates dev mode. DEV ONLY.") + flag.Parse() + var logw io.Writer = os.Stderr + if quiet { + logw = ioutil.Discard + } + log.Default = log.New(logw, &log.Config{ + HideDebug: !veryVerbose, + HideWarn: !(veryVerbose || verbose), + }) + controller.DevMode = devMode + controller.ShowDebugMessages = veryVerbose + server.ShowDebugMessages = veryVerbose + + // _____ _ _ ___ ___ + // |_ _|_| |___|_ | . | + // | | | | | -_|_ | . | + // |_| |_|_|___|___|___| + + fmt.Fprintf(logw, ` + _______ _______ + | | | + |____ | _ | Tile38 %s (%s) %d bit (%s/%s) + | | | Port: %d, PID: %d + |____ | _ | + | | | tile38.com + |_______|_______| +`+"\n", core.Version, core.GitSHA, strconv.IntSize, runtime.GOARCH, runtime.GOOS, port, os.Getpid()) + + if err := controller.ListenAndServe(port, dir); err != nil { + log.Fatal(err) + } +}