123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164 |
- package main
- import (
- "context"
- "encoding/json"
- "fmt"
- "image/jpeg"
- "io"
- "io/ioutil"
- "net"
- "net/http"
- "os"
- "os/exec"
- "path"
- "path/filepath"
- "runtime"
- "strconv"
- "strings"
- "sync"
- "syscall"
- "time"
- "github.com/openatx/atx-agent/jsonrpc"
- "github.com/gorilla/mux"
- "github.com/gorilla/websocket"
- "github.com/mholt/archiver"
- "github.com/openatx/androidutils"
- "github.com/openatx/atx-agent/cmdctrl"
- "github.com/prometheus/procfs"
- "github.com/rs/cors"
- )
- type Server struct {
- // tunnel *TunnelProxy
- httpServer *http.Server
- }
- func NewServer() *Server {
- server := &Server{}
- server.initHTTPServer()
- return server
- }
- func (server *Server) initHTTPServer() {
- m := mux.NewRouter()
- m.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
- renderHTML(w, "index.html")
- })
- m.HandleFunc("/version", func(w http.ResponseWriter, r *http.Request) {
- io.WriteString(w, version)
- })
- m.HandleFunc("/remote", func(w http.ResponseWriter, r *http.Request) {
- renderHTML(w, "remote.html")
- })
- // jsonrpc client to call uiautomator
- rpcc := jsonrpc.NewClient("http://127.0.0.1:9008/jsonrpc/0")
- rpcc.ErrorCallback = func() error {
- service.Restart("uiautomator")
- // if !service.Running("uiautomator") {
- // service.Start("uiautomator")
- // }
- return nil
- }
- rpcc.ErrorFixTimeout = 40 * time.Second
- rpcc.ServerOK = func() bool {
- return service.Running("uiautomator")
- }
- m.HandleFunc("/newCommandTimeout", func(w http.ResponseWriter, r *http.Request) {
- var timeout int
- err := json.NewDecoder(r.Body).Decode(&timeout) // TODO: auto get rotation
- if err != nil {
- http.Error(w, "Empty payload", 400) // bad request
- return
- }
- cmdTimeout := time.Duration(timeout) * time.Second
- uiautomatorTimer.Reset(cmdTimeout)
- renderJSON(w, map[string]interface{}{
- "success": true,
- "description": fmt.Sprintf("newCommandTimeout updated to %v", cmdTimeout),
- })
- }).Methods("POST")
- // robust communicate with uiautomator
- // If the service is down, restart it and wait it recover
- m.HandleFunc("/dump/hierarchy", func(w http.ResponseWriter, r *http.Request) {
- if !service.Running("uiautomator") {
- xmlContent, err := dumpHierarchy()
- if err != nil {
- log.Println("Err:", err)
- http.Error(w, err.Error(), http.StatusInternalServerError)
- return
- }
- renderJSON(w, map[string]interface{}{
- "jsonrpc": "2.0",
- "id": 1,
- "result": xmlContent,
- })
- return
- }
- resp, err := rpcc.RobustCall("dumpWindowHierarchy", false) // false: no compress
- if err != nil {
- log.Println("Err:", err)
- http.Error(w, err.Error(), http.StatusInternalServerError)
- return
- }
- renderJSON(w, resp)
- })
- m.HandleFunc("/proc/list", func(w http.ResponseWriter, r *http.Request) {
- ps, err := listAllProcs()
- if err != nil {
- http.Error(w, err.Error(), 500)
- return
- }
- renderJSON(w, ps)
- })
- m.HandleFunc("/proc/{pkgname}/meminfo", func(w http.ResponseWriter, r *http.Request) {
- pkgname := mux.Vars(r)["pkgname"]
- info, err := parseMemoryInfo(pkgname)
- if err != nil {
- http.Error(w, err.Error(), http.StatusInternalServerError)
- return
- }
- renderJSON(w, info)
- })
- m.HandleFunc("/proc/{pkgname}/meminfo/all", func(w http.ResponseWriter, r *http.Request) {
- pkgname := mux.Vars(r)["pkgname"]
- ps, err := listAllProcs()
- if err != nil {
- http.Error(w, err.Error(), 500)
- return
- }
- mems := make(map[string]map[string]int, 0)
- for _, p := range ps {
- if len(p.Cmdline) != 1 {
- continue
- }
- if p.Name == pkgname || strings.HasPrefix(p.Name, pkgname+":") {
- info, err := parseMemoryInfo(p.Name)
- if err != nil {
- continue
- }
- mems[p.Name] = info
- }
- }
- renderJSON(w, mems)
- })
- // make(map[int][]int)
- m.HandleFunc("/proc/{pkgname}/cpuinfo", func(w http.ResponseWriter, r *http.Request) {
- pkgname := mux.Vars(r)["pkgname"]
- pid, err := pidOf(pkgname)
- if err != nil {
- http.Error(w, err.Error(), http.StatusGone)
- return
- }
- info, err := readCPUInfo(pid)
- if err != nil {
- http.Error(w, err.Error(), http.StatusInternalServerError)
- return
- }
- renderJSON(w, info)
- })
- m.HandleFunc("/webviews", func(w http.ResponseWriter, r *http.Request) {
- netUnix, err := procfs.NewNetUnix()
- if err != nil {
- return
- }
- unixPaths := make(map[string]bool, 0)
- for _, row := range netUnix.Rows {
- if !strings.HasPrefix(row.Path, "@") {
- continue
- }
- if !strings.Contains(row.Path, "devtools_remote") {
- continue
- }
- unixPaths[row.Path[1:]] = true
- }
- socketPaths := make([]string, 0, len(unixPaths))
- for key := range unixPaths {
- socketPaths = append(socketPaths, key)
- }
- renderJSON(w, socketPaths)
- })
- m.HandleFunc("/webviews/{pkgname}", func(w http.ResponseWriter, r *http.Request) {
- packageName := mux.Vars(r)["pkgname"]
- netUnix, err := procfs.NewNetUnix()
- if err != nil {
- return
- }
- unixPaths := make(map[string]bool, 0)
- for _, row := range netUnix.Rows {
- if !strings.HasPrefix(row.Path, "@") {
- continue
- }
- if !strings.Contains(row.Path, "devtools_remote") {
- continue
- }
- unixPaths[row.Path[1:]] = true
- }
- result := make([]interface{}, 0)
- procs, err := findProcAll(packageName)
- for _, proc := range procs {
- cmdline, _ := proc.CmdLine()
- suffix := "_" + strconv.Itoa(proc.PID)
- for socketPath := range unixPaths {
- if strings.HasSuffix(socketPath, suffix) ||
- (packageName == "com.android.browser" && socketPath == "chrome_devtools_remote") {
- result = append(result, map[string]interface{}{
- "pid": proc.PID,
- "name": cmdline[0],
- "socketPath": socketPath,
- })
- }
- }
- }
- renderJSON(w, result)
- })
- m.HandleFunc("/pidof/{pkgname}", func(w http.ResponseWriter, r *http.Request) {
- pkgname := mux.Vars(r)["pkgname"]
- pid, err := pidOf(pkgname)
- if err != nil {
- http.Error(w, err.Error(), http.StatusGone)
- return
- }
- io.WriteString(w, strconv.Itoa(pid))
- })
- m.HandleFunc("/session/{pkgname}", func(w http.ResponseWriter, r *http.Request) {
- packageName := mux.Vars(r)["pkgname"]
- mainActivity, err := mainActivityOf(packageName)
- if err != nil {
- http.Error(w, err.Error(), http.StatusGone) // 410
- return
- }
- // Refs: https://stackoverflow.com/questions/12131555/leading-dot-in-androidname-really-required
- // MainActivity convert to .MainActivity
- // com.example.app.MainActivity keep same
- // app.MainActivity keep same
- // So only words not contains dot, need to add prefix "."
- if !strings.Contains(mainActivity, ".") {
- mainActivity = "." + mainActivity
- }
- flags := r.FormValue("flags")
- if flags == "" {
- flags = "-W -S" // W: wait launched, S: stop before started
- }
- timeout := r.FormValue("timeout") // supported value: 60s, 1m. 60 is invalid
- duration, err := time.ParseDuration(timeout)
- if err != nil {
- duration = 60 * time.Second
- }
- output, err := runShellTimeout(duration, "am", "start", flags, "-n", packageName+"/"+mainActivity)
- if err != nil {
- renderJSON(w, map[string]interface{}{
- "success": false,
- "error": err.Error(),
- "output": string(output),
- "mainActivity": mainActivity,
- })
- } else {
- renderJSON(w, map[string]interface{}{
- "success": true,
- "mainActivity": mainActivity,
- "output": string(output),
- })
- }
- }).Methods("POST")
- m.HandleFunc("/session/{pid:[0-9]+}:{pkgname}/{url:ping|jsonrpc/0}", func(w http.ResponseWriter, r *http.Request) {
- pkgname := mux.Vars(r)["pkgname"]
- pid, _ := strconv.Atoi(mux.Vars(r)["pid"])
- proc, err := procfs.NewProc(pid)
- if err != nil {
- http.Error(w, err.Error(), http.StatusGone) // 410
- return
- }
- cmdline, _ := proc.CmdLine()
- if len(cmdline) != 1 || cmdline[0] != pkgname {
- http.Error(w, fmt.Sprintf("cmdline expect [%s] but got %v", pkgname, cmdline), http.StatusGone)
- return
- }
- r.URL.Path = "/" + mux.Vars(r)["url"]
- uiautomatorProxy.ServeHTTP(w, r)
- })
- m.HandleFunc("/shell", func(w http.ResponseWriter, r *http.Request) {
- command := r.FormValue("command")
- if command == "" {
- command = r.FormValue("c")
- }
- timeoutSeconds := r.FormValue("timeout")
- if timeoutSeconds == "" {
- timeoutSeconds = "60"
- }
- seconds, err := strconv.Atoi(timeoutSeconds)
- if err != nil {
- http.Error(w, err.Error(), http.StatusBadRequest)
- return
- }
- c := Command{
- Args: []string{command},
- Shell: true,
- Timeout: time.Duration(seconds) * time.Second,
- }
- output, err := c.CombinedOutput()
- exitCode := cmdError2Code(err)
- renderJSON(w, map[string]interface{}{
- "output": string(output),
- "exitCode": exitCode,
- "error": err,
- })
- }).Methods("GET", "POST")
- // TODO(ssx): untested
- m.HandleFunc("/shell/background", func(w http.ResponseWriter, r *http.Request) {
- command := r.FormValue("command")
- if command == "" {
- command = r.FormValue("c")
- }
- c := Command{
- Args: []string{command},
- Shell: true,
- }
- pid, err := c.StartBackground()
- if err != nil {
- renderJSON(w, map[string]interface{}{
- "success": false,
- "description": err.Error(),
- })
- return
- }
- renderJSON(w, map[string]interface{}{
- "success": true,
- "pid": pid,
- "description": fmt.Sprintf("Successfully started program: %v", command),
- })
- })
- m.HandleFunc("/shell/stream", func(w http.ResponseWriter, r *http.Request) {
- w.Header().Set("Content-Type", "application/octet-stream")
- command := r.FormValue("command")
- if command == "" {
- command = r.FormValue("c")
- }
- c := exec.Command("sh", "-c", command)
- httpWriter := newFakeWriter(func(data []byte) (int, error) {
- n, err := w.Write(data)
- if err == nil {
- if f, ok := w.(http.Flusher); ok {
- f.Flush()
- }
- } else {
- log.Println("Write error")
- }
- return n, err
- })
- c.Stdout = httpWriter
- c.Stderr = httpWriter
- // wait until program quit
- cmdQuit := make(chan error, 0)
- go func() {
- cmdQuit <- c.Run()
- }()
- select {
- case <-httpWriter.Err:
- if c.Process != nil {
- c.Process.Signal(syscall.SIGTERM)
- }
- case <-cmdQuit:
- log.Println("command quit")
- }
- log.Println("program quit")
- })
- m.HandleFunc("/stop", func(w http.ResponseWriter, r *http.Request) {
- log.Println("stop all service")
- service.StopAll()
- log.Println("service stopped")
- io.WriteString(w, "Finished!")
- go func() {
- ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
- defer cancel() // The document says need to call cancel(), but I donot known why.
- server.httpServer.Shutdown(ctx)
- }()
- })
- m.HandleFunc("/services/{name}", func(w http.ResponseWriter, r *http.Request) {
- name := mux.Vars(r)["name"]
- var resp map[string]interface{}
- if !service.Exists(name) {
- w.WriteHeader(400) // bad request
- renderJSON(w, map[string]interface{}{
- "success": false,
- "description": fmt.Sprintf("service %s does not exist", strconv.Quote(name)),
- })
- return
- }
- switch r.Method {
- case "GET":
- resp = map[string]interface{}{
- "success": true,
- "running": service.Running(name),
- }
- case "POST":
- err := service.Start(name)
- switch err {
- case nil:
- resp = map[string]interface{}{
- "success": true,
- "description": "successfully started",
- }
- case cmdctrl.ErrAlreadyRunning:
- resp = map[string]interface{}{
- "success": true,
- "description": "already started",
- }
- default:
- resp = map[string]interface{}{
- "success": false,
- "description": "failure on start: " + err.Error(),
- }
- }
- case "DELETE":
- err := service.Stop(name)
- switch err {
- case nil:
- resp = map[string]interface{}{
- "success": true,
- "description": "successfully stopped",
- }
- case cmdctrl.ErrAlreadyStopped:
- resp = map[string]interface{}{
- "success": true,
- "description": "already stopped",
- }
- default:
- resp = map[string]interface{}{
- "success": false,
- "description": "failure on stop: " + err.Error(),
- }
- }
- default:
- resp = map[string]interface{}{
- "success": false,
- "description": "invalid request method: " + r.Method,
- }
- }
- if ok, success := resp["success"].(bool); ok {
- if !success {
- w.WriteHeader(400) // bad request
- }
- }
- renderJSON(w, resp)
- }).Methods("GET", "POST", "DELETE")
- // Deprecated use /services/{name} instead
- m.HandleFunc("/uiautomator", func(w http.ResponseWriter, r *http.Request) {
- err := service.Start("uiautomator")
- if err == nil {
- io.WriteString(w, "Successfully started")
- } else if err == cmdctrl.ErrAlreadyRunning {
- io.WriteString(w, "Already started")
- } else {
- http.Error(w, err.Error(), 500)
- }
- }).Methods("POST")
- // Deprecated
- m.HandleFunc("/uiautomator", func(w http.ResponseWriter, r *http.Request) {
- err := service.Stop("uiautomator", true) // wait until program quit
- if err == nil {
- io.WriteString(w, "Successfully stopped")
- } else if err == cmdctrl.ErrAlreadyStopped {
- io.WriteString(w, "Already stopped")
- } else {
- http.Error(w, err.Error(), 500)
- }
- }).Methods("DELETE")
- // Deprecated
- m.HandleFunc("/uiautomator", func(w http.ResponseWriter, r *http.Request) {
- running := service.Running("uiautomator")
- renderJSON(w, map[string]interface{}{
- "running": running,
- })
- }).Methods("GET")
- m.HandleFunc("/raw/{filepath:.*}", func(w http.ResponseWriter, r *http.Request) {
- filepath := "/" + mux.Vars(r)["filepath"]
- http.ServeFile(w, r, filepath)
- })
- m.HandleFunc("/finfo/{lpath:.*}", func(w http.ResponseWriter, r *http.Request) {
- lpath := "/" + mux.Vars(r)["lpath"]
- finfo, err := os.Stat(lpath)
- if err != nil {
- if os.IsNotExist(err) {
- http.Error(w, err.Error(), 404)
- } else {
- http.Error(w, err.Error(), 403) // forbidden
- }
- return
- }
- data := make(map[string]interface{}, 5)
- data["name"] = finfo.Name()
- data["path"] = lpath
- data["isDirectory"] = finfo.IsDir()
- data["size"] = finfo.Size()
- if finfo.IsDir() {
- files, err := ioutil.ReadDir(lpath)
- if err == nil {
- finfos := make([]map[string]interface{}, 0, 3)
- for _, f := range files {
- finfos = append(finfos, map[string]interface{}{
- "name": f.Name(),
- "path": filepath.Join(lpath, f.Name()),
- "isDirectory": f.IsDir(),
- })
- }
- data["files"] = finfos
- }
- }
- renderJSON(w, data)
- })
- // keep ApkService always running
- // if no activity in 5min, then restart apk service
- const apkServiceTimeout = 5 * time.Minute
- apkServiceTimer := NewSafeTimer(apkServiceTimeout)
- go func() {
- for range apkServiceTimer.C {
- log.Println("startservice com.github.uiautomator/.Service")
- runShell("am", "startservice", "-n", "com.github.uiautomator/.Service")
- apkServiceTimer.Reset(apkServiceTimeout)
- }
- }()
- deviceInfo := getDeviceInfo()
- m.HandleFunc("/info", func(w http.ResponseWriter, r *http.Request) {
- w.Header().Set("Content-Type", "application/json")
- json.NewEncoder(w).Encode(deviceInfo)
- })
- m.HandleFunc("/info/battery", func(w http.ResponseWriter, r *http.Request) {
- apkServiceTimer.Reset(apkServiceTimeout)
- deviceInfo.Battery.Update()
- // if err := server.tunnel.UpdateInfo(deviceInfo); err != nil {
- // io.WriteString(w, "Failure "+err.Error())
- // return
- // }
- io.WriteString(w, "Success")
- }).Methods("POST")
- m.HandleFunc("/info/rotation", func(w http.ResponseWriter, r *http.Request) {
- apkServiceTimer.Reset(apkServiceTimeout)
- var direction int // 0,1,2,3
- err := json.NewDecoder(r.Body).Decode(&direction) // TODO: auto get rotation
- if err == nil {
- deviceRotation = direction * 90
- log.Println("rotation change received:", deviceRotation)
- } else {
- rotation, er := androidutils.Rotation()
- if er != nil {
- log.Println("rotation auto get err:", er)
- http.Error(w, "Failure", 500)
- return
- }
- deviceRotation = rotation
- }
- // Kill not controled minicap
- killed := false
- procWalk(func(proc procfs.Proc) {
- executable, _ := proc.Executable()
- if filepath.Base(executable) != "minicap" {
- return
- }
- stat, err := proc.NewStat()
- if err != nil || stat.PPID != 1 { // only not controled minicap need killed
- return
- }
- if p, err := os.FindProcess(proc.PID); err == nil {
- log.Println("Kill", executable)
- p.Kill()
- killed = true
- }
- })
- if killed {
- service.Start("minicap")
- }
- // minicapHub.broadcast <- []byte("rotation " + strconv.Itoa(deviceRotation))
- updateMinicapRotation(deviceRotation)
- rotationPublisher.Submit(deviceRotation)
- // APK Service will send rotation to atx-agent when rotation changes
- runShellTimeout(5*time.Second, "am", "startservice", "--user", "0", "-n", "com.github.uiautomator/.Service")
- renderJSON(w, map[string]int{
- "rotation": deviceRotation,
- })
- // fmt.Fprintf(w, "rotation change to %d", deviceRotation)
- })
- /*
- # URLRules:
- # URLPath ends with / means directory, eg: $DEVICE_URL/upload/sdcard/
- # The rest means file, eg: $DEVICE_URL/upload/sdcard/a.txt
- #
- # Upload a file to destination
- $ curl -X POST -F file=@file.txt -F mode=0755 $DEVICE_URL/upload/sdcard/a.txt
- # Upload a directory (file must be zip), URLPath must ends with /
- $ curl -X POST -F file=@dir.zip -F dir=true $DEVICE_URL/upload/sdcard/atx-stuffs/
- */
- m.HandleFunc("/upload/{target:.*}", func(w http.ResponseWriter, r *http.Request) {
- target := mux.Vars(r)["target"]
- if runtime.GOOS != "windows" {
- target = "/" + target
- }
- isDir := r.FormValue("dir") == "true"
- var fileMode os.FileMode
- if _, err := fmt.Sscanf(r.FormValue("mode"), "%o", &fileMode); !isDir && err != nil {
- log.Printf("invalid file mode: %s", r.FormValue("mode"))
- fileMode = 0644
- } // %o base 8
- file, header, err := r.FormFile("file")
- if err != nil {
- http.Error(w, err.Error(), http.StatusBadRequest)
- return
- }
- defer func() {
- file.Close()
- r.MultipartForm.RemoveAll()
- }()
- var targetDir = target
- if !isDir {
- if strings.HasSuffix(target, "/") {
- target = path.Join(target, header.Filename)
- }
- targetDir = filepath.Dir(target)
- } else {
- if !strings.HasSuffix(target, "/") {
- http.Error(w, "URLPath must endswith / if upload a directory", 400)
- return
- }
- }
- if _, err := os.Stat(targetDir); os.IsNotExist(err) {
- os.MkdirAll(targetDir, 0755)
- }
- if isDir {
- err = archiver.Zip.Read(file, target)
- } else {
- err = copyToFile(file, target)
- }
- if err != nil {
- http.Error(w, err.Error(), http.StatusInternalServerError)
- return
- }
- if !isDir && fileMode != 0 {
- os.Chmod(target, fileMode)
- }
- if fileInfo, err := os.Stat(target); err == nil {
- fileMode = fileInfo.Mode()
- }
- w.Header().Set("Content-Type", "application/json; charset=utf-8")
- json.NewEncoder(w).Encode(map[string]interface{}{
- "target": target,
- "isDir": isDir,
- "mode": fmt.Sprintf("0%o", fileMode),
- })
- })
- m.HandleFunc("/download", func(w http.ResponseWriter, r *http.Request) {
- dst := r.FormValue("filepath")
- url := r.FormValue("url")
- var fileMode os.FileMode
- if _, err := fmt.Sscanf(r.FormValue("mode"), "%o", &fileMode); err != nil {
- log.Printf("invalid file mode: %s", r.FormValue("mode"))
- fileMode = 0644
- } // %o base 8
- key := background.HTTPDownload(url, dst, fileMode)
- io.WriteString(w, key)
- }).Methods("POST")
- m.HandleFunc("/download/{key}", func(w http.ResponseWriter, r *http.Request) {
- key := mux.Vars(r)["key"]
- status := background.Get(key)
- w.Header().Set("Content-Type", "application/json")
- json.NewEncoder(w).Encode(status)
- }).Methods("GET")
- m.HandleFunc("/packages", func(w http.ResponseWriter, r *http.Request) {
- var url = r.FormValue("url")
- filepath := TempFileName("/sdcard/tmp", ".apk")
- key := background.HTTPDownload(url, filepath, 0644)
- go func() {
- defer os.Remove(filepath) // release sdcard space
- state := background.Get(key)
- state.Status = "downloading"
- if err := background.Wait(key); err != nil {
- log.Println("http download error")
- state.Error = err.Error()
- state.Status = "failure"
- state.Message = "http download error"
- return
- }
- state.Status = "installing"
- if err := forceInstallAPK(filepath); err != nil {
- state.Error = err.Error()
- state.Status = "failure"
- } else {
- state.Status = "success"
- }
- }()
- renderJSON(w, map[string]interface{}{
- "success": true,
- "data": map[string]string{
- "id": key,
- },
- })
- }).Methods("POST")
- // id: int
- m.HandleFunc("/packages/{id}", func(w http.ResponseWriter, r *http.Request) {
- id := mux.Vars(r)["id"]
- state := background.Get(id)
- w.Header().Set("Content-Type", "application/json")
- data, _ := json.Marshal(state.Progress)
- renderJSON(w, map[string]interface{}{
- "success": true,
- "data": map[string]string{
- "status": state.Status,
- "description": string(data),
- },
- })
- json.NewEncoder(w).Encode(state)
- }).Methods("GET")
- m.HandleFunc("/packages", func(w http.ResponseWriter, r *http.Request) {
- pkgs, err := listPackages()
- if err != nil {
- w.WriteHeader(500)
- renderJSON(w, map[string]interface{}{
- "success": false,
- "description": err.Error(),
- })
- return
- }
- renderJSON(w, pkgs)
- }).Methods("GET")
- m.HandleFunc("/packages/{pkgname}/info", func(w http.ResponseWriter, r *http.Request) {
- pkgname := mux.Vars(r)["pkgname"]
- info, err := readPackageInfo(pkgname)
- if err != nil {
- renderJSON(w, map[string]interface{}{
- "success": false,
- "description": err.Error(), // "package " + strconv.Quote(pkgname) + " not found",
- })
- return
- }
- renderJSON(w, map[string]interface{}{
- "success": true,
- "data": info,
- })
- })
- m.HandleFunc("/packages/{pkgname}/icon", func(w http.ResponseWriter, r *http.Request) {
- pkgname := mux.Vars(r)["pkgname"]
- info, err := readPackageInfo(pkgname)
- if err != nil {
- http.Error(w, "package not found", 403)
- return
- }
- if info.Icon == nil {
- http.Error(w, "package not found", 400)
- return
- }
- w.Header().Set("Content-Type", "image/jpeg")
- jpeg.Encode(w, info.Icon, &jpeg.Options{Quality: 80})
- })
- // deprecated
- m.HandleFunc("/install", func(w http.ResponseWriter, r *http.Request) {
- var url = r.FormValue("url")
- var tmpdir = r.FormValue("tmpdir")
- if tmpdir == "" {
- tmpdir = "/data/local/tmp"
- }
- filepath := TempFileName(tmpdir, ".apk")
- key := background.HTTPDownload(url, filepath, 0644)
- go func() {
- defer os.Remove(filepath) // release sdcard space
- state := background.Get(key)
- state.Status = "downloading"
- if err := background.Wait(key); err != nil {
- log.Println("http download error")
- state.Error = err.Error()
- state.Message = "http download error"
- state.Status = "failure"
- return
- }
- state.Message = "installing"
- state.Status = "installing"
- if err := forceInstallAPK(filepath); err != nil {
- state.Error = err.Error()
- state.Message = "error install"
- state.Status = "failure"
- } else {
- state.Message = "success installed"
- state.Status = "success"
- }
- }()
- io.WriteString(w, key)
- }).Methods("POST")
- // deprecated
- m.HandleFunc("/install/{id}", func(w http.ResponseWriter, r *http.Request) {
- id := mux.Vars(r)["id"]
- state := background.Get(id)
- w.Header().Set("Content-Type", "application/json")
- json.NewEncoder(w).Encode(state)
- }).Methods("GET")
- // deprecated
- m.HandleFunc("/install/{id}", func(w http.ResponseWriter, r *http.Request) {
- id := mux.Vars(r)["id"]
- state := background.Get(id)
- if state.Progress != nil {
- if dproxy, ok := state.Progress.(*downloadProxy); ok {
- dproxy.Cancel()
- io.WriteString(w, "Cancelled")
- return
- }
- }
- io.WriteString(w, "Unable to canceled")
- }).Methods("DELETE")
- // fix minitouch
- m.HandleFunc("/minitouch", func(w http.ResponseWriter, r *http.Request) {
- if err := installMinitouch(); err == nil {
- log.Println("update minitouch success")
- io.WriteString(w, "Update minitouch success")
- } else {
- http.Error(w, err.Error(), http.StatusInternalServerError)
- }
- }).Methods("PUT")
- m.HandleFunc("/minitouch", func(w http.ResponseWriter, r *http.Request) {
- service.Stop("minitouch", true)
- io.WriteString(w, "minitouch stopped")
- }).Methods("DELETE")
- m.HandleFunc("/minitouch", singleFightNewerWebsocket(func(w http.ResponseWriter, r *http.Request, ws *websocket.Conn) {
- defer ws.Close()
- const wsWriteWait = 10 * time.Second
- wsWrite := func(messageType int, data []byte) error {
- ws.SetWriteDeadline(time.Now().Add(wsWriteWait))
- return ws.WriteMessage(messageType, data)
- }
- wsWrite(websocket.TextMessage, []byte("start @minitouch service"))
- if err := service.Start("minitouch"); err != nil && err != cmdctrl.ErrAlreadyRunning {
- wsWrite(websocket.TextMessage, []byte("@minitouch service start failed: "+err.Error()))
- return
- }
- unixSocketPath := minitouchSocketPath
- wsWrite(websocket.TextMessage, []byte("dial unix:"+unixSocketPath))
- log.Printf("minitouch connection: %v", r.RemoteAddr)
- retries := 0
- quitC := make(chan bool, 2)
- operC := make(chan TouchRequest, 10)
- defer func() {
- wsWrite(websocket.TextMessage, []byte(unixSocketPath+" websocket closed"))
- close(operC)
- }()
- go func() {
- for {
- if retries > 10 {
- log.Printf("unix %s connect failed", unixSocketPath)
- wsWrite(websocket.TextMessage, []byte(unixSocketPath+" listen timeout, possibly minitouch not installed"))
- ws.Close()
- break
- }
- conn, err := net.Dial("unix", unixSocketPath)
- if err != nil {
- retries++
- log.Printf("dial %s error: %v, wait 0.5s", unixSocketPath, err)
- select {
- case <-quitC:
- return
- case <-time.After(500 * time.Millisecond):
- }
- continue
- }
- log.Printf("unix %s connected, accepting requests", unixSocketPath)
- retries = 0 // connected, reset retries
- err = drainTouchRequests(conn, operC)
- conn.Close()
- if err != nil {
- log.Println("drain touch requests err:", err)
- } else {
- log.Printf("unix %s disconnected", unixSocketPath)
- break // operC closed
- }
- }
- }()
- var touchRequest TouchRequest
- for {
- err := ws.ReadJSON(&touchRequest)
- if err != nil {
- log.Println("readJson err:", err)
- quitC <- true
- break
- }
- select {
- case operC <- touchRequest:
- case <-time.After(2 * time.Second):
- wsWrite(websocket.TextMessage, []byte("touch request buffer full"))
- }
- }
- })).Methods("GET")
- // fix minicap
- m.HandleFunc("/minicap", func(w http.ResponseWriter, r *http.Request) {
- if err := installMinicap(); err == nil {
- log.Println("update minicap success")
- io.WriteString(w, "Update minicap success")
- } else {
- http.Error(w, err.Error(), http.StatusInternalServerError)
- }
- }).Methods("PUT")
- minicapHandler := broadcastWebsocket()
- m.HandleFunc("/minicap/broadcast", minicapHandler).Methods("GET")
- m.HandleFunc("/minicap", minicapHandler).Methods("GET")
- // TODO(ssx): perfer to delete
- // FIXME(ssx): screenrecord is not good enough, need to change later
- var recordCmd *exec.Cmd
- var recordDone = make(chan bool, 1)
- var recordLock sync.Mutex
- var recordFolder = "/sdcard/screenrecords/"
- var recordRunning = false
- m.HandleFunc("/screenrecord", func(w http.ResponseWriter, r *http.Request) {
- recordLock.Lock()
- defer recordLock.Unlock()
- if recordCmd != nil {
- http.Error(w, "screenrecord not closed", 400)
- return
- }
- os.RemoveAll(recordFolder)
- os.MkdirAll(recordFolder, 0755)
- recordCmd = exec.Command("screenrecord", recordFolder+"0.mp4")
- if err := recordCmd.Start(); err != nil {
- http.Error(w, err.Error(), 500)
- return
- }
- recordRunning = true
- go func() {
- for i := 1; recordCmd.Wait() == nil && i <= 20 && recordRunning; i++ { // set limit, to prevent too many videos. max 1 hour
- recordCmd = exec.Command("screenrecord", recordFolder+strconv.Itoa(i)+".mp4")
- if err := recordCmd.Start(); err != nil {
- log.Println("screenrecord error:", err)
- break
- }
- }
- recordDone <- true
- }()
- io.WriteString(w, "screenrecord started")
- }).Methods("POST")
- m.HandleFunc("/screenrecord", func(w http.ResponseWriter, r *http.Request) {
- recordLock.Lock()
- defer recordLock.Unlock()
- recordRunning = false
- if recordCmd != nil {
- if recordCmd.Process != nil {
- recordCmd.Process.Signal(os.Interrupt)
- }
- select {
- case <-recordDone:
- case <-time.After(5 * time.Second):
- // force kill
- exec.Command("pkill", "screenrecord").Run()
- }
- recordCmd = nil
- }
- w.Header().Set("Content-Type", "application/json")
- files, _ := ioutil.ReadDir(recordFolder)
- videos := []string{}
- for i := 0; i < len(files); i++ {
- videos = append(videos, fmt.Sprintf(recordFolder+"%d.mp4", i))
- }
- json.NewEncoder(w).Encode(map[string]interface{}{
- "videos": videos,
- })
- }).Methods("PUT")
- m.HandleFunc("/upgrade", func(w http.ResponseWriter, r *http.Request) {
- ver := r.FormValue("version")
- var err error
- if ver == "" {
- ver, err = getLatestVersion()
- if err != nil {
- http.Error(w, err.Error(), 500)
- return
- }
- }
- if ver == version {
- io.WriteString(w, "current version is already "+version)
- return
- }
- err = doUpdate(ver)
- if err != nil {
- http.Error(w, err.Error(), 500)
- return
- }
- io.WriteString(w, "update finished, restarting")
- go func() {
- log.Printf("restarting server")
- // TODO(ssx): runDaemon()
- }()
- })
- m.HandleFunc("/term", func(w http.ResponseWriter, r *http.Request) {
- if r.Header.Get("Upgrade") == "websocket" {
- handleTerminalWebsocket(w, r)
- return
- }
- renderHTML(w, "terminal.html")
- })
- screenshotIndex := -1
- nextScreenshotFilename := func() string {
- targetFolder := "/data/local/tmp/minicap-images"
- if _, err := os.Stat(targetFolder); err != nil {
- os.MkdirAll(targetFolder, 0755)
- }
- screenshotIndex = (screenshotIndex + 1) % 5
- return filepath.Join(targetFolder, fmt.Sprintf("%d.jpg", screenshotIndex))
- }
- m.HandleFunc("/screenshot", func(w http.ResponseWriter, r *http.Request) {
- targetURL := "/screenshot/0"
- if r.URL.RawQuery != "" {
- targetURL += "?" + r.URL.RawQuery
- }
- http.Redirect(w, r, targetURL, 302)
- }).Methods("GET")
- m.Handle("/jsonrpc/0", uiautomatorProxy)
- m.Handle("/ping", uiautomatorProxy)
- m.HandleFunc("/screenshot/0", func(w http.ResponseWriter, r *http.Request) {
- download := r.FormValue("download")
- if download != "" {
- w.Header().Set("Content-Disposition", "attachment; filename="+download)
- }
- thumbnailSize := r.FormValue("thumbnail")
- filename := nextScreenshotFilename()
- // android emulator use screencap
- // then minicap when binary and .so exists
- // then uiautomator when service(uiautomator) is running
- // last screencap
- method := "screencap"
- if getCachedProperty("ro.product.cpu.abi") == "x86" { // android emulator
- method = "screencap"
- } else if fileExists("/data/local/tmp/minicap") && fileExists("/data/local/tmp/minicap.so") && r.FormValue("minicap") != "false" && strings.ToLower(getCachedProperty("ro.product.manufacturer")) != "meizu" {
- method = "minicap"
- } else if service.Running("uiautomator") {
- method = "uiautomator"
- }
- var err error
- switch method {
- case "screencap":
- err = screenshotWithScreencap(filename)
- case "minicap":
- err = screenshotWithMinicap(filename, thumbnailSize)
- case "uiautomator":
- uiautomatorProxy.ServeHTTP(w, r)
- return
- }
- if err != nil && method != "screencap" {
- method = "screencap"
- err = screenshotWithScreencap(filename)
- }
- if err != nil {
- http.Error(w, err.Error(), 500)
- return
- }
- w.Header().Set("X-Screenshot-Method", method)
- http.ServeFile(w, r, filename)
- })
- m.HandleFunc("/wlan/ip", func(w http.ResponseWriter, r *http.Request) {
- itf, err := net.InterfaceByName("wlan0")
- if err != nil {
- http.Error(w, err.Error(), http.StatusInternalServerError)
- return
- }
- addrs, err := itf.Addrs()
- if err != nil {
- http.Error(w, err.Error(), http.StatusInternalServerError)
- return
- }
- for _, addr := range addrs {
- if v, ok := addr.(*net.IPNet); ok {
- io.WriteString(w, v.IP.String())
- }
- return
- }
- http.Error(w, "wlan0 have no ip address", 500)
- })
- m.Handle("/assets/{(.*)}", http.StripPrefix("/assets", http.FileServer(Assets)))
- var handler = cors.New(cors.Options{
- AllowedMethods: []string{"GET", "POST", "PUT", "DELETE"},
- }).Handler(m)
- // logHandler := handlers.LoggingHandler(os.Stdout, handler)
- server.httpServer = &http.Server{Handler: handler} // url(/stop) need it.
- }
- func (s *Server) Serve(lis net.Listener) error {
- return s.httpServer.Serve(lis)
- }
|