package adb import ( "context" "errors" "net" "os" "strconv" "strings" ) type Serial string type Connection int const ( USB Connection = iota Network ) // Create a Device with Connect() or a slice with Devices() // // Device contains the information necessary to connect to and // communicate with a device type Device struct { IsAuthorized bool SerialNo Serial ConnType Connection IP net.IPAddr Port uint FileHandle string // TODO change this to a discrete type } // Provides a connection string for Connect() type ConnOptions struct { Address net.IPAddr Port uint SerialNo Serial } // Connect to a device by IP:port. // // This will return a Device struct, which can be used to call other methods. // If the connection fails or cannot complete on time, Connect will return an error. // TODO func Connect(ctx context.Context, opts ConnOptions) (Device, error) { if opts.Port == 0 { opts.Port = 5555 } return Device{}, nil } func (d Device) ConnString() string { if d.Port == 0 { d.Port = 5555 } return d.IP.String() + ":" + strconv.Itoa(int(d.Port)) } // Connect to a previously discovered device. // // This function is helpful when connecting to a device found from the Devices call // or when reconnecting to a previously connected device. func (d Device) Reconnect(ctx context.Context) (Device, error) { if d.ConnType == USB { return d, ErrConnUSB } cmd := []string{"connect", d.ConnString()} stdout, stderr, errcode, err := execute(ctx, cmd) if err != nil { return d, err } if errcode != 0 { return d, ErrUnspecified } _, _ = stdout, stderr // TODO capture and store serial number into d before returning return d, nil } // Equivalent to running `adb devices`. // // This function returns a list of discovered devices, but note that they may not be connected. // It is recommended to call IsConnected() against the device you're interested in using and connect // if not already connected before proceeding. func Devices(ctx context.Context) ([]Device, error) { cmd := []string{"devices"} stdout, _, errcode, err := execute(ctx, cmd) devs := []Device{} if err != nil { return devs, err } if errcode != 0 { return devs, ErrUnspecified } return parseDevices(stdout) } // TODO add support for connected network devices func parseDevices(stdout string) ([]Device, error) { devs := []Device{} lines := strings.Split(stdout, "\n") for _, line := range lines { words := strings.Fields(line) if len(words) != 2 { continue } d := Device{ SerialNo: Serial(words[0]), IsAuthorized: words[1] == "device", } devs = append(devs, d) } return devs, nil } // Disconnect from a device. // // If a device is already disconnected or otherwise not found, returns an error. func (d Device) Disconnect(ctx context.Context) error { if d.ConnType != Network { return ErrConnUSB } _, _, _, err := execute(ctx, []string{"-s", d.ConnString(), "disconnect"}) return err } // KillServer kills the ADB server. // // Warning: this function call may cause inconsistency if not used properly. // Killing the ADB server shouldn't ever technically be necessary, but if you do // decide to use this function, note that it may invalidate all existing device structs. // Older versions of Android don't play nicely with kill-server, and some may // refuse following connection attempts if you don't disconnect from them before // calling this function. func KillServer(ctx context.Context) error { _, _, _, err := execute(ctx, []string{"kill-server"}) return err } // Push a file to a Device. // // Returns an error if src does not exist or there is an error copying the file. func (d Device) Push(ctx context.Context, src, dest string) error { _, err := os.Stat(src) if err != nil { return err } _, _, errcode, err := execute(ctx, []string{"-s", string(d.SerialNo), "push", src, dest}) if err != nil { return err } if errcode != 0 { return ErrUnspecified } return nil } // Pull a file from a Device. // // Returns an error if dest already exists or the file cannot be pulled. func (d Device) Pull(ctx context.Context, src, dest string) error { _, err := os.Stat(dest) if err == nil { return ErrDestExists } if !errors.Is(err, os.ErrNotExist) { return err } _, _, errcode, err := execute(ctx, []string{"-s", string(d.SerialNo), "pull", src, dest}) if err != nil { return err } if errcode != 0 { return ErrUnspecified } return nil } // Reboot attempts to reboot the device. // // Once the device reboots, you must manually reconnect. // Returns an error if the device cannot be contacted. func (d Device) Reboot(ctx context.Context) error { _, _, errcode, err := execute(ctx, []string{"-s", string(d.SerialNo), "reboot"}) if err != nil { return err } if errcode != 0 { return ErrUnspecified } return nil } // Root attempts to relaunch adb as root on the Device. // // Note, this may not be possible on most devices. // Returns an error if it can't be done. // The device connection will stay established. // Once adb is relaunched as root, it will stay root until rebooted. // Returns true if the device successfully relaunched as root. func (d Device) Root(ctx context.Context) (success bool, err error) { stdout, _, errcode, err := execute(ctx, []string{"-s", string(d.SerialNo), "root"}) if err != nil { return false, err } if errcode != 0 { return false, ErrUnspecified } if strings.Contains(stdout, "adbd is already running as root") || strings.Contains(stdout, "restarting adbd as root") { return true, nil } return false, nil }