diff --git a/adb/device.go b/adb/device.go new file mode 100644 index 0000000..62ab314 --- /dev/null +++ b/adb/device.go @@ -0,0 +1,247 @@ +// Copyright (c) 2015, Daniel Martí +// See LICENSE for licensing information + +package adb + +import ( + "bufio" + "fmt" + "io" + "os/exec" + "regexp" + "strconv" + "strings" +) + +type Device struct { + ID string + Usb string + Product string + Model string + Device string + ABIs []string + APILevel int +} + +var deviceRegex = regexp.MustCompile(`^([^\s]+)\s+device(.*)$`) + +func Devices() ([]*Device, error) { + cmd := exec.Command("adb", "devices", "-l") + stdout, err := cmd.StdoutPipe() + if err != nil { + return nil, err + } + if err := cmd.Start(); err != nil { + return nil, err + } + var devices []*Device + scanner := bufio.NewScanner(stdout) + for scanner.Scan() { + line := scanner.Text() + m := deviceRegex.FindStringSubmatch(line) + if m == nil { + continue + } + device := &Device{ + ID: m[1], + } + for _, extra := range strings.Split(m[2], " ") { + sp := strings.SplitN(extra, ":", 2) + if len(sp) < 2 { + continue + } + switch sp[0] { + case "usb": + device.Usb = sp[1] + case "product": + device.Product = sp[1] + case "model": + device.Model = sp[1] + case "device": + device.Device = sp[1] + } + } + + props, err := device.AdbProps() + if err != nil { + return nil, err + } + device.ABIs = getAbis(props) + if len(device.ABIs) == 0 { + return nil, fmt.Errorf("failed to get device ABIs") + } + device.APILevel, err = strconv.Atoi(props["ro.build.version.sdk"]) + if device.APILevel == 0 { + return nil, fmt.Errorf("failed to get device API level") + } + + devices = append(devices, device) + } + return devices, nil +} + +func (d *Device) AdbCmd(args ...string) *exec.Cmd { + cmdArgs := append([]string{"-s", d.ID}, args...) + return exec.Command("adb", cmdArgs...) +} + +func (d *Device) AdbShell(args ...string) *exec.Cmd { + shellArgs := append([]string{"shell"}, args...) + return d.AdbCmd(shellArgs...) +} + +var propLineRegex = regexp.MustCompile(`^\[(.*)\]: \[(.*)\]$`) + +func (d *Device) AdbProps() (map[string]string, error) { + cmd := d.AdbShell("getprop") + stdout, err := cmd.StdoutPipe() + if err != nil { + return nil, err + } + if err := cmd.Start(); err != nil { + return nil, err + } + props := make(map[string]string) + scanner := bufio.NewScanner(stdout) + for scanner.Scan() { + m := propLineRegex.FindStringSubmatch(scanner.Text()) + if m == nil { + continue + } + key, val := m[1], m[2] + props[key] = val + } + return props, nil +} + +func getFailureCode(r *regexp.Regexp, line string) string { + return r.FindStringSubmatch(line)[1] +} + +func getAbis(props map[string]string) []string { + // Android 5.0 and later specify a list of ABIs + if abilist, e := props["ro.product.cpu.abilist"]; e { + return strings.Split(abilist, ",") + } + // Older Android versions specify one primary ABI and optionally + // one secondary ABI + abi, e := props["ro.product.cpu.abi"] + if !e { + return nil + } + if abi2, e := props["ro.product.cpu.abi2"]; e { + return []string{abi, abi2} + } + return []string{abi} +} + +var installFailureRegex = regexp.MustCompile(`^Failure \[INSTALL_(.+)\]$`) + +func withOpts(cmd string, opts []string, args ...string) []string { + v := append([]string{"install"}, opts...) + return append(v, args...) +} + +func (d *Device) install(opts []string, path string) error { + cmd := d.AdbCmd(withOpts("install", opts, path)...) + stdout, err := cmd.StdoutPipe() + if err != nil { + return err + } + if err := cmd.Start(); err != nil { + return err + } + line := getResultLine(stdout) + if line == "Success" { + return nil + } + return parseError(getFailureCode(installFailureRegex, line)) +} + +func (d *Device) Install(path string) error { + return d.install(nil, path) +} + +func (d *Device) Upgrade(path string) error { + return d.install([]string{"-r"}, path) +} + +func getResultLine(out io.Reader) string { + scanner := bufio.NewScanner(out) + for scanner.Scan() { + l := scanner.Text() + if strings.HasPrefix(l, "Failure") || strings.HasPrefix(l, "Success") { + return l + } + } + return "" +} + +var deleteFailureRegex = regexp.MustCompile(`^Failure \[DELETE_(.+)\]$`) + +func (d *Device) Uninstall(pkg string) error { + cmd := d.AdbCmd("uninstall", pkg) + stdout, err := cmd.StdoutPipe() + if err != nil { + return err + } + if err := cmd.Start(); err != nil { + return err + } + line := getResultLine(stdout) + if line == "Success" { + return nil + } + return parseError(getFailureCode(deleteFailureRegex, line)) +} + +type Package struct { + ID string + VCode int + VName string +} + +var ( + packageRegex = regexp.MustCompile(`^ Package \[([^\s]+)\]`) + verCodeRegex = regexp.MustCompile(`^ versionCode=([0-9]+)`) + verNameRegex = regexp.MustCompile(`^ versionName=(.+)`) +) + +func (d *Device) Installed() (map[string]Package, error) { + cmd := d.AdbShell("dumpsys", "package", "packages") + stdout, err := cmd.StdoutPipe() + if err != nil { + return nil, err + } + if err := cmd.Start(); err != nil { + return nil, err + } + packages := make(map[string]Package) + scanner := bufio.NewScanner(stdout) + var cur Package + first := true + for scanner.Scan() { + l := scanner.Text() + if m := packageRegex.FindStringSubmatch(l); m != nil { + if first { + first = false + } else { + packages[cur.ID] = cur + cur = Package{} + } + cur.ID = m[1] + } else if m := verCodeRegex.FindStringSubmatch(l); m != nil { + n, err := strconv.Atoi(m[1]) + if err != nil { + panic(err) + } + cur.VCode = n + } else if m := verNameRegex.FindStringSubmatch(l); m != nil { + cur.VName = m[1] + } + } + if !first { + packages[cur.ID] = cur + } + return packages, nil +} diff --git a/adb/error.go b/adb/error.go new file mode 100644 index 0000000..66c3c2d --- /dev/null +++ b/adb/error.go @@ -0,0 +1,109 @@ +// Copyright (c) 2015, Daniel Martí +// See LICENSE for licensing information + +package adb + +import ( + "errors" + "fmt" +) + +var ( + // Common install and uninstall errors + ErrInternalError = errors.New("internal error") + ErrUserRestricted = errors.New("user restricted") + ErrAborted = errors.New("aborted") + + // Install errors + ErrAlreadyExists = errors.New("already exists") + ErrInvalidApk = errors.New("invalid apk") + ErrInvalidURI = errors.New("invalid uri") + ErrInsufficientStorage = errors.New("insufficient storage") + ErrDuplicatePackage = errors.New("duplicate package") + ErrNoSharedUser = errors.New("no shared user") + ErrUpdateIncompatible = errors.New("update incompatible") + ErrSharedUserIncompatible = errors.New("shared user incompatible") + ErrMissingSharedLibrary = errors.New("missing shared library") + ErrReplaceCouldntDelete = errors.New("replace couldn't delete") + ErrDexopt = errors.New("dexopt") + ErrOlderSdk = errors.New("older sdk") + ErrConflictingProvider = errors.New("conflicting provider") + ErrNewerSdk = errors.New("newer sdk") + ErrTestOnly = errors.New("test only") + ErrCPUAbiIncompatible = errors.New("cpu abi incompatible") + ErrMissingFeature = errors.New("missing feature") + ErrContainerError = errors.New("combiner error") + ErrInvalidInstallLocation = errors.New("invalid install location") + ErrMediaUnavailable = errors.New("media unavailable") + ErrVerificationTimeout = errors.New("verification timeout") + ErrVerificationFailure = errors.New("verification failure") + ErrPackageChanged = errors.New("package changed") + ErrUIDChanged = errors.New("uid changed") + ErrVersionDowngrade = errors.New("version downgrade") + ErrNotApk = errors.New("not apk") + ErrBadManifest = errors.New("bad manifest") + ErrUnexpectedException = errors.New("unexpected exception") + ErrNoCertificates = errors.New("no certificates") + ErrInconsistentCertificates = errors.New("inconsistent certificates") + ErrCertificateEncoding = errors.New("certificate encoding") + ErrBadPackageName = errors.New("bad package name") + ErrBadSharedUserID = errors.New("bad shared user id") + ErrManifestMalformed = errors.New("manifest malformed") + ErrManifestEmpty = errors.New("manifest empty") + ErrDuplicatePermission = errors.New("duplicate permission") + ErrNoMatchingAbis = errors.New("no matching abis") + + // Uninstall errors + ErrDevicePolicyManager = errors.New("device policy manager") + ErrOwnerBlocked = errors.New("owner blocked") +) + +var errorVals = map[string]error{ + "FAILED_ALREADY_EXISTS": ErrAlreadyExists, + "FAILED_INVALID_APK": ErrInvalidApk, + "FAILED_INVALID_URI": ErrInvalidURI, + "FAILED_INSUFFICIENT_STORAGE": ErrInsufficientStorage, + "FAILED_DUPLICATE_PACKAGE": ErrDuplicatePackage, + "FAILED_NO_SHARED_USER": ErrNoSharedUser, + "FAILED_UPDATE_INCOMPATIBLE": ErrUpdateIncompatible, + "FAILED_SHARED_USER_INCOMPATIBLE": ErrSharedUserIncompatible, + "FAILED_MISSING_SHARED_LIBRARY": ErrMissingSharedLibrary, + "FAILED_REPLACE_COULDNT_DELETE": ErrReplaceCouldntDelete, + "FAILED_DEXOPT": ErrDexopt, + "FAILED_OLDER_SDK": ErrOlderSdk, + "FAILED_CONFLICTING_PROVIDER": ErrConflictingProvider, + "FAILED_NEWER_SDK": ErrNewerSdk, + "FAILED_TEST_ONLY": ErrTestOnly, + "FAILED_CPU_ABI_INCOMPATIBLE": ErrCPUAbiIncompatible, + "FAILED_MISSING_FEATURE": ErrMissingFeature, + "FAILED_CONTAINER_ERROR": ErrContainerError, + "FAILED_INVALID_INSTALL_LOCATION": ErrInvalidInstallLocation, + "FAILED_MEDIA_UNAVAILABLE": ErrMediaUnavailable, + "FAILED_VERIFICATION_TIMEOUT": ErrVerificationTimeout, + "FAILED_VERIFICATION_FAILURE": ErrVerificationFailure, + "FAILED_PACKAGE_CHANGED": ErrPackageChanged, + "FAILED_UID_CHANGED": ErrUIDChanged, + "FAILED_VERSION_DOWNGRADE": ErrVersionDowngrade, + "PARSE_FAILED_NOT_APK": ErrNotApk, + "PARSE_FAILED_BAD_MANIFEST": ErrBadManifest, + "PARSE_FAILED_UNEXPECTED_EXCEPTION": ErrUnexpectedException, + "PARSE_FAILED_NO_CERTIFICATES": ErrNoCertificates, + "PARSE_FAILED_INCONSISTENT_CERTIFICATES": ErrInconsistentCertificates, + "PARSE_FAILED_CERTIFICATE_ENCODING": ErrCertificateEncoding, + "PARSE_FAILED_BAD_PACKAGE_NAME": ErrBadPackageName, + "PARSE_FAILED_BAD_SHARED_USER_ID": ErrBadSharedUserID, + "PARSE_FAILED_MANIFEST_MALFORMED": ErrManifestMalformed, + "PARSE_FAILED_MANIFEST_EMPTY": ErrManifestEmpty, + "FAILED_INTERNAL_ERROR": ErrInternalError, + "FAILED_USER_RESTRICTED": ErrUserRestricted, + "FAILED_DUPLICATE_PERMISSION": ErrDuplicatePermission, + "FAILED_NO_MATCHING_ABIS": ErrNoMatchingAbis, + "FAILED_ABORTED": ErrAborted, +} + +func parseError(s string) error { + if err, e := errorVals[s]; e { + return err + } + return fmt.Errorf("unknown error: %s", s) +} diff --git a/adb/error_test.go b/adb/error_test.go new file mode 100644 index 0000000..407a75d --- /dev/null +++ b/adb/error_test.go @@ -0,0 +1,23 @@ +// Copyright (c) 2015, Daniel Martí +// See LICENSE for licensing information + +package adb + +import "testing" + +func TestParseError(t *testing.T) { + tests := []struct { + in string + want error + }{ + {"FAILED_DEXOPT", ErrDexopt}, + {"PARSE_FAILED_NOT_APK", ErrNotApk}, + {"FAILED_ABORTED", ErrAborted}, + } + for _, c := range tests { + got := parseError(c.in) + if got != c.want { + t.Fatalf("Parse error in %s - wanted %v, got %v", c.in, c.want, got) + } + } +} diff --git a/adb/server.go b/adb/server.go new file mode 100644 index 0000000..001fe20 --- /dev/null +++ b/adb/server.go @@ -0,0 +1,28 @@ +// Copyright (c) 2015, Daniel Martí +// See LICENSE for licensing information + +package adb + +import ( + "fmt" + "net" + "os/exec" +) + +const ( + host = "127.0.0.1" + port = 5037 +) + +func IsServerRunning() bool { + conn, err := net.Dial("tcp", fmt.Sprintf("%s:%d", host, port)) + if err != nil { + return false + } + conn.Close() + return true +} + +func StartServer() error { + return exec.Command("adb", "start-server").Run() +}