Skip to content

Commit e74c6bf

Browse files
committed
Add Python adb code to new Android package
1 parent 7bcd823 commit e74c6bf

File tree

9 files changed

+590
-12
lines changed

9 files changed

+590
-12
lines changed

lgl_host_server.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
# Copyright (c) 2024 Arm Limited
44
#
55
# Permission is hereby granted, free of charge, to any person obtaining a copy
6-
# of this software and associated documentation files (the "Software"), to
6+
# of this software and associated documentation files (the 'Software'), to
77
# deal in the Software without restriction, including without limitation the
88
# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
99
# sell copies of the Software, and to permit persons to whom the Software is
@@ -12,7 +12,7 @@
1212
# The above copyright notice and this permission notice shall be included in
1313
# all copies or substantial portions of the Software.
1414
#
15-
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15+
# THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
1616
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
1717
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
1818
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
@@ -64,9 +64,9 @@ def main():
6464

6565
# Press to exit
6666
try:
67-
input("Press any key to exit ...\n\n")
67+
input('Press any key to exit ...\n\n')
6868
except KeyboardInterrupt:
69-
print("Exiting ...")
69+
print('Exiting ...')
7070
sys.exit(0)
7171

7272
return 0

lglpy/android/__init__.py

Whitespace-only changes.

lglpy/android/adb.py

