Commits

Ross Light committed c98d595

initial import

  • Participants

Comments (0)

Files changed (3)

+package main
+
+import (
+	"errors"
+	"net"
+	"strconv"
+)
+
+type IPFlag struct {
+	IP *net.IP
+}
+
+func (f IPFlag) String() string {
+	return f.IP.String()
+}
+
+func (f IPFlag) Set(value string) error {
+	*f.IP = net.ParseIP(value)
+	if *f.IP == nil {
+		return errors.New("Can't parse IP: " + value)
+	}
+	return nil
+}
+
+type RobotAddressFlag struct {
+	IP *net.IP
+}
+
+func (f RobotAddressFlag) String() string {
+	team, err := extractTeamNumber(*f.IP)
+	if err != nil {
+		return ""
+	}
+	return strconv.Itoa(team)
+}
+
+func (f RobotAddressFlag) Set(value string) error {
+	team, err := strconv.Atoi(value)
+	if err != nil {
+		return err
+	}
+	if team < 1 || team > 9999 {
+		return errors.New("Bad team number: " + value)
+	}
+	*f.IP = robotAddress(team)
+	return nil
+}
+package main
+
+import (
+	"bitbucket.org/zombiezen/ftp"
+	"errors"
+	"flag"
+	"fmt"
+	"io"
+	"log"
+	"net"
+	"os"
+	slashpath "path"
+	"path/filepath"
+	"strings"
+	"sync"
+)
+
+var (
+	Address *net.TCPAddr
+)
+
+func main() {
+	parseGlobalFlags()
+
+	c := lookupCommand(flag.Arg(0))
+	if c == nil {
+		fmt.Fprintf(os.Stderr, "unrecognized command %q\n", flag.Arg(0))
+		printUsage()
+		os.Exit(2)
+	}
+
+	err := c.Func()
+	if err != nil {
+		log.Fatal(err)
+	}
+}
+
+func parseGlobalFlags() {
+	Address = &net.TCPAddr{IP: robotAddress(1234)}
+	if team, err := hostTeamNumber(); err == nil {
+		Address.IP = robotAddress(team)
+	}
+
+	flag.Usage = printUsage
+	flag.Var(IPFlag{&Address.IP}, "address", "IP address to connect to")
+	flag.Var(RobotAddressFlag{&Address.IP}, "team", "Set the IP address via team number")
+	flag.IntVar(&Address.Port, "port", 21, "Port to connect to")
+	flag.Parse()
+	if flag.NArg() == 0 {
+		printUsage()
+		os.Exit(2)
+	}
+}
+
+func lookupCommand(name string) *Command {
+	for i := range commands {
+		if name == commands[i].Name {
+			return &commands[i]
+		}
+		for _, alias := range commands[i].Aliases {
+			if name == alias {
+				return &commands[i]
+			}
+		}
+	}
+	return nil
+}
+
+func printUsage() {
+	fmt.Println("usage: frctool [options] command [options] ...")
+	fmt.Println("\nGlobal options:")
+	flag.PrintDefaults()
+	fmt.Println("\nCommands:")
+	for i := range commands {
+		fmt.Printf("  %-7s %s\n", commands[i].Name, commands[i].Description)
+	}
+}
+
+func printCommandUsage(c *Command, fs *flag.FlagSet) {
+	fmt.Printf("usage: frctool [options] %s %s\n", c.Name, c.Usage)
+	fmt.Println("\nGlobal options:")
+	flag.PrintDefaults()
+	if fs != nil {
+		fmt.Printf("\n%s Options:\n", c.Name)
+		fs.PrintDefaults()
+	}
+}
+
+type Command struct {
+	Name        string
+	Aliases     []string
+	Usage       string
+	Description string
+	Func        func() error
+}
+
+var commands []Command
+
+func init() {
+	commands = []Command{
+		{
+			"c",
+			nil,
+			"FILE",
+			"Upload C/C++ program",
+			uploadC,
+		},
+		{
+			"lua",
+			nil,
+			"DIR",
+			"Upload Lua directory",
+			uploadLua,
+		},
+		{
+			"error",
+			nil,
+			"",
+			"Print Lua error",
+			downloadLuaError,
+		},
+	}
+}
+
+const (
+	frcPath      = "/ni-rt/system/FRC_UserProgram.out"
+	luaRoot      = "/lua"
+	luaErrorPath = "/lua-error.txt"
+)
+
+func uploadC() error {
+	if flag.NArg() != 2 {
+		printCommandUsage(lookupCommand("c"), nil)
+		os.Exit(2)
+	}
+
+	client, err := connect(Address)
+	if err != nil {
+		return err
+	}
+	defer client.Quit()
+
+	return upload(client, frcPath, flag.Arg(1))
+}
+
+func uploadLua() error {
+	fs := flag.NewFlagSet("lua", flag.ContinueOnError)
+	fs.Usage = func() {}
+	nclients := fs.Int("nthreads", 2, "Set the number of simultaneous downloads")
+	err := fs.Parse(flag.Args()[1:])
+
+	if err != nil || flag.NArg() != 1 {
+		printCommandUsage(lookupCommand("lua"), fs)
+		os.Exit(2)
+	}
+
+	// Get root directory, ensuring it ends in a trailing slash
+	root := filepath.Clean(fs.Arg(0))
+	if !strings.HasSuffix(root, string(filepath.Separator)) {
+		root += string(filepath.Separator)
+	}
+	if st, err := os.Stat(root); err == nil {
+		if !st.IsDir() {
+			return &os.PathError{"open", fs.Arg(0), errors.New("not a directory")}
+		}
+	} else {
+		return err
+	}
+
+	// Create clients
+	clients, err := createClientPool(*nclients, Address)
+	if err != nil {
+		return err
+	}
+	defer func() {
+		for i := range clients {
+			clients[i].Quit()
+		}
+	}()
+
+	// Change to Lua root directory
+	if reply, err := clients[0].Do("CWD " + luaRoot); err != nil || !reply.Positive() {
+		if err != nil {
+			return err
+		}
+
+		if reply.Code == ftp.CodeFileUnavailable {
+			// Directory doesn't exist, try to create it
+			log.Printf("Creating directory %q", luaRoot)
+			reply, err = clients[0].Do("MKD " + luaRoot)
+			if err != nil {
+				return err
+			} else if !reply.PositiveComplete() {
+				return reply
+			}
+
+			// Directory created, change to it
+			reply, err := clients[0].Do("CWD " + luaRoot)
+			if err != nil {
+				return err
+			} else if !reply.Positive() {
+				return reply
+			}
+		} else {
+			return reply
+		}
+	}
+
+	for _, client := range clients[1:] {
+		reply, err := client.Do("CWD " + luaRoot)
+		if err != nil {
+			return err
+		} else if !reply.Positive() {
+			return reply
+		}
+	}
+
+	// Start uploading pool
+	ch := make(chan uploadRequest)
+	var wg sync.WaitGroup
+	wg.Add(len(clients))
+	for i := range clients {
+		go func(client *ftp.Client) {
+			for r := range ch {
+				err := upload(client, r.DestPath, r.Path)
+				if err == nil {
+					log.Printf("Uploaded %q to %q", r.Path, slashpath.Join(luaRoot, r.DestPath))
+				} else {
+					// Log error, but try to continue uploading
+					log.Printf("ERROR for %q: %v", r.Path, err)
+				}
+			}
+			wg.Done()
+		}(clients[i])
+	}
+
+	// Walk files
+	err = filepath.Walk(root, func(path string, info os.FileInfo, err error) error {
+		if info.IsDir() || filepath.Ext(path) != ".lua" {
+			return nil
+		}
+
+		// Remove root directory prefix
+		destPath := path
+		if strings.HasPrefix(path, root) {
+			destPath = path[len(root):]
+		}
+		destPath = filepath.ToSlash(destPath)
+
+		// Start upload
+		ch <- uploadRequest{path, destPath}
+		return nil
+	})
+
+	close(ch)
+	wg.Wait()
+	return err
+}
+
+type uploadRequest struct {
+	Path     string
+	DestPath string
+}
+
+func downloadLuaError() error {
+	if flag.NArg() != 1 {
+		printCommandUsage(lookupCommand("error"), nil)
+		os.Exit(2)
+	}
+
+	client, err := connect(Address)
+	if err != nil {
+		return err
+	}
+	defer client.Quit()
+
+	conn, err := client.Text("RETR " + luaErrorPath)
+	if err != nil {
+		return err
+	}
+	defer conn.Close()
+
+	_, err = io.Copy(os.Stdout, conn)
+	return err
+}
+package main
+
+import (
+	"bitbucket.org/zombiezen/ftp"
+	"errors"
+	"fmt"
+	"io"
+	"log"
+	"net"
+	"os"
+)
+
+// connect dials to a FTP server.
+func connect(addr *net.TCPAddr) (*ftp.Client, error) {
+	log.Printf("Connecting to %v", addr)
+	conn, err := net.DialTCP("tcp", nil, addr)
+	if err != nil {
+		return nil, err
+	}
+	client, err := ftp.NewClient(conn)
+	if err != nil {
+		return nil, err
+	}
+	log.Print("Logging in anonymously")
+	if err := client.Login("anonymous", ""); err != nil {
+		return nil, err
+	}
+	return client, nil
+}
+
+// createClientPool opens up to n connections.  If there is an error obtaining
+// the first connection, it will be returned.  The function will return less
+// clients if an error occurs after the first connection.
+func createClientPool(n int, addr *net.TCPAddr) ([]*ftp.Client, error) {
+	clients := make([]*ftp.Client, 0, n)
+	for i := 0; i < n; i++ {
+		c, err := connect(addr)
+		if err != nil {
+			if i == 0 {
+				return nil, err
+			} else {
+				break
+			}
+		}
+		clients = append(clients, c)
+	}
+	return clients, nil
+}
+
+// extractTeamNumber extracts a team number from a FIRST IP address.
+func extractTeamNumber(ip net.IP) (team int, err error) {
+	ip4 := ip.To4()
+	if ip4 == nil {
+		return 0, fmt.Errorf("%v is not an IPv4 address", ip)
+	}
+	if ip4[0] != 10 {
+		return 0, fmt.Errorf("%v is not a FIRST address (must be in 10.0.0.0/24)", ip)
+	}
+	return int(ip4[1])*100 + int(ip4[2]), nil
+}
+
+// robotAddress returns the IP address of the robot with the given team number.
+func robotAddress(team int) net.IP {
+	return net.IPv4(10, byte(team/100), byte(team%100), 2)
+}
+
+// hostTeamNumber returns the team number of the host machine.  This is found by
+// querying the host's network interfaces.
+func hostTeamNumber() (int, error) {
+	addrs, err := net.InterfaceAddrs()
+	if err != nil {
+		return 0, err
+	}
+	for _, ia := range addrs {
+		if tcpAddr, ok := ia.(*net.TCPAddr); ok {
+			if team, err := extractTeamNumber(tcpAddr.IP); err == nil {
+				return team, nil
+			}
+		}
+	}
+	return 0, errors.New("Host has no IP address with a team number")
+}
+
+// upload copies a file via FTP
+func upload(client *ftp.Client, dest, source string) error {
+	f, err := os.Open(source)
+	if err != nil {
+		return err
+	}
+	defer f.Close()
+
+	dataConn, err := client.Binary("STOR " + dest)
+	if err != nil {
+		return err
+	}
+	defer dataConn.Close()
+
+	_, err = io.Copy(dataConn, f)
+	return err
+}