Skip to content

Commit

Permalink
Add remaining Android filesystem helpers
Browse files Browse the repository at this point in the history
  • Loading branch information
solidpixel committed Jan 6, 2025
1 parent 9b49b3a commit c250922
Show file tree
Hide file tree
Showing 4 changed files with 321 additions and 30 deletions.
61 changes: 60 additions & 1 deletion lglpy/android/adb.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,26 @@ def __init__(self, device: Optional[str] = None,
self.device = device
self.package = package

def set_device(self, device: str) -> None:
'''
Set the device for this connection.
Args:
device: The device identifier, as returned by 'adb devices', or
None for non-specific use.
'''
self.device = device

def set_package(self, package: str) -> None:
'''
Set the package for this connection.
Args:
package: The package name, as returned by `adb shell pm list
packages` or None for non-specific use.
'''
self.package = package

def get_base_command(self, args: Iterable[str]) -> list[str]:
'''
Get the root of an adb command, injecting device selector if needed.
Expand Down Expand Up @@ -194,6 +214,44 @@ def adb_async(self, *args: str, text: bool = True, shell: bool = False,
# Return the output process a user can use to wait, if needed.
return process

def adb_run(self, *args: str, text: bool = True, shell: bool = False,
quote: bool = False, check: bool = True) -> str:
'''
Call adb to synchronously run a device shell command as the Android
"shell" user, check its result, and capture its output if successful.
Commands can invoke adb directly, or via the host shell if invoked with
shell=True. When using shell=True on Unix hosts the arguments are
always quoted unless the argument is a '>' redirect shell argument. On
Windows beware of the security implications of the lack of quoting.
Args:
*args: List of command line parameters.
text: True if output is text, False if binary
shell: True if this should invoke via host shell, False if direct.
quote: True if arguments are quoted, False if unquoted.
check: True if result is checked, False if ignored.
Returns:
The stdout response written by adb.
Raises:
CalledProcessError: The invoked call failed.
'''
# Build the command list
commands = self.get_base_command(['shell'])
commands.extend(args)
packed_commands = self.pack_commands(commands, shell, quote)

# Invoke the command
rep = sp.run(packed_commands, check=check, shell=shell, text=text,
stdin=sp.DEVNULL,
stdout=sp.PIPE,
stderr=sp.PIPE)

# Return the output
return rep.stdout

def adb_runas(self, *args: str, text: bool = True, shell: bool = False,
quote: bool = False, check: bool = True) -> str:
'''
Expand All @@ -218,7 +276,8 @@ def adb_runas(self, *args: str, text: bool = True, shell: bool = False,
Raises:
CalledProcessError: The invoked call failed.
'''
assert self.package, 'Cannot use run-as without a package'
assert self.package, \
'Cannot use adb_runas() without package'

# Build the command list
commands = self.get_base_command(['shell', 'run-as', self.package])
Expand Down
103 changes: 86 additions & 17 deletions lglpy/android/filesystem.py
Original file line number Diff line number Diff line change
Expand Up @@ -108,14 +108,16 @@ def pull_file_from_tmp(
conn.adb('pull', device_path, host_dir)

if delete:
cls.delete_file_in_tmp(conn, file_name)
cls.delete_file_from_tmp(conn, file_name)
except sp.CalledProcessError:
return False

return True

@classmethod
def delete_file_in_tmp(cls, conn: ADBConnect, file_name: str) -> bool:
def delete_file_from_tmp(
cls, conn: ADBConnect, file_name: str,
error_ok: bool = False) -> bool:
'''
Delete a file from the device temp directory.
Expand All @@ -124,59 +126,126 @@ def delete_file_in_tmp(cls, conn: ADBConnect, file_name: str) -> bool:
Args:
conn: The adb connection.
file_name: The name of the file to delete.
error_ok: Ignore errors if the file doesn't exist.
Returns:
True if the file was deleted, False otherwise.
'''
device_path = posixpath.join(cls.TEMP_DIR, file_name)

try:
# Remove old file to prevent false success
conn.adb('shell', 'rm', '-f', device_path)
if error_ok:
conn.adb('shell', 'rm', '-f', device_path)
else:
conn.adb('shell', 'rm', device_path)
except sp.CalledProcessError:
return False

return True

@staticmethod
@classmethod
def push_file_to_package(
conn: ADBConnect, package: str, host_path: str,
cls, conn: ADBConnect, host_path: str,
executable: bool = False) -> bool:
'''
Push a file to the package data directory.
Push a file to the connection package directory.
File will be copied to, e.g.: /data/user/0/<package>/<file>
Args:
conn: The adb connection.
package: The name of the package.
host_path: The path of the file on the host file system.
executable: True if the file should be configured as executable.
Returns:
True if the file was copied, False otherwise.
'''
# TODO
return False
assert conn.package, \
'Cannot use push_file_to_package() without package'

# Determine the paths that we need
file_name = os.path.basename(host_path)
tmp_path = posixpath.join(cls.TEMP_DIR, file_name)

# Copy file to the temp directory
success = cls.push_file_to_tmp(conn, host_path, executable)
if not success:
return False

@staticmethod
# Copy file to the package directory
try:
conn.adb_runas('cp', tmp_path, '.')
except sp.CalledProcessError:
return False

# Delete the temp file copy
cls.delete_file_from_tmp(conn, file_name)

return True

@classmethod
def pull_file_from_package(
conn: ADBConnect, package: str,
src_file: str, host_dir: str) -> bool:
cls, conn: ADBConnect, src_file: str, host_dir: str,
delete: bool = False) -> bool:
'''
Pull a file from the package data directory to a host directory.
Pull a file from the connection package directory to a host directory.
File will be copied to: <host_dir>/<file>.
Args:
conn: The adb connection.
package: The name of the package.
src_file: The name of the file in the tmp directory.
host_path: The destination directory on the host file system.
Host directory will be created if it doesn't exist.
delete: Delete the file on the device after copying it.
Returns:
True if the file was copied, False otherwise.
'''
# TODO
return False
assert conn.package, \
'Cannot use pull_file_from_package() without package'

host_dir = os.path.abspath(host_dir)
os.makedirs(host_dir, exist_ok=True)

# You cannot adb pull from a package, even if it's debuggable, so
# this is the non-obvious solution ...
host_file = os.path.join(host_dir, src_file)
try:
conn.adb('exec-out', 'run-as', conn.package,
'cat', src_file, '>', host_file,
text=False, shell=True)

if delete:
cls.delete_file_from_package(conn, src_file)
except sp.SubprocessError:
return False

return True

@classmethod
def delete_file_from_package(
cls, conn: ADBConnect, file_name: str,
error_ok: bool = False) -> bool:
'''
Delete a file from the package directory.
File will be deleted from, e.g.: /data/user/0/<package>/<file>
Args:
conn: The adb connection.
file_name: The name of the file to delete.
error_ok: Ignore errors if the file doesn't exist.
Returns:
True if the file was deleted, False otherwise.
'''
try:
if error_ok:
conn.adb_runas('rm', '-f', file_name)
else:
conn.adb_runas('rm', file_name)
except sp.CalledProcessError:
return False

return True
Loading

0 comments on commit c250922

Please sign in to comment.