rclone/vendor/github.com/thinkhy/go-adb/device_extra.go
2019-02-14 10:44:21 +01:00

278 lines
6.4 KiB
Go

package adb
import (
"bufio"
"fmt"
"io"
"math/rand"
"net/http"
"os"
"regexp"
"strconv"
"strings"
"time"
retryablehttp "github.com/hashicorp/go-retryablehttp"
)
type Process struct {
User string
Pid int
Name string
}
// ListProcesses return list of Process
func (c *Device) ListProcesses() (ps []Process, err error) {
reader, err := c.OpenCommand("ps")
if err != nil {
return
}
defer reader.Close()
var fieldNames []string
bufrd := bufio.NewReader(reader)
for {
line, _, err := bufrd.ReadLine()
fields := strings.Fields(strings.TrimSpace(string(line)))
if len(fields) == 0 {
break
}
if err == io.EOF {
break
}
if fieldNames == nil {
fieldNames = fields
continue
}
var process Process
/* example output of command "ps"
USER PID PPID VSIZE RSS WCHAN PC NAME
root 1 0 684 540 ffffffff 00000000 S /init
root 2 0 0 0 ffffffff 00000000 S kthreadd
*/
if len(fields) != len(fieldNames)+1 {
continue
}
for index, name := range fieldNames {
value := fields[index]
switch strings.ToUpper(name) {
case "PID":
process.Pid, _ = strconv.Atoi(value)
case "NAME":
process.Name = fields[len(fields)-1]
case "USER":
process.User = value
}
}
if process.Pid == 0 {
continue
}
ps = append(ps, process)
}
return
}
// KillProcessByName return if killed success
func (c *Device) KillProcessByName(name string, sig int) error {
ps, err := c.ListProcesses()
if err != nil {
return err
}
for _, p := range ps {
if p.Name != name {
continue
}
// log.Printf("kill %s with pid: %d", p.Name, p.Pid)
_, _, er := c.RunCommandWithExitCode("kill", "-"+strconv.Itoa(sig), strconv.Itoa(p.Pid))
if er != nil {
return er
}
}
return nil
}
type PackageInfo struct {
Name string
Path string
Version struct {
Code int
Name string
}
}
var (
rePkgPath = regexp.MustCompile(`codePath=([^\s]+)`)
reVerCode = regexp.MustCompile(`versionCode=(\d+)`)
reVerName = regexp.MustCompile(`versionName=([^\s]+)`)
)
// StatPackage returns PackageInfo
// If package not found, err will be ErrPackageNotExist
func (c *Device) StatPackage(packageName string) (pi PackageInfo, err error) {
pi.Name = packageName
out, err := c.RunCommand("dumpsys", "package", packageName)
if err != nil {
return
}
matches := rePkgPath.FindStringSubmatch(out)
if len(matches) == 0 {
err = ErrPackageNotExist
return
}
pi.Path = matches[1]
matches = reVerCode.FindStringSubmatch(out)
if len(matches) == 0 {
err = ErrPackageNotExist
return
}
pi.Version.Code, _ = strconv.Atoi(matches[1])
matches = reVerName.FindStringSubmatch(out)
if len(matches) == 0 {
err = ErrPackageNotExist
return
}
pi.Version.Name = matches[1]
return
}
// Properties extract info from $ adb shell getprop
func (c *Device) Properties() (props map[string]string, err error) {
propOutput, err := c.RunCommand("getprop")
if err != nil {
return nil, err
}
re := regexp.MustCompile(`\[(.*?)\]:\s*\[(.*?)\]`)
matches := re.FindAllStringSubmatch(propOutput, -1)
props = make(map[string]string)
for _, m := range matches {
var key = m[1]
var val = m[2]
props[key] = val
}
return
}
/*
RunCommandWithExitCode use a little tricky to get exit code
The tricky is append "; echo :$?" to the command,
and parse out the exit code from output
*/
func (c *Device) RunCommandWithExitCode(cmd string, args ...string) (string, int, error) {
exArgs := append(args, ";", "echo", ":$?")
outStr, err := c.RunCommand(cmd, exArgs...)
if err != nil {
return outStr, 0, err
}
idx := strings.LastIndexByte(outStr, ':')
if idx == -1 {
return outStr, 0, fmt.Errorf("adb shell aborted, can not parse exit code")
}
exitCode, _ := strconv.Atoi(strings.TrimSpace(outStr[idx+1:]))
if exitCode != 0 {
commandLine, _ := prepareCommandLine(cmd, args...)
err = ShellExitError{commandLine, exitCode}
}
outStr = strings.Replace(outStr[0:idx], "\r\n", "\n", -1) // put somewhere else
return outStr, exitCode, err
}
type ShellExitError struct {
Command string
ExitCode int
}
func (s ShellExitError) Error() string {
return fmt.Sprintf("shell %s exit code %d", strconv.Quote(s.Command), s.ExitCode)
}
// DoWriteFile return an object, use this object can Cancel write and get Process
func (c *Device) DoSyncFile(path string, rd io.ReadCloser, size int64, perms os.FileMode) (aw *AsyncWriter, err error) {
dst, err := c.OpenWrite(path, perms, time.Now())
if err != nil {
return nil, err
}
awr := newAsyncWriter(c, dst, path, size)
go func() {
awr.doCopy(rd)
rd.Close()
}()
return awr, nil
}
func (c *Device) DoSyncLocalFile(dst string, src string, perms os.FileMode) (aw *AsyncWriter, err error) {
f, err := os.Open(src)
if err != nil {
return
}
finfo, err := f.Stat()
if err != nil {
return
}
return c.DoSyncFile(dst, f, finfo.Size(), perms)
}
func (c *Device) DoSyncHTTPFile(dst string, srcUrl string, perms os.FileMode) (aw *AsyncWriter, err error) {
res, err := retryablehttp.Get(srcUrl)
if err != nil {
return
}
var length int64
fmt.Sscanf(res.Header.Get("Content-Length"), "%d", &length)
return c.DoSyncFile(dst, res.Body, length, perms)
}
// WriteToFile write a reader stream to device
func (c *Device) WriteToFile(path string, rd io.Reader, perms os.FileMode) (written int64, err error) {
dst, err := c.OpenWrite(path, perms, time.Now())
if err != nil {
return
}
defer func() {
dst.Close()
if err != nil || written == 0 {
return
}
// wait until write finished.
fromTime := time.Now()
for {
if time.Since(fromTime) > time.Second*600 {
err = fmt.Errorf("write file to device timeout (10min)")
return
}
finfo, er := c.Stat(path)
if er != nil && !HasErrCode(er, FileNoExistError) {
err = er
return
}
if finfo == nil {
err = fmt.Errorf("target file %s not created", strconv.Quote(path))
return
}
if finfo != nil && finfo.Size == int32(written) {
break
}
time.Sleep(time.Duration(200+rand.Intn(100)) * time.Millisecond)
}
}()
written, err = io.Copy(dst, rd)
return
}
// WriteHttpToFile download http resource to device
func (c *Device) WriteHttpToFile(path string, urlStr string, perms os.FileMode) (written int64, err error) {
res, err := retryablehttp.Get(urlStr)
if err != nil {
return
}
defer res.Body.Close()
if res.StatusCode != http.StatusOK {
err = fmt.Errorf("http download <%s> status %v", urlStr, res.Status)
return
}
return c.WriteToFile(path, res.Body, perms)
}