mirror of
git://git.sv.gnu.org/emacs.git
synced 2025-12-30 17:10:51 -08:00
* test/infra/android/test-controller.el (ats-run-test): Strip text properties from value string. Inhibit text conversion. * test/lisp/emacs-lisp/find-func-tests.el (find-func-tests--locate-symbols): * test/lisp/emacs-lisp/rmc-tests.el (test-rmc--add-key-description) (test-rmc--add-key-description/with-attributes) (test-rmc--add-key-description/non-graphical-display) (test-read-multiple-choice, test-read-multiple-choice-help): Skip on Android in some wise or another.
2488 lines
91 KiB
EmacsLisp
2488 lines
91 KiB
EmacsLisp
;;; Submit code to a connected Android device -*- lexical-binding: t; -*-
|
||
|
||
;; Copyright (C) 2025 Free Software Foundation, Inc.
|
||
|
||
;; This file is part of GNU Emacs.
|
||
|
||
;; GNU Emacs is free software; you can redistribute it and/or modify
|
||
;; it under the terms of the GNU General Public License as published by
|
||
;; the Free Software Foundation, either version 3 of the License, or
|
||
;; (at your option) any later version.
|
||
|
||
;; GNU Emacs is distributed in the hope that it will be useful,
|
||
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||
;; GNU General Public License for more details.
|
||
|
||
;; You should have received a copy of the GNU General Public License
|
||
;; along with GNU Emacs. If not, see <https://www.gnu.org/licenses/>.
|
||
|
||
;;; Commentary:
|
||
;;
|
||
;; This file establishes connections to devices attached over `adb' and
|
||
;; arranges to execute the test driver and submit code to the same.
|
||
|
||
;;; Code:
|
||
|
||
(require 'tramp) ;; Only for a number of regexps.
|
||
|
||
|
||
|
||
;; Device management.
|
||
|
||
(defvar ats-adb-executable nil
|
||
"Name of the `adb' executable on this system, or nil if uninitialized.")
|
||
|
||
(defvar ats-adb-host nil
|
||
"Hostname and port on which the ADB server resides.
|
||
If nil, this value defaults to localhost and an ADB server will
|
||
automatically be started if none is currently executing.")
|
||
|
||
(defvar ats-adb-infile nil
|
||
"File providing the stdin of `adb' subprocesses.")
|
||
|
||
(defvar ats-cache nil
|
||
"Cache recording facts predicated of a device and its contents.")
|
||
|
||
(defvar ats-adb-disable-stderr t
|
||
"Whether not to print error output from subprocesses invoked by `ats-adb'.")
|
||
|
||
(defconst ats-adb-device-regexp
|
||
"\\([^[:space:]]+\\)[[:space:]]+\\([[:alnum:]]+\\)$"
|
||
"Regexp with which to extract devices from `adb devices' output.")
|
||
|
||
(defun ats-adb (&rest commands)
|
||
"Execute `adb COMMANDS' and insert its output into the current buffer.
|
||
Command output is inserted before point."
|
||
(unless ats-adb-executable
|
||
(setq ats-adb-executable
|
||
(or (executable-find "adb")
|
||
(progn
|
||
(message "Could not locate a suitable `adb' binary.
|
||
Please arrange that a version of the Android debugging bridge be present
|
||
in `exec-path' and be permitted to access connected USB devices.
|
||
For more information, visit https://developer.android.com/tools/adb.")
|
||
(error "Could not locate a suitable `adb' binary")))))
|
||
(let ((point (point)) (coding-system-for-read 'utf-8-unix))
|
||
(save-excursion
|
||
(when ats-adb-host
|
||
(setq commands (append (list "-H" ats-adb-host) commands)))
|
||
(let ((rc (apply #'call-process ats-adb-executable
|
||
ats-adb-infile
|
||
(or (and ats-adb-disable-stderr '(t nil)) t)
|
||
nil commands)))
|
||
(when (not (zerop rc))
|
||
(error "%s exited with %s"
|
||
(mapconcat #'shell-quote-argument
|
||
(cons ats-adb-executable commands)
|
||
" ")
|
||
rc))
|
||
;; Undo misguided EOL format conversion performed by the ADB
|
||
;; daemon on older releases of Android.
|
||
(let ((end (point)))
|
||
(goto-char point)
|
||
(while (re-search-forward "\r+$" end t)
|
||
(replace-match "")))))))
|
||
|
||
(defun ats-adb-process-filter (proc string)
|
||
"Insert STRING and update PROC's mark as the default filter does.
|
||
Remove all CR characters preceding newlines in STRING."
|
||
(when (buffer-live-p (process-buffer proc))
|
||
(with-current-buffer (process-buffer proc)
|
||
(let ((new-string (replace-regexp-in-string "\r$" "" string)))
|
||
(save-excursion
|
||
(goto-char (process-mark proc))
|
||
(insert new-string)
|
||
(set-marker (process-mark proc) (point)))))))
|
||
|
||
(defun ats-start-adb (&rest commands)
|
||
"Execute `adb COMMANDS' in an asynchronous subprocess.
|
||
Apply a process filter to delete errant carriage return
|
||
characters."
|
||
(unless ats-adb-executable
|
||
(setq ats-adb-executable
|
||
(or (executable-find "adb")
|
||
(progn
|
||
(message "Could not locate a suitable `adb' binary.
|
||
Please arrange that a version of the Android debugging bridge be present
|
||
in `exec-path' and be permitted to access connected USB devices.
|
||
For more information, visit https://developer.android.com/tools/adb.")
|
||
(error "Could not locate a suitable `adb' binary")))))
|
||
(let ((coding-system-for-read 'utf-8-unix))
|
||
(save-excursion
|
||
(when ats-adb-host
|
||
(setq commands (append (list "-H" ats-adb-host) commands)))
|
||
(let ((process (apply #'start-process " *ats adb*"
|
||
" *ats adb*"
|
||
ats-adb-executable commands)))
|
||
(prog1 process
|
||
(set-process-filter process #'ats-adb-process-filter))))))
|
||
|
||
(defun ats-enumerate-devices (&optional pred arg)
|
||
"Return a list of connected devices as an alist indiced by serial number.
|
||
Value is an alist of device serial numbers that may be provided
|
||
as the `-s' argument to `adb' and the state of the device, which
|
||
is a string that is either \"device\" for a fully available
|
||
device, or another value if the connection to the device is
|
||
defective.
|
||
|
||
If PRED is specified, invoke it on each device with ARG and its
|
||
serial number and state, and only return devices for which it
|
||
returns non-nil."
|
||
(with-temp-buffer
|
||
(ats-adb "devices")
|
||
(re-search-forward "List of devices attached\n" nil t)
|
||
(let ((devices nil))
|
||
(while (re-search-forward ats-adb-device-regexp
|
||
nil t nil)
|
||
(let ((name (match-string 1))
|
||
(state (match-string 2)))
|
||
(when (or (not pred) (funcall pred name state arg))
|
||
(push (cons name state) devices))))
|
||
(nreverse devices))))
|
||
|
||
(defun ats-online-devices ()
|
||
"Like `ats-enumerate-devices', but only return devices which are available."
|
||
(ats-enumerate-devices (lambda (_ state _)
|
||
(equal state "device"))))
|
||
|
||
|
||
|
||
;; Device introspection.
|
||
|
||
(defmacro ats-memoize (device key &rest bodyforms)
|
||
"Return the result of executing BODYFORMS with memoization.
|
||
Cache such result and avoid executing BODYFORMS more than once
|
||
with the same DEVICE and KEY."
|
||
(declare (indent 2))
|
||
(let ((device-key (gensym))
|
||
(cache (gensym))
|
||
(value (gensym)))
|
||
`(let* ((,device-key (concat (or ats-adb-host "localhost")
|
||
"/" ,device))
|
||
(,cache (or (cdr-safe (assoc ,device-key ats-cache))
|
||
(setf (alist-get ,device-key ats-cache
|
||
:testfn #'equal)
|
||
(make-hash-table :test #'equal))))
|
||
(,value (gethash ,key ,cache)))
|
||
(if ,value (car ,value)
|
||
(setq ,value (progn ,@bodyforms))
|
||
(prog1 ,value
|
||
(puthash ,key (list ,value) ,cache))))))
|
||
|
||
(defun ats-ps-device (device &optional predicate arg)
|
||
"Return a list of running processes on DEVICE.
|
||
Return a list each of whose elements is an alist between the names
|
||
of columns returned by `ps' and their values.
|
||
If PREDICATE is non-nil, accept only those processes for which
|
||
it returns true, with ARG provided as a second argument."
|
||
(with-temp-buffer
|
||
(ats-adb "-s" device "shell" "ps")
|
||
;; Examples:
|
||
;; USER PID PPID VSIZE RSS WCHAN PC NAME
|
||
;; USER PID PPID VSZ RSS WCHAN ADDR S NAME
|
||
(end-of-line)
|
||
(let* ((substr (buffer-substring (point-min) (point)))
|
||
(legend (mapcar #'intern
|
||
(string-split substr "[ \t]"
|
||
t "[[:space:]]")))
|
||
(state-present (memq 'S legend))
|
||
(last (car (last legend)))
|
||
(processes nil)
|
||
process)
|
||
(while (re-search-forward "[[:alnum:]]" nil t)
|
||
(backward-char)
|
||
(setq process nil)
|
||
(dolist (column legend)
|
||
(let ((beg (point)))
|
||
(re-search-forward (if (eq column last)
|
||
"[[:space:]]*$"
|
||
"\\([[:space:]]+\\|$\\)"))
|
||
;; The `S' column is on certain older systems not listed in
|
||
;; the legend but printed anyway before NAME.
|
||
(when (and (not state-present) (eq column 'NAME))
|
||
(save-excursion
|
||
(goto-char beg)
|
||
(save-match-data
|
||
(when (re-search-forward "\\([RSDZTtWXxKWPI]\\) " nil t)
|
||
(setq beg (point))
|
||
(push (cons 'S (match-string 1)) process)))))
|
||
(push (cons column (buffer-substring beg (match-beginning 0)))
|
||
process)))
|
||
(when (or (not predicate) (funcall predicate process arg))
|
||
(push (nreverse process) processes)))
|
||
(nreverse processes))))
|
||
|
||
(defun ats-getprop (device prop)
|
||
"Return the value of the system property PROP on DEVICE.
|
||
Among such properties are:
|
||
|
||
- `ro.build.version.sdk': The version of Android present on
|
||
the device."
|
||
(ats-memoize device (concat "ats-getprop/" prop)
|
||
(with-temp-buffer
|
||
(ats-adb "-s" device "shell" "getprop" prop)
|
||
(goto-char (point-max))
|
||
(when (eq (char-before) ?\n)
|
||
(delete-char -1))
|
||
(buffer-string))))
|
||
|
||
(defun ats-get-sdk-version (device)
|
||
"Return the version of Android installed on DEVICE."
|
||
(or (string-to-number (ats-getprop device "ro.build.version.sdk")) 0))
|
||
|
||
(defconst ats-package-list-regexp
|
||
"^\\([[:alnum:]\\.]+\\) \\([[:digit:]]+\\) \\([[:digit:]]\\).*/.*$"
|
||
"Regexp with which to validate the format of packages.list.")
|
||
|
||
(defun ats-is-package-debuggable (device pkg)
|
||
"Return whether the package identified by PKG is debuggable on DEVICE."
|
||
(ats-memoize device (concat "ats-is-package-debuggable/" pkg)
|
||
(with-temp-buffer
|
||
(if (ignore-errors
|
||
(ats-adb "-s" device "shell" "cat" "/data/system/packages.list")
|
||
(re-search-forward ats-package-list-regexp nil nil))
|
||
;; packages.list is readable. Search for an entry matching
|
||
;; PKG.
|
||
(progn
|
||
(goto-char (point-min))
|
||
(unless (re-search-forward (rx bol
|
||
(literal pkg)
|
||
" "
|
||
;; UID of package.
|
||
(group (+ (or alnum ".")))
|
||
" "
|
||
;; Package debuggability.
|
||
(group (or "0" "1"))
|
||
;; Package home directory.
|
||
(+ nonl)
|
||
"/"
|
||
(+ nonl)
|
||
eol)
|
||
nil t)
|
||
(error "No package on device: %s" pkg))
|
||
(equal (match-string 2) "1"))
|
||
;; If packages.list is unreadable (as when adbd is not executing
|
||
;; as root on recent OS releases), call run-as to establish
|
||
;; whether this package is debuggable.
|
||
(ignore-errors
|
||
(ats-adb "-s" device "shell" "run-as" pkg "echo" "emacs_token"))
|
||
(when (re-search-forward "run-as:" nil t)
|
||
;; Was an error message printed? Does it indicate that the
|
||
;; package is not present?
|
||
(when (re-search-forward "unknown" nil t)
|
||
(error "No package on device: %s" pkg))
|
||
nil)
|
||
(goto-char (point-min))
|
||
(re-search-forward "emacs_token" nil t)))))
|
||
|
||
(defun ats-list-users (device)
|
||
"Return a list of user IDs present on DEVICE.
|
||
Each element of the list produced is a list of the form:
|
||
|
||
(ID NAME EXTERNAL-STORAGE-DIRECTORY)"
|
||
(if (< (ats-get-sdk-version device) 17)
|
||
'((0 "Android user" "/sdcard"))
|
||
(ats-memoize device "ats-list-users"
|
||
(let ((users nil))
|
||
(with-temp-buffer
|
||
(ats-adb "-s" device "shell" "pm" "list" "users")
|
||
(while (re-search-forward
|
||
"^\tUserInfo{\\([[:digit:]]+\\):\\(.*?\\):.*$" nil t)
|
||
(push (list (string-to-number (match-string 1))
|
||
(match-string 2)
|
||
(if (equal (match-string 1) "0")
|
||
(or (ignore-errors
|
||
(ats-verify-directory
|
||
device "/storage/emulated/0"))
|
||
"/sdcard")
|
||
(or (ignore-errors
|
||
(ats-verify-directory
|
||
device
|
||
(format "/mnt/shell/emulated/%s" (match-string 1))))
|
||
(format "/storage/emulated/%s" (match-string 1)))))
|
||
users)))
|
||
(sort users :lessp (lambda (a b)
|
||
(< (car a) (car b)))
|
||
:in-place t)))))
|
||
|
||
(defun ats-get-package-aid (device package)
|
||
"Return the base AID of the provided PACKAGE on DEVICE.
|
||
This value may be treated as-is as the UID of PACKAGE running as
|
||
the default Android user, or provided to `ats-get-package-uid'
|
||
to derive the UID assigned to instances of it that are executing
|
||
as another user."
|
||
(ats-memoize device (concat "ats-get-package-aid/" package)
|
||
(with-temp-buffer
|
||
(ats-adb "-s" device "shell" "dumpsys" "package" package)
|
||
(re-search-forward (rx bol (+ space)
|
||
"Package [" (literal package) "]"
|
||
(+ nonl) ":" eol))
|
||
(re-search-forward "\\(userId\\|appId\\)=\\([[:digit:]]+\\)")
|
||
(string-to-number (match-string 2)))))
|
||
|
||
;; Ref:
|
||
;; https://android.googlesource.com/platform/system/core/+/master/libcutils/include/private/android_filesystem_config.h
|
||
;; https://android.googlesource.com/platform/system/core/+/master/libcutils/multiuser.cpp
|
||
|
||
(defconst ats-aid-user-offset 100000
|
||
"Value of `AID_USER_OFFSET' in `android_filesystem_config.h'.")
|
||
(defconst ats-aid-isolated-start 90000
|
||
"Value of `AID_ISOLATED_START' in `android_filesystem_config.h'.")
|
||
(defconst ats-aid-app-start 10000
|
||
"Value of `AID_APP_START' in `android_filesystem_config.h'.")
|
||
|
||
(defun ats-aid-to-uid (aid user)
|
||
"Derive a UID from an application ID and a user ID.
|
||
Return the UID that will be assigned to instances of that
|
||
application which is identified by AID when executing as the
|
||
Android user USER. AID should be a value returned by
|
||
`ats-get-package-uid', which see."
|
||
(+ (% aid ats-aid-user-offset) (* user ats-aid-user-offset)))
|
||
|
||
;; Ref:
|
||
;; https://android.googlesource.com/platform/bionic/+/master/libc/bionic/grp_pwd.cpp
|
||
|
||
(defun ats-uid-to-username (device uid)
|
||
"Return the name of an application user UID on DEVICE.
|
||
Signal if UID is not a valid application user ID."
|
||
(let ((appid (% uid ats-aid-user-offset))
|
||
(userid (/ uid ats-aid-user-offset)))
|
||
(if (>= (ats-get-sdk-version device) 16)
|
||
;; "New style" IDs with isolated environments.
|
||
(cond
|
||
((>= appid ats-aid-isolated-start)
|
||
(format "u%d_i%d" userid (- appid ats-aid-isolated-start)))
|
||
((>= appid ats-aid-app-start)
|
||
(format "u%d_a%d" userid (- appid ats-aid-app-start)))
|
||
(t
|
||
(error "UID is not representable: %d" uid)))
|
||
(cond
|
||
;; Old style IDs.
|
||
((>= appid ats-aid-app-start)
|
||
(format "app_%d" (- appid ats-aid-app-start)))
|
||
(t
|
||
(error "UID is not representable: %d" uid))))))
|
||
|
||
(defun ats-verify-directory (device dir)
|
||
"Verify whether DIR exists on DEVICE, and signal if not.
|
||
Value is DIR otherwise."
|
||
(with-temp-buffer
|
||
(ignore-errors
|
||
(ats-adb "-s" device "shell" "test" "-d" dir "&&" "echo" "ATS_OK"))
|
||
;; There are Android systems where `test' is neither installed to
|
||
;; /system/bin nor available as a shell builtin. On these systems,
|
||
;; this command prints an error message and exits.
|
||
(prog1 dir
|
||
(if (looking-at ".*\\btest\\b.*$")
|
||
;; Call `mkdir' and test whether it reports that the directory
|
||
;; already exists.
|
||
(progn
|
||
(erase-buffer)
|
||
(ignore-errors
|
||
(ats-adb "-s" device "shell" "mkdir" dir "||" "echo" "ATS_EXISTS"))
|
||
(goto-char (point-max))
|
||
(forward-line -1)
|
||
(unless (and (looking-at "ATS_EXISTS$")
|
||
(progn
|
||
(goto-char (point-min))
|
||
;; Skip any instance of `dir' in the error
|
||
;; message.
|
||
(search-forward dir nil t)
|
||
(looking-at ".*File exists.*")))
|
||
(error "Directory `%s' does not appear to exist" dir)))
|
||
(goto-char (point-max))
|
||
(forward-line -1)
|
||
(unless (looking-at "ATS_OK$")
|
||
(error "Directory `%s' does not exist" dir))))))
|
||
|
||
(defun ats-get-package-data-directory (device package user)
|
||
"Return PACKAGE's data directory on DEVICE.
|
||
Return PACKAGE's data directory when executing as that user
|
||
which is identified by the user ID USER."
|
||
(ats-memoize device (concat "ats-get-package-data-directory/"
|
||
package "/" (number-to-string user))
|
||
(with-temp-buffer
|
||
(ats-adb "-s" device "shell" "dumpsys" "package" package)
|
||
(re-search-forward (rx bol (+ space)
|
||
"Package [" (literal package) "]"
|
||
(+ nonl) ":" eol))
|
||
(if (eq user 0)
|
||
(progn
|
||
(or (save-excursion
|
||
;; Attempt to parse a dataDir= specification under a
|
||
;; User: heading. This line may be absent or not fall
|
||
;; under this heading on older Android systems.
|
||
(when (and (re-search-forward "^[[:space:]]+User 0: " nil t)
|
||
(re-search-forward "dataDir=\\(/.*$\\)" nil t))
|
||
(match-string 1)))
|
||
;; Resort to any dataDir= specification, as this is user
|
||
;; 0.
|
||
(and (re-search-forward "dataDir=\\(/.*$\\)" nil t)
|
||
(match-string 1))
|
||
;; Signal failure.
|
||
(error "Could not extract data directory of package `%s'" package)))
|
||
;; Attempt to extract a dataDir= specification printed under a
|
||
;; User heading.
|
||
(or (save-excursion
|
||
(when (and (re-search-forward (format "^[[:space:]]+User %d: "
|
||
user)
|
||
nil t)
|
||
(re-search-forward "dataDir=\\(/.*$\\)" nil t))
|
||
(match-string 1)))
|
||
;; If this fails (as on Android systems where "dumpsys
|
||
;; package" has not yet been revised to print user-specific
|
||
;; data directories), return "/data/user/%d/%s", but verify
|
||
;; that it exists.
|
||
(ats-verify-directory device (format "/data/user/%d/%s"
|
||
user package)))))))
|
||
|
||
(defun ats-get-user-external-storage-directory (device user)
|
||
"Return the external storage directory visible to USER on DEVICE."
|
||
(caddr (assq user (ats-list-users device))))
|
||
|
||
(defvar ats-transfer-padding (make-string 300 ?\n)
|
||
"Padding delivered before attempting to transfer shell scripts.")
|
||
|
||
(defun ats-exec-script (device script &optional package user)
|
||
"Execute SCRIPT on DEVICE and return its exit code.
|
||
Insert its output into the current buffer in the manner of
|
||
`ats-adb'. If PACKAGE and USER are specified, run this script
|
||
as PACKAGE, provided that it is debuggable."
|
||
(save-restriction
|
||
(narrow-to-region (point) (point))
|
||
(let* ((name (format "%s.sh" (make-temp-name "ats-")))
|
||
(fullname (concat (file-name-as-directory
|
||
temporary-file-directory)
|
||
name)))
|
||
(with-temp-buffer
|
||
(insert script)
|
||
(write-region (point-min) (point-max) fullname))
|
||
(unwind-protect
|
||
(let ((targetname (format "/data/local/tmp/%s" name)))
|
||
(with-temp-buffer
|
||
(ats-adb "-s" device "push" fullname targetname))
|
||
(if (not package)
|
||
(progn
|
||
(ats-adb "-s" device "shell" "sh" "-c"
|
||
(shell-quote-argument
|
||
(let ((arg (shell-quote-argument targetname t)))
|
||
(format
|
||
"sh %s; echo ats_exit: $?; (rm %s &> /dev/null)"
|
||
arg arg))
|
||
t)))
|
||
;; targetname names a script that will reconstruct SCRIPT
|
||
;; in the `files' subdirectory of the current working
|
||
;; directory.
|
||
;;
|
||
;; It is not possible reliably to transfer data through
|
||
;; `adb shell', as the device may allocate a
|
||
;; pseudoterminal, which imposes restrictions on both line
|
||
;; length and transfer size, and to compensate, this
|
||
;; script is first transferred to /data/local/tmp, and
|
||
;; piped into run-as on-device in a single concise
|
||
;; command.
|
||
(unless (ats-is-package-debuggable device package)
|
||
(error "Package is not debuggable: `%s'" package))
|
||
(let* ((pkgname (format "files/%s" name))
|
||
(src (shell-quote-argument targetname t))
|
||
(arg (shell-quote-argument pkgname t))
|
||
(version (ats-get-sdk-version device)))
|
||
(if (eq user 0)
|
||
(progn
|
||
(ats-adb
|
||
"-s" device "shell" "sh"
|
||
"-c" (shell-quote-argument
|
||
(format "run-as %s sh -c %s < %s"
|
||
package
|
||
(shell-quote-argument
|
||
(format "cat > %s" arg) t)
|
||
src)
|
||
t))
|
||
(ats-adb
|
||
"-s" device "shell" "run-as" package "sh"
|
||
"-c"
|
||
(shell-quote-argument
|
||
(format
|
||
"sh %s; echo ats_exit: $?; (rm %s &> /dev/null)"
|
||
arg arg)
|
||
t)))
|
||
(if (< version 23)
|
||
(error (concat "Cannot execute script as package and"
|
||
"non-default user on Android <= 5.1."))
|
||
(progn
|
||
(ats-adb
|
||
"-s" device "shell" "sh"
|
||
"-c" (shell-quote-argument
|
||
(format "run-as %s --user %d sh -c %s < %s"
|
||
package
|
||
user
|
||
(shell-quote-argument
|
||
(format "cat > %s" arg) t)
|
||
src)
|
||
t))
|
||
(ats-adb
|
||
"-s" device "shell" "run-as" package
|
||
"--user" (number-to-string user)
|
||
"sh" "-c"
|
||
(shell-quote-argument
|
||
(format
|
||
"sh %s; echo ats_exit: $?; (rm %s &> /dev/null)"
|
||
arg arg)
|
||
t))))))))
|
||
(with-demoted-errors "Deleting temporary script: %S"
|
||
(delete-file fullname))))
|
||
(goto-char (point-max))
|
||
(re-search-backward "ats_exit: \\([[:digit:]]+\\)$")
|
||
(prog1 (string-to-number (match-string 1))
|
||
(delete-region (point) (point-max)))))
|
||
|
||
(defsubst ats-exec-script-checked (device script &optional package user)
|
||
"Execute SCRIPT on DEVICE as PACKAGE and USER, as with `ats-exec-script'.
|
||
But signal an error if its exit code is non-zero."
|
||
(let ((rc (ats-exec-script device script package user)))
|
||
(when (/= rc 0)
|
||
(error "Script exited with return code %d:\n%s" rc script))))
|
||
|
||
(defun ats-use-private-staging-directory (device package user)
|
||
"Return whether PACKAGE running as USER admits of a private staging directory.
|
||
DEVICE is the device to which the test pertains.
|
||
|
||
A private staging directory is a staging directory within
|
||
PACKAGE's application data directory, enabling packages to be
|
||
debugged without holding external storage permissions."
|
||
(and (or (eq user 0)
|
||
;; `run-as --user' requires Android 6.0 or better.
|
||
(>= (ats-get-sdk-version device) 23))
|
||
(ats-is-package-debuggable device package)))
|
||
|
||
(defun ats-get-staging-directory (device package user)
|
||
"Create and return a staging directory for communication with PACKAGE.
|
||
Create and return a directory which is accessible both to this
|
||
instance of Emacs and to PACKAGE executing on DEVICE as USER."
|
||
;; Prefer invoking `run-as' to transfer files into a local directory.
|
||
(ats-memoize device (concat "ats-get-staging-directory/"
|
||
package "/" (number-to-string user))
|
||
(if (ats-use-private-staging-directory device package user)
|
||
(progn
|
||
(with-temp-buffer
|
||
;; The return value of mkdir is not tested and neither is
|
||
;; any attempt made to supply such flags as `-p', as no
|
||
;; flags to `mkdir' can be relied upon on Android.
|
||
(ats-exec-script device "mkdir files/ats-staging"
|
||
package user)
|
||
(erase-buffer)
|
||
(ats-exec-script-checked device "cd files/ats-staging && pwd"
|
||
package user)
|
||
(when (eq (char-before) ?\n)
|
||
(delete-char -1))
|
||
(buffer-string)))
|
||
;; Locate the external storage directory visible to USER.
|
||
(let* ((external-storage (ats-get-user-external-storage-directory
|
||
device user))
|
||
(subdirectory (format "%s/ats-staging" external-storage)))
|
||
(with-temp-buffer
|
||
(ats-exec-script device (format "mkdir %s" subdirectory))
|
||
(erase-buffer)
|
||
(ats-exec-script-checked device
|
||
(format "cd %s && pwd"
|
||
(shell-quote-argument
|
||
subdirectory t)))
|
||
(when (eq (char-before) ?\n)
|
||
(delete-char -1))
|
||
(buffer-string))))))
|
||
|
||
(defun ats-base64-available (device)
|
||
"Return whether a `base64' binary is available on DEVICE."
|
||
(ats-memoize device "ats-base64-available"
|
||
(with-temp-buffer
|
||
(ats-exec-script
|
||
device
|
||
(format "export TMPDIR=/data/local/tmp\n
|
||
base64 -d <<'_ATS_BASE64_EOF'\n%s\n_ATS_BASE64_EOF"
|
||
(base64-encode-string "Emacs_Hello")))
|
||
(equal (buffer-string) "Emacs_Hello"))))
|
||
|
||
(defun ats-echo-n-e (device)
|
||
"Return whether `echo -n -e' is understood by DEVICE."
|
||
(ats-memoize device "ats-proper-echo-flags"
|
||
(with-temp-buffer
|
||
;; The Almquist shell distributed with old Android releases treats
|
||
;; flags subsequent to the first as additional strings to be
|
||
;; printed.
|
||
(ats-exec-script device "echo -n -e '\\077'")
|
||
(equal (buffer-string) "?"))))
|
||
|
||
(defun ats-echo-c (device)
|
||
"Return whether \"echo -e '...\\c'\" is understood by DEVICE."
|
||
(ats-memoize device "ats-almquist-echo-flags"
|
||
(with-temp-buffer
|
||
(ats-exec-script device "echo -e '\\077\\c'")
|
||
(equal (buffer-string) "?"))))
|
||
|
||
(defvar ats-octab (make-vector 256 0)
|
||
"Vector of numbers between 0 and 255 and their octal representations.")
|
||
(dotimes (c 256)
|
||
(aset ats-octab c (format "\\0%o" c)))
|
||
|
||
(defun ats-upload-encode-binary (device file quoted)
|
||
"Generate an script that will echo the contents of FILE into QUOTED.
|
||
QUOTED must have been processed by `shell-quote-argument'.
|
||
The script will be suitable for execution on DEVICE."
|
||
;; We would prefer to use uuencode rather than echo, but it appears
|
||
;; even scarcer than base64.
|
||
(cond ((ats-base64-available device)
|
||
(with-temp-buffer
|
||
(set-buffer-multibyte nil)
|
||
(let ((coding-system-for-read 'no-conversion))
|
||
(insert-file-contents file))
|
||
(let ((encoded (base64-encode-string (buffer-string) nil)))
|
||
(erase-buffer)
|
||
(insert encoded)
|
||
(goto-char (point-min))
|
||
(insert "export TMPDIR=`pwd`\n"
|
||
"base64 -d <<_ATS_UPLOAD_EOF >"
|
||
quoted "\n")
|
||
(goto-char (point-max))
|
||
(insert "\n_ATS_UPLOAD_EOF\n"))
|
||
(buffer-string)))
|
||
((or (ats-echo-n-e device)
|
||
(ats-echo-c device))
|
||
(let* ((is-echo-c (not (ats-echo-n-e device)))
|
||
(echo-prefix (if is-echo-c "echo -e '" "echo -n -e '"))
|
||
(echo-suffix (if is-echo-c "\\c'\n" "'\n"))
|
||
(ats-upload-script
|
||
(shell-quote-argument
|
||
(concat (make-temp-name "ats-upload-") ".sh") t)))
|
||
(with-temp-buffer
|
||
(set-buffer-multibyte nil)
|
||
(let ((coding-system-for-read 'no-conversion))
|
||
(insert-file-contents file))
|
||
(with-output-to-string
|
||
(princ "export TMPDIR=`pwd`; cat <<_ATS_UPLOAD_EOF >")
|
||
(princ ats-upload-script)
|
||
(terpri)
|
||
(let ((point (point))
|
||
(point-max (point-max)))
|
||
(while (< point point-max)
|
||
(princ echo-prefix)
|
||
(let ((i (min 128 (- point-max point))))
|
||
(dotimes (idx i)
|
||
(princ (aref ats-octab (char-after (+ point idx)))))
|
||
(setq point (goto-char (+ point i))))
|
||
(princ echo-suffix)))
|
||
(princ "_ATS_UPLOAD_EOF\nsh ")
|
||
(princ ats-upload-script)
|
||
(princ (concat " > " quoted " && rm " ats-upload-script))))))
|
||
(t (error "Cannot decide by what means to encode a binary file"))))
|
||
|
||
(defun ats-upload (device file package user)
|
||
"Upload FILE to PACKAGE's staging directory on DEVICE.
|
||
Value is the file name on the device. USER is the numerical ID
|
||
of the Android user as which PACKAGE will execute."
|
||
(setq file (expand-file-name file))
|
||
(let ((staging-dir (ats-get-staging-directory device package user)))
|
||
(if (ats-use-private-staging-directory device package user)
|
||
;; Upload by way of `run-as'.
|
||
(let ((dst-file (concat staging-dir "/"
|
||
(file-name-nondirectory file))))
|
||
(with-temp-buffer
|
||
(ats-exec-script-checked
|
||
device
|
||
(let ((quoted (shell-quote-argument dst-file t)))
|
||
(ats-upload-encode-binary device file quoted))
|
||
package user))
|
||
dst-file)
|
||
(let ((dest-file-name
|
||
(concat staging-dir "/" (file-name-nondirectory file))))
|
||
(with-temp-buffer
|
||
(ats-adb "-s" device "push" file dest-file-name))
|
||
dest-file-name))))
|
||
|
||
(defun ats-download (device file package user)
|
||
"Download FILE from PACKAGE's staging directory on DEVICE.
|
||
FILE's contents should be UTF-8 text with Unix line endings.
|
||
Insert its contents at point in the current buffer. PACKAGE and
|
||
USER are as in `ats-upload'."
|
||
(let* ((dir-private-p
|
||
(ats-use-private-staging-directory device package user))
|
||
(exec-package (and dir-private-p package))
|
||
(exec-user (and dir-private-p user)))
|
||
(insert (with-temp-buffer
|
||
;; It is not reliable to cat binary data through adb, nor
|
||
;; possible to copy binary data as a package user to a
|
||
;; location where the `adb shell' user may access it, or
|
||
;; to transfer binary data over a `run-as' connection...
|
||
(ats-exec-script-checked device
|
||
(format "cat %s/%s"
|
||
(shell-quote-argument
|
||
(ats-get-staging-directory
|
||
device package user)
|
||
t)
|
||
(shell-quote-argument file t))
|
||
exec-package exec-user)
|
||
(buffer-string)))))
|
||
|
||
(defun ats-create-empty-temporary (device name package user)
|
||
"Create an empty temporary file NAME in PACKAGE's staging directory.
|
||
DEVICE is the device where this temporary file is to be created.
|
||
USER is the user as which PACKAGE is expected to execute, and
|
||
value is the name of the said file."
|
||
(let* ((staging-dir (ats-get-staging-directory device package user))
|
||
(name (concat staging-dir "/" name)))
|
||
(unless (ats-use-private-staging-directory device package user)
|
||
(setq package nil user nil))
|
||
(with-temp-buffer
|
||
(ats-exec-script-checked device
|
||
(format "cat </dev/null >%s"
|
||
(shell-quote-argument name t))
|
||
package user))
|
||
name))
|
||
|
||
(defun ats-run-jar (device jar class &rest params)
|
||
"Upload and execute the Dalvik archive JAR on DEVICE.
|
||
CLASS must be the name of the archive file's main class. Value
|
||
is the exit code of the `app_process' process, and its output is
|
||
inserted in the manner of `ats-exec-script'."
|
||
(let* ((jar (expand-file-name jar))
|
||
(name (file-name-nondirectory jar))
|
||
(tempname (concat "/data/local/tmp/" name)))
|
||
(with-temp-buffer
|
||
(ats-adb "-s" device "push" jar tempname))
|
||
(ats-exec-script device (concat
|
||
"export ANDROID_DATA=/data/local/tmp;\n"
|
||
;; `dalvik-cache' must be a writable
|
||
;; directory in which dalvikvm is
|
||
;; able to store optimized dex code.
|
||
"mkdir /data/local/tmp/dalvik-cache"
|
||
" &> /dev/null\n"
|
||
"app_process -Djava.class.path="
|
||
(shell-quote-argument tempname t)
|
||
" /data/local/tmp "
|
||
(shell-quote-argument class t)
|
||
" "
|
||
(mapconcat (lambda (arg)
|
||
(shell-quote-argument arg t))
|
||
params " ")))))
|
||
|
||
(defun ats-supports-am-force-stop (device)
|
||
"Return whether DEVICE supports the command `am force-stop'."
|
||
(ats-memoize device "ats-supports-am-force-stop"
|
||
(with-temp-buffer
|
||
(ignore-errors
|
||
(ats-adb "-s" device "shell" "am"))
|
||
(not (null (re-search-forward "\\bforce-stop\\b" nil t))))))
|
||
|
||
(defun ats-supports-am-force-stop-user (device)
|
||
"Return whether DEVICE supports the command `am force-stop --user'."
|
||
(ats-memoize device "ats-supports-am-force-stop-user"
|
||
(with-temp-buffer
|
||
(ignore-errors
|
||
(ats-adb "-s" device "shell" "am"))
|
||
(not (null (re-search-forward
|
||
"^.*\\bforce-stop\\b[^[:alnum:]]+--user.*$"
|
||
nil t))))))
|
||
|
||
(defun ats-kill-process-by-username-and-name (device username name
|
||
&optional pkgname user)
|
||
"Kill any process with NAME running with the username USERNAME.
|
||
If PKGNAME is a debuggable package, do so as that package's user
|
||
and as the Android user USER. DEVICE is the device on which to
|
||
operate."
|
||
(let ((any-killed nil))
|
||
(with-temp-buffer
|
||
(dolist (proc (ats-ps-device device
|
||
(lambda (item _)
|
||
(and (equal (cdr (assq 'NAME item))
|
||
name)
|
||
(equal (cdr (assq 'USER item))
|
||
username)))))
|
||
(let* ((debuggable (and pkgname
|
||
(ats-is-package-debuggable device pkgname)))
|
||
(run-as (and debuggable pkgname))
|
||
(user (and debuggable user))
|
||
(rc (ats-exec-script device (format "kill -9 %s"
|
||
(cdr (assq 'PID proc)))
|
||
run-as user)))
|
||
(unless (eq rc 0)
|
||
(error "Could not terminate an existing instance of `%s' (PID %s).
|
||
Please attempt to terminate this package by hand (as from the
|
||
App Info Settings page) before invoking this command"
|
||
name (assq 'PID proc)))
|
||
(setq any-killed t))))
|
||
any-killed))
|
||
|
||
(defconst ats-portforward-local-type-regexp
|
||
(concat "\\(tcp\\|localabstract\\|localreserved\\|localfilesystem"
|
||
"\\|dev\\)")
|
||
"Regexp matching valid ADB port forwarding types.")
|
||
|
||
(defconst ats-portforward-remote-type-regexp
|
||
(concat "\\(tcp\\|localabstract\\|localreserved\\|localfilesystem"
|
||
"\\|dev\\|jdwp\\|vsock\\|acceptfd\\)")
|
||
"Regexp matching valid ADB port forwarding types.")
|
||
|
||
(defconst ats-portforward-list-regexp (concat
|
||
"^"
|
||
;; Type & whitespace.
|
||
"\\(.*\\)[[:space:]]+"
|
||
;; Local port type and name.
|
||
ats-portforward-local-type-regexp ":"
|
||
"\\(.*\\)[[:space:]]"
|
||
;; Local port type and name.
|
||
ats-portforward-remote-type-regexp ":"
|
||
"\\(.*\\)$")
|
||
"Regexp with which to parse port forwarding lists printed by ADB.")
|
||
|
||
(defconst ats-portreverse-type-regexp
|
||
"\\(tcp\\|localabstract\\|localreserved\\|localfilesystem\\)"
|
||
"Regexp matching valid ADB port forwarding types.")
|
||
|
||
(defconst ats-portreverse-list-regexp (concat
|
||
"^"
|
||
;; Type & whitespace.
|
||
"\\(.*\\)[[:space:]]+"
|
||
;; Remote port type and name.
|
||
ats-portreverse-type-regexp ":"
|
||
"\\(.*\\)[[:space:]]"
|
||
;; Local port type and name.
|
||
ats-portreverse-type-regexp ":"
|
||
"\\(.*\\)$")
|
||
"Regexp with which to parse port forwarding lists printed by ADB.")
|
||
|
||
(defun ats-reverse-list (device)
|
||
"List connections being reverse-proxied from DEVICE.
|
||
Value is a list each of whose elements partakes of the form:
|
||
|
||
(TYPE REMOTE-PROTO REMOTE-PORT LOCAL-PROTO LOCAL-PORT)"
|
||
(let ((regexp ats-portreverse-list-regexp)
|
||
(connections nil))
|
||
(with-temp-buffer
|
||
(ats-adb "-s" device "reverse" "--list")
|
||
(while (re-search-forward regexp nil t)
|
||
(push (list (match-string 1) (match-string 2)
|
||
(match-string 3) (match-string 4)
|
||
(match-string 5))
|
||
connections)))
|
||
(nreverse connections)))
|
||
|
||
(defun ats-reverse-tcp (device local port)
|
||
"Proxy to the local TCP port LOCAL from PORT on DEVICE.
|
||
If PORT is 0, select a suitable free port on DEVICE or that of
|
||
an existing forwarding session. Return PORT or the selected
|
||
port as the case may be.
|
||
|
||
This is not supported by all versions of Android."
|
||
(when (and (eq port 0) (< (ats-get-sdk-version device) 26))
|
||
(error "Automatic port selection is unavailable < Android 8.0"))
|
||
(or (let ((str (number-to-string local))
|
||
(port-str (number-to-string port))
|
||
(value nil))
|
||
;; Is the local port already being forwarded to PORT (or any
|
||
;; port if that be zero)?
|
||
(dolist (conn (ats-reverse-list device) value)
|
||
(when (and (equal (nth 3 conn) "tcp")
|
||
(equal (nth 4 conn) str)
|
||
(equal (nth 1 conn) "tcp")
|
||
(or (eq port 0)
|
||
(equal (nth 2 conn) port-str)))
|
||
(setq value (string-to-number (nth 2 conn))))))
|
||
(with-temp-buffer
|
||
(ats-adb "-s" device "reverse" (format "tcp:%d" port)
|
||
(format "tcp:%d" local))
|
||
(let ((num (string-to-number (buffer-string))))
|
||
(if (zerop num)
|
||
(if (and (not (eq port 0)) (eq (point-min) (point-max)))
|
||
port
|
||
(error "Failed to establish reverse proxy \
|
||
to `localhost:%d' from `tcp:%d':\n%s" local port (buffer-string)))
|
||
num)))))
|
||
|
||
(defun ats-forward-list (device)
|
||
"List connections being proxied to DEVICE.
|
||
Value is a list each of whose elements partakes of the form:
|
||
|
||
(DEVICE LOCAL-PROTO LOCAL-PORT REMOTE-PROTO REMOTE-PORT)
|
||
|
||
DEVICE is only returned in the interests of consistency with
|
||
`ats-reverse-list'."
|
||
(let ((regexp ats-portforward-list-regexp)
|
||
(connections nil))
|
||
(with-temp-buffer
|
||
(ats-adb "forward" "--list")
|
||
(while (re-search-forward regexp nil t)
|
||
(when (equal (match-string 1) device)
|
||
(push (list (match-string 1) (match-string 2)
|
||
(match-string 3) (match-string 4)
|
||
(match-string 5))
|
||
connections))))
|
||
(nreverse connections)))
|
||
|
||
(defun ats-forward-tcp (device port local)
|
||
"Proxy to the remote TCP port PORT on DEVICE from LOCAL.
|
||
If LOCAL is 0, select a suitable local free port or that of an
|
||
existing forwarding session. Return LOCAL or the selected port
|
||
as the case may be."
|
||
(or (let ((str (number-to-string port))
|
||
(local-str (number-to-string local))
|
||
(value nil))
|
||
;; Is the local port already being forwarded?
|
||
(dolist (conn (ats-forward-list device) value)
|
||
(when (and (equal (nth 3 conn) "tcp")
|
||
(equal (nth 4 conn) str)
|
||
(equal (nth 1 conn) "tcp")
|
||
(or (eq local 0)
|
||
(equal (nth 2 conn) local-str)))
|
||
(setq value (string-to-number (nth 2 conn))))))
|
||
(with-temp-buffer
|
||
(ats-adb "-s" device "forward" (format "tcp:%d" local)
|
||
(format "tcp:%d" port))
|
||
(let ((num (string-to-number (buffer-string))))
|
||
(if (zerop num)
|
||
(if (and (not (eq local 0)) (eq (point-min) (point-max)))
|
||
local
|
||
(error "Failed to establish proxy \
|
||
from `localhost:%d' to `tcp:%d':\n%s" local port (buffer-string)))
|
||
num)))))
|
||
|
||
(defun ats-is-tail-available (device)
|
||
"Return whether `tail is available on DEVICE and functional."
|
||
(ats-memoize device "ats-is-tail-available"
|
||
(with-temp-buffer
|
||
(when (eq (ats-exec-script device "tail < /dev/null\n") 0)
|
||
(erase-buffer)
|
||
;; Now run `tail --help' and search for any lines indicating
|
||
;; that `tail -f' is unimplemented, e.g.:
|
||
;; usage: tail [-n|c NUMBER] [-f] [FILE...]
|
||
|
||
;; Copy last lines from files to stdout. If no files listed, copy from
|
||
;; stdin. Filename "-" is a synonym for stdin.
|
||
|
||
;; -n output the last NUMBER lines (default 10), +X counts from start.
|
||
;; -c output the last NUMBER bytes, +NUMBER counts from start
|
||
;; #-f follow FILE(s), waiting for more data to be appended [TODO]
|
||
;;
|
||
;; This may fail if tail does not implement `--help'.
|
||
(ignore-errors
|
||
(ats-adb "-s" device "shell" "tail" "--help"))
|
||
(not (re-search-forward "^#?-f.*follow.+TODO.*$" nil t))))))
|
||
|
||
|
||
|
||
;; Component management.
|
||
|
||
(defconst ats-java-int-min (- (expt 2 31))
|
||
"Value of `Integer.MIN_VALUE' in Java.")
|
||
|
||
(defconst ats-java-int-max (1- (expt 2 31))
|
||
"Value of `Integer.MAX_VALUE' in Java.")
|
||
|
||
(defconst ats-java-long-min (- (expt 2 63))
|
||
"Value of `Long.MIN_VALUE' in Java.")
|
||
|
||
(defconst ats-java-long-max (1- (expt 2 63))
|
||
"Value of `Long.MAX_VALUE' in Java.")
|
||
|
||
(defun ats-intent-array-type (element)
|
||
"Return the type of an Intent array from its first element ELEMENT."
|
||
(cond ((stringp element) "--esa")
|
||
((integerp element) "--eia")
|
||
((and (consp element) (eq (car element) 'long)) "--ela")
|
||
((floatp element) "--efa")
|
||
(t (error "Invalid Intent array element: %s" element))))
|
||
|
||
(defun ats-fmt-array-element (atype element)
|
||
"Format an array ELEMENT appropriately for an array of type ATYPE."
|
||
(cond ((equal atype "--esa")
|
||
(if (stringp element)
|
||
(replace-regexp-in-string "," "\\\\," element)
|
||
(error "Array elements are not uniform of type")))
|
||
((equal atype "--eia")
|
||
(if (integerp element)
|
||
(progn
|
||
(if (or (< element ats-java-int-min)
|
||
(> element ats-java-int-max))
|
||
(error "Integer not representable by Java `int': %d"
|
||
element)
|
||
(format "%d" element)))
|
||
(error "Array elements are not uniform of type")))
|
||
((equal atype "--ela")
|
||
(if (and (consp element) (eq (car element) 'long))
|
||
(let ((element (cdr element)))
|
||
(if (or (< element ats-java-long-min)
|
||
(> element ats-java-long-max))
|
||
(error "Integer not representable by Java `long': %d"
|
||
element)
|
||
(format "%d" element)))
|
||
(error "Array elements are not uniform of type")))
|
||
((equal atype "--efa")
|
||
(if (floatp element)
|
||
(format "%f" element)
|
||
(error "Array elements are not uniform of type")))))
|
||
|
||
(defun ats-build-intent (data)
|
||
"Construct an intent arg list from an alist DATA.
|
||
DATA's keys must either be one of the annexed keywords, or a
|
||
string property name. The value of each element with a string
|
||
key must be:
|
||
|
||
- A string.
|
||
- A cons of the form `(uri . URI)', where URI is an Android URI.
|
||
- A fixnum or bignum, which is treated as an integer and
|
||
mustn't exceed the limits of Java's `int' type's
|
||
representation.
|
||
- A cons of the form `(long . LONG)', where LONG is a fixnum
|
||
or a bignum.
|
||
- A float.
|
||
- A boolean t or nil.
|
||
- A list of any single type of item listed above, excluding
|
||
`(uri . URI)' and booleans.
|
||
|
||
That which follows is a list of keywords that may appear as keys
|
||
juxtaposed with the meaning of their values.
|
||
|
||
:action ACTION
|
||
The action taken by this intent, e.g. `android.intent.action.VIEW'.
|
||
|
||
:data URI
|
||
URI data to be attached to this intent.
|
||
|
||
:type TYPE
|
||
The MIME type of this intent's data.
|
||
|
||
:category CATEGORY
|
||
This intent's category, e.g. `android.intent.category.DEFAULT'.
|
||
|
||
:component COMPONENT
|
||
This intent's target component, e.g. `org.gnu.emacs/.EmacsActivity'.
|
||
|
||
:flags FLAGS
|
||
A fixnum or bignum specifying integer flags affecting the intent.
|
||
|
||
Value is a list of command line arguments fit to be provided to
|
||
`am' commands, or to `AtsStub.class'."
|
||
(let ((directives nil))
|
||
(dolist (element data)
|
||
(let ((key (car element)))
|
||
(cond
|
||
((eq key :action)
|
||
(push "-a" directives)
|
||
(push (cdr element) directives))
|
||
((eq key :data)
|
||
(push "-d" directives)
|
||
(push (cdr element) directives))
|
||
((eq key :type)
|
||
(push "-t" directives)
|
||
(push (cdr element) directives))
|
||
((eq key :category)
|
||
(push "-c" directives)
|
||
(push (cdr element) directives))
|
||
((eq key :component)
|
||
(push "-n" directives)
|
||
(push (cdr element) directives))
|
||
((eq key :flags)
|
||
(push "-f" directives)
|
||
(push (format "%d" (cdr element)) directives))
|
||
((stringp key)
|
||
(let ((value (cdr element)))
|
||
(cond ((stringp value)
|
||
(push "-e" directives)
|
||
(push key directives)
|
||
(push value directives))
|
||
((and (consp value) (eq (car value) 'uri))
|
||
(push "--eu" directives)
|
||
(push key directives)
|
||
(push (cdr value) directives))
|
||
((integerp value)
|
||
(when (or (< value ats-java-int-min)
|
||
(> value ats-java-int-max))
|
||
(error "Integer not representable by Java `int': %d"
|
||
value))
|
||
(push "--ei" directives)
|
||
(push key directives)
|
||
(push (format "%d" value) directives))
|
||
((and (consp value) (eq (car value) 'long))
|
||
(when (or (< (cdr value) ats-java-long-min)
|
||
(> (cdr value) ats-java-long-max))
|
||
(error "Integer not representable by Java `long': %d"
|
||
(cdr value)))
|
||
(push "--el" directives)
|
||
(push key directives)
|
||
(push (format "%d" (cdr value)) directives))
|
||
((floatp value)
|
||
(push "--ef" directives)
|
||
(push key directives)
|
||
(push (format "%f" value) directives))
|
||
((or (eq value t) (null value))
|
||
(push "--ez" directives)
|
||
(push key directives)
|
||
(push (or (and value "true") "false") directives))
|
||
((listp value)
|
||
(let ((atype (ats-intent-array-type (car value))))
|
||
(push atype directives)
|
||
(push key directives)
|
||
(push (mapconcat (lambda (element)
|
||
(ats-fmt-array-element atype element))
|
||
value ",")
|
||
directives)))
|
||
(t (error "Invalid property value: %s" value)))))
|
||
(t (error "Invalid key: %s" key)))))
|
||
(nreverse directives)))
|
||
|
||
(defvar ats-working-stub-file nil
|
||
"Name of a functioning AtsStub Java archive.")
|
||
|
||
(defvar ats-file-directory)
|
||
(defun ats-am-start-intent (device user data)
|
||
"Start an activity identified by the Intent DATA on DEVICE.
|
||
DATA should be provided in such a format as `ats-build-intent'
|
||
accepts.
|
||
USER should identify the Android user for whom DATA will be
|
||
started."
|
||
(let ((args (ats-build-intent data)))
|
||
(when (not (eq user 0))
|
||
(push (number-to-string user) args)
|
||
(push "--user" args))
|
||
;; If the device is running Android 5.0 or later, whose `am' command
|
||
;; supports array parameter construction, simply invoke `am start'.
|
||
(if (>= (ats-get-sdk-version device) 21)
|
||
(with-temp-buffer
|
||
(ignore-errors
|
||
(let ((ats-adb-disable-stderr nil))
|
||
(ats-adb "-s" device "shell" "sh" "-c"
|
||
(shell-quote-argument
|
||
(format "am start %s && echo ats_success"
|
||
(mapconcat (lambda (arg)
|
||
(shell-quote-argument arg t))
|
||
args " "))
|
||
t))))
|
||
(goto-char (point-max))
|
||
(unless (re-search-backward "^ats_success$" nil t)
|
||
(error "`am start' failed with the following output:\n%s"
|
||
(buffer-string))))
|
||
;; Otherwise, invoke a short Java stub class that invokes the
|
||
;; ActivityManager.
|
||
(let ((stub-file (or ats-working-stub-file
|
||
(expand-file-name
|
||
(read-file-name "stub.zip file: "
|
||
(concat
|
||
(file-name-as-directory
|
||
ats-file-directory)
|
||
(file-name-as-directory "bin"))
|
||
"stub.zip" t nil
|
||
(lambda (filename)
|
||
(member
|
||
(file-name-extension filename)
|
||
'("zip" "jar" "dex"))))))))
|
||
(unless (file-regular-p stub-file)
|
||
(error "Invalid or nonexistent ActivityManager stub: %s"
|
||
stub-file))
|
||
(with-temp-buffer
|
||
(unless (zerop (apply #'ats-run-jar device
|
||
stub-file "ats.AtsStub"
|
||
"start" args))
|
||
(error "ActivityManager stub failed with the following output:\n%s"
|
||
(buffer-string))))
|
||
;; Save the stub file upon success.
|
||
(setq ats-working-stub-file stub-file))))
|
||
nil)
|
||
|
||
(defun ats-create-commfile (device package user)
|
||
"Create a file to which a remote program may write data.
|
||
DEVICE, PACKAGE, and USER, identify the device and environment
|
||
from which the file must be available, in the same sense as in
|
||
`ats-get-staging-directory'.
|
||
|
||
The data written to the file must be exceedingly minuscule (just
|
||
adequate to enable a connection to be established between
|
||
controller and driver), and such a file ought to be provided to
|
||
`ats-watch-commfile', which see."
|
||
(let ((tempname (make-temp-name "ats-commfile-")))
|
||
(ats-create-empty-temporary device tempname package user)))
|
||
|
||
(defun ats-watch-commfile (device commfile package user)
|
||
"Poll the contents of COMMFILE as PACKAGE and as USER.
|
||
Return the contents of the first line written to the file and
|
||
delete the same once a newline is written.
|
||
DEVICE is the device where COMMFILE resides."
|
||
(unless (ats-use-private-staging-directory device package user)
|
||
(setq package nil user nil))
|
||
(prog1
|
||
(cond ((and (ats-is-tail-available device)
|
||
;; `tail -f' is defective on Android <= 8.1.
|
||
(> (ats-get-sdk-version device) 28))
|
||
;; Excellent, tail -f exists. Collect process output into a
|
||
;; buffer till the first newline is received.
|
||
(let* ((command-line (cond
|
||
((eq user 0)
|
||
(list "-s" device "shell"
|
||
"run-as" package
|
||
"tail" "-f" "-c1300" commfile))
|
||
(user
|
||
(list "-s" device "shell"
|
||
"run-as" package
|
||
"--user" (number-to-string user)
|
||
"tail" "-f" "-c1300" commfile))
|
||
(t (list "-s" device "shell"
|
||
"tail" "-f" "-c1300" commfile))))
|
||
(process (apply #'ats-start-adb command-line))
|
||
(time (float-time))
|
||
(data nil))
|
||
(set-process-query-on-exit-flag process nil)
|
||
(with-current-buffer (process-buffer process)
|
||
(unwind-protect
|
||
(while (not data)
|
||
(when (accept-process-output process 1 nil)
|
||
(when (search-forward "\n" nil t)
|
||
(setq data (buffer-substring (point-min)
|
||
(1- (point))))))
|
||
(when (not (eq (process-status process) 'run))
|
||
(error "`adb' died unexpectedly..."))
|
||
(message
|
||
"Waiting for response from remote process... (%d s)"
|
||
(floor (- (float-time) time))))
|
||
(kill-buffer)))
|
||
data))
|
||
(t ;; Periodic polling must be resorted to instead.
|
||
(let ((value nil)
|
||
(command-line (cond
|
||
((eq user 0)
|
||
(list "-s" device "shell"
|
||
"run-as" package
|
||
"cat" commfile))
|
||
(user
|
||
(list "-s" device "shell"
|
||
"run-as" package
|
||
"--user" (number-to-string user)
|
||
"cat" commfile))
|
||
(t (list "-s" device "shell"
|
||
"cat" commfile))))
|
||
(time (float-time)))
|
||
;; I would rather have exercised sticky broadcasts, but
|
||
;; it's impossible to post them from Emacs Lisp on the
|
||
;; driver's side...
|
||
(with-temp-buffer
|
||
(while (not value)
|
||
(sleep-for 1.0)
|
||
(message
|
||
"Waiting for response from remote process... (%d s)"
|
||
(floor (- (float-time) time)))
|
||
(erase-buffer)
|
||
;; XXX: how ought errors reliably be separated from
|
||
;; this command's ordinary output?
|
||
(apply #'ats-adb command-line)
|
||
(when (search-forward "\n" nil t)
|
||
(setq value (buffer-substring (point-min)
|
||
(1- (point))))))
|
||
value))))
|
||
(with-temp-buffer
|
||
(ats-exec-script-checked
|
||
device (format "rm %s" (shell-quote-argument commfile t))
|
||
package user))))
|
||
|
||
|
||
|
||
;; Connection management.
|
||
|
||
(defvar ats-file-directory (and load-file-name
|
||
(file-name-directory load-file-name))
|
||
"Directory holding `test-controller.el'.")
|
||
|
||
(defvar ats-server nil
|
||
"ATS server process or nil if yet unavailable.")
|
||
|
||
(defvar ats-default-port 45419
|
||
"Port on which ATS servers listen if auto selection is unavailable.")
|
||
|
||
(defvar ats-accepting-connection nil
|
||
"UUID of connections being established.")
|
||
|
||
(defvar-local ats-associated-process nil
|
||
"ATS process associated with this buffer.
|
||
Such a process will be returned by `ats-read-connection' without
|
||
prompting the user.")
|
||
|
||
(defun ats-address-to-hostname (address)
|
||
"Return the hostname component of the address ADDRESS."
|
||
(progn
|
||
(string-match "\\[?\\(.+?\\)\\]?\\(:[[:alnum:]]+\\)?$" address)
|
||
(match-string 1 address)))
|
||
|
||
(defun ats-is-localhost-p (address)
|
||
"Return whether the hostname in ADDRESS identifies this machine or is nil."
|
||
(or (not address)
|
||
(let ((host (ats-address-to-hostname address)))
|
||
(let ((address-info (network-lookup-address-info host))
|
||
(localhost-info (network-lookup-address-info "localhost")))
|
||
(catch 'result
|
||
(dolist (addr address-info)
|
||
(dolist (addr-1 localhost-info)
|
||
(when (equal addr addr-1)
|
||
(throw 'result t)))))))))
|
||
|
||
(defun ats-server-sentinel (process _)
|
||
"Sentinel function for ATS connections.
|
||
PROCESS is the connection at hand."
|
||
(when (process-get process 'ats-connection-details)
|
||
(ats-disconnect-internal process)
|
||
(kill-buffer (process-buffer process))))
|
||
|
||
(defun ats-server-log (_ connection _)
|
||
"Log function for `ats-server' processes.
|
||
If `ats-accepting-connection' is non-nil, read a string from
|
||
CONNECTION identifying the process, and, if in agreement with
|
||
the former variable, establish a connection and throw.
|
||
Otherwise, terminate the connection."
|
||
(if (not ats-accepting-connection)
|
||
(progn
|
||
(process-send-string connection "-not-accepting-connections\n")
|
||
(delete-process connection))
|
||
(with-current-buffer (process-buffer connection)
|
||
(while connection
|
||
(let ((beg (point)))
|
||
(message "Device connected...")
|
||
(when (accept-process-output connection)
|
||
(goto-char beg)
|
||
(when (search-forward "\n" (process-mark connection) t)
|
||
(let ((uuid (buffer-substring (point-min) (1- (point)))))
|
||
(if (equal uuid ats-accepting-connection)
|
||
(progn
|
||
(process-send-string connection "-ok\n")
|
||
(delete-region (point-min) (point))
|
||
(throw 'connection-established connection))
|
||
(process-send-string connection
|
||
(concat "-incorrect-uuid "
|
||
uuid
|
||
" "
|
||
ats-accepting-connection
|
||
"\n"))
|
||
(delete-process connection)
|
||
(setq connection nil))))))))))
|
||
|
||
(defsubst ats-server-exists-p ()
|
||
"Return whether the ATS server is alive and well.
|
||
Value, if non-nil, is the port on which it listens."
|
||
(and ats-server
|
||
(eq (process-status ats-server) 'listen)
|
||
(process-contact ats-server :service)))
|
||
|
||
(defun ats-start-server ()
|
||
"Start a server to which remote devices may connect.
|
||
Alternatively, return a value pertaining to an existing server.
|
||
Value is the port on which it will listen."
|
||
(if (ats-server-exists-p)
|
||
(process-contact ats-server :service)
|
||
(let ((process
|
||
(make-network-process :name " *ats server*"
|
||
:server t
|
||
:host 'local
|
||
:service (if (featurep 'make-network-process
|
||
'(:service t))
|
||
t
|
||
ats-default-port)
|
||
:family 'ipv4
|
||
:coding 'no-conversion
|
||
:sentinel #'ats-server-sentinel
|
||
:log #'ats-server-log)))
|
||
(setq ats-server process)
|
||
(process-contact process :service))))
|
||
|
||
(defvar ats-await-connection-timeout 180
|
||
"Timeout after which to declare a connection failure.")
|
||
|
||
(defun ats-await-connection (uuid device)
|
||
"Await a connection by a client identifying as UUID.
|
||
DEVICE should be the name of the device to which the connection
|
||
is to be established, to be printed in timeout methods.
|
||
Value is the connection established between the ATS server,
|
||
which must already have been started, and the client.
|
||
Signal an error if connection establishment times out."
|
||
(unless (ats-server-exists-p)
|
||
(error "The ATS server is off-line. Please call `ats-start-server'"))
|
||
(let ((ats-accepting-connection uuid))
|
||
(prog1 (catch 'connection-established
|
||
(with-timeout (ats-await-connection-timeout
|
||
(error "Connection to `%s' timed out..."
|
||
device))
|
||
(let ((time (float-time)))
|
||
(while t
|
||
(message "Connecting... (%s s)"
|
||
(let* ((current-time (float-time))
|
||
(elapsed (- current-time time)))
|
||
(floor elapsed)))
|
||
(accept-process-output nil 1)))))
|
||
(message ""))))
|
||
|
||
(defun ats-forward-server-sentinel (process _)
|
||
"Terminate PROCESS's buffer after it completes."
|
||
(when (not (memq (process-status process) '(run stop)))
|
||
(when (and (process-buffer process)
|
||
(buffer-live-p (process-buffer process)))
|
||
(kill-buffer (process-buffer process)))))
|
||
|
||
(defun ats-forward-server-filter (process string)
|
||
"Prompt for a password or other details if requested by PROCESS.
|
||
Set the process property `ats-connection-established' to t if a
|
||
string indicating success is read, and insert STRING."
|
||
(when (buffer-live-p (process-buffer process))
|
||
(with-current-buffer (process-buffer process)
|
||
(let ((string (string-replace "\r" "" string)))
|
||
(insert string)
|
||
(cond
|
||
((string-prefix-p "ATS_CONNECTION_ESTABLISHED" string)
|
||
(process-put process 'ats-connection-established t))
|
||
((string-match comint-password-prompt-regexp string)
|
||
(process-send-string
|
||
process (concat (read-passwd string) "\n")))
|
||
((string-match tramp-yesno-prompt-regexp string)
|
||
(process-send-string
|
||
process (concat
|
||
(or (and (yes-or-no-p string) "yes") "no") "\n")))
|
||
((string-match tramp-yn-prompt-regexp string)
|
||
(process-send-string
|
||
process (concat
|
||
(or (and (y-or-n-p string) "yes") "no") "\n"))))))))
|
||
|
||
(defun ats-reverse-server (address port)
|
||
"Proxy to port PORT here from the server at ADDRESS, over SSH.
|
||
Value is the port at the destination."
|
||
(if (ats-is-localhost-p address)
|
||
port
|
||
(let* ((host (ats-address-to-hostname address))
|
||
(name (format " *ats-reverse-server %s:%d*" host port))
|
||
(existing-process (get-process name)))
|
||
;; Is that connection available?
|
||
(if (and existing-process
|
||
(process-get existing-process 'ats-connection-established))
|
||
port
|
||
;; Kill it.
|
||
(when existing-process
|
||
(kill-process existing-process))
|
||
(let ((process (start-process name name "ssh" "-o"
|
||
"ExitOnForwardFailure=yes"
|
||
"-R"
|
||
(format "%d:localhost:%d" port port)
|
||
host
|
||
(concat
|
||
"echo ATS_CONNECTION_ESTABLISHED; "
|
||
"while :; do sleep 10; done"))))
|
||
(set-process-sentinel process #'ats-forward-server-sentinel)
|
||
(set-process-filter process #'ats-forward-server-filter)
|
||
(save-window-excursion
|
||
(pop-to-buffer (process-buffer process))
|
||
(while (not (process-get process 'ats-connection-established))
|
||
(if (not (eq (process-status process) 'run))
|
||
(error "ssh forwarding failed with exit code: %d"
|
||
(process-exit-status process))
|
||
(accept-process-output process))))
|
||
port)))))
|
||
|
||
(defun ats-forward-server (address port)
|
||
"Forward from hence to the service at PORT on server ADDRESS over SSH.
|
||
Value is the local port which being forwarded to the destination."
|
||
(if (ats-is-localhost-p address)
|
||
port
|
||
(let* ((host (ats-address-to-hostname address))
|
||
(name (format " *ats-forward-server %s:%d*" host port))
|
||
(existing-process (get-process name)))
|
||
;; Is that connection available?
|
||
(if (and existing-process
|
||
(process-get existing-process 'ats-connection-established))
|
||
port
|
||
;; Kill it.
|
||
(when existing-process
|
||
(kill-process existing-process))
|
||
(let ((process (start-process name name "ssh" "-o"
|
||
"ExitOnForwardFailure=yes"
|
||
"-L"
|
||
(format "%d:localhost:%d" port port)
|
||
host
|
||
(concat
|
||
"echo ATS_CONNECTION_ESTABLISHED; "
|
||
"while :; do sleep 10; done"))))
|
||
(set-process-sentinel process #'ats-forward-server-sentinel)
|
||
(set-process-filter process #'ats-forward-server-filter)
|
||
(save-window-excursion
|
||
(pop-to-buffer (process-buffer process))
|
||
(while (not (process-get process 'ats-connection-established))
|
||
(if (not (eq (process-status process) 'run))
|
||
(error "ssh forwarding failed with exit code: %d"
|
||
(process-exit-status process))
|
||
(accept-process-output process))))
|
||
port)))))
|
||
|
||
(defun ats-cancel-forward-server (address port)
|
||
"Cease forwarding to PORT at ADDRESS over SSH."
|
||
(unless (ats-is-localhost-p address)
|
||
(let* ((host (ats-address-to-hostname address))
|
||
(name (format " *ats-forward-server %s:%d*" host port))
|
||
(process (get-process name)))
|
||
(with-local-quit
|
||
(when (and process
|
||
(memq (process-status process) '(run stop)))
|
||
(interrupt-process process)
|
||
(while (memq (process-status process) '(run stop))
|
||
(accept-process-output process nil nil t)))))))
|
||
|
||
(defconst ats-remote-port 10053
|
||
"ATS port on devices with reverse forwarding but no auto port selection.
|
||
This is offset by the user ID.")
|
||
|
||
(defmacro ats-in-connection-context (process details &rest bodyforms)
|
||
"Evaluate BODYFORMS in PROCESS's context.
|
||
Bind PROCESS's connection details to DETAILS, bind
|
||
`ats-adb-host' to the value under which PROCESS was created, and
|
||
select PROCESS's buffer."
|
||
(declare (indent 2))
|
||
(let ((old-proc process) (process (gensym)))
|
||
`(let* ((,process ,old-proc)
|
||
(,details (process-get ,process 'ats-connection-details)))
|
||
(with-current-buffer (process-buffer ,process)
|
||
(unless ,details
|
||
(error "Not an ATS process: %S" ,process))
|
||
(let ((ats-adb-host (cdr (assq 'host ,details))))
|
||
,@bodyforms)))))
|
||
|
||
(defvar ats-outstanding-reverse-connection nil
|
||
"If non-nil, a list of (HOST DEVICE REMOTE-PORT).
|
||
Which elements are, respectively, the hostname, device, and
|
||
remote port of a reverse proxy connection reserved for a
|
||
connection still being established that mustn't be terminated.")
|
||
|
||
(defun ats-terminate-reverse-safely (device remote-port &optional process)
|
||
"Terminate a reverse forwarding connection from DEVICE:REMOTE-PORT if unused.
|
||
Call `adb -s DEVICE reverse --remove tcp:REMOTE-PORT' safely.
|
||
That is to say, unless REMOTE-PORT on DEVICE is reserved by any
|
||
connection presently established or being established, with the
|
||
exception of PROCESS, if specified."
|
||
(let ((canon-host (or ats-adb-host "localhost")))
|
||
(catch 'abort
|
||
;; Cancel reverse forwarding, but only after guaranteeing that no
|
||
;; other connections exist with the same remote port and device.
|
||
(dolist (proc (process-list))
|
||
(let ((details (and (not (eq process proc))
|
||
(process-get proc 'ats-connection-details))))
|
||
(when details
|
||
(let ((other-host (or (cdr (assq 'host details)) "localhost"))
|
||
(other-device (cdr (assq 'device details)))
|
||
(other-remote-port (cdr (assq 'remote-port details))))
|
||
(when (and (equal canon-host other-host)
|
||
(equal device other-device)
|
||
(eq remote-port other-remote-port))
|
||
(throw 'abort nil))))))
|
||
;; And that the port is not reserved for any connection in
|
||
;; the making.
|
||
(when ats-outstanding-reverse-connection
|
||
(let ((other-host (nth 0 ats-outstanding-reverse-connection))
|
||
(other-device (nth 1 ats-outstanding-reverse-connection))
|
||
(other-port (nth 2 ats-outstanding-reverse-connection)))
|
||
(when (and (equal canon-host other-host)
|
||
(equal device other-device)
|
||
(eq remote-port other-port))
|
||
(throw 'abort nil))))
|
||
(message
|
||
"Canceling reverse forwarding to `%s:%d' from `localhost'"
|
||
device remote-port)
|
||
(ats-adb "-s" device "reverse" "--remove"
|
||
(format "tcp:%d" remote-port)))))
|
||
|
||
(defun ats-disconnect-internal (process)
|
||
"Clean up the ATS connection represented by PROCESS.
|
||
If the connection was initiated by forwarding to the device,
|
||
terminate the local forwarding process if any, and remove the
|
||
port forward from the destination. If initialization was
|
||
effected by reverse forwarding from the device, terminate this
|
||
reverse forwarding session if no other process is forwarding on
|
||
the same port."
|
||
(ats-in-connection-context (get-process process) details
|
||
(let ((device (cdr (assq 'device details)))
|
||
(method (cdr (assq 'connection-method details))))
|
||
(when (eq method 'forward)
|
||
(with-demoted-errors "Error in disconnecting device: %S"
|
||
;; It is necessary to cancel port forwarding from the device
|
||
;; to this host.
|
||
(let ((host-port (cdr (assq 'host-port details))))
|
||
(message "Canceling port forwarding from `localhost' to `%s:%d'"
|
||
ats-adb-host host-port)
|
||
(ats-cancel-forward-server ats-adb-host host-port)))
|
||
(with-demoted-errors "Error in disconnecting device: %S"
|
||
;; It is necessary to cancel port forwarding from the device
|
||
;; to this host.
|
||
(let ((host-port (cdr (assq 'host-port details))))
|
||
(message "Canceling port forwarding from the device to `%s:%d'"
|
||
ats-adb-host host-port)
|
||
(ats-adb "-s" device "forward" "--remove"
|
||
(format "tcp:%d" host-port)))))
|
||
(when (eq method 'reverse)
|
||
(with-demoted-errors "Error in disconnecting device: %S"
|
||
(let ((remote-port (cdr (assq 'remote-port details))))
|
||
(ats-terminate-reverse-safely device remote-port process)))))))
|
||
|
||
(defun ats-read-connection (prompt)
|
||
"Read an ATS connection from the user, with completion.
|
||
If `ats-associated-process' is set in the current buffer, return
|
||
this process if it remains alive. PROMPT is the prompt
|
||
displayed by `completing-read'. Value is a process representing
|
||
such a connection."
|
||
(or (and ats-associated-process
|
||
(eq (process-status ats-associated-process) 'open)
|
||
ats-associated-process)
|
||
(let ((procs))
|
||
(dolist (proc (process-list))
|
||
(when (process-get proc 'ats-connection-details)
|
||
(push (buffer-name (process-buffer proc)) procs)))
|
||
(let ((buffer (completing-read prompt procs
|
||
nil t nil
|
||
'ats-read-processes)))
|
||
(get-buffer-process buffer)))))
|
||
|
||
(defun ats-disconnect (process)
|
||
"Disconnect from the ATS connection represented by PROCESS.
|
||
Interactively, prompt for a process to disconnect.
|
||
|
||
Close PROCESS's connection if appropriate and remove any port
|
||
forwarding currently in place."
|
||
(interactive (list (ats-read-connection "Disconnect from: ")))
|
||
(ats-in-connection-context (get-process process) details
|
||
(delete-process process)))
|
||
|
||
(defun ats-establish-connection (process details &optional interactive)
|
||
"Finalize a connection represented by PROCESS.
|
||
DETAILS should be an alist of connection information to which
|
||
`ats-adb-host' is appended, with the following keys:
|
||
|
||
- `connection-method'
|
||
Either `forward' or `reverse', indicating respectively that
|
||
the connection was established by forwarding to the remote
|
||
device and by forwarding from the local device.
|
||
|
||
- `device'
|
||
Serial number of the device, identifying it to ADB.
|
||
|
||
- `user'
|
||
ID of the user on the device as which the remote process
|
||
executes.
|
||
|
||
- `local-port'
|
||
That port from which `host-port' on the ADB host system is
|
||
being forwarded to, if `connection-method' is `forward'.
|
||
|
||
- `remote-port'
|
||
That port to which `host-port' is being forwarded from,
|
||
if `connection-method' is `reverse'.
|
||
|
||
- `host-port'
|
||
The port on the ADB host system mediating between the local
|
||
and the remote system.
|
||
|
||
If INTERACTIVE, open a Lisp interaction buffer with
|
||
`ats-open-lisp-interaction-buffer'.
|
||
|
||
Value is PROCESS itself."
|
||
(process-put process 'ats-connection-details
|
||
(append `((host . ,ats-adb-host)
|
||
(eval-serial . 0))
|
||
details))
|
||
(let ((device (cdr (assq 'device details)))
|
||
(user (cdr (assq 'user details)))
|
||
(host (or ats-adb-host "localhost")))
|
||
(with-current-buffer (process-buffer process)
|
||
(if (eq user 0)
|
||
(rename-buffer (format " *ats connection for %s (on %s)*"
|
||
device host)
|
||
t)
|
||
(rename-buffer (format " *ats connection for %s (on %s, as %d)*"
|
||
device host user)
|
||
t)))
|
||
(message "Connection established to %s (on %s)"
|
||
(cdr (assq 'device details)) host))
|
||
(prog1 process
|
||
(when interactive
|
||
(ats-open-lisp-interaction-buffer process))))
|
||
|
||
;;;###autoload
|
||
(defun ats-connect (device user &optional host interactive)
|
||
"Establish a connection to DEVICE on HOST executing as USER.
|
||
HOST, if nil, defaults to `ats-adb-host'.
|
||
If an instance of Emacs is already executing on DEVICE and the
|
||
test driver is available, connect to this test driver.
|
||
Otherwise, terminate any existing Emacs sessions, upload the
|
||
test driver, load it into a new Emacs session, and establish a
|
||
connection.
|
||
|
||
Interactively, prompt for a device and a user on the device to
|
||
which to connect. With a prefix argument, also prompt for the
|
||
address of an ADB daemon on a host machine whose devices are to
|
||
be connected to (which requires that OpenSSH be installed on
|
||
this machine and an SSH daemon be executing on the host)."
|
||
(interactive (let* ((host (or (and current-prefix-arg
|
||
(read-string "ADB hostname: "))
|
||
ats-adb-host))
|
||
(ats-adb-host host)
|
||
(device
|
||
(completing-read "Connect to device: "
|
||
(mapcar #'car
|
||
(ats-online-devices))
|
||
nil t nil 'ats-connect-device))
|
||
(user-alist
|
||
(mapcar (lambda (user)
|
||
(cons (format "%s (%d)"
|
||
(cadr user) (car user))
|
||
(car user)))
|
||
(ats-list-users device)))
|
||
(user
|
||
(let ((completions-sort nil))
|
||
(completing-read "Select a user: "
|
||
user-alist nil t))))
|
||
(list device (or (cdr (assoc user user-alist))
|
||
(error "Unknown user: %s" user))
|
||
host t)))
|
||
;; Terminate any existing instances of Emacs executing as this user.
|
||
(let* ((ats-adb-host host)
|
||
(emacs-aid (ats-get-package-aid device "org.gnu.emacs"))
|
||
(emacs-uid (ats-aid-to-uid emacs-aid user))
|
||
(emacs-username (ats-uid-to-username device emacs-uid)))
|
||
;; Start Emacs and arrange to load the test driver.
|
||
(cond
|
||
((ats-supports-am-force-stop-user device)
|
||
(with-temp-buffer
|
||
(ats-adb "-s" device "shell" "am" "force-stop" "--user"
|
||
(number-to-string user) "org.gnu.emacs")))
|
||
((and (ats-supports-am-force-stop device)
|
||
(eq user 0))
|
||
(with-temp-buffer
|
||
(ats-adb "-s" device "shell" "am" "force-stop"
|
||
"org.gnu.emacs")))
|
||
(t (when (ats-kill-process-by-username-and-name
|
||
device emacs-username "org.gnu.emacs" "org.gnu.emacs" user)
|
||
(dotimes (_ 3)
|
||
;; This must be repeated several times or the ActivityManager
|
||
;; may attempt to restart Emacs with the previous intent's
|
||
;; parameters.
|
||
(sleep-for 0.25)
|
||
(ats-kill-process-by-username-and-name
|
||
device emacs-username "org.gnu.emacs" "org.gnu.emacs" user))))))
|
||
;; Upload the test driver.
|
||
(let* ((ats-adb-host host)
|
||
(ats-file (let ((file (and ats-file-directory
|
||
(concat (file-name-as-directory
|
||
ats-file-directory)
|
||
"test-driver.el"))))
|
||
(or (and file (file-exists-p file) file)
|
||
(read-file-name "ATS test driver file: "))))
|
||
(file (ats-upload device ats-file "org.gnu.emacs" user))
|
||
;; Start the server.
|
||
(server-port (ats-start-server))
|
||
;; Forward the server to the ADB host.
|
||
(host-port (ats-reverse-server ats-adb-host server-port))
|
||
;; Forward the server to the device.
|
||
(remote-port (ignore-errors
|
||
(if (>= (ats-get-sdk-version device) 26)
|
||
;; Automatically select a port to open on
|
||
;; the device.
|
||
(ats-reverse-tcp device host-port 0)
|
||
;; Derive a fixed port from the user ID.
|
||
(ats-reverse-tcp device host-port
|
||
(+ ats-remote-port user)))))
|
||
(uuid (if (executable-find "uuidgen")
|
||
(string-trim
|
||
(shell-command-to-string "uuidgen"))
|
||
(format "%x" (random most-positive-fixnum))))
|
||
process)
|
||
(if remote-port
|
||
(progn
|
||
;; Launch Emacs with arguments directing it to load the test
|
||
;; driver file and connect to the local port, and begin to
|
||
;; wait.
|
||
;;
|
||
;; Care must be exercised that process sentinels are not
|
||
;; executed before `ats-outstanding-reverse-connection' is
|
||
;; bound or after a connection is established!
|
||
(unwind-protect
|
||
(let ((ats-outstanding-reverse-connection
|
||
(list (or ats-adb-host "localhost")
|
||
device remote-port)))
|
||
(ats-am-start-intent
|
||
device user
|
||
`((:component . "org.gnu.emacs/.EmacsActivity")
|
||
("org.gnu.emacs.STARTUP_ARGUMENTS"
|
||
"-q" "--load" ,file "--eval"
|
||
,(format "(ats-establish-connection \"localhost\" %d \"%s\")"
|
||
remote-port uuid))))
|
||
(setq process
|
||
(let* ((process (ats-await-connection uuid device)))
|
||
(ats-establish-connection
|
||
process `((connection-method . reverse)
|
||
(remote-port . ,remote-port)
|
||
(host-port . ,host-port)
|
||
(user . ,user)
|
||
(device . ,device))
|
||
interactive))))
|
||
;; On failure, cease forwarding to this device, but permit
|
||
;; the connection to the host to remain.
|
||
(unless process
|
||
(with-demoted-errors "Winding up failed connection: %S"
|
||
(ats-terminate-reverse-safely device remote-port))))
|
||
process)
|
||
(message "Reverse forwarding is unsupported by this device.")
|
||
(sit-for 1 t)
|
||
(message "Instructing the device to establish a proxy connection instead.")
|
||
(sit-for 1 t)
|
||
;; Since there are no alternative means by which to communicate
|
||
;; with a non-debuggable Emacs instance, create a file accessible
|
||
;; both to ADB and to Emacs, and arrange to store Emacs's server
|
||
;; port there.
|
||
(let ((commfile (ats-create-commfile device "org.gnu.emacs" user)))
|
||
(ats-am-start-intent
|
||
device user
|
||
`((:component . "org.gnu.emacs/.EmacsActivity")
|
||
("org.gnu.emacs.STARTUP_ARGUMENTS"
|
||
"-q" "--load" ,file "--eval"
|
||
,(format "(ats-initiate-connection %S)" commfile))))
|
||
(let* ((portno (with-timeout
|
||
(ats-await-connection-timeout
|
||
(error "Connection to `%s' timed out..." device))
|
||
(ats-watch-commfile device commfile
|
||
"org.gnu.emacs" user)))
|
||
(remote-port (string-to-number portno)))
|
||
(when (zerop remote-port)
|
||
(error "Failed to read port number from device"))
|
||
;; Forward it.
|
||
(let* ((host-port (ats-forward-tcp device remote-port 0))
|
||
(name (format " *ats connection for %s (on %s)*"
|
||
device (or ats-adb-host "localhost")))
|
||
local-port process)
|
||
(condition-case err
|
||
(progn
|
||
(setq local-port (ats-forward-server ats-adb-host host-port))
|
||
(setq process (make-network-process
|
||
:name name
|
||
:buffer name
|
||
:host 'local
|
||
:service local-port
|
||
:coding 'no-conversion
|
||
:sentinel #'ats-server-sentinel))
|
||
(process-send-string process "-ok\n")
|
||
(ats-establish-connection process
|
||
`((connection-method . forward)
|
||
(local-port . ,local-port)
|
||
(host-port . ,host-port)
|
||
(user . ,user)
|
||
(device . ,device))
|
||
interactive))
|
||
(error
|
||
(when process
|
||
;; Finalize the failed process as best as can be
|
||
;; managed.
|
||
(with-demoted-errors "Winding up failed connection: %S"
|
||
(ats-disconnect-internal process)))
|
||
(when local-port
|
||
(with-demoted-errors "Winding up failed connection: %S"
|
||
;; Though local-port serves to attest whether a
|
||
;; forwarding connection has been established, yet it
|
||
;; is the destination port that identifies such a
|
||
;; connection to `ats-cancel-forward-server', which
|
||
;; is not consistent with `adb forward --remove'.
|
||
(ats-cancel-forward-server ats-adb-host host-port)))
|
||
(with-demoted-errors "Winding up failed connection: %S"
|
||
(ats-adb "-s" device "forward" "--remove"
|
||
(format "tcp:%d" host-port)))
|
||
(signal (car err) (cdr err))))))))))
|
||
|
||
|
||
|
||
;; Command submission and execution.
|
||
|
||
;; (defvar ats-eval-tm 0)
|
||
|
||
(defun ats-eval (process form &optional as-printed raw)
|
||
"Evaluate FORM in PROCESS, which form must be printable.
|
||
Form should evaluate to a value that must be printable, or
|
||
signal an error. Value is (ok . VALUE) if no error was
|
||
signaled, or (error . VALUE) otherwise. If RAW, instruct
|
||
PROCESS not to attempt to decode the printed representation of
|
||
FORM as multibyte text; this does not influence the decoding
|
||
whatever value it returns.
|
||
|
||
Set AS-PRINTED to insist that the value be returned as a string;
|
||
this enables non-printable values to be returned in a meaningful
|
||
manner."
|
||
(ats-in-connection-context process details
|
||
(save-restriction
|
||
(let* ((str (encode-coding-string
|
||
(prin1-to-string form) 'utf-8-emacs t))
|
||
(length (length str))
|
||
(serial (setf (alist-get 'eval-serial details)
|
||
(1+ (alist-get 'eval-serial details))))
|
||
(serial-str (number-to-string serial))
|
||
(request-regexp (rx bol "\fats-request:"
|
||
(literal serial-str)
|
||
" " (group (+ digit)) "\n"))
|
||
(point (point))
|
||
size form)
|
||
(process-send-string process
|
||
(format "-eval %d %d %s %s\n" serial
|
||
length
|
||
(if as-printed "t" "nil")
|
||
(if raw "nil" "t")))
|
||
(process-send-string process str)
|
||
;; Read the resultant form.
|
||
(while (not form)
|
||
(when (not (eq (process-status process) 'open))
|
||
(error "Connection terminated unexpectedly..."))
|
||
;; (let ((t1 (float-time)))
|
||
;; (prog1 (accept-process-output process nil nil 1)
|
||
;; (setq ats-eval-tm (+ (- (float-time) t1)
|
||
;; ats-eval-tm))))
|
||
(when (accept-process-output process)
|
||
(when (not size)
|
||
;; First skip all output till the header is read.
|
||
(save-excursion
|
||
(goto-char point)
|
||
(when-let* ((start (re-search-forward
|
||
request-regexp nil t)))
|
||
(setq size (string-to-number (match-string 1)))
|
||
(delete-region (point-min) (point)))))
|
||
(when size
|
||
;; Read SIZE bytes from the process.
|
||
(when (>= (- (point-max) (point-min)) size)
|
||
(narrow-to-region (point-min) (+ (point-min) size))
|
||
(goto-char (point-min))
|
||
(setq form (car (read-from-string
|
||
(decode-coding-string
|
||
(buffer-string)
|
||
'utf-8-unix t))))))))
|
||
form))))
|
||
|
||
|
||
|
||
;; Remote Lisp Interaction mode.
|
||
|
||
(defvar ats-remote-eval-defuns
|
||
'(progn
|
||
(defalias 'ats-remote-eval-on-device
|
||
#'(lambda (form)
|
||
"Remotely evaluate a submitted form FORM.
|
||
Collect FORM's standard output and return values, and return a
|
||
list of the form (ok STANDARD-OUTPUT VALUE VALUE-TRUNCATED),
|
||
where STANDARD-OUTPUT is any output the form has printed or
|
||
inserted, VALUE is FORM's value, and VALUE-TRUNCATED is FORM's
|
||
value after truncation as in the manner of `eval-expression',
|
||
both as strings.
|
||
|
||
If FORM should signal an error, value becomes (error ERROR),
|
||
where ERROR is a cons of the error's symbol and of its data."
|
||
(condition-case error
|
||
(let ((standard-output
|
||
(get-buffer-create "*ats-standard-output*")))
|
||
(with-current-buffer standard-output
|
||
(erase-buffer)
|
||
(let ((value (eval form nil)))
|
||
(list 'ok (buffer-string)
|
||
(prin1-to-string value)
|
||
(let ((print-length eval-expression-print-length)
|
||
(print-level eval-expression-print-level))
|
||
(prin1-to-string value))))))
|
||
(error (list 'error error))))))
|
||
"Forms to be evaluated on the remote device before remote evaluation.")
|
||
|
||
(defun ats-remote-eval-print-sexp
|
||
(value value-truncated output &optional no-truncate)
|
||
"Print VALUE and VALUE-TRUNCATED (a string) to OUTPUT.
|
||
The manner of printing is subject to NO-TRUNCATE.
|
||
Adapted from `elisp--eval-last-sexp-print-value' in
|
||
`elisp-mode.el'."
|
||
(let* ((unabbreviated value) (beg (point)) end)
|
||
(prog1 (princ (if no-truncate
|
||
value
|
||
value-truncated)
|
||
output)
|
||
(setq end (point))
|
||
(when (and (bufferp output)
|
||
(or (not (null print-length))
|
||
(not (null print-level)))
|
||
(not (string= unabbreviated
|
||
(buffer-substring-no-properties beg end))))
|
||
(last-sexp-setup-props beg end value
|
||
unabbreviated
|
||
(buffer-substring-no-properties beg end))))))
|
||
|
||
(defun ats-remote-eval-for-interaction (process form &optional no-truncate)
|
||
"Evaluate FORM for Lisp interaction in a remote device.
|
||
PROCESS represents the connection to the said device. Insert
|
||
text printed by FORM to standard output and its return value on
|
||
success, as would `eval-last-sexp', and signal an error on
|
||
failure.
|
||
If NO-TRUNCATE, print FORM's value in full without truncation."
|
||
(let ((details (process-get process 'ats-connection-details))
|
||
rc)
|
||
;; First, set up a utility function.
|
||
(unless (cdr (assq 'remote-eval-initialized details))
|
||
(setq rc (ats-eval process ats-remote-eval-defuns))
|
||
(when (eq (car rc) 'error)
|
||
(error "Could not initialize remote evaluation: %S"
|
||
(cdr rc)))
|
||
(process-put process 'ats-connection-details
|
||
(cons '(remote-eval-initialized . t) details)))
|
||
;; Next, really evaluate the form, and also, recognize and convert
|
||
;; errors in preparing to evaluate the form appropriately.
|
||
(let ((value (ats-eval process
|
||
`(let ((eval-expression-print-length
|
||
,eval-expression-print-length)
|
||
(eval-expression-print-level
|
||
,eval-expression-print-level))
|
||
(ats-remote-eval-on-device ',form)))))
|
||
(cond ((eq (car value) 'ok)
|
||
;; The form was read successfully, but evaluation may
|
||
;; nevertheless have terminated with an error.
|
||
(let ((value (cdr value)))
|
||
(cond ((eq (car value) 'ok)
|
||
(insert (cadr value))
|
||
(ats-remote-eval-print-sexp (caddr value)
|
||
(cadddr value)
|
||
(current-buffer)
|
||
no-truncate))
|
||
((eq (car value) 'error)
|
||
(signal (caadr value)
|
||
(cdadr value))))))
|
||
((eq (car value) 'error)
|
||
;; The device could not decode the form.
|
||
(error "Error decoding form on device: %S" (cdr value)))))))
|
||
|
||
(defun ats-remote-eval-print-last-sexp (process &optional arg)
|
||
"Evaluate sexp before point; print value into the current buffer.
|
||
Evaluation transpires in the device controlled by the remote
|
||
connection represented by PROCESS. ARG inhibits truncation of
|
||
printed values, as in `eval-print-last-sexp'."
|
||
(interactive (list (ats-read-connection "Connection: ")
|
||
current-prefix-arg))
|
||
(insert "\n")
|
||
(ats-remote-eval-for-interaction process (elisp--preceding-sexp)
|
||
arg)
|
||
(insert "\n"))
|
||
|
||
(defun ats-remote-eval-last-sexp (process &optional arg)
|
||
"Evaluate sexp before point.
|
||
Subsequently, print value and inserted text in the echo area.
|
||
Evaluation transpires in the device controlled by the remote
|
||
connection represented by PROCESS. ARG inhibits truncation of
|
||
printed values, as in `eval-print-last-sexp'."
|
||
(interactive (list (ats-read-connection "Connection: ")
|
||
current-prefix-arg))
|
||
(let ((sexp (elisp--preceding-sexp)))
|
||
(with-temp-buffer
|
||
(ats-remote-eval-for-interaction process sexp arg)
|
||
(message (buffer-string)))))
|
||
|
||
(defun ats-remote-eval-defun (process)
|
||
"Evaluate defun around or after point.
|
||
Evaluation transpires in the device controlled by the remote
|
||
connection represented by PROCESS."
|
||
(interactive (list (ats-read-connection "Connection: ")))
|
||
(let ((standard-output t) form)
|
||
;; Read the form from the buffer, and record where it ends.
|
||
(save-excursion
|
||
(end-of-defun)
|
||
(beginning-of-defun)
|
||
(setq form (read (current-buffer))))
|
||
(with-temp-buffer
|
||
(ats-remote-eval-for-interaction process form)
|
||
(message (buffer-string)))))
|
||
|
||
(defun ats-remote-eval-region-or-buffer (process)
|
||
"Evaluate the forms in the active region or the whole buffer.
|
||
Evaluation transpires in the device controlled by the remote
|
||
connection represented by PROCESS."
|
||
(interactive (list (ats-read-connection "Connection: ")))
|
||
(let ((evalstring (if (use-region-p)
|
||
(buffer-substring (region-beginning)
|
||
(region-end))
|
||
(buffer-string))))
|
||
(ats-eval process `(with-temp-buffer
|
||
(insert ,evalstring)
|
||
(eval-buffer)))))
|
||
|
||
(defvar ats-lisp-interaction-mode-map
|
||
(let ((map (make-sparse-keymap)))
|
||
(define-key map [remap eval-print-last-sexp]
|
||
#'ats-remote-eval-print-last-sexp)
|
||
(define-key map [remap eval-defun]
|
||
#'ats-remote-eval-defun)
|
||
(define-key map [remap elisp-eval-region-or-buffer]
|
||
#'ats-remote-eval-region-or-buffer)
|
||
(define-key map [remap eval-last-sexp]
|
||
#'ats-remote-eval-last-sexp)
|
||
map)
|
||
"Keymap applied in `ats-lisp-interaction-mode' buffers.")
|
||
|
||
(easy-menu-define ats-lisp-interaction-mode-menu
|
||
ats-lisp-interaction-mode-map
|
||
"Menu for Ats Lisp Interaction mode."
|
||
'("Lisp-Interaction"
|
||
["Complete Lisp Symbol" completion-at-point
|
||
:help "Perform completion on Lisp symbol preceding point"]
|
||
["Indent or Pretty-Print" indent-pp-sexp
|
||
:help "Indent each line of the list starting just after point, or prettyprint it"]
|
||
["Evaluate and Print" ats-remote-eval-print-last-sexp
|
||
:help "Evaluate sexp before point; print value into current buffer"]
|
||
["Evaluate Defun" ats-remote-eval-defun
|
||
:help "Evaluate the top-level form containing point, or after point"]))
|
||
|
||
(define-derived-mode ats-lisp-interaction-mode lisp-interaction-mode
|
||
`("Remote Lisp Interaction"
|
||
(:eval (unless (and ats-associated-process
|
||
(processp ats-associated-process)
|
||
(eq (process-status ats-associated-process)
|
||
'open))
|
||
,(propertize " disconnected" 'face 'error))))
|
||
"Variant of `lisp-interaction-mode' that executes forms remotely.
|
||
This derivative of `lisp-interaction-mode' rebinds such commands
|
||
as \\[eval-print-last-sexp] to variants which submit forms for
|
||
execution on remote Android devices connected over `adb'. It
|
||
also disables a number of features unsupported by remote
|
||
execution facilities, such as edebug.")
|
||
|
||
(defun ats-open-lisp-interaction-buffer (process)
|
||
"Open an Ats Lisp Interaction Mode buffer on PROCESS
|
||
Create and display a buffer in `ats-lisp-interaction-mode'; that
|
||
is, a mode akin to `lisp-interaction-mode' but which submits
|
||
forms typed to a remote Android device over the connection
|
||
represented by PROCESS."
|
||
(interactive (list (ats-read-connection "Connection: ")))
|
||
(ats-in-connection-context process details
|
||
(let ((device (cdr (assq 'device details)))
|
||
(user (cdr (assq 'user details))))
|
||
(with-current-buffer (get-buffer-create
|
||
(format "*Lisp Interaction in %s (on %s%s)*"
|
||
device
|
||
(or ats-adb-host "localhost")
|
||
(if (not (eq user 0))
|
||
(format ", as %d" user)
|
||
"")))
|
||
(ats-lisp-interaction-mode)
|
||
(setq ats-associated-process process)
|
||
(when (eq (buffer-size) 0)
|
||
(insert (format "\
|
||
;; This buffer enables typed Lisp forms to be executed in the device `%s' on `%s'.
|
||
;; View the doc string of `ats-lisp-interaction-mode' for specifics.\n\n"
|
||
device
|
||
(or ats-adb-host "localhost")))
|
||
(save-excursion
|
||
(goto-char (point-min))
|
||
(fill-region (point) (progn
|
||
(end-of-line)
|
||
(point)))
|
||
(goto-char (point-max))
|
||
(beginning-of-line)
|
||
(fill-region (point) (point-max))))
|
||
(pop-to-buffer (current-buffer))))))
|
||
|
||
|
||
;; ERT regression testing.
|
||
|
||
(defvar ats-emacs-test-directory
|
||
(and load-file-name
|
||
(expand-file-name
|
||
(concat (file-name-directory load-file-name)
|
||
"../../")))
|
||
"Directory in which to locate Emacs regression tests, or nil otherwise.")
|
||
|
||
(defun ats-upload-test (process dir test-name)
|
||
"Upload a test file and its resources to a remote device.
|
||
PROCESS represents the connection to the device.
|
||
TEST-NAME concatenated with \"-tests.el\" should identify a file
|
||
in DIR implementing a series of ERC regression tests. If there
|
||
is additionally a directory by the name TEST-NAME-resources in
|
||
the same directory, upload it to the remote device also.
|
||
Once uploaded, tests defined in the file may be loaded and
|
||
executed by means of `ats-exec-tests'."
|
||
(interactive
|
||
(let* ((connection (ats-read-connection "Connection: "))
|
||
(dir (or ats-emacs-test-directory
|
||
(read-directory-name "Test base directory: "
|
||
nil nil t)))
|
||
(test (completing-read "Test to upload: "
|
||
(ats-list-tests-locally dir)
|
||
nil t nil
|
||
'ats-uploaded-tests)))
|
||
(list connection dir test)))
|
||
(let* ((dir-name (file-name-as-directory
|
||
(expand-file-name dir)))
|
||
(test-file
|
||
(concat dir-name test-name "-tests.el"))
|
||
(internal-resource-directory
|
||
(concat dir-name (file-name-directory test-name)
|
||
"resources"))
|
||
(resources-directory
|
||
(if (file-directory-p internal-resource-directory)
|
||
internal-resource-directory
|
||
(concat dir-name test-name "-resources")))
|
||
;; Strip all directories from the test name.
|
||
(default-directory (file-name-directory test-file)))
|
||
(unless (file-regular-p test-file)
|
||
(error "Not a regular file: %s" test-file))
|
||
(if (file-directory-p resources-directory)
|
||
;; Create a compressed tar file. Though a cpio implementation
|
||
;; exists in the sources for Android 2.2's command line tools,
|
||
;; yet it is often deleted in release builds of the OS to reduce
|
||
;; storage utilization, so it is best to resort to tar and gzip,
|
||
;; which Emacs is able to decompress without command line
|
||
;; utilities.
|
||
(let ((temp-file (make-temp-file "ats-" nil ".tar"))
|
||
(bare-test-file (file-name-nondirectory test-file))
|
||
(bare-test-resources
|
||
(file-name-nondirectory resources-directory)))
|
||
(unwind-protect
|
||
(progn
|
||
(let ((rc (call-process
|
||
"tar" nil nil nil "cfh" temp-file
|
||
bare-test-file bare-test-resources)))
|
||
(unless (eq 0 rc)
|
||
(error "tar exited with code: %d" rc)))
|
||
;; Compress this file.
|
||
(with-temp-buffer
|
||
(set-buffer-multibyte nil)
|
||
(let ((rc (call-process "gzip" nil '(t nil) nil
|
||
"-c" temp-file)))
|
||
(unless (eq 0 rc)
|
||
(error "gzip -c exited with code: %d" rc))
|
||
;; Write this compressed data to the destination and
|
||
;; decompress it there.
|
||
(let ((rc (ats-eval
|
||
process
|
||
`(with-temp-buffer
|
||
(set-buffer-multibyte nil)
|
||
(insert ,(buffer-string))
|
||
(zlib-decompress-region (point-min)
|
||
(point-max))
|
||
(let ((dir
|
||
(concat (file-name-as-directory
|
||
temporary-file-directory)
|
||
"ats-tests/" ,test-name)))
|
||
(if (file-directory-p dir)
|
||
(let ((files
|
||
(directory-files-recursively
|
||
dir ""))
|
||
(default-directory dir))
|
||
(mapc #'delete-file files))
|
||
(make-directory dir t))
|
||
(let ((default-directory dir)
|
||
;; Otherwise file name handlers
|
||
;; such as `epa-file-handler'
|
||
;; are liable to interfere with
|
||
;; the extraction process.
|
||
(file-name-handler-alist nil))
|
||
(require 'tar-mode)
|
||
(tar-mode)
|
||
(tar-untar-buffer))))
|
||
nil t)))
|
||
(when (eq (car rc) 'error)
|
||
(error "Remote error: %S" (cdr rc)))
|
||
(message "Uploaded test `%s'" test-name)))))
|
||
(with-demoted-errors "Removing temporary file: %S"
|
||
(delete-file temp-file))))
|
||
;; Just compress and transfer the file alone.
|
||
(with-temp-buffer
|
||
(set-buffer-multibyte nil)
|
||
(let ((rc (call-process "gzip" nil '(t nil) nil
|
||
"-c" test-file)))
|
||
(unless (eq 0 rc)
|
||
(error "gzip -c exited with code: %d" rc))
|
||
;; Write this compressed data to the destination and
|
||
;; decompress it there.
|
||
(let ((rc (ats-eval
|
||
process
|
||
`(with-temp-buffer
|
||
(set-buffer-multibyte nil)
|
||
(insert ,(buffer-string))
|
||
(zlib-decompress-region (point-min)
|
||
(point-max))
|
||
(let* ((dir
|
||
(concat (file-name-as-directory
|
||
temporary-file-directory)
|
||
"ats-tests/" ,test-name))
|
||
(dir-1 (file-name-as-directory dir)))
|
||
(if (file-directory-p dir)
|
||
(let ((files
|
||
(directory-files-recursively
|
||
dir ""))
|
||
(default-directory dir))
|
||
(mapc #'delete-file files))
|
||
(make-directory dir t))
|
||
(write-region
|
||
(point-min) (point-max)
|
||
(concat dir-1 ,(file-name-nondirectory
|
||
test-file)))))
|
||
nil t)))
|
||
(when (eq (car rc) 'error)
|
||
(error "Remote error: %S" (cdr rc)))
|
||
(message "Uploaded test `%s'" test-name)))))))
|
||
|
||
(defun ats-list-tests-locally (dir)
|
||
"Return a list of tests defined in DIR.
|
||
DIR ought to be the `test' directory in the Emacs repository or
|
||
a likewise structured directory tree."
|
||
(let* ((default-directory (expand-file-name dir))
|
||
(start (length default-directory)))
|
||
(let ((dirs (directory-files-recursively
|
||
dir "^[[:alnum:]-]+-tests\\.el$"
|
||
;; Do not recurse into resource directories, as ERC's
|
||
;; contain several files that resemble tests.
|
||
nil (lambda (dir-name)
|
||
(and (not (equal (file-name-nondirectory dir-name)
|
||
"resources"))
|
||
(not (string-suffix-p "-resources" dir-name))))))
|
||
tests)
|
||
(dolist (dir dirs)
|
||
(let ((len (length dir)))
|
||
(push (substring dir start (- len 9)) tests)))
|
||
(nreverse tests))))
|
||
|
||
(defun ats-list-tests (process)
|
||
"Enumerate those tests which have already been uploaded to PROCESS.
|
||
Return a list of strings identifying tests which have been
|
||
uploaded to the remote device represented by PROCESS, as by
|
||
`ats-upload-tests', and which may be executed with
|
||
`ats-exec-tests'."
|
||
(let ((rc (ats-eval
|
||
process
|
||
`(let* ((dir (concat (file-name-as-directory
|
||
temporary-file-directory)
|
||
"ats-tests"))
|
||
(len (length (file-name-as-directory dir)))
|
||
(default-directory dir)
|
||
(is-test-directory '(lambda (dir name)
|
||
(file-regular-p
|
||
(format "%s/%s-tests.el"
|
||
dir name)))))
|
||
(let ((dirs
|
||
(directory-files-recursively
|
||
dir "" t
|
||
;; Do not iterate into directories that are tests of
|
||
;; themselves, or their resources.
|
||
(lambda (dir)
|
||
(let* ((name (file-name-nondirectory dir)))
|
||
(and (not (funcall is-test-directory name dir))
|
||
(not (equal name "resources"))
|
||
(not (string-suffix-p name "-resources")))))))
|
||
(tests nil))
|
||
(dolist (dir dirs)
|
||
(when (funcall is-test-directory
|
||
dir
|
||
(file-name-nondirectory dir))
|
||
(push (substring dir len) tests)))
|
||
(nreverse tests))))))
|
||
(when (eq (car rc) 'error)
|
||
(error "Remote error: %S" (cdr rc)))
|
||
(cdr rc)))
|
||
|
||
(defun ats-run-test (process test &optional selector)
|
||
"Run tests defined in a single test TEST on a remote device.
|
||
PROCESS represents the device on which to execute these tests.
|
||
SELECTOR is an ERT test selector, as with `ert-select-tests'.
|
||
\(You may upload tests beforehand by calling `ats-upload-test'.)
|
||
Display the output of the tests executed in a buffer."
|
||
(interactive
|
||
(let* ((connection
|
||
(ats-read-connection "Connection: "))
|
||
(test
|
||
(completing-read "Test to execute: "
|
||
(ats-list-tests connection)
|
||
nil t nil 'ats-tests-executed)))
|
||
(list connection test)))
|
||
;; Attempt to byte-compile this test file.
|
||
(let ((rc (ats-eval
|
||
process
|
||
`(progn
|
||
(let* ((dir (concat (file-name-as-directory
|
||
temporary-file-directory)
|
||
"ats-tests/" ,test))
|
||
(name ,(file-name-nondirectory test))
|
||
(testfile (concat (file-name-as-directory dir)
|
||
name "-tests.el")))
|
||
(with-temp-buffer
|
||
(let ((value (byte-compile-file testfile))
|
||
(byte-compile-log-buffer (buffer-name)))
|
||
(cond ((eq value 'no-byte-compile)
|
||
testfile)
|
||
(value
|
||
(byte-compile-dest-file testfile))
|
||
(t (list (buffer-string))))))))))
|
||
(device (cdr (assq 'device (process-get
|
||
process 'ats-connection-details))))
|
||
file-name)
|
||
(cond ((eq (car rc) 'error)
|
||
(error "Error during byte-compilation of `%s-tests.el': %S"
|
||
test (cdr rc)))
|
||
((listp (cdr rc))
|
||
(error
|
||
"Encountered errors byte-compiling `%s-tests.el':\n%s"
|
||
test (cadr rc)))
|
||
(t (setq file-name (cdr rc))))
|
||
;; Delete all tests, load the byte-compiled test file, and execute
|
||
;; those tests just defined subject to SELECTOR.
|
||
(with-current-buffer (get-buffer-create "*Test Output*")
|
||
(insert (format "=== Executing %s on %s ===\n" test device))
|
||
(redisplay)
|
||
(setq rc (ats-eval process
|
||
`(progn
|
||
(require 'ert)
|
||
(ert-delete-all-tests)
|
||
(load ,file-name)
|
||
(with-temp-buffer
|
||
(let* ((temp-buffer (current-buffer))
|
||
(standard-output temp-buffer)
|
||
;; Disable remote tests for the
|
||
;; present...
|
||
(ert-remote-temporary-file-directory
|
||
null-device)
|
||
(overriding-text-conversion-style nil)
|
||
(set-message-function
|
||
(lambda (message)
|
||
(with-current-buffer temp-buffer
|
||
(insert message "\n")))))
|
||
(let ((noninteractive t))
|
||
(ert-run-tests-batch ',selector))
|
||
(insert "=== Test execution complete ===\n")
|
||
(buffer-substring-no-properties
|
||
(point-min) (point-max)))))))
|
||
(cond ((eq (car rc) 'error)
|
||
(error "Error executing `%s-tests.el': %S" test (cdr rc)))
|
||
(t (progn
|
||
(goto-char (point-max))
|
||
(insert (cdr rc))
|
||
(pop-to-buffer (current-buffer))))))))
|
||
|
||
(defun ats-upload-all-tests (process dir)
|
||
"Upload every Emacs test in DIR to the device represented by PROCESS.
|
||
Upload each and every test defined in DIR to the said device."
|
||
(interactive
|
||
(list (ats-read-connection "Connection: ")
|
||
(or ats-emacs-test-directory
|
||
(read-directory-name "Test base directory: "
|
||
nil nil t))))
|
||
(let ((tests (ats-list-tests-locally dir)))
|
||
(unless current-prefix-arg
|
||
(dolist-with-progress-reporter (test tests)
|
||
"Uploading tests to device..."
|
||
(ats-upload-test process dir test)))))
|
||
|
||
(defun ats-run-all-tests (process &optional selector)
|
||
"Run every Emacs test uploaded to the device represented by PROCESS.
|
||
Execute every Emacs test that has been uploaded to PROCESS,
|
||
subject to SELECTOR, as in `ert-run-tests'."
|
||
(interactive (list (ats-read-connection "Connection: ")
|
||
(and current-prefix-arg (read))))
|
||
(let ((tests (ats-list-tests process)))
|
||
(dolist-with-progress-reporter (test tests)
|
||
"Running tests..."
|
||
(ats-run-test process test selector))))
|
||
|
||
(provide 'test-controller)
|
||
|
||
;;; test-controller.el ends here
|
||
|
||
;; Local Variables:
|
||
;; emacs-lisp-docstring-fill-column: 64
|
||
;; indent-tabs-mode: t
|
||
;; End:
|