Lines changed: 233 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,233 @@
1+
# SPDX-License-Identifier: MIT
2+
# -----------------------------------------------------------------------------
3+
# Copyright (c) 2024-2025 Arm Limited
4+
#
5+
# Permission is hereby granted, free of charge, to any person obtaining a copy
6+
# of this software and associated documentation files (the 'Software'), to
7+
# deal in the Software without restriction, including without limitation the
8+
# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
9+
# sell copies of the Software, and to permit persons to whom the Software is
10+
# furnished to do so, subject to the following conditions:
11+
#
12+
# The above copyright notice and this permission notice shall be included in
13+
# all copies or substantial portions of the Software.
14+
#
15+
# THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
# SOFTWARE.
22+
# -----------------------------------------------------------------------------
23+
24+
# This module implements a simple wrapper around the Android Debug Bridge
25+
# command line tool which can be used to transfer files and copy data to a
26+
# remote device.
27+
28+
from typing import Any, Optional
29+
30+
31+
import os
32+
import shlex
33+
import subprocess as sp
34+
35+
36+
class ADBConnect:
37+
'''
38+
A wrapper around adb which can be used to connect to a specific device,
39+
and run commands as a specific package.
40+
41+
Attributes:
42+
device: The name of the connected device, or None for generic use.
43+
package: The name of the debuggable package, or None for generic use.
44+
'''
45+
46+
def __init__(self, device: Optional[str] = None,
47+
package: Optional[str] = None):
48+
'''
49+
Create a new device, defaulting to non-specific use.
50+
51+
Args:
52+
device: The device identifier, as returned by 'adb devices', or
53+
None for non-specific use.
54+
package: The package name, as returned by `adb shell pm list
55+
packages` or None for non-specific use.
56+
'''
57+
self.device = device
58+
self.package = package
59+
60+
def get_base_command(self, args: list[str]) -> list[str]:
61+
'''
62+
Get the root of an adb command, injecting device selector if needed.
63+
64+
Args:
65+
args: The user argument list, may be empty.
66+
67+
Returns:
68+
The fully populated command list.
69+
'''
70+
commands = ['adb']
71+
if self.device:
72+
commands.extend(['-s', self.device])
73+
commands.extend(args)
74+
75+
return commands
76+
77+
def pack_commands(self, commands: list[str],
78+
shell: bool, quote: bool) -> list[str] | str:
79+
'''
80+
Pack a set of command lines suitable for a subprocess call.
81+
82+
Args:
83+
commands: List of command line parameters.
84+
shell: True if this should invoke the host shell.
85+
quote: True if arguments are quoted before forwarding.
86+
87+
Return:
88+
Appropriated packed command line arguments for the host OS.
89+
'''
90+
# Run via the host shell
91+
if shell:
92+
# Unix shells need a flattened command for shell commands
93+
if os.name != 'nt':
94+
quoted_commands = []
95+
for command in commands:
96+
if command != '>':
97+
command = shlex.quote(command)
98+
quoted_commands.append(command)
99+
100+
return ' '.join(quoted_commands)
101+
102+
# We do not currently quote on other host shells
103+
return commands
104+
105+
# Run via direct invocation of adb with quoting for target shell
106+
if quote:
107+
return [shlex.quote(arg) for arg in commands]
108+
109+
# Run via direct invocation of adb without any additional quoting
110+
return commands
111+
112+
def adb(self, *args: str, text: bool = True, shell: bool = False,
113+
quote: bool = False, check: bool = True) -> str:
114+
'''
115+
Call adb to synchronously run a command, check its result, and capture
116+
its output if successful.
117+
118+
Commands can invoke adb directly, or via the host shell if invoked with
119+
shell=True. When using shell=True on Unix hosts the arguments are
120+
always quoted unless the argument is a '>' redirect shell argument. On
121+
Windows beware of the security implications of the lack of quoting.
122+
123+
Args:
124+
*args: List of command line parameters.
125+
text: True if output is text, False if binary
126+
shell: True if this should invoke via host shell, False if direct.
127+
quote: True if arguments are quoted, False if unquoted.
128+
check: True if result is checked, False if ignored.
129+
130+
Returns:
131+
The stdout response written by adb.
132+
133+
Raises:
134+
CalledProcessError: The invoked call failed.
135+
'''
136+
# Build the command list
137+
commands = self.get_base_command(args)
138+
packed_commands = self.pack_commands(commands, shell, quote)
139+
140+
# Invoke the command
141+
rep = sp.run(packed_commands, check=check, shell=shell, text=text,
142+
stdin=sp.DEVNULL,
143+
stdout=sp.PIPE,
144+
stderr=sp.PIPE)
145+
146+
# Return the output
147+
return rep.stdout
148+
149+
def adb_async(self, *args: str, text: bool = True, shell: bool = False,
150+
quote: bool = False, pipe: bool = False) -> sp.Popen:
151+
'''
152+
Call adb to asynchronously run a command, without waiting for it to
153+
complete.
154+
155+
Commands can invoke adb directly, or via the host shell if invoked with
156+
shell=True. When using shell=True on Unix hosts the arguments are
157+
always quoted unless the argument is a '>' redirect shell argument. On
158+
Windows beware of the security implications of the lack of quoting.
159+
160+
By default, the adb stdout data is discarded. It can be kept by setting
161+
pipe=True, but in this case the caller must call communicate() on the
162+
returned object to avoid the child blocking indefinitely if the OS pipe
163+
buffer fills up.
164+
165+
Args:
166+
*args: List of command line parameters.
167+
text: True if output is text, False if binary
168+
shell: True if this should invoke via host shell, False if direct.
169+
quote: True if arguments are quoted, False if unquoted.
170+
pipe: True if child stdout is collected, False if discarded.
171+
172+
Returns:
173+
The process handle.
174+
175+
Raises:
176+
CalledProcessError: The invoked call failed.
177+
'''
178+
# Setup the configuration
179+
output = sp.PIPE if pipe else sp.DEVNULL
180+
181+
# Build the command list
182+
commands = self.get_base_command(args)
183+
packed_commands = self.pack_commands(commands, shell, quote)
184+
185+
# Sink inputs to DEVNULL to stop the child process stealing keyboard
186+
# Sink outputs to DEVNULL to stop full output buffers blocking child
187+
process = sp.Popen(packed_commands, text=text, shell=shell,
188+
stdin=sp.DEVNULL,
189+
stdout=output,
190+
stderr=sp.DEVNULL)
191+
192+
# Return the output process a user can use to wait, if needed.
193+
return process
194+
195+
def adb_runas(self, *args: str, text: bool = True, shell: bool = False,
196+
quote: bool = False, check: bool = True) -> str:
197+
'''
198+
Call adb to synchronously run a device shell command as the package
199+
user, check its result, and capture its output if successful.
200+
201+
Commands can invoke adb directly, or via the host shell if invoked with
202+
shell=True. When using shell=True on Unix hosts the arguments are
203+
always quoted unless the argument is a '>' redirect shell argument. On
204+
Windows beware of the security implications of the lack of quoting.
205+
206+
Args:
207+
*args: List of command line parameters.
208+
text: True if output is text, False if binary
209+
shell: True if this should invoke via host shell, False if direct.
210+
quote: True if arguments are quoted, False if unquoted.
211+
check: True if result is checked, False if ignored.
212+
213+
Returns:
214+
The stdout response written by adb.
215+
216+
Raises:
217+
CalledProcessError: The invoked call failed.
218+
'''
219+
assert self.package, 'Cannot use run-as without a package'
220+
221+
# Build the command list
222+
commands = self.get_base_command(['shell', 'run-as', self.package])
223+
commands.extend(args)
224+
packed_commands = self.pack_commands(commands, shell, quote)
225+
226+
# Invoke the command
227+
rep = sp.run(packed_commands, check=check, shell=shell, text=text,
228+
stdin=sp.DEVNULL,
229+
stdout=sp.PIPE,
230+
stderr=sp.PIPE)
231+
232+
# Return the output
233+
return rep.stdout

0 commit comments

Comments
 (0)