diff --git a/Xamarin.Android.sln b/Xamarin.Android.sln
index 480b25a4ca3..8dcbe7a5396 100644
--- a/Xamarin.Android.sln
+++ b/Xamarin.Android.sln
@@ -156,6 +156,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Java.Runtime.Environment",
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "create-android-api", "build-tools\create-android-api\create-android-api.csproj", "{BA4D889D-066B-4C2C-A973-09E319CBC396}"
EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "debug-session-prep", "tools\debug-session-prep\debug-session-prep.csproj", "{087C42C4-6B45-4020-AB39-52515265082E}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "xadebug", "tools\xadebug\xadebug.csproj", "{F4D105FA-D394-437A-B3BA-0EC56C6734C3}"
+EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Xamarin.Android.Tools.Aidl-Tests", "tests\Xamarin.Android.Tools.Aidl-Tests\Xamarin.Android.Tools.Aidl-Tests.csproj", "{A39B6D7C-6616-40D6-8AE4-C6CEE93D2708}"
EndProject
Global
@@ -300,12 +304,10 @@ Global
{B8105878-D423-4159-A3E7-028298281EC6}.Debug|AnyCPU.Build.0 = Debug|Any CPU
{B8105878-D423-4159-A3E7-028298281EC6}.Release|AnyCPU.ActiveCfg = Release|Any CPU
{B8105878-D423-4159-A3E7-028298281EC6}.Release|AnyCPU.Build.0 = Release|Any CPU
-
{43564FB3-0F79-4FF4-A2B0-B1637072FF01}.Debug|AnyCPU.ActiveCfg = Debug|Any CPU
{43564FB3-0F79-4FF4-A2B0-B1637072FF01}.Debug|AnyCPU.Build.0 = Debug|Any CPU
{43564FB3-0F79-4FF4-A2B0-B1637072FF01}.Release|AnyCPU.ActiveCfg = Release|Any CPU
{43564FB3-0F79-4FF4-A2B0-B1637072FF01}.Release|AnyCPU.Build.0 = Release|Any CPU
-
{3DE17662-DCD6-4F49-AF06-D39AACC8649A}.Debug|AnyCPU.ActiveCfg = Debug|Any CPU
{3DE17662-DCD6-4F49-AF06-D39AACC8649A}.Debug|AnyCPU.Build.0 = Debug|Any CPU
{3DE17662-DCD6-4F49-AF06-D39AACC8649A}.Release|AnyCPU.ActiveCfg = Release|Any CPU
@@ -418,6 +420,10 @@ Global
{D8E14B43-E929-4C18-9FA6-2C3DC47EFC17}.Debug|AnyCPU.Build.0 = Debug|Any CPU
{D8E14B43-E929-4C18-9FA6-2C3DC47EFC17}.Release|AnyCPU.ActiveCfg = Release|Any CPU
{D8E14B43-E929-4C18-9FA6-2C3DC47EFC17}.Release|AnyCPU.Build.0 = Release|Any CPU
+ {087C42C4-6B45-4020-AB39-52515265082E}.Debug|AnyCPU.ActiveCfg = Debug|Any CPU
+ {087C42C4-6B45-4020-AB39-52515265082E}.Debug|AnyCPU.Build.0 = Debug|Any CPU
+ {087C42C4-6B45-4020-AB39-52515265082E}.Release|AnyCPU.ActiveCfg = Release|Any CPU
+ {087C42C4-6B45-4020-AB39-52515265082E}.Release|AnyCPU.Build.0 = Release|Any CPU
{C0E44558-FEE3-4DD3-986A-3F46DD1BF41B}.Debug|AnyCPU.ActiveCfg = Debug|Any CPU
{C0E44558-FEE3-4DD3-986A-3F46DD1BF41B}.Debug|AnyCPU.Build.0 = Debug|Any CPU
{C0E44558-FEE3-4DD3-986A-3F46DD1BF41B}.Release|AnyCPU.ActiveCfg = Release|Any CPU
@@ -426,6 +432,10 @@ Global
{BA4D889D-066B-4C2C-A973-09E319CBC396}.Debug|AnyCPU.Build.0 = Debug|Any CPU
{BA4D889D-066B-4C2C-A973-09E319CBC396}.Release|AnyCPU.ActiveCfg = Release|Any CPU
{BA4D889D-066B-4C2C-A973-09E319CBC396}.Release|AnyCPU.Build.0 = Release|Any CPU
+ {F4D105FA-D394-437A-B3BA-0EC56C6734C3}.Debug|AnyCPU.ActiveCfg = Debug|Any CPU
+ {F4D105FA-D394-437A-B3BA-0EC56C6734C3}.Debug|AnyCPU.Build.0 = Debug|Any CPU
+ {F4D105FA-D394-437A-B3BA-0EC56C6734C3}.Release|AnyCPU.ActiveCfg = Release|Any CPU
+ {F4D105FA-D394-437A-B3BA-0EC56C6734C3}.Release|AnyCPU.Build.0 = Release|Any CPU
{A39B6D7C-6616-40D6-8AE4-C6CEE93D2708}.Debug|AnyCPU.ActiveCfg = Debug|Any CPU
{A39B6D7C-6616-40D6-8AE4-C6CEE93D2708}.Debug|AnyCPU.Build.0 = Debug|Any CPU
{A39B6D7C-6616-40D6-8AE4-C6CEE93D2708}.Release|AnyCPU.ActiveCfg = Release|Any CPU
@@ -499,8 +509,10 @@ Global
{DA50FC92-7FE7-48B5-BDB6-CDA57B37BB51} = {864062D3-A415-4A6F-9324-5820237BA058}
{4EFCED6E-9A6B-453A-94E4-CE4B736EC684} = {864062D3-A415-4A6F-9324-5820237BA058}
{D8E14B43-E929-4C18-9FA6-2C3DC47EFC17} = {864062D3-A415-4A6F-9324-5820237BA058}
+ {087C42C4-6B45-4020-AB39-52515265082E} = {864062D3-A415-4A6F-9324-5820237BA058}
{C0E44558-FEE3-4DD3-986A-3F46DD1BF41B} = {04E3E11E-B47D-4599-8AFC-50515A95E715}
{BA4D889D-066B-4C2C-A973-09E319CBC396} = {E351F97D-EA4F-4E7F-AAA0-8EBB1F2A4A62}
+ {F4D105FA-D394-437A-B3BA-0EC56C6734C3} = {864062D3-A415-4A6F-9324-5820237BA058}
{A39B6D7C-6616-40D6-8AE4-C6CEE93D2708} = {CAB438D8-B0F5-4AF0-BEBD-9E2ADBD7B483}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
diff --git a/build-tools/create-packs/Microsoft.Android.Sdk.proj b/build-tools/create-packs/Microsoft.Android.Sdk.proj
index 7deb4a8975a..957a493d033 100644
--- a/build-tools/create-packs/Microsoft.Android.Sdk.proj
+++ b/build-tools/create-packs/Microsoft.Android.Sdk.proj
@@ -51,6 +51,10 @@ core workload SDK packs imported by WorkloadManifest.targets.
<_PackageFiles Include="$(MicrosoftAndroidSdkOutDir)class-parse.dll" PackagePath="tools" />
<_PackageFiles Include="$(MicrosoftAndroidSdkOutDir)class-parse.pdb" PackagePath="tools" />
<_PackageFiles Include="$(MicrosoftAndroidSdkOutDir)class-parse.runtimeconfig.json" PackagePath="tools" />
+ <_PackageFiles Include="$(MicrosoftAndroidSdkOutDir)debug-session-prep.deps.json" PackagePath="tools" />
+ <_PackageFiles Include="$(MicrosoftAndroidSdkOutDir)debug-session-prep.dll" PackagePath="tools" />
+ <_PackageFiles Include="$(MicrosoftAndroidSdkOutDir)debug-session-prep.pdb" PackagePath="tools" />
+ <_PackageFiles Include="$(MicrosoftAndroidSdkOutDir)debug-session-prep.runtimeconfig.json" PackagePath="tools" />
<_PackageFiles Include="$(MicrosoftAndroidSdkOutDir)generator.dll" PackagePath="tools" />
<_PackageFiles Include="$(MicrosoftAndroidSdkOutDir)generator.pdb" PackagePath="tools" />
<_PackageFiles Include="$(MicrosoftAndroidSdkOutDir)generator.runtimeconfig.json" PackagePath="tools" />
diff --git a/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.NativeProfilingAndDebugging.targets b/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.NativeProfilingAndDebugging.targets
new file mode 100644
index 00000000000..71c41d8d788
--- /dev/null
+++ b/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.NativeProfilingAndDebugging.targets
@@ -0,0 +1,88 @@
+
+
+
+
+
+
+
+
+ <_AndroidEnableNativeCodeProfiling>False
+ <_AndroidEnableNativeDebugging>False
+
+
+ _ResolveMonoAndroidSdks;
+ _SetupNativeCodeProfiling;
+ Build;
+ Install;
+
+
+
+ _ResolveMonoAndroidSdks;
+ _SetupNativeCodeDebugging;
+ Build;
+ Install;
+
+
+
+
+
+ <_AndroidEnableNativeCodeProfiling>True
+ <_AndroidAotStripLibraries>False
+ <_AndroidStripNativeLibraries>False
+
+
+ apk
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ <_AndroidEnableNativeDebugging>True
+ <_AndroidAotStripLibraries>False
+ <_AndroidStripNativeLibraries>False
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Xamarin.Android.Build.Tasks/Resources/debug-app.ps1 b/src/Xamarin.Android.Build.Tasks/Resources/debug-app.ps1
new file mode 100644
index 00000000000..b12418a1581
--- /dev/null
+++ b/src/Xamarin.Android.Build.Tasks/Resources/debug-app.ps1
@@ -0,0 +1,9 @@
+#
+# How to read vars from a file
+#
+# $vars = Get-Content -Raw ./settings.txt | ConvertFrom-StringData
+#
+# Access to them:
+#
+# $vars.one
+# write-host "$($vars.one)"
diff --git a/src/Xamarin.Android.Build.Tasks/Resources/debug-app.sh b/src/Xamarin.Android.Build.Tasks/Resources/debug-app.sh
new file mode 100755
index 00000000000..fcc0b4e1ea2
--- /dev/null
+++ b/src/Xamarin.Android.Build.Tasks/Resources/debug-app.sh
@@ -0,0 +1,154 @@
+#!/bin/bash
+MY_DIR="$(cd $(dirname $0);pwd)"
+
+ADB_PATH="@ADB_PATH@"
+APP_LIBS_DIR="@APP_LIBS_DIR@"
+CONFIG_SCRIPT_NAME="@CONFIG_SCRIPT_NAME@"
+DEBUG_SESSION_PREP_PATH="@DEBUG_SESSION_PREP_PATH@"
+DEFAULT_ACTIVITY_NAME="@ACTIVITY_NAME@"
+LLDB_SCRIPT_NAME="@LLDB_SCRIPT_NAME@"
+NDK_DIR="@NDK_DIR@"
+OUTPUT_DIR="@OUTPUT_DIR@"
+PACKAGE_NAME="@PACKAGE_NAME@"
+SUPPORTED_ABIS_ARRAY=(@SUPPORTED_ABIS@)
+ADB_DEVICE=""
+
+function die()
+{
+ echo "$@"
+ exit 1
+}
+
+function die_if_failed()
+{
+ if [ $? -ne 0 ]; then
+ die "$@"
+ fi
+}
+
+function run_adb_no_echo()
+{
+ local args=""
+
+ if [ -n "${ADB_DEVICE}" ]; then
+ args="-s ${ADB_DEVICE}"
+ fi
+
+ adb ${args} "$@"
+}
+
+function run_adb()
+{
+ echo Running: adb ${args} "$@"
+ run_adb_no_echo "$@"
+}
+
+function run_adb_echo_info()
+{
+ local log_file="$1"
+ shift
+
+ echo Running: adb ${args} "$@"
+ echo Logging to: "${log_file}"
+ echo
+}
+
+function run_adb_with_log()
+{
+ local log_file="$1"
+ shift
+
+ run_adb_echo_info "${log_file}" "$@"
+ run_adb_no_echo "$@" > "${log_file}" 2>&1
+}
+
+function run_adb_with_log_bg()
+{
+ local log_file="$1"
+ shift
+
+ run_adb_echo_info "${log_file}" "$@"
+ run_adb_no_echo "$@" > "${log_file}" 2>&1 &
+}
+
+#TODO: APP_LIBS_DIR needs to be appended the abi-specific subdir
+#TODO: make NDK_DIR overridable via a parameter
+#TOOD: add a parameter to specify the Android device to target
+#TODO: detect whether we have dotnet in $PATH and whether it's a compatible version
+#TODO: add an option to make XA wait for debugger to connect
+#TODO: add a parameter to specify activity to start
+
+ACTIVITY_NAME="${DEFAULT_ACTIVITY_NAME}"
+
+SUPPORTED_ABIS_ARG=""
+for sa in "${SUPPORTED_ABIS_ARRAY[@]}"; do
+ if [ -z "${SUPPORTED_ABIS_ARG}" ]; then
+ SUPPORTED_ABIS_ARG="${sa}"
+ else
+ SUPPORTED_ABIS_ARG="${SUPPORTED_ABIS_ARG},${sa}"
+ fi
+done
+
+dotnet "${DEBUG_SESSION_PREP_PATH}" \
+ -s "${SUPPORTED_ABIS_ARG}" \
+ -p "${PACKAGE_NAME}" \
+ -n "${NDK_DIR}" \
+ -o "${OUTPUT_DIR}" \
+ -l "${APP_LIBS_DIR}" \
+ -c "${CONFIG_SCRIPT_NAME}" \
+ -g "${LLDB_SCRIPT_NAME}"
+
+die_if_failed Debug preparation app failed
+
+CONFIG_SCRIPT_PATH="${OUTPUT_DIR}/${CONFIG_SCRIPT_NAME}"
+if [ ! -f "${CONFIG_SCRIPT_PATH}" ]; then
+ die Config script ${CONFIG_SCRIPT_PATH} not found
+fi
+
+source "${CONFIG_SCRIPT_PATH}"
+
+# Determine cross section of supported and device ABIs
+ALLOWED_ABIS=()
+
+for dabi in "${DEVICE_AVAILABLE_ABIS[@]}"; do
+ for sabi in "${SUPPORTED_ABIS_ARRAY[@]}"; do
+ if [ "${dabi}" == "${sabi}" ]; then
+ ALLOWED_ABIS+="${dabi}"
+ fi
+ done
+done
+
+if [ ${#ALLOWED_ABIS[@]} -le 0 ]; then
+ die Application does not support any ABIs available on device
+fi
+
+ADB_DEBUG_SERVER_LOG="${OUTPUT_DIR}/lldb-debug-server.log"
+echo Starting debug server on device
+echo stdout and stderr will be redirected to: ${ADB_DEBUG_SERVER_LOG}
+
+LLDB_SERVER_PLATFORM_LOG="${DEVICE_LLDB_DIR}/log/platform.log"
+LLDB_SERVER_STDOUT_LOG="${DEVICE_LLDB_DIR}/log/platform-stdout.log"
+LLDB_SERVER_GDB_LOG="${DEVICE_LLDB_DIR}/log/gdb-server.log"
+
+run_adb shell run-as ${PACKAGE_NAME} kill -9 "\"\`pidof ${DEVICE_DEBUG_SERVER_LAUNCHER}\`\""
+run_adb_with_log_bg "${ADB_DEBUG_SERVER_LOG}" shell run-as ${PACKAGE_NAME} ${DEVICE_DEBUG_SERVER_LAUNCHER} ${DEVICE_LLDB_DIR} ${SOCKET_SCHEME} ${SOCKET_DIR} ${SOCKET_NAME} "\"lldb process:gdb-remote packets\""
+
+LAUNCH_SPEC=${PACKAGE_NAME}/${ACTIVITY_NAME}
+run_adb shell am start -S -W ${LAUNCH_SPEC}
+die_if_failed Failed to start ${LAUNCH_SPEC}
+
+APP_PID=$(run_adb_no_echo shell pidof ${PACKAGE_NAME})
+die_if_failed Failed to get ${PACKAGE_NAME} PID on device
+
+LLDB_SCRIPT_PATH="${OUTPUT_DIR}/${LLDB_SCRIPT_NAME}"
+echo App PID: ${APP_PID}
+echo "attach ${APP_PID}" >> "${LLDB_SCRIPT_PATH}"
+
+#TODO: start the app if not running
+#TODO: pass app pid to lldb, with -p or --attach-pid
+export TERMINFO=/usr/share/terminfo
+"${LLDB_PATH}" --source "${LLDB_SCRIPT_PATH}"
+
+run_adb_with_log "${OUTPUT_DIR}/lldb-platform.log" shell run-as ${PACKAGE_NAME} cat ${LLDB_SERVER_PLATFORM_LOG}
+run_adb_with_log "${OUTPUT_DIR}/lldb-platform-stdout.log" shell run-as ${PACKAGE_NAME} cat ${LLDB_SERVER_STDOUT_LOG}
+run_adb_with_log "${OUTPUT_DIR}/lldb-gdb-server.log" shell run-as ${PACKAGE_NAME} cat ${LLDB_SERVER_GDB_LOG}
diff --git a/src/Xamarin.Android.Build.Tasks/Tasks/BuildApk.cs b/src/Xamarin.Android.Build.Tasks/Tasks/BuildApk.cs
index 3b9b763d3af..95c2971af66 100644
--- a/src/Xamarin.Android.Build.Tasks/Tasks/BuildApk.cs
+++ b/src/Xamarin.Android.Build.Tasks/Tasks/BuildApk.cs
@@ -95,6 +95,10 @@ public class BuildApk : AndroidTask
public bool UseAssemblyStore { get; set; }
+ public bool NativeCodeProfilingEnabled { get; set; }
+
+ public string DeviceSdkVersion { get; set; }
+
[Required]
public string ProjectFullPath { get; set; }
@@ -192,6 +196,7 @@ void ExecuteWithAbi (string [] supportedAbis, string apkInputPath, string apkOut
}
AddRuntimeLibraries (apk, supportedAbis);
+ AddProfilingScripts (apk, supportedAbis);
apk.Flush();
AddNativeLibraries (files, supportedAbis);
AddAdditionalNativeLibraries (files, supportedAbis);
@@ -621,9 +626,11 @@ CompressionMethod GetCompressionMethod (string fileName)
return CompressionMethod.Default;
}
+ string GetArchiveAbiLibPath (string abi, string fileName) => $"lib/{abi}/{fileName}";
+
void AddNativeLibraryToArchive (ZipArchiveEx apk, string abi, string filesystemPath, string inArchiveFileName)
{
- string archivePath = $"lib/{abi}/{inArchiveFileName}";
+ string archivePath = GetArchiveAbiLibPath (abi, inArchiveFileName);
existingEntries.Remove (archivePath);
CompressionMethod compressionMethod = GetCompressionMethod (archivePath);
if (apk.SkipExistingFile (filesystemPath, archivePath, compressionMethod)) {
@@ -634,6 +641,26 @@ void AddNativeLibraryToArchive (ZipArchiveEx apk, string abi, string filesystemP
apk.Archive.AddEntry (archivePath, File.OpenRead (filesystemPath), compressionMethod);
}
+ void AddProfilingScripts (ZipArchiveEx apk, string [] supportedAbis)
+ {
+ if (!NativeCodeProfilingEnabled || !Int32.TryParse (DeviceSdkVersion, out int sdkVersion) || sdkVersion < 26) {
+ // wrap.sh is available on Android O (API 26) or newer
+ return;
+ }
+
+ string wrapScript = "#!/system/bin/sh\n$@\n";
+ byte[] wrapScriptBytes = new UTF8Encoding (false).GetBytes (wrapScript);
+ EntryPermissions wrapScriptPermissions =
+ EntryPermissions.WorldRead | EntryPermissions.WorldExecute |
+ EntryPermissions.GroupRead | EntryPermissions.GroupExecute |
+ EntryPermissions.OwnerRead | EntryPermissions.OwnerWrite | EntryPermissions.OwnerExecute;
+
+ foreach (var abi in supportedAbis) {
+ string path = GetArchiveAbiLibPath (abi, "wrap.sh");
+ apk.Archive.AddEntry (wrapScriptBytes, path, wrapScriptPermissions, CompressionMethod.Default);
+ }
+ }
+
void AddRuntimeLibraries (ZipArchiveEx apk, string [] supportedAbis)
{
foreach (var abi in supportedAbis) {
@@ -807,7 +834,7 @@ private void AddAdditionalNativeLibraries (ArchiveFileList files, string [] supp
void AddNativeLibrary (ArchiveFileList files, string path, string abi, string archiveFileName)
{
string fileName = string.IsNullOrEmpty (archiveFileName) ? Path.GetFileName (path) : archiveFileName;
- var item = (filePath: path, archivePath: $"lib/{abi}/{fileName}");
+ var item = (filePath: path, archivePath: GetArchiveAbiLibPath (abi, fileName));
if (files.Any (x => x.archivePath == item.archivePath)) {
Log.LogCodedWarning ("XA4301", path, 0, Properties.Resources.XA4301, item.archivePath);
return;
diff --git a/src/Xamarin.Android.Build.Tasks/Tasks/CreateTypeManagerJava.cs b/src/Xamarin.Android.Build.Tasks/Tasks/CreateTypeManagerJava.cs
index 37872f31927..6b7fa4ac594 100644
--- a/src/Xamarin.Android.Build.Tasks/Tasks/CreateTypeManagerJava.cs
+++ b/src/Xamarin.Android.Build.Tasks/Tasks/CreateTypeManagerJava.cs
@@ -1,5 +1,4 @@
using System.IO;
-using System.Reflection;
using System.Text;
using System;
@@ -18,11 +17,9 @@ public class CreateTypeManagerJava : AndroidTask
[Required]
public string OutputFilePath { get; set; }
- static readonly Assembly ExecutingAssembly = Assembly.GetExecutingAssembly ();
-
public override bool RunTask ()
{
- string? content = ReadResource (ResourceName);
+ string? content = MonoAndroidHelper.ReadManifestResource (Log, ResourceName);
if (String.IsNullOrEmpty (content)) {
return false;
@@ -61,19 +58,5 @@ public override bool RunTask ()
return !Log.HasLoggedErrors;
}
-
- string? ReadResource (string resourceName)
- {
- using (var from = ExecutingAssembly.GetManifestResourceStream (resourceName)) {
- if (from == null) {
- Log.LogCodedError ("XA0116", Properties.Resources.XA0116, resourceName);
- return null;
- }
-
- using (var sr = new StreamReader (from)) {
- return sr.ReadToEnd ();
- }
- }
- }
}
}
diff --git a/src/Xamarin.Android.Build.Tasks/Tasks/DebugNativeCode.cs b/src/Xamarin.Android.Build.Tasks/Tasks/DebugNativeCode.cs
new file mode 100644
index 00000000000..bc8bdb1c89d
--- /dev/null
+++ b/src/Xamarin.Android.Build.Tasks/Tasks/DebugNativeCode.cs
@@ -0,0 +1,77 @@
+using System;
+using System.Collections.Generic;
+
+using Microsoft.Android.Build.Tasks;
+using Microsoft.Build.Framework;
+
+namespace Xamarin.Android.Tasks
+{
+ public class DebugNativeCode : AndroidAsyncTask
+ {
+ public override string TaskPrefix => "DNC";
+
+ [Required]
+ public string AndroidNdkPath { get; set; }
+
+ [Required]
+ public string PackageName { get; set; }
+
+ [Required]
+ public string[] SupportedAbis { get; set; }
+
+ [Required]
+ public string AdbPath { get; set; }
+
+ [Required]
+ public string MainActivityName { get; set; }
+
+ [Required]
+ public string IntermediateOutputDir { get; set; }
+
+ [Required]
+ public ITaskItem[] NativeLibraries { get; set; }
+
+ public string ActivityName { get; set; }
+ public string TargetDeviceName { get; set; }
+
+ public async override System.Threading.Tasks.Task RunTaskAsync ()
+ {
+ var nativeLibs = new Dictionary> (StringComparer.OrdinalIgnoreCase);
+ foreach (ITaskItem item in NativeLibraries) {
+ string? abi = null;
+ string? rid = item.GetMetadata ("RuntimeIdentifier");
+
+ if (!String.IsNullOrEmpty (rid)) {
+ abi = NdkHelper.RIDToABI (rid);
+ }
+
+ if (String.IsNullOrEmpty (abi)) {
+ abi = item.GetMetadata ("abi");
+ }
+
+ if (String.IsNullOrEmpty (abi)) {
+ Log.LogDebugMessage ($"Ignoring native library {item.ItemSpec} because it doesn't specify its ABI");
+ continue;
+ }
+
+ if (!nativeLibs.TryGetValue (abi, out List abiLibs)) {
+ abiLibs = new List ();
+ nativeLibs.Add (abi, abiLibs);
+ }
+ abiLibs.Add (item.ItemSpec);
+ }
+
+ var prep = new NativeDebugPrep (Log);
+ prep.Prepare (
+ AdbPath,
+ AndroidNdkPath,
+ String.IsNullOrEmpty (ActivityName) ? MainActivityName : ActivityName,
+ IntermediateOutputDir,
+ PackageName,
+ SupportedAbis,
+ nativeLibs,
+ TargetDeviceName
+ );
+ }
+ }
+}
diff --git a/src/Xamarin.Android.Build.Tasks/Tasks/GenerateJavaStubs.cs b/src/Xamarin.Android.Build.Tasks/Tasks/GenerateJavaStubs.cs
index e1c36a308d8..633d3246db2 100644
--- a/src/Xamarin.Android.Build.Tasks/Tasks/GenerateJavaStubs.cs
+++ b/src/Xamarin.Android.Build.Tasks/Tasks/GenerateJavaStubs.cs
@@ -92,6 +92,11 @@ public class GenerateJavaStubs : AndroidTask
public ITaskItem[] Environments { get; set; }
+ public bool NativeCodeProfilingEnabled { get; set; }
+ public string DeviceSdkVersion { get; set; }
+
+ public bool NativeDebuggingEnabled { get; set; }
+
[Output]
public string [] GeneratedBinaryTypeMaps { get; set; }
@@ -358,11 +363,22 @@ void Run (DirectoryAssemblyResolver res, bool useMarshalMethods)
}
manifest.Assemblies.AddRange (userAssemblies.Values);
- if (!String.IsNullOrWhiteSpace (CheckedBuild)) {
+ bool checkedBuild = !String.IsNullOrWhiteSpace (CheckedBuild);
+ if (NativeCodeProfilingEnabled || NativeDebuggingEnabled || checkedBuild) {
// We don't validate CheckedBuild value here, this will be done in BuildApk. We just know that if it's
// on then we need android:debuggable=true and android:extractNativeLibs=true
+ //
+ // For profiling we only need android:debuggable=true
+ //
manifest.ForceDebuggable = true;
- manifest.ForceExtractNativeLibs = true;
+ if (checkedBuild || NativeDebuggingEnabled) {
+ manifest.ForceExtractNativeLibs = true;
+ }
+
+ // is supported on Android Q (API 29) or newer
+ if (NativeCodeProfilingEnabled && Int32.TryParse (DeviceSdkVersion, out int sdkVersion) && sdkVersion >= 29) {
+ manifest.Profileable = true;
+ }
}
var additionalProviders = manifest.Merge (Log, cache, allJavaTypes, ApplicationJavaClass, EmbedAssemblies, BundledWearApplicationName, MergedManifestDocuments);
diff --git a/src/Xamarin.Android.Build.Tasks/Tasks/LinkApplicationSharedLibraries.cs b/src/Xamarin.Android.Build.Tasks/Tasks/LinkApplicationSharedLibraries.cs
index bbe49073e8c..fec20276ae2 100644
--- a/src/Xamarin.Android.Build.Tasks/Tasks/LinkApplicationSharedLibraries.cs
+++ b/src/Xamarin.Android.Build.Tasks/Tasks/LinkApplicationSharedLibraries.cs
@@ -38,7 +38,7 @@ sealed class InputFiles
public ITaskItem[] ApplicationSharedLibraries { get; set; }
[Required]
- public bool DebugBuild { get; set; }
+ public bool KeepDebugInfo { get; set; }
[Required]
public string AndroidBinUtilsDirectory { get; set; }
@@ -132,7 +132,7 @@ IEnumerable GetLinkerConfigs ()
"--warn-shared-textrel " +
"--fatal-warnings";
- string stripSymbolsArg = DebugBuild ? String.Empty : " -s";
+ string stripSymbolsArg = KeepDebugInfo ? String.Empty : " -s";
string ld = Path.Combine (AndroidBinUtilsDirectory, MonoAndroidHelper.GetExecutablePath (AndroidBinUtilsDirectory, "ld"));
var targetLinkerArgs = new List ();
diff --git a/src/Xamarin.Android.Build.Tasks/Tasks/ProfileNativeCode.cs b/src/Xamarin.Android.Build.Tasks/Tasks/ProfileNativeCode.cs
new file mode 100644
index 00000000000..eb3d9142f85
--- /dev/null
+++ b/src/Xamarin.Android.Build.Tasks/Tasks/ProfileNativeCode.cs
@@ -0,0 +1,111 @@
+using System;
+using System.IO;
+
+using Microsoft.Android.Build.Tasks;
+using Microsoft.Build.Framework;
+using Xamarin.Android.Tools;
+
+namespace Xamarin.Android.Tasks
+{
+ public class ProfileNativeCode : AndroidAsyncTask
+ {
+ const string PythonName = "python3";
+
+ public override string TaskPrefix => "PNC";
+
+ string[]? pathExt;
+
+ public string DeviceSdkVersion { get; set; }
+ public bool DeviceIsEmulator { get; set; }
+ public string[] DeviceSupportedAbis { get; set; }
+ public string DevicePrimaryABI { get; set; }
+
+ [Required]
+ public string SimplePerfDirectory { get; set; }
+ public string NdkPythonDirectory { get; set; }
+
+ public async override System.Threading.Tasks.Task RunTaskAsync ()
+ {
+ string? pythonPath = null;
+
+ if (!String.IsNullOrEmpty (NdkPythonDirectory)) {
+ pythonPath = Path.Combine (NdkPythonDirectory, "bin", PythonName);
+ if (!File.Exists (pythonPath)) {
+ pythonPath = null;
+ }
+ }
+
+ if (String.IsNullOrEmpty (pythonPath)) {
+ Log.LogWarning ($"NDK {PythonName} not found, will attempt to find one in a system location");
+ pythonPath = FindPython ();
+ if (String.IsNullOrEmpty (pythonPath)) {
+ Log.LogWarning ($"System {PythonName} not found, will attempt to use executable name without path");
+ pythonPath = PythonName;
+ }
+ }
+
+ string? appProfilerScript = Path.Combine (SimplePerfDirectory, "app_profiler.py");
+ if (!File.Exists (appProfilerScript)) {
+ Log.LogError ($"Profiling script {appProfilerScript} not found");
+ return;
+ }
+
+ // TODO: prepare a directory with unstripped native libraries (for use with the profiler's -lib argument)
+ Console.WriteLine ($"python3 path: {pythonPath}");
+ Console.WriteLine ($"profiler script path: {appProfilerScript}");
+
+ var python = new PythonRunner (Log, pythonPath);
+
+ // TODO: params
+ bool success = await python.RunScript (appProfilerScript);
+ }
+
+ string? FindPython ()
+ {
+ // TODO: might be a good idea to try to look for `python` and check its version, if python3 isn't found
+ if (OS.IsWindows) {
+ string? envvar = Environment.GetEnvironmentVariable ("PATHEXT");
+ if (String.IsNullOrEmpty (envvar)) {
+ pathExt = new string[] { ".exe", ".bat", ".cmd" };
+ } else {
+ pathExt = envvar.Split (Path.PathSeparator);
+ }
+ }
+
+ string? pathVar = Environment.GetEnvironmentVariable ("PATH")?.Trim ();
+ if (String.IsNullOrEmpty (pathVar)) {
+ return null;
+ }
+
+ foreach (string dir in pathVar.Split (Path.PathSeparator)) {
+ string? exe = GetExecutablePath (dir, PythonName);
+ if (!String.IsNullOrEmpty (exe)) {
+ return exe;
+ }
+ }
+
+ return null;
+ }
+
+ string? GetExecutablePath (string dir, string baseExeName)
+ {
+ string exePath = Path.Combine (dir, baseExeName);
+ if (!OS.IsWindows) {
+ if (File.Exists (exePath)) {
+ return exePath;
+ }
+
+ return null;
+ }
+
+ foreach (string ext in pathExt) {
+ string exe = $"{exePath}{ext}";
+ if (File.Exists (exe)) {
+ return exe;
+ }
+ }
+
+ return null;
+ }
+ }
+}
diff --git a/src/Xamarin.Android.Build.Tasks/Tasks/SetupNativeCodeProfiling.cs b/src/Xamarin.Android.Build.Tasks/Tasks/SetupNativeCodeProfiling.cs
new file mode 100644
index 00000000000..da01314b91d
--- /dev/null
+++ b/src/Xamarin.Android.Build.Tasks/Tasks/SetupNativeCodeProfiling.cs
@@ -0,0 +1,64 @@
+using System;
+using System.IO;
+
+using Microsoft.Android.Build.Tasks;
+using Microsoft.Build.Framework;
+
+namespace Xamarin.Android.Tasks
+{
+ public class SetupNativeCodeProfiling : AndroidAsyncTask
+ {
+ public override string TaskPrefix => "SNCP";
+
+ [Required]
+ public string AdbPath { get; set; }
+
+ [Required]
+ public string AndroidNdkPath { get; set; }
+
+ public string TargetDeviceName { get; set; }
+
+ [Output]
+ public string DeviceSdkVersion { get; set; }
+
+ [Output]
+ public bool DeviceIsEmulator { get; set; }
+
+ [Output]
+ public string[] DeviceSupportedAbis { get; set; }
+
+ [Output]
+ public string DevicePrimaryABI { get; set; }
+
+ [Output]
+ public string SimplePerfDirectory { get; set; }
+
+ [Output]
+ public string NdkPythonDirectory { get; set; }
+
+ public async override System.Threading.Tasks.Task RunTaskAsync ()
+ {
+ var adi = new AndroidDeviceInfo (Log, AdbPath, TargetDeviceName);
+ await adi.Detect ();
+
+ DeviceSdkVersion = adi.DeviceSdkVersion ?? String.Empty;
+ DeviceIsEmulator = adi.DeviceIsEmulator;
+ DeviceSupportedAbis = adi.DeviceSupportedAbis ?? new string[] {};
+ DevicePrimaryABI = adi.DevicePrimaryABI ?? String.Empty;
+
+ string simplePerfPath = Path.Combine (AndroidNdkPath, "simpleperf");
+ if (Directory.Exists (simplePerfPath)) {
+ SimplePerfDirectory = simplePerfPath;
+ } else {
+ Log.LogError ($"Simpleperf directory '{simplePerfPath}' not found");
+ }
+
+ string ndkPythonPath = Path.Combine (AndroidNdkPath, "toolchains", "llvm", "prebuilt", NdkHelper.ToolchainHostName, "python3");
+ if (Directory.Exists (ndkPythonPath)) {
+ NdkPythonDirectory = ndkPythonPath;
+ } else {
+ Log.LogWarning ($"NDK Python 3 directory '{ndkPythonPath}' does not exist");
+ }
+ }
+ }
+}
diff --git a/src/Xamarin.Android.Build.Tasks/Utilities/AdbRunner.cs b/src/Xamarin.Android.Build.Tasks/Utilities/AdbRunner.cs
new file mode 100644
index 00000000000..02e7cbd3da2
--- /dev/null
+++ b/src/Xamarin.Android.Build.Tasks/Utilities/AdbRunner.cs
@@ -0,0 +1,289 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Threading.Tasks;
+
+#if NO_MSBUILD
+using LoggerType = Xamarin.Android.Utilities.XamarinLoggingHelper;
+#else // def NO_MSBUILD
+using LoggerType = Microsoft.Build.Utilities.TaskLoggingHelper;
+#endif // ndef NO_MSBUILD
+
+namespace Xamarin.Android.Tasks
+{
+ class AdbRunner : ToolRunner
+ {
+ public delegate bool OutputLineFilter (bool isStdErr, string line);
+
+ class AdbOutputSink : ToolOutputSink
+ {
+ bool isStdError;
+ OutputLineFilter? lineFilter;
+
+ public Action? LineCallback { get; set; }
+
+ public AdbOutputSink (LoggerType logger, bool isStdError, OutputLineFilter? lineFilter)
+ : base (logger)
+ {
+ this.isStdError = isStdError;
+ this.lineFilter = lineFilter;
+
+ LogLinePrefix = "adb";
+ }
+
+ public override void WriteLine (string? value)
+ {
+ if (lineFilter != null && lineFilter (isStdError, value ?? String.Empty)) {
+ return;
+ }
+
+ base.WriteLine (value);
+ LineCallback?.Invoke (isStdError, value ?? String.Empty);
+ }
+ }
+
+ class AdbStdErrorWrapper : ProcessStandardStreamWrapper
+ {
+ OutputLineFilter? lineFilter;
+
+ public AdbStdErrorWrapper (LoggerType logger, OutputLineFilter? lineFilter)
+ : base (logger)
+ {
+ this.lineFilter = lineFilter;
+ }
+
+ protected override string? PreprocessMessage (string? message, out bool ignoreLine)
+ {
+ if (lineFilter == null) {
+ return base.PreprocessMessage (message, out ignoreLine);
+ }
+
+ ignoreLine = lineFilter (isStdErr: true, line: message ?? String.Empty);
+ return message;
+ }
+ }
+
+ string[]? initialParams;
+
+ public int ExitCode { get; private set; }
+
+ public AdbRunner (LoggerType logger, string adbPath, string? deviceSerial = null)
+ : base (logger, adbPath)
+ {
+ if (!String.IsNullOrEmpty (deviceSerial)) {
+ initialParams = new string[] { "-s", deviceSerial };
+ }
+ }
+
+ public async Task<(bool success, string output)> Forward (string local, string remote)
+ {
+ var runner = CreateAdbRunner ();
+ runner.AddArgument ("forward");
+ runner.AddArgument (local);
+ runner.AddArgument (remote);
+
+ return await CaptureAdbOutput (runner);
+ }
+
+ public async Task Pull (string remotePath, string localPath)
+ {
+ var runner = CreateAdbRunner ();
+ runner.AddArgument ("pull");
+ runner.AddArgument (remotePath);
+ runner.AddArgument (localPath);
+
+ return await RunAdb (runner);
+ }
+
+ public async Task Push (string localPath, string remotePath)
+ {
+ var runner = CreateAdbRunner ();
+ runner.AddArgument ("push");
+ runner.AddArgument (localPath);
+ runner.AddArgument (remotePath);
+
+ return await RunAdb (runner);
+ }
+
+ public async Task Install (string apkPath, bool apkIsDebuggable = false, bool replaceExisting = true, bool noStreaming = true)
+ {
+ var runner = CreateAdbRunner ();
+ runner.AddArgument ("install");
+
+ if (replaceExisting) {
+ runner.AddArgument ("-r");
+ }
+
+ if (apkIsDebuggable) {
+ runner.AddArgument ("-d"); // Allow version code downgrade
+ }
+
+ if (noStreaming) {
+ runner.AddArgument ("--no-streaming");
+ }
+
+ runner.AddQuotedArgument (apkPath);
+
+ return await RunAdb (runner);
+ }
+
+ public async Task<(bool success, string output)> CreateDirectoryAs (string packageName, string directoryPath)
+ {
+ return await RunAs (packageName, "mkdir", "-p", directoryPath);
+ }
+
+ public async Task<(bool success, string output)> RunAs (string packageName, string command, params string[] args)
+ {
+ if (String.IsNullOrEmpty (packageName)) {
+ throw new ArgumentException ("must not be null or empty", nameof (packageName));
+ }
+
+ var shellArgs = new List {
+ packageName,
+ command,
+ };
+
+ if (args != null && args.Length > 0) {
+ shellArgs.AddRange (args);
+ }
+
+ return await Shell ("run-as", (IEnumerable)shellArgs, lineFilter: null);
+ }
+
+ public async Task<(bool success, string output)> GetAppDataDirectory (string packageName)
+ {
+ return await RunAs (packageName, "/system/bin/sh", "-c", "pwd");
+ }
+
+ public async Task<(bool success, string output)> Shell (string command, List args, OutputLineFilter? lineFilter = null)
+ {
+ return await Shell (command, (IEnumerable)args, lineFilter: null);
+ }
+
+ public async Task<(bool success, string output)> Shell (string command, params string[] args)
+ {
+ return await Shell (command, (IEnumerable)args, lineFilter: null);
+ }
+
+ public async Task<(bool success, string output)> Shell (OutputLineFilter lineFilter, string command, params string[] args)
+ {
+ return await Shell (command, (IEnumerable)args, lineFilter);
+ }
+
+ async Task<(bool success, string output)> Shell (string command, IEnumerable? args, OutputLineFilter? lineFilter)
+ {
+ if (String.IsNullOrEmpty (command)) {
+ throw new ArgumentException ("must not be null or empty", nameof (command));
+ }
+
+ var runner = CreateAdbRunner ();
+
+ runner.AddArgument ("shell");
+ runner.AddArgument (command);
+ if (args != null) {
+ foreach (string arg in args) {
+ runner.AddArgument (arg);
+ }
+ }
+
+ return await CaptureAdbOutput (runner, lineFilter);
+ }
+
+ public async Task<(bool success, string output)> GetPropertyValue (string propertyName)
+ {
+ var runner = CreateAdbRunner ();
+ return await GetPropertyValue (runner, propertyName);
+ }
+
+ async Task<(bool success, string output)> GetPropertyValue (ProcessRunner runner, string propertyName)
+ {
+ runner.ClearArguments ();
+ runner.ClearOutputSinks ();
+ return await Shell ("getprop", propertyName);
+ }
+
+ async Task<(bool success, string output)> CaptureAdbOutput (ProcessRunner runner, OutputLineFilter? lineFilter = null, bool firstLineOnly = false)
+ {
+ string? outputLine = null;
+ List? lines = null;
+
+ using AdbOutputSink? stderrSink = lineFilter != null ? new AdbOutputSink (Logger, isStdError: true, lineFilter: lineFilter) : null;
+ using var stdoutSink = new AdbOutputSink (Logger, isStdError: false, lineFilter: lineFilter);
+
+ SetupOutputSinks (runner, stdoutSink, stderrSink, ignoreStderr: stderrSink == null);
+ stdoutSink.LineCallback = (bool isStdErr, string line) => {
+ if (firstLineOnly) {
+ if (outputLine != null) {
+ return;
+ }
+ outputLine = line.Trim ();
+ return;
+ }
+
+ if (lines == null) {
+ lines = new List ();
+ }
+ lines.Add (line.Trim ());
+ };
+
+ ProcessStandardStreamWrapper? origStderrWrapper = runner.StandardErrorEchoWrapper;
+ using AdbStdErrorWrapper? stderrWrapper = lineFilter != null ? new AdbStdErrorWrapper (Logger, lineFilter) : null;
+
+ try {
+ runner.StandardErrorEchoWrapper = stderrWrapper;
+ if (!await RunAdb (runner, setupOutputSink: false)) {
+ return (false, FormatOutputWithLines (lines));
+ }
+ } finally {
+ runner.StandardErrorEchoWrapper = origStderrWrapper;
+ }
+
+ if (firstLineOnly) {
+ return (true, outputLine ?? String.Empty);
+ }
+
+ return (true, FormatOutputWithLines (lines));
+
+ string FormatOutputWithLines (List? lines) => lines != null ? String.Join (Environment.NewLine, lines) : String.Empty;
+ }
+
+ async Task RunAdb (ProcessRunner runner, bool setupOutputSink = true, bool ignoreStderr = true)
+ {
+ return await RunTool (
+ () => {
+ TextWriter? stdoutSink = null;
+ TextWriter? stderrSink = null;
+ if (setupOutputSink) {
+ stdoutSink = new AdbOutputSink (Logger, isStdError: false, lineFilter: null);
+ if (!ignoreStderr) {
+ stderrSink = new AdbOutputSink (Logger, isStdError: true, lineFilter: null);
+ }
+
+ SetupOutputSinks (runner, stdoutSink, stderrSink, ignoreStderr);
+ }
+
+ try {
+ bool ret = runner.Run ();
+ ExitCode = runner.ExitCode;
+ return ret;
+ } catch {
+ ExitCode = -0xDEAD;
+ throw;
+ } finally {
+ stdoutSink?.Dispose ();
+ stderrSink?.Dispose ();
+ }
+ }
+ );
+ }
+
+ ProcessRunner CreateAdbRunner ()
+ {
+ ProcessRunner ret = CreateProcessRunner (initialParams);
+
+ // Let's make sure all the messages we get are in English, since we need to parse some of them to detect problems
+ ret.Environment["LANG"] = "C";
+ return ret;
+ }
+ }
+}
diff --git a/src/Xamarin.Android.Build.Tasks/Utilities/AndroidDeviceInfo.cs b/src/Xamarin.Android.Build.Tasks/Utilities/AndroidDeviceInfo.cs
new file mode 100644
index 00000000000..3feb6c8417d
--- /dev/null
+++ b/src/Xamarin.Android.Build.Tasks/Utilities/AndroidDeviceInfo.cs
@@ -0,0 +1,124 @@
+using System;
+using System.Threading.Tasks;
+
+using Microsoft.Build.Utilities;
+
+using TPLTask = System.Threading.Tasks.Task;
+
+namespace Xamarin.Android.Tasks
+{
+ class AndroidDeviceInfo
+ {
+ string? targetDevice;
+ TaskLoggingHelper log;
+ string adbPath;
+
+ public string? DeviceSdkVersion { get; private set; }
+ public bool DeviceIsEmulator { get; private set; }
+ public string[]? DeviceSupportedAbis { get; private set; }
+ public string? DevicePrimaryABI { get; private set; }
+
+ public AndroidDeviceInfo (TaskLoggingHelper logger, string adbPath, string? targetDevice = null)
+ {
+ this.targetDevice = targetDevice;
+ this.log = logger;
+ this.adbPath = adbPath;
+ }
+
+ public async TPLTask Detect ()
+ {
+ var adb = new AdbRunner (log, adbPath, targetDevice);
+
+ DeviceSdkVersion = await GetProperty (adb, "ro.build.version.sdk", "Android SDK version");
+ DevicePrimaryABI = await GetProperty (adb, "ro.product.cpu.abi", "primary ABI");
+
+ string abis = await GetProperty (adb, "ro.product.cpu.abilist", "ABI list");
+ DeviceSupportedAbis = abis?.Split (',');
+
+ string? fingerprint = await GetProperty (adb, "ro.build.fingerprint", "fingerprint");
+ if (CheckProperty (fingerprint, (string v) => v.StartsWith ("generic", StringComparison.Ordinal))) {
+ DeviceIsEmulator = true;
+ return;
+ }
+
+ string? model = await GetProperty (adb, "ro.product.model", "product model");
+ if (!String.IsNullOrEmpty (model)) {
+ if (Contains (model, "google_sdk") ||
+ Contains (model, "droid4x", StringComparison.OrdinalIgnoreCase) ||
+ Contains (model, "Emulator") ||
+ Contains (model, "Android SDK built for x86", StringComparison.OrdinalIgnoreCase)
+ ) {
+ DeviceIsEmulator = true;
+ return;
+ }
+ }
+
+ string? manufacturer = await GetProperty (adb, "ro.product.manufacturer", "product manufacturer");
+ if (CheckProperty (manufacturer, (string v) => Contains (v, "Genymotion", StringComparison.OrdinalIgnoreCase))) {
+ DeviceIsEmulator = true;
+ return;
+ }
+
+ string? hardware = await GetProperty (adb, "ro.hardware", "hardware model");
+ if (!String.IsNullOrEmpty (hardware)) {
+ if (Contains (hardware, "goldfish") ||
+ Contains (hardware, "ranchu") ||
+ Contains (hardware, "vbox86")
+ ) {
+ DeviceIsEmulator = true;
+ return;
+ }
+ }
+
+ string? product = await GetProperty (adb, "ro.product.name", "product name");
+ if (!String.IsNullOrEmpty (product)) {
+ if (Contains (product, "sdk_google") ||
+ Contains (product, "google_sdk") ||
+ Contains (product, "sdk") ||
+ Contains (product, "sdk_x86") ||
+ Contains (product, "sdk_gphone64_arm64") ||
+ Contains (product, "vbox86p") ||
+ Contains (product, "emulator") ||
+ Contains (product, "simulator")
+ ) {
+ DeviceIsEmulator = true;
+ return;
+ }
+ }
+
+ bool Contains (string s, string sub, StringComparison comparison = StringComparison.Ordinal)
+ {
+#if NETCOREAPP
+ return s.Contains (sub, comparison);
+#else
+ return s.IndexOf (sub, comparison) >= 0;
+#endif
+ }
+
+ bool CheckProperty (string? value, Func checker)
+ {
+ if (String.IsNullOrEmpty (value)) {
+ return false;
+ }
+
+ return checker (value);
+ }
+ }
+
+ async Task GetProperty (AdbRunner adb, string propertyName, string errorWhat)
+ {
+ (bool success, string propertyValue) = await adb.GetPropertyValue (propertyName);
+ if (!success) {
+ log.LogWarning ($"Failed to get {errorWhat} from device");
+ return default;
+ }
+
+ return propertyValue;
+ }
+
+ bool IsEmulator (string? model)
+ {
+ return false;
+ }
+ }
+}
diff --git a/src/Xamarin.Android.Build.Tasks/Utilities/DotnetSymbolRunner.cs b/src/Xamarin.Android.Build.Tasks/Utilities/DotnetSymbolRunner.cs
new file mode 100644
index 00000000000..9b8fed3d8aa
--- /dev/null
+++ b/src/Xamarin.Android.Build.Tasks/Utilities/DotnetSymbolRunner.cs
@@ -0,0 +1,41 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Threading.Tasks;
+
+using Microsoft.Build.Utilities;
+
+namespace Xamarin.Android.Tasks
+{
+ class DotnetSymbolRunner : ToolRunner
+ {
+ public DotnetSymbolRunner (TaskLoggingHelper logger, string dotnetSymbolPath)
+ : base (logger, dotnetSymbolPath)
+ {}
+
+ public async Task Fetch (string nativeLibraryPath, bool enableDiagnostics = false)
+ {
+ var runner = CreateProcessRunner ();
+ runner.AddArgument ("--symbols");
+ runner.AddArgument ("--timeout").AddArgument ("1");
+ runner.AddArgument ("--overwrite");
+
+ if (enableDiagnostics) {
+ runner.AddArgument ("--diagnostics");
+ }
+
+ runner.AddArgument (nativeLibraryPath);
+
+ return await Run (runner);
+ }
+
+ async Task Run (ProcessRunner runner)
+ {
+ return await RunTool (
+ () => {
+ return runner.Run ();
+ }
+ );
+ }
+ }
+}
diff --git a/src/Xamarin.Android.Build.Tasks/Utilities/ELFHelper.cs b/src/Xamarin.Android.Build.Tasks/Utilities/ELFHelper.cs
index a7a00b4af60..56f2ea31ab7 100644
--- a/src/Xamarin.Android.Build.Tasks/Utilities/ELFHelper.cs
+++ b/src/Xamarin.Android.Build.Tasks/Utilities/ELFHelper.cs
@@ -1,6 +1,7 @@
using System;
using System.Collections.Concurrent;
using System.IO;
+using System.Text;
using ELFSharp;
using ELFSharp.ELF;
@@ -15,6 +16,89 @@ namespace Xamarin.Android.Tasks
{
static class ELFHelper
{
+ public static bool IsAOTLibrary (TaskLoggingHelper log, string path)
+ {
+ IELF? elf = ReadElfFile (log, path, "Unable to check if file is an AOT shared library.");
+ if (elf == null) {
+ return false;
+ }
+
+ return IsAOTLibrary (elf);
+ }
+
+ static bool IsAOTLibrary (IELF elf)
+ {
+ ISymbolTable? symtab = GetSymbolTable (elf, ".dynsym");
+ if (symtab == null) {
+ // We can't be sure what the DSO is, play safe
+ return false;
+ }
+
+ foreach (var entry in symtab.Entries) {
+ if (String.Compare ("mono_aot_file_info", entry.Name, StringComparison.Ordinal) == 0 && entry.Type == ELFSymbolType.Object) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ public static bool HasDebugSymbols (TaskLoggingHelper log, string path)
+ {
+ return HasDebugSymbols (log, path, out bool _);
+ }
+
+ public static bool HasDebugSymbols (TaskLoggingHelper log, string path, out bool usesDebugLink)
+ {
+ usesDebugLink = false;
+ IELF? elf = ReadElfFile (log, path, "Skipping debug symbols presence check.");
+ if (elf == null) {
+ return false;
+ }
+
+ if (HasDebugSymbols (elf)) {
+ return true;
+ }
+
+ ISection? gnuDebugLink = GetSection (elf, ".gnu_debuglink");
+ if (gnuDebugLink == null) {
+ return false;
+ }
+ usesDebugLink = true;
+
+ byte[] contents = gnuDebugLink.GetContents ();
+ if (contents == null || contents.Length == 0) {
+ return false;
+ }
+
+ // .gnu_debuglink section format: https://sourceware.org/gdb/current/onlinedocs/gdb/Separate-Debug-Files.html#index-_002egnu_005fdebuglink-sections
+ int nameEnd = -1;
+ for (int i = 0; i < contents.Length; i++) {
+ if (contents[i] == 0) {
+ nameEnd = i;
+ break;
+ }
+ }
+
+ if (nameEnd < 2) {
+ // Name is terminated with a 0 byte, so we need at least 2 bytes
+ return false;
+ }
+
+ string debugInfoFileName = Encoding.UTF8.GetString (contents, 0, nameEnd);
+ if (String.IsNullOrEmpty (debugInfoFileName)) {
+ return false;
+ }
+
+ string debugFilePath = Path.Combine (Path.GetDirectoryName (path), debugInfoFileName);
+ return File.Exists (debugFilePath);
+ }
+
+ static bool HasDebugSymbols (IELF elf)
+ {
+ return GetSymbolTable (elf, ".symtab") != null;
+ }
+
public static bool IsEmptyAOTLibrary (TaskLoggingHelper log, string path)
{
if (String.IsNullOrEmpty (path) || !File.Exists (path)) {
@@ -73,26 +157,12 @@ static bool IsLibraryReference (IStringTable stringTable, IDynamicEntry dynEntry
static bool IsEmptyAOTLibrary (TaskLoggingHelper log, string path, IELF elf)
{
- ISymbolTable? symtab = GetSymbolTable (elf, ".dynsym");
- if (symtab == null) {
- // We can't be sure what the DSO is, play safe
+ if (!IsAOTLibrary (elf)) {
+ // Not a MonoVM AOT shared library
return false;
}
- bool mono_aot_file_info_found = false;
- foreach (var entry in symtab.Entries) {
- if (String.Compare ("mono_aot_file_info", entry.Name, StringComparison.Ordinal) == 0 && entry.Type == ELFSymbolType.Object) {
- mono_aot_file_info_found = true;
- break;
- }
- }
-
- if (!mono_aot_file_info_found) {
- // Not a MonoVM AOT assembly
- return false;
- }
-
- symtab = GetSymbolTable (elf, ".symtab");
+ ISymbolTable? symtab = GetSymbolTable (elf, ".symtab");
if (symtab == null) {
// The DSO is stripped, we can't tell if there are any functions defined (.text will be present anyway)
// We perhaps **can** take a look at the .text section size, but it's not a solid check...
@@ -161,5 +231,16 @@ bool IsNonEmptyCodeSymbol (SymbolEntry? symbolEntry) where T : struct
return section;
}
+
+ static IELF? ReadElfFile (TaskLoggingHelper log, string path, string customErrorMessage)
+ {
+ try {
+ return ELFReader.Load (path);
+ } catch (Exception ex) {
+ log.LogWarning ($"{path} may not be a valid ELF binary. ${customErrorMessage}");
+ log.LogWarningFromException (ex, showStackTrace: false);
+ return null;
+ }
+ }
}
}
diff --git a/src/Xamarin.Android.Build.Tasks/Utilities/LldbRunner.cs b/src/Xamarin.Android.Build.Tasks/Utilities/LldbRunner.cs
new file mode 100644
index 00000000000..285c1901092
--- /dev/null
+++ b/src/Xamarin.Android.Build.Tasks/Utilities/LldbRunner.cs
@@ -0,0 +1,67 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Threading.Tasks;
+
+using Microsoft.Build.Utilities;
+using Xamarin.Android.Tools;
+
+namespace Xamarin.Android.Tasks
+{
+ class LldbRunner : ToolRunner
+ {
+ bool needPythonEnvvars;
+ string scriptPath;
+
+ public LldbRunner (TaskLoggingHelper logger, string lldbPath, string lldbScriptPath)
+ : base (logger, lldbPath)
+ {
+ // If we're invoking the executable directly, we need to set up Python environment variables or lldb won't run
+ string ext = Path.GetExtension (lldbPath);
+ if (String.Compare (".sh", ext, StringComparison.OrdinalIgnoreCase) == 0 ||
+ String.Compare (".cmd", ext, StringComparison.OrdinalIgnoreCase) == 0 ||
+ String.Compare (".bat", ext, StringComparison.OrdinalIgnoreCase) == 0) {
+ needPythonEnvvars = false;
+ } else {
+ needPythonEnvvars = true;
+ }
+
+ scriptPath = lldbScriptPath;
+
+ EchoStandardError = false;
+ EchoStandardOutput = false;
+ ProcessTimeout = TimeSpan.MaxValue;
+ }
+
+ public bool Run ()
+ {
+ var runner = CreateProcessRunner ("--source", scriptPath);
+ if (!OS.IsWindows) {
+ // lldb bundled with the NDK is unable to find it on its own.
+ // It searches for the database at "/buildbot/src/android/llvm-toolchain/out/lib/libncurses-linux-install/share/terminfo"
+ runner.Environment.Add ("TERMINFO", "/usr/share/terminfo");
+ }
+
+ if (needPythonEnvvars) {
+ // We assume our LLDB path is within the NDK root
+ string pythonDir = Path.GetFullPath (Path.Combine (Path.GetDirectoryName (ToolPath), "..", "python3"));
+ runner.Environment.Add ("PYTHONHOME", pythonDir);
+
+ if (!OS.IsWindows) {
+ string envvarName = OS.IsMac ? "DYLD_LIBRARY_PATH" : "LD_LIBRARY_PATH";
+ string oldLibraryPath = Environment.GetEnvironmentVariable (envvarName) ?? String.Empty;
+ string pythonLibDir = Path.Combine (pythonDir, "lib");
+ runner.Environment.Add (envvarName, $"{pythonLibDir}:${oldLibraryPath}");
+ }
+ }
+
+ try {
+ return runner.Run ();
+ } catch (Exception ex) {
+ Logger.LogWarning ("LLDB failed with exception");
+ Logger.LogWarningFromException (ex);
+ return false;
+ }
+ }
+ }
+}
diff --git a/src/Xamarin.Android.Build.Tasks/Utilities/ManifestDocument.cs b/src/Xamarin.Android.Build.Tasks/Utilities/ManifestDocument.cs
index 3f648c5cfcb..8d425a81ff2 100644
--- a/src/Xamarin.Android.Build.Tasks/Utilities/ManifestDocument.cs
+++ b/src/Xamarin.Android.Build.Tasks/Utilities/ManifestDocument.cs
@@ -95,6 +95,11 @@ internal class ManifestDocument
public bool ForceDebuggable { get; set; }
public string VersionName { get; set; }
+ ///
+ /// Available on API 29 or newer
+ ///
+ public bool Profileable { get; set; }
+
string versionCode;
///
@@ -422,6 +427,12 @@ public IList Merge (TaskLoggingHelper log, TypeDefinitionCache cache, Li
debuggable.Value = "true";
}
+ if (Profileable) {
+ var profileable = new XElement ("profileable");
+ profileable.Add (new XAttribute (androidNs + "shell", "true"));
+ app.Add (profileable);
+ }
+
if (Debug || NeedsInternet)
AddInternetPermissionForDebugger ();
diff --git a/src/Xamarin.Android.Build.Tasks/Utilities/MonoAndroidHelper.cs b/src/Xamarin.Android.Build.Tasks/Utilities/MonoAndroidHelper.cs
index 179c78cb6b4..adab2d230cc 100644
--- a/src/Xamarin.Android.Build.Tasks/Utilities/MonoAndroidHelper.cs
+++ b/src/Xamarin.Android.Build.Tasks/Utilities/MonoAndroidHelper.cs
@@ -534,5 +534,19 @@ public static string GetRelativePathForAndroidAsset (string assetsDirectory, ITa
path = head.Length == path.Length ? path : path.Substring ((head.Length == 0 ? 0 : head.Length + 1) + assetsDirectory.Length).TrimStart (DirectorySeparators);
return path;
}
+
+ public static string? ReadManifestResource (TaskLoggingHelper log, string resourceName)
+ {
+ using (var from = System.Reflection.Assembly.GetExecutingAssembly ().GetManifestResourceStream (resourceName)) {
+ if (from == null) {
+ log.LogCodedError ("XA0116", Properties.Resources.XA0116, resourceName);
+ return null;
+ }
+
+ using (var sr = new StreamReader (from)) {
+ return sr.ReadToEnd ();
+ }
+ }
+ }
}
}
diff --git a/src/Xamarin.Android.Build.Tasks/Utilities/NativeDebugPrep.cs b/src/Xamarin.Android.Build.Tasks/Utilities/NativeDebugPrep.cs
new file mode 100644
index 00000000000..76d94a1a3eb
--- /dev/null
+++ b/src/Xamarin.Android.Build.Tasks/Utilities/NativeDebugPrep.cs
@@ -0,0 +1,183 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Reflection;
+using System.Runtime.InteropServices;
+using System.Text;
+
+using Microsoft.Android.Build.Tasks;
+using Microsoft.Build.Utilities;
+using Xamarin.Android.Tools;
+
+namespace Xamarin.Android.Tasks
+{
+ class NativeDebugPrep
+ {
+ const string ConfigScriptName = "debug-app-config";
+ const string LldbScriptName = "lldb.x";
+
+ static HashSet xaLibraries = new HashSet (StringComparer.Ordinal) {
+ "libmonodroid.so",
+ "libxamarin-app.so",
+ "libxamarin-debug-app-helper.so",
+ };
+
+ TaskLoggingHelper log;
+
+ public NativeDebugPrep (TaskLoggingHelper logger)
+ {
+ log = logger;
+ }
+
+ [DllImport ("c", SetLastError=true, EntryPoint="chmod")]
+ private static extern int chmod (string path, uint mode);
+
+ public void Prepare (string adbPath, string ndkRootPath, string activityName,
+ string outputDirRoot, string packageName, string[] supportedAbis,
+ Dictionary> nativeLibsPerAbi, string? targetDevice)
+ {
+ bool isWindows = OS.IsWindows;
+ string scriptResourceName = isWindows ? "debug-app.ps1" : "debug-app.sh";
+ string? script = MonoAndroidHelper.ReadManifestResource (log, scriptResourceName);
+
+ if (String.IsNullOrEmpty (script)) {
+ log.LogError ($"Failed to read script resource '{scriptResourceName}'");
+ return;
+ }
+
+ string? appLibrariesRoot = CopyLibraries (outputDirRoot, nativeLibsPerAbi);
+ string scriptConfigExt = isWindows ? ".ps1" : ".sh";
+ string scriptOutput = Path.Combine (outputDirRoot, scriptResourceName);
+
+ var sb = new StringBuilder (script);
+
+ // TODO: perhaps use relative paths for APP_LIBS_DIR and OUTPUT_DIR?
+ sb.Replace ("@ACTIVITY_NAME@", activityName);
+ sb.Replace ("@ADB_PATH@", adbPath);
+ sb.Replace ("@APP_LIBS_DIR@", appLibrariesRoot ?? String.Empty);
+ sb.Replace ("@CONFIG_SCRIPT_NAME@", $"{ConfigScriptName}{scriptConfigExt}");
+ sb.Replace ("@DEBUG_SESSION_PREP_PATH@", Path.Combine (Path.GetDirectoryName (typeof(NativeDebugPrep).Assembly.Location), "debug-session-prep.dll"));
+ sb.Replace ("@LLDB_SCRIPT_NAME@", LldbScriptName);
+ sb.Replace ("@NDK_DIR@", Path.GetFullPath (ndkRootPath));
+ sb.Replace ("@OUTPUT_DIR@", Path.GetFullPath (outputDirRoot));
+ sb.Replace ("@PACKAGE_NAME@", packageName);
+
+ var abis = new StringBuilder ();
+ bool first = true;
+ foreach (string abi in supportedAbis) {
+ if (first) {
+ first = false;
+ } else {
+ abis.Append (isWindows ? ", " : " ");
+ }
+ abis.Append ($"\"{abi}\"");
+ }
+ sb.Replace ("@SUPPORTED_ABIS@", abis.ToString ());
+
+ Directory.CreateDirectory (Path.GetDirectoryName (scriptOutput));
+
+ using var fs = File.Open (scriptOutput, FileMode.Create, FileAccess.Write, FileShare.Read);
+ using var sw = new StreamWriter (fs, Files.UTF8withoutBOM);
+
+ sw.Write (sb.ToString ());
+ sw.Flush ();
+ sw.Close ();
+ fs.Close ();
+
+ // 493 decimal is 0755 octal - makes the file rwx for the owner and rx for everybody else
+ if (!isWindows && chmod (scriptOutput, 493) != 0) {
+ log.LogWarning ($"Failed to make {scriptOutput} executable");
+ }
+
+ // TODO: color?
+ Console.WriteLine ();
+ Console.WriteLine ("You can start the debugging session by running the following command now:");
+ Console.WriteLine ($" {scriptOutput}");
+ Console.WriteLine ();
+ }
+
+ string? CopyLibraries (string outputDirRoot, Dictionary> nativeLibsPerAbi)
+ {
+ if (nativeLibsPerAbi.Count == 0) {
+ return null;
+ }
+
+ string appLibsRoot = Path.Combine (outputDirRoot, "lldb", "lib");
+ log.LogDebugMessage ($"Copying application native libararies to {appLibsRoot}");
+
+ DotnetSymbolRunner? dotnetSymbol = GetDotnetSymbolRunner ();
+ bool haveLibsWithoutSymbols = false;
+ foreach (var kvp in nativeLibsPerAbi) {
+ string abi = kvp.Key;
+ List libs = kvp.Value;
+
+ string abiDir = Path.Combine (appLibsRoot, abi);
+ foreach (string library in libs) {
+ log.LogDebugMessage ($" [{abi}] {library}");
+
+ string fileName = Path.GetFileName (library);
+ if (fileName.StartsWith ("libmono-android.")) {
+ fileName = "libmonodroid.so";
+ }
+
+ string destPath = Path.Combine (appLibsRoot, abi, fileName);
+ Directory.CreateDirectory (Path.GetDirectoryName (destPath));
+ File.Copy (library, destPath, true);
+
+ if (!EnsureSharedLibraryHasSymboles (destPath, dotnetSymbol)) {
+ haveLibsWithoutSymbols = true;
+ }
+ }
+ }
+
+ if (haveLibsWithoutSymbols) {
+ log.LogWarning ($"One or more native libraries have no debug symbols.");
+ if (dotnetSymbol == null) {
+ log.LogWarning ($"The dotnet-symbol tool was not found. It can be installed using: dotnet tool install -g dotnet-symbol");
+ }
+ }
+
+ return Path.GetFullPath (appLibsRoot);
+ }
+
+ bool EnsureSharedLibraryHasSymboles (string libraryPath, DotnetSymbolRunner? dotnetSymbol)
+ {
+ bool tryToFetchSymbols = false;
+ bool hasSymbols = ELFHelper.HasDebugSymbols (log, libraryPath, out bool usesDebugLink);
+ string libName = Path.GetFileName (libraryPath);
+
+ if (!xaLibraries.Contains (libName)) {
+ if (ELFHelper.IsAOTLibrary (log, libraryPath)) {
+ return true; // We don't care about symbols, AOT libraries are only data
+ }
+
+ // It might be a framework shared library, we'll try to fetch symbols if necessary and possible
+ tryToFetchSymbols = !hasSymbols && usesDebugLink;
+ }
+
+ if (tryToFetchSymbols && dotnetSymbol != null) {
+ log.LogMessage ($"Attempting to download debug symbols from symbol server");
+ if (!dotnetSymbol.Fetch (libraryPath).Result) {
+ log.LogWarning ($"Failed to download debug symbols for {libraryPath}");
+ }
+ }
+
+ hasSymbols = ELFHelper.HasDebugSymbols (log, libraryPath);
+ return hasSymbols;
+ }
+
+ DotnetSymbolRunner? GetDotnetSymbolRunner ()
+ {
+ string dotnetSymbolPath = Path.Combine (Environment.GetFolderPath (Environment.SpecialFolder.UserProfile), ".dotnet", "tools", "dotnet-symbol");
+ if (OS.IsWindows) {
+ dotnetSymbolPath = $"{dotnetSymbolPath}.exe";
+ }
+
+ if (!File.Exists (dotnetSymbolPath)) {
+ return null;
+ }
+
+ return new DotnetSymbolRunner (log, dotnetSymbolPath);
+ }
+ }
+}
diff --git a/src/Xamarin.Android.Build.Tasks/Utilities/NativeDebugger.cs b/src/Xamarin.Android.Build.Tasks/Utilities/NativeDebugger.cs
new file mode 100644
index 00000000000..8c63b12d763
--- /dev/null
+++ b/src/Xamarin.Android.Build.Tasks/Utilities/NativeDebugger.cs
@@ -0,0 +1,843 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Text;
+
+using Microsoft.Android.Build.Tasks;
+using Microsoft.Build.Utilities;
+using Xamarin.Android.Tools;
+
+using TPL = System.Threading.Tasks;
+
+namespace Xamarin.Android.Tasks
+{
+ // Starting LLDB server: /data/data/net.twistedcode.myapplication/lldb/bin/start_lldb_server.sh /data/data/net.twistedcode.myapplication/lldb unix-abstract /net.twistedcode.myapplication-0 platform-1668626161269.sock "lldb process:gdb-remote packets"
+
+ ///
+ /// Interface to lldb, the NDK native code debugger.
+ ///
+ class NativeDebugger
+ {
+ const ConsoleColor ErrorColor = ConsoleColor.Red;
+ const ConsoleColor DebugColor = ConsoleColor.DarkGray;
+ const ConsoleColor InfoColor = ConsoleColor.Green;
+ const ConsoleColor MessageColor = ConsoleColor.Gray;
+ const ConsoleColor WarningColor = ConsoleColor.Yellow;
+ const ConsoleColor StatusLabel = ConsoleColor.Cyan;
+ const ConsoleColor StatusText = ConsoleColor.White;
+
+ const string ServerLauncherScriptName = "xa_start_lldb_server.sh";
+
+ enum LogLevel
+ {
+ Error,
+ Warning,
+ Info,
+ Message,
+ Debug
+ }
+
+ sealed class Context
+ {
+ public AdbRunner adb;
+ public int apiLevel;
+ public string abi;
+ public string arch;
+ public bool appIs64Bit;
+ public string appDataDir;
+ public string debugServerPath;
+ public string debugServerScriptPath;
+ public string debugSocketPath;
+ public string outputDir;
+ public string appLibrariesDir;
+ public string appLldbBaseDir;
+ public string appLldbBinDir;
+ public string appLldbLogDir;
+ public uint applicationPID;
+ public string lldbScriptPath;
+ public string zygotePath;
+ public int debugServerPort;
+ public string domainSocketDir;
+ public string platformSocketName;
+ public List? nativeLibraries;
+ public List deviceBinaryDirs;
+ }
+
+ // We want the shell/batch scripts first, since they set up Python environment for the debugger
+ static readonly string[] lldbNames = {
+ "lldb.sh",
+ "lldb",
+ "lldb.cmd",
+ "lldb.exe",
+ };
+
+ static readonly string[] abiProperties = {
+ // new properties
+ "ro.product.cpu.abilist",
+
+ // old properties
+ "ro.product.cpu.abi",
+ "ro.product.cpu.abi2",
+ };
+
+ static readonly string[] deviceLibraries = {
+ "libc.so",
+ "libm.so",
+ "libdl.so",
+ };
+
+ static HashSet xaLibraries = new HashSet (StringComparer.Ordinal) {
+ "libmonodroid.so",
+ "libxamarin-app.so",
+ "libxamarin-debug-app-helper.so",
+ };
+
+ static readonly object consoleLock = new object ();
+ static readonly UTF8Encoding UTF8NoBOM = new UTF8Encoding (false);
+
+ TaskLoggingHelper log;
+ string packageName;
+ string lldbPath;
+ string adbPath;
+ string outputDir;
+ Dictionary hostLldbServerPaths;
+ string[] supportedAbis;
+
+ public string? AdbDeviceTarget { get; set; }
+ public bool UseLldbGUI { get; set; } = true;
+ public string? CustomLldbCommandsFilePath { get; set; }
+
+ public IDictionary>? NativeLibrariesPerABI { get; set; }
+
+ public NativeDebugger (TaskLoggingHelper logger, string adbPath, string ndkRootPath, string outputDirRoot, string packageName, string[] supportedAbis)
+ {
+ this.log = logger;
+ this.packageName = packageName;
+ this.adbPath = adbPath;
+ this.supportedAbis = supportedAbis;
+ outputDir = Path.Combine (outputDirRoot, "native-debug");
+
+ if (!FindTools (ndkRootPath, supportedAbis)) {
+ throw new InvalidOperationException ("Failed to find all the required tools and utilities");
+ }
+ }
+
+ ///
+ /// Detect PID of the running application and attach the debugger to it
+ ///
+ public bool Attach ()
+ {
+ Context? context = Init ();
+
+ return context != null;
+ }
+
+ ///
+ /// Launch the application under control of the debugger.
+ ///
+ public bool Launch (string activityName)
+ {
+ if (String.IsNullOrEmpty (activityName)) {
+ throw new ArgumentException ("must not be null or empty", nameof (activityName));
+ }
+
+ Context? context = Init ();
+ if (context == null) {
+ return false;
+ }
+
+ // Start the app, tell it to wait for debugger to attach and to kill any running instance
+ // We tell `am` to wait ('-W') for the app to start, so that `pidof` then can find the process
+ string launchName = $"{packageName}/{activityName}";
+ LogLine ();
+ LogStatusLine ("Launching activity", launchName);
+ LogLine ("Waiting for the activity to start...");
+ (bool success, string output) = context.adb.Shell ("am", "start", "-S", "-W", launchName).Result;
+ if (!success) {
+ LogErrorLine ("Failed to launch the activity");
+ return false;
+ }
+
+ long appPID = GetDeviceProcessID (context, packageName);
+ if (appPID <= 0) {
+ LogErrorLine ("Failed to obtain PID of the running application");
+ LogErrorLine (output);
+ return false;
+ }
+ context.applicationPID = (uint)appPID;
+
+ LogStatusLine ("Application PID", $"{context.applicationPID}");
+
+ StartDebugServer (context);
+
+ // GenerateLldbScript (context);
+
+ // LogDebugLine ($"Starting LLDB: {lldbPath}");
+ // var lldb = new LldbRunner (log, lldbPath, context.lldbScriptPath);
+ // if (lldb.Run ()) {
+ // LogWarning ("LLDB failed?");
+ // }
+
+ // KillDebugServer (context);
+ // LogDebugLine ("Waiting on the debug server process to quit");
+ // (success, output) = debugServerTask.Result;
+
+ return true;
+ }
+
+ void GenerateLldbScript (Context context)
+ {
+ context.lldbScriptPath = Path.Combine (context.outputDir, $"{context.arch}-lldb-script.txt");
+
+ using (var f = File.OpenWrite (context.lldbScriptPath)) {
+ using (var sw = new StreamWriter (f, UTF8NoBOM)) {
+ string systemPaths = String.Join (" ", context.deviceBinaryDirs.Select (d => $"'{Path.GetFullPath(d)}'" ));
+ sw.WriteLine ($"settings append target.exec-search-paths '{Path.GetFullPath (context.appLibrariesDir)}' {systemPaths}");
+ sw.WriteLine ($"target create '{Path.GetFullPath (context.zygotePath)}'");
+ sw.WriteLine ($"target modules search-paths add / '{Path.GetFullPath (outputDir)}/'");
+ sw.WriteLine ($"gdb-remote {context.debugServerPort}");
+
+ if (UseLldbGUI) {
+ sw.WriteLine ($"gui");
+ }
+
+ if (!String.IsNullOrEmpty (CustomLldbCommandsFilePath)) {
+ sw.WriteLine ();
+ sw.Write (File.ReadAllText (CustomLldbCommandsFilePath));
+ sw.WriteLine ();
+ }
+
+ sw.Flush ();
+ }
+ }
+ }
+
+ bool KillDebugServer (Context context)
+ {
+ long serverPID = GetDeviceProcessID (context, context.debugServerPath, quiet: true);
+ if (serverPID <= 0) {
+ return true;
+ }
+
+ LogDebugLine ("Killing previous instance of the debug server");
+ (bool success, string _) = context.adb.RunAs (packageName, "kill", "-9", $"{serverPID}").Result;
+ return success;
+ }
+
+ (AdbRunner? runner, TPL.Task<(bool success, string output)>? task) StartDebugServer (Context context)
+ {
+ LogDebugLine ($"Starting debug server on device: {context.debugServerScriptPath}");
+
+ if (!KillDebugServer (context)) {
+ LogWarningLine ("Failed to kill previous instance of the debug server");
+ }
+
+ context.domainSocketDir = $"/xa-{packageName}-0";
+
+ var rnd = new Random ();
+ context.platformSocketName = $"xa-platform-{rnd.Next ()}.sock";
+
+ var args = new List {
+ "shell",
+ "run-as",
+ packageName,
+ context.debugServerScriptPath,
+ context.appLldbBaseDir, // LLDB directory
+ "unix-abstract", // Listener socket scheme (unix-abstract: virtual, not on the filesystem)
+ context.domainSocketDir, // Directory where listener socket will be created
+ context.platformSocketName, // name of the socket to create
+ "'\"lldb process:gdb-remote packets\"'", // LLDB log channels
+ context.arch
+ };
+
+ string command = String.Join (" ", args);
+ LogDebugLine ($"Launch command: adb {command}");
+
+ var runner = CreateAdbRunner ();
+ runner.ProcessTimeout = TimeSpan.MaxValue;
+
+ // TPL.Task<(bool success, string output)> task = runner.RunAs (
+ // packageName,
+ // context.debugServerScriptPath,
+ // context.appLldbBaseDir, // LLDB directory
+ // "unix-abstract", // Listener socket scheme (unix-abstract: virtual, not on the filesystem)
+ // context.domainSocketDir, // Directory where listener socket will be created
+ // context.platformSocketName, // name of the socket to create
+ // "'\"lldb process:gdb-remote packets\"'", // LLDB log channels
+ // context.arch // LLDB architecture
+ // );
+
+ return (runner, null);
+ }
+
+ long GetDeviceProcessID (Context context, string processName, bool quiet = false)
+ {
+ (bool success, string output) = context.adb.Shell ("pidof", processName).Result;
+ if (!success) {
+ if (!quiet) {
+ LogErrorLine ($"Failed to obtain PID of process '{processName}'");
+ LogErrorLine (output);
+ }
+ return -1;
+ }
+
+ output = output.Trim ();
+ if (!UInt32.TryParse (output, out uint pid)) {
+ if (!quiet) {
+ LogErrorLine ($"Unable to parse string '{output}' as the package's PID");
+ }
+ return -1;
+ }
+
+ return pid;
+ }
+
+ AdbRunner CreateAdbRunner () => new AdbRunner (log, adbPath, AdbDeviceTarget);
+
+ Context? Init ()
+ {
+ LogLine ();
+
+ var context = new Context {
+ adb = CreateAdbRunner ()
+ };
+
+ (bool success, string output) = context.adb.GetPropertyValue ("ro.build.version.sdk").Result;
+ if (!success || String.IsNullOrEmpty (output) || !Int32.TryParse (output, out int apiLevel)) {
+ LogErrorLine ("Unable to determine connected device's API level");
+ return null;
+ }
+ context.apiLevel = apiLevel;
+
+ // Warn on old Pixel C firmware (b/29381985). Newer devices may have Yama
+ // enabled but still work with ndk-gdb (b/19277529).
+ (success, output) = context.adb.Shell ("cat", "/proc/sys/kernel/yama/ptrace_scope", "2>/dev/null").Result;
+ if (success &&
+ YamaOK (output.Trim ()) &&
+ PropertyIsEqualTo (context.adb.GetPropertyValue ("ro.build.product").Result, "dragon") &&
+ PropertyIsEqualTo (context.adb.GetPropertyValue ("ro.product.name").Result, "ryu")
+ ) {
+ LogWarningLine ("WARNING: The device uses Yama ptrace_scope to restrict debugging. ndk-gdb will");
+ LogWarningLine (" likely be unable to attach to a process. With root access, the restriction");
+ LogWarningLine (" can be lifted by writing 0 to /proc/sys/kernel/yama/ptrace_scope. Consider");
+ LogWarningLine (" upgrading your Pixel C to MXC89L or newer, where Yama is disabled.");
+ LogLine ();
+ }
+
+ if (!DetermineArchitectureAndABI (context)) {
+ return null;
+ }
+
+ if (!DetermineAppDataDirectory (context)) {
+ return null;
+ }
+
+ if (!PushDebugServer (context)) {
+ return null;
+ }
+
+ if (!CopyLibraries (context)) {
+ return null;
+ }
+
+ context.debugSocketPath = $"{context.appDataDir}/debug_socket";
+
+ return context;
+
+ bool YamaOK (string output)
+ {
+ return !String.IsNullOrEmpty (output) && String.Compare ("0", output, StringComparison.Ordinal) != 0;
+ }
+
+ bool PropertyIsEqualTo ((bool haveProperty, string value) result, string expected)
+ {
+ return
+ result.haveProperty &&
+ !String.IsNullOrEmpty (result.value) &&
+ String.Compare (result.value, expected, StringComparison.Ordinal) == 0;
+ }
+ }
+
+ bool EnsureSharedLibraryHasSymboles (string libraryPath, DotnetSymbolRunner? dotnetSymbol)
+ {
+ bool tryToFetchSymbols = false;
+ bool hasSymbols = ELFHelper.HasDebugSymbols (log, libraryPath, out bool usesDebugLink);
+ string libName = Path.GetFileName (libraryPath);
+
+ if (!xaLibraries.Contains (libName)) {
+ if (ELFHelper.IsAOTLibrary (log, libraryPath)) {
+ return true; // We don't are about symbols, AOT libraries are only data
+ }
+
+ // It might be a framework shared library, we'll try to fetch symbols if necessary and possible
+ tryToFetchSymbols = !hasSymbols && usesDebugLink;
+ }
+
+ if (tryToFetchSymbols && dotnetSymbol != null) {
+ LogInfoLine ($" Attempting to download debug symbols from symbol server");
+ if (!dotnetSymbol.Fetch (libraryPath).Result) {
+ LogWarningLine ($" Warning: failed to download debug symbols for {libraryPath}");
+ } else {
+ LogLine ();
+ }
+ }
+
+ hasSymbols = ELFHelper.HasDebugSymbols (log, libraryPath);
+ return hasSymbols;
+ }
+
+ DotnetSymbolRunner? GetDotnetSymbolRunner ()
+ {
+ string dotnetSymbolPath = Path.Combine (Environment.GetFolderPath (Environment.SpecialFolder.UserProfile), ".dotnet", "tools", "dotnet-symbol");
+ if (OS.IsWindows) {
+ dotnetSymbolPath = $"{dotnetSymbolPath}.exe";
+ }
+
+ if (!File.Exists (dotnetSymbolPath)) {
+ return null;
+ }
+
+ return new DotnetSymbolRunner (log, dotnetSymbolPath);
+ }
+
+ bool CopyLibraries (Context context)
+ {
+ LogInfoLine ("Populating local native library cache");
+ context.appLibrariesDir = Path.Combine (context.outputDir, "app", "lib");
+ if (!Directory.Exists (context.appLibrariesDir)) {
+ Directory.CreateDirectory (context.appLibrariesDir);
+ }
+
+ if (context.nativeLibraries != null) {
+ LogInfoLine (" Copying application native libraries");
+ bool haveLibsWithoutSymbols = false;
+ DotnetSymbolRunner? dotnetSymbol = GetDotnetSymbolRunner ();
+
+ foreach (string library in context.nativeLibraries) {
+ LogLine ($" {library}");
+
+ string fileName = Path.GetFileName (library);
+ if (fileName.StartsWith ("libmono-android.")) {
+ fileName = "libmonodroid.so";
+ }
+ string destPath = Path.Combine (context.appLibrariesDir, fileName);
+ File.Copy (library, destPath, true);
+
+ if (!EnsureSharedLibraryHasSymboles (destPath, dotnetSymbol)) {
+ haveLibsWithoutSymbols = true;
+ }
+ }
+
+ if (haveLibsWithoutSymbols) {
+ LogWarningLine ($"One or more native libraries have no debug symbols.");
+ if (dotnetSymbol == null) {
+ LogWarningLine ($"The dotnet-symbol tool was not found. It can be installed using: dotnet tool install -g dotnet-symbol");
+ }
+ }
+ }
+
+ var requiredFiles = new List ();
+ var libraries = new List ();
+ string libraryPath;
+
+ if (context.appIs64Bit) {
+ libraryPath = "/system/lib64";
+
+ string zygotePath = "/system/bin/app_process64";
+ requiredFiles.Add (zygotePath);
+ context.zygotePath = $"{context.outputDir}{ToLocalPathFormat (zygotePath)}";
+ requiredFiles.Add ("/system/bin/linker64");
+ } else {
+ libraryPath = "/system/lib";
+ requiredFiles.Add ("/system/bin/linker");
+ }
+
+ foreach (string lib in deviceLibraries) {
+ requiredFiles.Add ($"{libraryPath}/{lib}");
+ }
+
+ LogLine ();
+ LogInfoLine (" Copying binaries from device");
+ var dirs = new HashSet (StringComparer.Ordinal);
+
+ foreach (string file in requiredFiles) {
+ string filePath = ToLocalPathFormat (file);
+ string localPath = $"{context.outputDir}{filePath}";
+ string localDir = Path.GetDirectoryName (localPath);
+
+ if (!Directory.Exists (localDir)) {
+ Directory.CreateDirectory (localDir);
+ if (!dirs.Contains (localDir)) {
+ dirs.Add (localDir);
+ }
+ }
+
+ Log ($" From '{file}' to '{localPath}' ");
+ if (!context.adb.Pull (file, localPath).Result) {
+ LogLine ("[FAILED]");
+ } else {
+ LogLine ("[SUCCESS]");
+ }
+ }
+
+ context.deviceBinaryDirs = new List (dirs);
+ if (context.appIs64Bit) {
+ return true;
+ }
+
+ // /system/bin/app_process is 32-bit on 32-bit devices, but a symlink to
+ // # app_process64 on 64-bit. If we need the 32-bit version, try to pull
+ // # app_process32, and if that fails, pull app_process.
+ string destination = $"{context.outputDir}{ToLocalPathFormat ("/system/bin/app_process")}";
+ string? source = "/system/bin/app_process32";
+
+ if (!context.adb.Pull (source, destination).Result) {
+ source = "/system/bin/app_process";
+ if (!context.adb.Pull (source, destination).Result) {
+ source = null;
+ }
+ }
+
+ if (String.IsNullOrEmpty (source)) {
+ LogErrorLine ("Failed to copy 32-bit app_process");
+ return false;
+ }
+ LogLine ($" From '{source}' to '{destination}' ");
+ context.zygotePath = destination;
+
+ return true;
+
+ string ToLocalPathFormat (string path) => OS.IsWindows ? path.Replace ("/", "\\") : path;
+ }
+
+ bool PushDebugServer (Context context)
+ {
+ if (!hostLldbServerPaths.TryGetValue (context.abi, out string debugServerPath)) {
+ LogErrorLine ($"Debug server for abi '{context.abi}' not found.");
+ return false;
+ }
+
+ if (!context.adb.CreateDirectoryAs (packageName, context.appLldbBinDir).Result.success) {
+ LogErrorLine ($"Failed to create debug server destination directory on device, {context.appLldbBinDir}");
+ return false;
+ }
+
+ //string serverName = $"xa-{context.arch}-{Path.GetFileName (debugServerPath)}";
+ string serverName = Path.GetFileName (debugServerPath);
+ context.debugServerPath = $"{context.appLldbBinDir}/{serverName}";
+
+ KillDebugServer (context);
+
+ // Always push the server binary, as we don't know what version might already be there
+ if (!PushServerExecutable (context, debugServerPath, context.debugServerPath)) {
+ return false;
+ }
+ LogStatusLine ("Debug server path on device", context.debugServerPath);
+
+ string? launcherScript = MonoAndroidHelper.ReadManifestResource (log, ServerLauncherScriptName);
+ if (String.IsNullOrEmpty (launcherScript)) {
+ return false;
+ }
+
+ string launcherScriptPath = Path.Combine (context.outputDir, ServerLauncherScriptName);
+ Directory.CreateDirectory (Path.GetDirectoryName (launcherScriptPath));
+ File.WriteAllText (launcherScriptPath, launcherScript, UTF8NoBOM);
+
+ context.debugServerScriptPath = $"{context.appLldbBinDir}/{Path.GetFileName (launcherScriptPath)}";
+ if (!PushServerExecutable (context, launcherScriptPath, context.debugServerScriptPath)) {
+ return false;
+ }
+ LogStatusLine ("Debug server launcher script path on device", context.debugServerScriptPath);
+ LogLine ();
+
+ return true;
+ }
+
+ bool PushServerExecutable (Context context, string hostSource, string deviceDestination)
+ {
+ string executableName = Path.GetFileName (deviceDestination);
+
+ // Always push the executable, as we don't know what version might already be there
+ LogDebugLine ($"Uploading {hostSource} to device");
+
+ // First upload to temporary path, as it's writable for everyone
+ string remotePath = $"/data/local/tmp/{executableName}";
+ if (!context.adb.Push (hostSource, remotePath).Result) {
+ LogErrorLine ($"Failed to upload debug server {hostSource} to device path {remotePath}");
+ return false;
+ }
+
+ // Next, copy it to the app dir, with run-as
+ (bool success, string output) = context.adb.Shell (
+ "cat", remotePath, "|",
+ "run-as", packageName,
+ "sh", "-c", $"'cat > {deviceDestination}'"
+ ).Result;
+
+ if (!success) {
+ LogErrorLine ($"Failed to copy debug executable to device, from {hostSource} to {deviceDestination}");
+ return false;
+ }
+
+ (success, output) = context.adb.RunAs (packageName, "chmod", "700", deviceDestination).Result;
+ if (!success) {
+ LogErrorLine ($"Failed to make debug server executable on device, at {deviceDestination}");
+ return false;
+ }
+
+ return true;
+ }
+
+ bool DetermineAppDataDirectory (Context context)
+ {
+ (bool success, string output) = context.adb.GetAppDataDirectory (packageName).Result;
+ if (!success) {
+ LogErrorLine ($"Unable to determine data directory for package '{packageName}'");
+ return false;
+ }
+
+ context.appDataDir = output.Trim ();
+ LogStatusLine ($"Application data directory on device", context.appDataDir);
+ LogLine ();
+
+ context.appLldbBaseDir = $"{context.appDataDir}/lldb";
+ context.appLldbBinDir = $"{context.appLldbBaseDir}/bin";
+ context.appLldbLogDir = $"{context.appLldbBaseDir}/log";
+
+ // Applications with minSdkVersion >= 24 will have their data directories
+ // created with rwx------ permissions, preventing adbd from forwarding to
+ // the gdbserver socket. To be safe, if we're on a device >= 24, always
+ // chmod the directory.
+ if (context.apiLevel >= 24) {
+ (success, output) = context.adb.RunAs (packageName, "/system/bin/chmod", "a+x", context.appDataDir).Result;
+ if (!success) {
+ LogErrorLine ("Failed to make application data directory world executable");
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ bool DetermineArchitectureAndABI (Context context)
+ {
+ string[]? deviceABIs = null;
+
+ foreach (string prop in abiProperties) {
+ (bool success, string value) = context.adb.GetPropertyValue (prop).Result;
+ if (!success) {
+ continue;
+ }
+
+ deviceABIs = value.Split (',');
+ break;
+ }
+
+ if (deviceABIs == null || deviceABIs.Length == 0) {
+ LogErrorLine ("Unable to determine device ABI");
+ return false;
+ }
+
+ LogABIs ("Application", supportedAbis);
+ LogABIs (" Device", deviceABIs);
+
+ foreach (string deviceABI in deviceABIs) {
+ foreach (string appABI in supportedAbis) {
+ if (String.Compare (appABI, deviceABI, StringComparison.OrdinalIgnoreCase) == 0) {
+ context.abi = deviceABI;
+ context.arch = context.abi switch {
+ "armeabi" => "arm",
+ "armeabi-v7a" => "arm",
+ "arm64-v8a" => "arm64",
+ _ => context.abi,
+ };
+
+ LogStatusLine ($" Selected ABI", $"{context.abi} (architecture: {context.arch})");
+
+ context.appIs64Bit = context.abi.IndexOf ("64", StringComparison.Ordinal) >= 0;
+ context.outputDir = Path.Combine (outputDir, context.abi);
+ if (NativeLibrariesPerABI != null && NativeLibrariesPerABI.TryGetValue (context.abi, out List abiLibraries)) {
+ context.nativeLibraries = abiLibraries;
+ }
+ return true;
+ }
+ }
+ }
+
+ LogErrorLine ($"Application cannot run on the selected device: no matching ABI found");
+ return false;
+
+ void LogABIs (string which, string[] abis)
+ {
+ LogStatusLine ($"{which} ABIs", String.Join (", ", abis));
+ }
+ }
+
+ string GetLlvmVersion (string toolchainDir)
+ {
+ string path = Path.Combine (toolchainDir, "AndroidVersion.txt");
+ if (!File.Exists (path)) {
+ throw new InvalidOperationException ($"LLVM version file not found at '{path}'");
+ }
+
+ string[] lines = File.ReadAllLines (path);
+ string? line = lines.Length >= 1 ? lines[0].Trim () : null;
+ if (String.IsNullOrEmpty (line)) {
+ throw new InvalidOperationException ($"Unable to read LLVM version from '{path}'");
+ }
+
+ return line;
+ }
+
+ bool FindTools (string ndkRootPath, string[] supportedAbis)
+ {
+ string toolchainDir = Path.Combine (ndkRootPath, NdkHelper.RelativeToolchainDir);
+ string toolchainBinDir = Path.Combine (toolchainDir, "bin");
+ string? path = null;
+
+ foreach (string lldb in lldbNames) {
+ path = Path.Combine (toolchainBinDir, lldb);
+ if (File.Exists (path)) {
+ break;
+ }
+ }
+
+ if (String.IsNullOrEmpty (path)) {
+ LogErrorLine ($"Unable to locate lldb executable in '{toolchainBinDir}'");
+ return false;
+ }
+ lldbPath = path;
+
+ hostLldbServerPaths = new Dictionary (StringComparer.OrdinalIgnoreCase);
+ string llvmVersion = GetLlvmVersion (toolchainDir);
+ foreach (string abi in supportedAbis) {
+ string llvmAbi = NdkHelper.TranslateAbiToLLVM (abi);
+ path = Path.Combine (ndkRootPath, NdkHelper.RelativeToolchainDir, "lib64", "clang", llvmVersion, "lib", "linux", llvmAbi, "lldb-server");
+ if (!File.Exists (path)) {
+ LogErrorLine ($"LLVM lldb server component for ABI '{abi}' not found at '{path}'");
+ return false;
+ }
+
+ hostLldbServerPaths.Add (abi, path);
+ }
+
+ if (hostLldbServerPaths.Count == 0) {
+ LogErrorLine ("Unable to find any lldb-server executables, debugging not possible");
+ return false;
+ }
+
+ return true;
+ }
+
+ void Log (string? message)
+ {
+ Log (LogLevel.Message, message);
+ }
+
+ void LogLine (string? message = null)
+ {
+ Log ($"{message}{Environment.NewLine}");
+ }
+
+ void LogWarning (string? message)
+ {
+ Log (LogLevel.Warning, message);
+ }
+
+ void LogWarningLine (string? message)
+ {
+ LogWarning ($"{message}{Environment.NewLine}");
+ }
+
+ void LogError (string? message)
+ {
+ Log (LogLevel.Error, message);
+ }
+
+ void LogErrorLine (string? message)
+ {
+ LogError ($"{message}{Environment.NewLine}");
+ }
+
+ void LogInfo (string? message)
+ {
+ Log (LogLevel.Info, message);
+ }
+
+ void LogInfoLine (string? message)
+ {
+ LogInfo ($"{message}{Environment.NewLine}");
+ }
+
+ void LogDebug (string? message)
+ {
+ Log (LogLevel.Debug, message);
+ }
+
+ void LogDebugLine (string? message)
+ {
+ LogDebug ($"{message}{Environment.NewLine}");
+ }
+
+ void LogStatusLine (string label, string text)
+ {
+ Log (LogLevel.Info, $"{label}: ", StatusLabel);
+ Log (LogLevel.Info, $"{text}{Environment.NewLine}", StatusText);
+ }
+
+ void Log (LogLevel level, string? message)
+ {
+ Log (level, message, ForegroundColor (level));
+ }
+
+ void Log (LogLevel level, string? message, ConsoleColor color)
+ {
+ TextWriter writer = level == LogLevel.Error ? Console.Error : Console.Out;
+ message = message ?? String.Empty;
+
+ ConsoleColor fg = ConsoleColor.Gray;
+ try {
+ lock (consoleLock) {
+ fg = Console.ForegroundColor;
+ Console.ForegroundColor = color;
+ }
+
+ writer.Write (message);
+ } finally {
+ Console.ForegroundColor = fg;
+ }
+
+ if (!String.IsNullOrEmpty (message)) {
+ switch (level) {
+ case LogLevel.Error:
+ log.LogError (message);
+ break;
+
+ case LogLevel.Warning:
+ log.LogWarning (message);
+ break;
+
+ default:
+ case LogLevel.Message:
+ case LogLevel.Info:
+ log.LogMessage (message);
+ break;
+
+ case LogLevel.Debug:
+ log.LogDebugMessage (message);
+ break;
+ }
+ }
+ }
+
+ ConsoleColor ForegroundColor (LogLevel level) => level switch {
+ LogLevel.Error => ErrorColor,
+ LogLevel.Warning => WarningColor,
+ LogLevel.Info => InfoColor,
+ LogLevel.Debug => DebugColor,
+ LogLevel.Message => MessageColor,
+ _ => MessageColor,
+ };
+ }
+}
diff --git a/src/Xamarin.Android.Build.Tasks/Utilities/NdkHelper.cs b/src/Xamarin.Android.Build.Tasks/Utilities/NdkHelper.cs
new file mode 100644
index 00000000000..f5062abeea5
--- /dev/null
+++ b/src/Xamarin.Android.Build.Tasks/Utilities/NdkHelper.cs
@@ -0,0 +1,58 @@
+using System;
+using System.IO;
+
+using System.Runtime.InteropServices;
+
+namespace Xamarin.Android.Tasks
+{
+ static class NdkHelper
+ {
+ static readonly string toolchainHostName;
+ static readonly string relativeToolchainDir;
+
+ public static string ToolchainHostName => toolchainHostName;
+ public static string RelativeToolchainDir => relativeToolchainDir;
+
+ static NdkHelper ()
+ {
+ string os;
+ if (RuntimeInformation.IsOSPlatform (OSPlatform.Windows)) {
+ os = "windows";
+ } else if (RuntimeInformation.IsOSPlatform (OSPlatform.OSX)) {
+ os = "darwin";
+ } else if (RuntimeInformation.IsOSPlatform (OSPlatform.Linux)) {
+ os = "linux";
+ } else {
+ throw new InvalidOperationException ($"Unsupported OS {RuntimeInformation.OSDescription}");
+ }
+
+ // We care only about the latest NDK versions, they have only x86_64 versions. We'll need to revisit the code once
+ // native macOS/arm64 toolchain is out.
+ toolchainHostName = $"{os}-x86_64";
+
+ relativeToolchainDir = Path.Combine ("toolchains", "llvm", "prebuilt", toolchainHostName);
+ }
+
+ public static string TranslateAbiToLLVM (string xaAbi)
+ {
+ return xaAbi switch {
+ "armeabi-v7a" => "arm",
+ "arm64-v8a" => "aarch64",
+ "x86" => "i386",
+ "x86_64" => "x86_64",
+ _ => throw new InvalidOperationException ($"Unknown ABI '{xaAbi}'"),
+ };
+ }
+
+ public static string RIDToABI (string rid)
+ {
+ return rid switch {
+ "android-arm" => "armeabi-v7a",
+ "android-arm64" => "arm64-v8a",
+ "android-x86" => "x86",
+ "android-x64" => "x86_64",
+ _ => throw new InvalidOperationException ($"Unknown RID '{rid}'")
+ };
+ }
+ }
+}
diff --git a/src/Xamarin.Android.Build.Tasks/Utilities/ProcessRunner.cs b/src/Xamarin.Android.Build.Tasks/Utilities/ProcessRunner.cs
new file mode 100644
index 00000000000..f7e45f8b822
--- /dev/null
+++ b/src/Xamarin.Android.Build.Tasks/Utilities/ProcessRunner.cs
@@ -0,0 +1,374 @@
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.IO;
+using System.Text;
+using System.Threading;
+
+#if NO_MSBUILD
+using LoggerType = Xamarin.Android.Utilities.XamarinLoggingHelper;
+#else
+using Microsoft.Android.Build.Tasks;
+using Microsoft.Build.Utilities;
+
+using LoggerType = Microsoft.Build.Utilities.TaskLoggingHelper;
+#endif
+
+namespace Xamarin.Android.Tasks
+{
+ class ProcessRunner
+ {
+ public const string StdoutSeverityName = "stdout | ";
+ public const string StderrSeverityName = "stderr | ";
+
+ public enum ErrorReasonCode
+ {
+ NotExecutedYet,
+ NoError,
+ CommandNotFound,
+ ExecutionTimedOut,
+ ExitCodeNotZero,
+ };
+
+ static readonly TimeSpan DefaultProcessTimeout = TimeSpan.FromMinutes (5);
+ static readonly TimeSpan DefaultOutputTimeout = TimeSpan.FromSeconds (10);
+
+ sealed class WriterGuard
+ {
+ public readonly object WriteLock = new object ();
+ public readonly TextWriter Writer;
+
+ public WriterGuard (TextWriter writer)
+ {
+ Writer = writer;
+ }
+ }
+
+ string command;
+ List? arguments;
+ List? stderrSinks;
+ List? stdoutSinks;
+ Dictionary? guardCache;
+ bool defaultStdoutEchoWrapperAdded;
+ ProcessStandardStreamWrapper? defaultStderrEchoWrapper;
+ LoggerType log;
+
+ public string Command => command;
+
+ public string Arguments {
+ get {
+ if (arguments == null)
+ return String.Empty;
+ return String.Join (" ", arguments);
+ }
+ }
+
+ public string FullCommandLine {
+ get {
+ string args = Arguments;
+ if (String.IsNullOrEmpty (args))
+ return command;
+ return $"{command} {args}";
+ }
+ }
+
+ public Dictionary Environment { get; } = new Dictionary (StringComparer.Ordinal);
+ public int ExitCode { get; private set; } = -1;
+ public ErrorReasonCode ErrorReason { get; private set; } = ErrorReasonCode.NotExecutedYet;
+ public bool EchoCmdAndArguments { get; set; } = true;
+ public bool EchoStandardOutput { get; set; }
+ public ProcessStandardStreamWrapper.LogLevel EchoStandardOutputLevel { get; set; } = ProcessStandardStreamWrapper.LogLevel.Message;
+ public bool EchoStandardError { get; set; }
+ public ProcessStandardStreamWrapper.LogLevel EchoStandardErrorLevel { get; set; } = ProcessStandardStreamWrapper.LogLevel.Error;
+ public ProcessStandardStreamWrapper? StandardOutputEchoWrapper { get; set; }
+ public ProcessStandardStreamWrapper? StandardErrorEchoWrapper { get; set; }
+ public Encoding StandardOutputEncoding { get; set; } = Encoding.Default;
+ public Encoding StandardErrorEncoding { get; set; } = Encoding.Default;
+ public TimeSpan StandardOutputTimeout { get; set; } = DefaultOutputTimeout;
+ public TimeSpan StandardErrorTimeout { get; set; } = DefaultOutputTimeout;
+ public TimeSpan ProcessTimeout { get; set; } = DefaultProcessTimeout;
+ public string? WorkingDirectory { get; set; }
+ public Action? StartInfoCallback { get; set; }
+
+ public ProcessRunner (LoggerType logger, string command, params string?[] arguments)
+ : this (logger, command, false, arguments)
+ {}
+
+ public ProcessRunner (LoggerType logger, string command, bool ignoreEmptyArguments, params string?[] arguments)
+ {
+ if (String.IsNullOrEmpty (command)) {
+ throw new ArgumentException ("must not be null or empty", nameof (command));
+ }
+
+ log = logger;
+ this.command = command;
+ AddArgumentsInternal (ignoreEmptyArguments, arguments);
+ }
+
+ public ProcessRunner ClearArguments ()
+ {
+ arguments?.Clear ();
+ return this;
+ }
+
+ public ProcessRunner ClearOutputSinks ()
+ {
+ stderrSinks?.Clear ();
+ stdoutSinks?.Clear ();
+ return this;
+ }
+
+ public ProcessRunner AddArguments (params string?[]? arguments)
+ {
+ return AddArguments (true, arguments);
+ }
+
+ public ProcessRunner AddArguments (bool ignoreEmptyArguments, params string?[]? arguments)
+ {
+ AddArgumentsInternal (ignoreEmptyArguments, arguments);
+ return this;
+ }
+
+ void AddArgumentsInternal (bool ignoreEmptyArguments, params string?[]? arguments)
+ {
+ if (arguments == null || arguments.Length == 0) {
+ return;
+ }
+
+ for (int i = 0; i < arguments.Length; i++) {
+ string? argument = arguments [i]?.Trim ();
+ if (String.IsNullOrEmpty (argument)) {
+ if (ignoreEmptyArguments) {
+ continue;
+ }
+ throw new InvalidOperationException ($"Argument {i} is null or empty");
+ }
+
+ AddQuotedArgument (argument!);
+ }
+ }
+
+ public ProcessRunner AddArgument (string argument)
+ {
+ if (String.IsNullOrEmpty (argument)) {
+ throw new ArgumentException ("must not be null or empty", nameof (argument));
+ }
+
+ AddToList (argument, ref arguments);
+ return this;
+ }
+
+ public ProcessRunner AddQuotedArgument (string argument)
+ {
+ if (String.IsNullOrEmpty (argument)) {
+ throw new ArgumentException ("must not be null or empty", nameof (argument));
+ }
+
+ return AddArgument (QuoteArgument (argument));
+ }
+
+ public static string QuoteArgument (string argument)
+ {
+ if (String.IsNullOrEmpty (argument)) {
+ return String.Empty;
+ }
+
+ if (argument.IndexOf ('"') >= 0) {
+ argument = argument.Replace ("\"", "\\\"");
+ }
+
+ return $"\"{argument}\"";
+ }
+
+ public ProcessRunner AddStandardErrorSink (TextWriter writer)
+ {
+ if (writer == null) {
+ throw new ArgumentNullException (nameof (writer));
+ }
+
+ AddToList (GetGuard (writer), ref stderrSinks);
+ return this;
+ }
+
+ public ProcessRunner AddStandardOutputSink (TextWriter writer)
+ {
+ if (writer == null) {
+ throw new ArgumentNullException (nameof (writer));
+ }
+
+ AddToList (GetGuard (writer), ref stdoutSinks);
+ return this;
+ }
+
+ WriterGuard GetGuard (TextWriter writer)
+ {
+ if (guardCache == null)
+ guardCache = new Dictionary ();
+
+ if (guardCache.TryGetValue (writer, out WriterGuard? ret) && ret != null)
+ return ret;
+
+ ret = new WriterGuard (writer);
+ guardCache.Add (writer, ret);
+ return ret;
+ }
+
+ public bool Run ()
+ {
+ if (EchoStandardOutput) {
+ if (StandardOutputEchoWrapper != null) {
+ AddStandardOutputSink (StandardOutputEchoWrapper);
+ } else if (!defaultStdoutEchoWrapperAdded) {
+ AddStandardOutputSink (new ProcessStandardStreamWrapper (log) { LoggingLevel = EchoStandardOutputLevel, LogPrefix = StdoutSeverityName });
+ defaultStdoutEchoWrapperAdded = true;
+ }
+ }
+
+ if (EchoStandardError) {
+ if (StandardErrorEchoWrapper != null) {
+ AddStandardErrorSink (StandardErrorEchoWrapper);
+ } else if (defaultStderrEchoWrapper == null) {
+ defaultStderrEchoWrapper = new ProcessStandardStreamWrapper (log) { LoggingLevel = EchoStandardErrorLevel, LogPrefix = StderrSeverityName };
+ AddStandardErrorSink (defaultStderrEchoWrapper);
+ }
+ }
+
+ ManualResetEventSlim? stdout_done = null;
+ ManualResetEventSlim? stderr_done = null;
+
+ if (stderrSinks != null && stderrSinks.Count > 0) {
+ stderr_done = new ManualResetEventSlim (false);
+ }
+
+ if (stdoutSinks != null && stdoutSinks.Count > 0) {
+ stdout_done = new ManualResetEventSlim (false);
+ }
+
+ var psi = new ProcessStartInfo (command) {
+ UseShellExecute = false,
+ CreateNoWindow = true,
+ WindowStyle = ProcessWindowStyle.Hidden,
+ RedirectStandardError = stderr_done != null,
+ RedirectStandardOutput = stdout_done != null,
+ };
+
+ if (Environment.Count > 0) {
+ foreach (var kvp in Environment) {
+ psi.Environment.Add (kvp.Key, kvp.Value);
+ }
+ }
+
+ if (arguments != null) {
+ psi.Arguments = String.Join (" ", arguments);
+ }
+
+ if (!String.IsNullOrEmpty (WorkingDirectory)) {
+ psi.WorkingDirectory = WorkingDirectory;
+ }
+
+ if (psi.RedirectStandardError) {
+ StandardErrorEncoding = StandardErrorEncoding;
+ }
+
+ if (psi.RedirectStandardOutput) {
+ StandardOutputEncoding = StandardOutputEncoding;
+ }
+
+ if (StartInfoCallback != null) {
+ StartInfoCallback (psi);
+ }
+
+ var process = new Process {
+ StartInfo = psi
+ };
+
+ if (EchoCmdAndArguments) {
+ log.LogDebugMessage ($"Running: {FullCommandLine}");
+ }
+
+ try {
+ process.Start ();
+ } catch (System.ComponentModel.Win32Exception ex) {
+ log.LogError ($"Process failed to start: {ex.Message}");
+ log.LogDebugMessage (ex.ToString ());
+
+ ErrorReason = ErrorReasonCode.CommandNotFound;
+ return false;
+ }
+
+ if (psi.RedirectStandardError) {
+ process.ErrorDataReceived += (object sender, DataReceivedEventArgs e) => {
+ if (e.Data != null) {
+ WriteOutput (e.Data, stderrSinks!);
+ } else {
+ stderr_done!.Set ();
+ }
+ };
+ process.BeginErrorReadLine ();
+ }
+
+ if (psi.RedirectStandardOutput) {
+ process.OutputDataReceived += (object sender, DataReceivedEventArgs e) => {
+ if (e.Data != null) {
+ WriteOutput (e.Data, stdoutSinks!);
+ } else {
+ stdout_done!.Set ();
+ }
+ };
+ process.BeginOutputReadLine ();
+ }
+
+ int timeout = ProcessTimeout == TimeSpan.MaxValue ? -1 : (int)ProcessTimeout.TotalMilliseconds;
+ bool exited = process.WaitForExit (timeout);
+ if (!exited) {
+ log.LogError ($"Process '{FullCommandLine}' timed out after {ProcessTimeout}");
+ ErrorReason = ErrorReasonCode.ExecutionTimedOut;
+ process.Kill ();
+ }
+
+ // See: https://docs.microsoft.com/en-us/dotnet/api/system.diagnostics.process.waitforexit?view=netframework-4.7.2#System_Diagnostics_Process_WaitForExit)
+ if (psi.RedirectStandardError || psi.RedirectStandardOutput) {
+ process.WaitForExit ();
+ }
+
+ if (stderr_done != null) {
+ stderr_done.Wait (StandardErrorTimeout);
+ }
+
+ if (stdout_done != null) {
+ stdout_done.Wait (StandardOutputTimeout);
+ }
+
+ ExitCode = process.ExitCode;
+ if (ExitCode != 0 && ErrorReason == ErrorReasonCode.NotExecutedYet) {
+ ErrorReason = ErrorReasonCode.ExitCodeNotZero;
+ return false;
+ }
+
+ if (exited) {
+ ErrorReason = ErrorReasonCode.NoError;
+ }
+
+ return exited;
+ }
+
+ void WriteOutput (string data, List sinks)
+ {
+ foreach (WriterGuard wg in sinks) {
+ if (wg == null || wg.Writer == null)
+ continue;
+
+ lock (wg.WriteLock) {
+ wg.Writer.WriteLine (data);
+ }
+ }
+ }
+
+ void AddToList (T item, ref List? list)
+ {
+ if (list == null)
+ list = new List ();
+ list.Add (item);
+ }
+ }
+}
diff --git a/src/Xamarin.Android.Build.Tasks/Utilities/ProcessStandardStreamWrapper.cs b/src/Xamarin.Android.Build.Tasks/Utilities/ProcessStandardStreamWrapper.cs
new file mode 100644
index 00000000000..907052dc473
--- /dev/null
+++ b/src/Xamarin.Android.Build.Tasks/Utilities/ProcessStandardStreamWrapper.cs
@@ -0,0 +1,88 @@
+using System;
+using System.IO;
+using System.Text;
+
+#if NO_MSBUILD
+using LoggerType = Xamarin.Android.Utilities.XamarinLoggingHelper;
+#else
+using Microsoft.Android.Build.Tasks;
+using Microsoft.Build.Utilities;
+
+using LoggerType = Microsoft.Build.Utilities.TaskLoggingHelper;
+#endif
+
+namespace Xamarin.Android.Tasks
+{
+ class ProcessStandardStreamWrapper : TextWriter
+ {
+ public enum LogLevel
+ {
+ Error,
+ Warning,
+ Info,
+ Message,
+ Debug,
+ }
+
+ LoggerType log;
+
+ public LogLevel LoggingLevel { get; set; } = LogLevel.Debug;
+ public string? LogPrefix { get; set; }
+
+ public override Encoding Encoding => Encoding.Default;
+
+ public ProcessStandardStreamWrapper (LoggerType logger)
+ {
+ log = logger;
+ }
+
+ public override void WriteLine (string? value)
+ {
+ DoWrite (value);
+ }
+
+ protected virtual string? PreprocessMessage (string? message, out bool ignoreLine)
+ {
+ ignoreLine = false;
+ return message;
+ }
+
+ void DoWrite (string? message)
+ {
+ bool ignoreLine;
+
+ message = PreprocessMessage (message, out ignoreLine) ?? String.Empty;
+ if (ignoreLine) {
+ return;
+ }
+
+ if (!String.IsNullOrEmpty (LogPrefix)) {
+ message = $"{LogPrefix}{message}";
+ }
+
+ switch (LoggingLevel) {
+ case LogLevel.Error:
+ log.LogError (message);
+ break;
+
+ case LogLevel.Warning:
+ log.LogWarning (message);
+ break;
+
+ case LogLevel.Info:
+ case LogLevel.Message:
+ log.LogMessage (message);
+ break;
+
+ case LogLevel.Debug:
+ log.LogDebugMessage (message);
+ break;
+
+ default:
+ log.LogWarning ($"Unsupported log level {LoggingLevel}");
+ log.LogMessage (message);
+ break;
+ }
+ }
+ }
+}
diff --git a/src/Xamarin.Android.Build.Tasks/Utilities/PythonRunner.cs b/src/Xamarin.Android.Build.Tasks/Utilities/PythonRunner.cs
new file mode 100644
index 00000000000..dc60b76a5c5
--- /dev/null
+++ b/src/Xamarin.Android.Build.Tasks/Utilities/PythonRunner.cs
@@ -0,0 +1,35 @@
+using System.Threading.Tasks;
+
+using Microsoft.Build.Utilities;
+
+namespace Xamarin.Android.Tasks
+{
+ class PythonRunner : ToolRunner
+ {
+ public PythonRunner (TaskLoggingHelper logger, string pythonPath)
+ : base (logger, pythonPath)
+ {}
+
+ public async Task RunScript (string scriptPath, params string[] arguments)
+ {
+ ProcessRunner runner = CreateProcessRunner ();
+
+ if (arguments != null && arguments.Length > 0) {
+ foreach (string arg in arguments) {
+ runner.AddArgument (arg);
+ }
+ }
+
+ return await RunPython (runner);
+ }
+
+ async Task RunPython (ProcessRunner runner)
+ {
+ return await RunTool (
+ () => {
+ return runner.Run ();
+ }
+ );
+ }
+ }
+}
diff --git a/src/Xamarin.Android.Build.Tasks/Utilities/ToolRunner.cs b/src/Xamarin.Android.Build.Tasks/Utilities/ToolRunner.cs
new file mode 100644
index 00000000000..876a1fb33d2
--- /dev/null
+++ b/src/Xamarin.Android.Build.Tasks/Utilities/ToolRunner.cs
@@ -0,0 +1,92 @@
+using System;
+using System.IO;
+using System.Text;
+using System.Threading.Tasks;
+
+#if NO_MSBUILD
+using LoggerType = Xamarin.Android.Utilities.XamarinLoggingHelper;
+#else // def NO_MSBUILD
+using Microsoft.Android.Build.Tasks;
+
+using LoggerType = Microsoft.Build.Utilities.TaskLoggingHelper;
+#endif // ndef NO_MSBUILD
+
+using TPLTask = System.Threading.Tasks.Task;
+
+namespace Xamarin.Android.Tasks
+{
+ abstract class ToolRunner
+ {
+ protected abstract class ToolOutputSink : TextWriter
+ {
+ protected string LogLinePrefix { get; set; } = String.Empty;
+
+ LoggerType log;
+
+ public override Encoding Encoding => Encoding.Default;
+
+ protected ToolOutputSink (LoggerType logger)
+ {
+ log = logger;
+ }
+
+ public override void WriteLine (string? value)
+ {
+ string message;
+
+ if (!String.IsNullOrEmpty (LogLinePrefix)) {
+ message = $"{LogLinePrefix}> {value ?? String.Empty}";
+ } else {
+ message = value ?? String.Empty;
+ }
+
+ log.LogDebugMessage (message);
+ }
+ }
+
+ static readonly TimeSpan DefaultProcessTimeout = TimeSpan.FromMinutes (15);
+
+ protected LoggerType Logger { get; }
+ public string ToolPath { get; }
+ public bool EchoCmdAndArguments { get; set; } = true;
+ public bool EchoStandardError { get; set; } = true;
+ public bool EchoStandardOutput { get; set; }
+ public virtual TimeSpan ProcessTimeout { get; set; } = DefaultProcessTimeout;
+
+ protected ToolRunner (LoggerType logger, string toolPath)
+ {
+ if (String.IsNullOrEmpty (toolPath)) {
+ throw new ArgumentException ("must not be null or empty", nameof (toolPath));
+ }
+
+ Logger = logger;
+ ToolPath = toolPath;
+ }
+
+ protected virtual ProcessRunner CreateProcessRunner (params string?[]? initialParams)
+ {
+ var runner = new ProcessRunner (Logger, ToolPath) {
+ ProcessTimeout = ProcessTimeout,
+ EchoCmdAndArguments = EchoCmdAndArguments,
+ EchoStandardError = EchoStandardError,
+ EchoStandardOutput = EchoStandardOutput,
+ };
+
+ runner.AddArguments (initialParams);
+ return runner;
+ }
+
+ protected virtual async Task RunTool (Func runner)
+ {
+ return await TPLTask.Run (runner);
+ }
+
+ protected void SetupOutputSinks (ProcessRunner runner, TextWriter stdoutSink, TextWriter? stderrSink = null, bool ignoreStderr = false)
+ {
+ if (!ignoreStderr) {
+ runner.AddStandardErrorSink (stderrSink ?? stdoutSink);
+ }
+ runner.AddStandardOutputSink (stdoutSink);
+ }
+ }
+}
diff --git a/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Build.Tasks.csproj b/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Build.Tasks.csproj
index b677c0ece0e..e07b13d6864 100644
--- a/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Build.Tasks.csproj
+++ b/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Build.Tasks.csproj
@@ -412,6 +412,12 @@
Resource.Designer.snk
+
+ debug-app.sh
+
+
+ debug-app.ps1
+
diff --git a/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets b/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets
index c642f4507a7..77bb835fc92 100644
--- a/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets
+++ b/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets
@@ -333,6 +333,7 @@ Copyright (C) 2011-2012 Xamarin. All rights reserved.
<_AndroidAotStripLibraries Condition=" '$(_AndroidAotStripLibraries)' == '' And '$(AndroidIncludeDebugSymbols)' != 'true' ">True
+ <_AndroidStripNativeLibraries Condition=" '$(_AndroidStripNativeLibraries)' == '' And '$(AndroidIncludeDebugSymbols)' != 'true' ">True
false
true
True
@@ -370,6 +371,7 @@ Copyright (C) 2011-2012 Xamarin. All rights reserved.
Imports
*******************************************
-->
+
@@ -1565,7 +1567,10 @@ because xbuild doesn't support framework reference assemblies.
LinkingEnabled="$(_LinkingEnabled)"
HaveMultipleRIDs="$(_HaveMultipleRIDs)"
IntermediateOutputDirectory="$(IntermediateOutputPath)"
- Environments="@(AndroidEnvironment);@(LibraryEnvironments)">
+ Environments="@(AndroidEnvironment);@(LibraryEnvironments)"
+ NativeDebuggingEnabled="$(_AndroidEnableNativeDebugging)"
+ NativeCodeProfilingEnabled="$(_AndroidEnableNativeCodeProfiling)"
+ DeviceSdkVersion="$(_AndroidDeviceSdkVersion)">
@@ -2080,10 +2085,15 @@ because xbuild doesn't support framework reference assemblies.
DependsOnTargets="_CompileNativeAssemblySources;_PrepareApplicationSharedLibraryItems"
Inputs="@(_NativeAssemblyTarget)"
Outputs="@(_ApplicationSharedLibrary)">
+
+ <_KeepDebugInfo Condition=" '$(_AndroidStripNativeLibraries)' != 'true' ">True
+ <_KeepDebugInfo Condition=" '$(_AndroidStripNativeLibraries)' == 'true' ">False
+
+
@@ -2162,7 +2172,9 @@ because xbuild doesn't support framework reference assemblies.
CheckedBuild="$(_AndroidCheckedBuild)"
RuntimeConfigBinFilePath="$(_BinaryRuntimeConfigPath)"
ExcludeFiles="@(AndroidPackagingOptionsExclude)"
- UseAssemblyStore="$(AndroidUseAssemblyStore)">
+ UseAssemblyStore="$(AndroidUseAssemblyStore)"
+ NativeCodeProfilingEnabled="$(_AndroidEnableNativeCodeProfiling)"
+ DeviceSdkVersion="$(_AndroidDeviceSdkVersion)">
+ UseAssemblyStore="$(AndroidUseAssemblyStore)"
+ NativeCodeProfilingEnabled="$(_AndroidEnableNativeCodeProfiling)"
+ DeviceSdkVersion="$(_AndroidDeviceSdkVersion)">
+#include
+#include
+#include
+#include
+#include
+#include
+
+#include "android-system.hh"
+#include "globals.hh"
+#include "logger.hh"
+#include "simpleperf.hh"
+#include "strings.hh"
+
+using namespace xamarin::android::internal;
+
+std::string
+RecordOptions::get_default_output_filename () noexcept
+{
+ time_t t = time (nullptr);
+
+ struct tm tm;
+ if (localtime_r (&t, &tm) != &tm) {
+ return "perf.data";
+ }
+
+ char* buf = nullptr;
+
+ // TODO: don't use asprintf
+ asprintf (&buf, "perf-%02d-%02d-%02d-%02d-%02d.data", tm.tm_mon + 1, tm.tm_mday, tm.tm_hour, tm.tm_min, tm.tm_sec);
+
+ std::string result = buf;
+ free (buf);
+
+ return result;
+}
+
+std::vector
+RecordOptions::to_record_args () noexcept
+{
+ std::vector args;
+ if (output_filename.empty ()) {
+ output_filename = get_default_output_filename ();
+ }
+
+ args.insert (args.end (), {"-o", output_filename});
+ args.insert (args.end (), {"-e", event});
+ args.insert (args.end (), {"-f", std::to_string(freq)});
+
+ if (duration_in_seconds != 0.0) {
+ args.insert (args.end (), {"--duration", std::to_string (duration_in_seconds)});
+ }
+
+ if (threads.empty ()) {
+ args.insert (args.end (), {"-p", std::to_string (getpid ())});
+ } else {
+ std::string threads_arg;
+
+ for (auto const& thread_id : threads) {
+ if (!threads_arg.empty ()) {
+ threads_arg.append (",");
+ }
+
+ threads_arg.append (std::to_string (thread_id));
+ }
+
+ args.insert(args.end (), {"-t", threads_arg});
+ }
+
+ if (dwarf_callgraph) {
+ args.push_back ("-g");
+ } else if (fp_callgraph) {
+ args.insert (args.end (), {"--call-graph", "fp"});
+ }
+
+ if (trace_offcpu) {
+ args.push_back ("--trace-offcpu");
+ }
+
+ return args;
+}
+
+ProfileSession::ProfileSession () noexcept
+{
+ std::string input_file {"/proc/self/cmdline"};
+ FILE* fp = fopen (input_file.c_str (), "r");
+ if (fp == nullptr) {
+ log_error (LOG_DEFAULT, "simpleperf: failed to open %s: %s", input_file.c_str (), strerror (errno));
+ return;
+ }
+
+ std::string s = read_file (fp, input_file);
+ for (size_t i = 0; i < s.size (); i++) {
+ if (s[i] == '\0') {
+ s = s.substr (0, i);
+ break;
+ }
+ }
+
+ std::string app_data_dir = "/data/data/" + s;
+ uid_t uid = getuid ();
+ if (uid >= AID_USER_OFFSET) {
+ int user_id = uid / AID_USER_OFFSET;
+ app_data_dir = "/data/user/" + std::to_string (user_id) + "/" + s;
+ }
+
+ session_valid = true;
+}
+
+std::string
+ProfileSession::read_file (FILE* fp, std::string const& path) noexcept
+{
+ std::string s;
+ if (fp == nullptr) {
+ return s;
+ }
+
+ constexpr size_t BUF_SIZE = 200;
+ std::array buf;
+
+ while (true) {
+ size_t n = fread (buf.data (), 1, buf.size (), fp);
+ if (n < buf.size ()) {
+ if (ferror (fp)) {
+ log_warn (LOG_DEFAULT, "simpleperf: an error occurred while reading input file %s: %s", path.c_str (), strerror (errno));
+ }
+
+ break;
+ }
+
+ s.insert (s.end (), buf.data (), buf.data () + n);
+ }
+
+ fclose (fp);
+ return s;
+}
+
+bool
+ProfileSession::session_is_valid () const noexcept
+{
+ if (session_valid) {
+ return true;
+ }
+
+ log_warn (LOG_DEFAULT, "simpleperf: profiling session object hasn't been initialized properly, profiling will NOT produce any results");
+ return false;
+}
+
+std::string
+ProfileSession::find_simpleperf_in_temp_dir () const noexcept
+{
+ const std::string path = "/data/local/tmp/simpleperf";
+ if (!is_executable_file (path)) {
+ return "";
+ }
+ // Copy it to app_dir to execute it.
+ const std::string to_path = app_data_dir_ + "/simpleperf";
+ if (!run_cmd ({"/system/bin/cp", path.c_str(), to_path.c_str()}, nullptr)) {
+ return "";
+ }
+
+ // For apps with target sdk >= 29, executing app data file isn't allowed.
+ // For android R, app context isn't allowed to use perf_event_open.
+ // So test executing downloaded simpleperf.
+ std::string s;
+ if (!run_cmd ({to_path.c_str(), "list", "sw"}, &s)) {
+ return "";
+ }
+
+ if (s.find ("cpu-clock") == std::string::npos) {
+ return "";
+ }
+
+ return to_path;
+}
+
+bool
+ProfileSession::run_cmd (std::vector args, std::string* standard_output) noexcept
+{
+ std::array stdout_fd;
+ if (pipe (stdout_fd.data ()) != 0) {
+ return false;
+ }
+
+ args.push_back (nullptr);
+
+ // Fork handlers (like gsl_library_close) may hang in a multi-thread environment.
+ // So we use vfork instead of fork to avoid calling them.
+ int pid = vfork ();
+ if (pid == -1) {
+ log_warn (LOG_DEFAULT, "simpleperf: `vfork` failed: %s", strerror (errno));
+ return false;
+ }
+
+ if (pid == 0) {
+ // child process
+ close (stdout_fd[0]);
+ dup2 (stdout_fd[1], 1);
+ close (stdout_fd[1]);
+
+ execvp (const_cast(args[0]), const_cast(args.data ()));
+
+ log_error (LOG_DEFAULT, "simpleperf: failed to run %s: %s", args[0], strerror (errno));
+ _exit (1);
+ }
+
+ // parent process
+ close (stdout_fd[1]);
+
+ int status;
+ pid_t result = TEMP_FAILURE_RETRY (waitpid (pid, &status, 0));
+ if (result == -1) {
+ log_error (LOG_DEFAULT, "simpleperf: failed to call waitpid: %s", strerror (errno));
+ }
+
+ if (!WIFEXITED(status) || WEXITSTATUS(status) != 0) {
+ return false;
+ }
+
+ if (standard_output == nullptr) {
+ close (stdout_fd[0]);
+ } else {
+ *standard_output = read_file (fdopen (stdout_fd[0], "r"), "pipe");
+ }
+
+ return true;
+}
+
+bool
+ProfileSession::is_executable_file (const std::string& path) noexcept
+{
+ struct stat st;
+
+ if (stat (path.c_str (), &st) != 0) {
+ return false;
+ }
+
+ return S_ISREG(st.st_mode) && ((st.st_mode & S_IXUSR) == S_IXUSR);
+}
+
+std::string
+ProfileSession::find_simpleperf () const noexcept
+{
+ // 1. Try /data/local/tmp/simpleperf first. Probably it's newer than /system/bin/simpleperf.
+ std::string simpleperf_path = find_simpleperf_in_temp_dir ();
+ if (!simpleperf_path.empty()) {
+ return simpleperf_path;
+ }
+
+ // 2. Try /system/bin/simpleperf, which is available on Android >= Q.
+ simpleperf_path = "/system/bin/simpleperf";
+ if (is_executable_file (simpleperf_path)) {
+ return simpleperf_path;
+ }
+
+ log_error (LOG_DEFAULT, "simpleperf: can't find simpleperf on device. Please run api_profiler.py.");
+ return "";
+}
+
+bool
+ProfileSession::check_if_perf_enabled () noexcept
+{
+ dynamic_local_string prop;
+
+ if (androidSystem.monodroid_get_system_property ("persist.simpleperf.profile_app_uid", prop) <= 0) {
+ return false;
+ }
+
+ if (prop.get () == std::to_string (getuid ())) {
+ prop.clear ();
+
+ androidSystem.monodroid_get_system_property ("persist.simpleperf.profile_app_expiration_time", prop);
+ if (!prop.empty ()) {
+ errno = 0;
+ long expiration_time = strtol (prop.get (), nullptr, 10);
+ if (errno == 0 && expiration_time > time (nullptr)) {
+ return true;
+ }
+ }
+ }
+
+ if (androidSystem.monodroid_get_system_property ("security.perf_harden", prop) <= 0 || prop.empty ()) {
+ return true;
+ }
+
+ if (prop.get ()[0] == '1') {
+ log_error (LOG_DEFAULT, "simpleperf: recording app isn't enabled on the device. Please run api_profiler.py.");
+ return false;
+ }
+
+ return true;
+}
+
+bool
+ProfileSession::create_simpleperf_data_dir () const noexcept
+{
+ struct stat st;
+ if (stat (simpleperf_data_dir_.c_str (), &st) == 0 && S_ISDIR (st.st_mode)) {
+ return true;
+ }
+
+ if (mkdir (simpleperf_data_dir_.c_str (), 0700) == -1) {
+ log_error (LOG_DEFAULT, "simpleperf: failed to create simpleperf data dir %s: %s", simpleperf_data_dir_.c_str(), strerror (errno));
+ return false;
+ }
+
+ return true;
+}
+
+bool
+ProfileSession::create_simpleperf_process (std::string const& simpleperf_path, std::vector const& record_args) noexcept
+{
+ // 1. Create control/reply pips.
+ std::array control_fd { -1, -1 };
+ std::array reply_fd { -1, -1 };
+
+ if (pipe (control_fd.data ()) != 0 || pipe (reply_fd.data ()) != 0) {
+ log_error (LOG_DEFAULT, "simpleperf: failed to call pipe: %s", strerror (errno));
+ return false;
+ }
+
+ // 2. Prepare simpleperf arguments.
+ std::vector args;
+ args.emplace_back (simpleperf_path);
+ args.emplace_back ("record");
+ args.emplace_back ("--log-to-android-buffer");
+ args.insert (args.end (), {"--log", "debug"});
+ args.emplace_back ("--stdio-controls-profiling");
+ args.emplace_back ("--in-app");
+ args.insert (args.end (), {"--tracepoint-events", "/data/local/tmp/tracepoint_events"});
+ args.insert (args.end (), record_args.begin (), record_args.end ());
+
+ char* argv[args.size () + 1];
+ for (size_t i = 0; i < args.size (); ++i) {
+ argv[i] = &args[i][0];
+ }
+ argv[args.size ()] = nullptr;
+
+ // 3. Start simpleperf process.
+ // Fork handlers (like gsl_library_close) may hang in a multi-thread environment.
+ // So we use vfork instead of fork to avoid calling them.
+ int pid = vfork ();
+ if (pid == -1) {
+ auto close_fds = [](std::array const& fds) {
+ for (auto fd : fds) {
+ close (fd);
+ }
+ };
+
+ log_error (LOG_DEFAULT, "simpleperf: failed to fork: %s", strerror (errno));
+ close_fds (control_fd);
+ close_fds (reply_fd);
+
+ return false;
+ }
+
+ if (pid == 0) {
+ // child process
+ close (control_fd[1]);
+ dup2 (control_fd[0], 0); // simpleperf read control cmd from fd 0.
+ close (control_fd[0]);
+ close (reply_fd[0]);
+ dup2 (reply_fd[1], 1); // simpleperf writes reply to fd 1.
+ close (reply_fd[0]);
+ chdir (simpleperf_data_dir_.c_str());
+ execvp (argv[0], argv);
+
+ log_fatal (LOG_DEFAULT, "simpleperf: failed to call exec: %s", strerror (errno));
+ }
+
+ // parent process
+ close (control_fd[0]);
+ control_fd_ = control_fd[1];
+ close (reply_fd[1]);
+ reply_fd_ = reply_fd[0];
+ simpleperf_pid_ = pid;
+
+ // 4. Wait until simpleperf starts recording.
+ std::string start_flag = read_reply ();
+ if (start_flag != "started") {
+ log_error (LOG_DEFAULT, "simpleperf: failed to receive simpleperf start flag");
+ return false;
+ }
+
+ return true;
+}
+
+void
+ProfileSession::start_recording (std::vector const& record_args) noexcept
+{
+ if (!session_is_valid () || !check_if_perf_enabled ()) {
+ return;
+ }
+
+ std::lock_guard guard {lock_};
+ if (state_ != State::NOT_YET_STARTED) {
+ log_error (LOG_DEFAULT, "simpleperf: start_recording: session in wrong state %d", state_);
+ }
+
+ for (auto const& arg : record_args) {
+ if (arg == "--trace-offcpu") {
+ trace_offcpu_ = true;
+ }
+ }
+
+ std::string simpleperf_path = find_simpleperf ();
+
+ if (!create_simpleperf_data_dir ()) {
+ return;
+ }
+
+ if (!create_simpleperf_process (simpleperf_path, record_args)) {
+ return;
+ }
+
+ state_ = State::STARTED;
+}
+
+void
+ProfileSession::pause_recording () noexcept
+{
+ if (!session_is_valid ()) {
+ return;
+ }
+
+ std::lock_guard guard(lock_);
+ if (state_ != State::STARTED) {
+ log_error (LOG_DEFAULT, "simpleperf: pause_recording: session in wrong state %d", state_);
+ return;
+ }
+
+ if (trace_offcpu_) {
+ log_warn (LOG_DEFAULT, "simpleperf: --trace-offcpu doesn't work well with pause/resume recording");
+ }
+
+ if (!send_cmd ("pause")) {
+ return;
+ }
+
+ state_ = State::PAUSED;
+}
+
+void
+ProfileSession::resume_recording () noexcept
+{
+ if (!session_is_valid ()) {
+ return;
+ }
+
+ std::lock_guard guard {lock_};
+
+ if (state_ != State::PAUSED) {
+ log_error (LOG_DEFAULT, "simpleperf: resume_recording: session in wrong state %d", state_);
+ }
+
+ if (!send_cmd ("resume")) {
+ return;
+ }
+
+ state_ = State::STARTED;
+}
+
+void
+ProfileSession::stop_recording () noexcept
+{
+ if (!session_is_valid ()) {
+ return;
+ }
+
+ std::lock_guard guard {lock_};
+
+ if (state_ != State::STARTED && state_ != State::PAUSED) {
+ log_error (LOG_DEFAULT, "simpleperf: stop_recording: session in wrong state %d", state_);
+ return;
+ }
+
+ // Send SIGINT to simpleperf to stop recording.
+ if (kill (simpleperf_pid_, SIGINT) == -1) {
+ log_error (LOG_DEFAULT, "simpleperf: failed to stop simpleperf: %s", strerror (errno));
+ return;
+ }
+
+ int status;
+ pid_t result = TEMP_FAILURE_RETRY(waitpid(simpleperf_pid_, &status, 0));
+ if (result == -1) {
+ log_error (LOG_DEFAULT, "simpleperf: failed to call waitpid: %s", strerror (errno));
+ return;
+ }
+
+ if (!WIFEXITED (status) || WEXITSTATUS (status) != 0) {
+ log_error (LOG_DEFAULT, "simpleperf: simpleperf exited with error, status = 0x%x", status);
+ return;
+ }
+
+ state_ = State::STOPPED;
+}
+
+std::string
+ProfileSession::read_reply () noexcept
+{
+ std::string s;
+ while (true) {
+ char c;
+ ssize_t result = TEMP_FAILURE_RETRY (read (reply_fd_, &c, 1));
+ if (result <= 0 || c == '\n') {
+ break;
+ }
+ s.push_back(c);
+ }
+
+ return s;
+}
+
+bool
+ProfileSession::send_cmd (std::string const& cmd) noexcept
+{
+ std::string data = cmd + "\n";
+
+ if (TEMP_FAILURE_RETRY (write (control_fd_, &data[0], data.size())) != static_cast(data.size ())) {
+ log_error (LOG_DEFAULT, "simpleperf: failed to send cmd to simpleperf: %s", strerror (errno));
+ return false;
+ }
+
+ if (read_reply () != "ok") {
+ log_error (LOG_DEFAULT, "simpleperf: failed to run cmd in simpleperf: %s", cmd.c_str ());
+ return false;
+ }
+
+ return true;
+}
diff --git a/src/monodroid/jni/simpleperf.hh b/src/monodroid/jni/simpleperf.hh
new file mode 100644
index 00000000000..2f737d73b4c
--- /dev/null
+++ b/src/monodroid/jni/simpleperf.hh
@@ -0,0 +1,246 @@
+//
+// SPDX-License-Identifier: MIT
+// SPDX-License-Identifier: Apache-2.0
+//
+// Support for managing simpleperf session from within the runtime
+//
+// Heavily based on https://android.googlesource.com/platform/system/extras/+/refs/tags/android-13.0.0_r11/simpleperf/app_api/cpp/simpleperf.cpp
+//
+// Because the original code is licensed under the `Apache-2.0` license, this file is dual-licensed under the `MIT` and
+// `Apache-2.0` licenses
+//
+// We can't use the original source because of the C++ stdlib features it uses (I/O streams which we can't use because
+// we don't reference libc++)
+//
+// The API is very similar to the original, with occasional stylistic changes and some behavioral changes (for instance,
+// we do not abort the process if tracing fails - instead we log errors and continue running). Portions are
+// reimplemented in a way that better fits Xamarin.Android's purposes. We also don't split the classes into interface
+// and implementation bits - there's no need since we use this API entirely internally.
+//
+// Stylistic changes include indentation and renaming of all methods to use lower-case words separated by underscores instead of
+// camel case (because that's the style Xamarin.Android sources use). Names are otherwise the same as in the original
+// code, for easier porting of potential changes.
+//
+#if !defined (__SIMPLEPERF_HH)
+#define __SIMPLEPERF_HH
+
+#include
+#include
+
+#include "cppcompat.hh"
+
+namespace xamarin::android::internal
+{
+ enum class RecordCmd
+ {
+ CMD_PAUSE_RECORDING = 1,
+ CMD_RESUME_RECORDING,
+ };
+
+ /**
+ * RecordOptions sets record options used by ProfileSession. The options are
+ * converted to a string list in toRecordArgs(), which is then passed to
+ * `simpleperf record` cmd. Run `simpleperf record -h` or
+ * `run_simpleperf_on_device.py record -h` for help messages.
+ *
+ * Example:
+ * RecordOptions options;
+ * options.set_duration (3).record_dwarf_call_graph ().set_output_filename ("perf.data");
+ * ProfileSession session;
+ * session.start_recording (options);
+ */
+ class RecordOptions final
+ {
+ public:
+ /**
+ * Set output filename. Default is perf-----.data.
+ * The file will be generated under simpleperf_data/.
+ */
+ RecordOptions& set_output_filename (std::string const& filename) noexcept
+ {
+ output_filename = filename;
+ return *this;
+ }
+
+ /**
+ * Set event to record. Default is cpu-cycles. See `simpleperf list` for all available events.
+ */
+ RecordOptions& set_event (std::string const& wanted_event) noexcept
+ {
+ event = wanted_event;
+ return *this;
+ }
+
+ /**
+ * Set how many samples to generate each second running. Default is 4000.
+ */
+ RecordOptions& set_sample_frequency (size_t wanted_freq) noexcept
+ {
+ freq = wanted_freq;
+ return *this;
+ }
+
+ /**
+ * Set record duration. The record stops after `durationInSecond` seconds. By default,
+ * record stops only when stopRecording() is called.
+ */
+ RecordOptions& set_duration (double wanted_duration_in_seconds) noexcept
+ {
+ duration_in_seconds = wanted_duration_in_seconds;
+ return *this;
+ }
+
+ /**
+ * Record some threads in the app process. By default, record all threads in the process.
+ */
+ RecordOptions& set_sample_threads (std::vector const& wanted_threads) noexcept
+ {
+ threads = wanted_threads;
+ return *this;
+ }
+
+ /**
+ * Record dwarf based call graph. It is needed to get Java callstacks.
+ */
+ RecordOptions& record_dwarf_call_graph () noexcept
+ {
+ dwarf_callgraph = true;
+ fp_callgraph = false;
+ return *this;
+ }
+
+ /**
+ * Record frame pointer based call graph. It is suitable to get C++ callstacks on 64bit devices.
+ */
+ RecordOptions& record_frame_pointer_call_graph () noexcept
+ {
+ fp_callgraph = true;
+ dwarf_callgraph = false;
+ return *this;
+ }
+
+ /**
+ * Trace context switch info to show where threads spend time off cpu.
+ */
+ RecordOptions& trace_off_cpu () noexcept
+ {
+ trace_offcpu = true;
+ return *this;
+ }
+
+ /**
+ * Translate record options into arguments for `simpleperf record` cmd.
+ */
+ std::vector to_record_args () noexcept;
+
+ private:
+ static std::string get_default_output_filename () noexcept;
+
+ private:
+ std::string output_filename;
+ std::string event = "cpu-cycles";
+ size_t freq = 4000;
+ double duration_in_seconds = 0.0;
+ std::vector threads;
+ bool dwarf_callgraph = false;
+ bool fp_callgraph = false;
+ bool trace_offcpu = false;
+ };
+
+
+ enum class State
+ {
+ NOT_YET_STARTED,
+ STARTED,
+ PAUSED,
+ STOPPED,
+ };
+
+ /**
+ * ProfileSession uses `simpleperf record` cmd to generate a recording file.
+ * It allows users to start recording with some options, pause/resume recording
+ * to only profile interested code, and stop recording.
+ *
+ * Example:
+ * RecordOptions options;
+ * options.set_dwarf_call_graph ();
+ * ProfileSession session;
+ * session.start_recording (options);
+ * sleep(1);
+ * session.pause_recording ();
+ * sleep(1);
+ * session.resume_recording ();
+ * sleep(1);
+ * session.stop_recording ();
+ *
+ * It logs when error happens, does not abort the process. To read error messages of simpleperf record
+ * process, filter logcat with `simpleperf`.
+ */
+ class ProfileSession final
+ {
+ private:
+ static constexpr uid_t AID_USER_OFFSET = 100000;
+
+ public:
+ ProfileSession () noexcept;
+
+ /**
+ * Start recording.
+ * @param options RecordOptions
+ */
+ void start_recording (RecordOptions& options) noexcept
+ {
+ start_recording (options.to_record_args ());
+ }
+
+ /**
+ * Start recording.
+ * @param args arguments for `simpleperf record` cmd.
+ */
+ void start_recording (std::vector const& record_args) noexcept;
+
+ /**
+ * Pause recording. No samples are generated in paused state.
+ */
+ void pause_recording () noexcept;
+
+ /**
+ * Resume a paused session.
+ */
+ void resume_recording () noexcept;
+
+ /**
+ * Stop recording and generate a recording file under appDataDir/simpleperf_data/.
+ */
+ void stop_recording () noexcept;
+
+ private:
+ bool session_is_valid () const noexcept;
+
+ std::string find_simpleperf_in_temp_dir () const noexcept;
+ std::string find_simpleperf () const noexcept;
+ bool create_simpleperf_data_dir () const noexcept;
+ bool create_simpleperf_process (std::string const& simpleperf_path, std::vector const& record_args) noexcept;
+ std::string read_reply () noexcept;
+ bool send_cmd (std::string const& cmd) noexcept;
+
+ static std::string read_file (FILE* fp, std::string const& path) noexcept;
+ static bool is_executable_file (const std::string& path) noexcept;
+ static bool run_cmd (std::vector args, std::string* standard_output) noexcept;
+ static bool check_if_perf_enabled () noexcept;
+
+ private:
+ // Clunky, but we want error in initialization to be non-fatal to the app
+ bool session_valid = false;
+
+ const std::string app_data_dir_;
+ const std::string simpleperf_data_dir_;
+ std::mutex lock_; // Protect all members below.
+ State state_ = State::NOT_YET_STARTED;
+ pid_t simpleperf_pid_ = -1;
+ int control_fd_ = -1;
+ int reply_fd_ = -1;
+ bool trace_offcpu_ = false;
+ };
+}
+#endif // ndef __SIMPLEPERF_HH
diff --git a/tools/debug-session-prep/Debug.Session.Prep/AndroidDevice.cs b/tools/debug-session-prep/Debug.Session.Prep/AndroidDevice.cs
new file mode 100644
index 00000000000..c91199f532a
--- /dev/null
+++ b/tools/debug-session-prep/Debug.Session.Prep/AndroidDevice.cs
@@ -0,0 +1,435 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+
+using Xamarin.Android.Tasks;
+using Xamarin.Android.Utilities;
+
+namespace Xamarin.Debug.Session.Prep;
+
+class AndroidDevice
+{
+ const string ServerLauncherScriptName = "xa_start_lldb_server.sh";
+
+ static readonly string[] abiProperties = {
+ // new properties
+ "ro.product.cpu.abilist",
+
+ // old properties
+ "ro.product.cpu.abi",
+ "ro.product.cpu.abi2",
+ };
+
+ static readonly string[] serialNumberProperties = {
+ "ro.serialno",
+ "ro.boot.serialno",
+ };
+
+ string packageName;
+ string adbPath;
+ string[] supportedAbis;
+ int apiLevel = -1;
+ string? appDataDir;
+ string? appLldbBaseDir;
+ string? appLldbBinDir;
+ string? appLldbLogDir;
+ string? appLldbTmpDir;
+ string? mainAbi;
+ string? mainArch;
+ string[]? availableAbis;
+ string[]? availableArches;
+ string? serialNumber;
+ bool appIs64Bit;
+ string? deviceLdd;
+ string? deviceDebugServerPath;
+ string? deviceDebugServerScriptPath;
+ string outputDir;
+
+ XamarinLoggingHelper log;
+ AdbRunner adb;
+ AndroidNdk ndk;
+
+ public int ApiLevel => apiLevel;
+ public string[] AvailableAbis => availableAbis ?? new string[] {};
+ public string[] AvailableArches => availableArches ?? new string[] {};
+ public string MainArch => mainArch ?? String.Empty;
+ public string MainAbi => mainAbi ?? String.Empty;
+ public string SerialNumber => serialNumber ?? String.Empty;
+ public string DebugServerLauncherScriptPath => deviceDebugServerScriptPath;
+ public string LldbBaseDir => appLldbBaseDir;
+ public AdbRunner AdbRunner => adb;
+
+ public AndroidDevice (XamarinLoggingHelper log, AndroidNdk ndk, string outputDir, string adbPath, string packageName, string[] supportedAbis, string? adbTargetDevice = null)
+ {
+ this.adbPath = adbPath;
+ this.log = log;
+ this.packageName = packageName;
+ this.supportedAbis = supportedAbis;
+ this.ndk = ndk;
+ this.outputDir = outputDir;
+
+ adb = new AdbRunner (log, adbPath, adbTargetDevice);
+ }
+
+ // TODO: implement manual error checking on API 21, since `adb` won't ever return any error code other than 0 - we need to look at the output of any command to determine
+ // whether or not it was successful. Ugh.
+ public bool GatherInfo ()
+ {
+ (bool success, string output) = adb.GetPropertyValue ("ro.build.version.sdk").Result;
+ if (!success || String.IsNullOrEmpty (output) || !Int32.TryParse (output, out apiLevel)) {
+ log.ErrorLine ("Unable to determine connected device's API level");
+ return false;
+ }
+
+ // Warn on old Pixel C firmware (b/29381985). Newer devices may have Yama
+ // enabled but still work with ndk-gdb (b/19277529).
+ (success, output) = adb.Shell ("cat", "/proc/sys/kernel/yama/ptrace_scope", "2>/dev/null").Result;
+ if (success &&
+ YamaOK (output.Trim ()) &&
+ PropertyIsEqualTo (adb.GetPropertyValue ("ro.build.product").Result, "dragon") &&
+ PropertyIsEqualTo (adb.GetPropertyValue ("ro.product.name").Result, "ryu")
+ ) {
+ log.WarningLine ("WARNING: The device uses Yama ptrace_scope to restrict debugging. ndk-gdb will");
+ log.WarningLine (" likely be unable to attach to a process. With root access, the restriction");
+ log.WarningLine (" can be lifted by writing 0 to /proc/sys/kernel/yama/ptrace_scope. Consider");
+ log.WarningLine (" upgrading your Pixel C to MXC89L or newer, where Yama is disabled.");
+ log.WarningLine ();
+ }
+
+ if (!DetermineArchitectureAndABI ()) {
+ return false;
+ }
+
+ if (!DetermineAppDataDirectory ()) {
+ return false;
+ }
+
+ serialNumber = GetFirstFoundPropertyValue (serialNumberProperties);
+ if (String.IsNullOrEmpty (serialNumber)) {
+ log.WarningLine ("Unable to determine device serial number");
+ } else {
+ log.StatusLine ($"Device serial number", serialNumber);
+ }
+
+ return true;
+
+ bool YamaOK (string output)
+ {
+ return !String.IsNullOrEmpty (output) && String.Compare ("0", output, StringComparison.Ordinal) != 0;
+ }
+
+ bool PropertyIsEqualTo ((bool haveProperty, string value) result, string expected)
+ {
+ return
+ result.haveProperty &&
+ !String.IsNullOrEmpty (result.value) &&
+ String.Compare (result.value, expected, StringComparison.Ordinal) == 0;
+ }
+ }
+
+ public bool Prepare (out string? mainProcessPath)
+ {
+ mainProcessPath = null;
+ if (!DetectTools ()) {
+ return false;
+ }
+
+ if (!PushDebugServer ()) {
+ return false;
+ }
+
+ if (!PullLibraries (out mainProcessPath)) {
+ return false;
+ }
+
+ return true;
+ }
+
+ bool PullLibraries (out string? mainProcessPath)
+ {
+ mainProcessPath = null;
+ DeviceLibraryCopier copier;
+
+ if (String.IsNullOrEmpty (deviceLdd)) {
+ copier = new NoLddDeviceLibraryCopier (
+ log,
+ adb,
+ appIs64Bit,
+ outputDir,
+ this
+ );
+ } else {
+ copier = new LddDeviceLibraryCopier (
+ log,
+ adb,
+ appIs64Bit,
+ outputDir,
+ this
+ );
+ }
+
+ return copier.Copy (out mainProcessPath);
+ }
+
+ bool PushDebugServer ()
+ {
+ string? debugServerPath = ndk.GetDebugServerPath (mainAbi!);
+ if (String.IsNullOrEmpty (debugServerPath)) {
+ return false;
+ }
+
+ if (!CreateLldbDir (appLldbBinDir!) || !CreateLldbDir (appLldbLogDir) || !CreateLldbDir (appLldbTmpDir)) {
+ return false;
+ }
+
+ //string serverName = $"xa-{context.arch}-{Path.GetFileName (debugServerPath)}";
+ string serverName = Path.GetFileName (debugServerPath);
+ deviceDebugServerPath = $"{appLldbBinDir}/{serverName}";
+
+ KillDebugServer (deviceDebugServerPath);
+
+ // Always push the server binary, as we don't know what version might already be there
+ if (!PushServerExecutable (debugServerPath, deviceDebugServerPath)) {
+ return false;
+ }
+ log.StatusLine ("Debug server path on device", deviceDebugServerPath);
+
+ string? launcherScript = Utilities.ReadManifestResource (log, ServerLauncherScriptName);
+ if (String.IsNullOrEmpty (launcherScript)) {
+ return false;
+ }
+
+ string launcherScriptPath = Path.Combine (outputDir, ServerLauncherScriptName);
+ Directory.CreateDirectory (Path.GetDirectoryName (launcherScriptPath)!);
+ File.WriteAllText (launcherScriptPath, launcherScript, Utilities.UTF8NoBOM);
+
+ deviceDebugServerScriptPath = $"{appLldbBinDir}/{Path.GetFileName (launcherScriptPath)}";
+ if (!PushServerExecutable (launcherScriptPath, deviceDebugServerScriptPath)) {
+ return false;
+ }
+ log.StatusLine ("Debug server launcher script path on device", deviceDebugServerScriptPath);
+ log.MessageLine ();
+
+ return true;
+
+ bool CreateLldbDir (string dir)
+ {
+ if (!adb.CreateDirectoryAs (packageName, dir).Result.success) {
+ log.ErrorLine ($"Failed to create debug server destination directory on device, {dir}");
+ return false;
+ }
+
+ return true;
+ }
+ }
+
+ bool PushServerExecutable (string hostSource, string deviceDestination)
+ {
+ string executableName = Path.GetFileName (deviceDestination);
+
+ // Always push the executable, as we don't know what version might already be there
+ log.DebugLine ($"Uploading {hostSource} to device");
+
+ // First upload to temporary path, as it's writable for everyone
+ string remotePath = $"/data/local/tmp/{executableName}";
+ if (!adb.Push (hostSource, remotePath).Result) {
+ log.ErrorLine ($"Failed to upload debug server {hostSource} to device path {remotePath}");
+ return false;
+ }
+
+ // Next, copy it to the app dir, with run-as
+ (bool success, string output) = adb.Shell (
+ "cat", remotePath, "|",
+ "run-as", packageName,
+ "sh", "-c", $"'cat > {deviceDestination}'"
+ ).Result;
+
+ if (!success) {
+ log.ErrorLine ($"Failed to copy debug executable to device, from {hostSource} to {deviceDestination}");
+ return false;
+ }
+
+ (success, output) = adb.RunAs (packageName, "chmod", "700", deviceDestination).Result;
+ if (!success) {
+ log.ErrorLine ($"Failed to make debug server executable on device, at {deviceDestination}");
+ return false;
+ }
+
+ return true;
+ }
+
+ // TODO: handle multiple pids
+ bool KillDebugServer (string debugServerPath)
+ {
+ long serverPID = GetDeviceProcessID (debugServerPath, quiet: false);
+ if (serverPID <= 0) {
+ return true;
+ }
+
+ log.DebugLine ("Killing previous instance of the debug server");
+ (bool success, string _) = adb.RunAs (packageName, "kill", "-9", $"{serverPID}").Result;
+ return success;
+ }
+
+ long GetDeviceProcessID (string processName, bool quiet = false)
+ {
+ (bool success, string output) = adb.Shell ("pidof", Path.GetFileName (processName)).Result;
+ if (!success) {
+ if (!quiet) {
+ log.ErrorLine ($"Failed to obtain PID of process '{processName}'");
+ log.ErrorLine (output);
+ }
+ return -1;
+ }
+
+ output = output.Trim ();
+ if (!UInt32.TryParse (output, out uint pid)) {
+ if (!quiet) {
+ log.ErrorLine ($"Unable to parse string '{output}' as the package's PID");
+ }
+ return -1;
+ }
+
+ return pid;
+ }
+
+ bool DetectTools ()
+ {
+ // Not all versions of Android have the `which` utility, all of them have `whence`
+ // Also, API 21 adbd will not return an error code to us... But since we know that 21
+ // doesn't have LDD, we'll cheat
+ deviceLdd = null;
+ if (apiLevel > 21) {
+ (bool success, string output) = adb.Shell ("whence", "ldd").Result;
+ if (success) {
+ log.DebugLine ($"Found `ldd` on device at '{output}'");
+ deviceLdd = output;
+ }
+ }
+
+ if (String.IsNullOrEmpty (deviceLdd)) {
+ log.DebugLine ("`ldd` not found on device");
+ }
+
+ return true;
+ }
+
+ bool DetermineAppDataDirectory ()
+ {
+ (bool success, string output) = adb.GetAppDataDirectory (packageName).Result;
+ if (!AppDataDirFound (success, output)) {
+ log.ErrorLine ($"Unable to determine data directory for package '{packageName}'");
+ return false;
+ }
+
+ appDataDir = output.Trim ();
+ log.StatusLine ($"Application data directory on device", appDataDir);
+
+ appLldbBaseDir = $"{appDataDir}/lldb";
+ appLldbBinDir = $"{appLldbBaseDir}/bin";
+ appLldbLogDir = $"{appLldbBaseDir}/log";
+ appLldbTmpDir = $"{appLldbBaseDir}/tmp";
+
+ // Applications with minSdkVersion >= 24 will have their data directories
+ // created with rwx------ permissions, preventing adbd from forwarding to
+ // the gdbserver socket. To be safe, if we're on a device >= 24, always
+ // chmod the directory.
+ if (apiLevel >= 24) {
+ (success, output) = adb.RunAs (packageName, "/system/bin/chmod", "a+x", appDataDir).Result;
+ if (!success) {
+ log.ErrorLine ("Failed to make application data directory world executable");
+ return false;
+ }
+ }
+
+ return true;
+
+ bool AppDataDirFound (bool success, string output)
+ {
+ if (apiLevel > 21) {
+ return success;
+ }
+
+ if (output.IndexOf ("run-as: Package", StringComparison.OrdinalIgnoreCase) >= 0 &&
+ output.IndexOf ("is unknown", StringComparison.OrdinalIgnoreCase) >= 0)
+ {
+ return false;
+ }
+
+ return true;
+ }
+ }
+
+ bool DetermineArchitectureAndABI ()
+ {
+ string? propValue = GetFirstFoundPropertyValue (abiProperties);
+ string[]? deviceABIs = propValue?.Split (',');
+
+ if (deviceABIs == null || deviceABIs.Length == 0) {
+ log.ErrorLine ("Unable to determine device ABI");
+ return false;
+ }
+
+ LogABIs ("Application", supportedAbis);
+ LogABIs (" Device", deviceABIs);
+
+ bool gotValidAbi = false;
+ var possibleAbis = new List ();
+ var possibleArches = new List ();
+
+ foreach (string deviceABI in deviceABIs) {
+ foreach (string appABI in supportedAbis) {
+ if (String.Compare (appABI, deviceABI, StringComparison.OrdinalIgnoreCase) == 0) {
+ string arch = AbiToArch (deviceABI);
+
+ if (!gotValidAbi) {
+ mainAbi = deviceABI;
+ mainArch = arch;
+
+ log.StatusLine ($" Selected ABI", $"{mainAbi} (architecture: {mainArch})");
+
+ appIs64Bit = mainAbi.IndexOf ("64", StringComparison.Ordinal) >= 0;
+ gotValidAbi = true;
+ }
+
+ possibleAbis.Add (deviceABI);
+ possibleArches.Add (arch);
+ }
+ }
+ }
+
+ if (!gotValidAbi) {
+ log.ErrorLine ($"Application cannot run on the selected device: no matching ABI found");
+ }
+
+ availableAbis = possibleAbis.ToArray ();
+ availableArches = possibleArches.ToArray ();
+ return gotValidAbi;
+
+ void LogABIs (string which, string[] abis)
+ {
+ log.StatusLine ($"{which} ABIs", String.Join (", ", abis));
+ }
+
+ string AbiToArch (string abi) => abi switch {
+ "armeabi" => "arm",
+ "armeabi-v7a" => "arm",
+ "arm64-v8a" => "arm64",
+ _ => abi,
+ };
+ }
+
+ string? GetFirstFoundPropertyValue (string[] propertyNames)
+ {
+ foreach (string prop in propertyNames) {
+ (bool success, string value) = adb.GetPropertyValue (prop).Result;
+ if (!success) {
+ continue;
+ }
+
+ return value;
+ }
+
+ return null;
+ }
+}
diff --git a/tools/debug-session-prep/Debug.Session.Prep/AndroidNdk.cs b/tools/debug-session-prep/Debug.Session.Prep/AndroidNdk.cs
new file mode 100644
index 00000000000..48ec5dca23c
--- /dev/null
+++ b/tools/debug-session-prep/Debug.Session.Prep/AndroidNdk.cs
@@ -0,0 +1,101 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+
+using Xamarin.Android.Utilities;
+using Xamarin.Android.Tasks;
+
+namespace Xamarin.Debug.Session.Prep;
+
+class AndroidNdk
+{
+ // We want the shell/batch scripts first, since they set up Python environment for the debugger
+ static readonly string[] lldbNames = {
+ "lldb.sh",
+ "lldb",
+ "lldb.cmd",
+ "lldb.exe",
+ };
+
+ Dictionary hostLldbServerPaths;
+ XamarinLoggingHelper log;
+ string? lldbPath;
+
+ public string LldbPath => lldbPath ?? String.Empty;
+
+ public AndroidNdk (XamarinLoggingHelper log, string ndkRootPath, string[] supportedAbis)
+ {
+ this.log = log;
+ hostLldbServerPaths = new Dictionary (StringComparer.Ordinal);
+
+ if (!FindTools (ndkRootPath, supportedAbis)) {
+ throw new InvalidOperationException ("Failed to find all the required NDK tools");
+ }
+ }
+
+ public string? GetDebugServerPath (string abi)
+ {
+ if (!hostLldbServerPaths.TryGetValue (abi, out string? debugServerPath) || String.IsNullOrEmpty (debugServerPath)) {
+ log.ErrorLine ($"Debug server for abi '{abi}' not found.");
+ return null;
+ }
+
+ return debugServerPath;
+ }
+
+ bool FindTools (string ndkRootPath, string[] supportedAbis)
+ {
+ string toolchainDir = Path.Combine (ndkRootPath, NdkHelper.RelativeToolchainDir);
+ string toolchainBinDir = Path.Combine (toolchainDir, "bin");
+ string? path = null;
+
+ foreach (string lldb in lldbNames) {
+ path = Path.Combine (toolchainBinDir, lldb);
+ if (File.Exists (path)) {
+ break;
+ }
+ }
+
+ if (String.IsNullOrEmpty (path)) {
+ log.ErrorLine ($"Unable to locate lldb executable in '{toolchainBinDir}'");
+ return false;
+ }
+ lldbPath = path;
+
+ hostLldbServerPaths = new Dictionary (StringComparer.OrdinalIgnoreCase);
+ string llvmVersion = GetLlvmVersion (toolchainDir);
+ foreach (string abi in supportedAbis) {
+ string llvmAbi = NdkHelper.TranslateAbiToLLVM (abi);
+ path = Path.Combine (ndkRootPath, NdkHelper.RelativeToolchainDir, "lib64", "clang", llvmVersion, "lib", "linux", llvmAbi, "lldb-server");
+ if (!File.Exists (path)) {
+ log.ErrorLine ($"LLVM lldb server component for ABI '{abi}' not found at '{path}'");
+ return false;
+ }
+
+ hostLldbServerPaths.Add (abi, path);
+ }
+
+ if (hostLldbServerPaths.Count == 0) {
+ log.ErrorLine ("Unable to find any lldb-server executables, debugging not possible");
+ return false;
+ }
+
+ return true;
+ }
+
+ string GetLlvmVersion (string toolchainDir)
+ {
+ string path = Path.Combine (toolchainDir, "AndroidVersion.txt");
+ if (!File.Exists (path)) {
+ throw new InvalidOperationException ($"LLVM version file not found at '{path}'");
+ }
+
+ string[] lines = File.ReadAllLines (path);
+ string? line = lines.Length >= 1 ? lines[0].Trim () : null;
+ if (String.IsNullOrEmpty (line)) {
+ throw new InvalidOperationException ($"Unable to read LLVM version from '{path}'");
+ }
+
+ return line;
+ }
+}
diff --git a/tools/debug-session-prep/Debug.Session.Prep/DeviceLibrariesCopier.cs b/tools/debug-session-prep/Debug.Session.Prep/DeviceLibrariesCopier.cs
new file mode 100644
index 00000000000..96a793a08aa
--- /dev/null
+++ b/tools/debug-session-prep/Debug.Session.Prep/DeviceLibrariesCopier.cs
@@ -0,0 +1,66 @@
+using System;
+
+using Xamarin.Android.Utilities;
+using Xamarin.Android.Tasks;
+
+namespace Xamarin.Debug.Session.Prep;
+
+abstract class DeviceLibraryCopier
+{
+ protected XamarinLoggingHelper Log { get; }
+ protected bool AppIs64Bit { get; }
+ protected string LocalDestinationDir { get; }
+ protected AdbRunner Adb { get; }
+ protected AndroidDevice Device { get; }
+
+ protected DeviceLibraryCopier (XamarinLoggingHelper log, AdbRunner adb, bool appIs64Bit, string localDestinationDir, AndroidDevice device)
+ {
+ Log = log;
+ Adb = adb;
+ AppIs64Bit = appIs64Bit;
+ LocalDestinationDir = localDestinationDir;
+ Device = device;
+ }
+
+ protected string? FetchZygote ()
+ {
+ string zygotePath;
+ string destination;
+
+ if (AppIs64Bit) {
+ zygotePath = "/system/bin/app_process64";
+ destination = Utilities.MakeLocalPath (LocalDestinationDir, zygotePath);
+
+ Utilities.MakeFileDirectory (destination);
+ if (!Adb.Pull (zygotePath, destination).Result) {
+ Log.ErrorLine ("Failed to copy 64-bit app_process64");
+ return null;
+ }
+ } else {
+ // /system/bin/app_process is 32-bit on 32-bit devices, but a symlink to
+ // app_process64 on 64-bit. If we need the 32-bit version, try to pull
+ // app_process32, and if that fails, pull app_process.
+ destination = Utilities.MakeLocalPath (LocalDestinationDir, "/system/bin/app_process");
+ string? source = "/system/bin/app_process32";
+
+ Utilities.MakeFileDirectory (destination);
+ if (!Adb.Pull (source, destination).Result) {
+ source = "/system/bin/app_process";
+ if (!Adb.Pull (source, destination).Result) {
+ source = null;
+ }
+ }
+
+ if (String.IsNullOrEmpty (source)) {
+ Log.ErrorLine ("Failed to copy 32-bit app_process");
+ return null;
+ }
+
+ zygotePath = destination;
+ }
+
+ return zygotePath;
+ }
+
+ public abstract bool Copy (out string? zygotePath);
+}
diff --git a/tools/debug-session-prep/Debug.Session.Prep/LdConfigParser.cs b/tools/debug-session-prep/Debug.Session.Prep/LdConfigParser.cs
new file mode 100644
index 00000000000..71b33086520
--- /dev/null
+++ b/tools/debug-session-prep/Debug.Session.Prep/LdConfigParser.cs
@@ -0,0 +1,167 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+
+using Xamarin.Android.Utilities;
+
+namespace Xamarin.Debug.Session.Prep;
+
+class LdConfigParser
+{
+ XamarinLoggingHelper log;
+
+ public LdConfigParser (XamarinLoggingHelper log)
+ {
+ this.log = log;
+ }
+
+ // Format: https://android.googlesource.com/platform/bionic/+/master/linker/ld.config.format.md
+ //
+ public (List searchPaths, HashSet permittedPaths) Parse (string localLdConfigPath, string deviceBinDirectory, string libDirName)
+ {
+ var searchPaths = new List ();
+ var permittedPaths = new HashSet ();
+ bool foundSomeSection = false;
+ bool insideMatchingSection = false;
+ string normalizedDeviceBinDirectory = Utilities.NormalizeDirectoryPath (deviceBinDirectory);
+ string? sectionName = null;
+
+ log.DebugLine ($"Parsing LD config file '{localLdConfigPath}'");
+ int lineCounter = 0;
+ var namespaces = new List {
+ "default"
+ };
+
+ foreach (string l in File.ReadLines (localLdConfigPath)) {
+ lineCounter++;
+ string line = l.Trim ();
+ if (line.Length == 0 || line.StartsWith ('#')) {
+ continue;
+ }
+
+ // The `dir.*` entries are before any section, don't waste time looking for them if we've parsed a section already
+ if (!foundSomeSection && sectionName == null) {
+ sectionName = GetMatchingDirMapping (normalizedDeviceBinDirectory, line);
+ if (sectionName != null) {
+ log.DebugLine ($"Found section name on line {lineCounter}: '{sectionName}'");
+ continue;
+ }
+ }
+
+ if (line[0] == '[') {
+ foundSomeSection = true;
+ insideMatchingSection = String.Compare (line, $"[{sectionName}]", StringComparison.Ordinal) == 0;
+ if (insideMatchingSection) {
+ log.DebugLine ($"Found section '{sectionName}' start on line {lineCounter}");
+ }
+ }
+
+ if (!insideMatchingSection) {
+ continue;
+ }
+
+ if (line.StartsWith ("additional.namespaces", StringComparison.Ordinal) && GetVariableAssignmentParts (line, out string? name, out string? value)) {
+ foreach (string v in value!.Split (',')) {
+ string nsName = v.Trim ();
+ if (nsName.Length == 0) {
+ continue;
+ }
+
+ log.DebugLine ($"Adding additional namespace '{nsName}'");
+ namespaces.Add (nsName);
+ }
+ continue;
+ }
+
+ MaybeAddLibraryPath (searchPaths, permittedPaths, namespaces, line, libDirName);
+ }
+
+ return (searchPaths, permittedPaths);
+
+ }
+
+ void MaybeAddLibraryPath (List searchPaths, HashSet permittedPaths, List knownNamespaces, string configLine, string libDirName)
+ {
+ if (!configLine.StartsWith ("namespace.", StringComparison.Ordinal)) {
+ return;
+ }
+
+ // not interested in ASAN libraries
+ if (configLine.IndexOf (".asan.", StringComparison.Ordinal) > 0) {
+ return;
+ }
+
+ foreach (string ns in knownNamespaces) {
+ if (!GetVariableAssignmentParts (configLine, out string? name, out string? value)) {
+ continue;
+ }
+
+ string varName = $"namespace.{ns}.search.paths";
+ if (String.Compare (varName, name, StringComparison.Ordinal) == 0) {
+ AddPath (searchPaths, "search", value!);
+ continue;
+ }
+
+ varName = $"namespace.{ns}.permitted.paths";
+ if (String.Compare (varName, name, StringComparison.Ordinal) == 0) {
+ AddPath (permittedPaths, "permitted", value!, checkIfAlreadyAdded: true);
+ }
+ }
+
+ void AddPath (ICollection list, string which, string value, bool checkIfAlreadyAdded = false)
+ {
+ string path = Utilities.NormalizeDirectoryPath (value.Replace ("${LIB}", libDirName));
+
+ if (checkIfAlreadyAdded && list.Contains (path)) {
+ return;
+ }
+
+ log.DebugLine ($"Adding library {which} path: {path}");
+ list.Add (path);
+ }
+ }
+
+ string? GetMatchingDirMapping (string deviceBinDirectory, string configLine)
+ {
+ const string LinePrefix = "dir.";
+
+ string line = configLine.Trim ();
+ if (line.Length == 0 || !line.StartsWith (LinePrefix, StringComparison.Ordinal)) {
+ return null;
+ }
+
+ if (!GetVariableAssignmentParts (line, out string? name, out string? value)) {
+ return null;
+ }
+
+ string dirPath = Utilities.NormalizeDirectoryPath (value!);
+ if (String.Compare (dirPath, deviceBinDirectory, StringComparison.Ordinal) != 0) {
+ return null;
+ }
+
+ string ns = name!.Substring (LinePrefix.Length).Trim ();
+ if (String.IsNullOrEmpty (ns)) {
+ return null;
+ }
+
+ return ns;
+ }
+
+ bool GetVariableAssignmentParts (string line, out string? name, out string? value)
+ {
+ name = value = null;
+
+ string[] parts = line.Split ("+=", 2);
+ if (parts.Length != 2) {
+ parts = line.Split ('=', 2);
+ if (parts.Length != 2) {
+ return false;
+ }
+ }
+
+ name = parts[0].Trim ();
+ value = parts[1].Trim ();
+
+ return true;
+ }
+}
diff --git a/tools/debug-session-prep/Debug.Session.Prep/LddDeviceLibrariesCopier.cs b/tools/debug-session-prep/Debug.Session.Prep/LddDeviceLibrariesCopier.cs
new file mode 100644
index 00000000000..ea7216399cc
--- /dev/null
+++ b/tools/debug-session-prep/Debug.Session.Prep/LddDeviceLibrariesCopier.cs
@@ -0,0 +1,18 @@
+using System;
+
+using Xamarin.Android.Utilities;
+using Xamarin.Android.Tasks;
+
+namespace Xamarin.Debug.Session.Prep;
+
+class LddDeviceLibraryCopier : DeviceLibraryCopier
+{
+ public LddDeviceLibraryCopier (XamarinLoggingHelper log, AdbRunner adb, bool appIs64Bit, string localDestinationDir, AndroidDevice device)
+ : base (log, adb, appIs64Bit, localDestinationDir, device)
+ {}
+
+ public override bool Copy (out string? zygotePath)
+ {
+ throw new NotImplementedException();
+ }
+}
diff --git a/tools/debug-session-prep/Debug.Session.Prep/LldbModuleCache.cs b/tools/debug-session-prep/Debug.Session.Prep/LldbModuleCache.cs
new file mode 100644
index 00000000000..89a9b4cfcf3
--- /dev/null
+++ b/tools/debug-session-prep/Debug.Session.Prep/LldbModuleCache.cs
@@ -0,0 +1,170 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+
+using ELFSharp.ELF;
+using ELFSharp.ELF.Sections;
+using Xamarin.Android.Utilities;
+
+namespace Xamarin.Debug.Session.Prep;
+
+abstract class LldbModuleCache
+{
+ protected AndroidDevice Device { get; }
+ protected string CacheDirPath { get; }
+ protected XamarinLoggingHelper Log { get; }
+
+ protected LldbModuleCache (XamarinLoggingHelper log, AndroidDevice device)
+ {
+ Device = device;
+ Log = log;
+
+ CacheDirPath = Path.Combine (
+ Environment.GetFolderPath (Environment.SpecialFolder.UserProfile),
+ ".lldb",
+ "module_cache",
+ "remote-android", // "platform" used by LLDB in our case
+ device.SerialNumber
+ );
+ }
+
+ public void Populate (string zygotePath)
+ {
+ string? localPath = FetchFileFromDevice (zygotePath);
+ if (localPath == null) {
+ // TODO: should we perhaps fetch a set of "basic" libraries here, as a fallback?
+ Log.WarningLine ($"Unable to fetch Android application launcher binary ('{zygotePath}') from device. No cache of shared modules will be generated");
+ return;
+ }
+
+ var alreadyDownloaded = new HashSet (StringComparer.Ordinal);
+ using IELF? elf = ReadElfFile (localPath);
+
+ FetchDependencies (elf, alreadyDownloaded, localPath);
+ }
+
+ void FetchDependencies (IELF? elf, HashSet alreadyDownloaded, string localPath)
+ {
+ if (elf == null) {
+ Log.DebugLine ($"Failed to open '{localPath}' as an ELF file. Ignoring.");
+ return;
+ }
+
+ var dynstr = GetSection (elf, ".dynstr") as IStringTable;
+ if (dynstr == null) {
+ Log.DebugLine ($"ELF binary {localPath} has no .dynstr section, unable to read referenced shared library names");
+ return;
+ }
+
+ var needed = new HashSet (StringComparer.Ordinal);
+ foreach (IDynamicSection section in elf.GetSections ()) {
+ foreach (IDynamicEntry entry in section.Entries) {
+ if (entry.Tag != DynamicTag.Needed) {
+ continue;
+ }
+
+ AddNeeded (dynstr, entry);
+ }
+ }
+
+ Log.DebugLine ($"Binary {localPath} references the following libraries:");
+ foreach (string lib in needed) {
+ Log.Debug ($" {lib}");
+ if (alreadyDownloaded.Contains (lib)) {
+ Log.DebugLine (" [already downloaded]");
+ continue;
+ }
+
+ string? deviceLibraryPath = GetSharedLibraryPath (lib);
+ if (String.IsNullOrEmpty (deviceLibraryPath)) {
+ Log.DebugLine (" [device path unknown]");
+ Log.WarningLine ($"Referenced libary '{lib}' not found on device");
+ continue;
+ }
+
+ Log.DebugLine (" [downloading]");
+ Log.Status ("Downloading", deviceLibraryPath);
+ string? localLibraryPath = FetchFileFromDevice (deviceLibraryPath);
+ if (String.IsNullOrEmpty (localLibraryPath)) {
+ Log.Log (LogLevel.Info, " [FAILED]", XamarinLoggingHelper.ErrorColor);
+ continue;
+ }
+ Log.LogLine (LogLevel.Info, " [SUCCESS]", XamarinLoggingHelper.InfoColor);
+
+ alreadyDownloaded.Add (lib);
+ using IELF? libElf = ReadElfFile (localLibraryPath);
+ FetchDependencies (libElf, alreadyDownloaded, localLibraryPath);
+ }
+
+ void AddNeeded (IStringTable stringTable, IDynamicEntry entry)
+ {
+ ulong index;
+ if (entry is DynamicEntry entry64) {
+ index = entry64.Value;
+ } else if (entry is DynamicEntry entry32) {
+ index = (ulong)entry32.Value;
+ } else {
+ Log.WarningLine ($"DynamicEntry neither 32 nor 64 bit? Weird");
+ return;
+ }
+
+ string name = stringTable[(long)index];
+ if (needed.Contains (name)) {
+ return;
+ }
+
+ needed.Add (name);
+ }
+ }
+
+ string? FetchFileFromDevice (string deviceFilePath)
+ {
+ string localFilePath = Utilities.MakeLocalPath (CacheDirPath, deviceFilePath);
+ string localTempFilePath = $"{localFilePath}.tmp";
+
+ Directory.CreateDirectory (Path.GetDirectoryName (localFilePath)!);
+
+ if (!Device.AdbRunner.Pull (deviceFilePath, localTempFilePath).Result) {
+ Log.ErrorLine ($"Failed to download {deviceFilePath} from the attached device");
+ return null;
+ }
+
+ File.Move (localTempFilePath, localFilePath, true);
+ return localFilePath;
+ }
+
+ protected string GetUnixFileName (string path)
+ {
+ int idx = path.LastIndexOf ('/');
+ if (idx >= 0 && idx != path.Length - 1) {
+ return path.Substring (idx + 1);
+ }
+
+ return path;
+ }
+
+ protected abstract string? GetSharedLibraryPath (string libraryName);
+
+ IELF? ReadElfFile (string path)
+ {
+ try {
+ if (ELFReader.TryLoad (path, out IELF ret)) {
+ return ret;
+ }
+ } catch (Exception ex) {
+ Log.WarningLine ($"{path} may not be a valid ELF binary.");
+ Log.WarningLine (ex.ToString ());
+ }
+
+ return null;
+ }
+
+ ISection? GetSection (IELF elf, string sectionName)
+ {
+ if (!elf.TryGetSection (sectionName, out ISection section)) {
+ return null;
+ }
+
+ return section;
+ }
+}
diff --git a/tools/debug-session-prep/Debug.Session.Prep/NoLddDeviceLibrariesCopier.cs b/tools/debug-session-prep/Debug.Session.Prep/NoLddDeviceLibrariesCopier.cs
new file mode 100644
index 00000000000..a33f14917a2
--- /dev/null
+++ b/tools/debug-session-prep/Debug.Session.Prep/NoLddDeviceLibrariesCopier.cs
@@ -0,0 +1,190 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+
+using Xamarin.Android.Utilities;
+using Xamarin.Android.Tasks;
+
+namespace Xamarin.Debug.Session.Prep;
+
+class NoLddDeviceLibraryCopier : DeviceLibraryCopier
+{
+ const string LdConfigPath = "/system/etc/ld.config.txt";
+
+ // To make things interesting, it turns out that API29 devices have **both** API 28 and 29 (they report 28) and for that reason they have TWO config files for ld...
+ const string LdConfigPath28 = "/etc/ld.config.28.txt";
+ const string LdConfigPath29 = "/etc/ld.config.29.txt";
+
+ // TODO: We probably need a "provider" for the list of paths, since on ARM devices, /system/lib{64} directories contain x86/x64 binaries, and the ARM binaries are found in
+ // /system/lib{64]/arm{64} (but not on all devices, of course... e.g. Pixel 6 Pro doesn't have these)
+ //
+ // List of directory paths to use when the device has neither ldd nor /system/etc/ld.config.txt
+ static readonly string[] FallbackLibraryDirectories = {
+ "/system/@LIB@",
+ "/system/@LIB@/drm",
+ "/system/@LIB@/egl",
+ "/system/@LIB@/hw",
+ "/system/@LIB@/soundfx",
+ "/system/@LIB@/ssl",
+ "/system/@LIB@/ssl/engines",
+
+ // /system/vendor is a symlink to /vendor on some Android versions, we'll skip the latter then
+ "/system/vendor/@LIB@",
+ "/system/vendor/@LIB@/egl",
+ "/system/vendor/@LIB@/mediadrm",
+ };
+
+ public NoLddDeviceLibraryCopier (XamarinLoggingHelper log, AdbRunner adb, bool appIs64Bit, string localDestinationDir, AndroidDevice device)
+ : base (log, adb, appIs64Bit, localDestinationDir, device)
+ {}
+
+ public override bool Copy (out string? zygotePath)
+ {
+ zygotePath = FetchZygote ();
+ if (String.IsNullOrEmpty (zygotePath)) {
+ Log.ErrorLine ("Unable to determine path of the zygote process on device");
+ return false;
+ }
+
+ (List searchPaths, HashSet permittedPaths) = GetLibraryPaths ();
+
+ // Collect file listings for all the search directories
+ var sharedLibraries = new List ();
+ foreach (string path in searchPaths) {
+ AddSharedLibraries (sharedLibraries, path, permittedPaths);
+ }
+
+ var moduleCache = new NoLddLldbModuleCache (Log, Device, sharedLibraries);
+ moduleCache.Populate (zygotePath);
+
+ return true;
+ }
+
+ void AddSharedLibraries (List sharedLibraries, string deviceDirPath, HashSet permittedPaths)
+ {
+ AdbRunner.OutputLineFilter filterOutErrors = (bool isStdError, string line) => {
+ if (!isStdError) {
+ return false; // don't suppress any lines on stdout
+ }
+
+ // Ignore these, since we don't really care and there's no point in spamming the output with red
+ return
+ line.IndexOf ("Permission denied", StringComparison.OrdinalIgnoreCase) >= 0 ||
+ line.IndexOf ("No such file or directory", StringComparison.OrdinalIgnoreCase) >= 0;
+ };
+
+ (bool success, string output) = Adb.Shell (filterOutErrors, "ls", "-l", deviceDirPath).Result;
+ if (!success) {
+ // We can't rely on `success` because `ls -l` will return an error code if the directory exists but has any entries access to whose is not permitted
+ if (output.IndexOf ("No such file or directory", StringComparison.OrdinalIgnoreCase) >= 0) {
+ Log.DebugLine ($"Shared libraries directory {deviceDirPath} not found on device");
+ return;
+ }
+ }
+
+ Log.DebugLine ($"Adding shared libraries from {deviceDirPath}");
+ foreach (string l in output.Split ('\n')) {
+ string line = l.Trim ();
+ if (line.Length == 0) {
+ continue;
+ }
+
+ // `ls -l` output has 8 columns for filesystem entries
+ string[] parts = line.Split (' ', 8, StringSplitOptions.RemoveEmptyEntries);
+ if (parts.Length != 8) {
+ continue;
+ }
+
+ string permissions = parts[0].Trim ();
+ string name = parts[7].Trim ();
+
+ // First column, permissions: `drwxr-xr-x`, `-rw-r--r--` etc
+ if (permissions[0] == 'd') {
+ // Directory
+ string nestedDirPath = $"{deviceDirPath}{name}/";
+ if (permittedPaths.Count > 0 && !permittedPaths.Contains (nestedDirPath)) {
+ Log.DebugLine ($"Directory '{nestedDirPath}' is not in the list of permitted directories, ignoring");
+ continue;
+ }
+
+ AddSharedLibraries (sharedLibraries, nestedDirPath, permittedPaths);
+ continue;
+ }
+
+ // Ignore entries that aren't regular .so files or symlinks
+ if ((permissions[0] != '-' && permissions[0] != 'l') || !name.EndsWith (".so", StringComparison.Ordinal)) {
+ continue;
+ }
+
+ string libPath;
+ if (permissions[0] == 'l') {
+ // Let's hope there are no libraries with -> in their name :P (if there are, we should use `readlink`)
+ const string SymlinkArrow = "->";
+
+ // Symlink, we'll add the target library instead
+ int idx = name.IndexOf (SymlinkArrow, StringComparison.Ordinal);
+ if (idx > 0) {
+ libPath = name.Substring (idx + SymlinkArrow.Length).Trim ();
+ } else {
+ Log.WarningLine ($"'ls -l' output line contains a symbolic link, but I can't determine the target:");
+ Log.WarningLine ($" '{line}'");
+ Log.WarningLine ("Ignoring this entry");
+ continue;
+ }
+ } else {
+ libPath = $"{deviceDirPath}{name}";
+ }
+
+ Log.DebugLine ($" {libPath}");
+ sharedLibraries.Add (libPath);
+ }
+ }
+
+ (List searchPaths, HashSet permittedPaths) GetLibraryPaths ()
+ {
+ string lib = AppIs64Bit ? "lib64" : "lib";
+
+ if (Device.ApiLevel == 21) {
+ // API21 devices (at least emulators) don't return adb error codes, so to avoid awkward error message parsing, we're going to just skip detection since we
+ // know what API21 has and doesn't have
+ return (GetFallbackDirs (), new HashSet ());
+ }
+
+ string localLdConfigPath = Utilities.MakeLocalPath (LocalDestinationDir, LdConfigPath);
+ Utilities.MakeFileDirectory (localLdConfigPath);
+
+ string deviceLdConfigPath;
+
+ if (Device.ApiLevel == 28) {
+ deviceLdConfigPath = LdConfigPath28;
+ } else if (Device.ApiLevel == 29) {
+ deviceLdConfigPath = LdConfigPath29;
+ } else {
+ deviceLdConfigPath = LdConfigPath;
+ }
+
+ if (!Adb.Pull (deviceLdConfigPath, localLdConfigPath).Result) {
+ Log.DebugLine ($"Device doesn't have {LdConfigPath}");
+ return (GetFallbackDirs (), new HashSet ());
+ } else {
+ Log.DebugLine ($"Downloaded {deviceLdConfigPath} to {localLdConfigPath}");
+ }
+
+ var parser = new LdConfigParser (Log);
+
+ // The app executables (app_process and app_process32) are both in /system/bin, so we can limit our
+ // library search paths to this location.
+ (List searchPaths, HashSet permittedPaths) = parser.Parse (localLdConfigPath, "/system/bin", lib);
+ if (searchPaths.Count == 0) {
+ searchPaths = GetFallbackDirs ();
+ }
+
+ return (searchPaths, permittedPaths);
+
+ List GetFallbackDirs ()
+ {
+ Log.DebugLine ("Using fallback library directories for this device");
+ return FallbackLibraryDirectories.Select (l => l.Replace ("@LIB@", lib)).ToList ();
+ }
+ }
+}
diff --git a/tools/debug-session-prep/Debug.Session.Prep/NoLddLldbModuleCache.cs b/tools/debug-session-prep/Debug.Session.Prep/NoLddLldbModuleCache.cs
new file mode 100644
index 00000000000..ca22fdf332d
--- /dev/null
+++ b/tools/debug-session-prep/Debug.Session.Prep/NoLddLldbModuleCache.cs
@@ -0,0 +1,41 @@
+using System;
+using System.Collections.Generic;
+
+using Xamarin.Android.Utilities;
+
+namespace Xamarin.Debug.Session.Prep;
+
+class NoLddLldbModuleCache : LldbModuleCache
+{
+ List deviceSharedLibraries;
+ Dictionary libraryCache;
+
+ public NoLddLldbModuleCache (XamarinLoggingHelper log, AndroidDevice device, List deviceSharedLibraries)
+ : base (log, device)
+ {
+ this.deviceSharedLibraries = deviceSharedLibraries;
+ libraryCache = new Dictionary (StringComparer.Ordinal);
+ }
+
+ protected override string? GetSharedLibraryPath (string libraryName)
+ {
+ if (libraryCache.TryGetValue (libraryName, out string? libraryPath)) {
+ return libraryPath;
+ }
+
+ // List is sorted on the order of directories as specified by ld.config.txt, file entries aren't
+ // sorted inside.
+ foreach (string libPath in deviceSharedLibraries) {
+ string fileName = GetUnixFileName (libPath);
+
+ if (String.Compare (libraryName, fileName, StringComparison.Ordinal) == 0) {
+ libraryCache.Add (libraryName, libPath);
+ return libPath;
+ }
+ }
+
+ // Cache misses, too, the list isn't going to change
+ libraryCache.Add (libraryName, null);
+ return null;
+ }
+}
diff --git a/tools/debug-session-prep/Debug.Session.Prep/Utilities.cs b/tools/debug-session-prep/Debug.Session.Prep/Utilities.cs
new file mode 100644
index 00000000000..5ff6f08075c
--- /dev/null
+++ b/tools/debug-session-prep/Debug.Session.Prep/Utilities.cs
@@ -0,0 +1,78 @@
+using System;
+using System.IO;
+using System.Reflection;
+using System.Runtime.InteropServices;
+using System.Text;
+
+using Xamarin.Android.Utilities;
+
+namespace Xamarin.Debug.Session.Prep;
+
+static class Utilities
+{
+ public static readonly UTF8Encoding UTF8NoBOM = new UTF8Encoding (false);
+
+ public static bool IsMacOS { get; private set; }
+ public static bool IsLinux { get; private set; }
+ public static bool IsWindows { get; private set; }
+
+ static Utilities ()
+ {
+ if (RuntimeInformation.IsOSPlatform (OSPlatform.Windows)) {
+ IsWindows = true;
+ } else if (RuntimeInformation.IsOSPlatform (OSPlatform.OSX)) {
+ IsMacOS = true;
+ } else if (RuntimeInformation.IsOSPlatform (OSPlatform.Linux)) {
+ IsLinux = true;
+ }
+ }
+
+ public static void MakeFileDirectory (string filePath)
+ {
+ if (String.IsNullOrEmpty (filePath)) {
+ return;
+ }
+
+ string? dirName = Path.GetDirectoryName (filePath);
+ if (String.IsNullOrEmpty (dirName)) {
+ return;
+ }
+
+ Directory.CreateDirectory (dirName);
+ }
+
+ public static string? ReadManifestResource (XamarinLoggingHelper log, string resourceName)
+ {
+ using (var from = Assembly.GetExecutingAssembly ().GetManifestResourceStream (resourceName)) {
+ if (from == null) {
+ log.ErrorLine ($"Manifest resource '{resourceName}' cannot be loaded");
+ return null;
+ }
+
+ using (var sr = new StreamReader (from)) {
+ return sr.ReadToEnd ();
+ }
+ }
+ }
+
+ public static string NormalizeDirectoryPath (string dirPath)
+ {
+ if (dirPath.EndsWith ('/')) {
+ return dirPath;
+ }
+
+ return $"{dirPath}/";
+ }
+
+ public static string ToLocalPathFormat (string path) => IsWindows ? path.Replace ("/", "\\") : path;
+
+ public static string MakeLocalPath (string localDirectory, string remotePath)
+ {
+ string remotePathLocalFormat = ToLocalPathFormat (remotePath);
+ if (remotePath[0] == '/') {
+ return $"{localDirectory}{remotePathLocalFormat}";
+ }
+
+ return Path.Combine (localDirectory, remotePathLocalFormat);
+ }
+}
diff --git a/tools/debug-session-prep/Main.cs b/tools/debug-session-prep/Main.cs
new file mode 100644
index 00000000000..e3f5e5714c9
--- /dev/null
+++ b/tools/debug-session-prep/Main.cs
@@ -0,0 +1,290 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Text;
+
+using Mono.Options;
+using Xamarin.Android.Utilities;
+
+namespace Xamarin.Debug.Session.Prep;
+
+class App
+{
+ const string DefaultAdbPath = "adb";
+
+ static readonly Dictionary SupportedAbiMap = new Dictionary (StringComparer.OrdinalIgnoreCase) {
+ {"arm32", "armeabi-v7a"},
+ {"arm64", "arm64-v8a"},
+ {"arm64-v8a", "arm64-v8a"},
+ {"armeabi", "armeabi-v7a"},
+ {"armeabi-v7a", "armeabi-v7a"},
+ {"x86", "x86"},
+ {"x86_64", "x86_64"},
+ {"x64", "x86_64"}
+ };
+
+ sealed class ParsedOptions
+ {
+ public string? AdbPath;
+ public string? PackageName;
+ public string[]? SupportedABIs;
+ public string? TargetDevice;
+ public bool ShowHelp;
+ public bool Verbose = true; // TODO: remove the default once development is done
+ public string? AppNativeLibrariesDir;
+ public string? NdkDirPath;
+ public string? OutputDirPath;
+ public string? ConfigScriptName;
+ public string? LldbScriptName;
+ }
+
+ static int Main (string[] args)
+ {
+ bool haveOptionErrors = false;
+ var parsedOptions = new ParsedOptions ();
+ var log = new XamarinLoggingHelper {
+ Verbose = parsedOptions.Verbose,
+ };
+
+ var opts = new OptionSet {
+ "Usage: debug-session-prep [REQUIRED_OPTIONS] [OPTIONS]",
+ "",
+ "REQUIRED_OPTIONS are:",
+ { "p|package-name=", "name of the application package", v => parsedOptions.PackageName = EnsureNonEmptyString (log, "-p|--package-name", v, ref haveOptionErrors) },
+ { "s|supported-abis=", "comma-separated list of ABIs the application supports", v => parsedOptions.SupportedABIs = EnsureSupportedABIs (log, "-s|--supported-abis", v, ref haveOptionErrors) },
+ { "l|lib-dir=", "{PATH} to the directory where application native libraries were copied (relative to output directory, below)", v => parsedOptions.AppNativeLibrariesDir = v },
+ { "n|ndk-dir=", "{PATH} to to the Android NDK root directory", v => parsedOptions.NdkDirPath = v },
+ { "o|output-dir=", "{PATH} to directory which will contain various generated files (logs, scripts etc)", v => parsedOptions.OutputDirPath = v },
+ { "c|config-script=", "{NAME} of the launcher configuration script which will be created in the output directory", v => parsedOptions.ConfigScriptName = v },
+ { "g|lldb-script=", "{NAME} of the LLDB script which will be created in the output directory", v => parsedOptions.LldbScriptName = v },
+ "",
+ "OPTIONS are:",
+ { "a|adb=", "{PATH} to adb to use for this session", v => parsedOptions.AdbPath = EnsureNonEmptyString (log, "-a|--adb", v, ref haveOptionErrors) },
+ { "d|device=", "ID of {DEVICE} to target for this session", v => parsedOptions.TargetDevice = EnsureNonEmptyString (log, "-d|--device", v, ref haveOptionErrors) },
+ "",
+ { "v|verbose", "Show debug messages", v => parsedOptions.Verbose = true },
+ { "h|help|?", "Show this help screen", v => parsedOptions.ShowHelp = true },
+ "",
+ $"Supported ABI names are: {GetSupportedAbiNames ()}",
+ "",
+ };
+
+ List rest = opts.Parse (args);
+ log.Verbose = parsedOptions.Verbose;
+
+ if (parsedOptions.ShowHelp) {
+ opts.WriteOptionDescriptions (Console.Out);
+ return 0;
+ }
+
+ if (haveOptionErrors) {
+ return 1;
+ }
+
+ bool missingRequiredOptions = false;
+ if (parsedOptions.SupportedABIs == null || parsedOptions.SupportedABIs.Length == 0) {
+ log.ErrorLine ("The '-s|--supported-abis' option must be used to provide a non-empty list of Android ABIs supported by the application");
+ missingRequiredOptions = true;
+ }
+
+ if (String.IsNullOrEmpty (parsedOptions.PackageName)) {
+ log.ErrorLine ("The '-p|--package-name' option must be used to provide non-empty application package name");
+ missingRequiredOptions = true;
+ }
+
+ if (String.IsNullOrEmpty (parsedOptions.NdkDirPath)) {
+ log.ErrorLine ("The '-n|--ndk-dir' option must be used to specify the directory where Android NDK is installed");
+ missingRequiredOptions = true;
+ }
+
+ if (String.IsNullOrEmpty (parsedOptions.OutputDirPath)) {
+ log.ErrorLine ("The '-o|--output-dir' option must be used to specify the directory where generated files will be placed");
+ missingRequiredOptions = true;
+ }
+
+ if (String.IsNullOrEmpty (parsedOptions.AppNativeLibrariesDir)) {
+ log.ErrorLine ("The '-l|--lib-dir' option must be used to specify the directory where application shared libraries were copied");
+ missingRequiredOptions = true;
+ }
+
+ if (String.IsNullOrEmpty (parsedOptions.ConfigScriptName)) {
+ log.ErrorLine ("The '-c|--config-script' option must be used to specify name of the launcher configuration script");
+ missingRequiredOptions = true;
+ }
+
+ if (String.IsNullOrEmpty (parsedOptions.LldbScriptName)) {
+ log.ErrorLine ("The '-g|--lldb-script' option must be used to specify name of the LLDB script");
+ missingRequiredOptions = true;
+ }
+
+ if (missingRequiredOptions) {
+ return 1;
+ }
+
+ var ndk = new AndroidNdk (log, parsedOptions.NdkDirPath!, parsedOptions.SupportedABIs!);
+ var device = new AndroidDevice (
+ log,
+ ndk,
+ parsedOptions.OutputDirPath!,
+ parsedOptions.AdbPath ?? DefaultAdbPath,
+ parsedOptions.PackageName!,
+ parsedOptions.SupportedABIs!,
+ parsedOptions.TargetDevice
+ );
+
+ if (!device.GatherInfo ()) {
+ return 1;
+ }
+
+ if (device.ApiLevel < 21) {
+ log.ErrorLine ($"Only Android API level 21 and newer are supported");
+ return 1;
+ }
+
+ if (!device.Prepare (out string? mainProcessPath) || String.IsNullOrEmpty (mainProcessPath)) {
+ log.ErrorLine ("Failed to prepare for debugging session");
+ return 1;
+ }
+
+ string socketScheme = "unix-abstract";
+ string socketDir = $"/xa-{parsedOptions.PackageName}";
+
+ var rnd = new Random ();
+ string socketName = $"xa-platform-{rnd.NextInt64 ()}.sock";
+
+ WriteConfigScript (parsedOptions, device, ndk, socketScheme, socketDir, socketName);
+ WriteLldbScript (parsedOptions, device, socketScheme, socketDir, socketName, mainProcessPath);
+
+ return 0;
+ }
+
+ static FileStream OpenScriptStream (string path)
+ {
+ return File.Open (path, FileMode.Create, FileAccess.Write, FileShare.Read);
+ }
+
+ static StreamWriter OpenScriptWriter (FileStream fs)
+ {
+ return new StreamWriter (fs, Utilities.UTF8NoBOM);
+ }
+
+ static void WriteLldbScript (ParsedOptions parsedOptions, AndroidDevice device, string socketScheme, string socketDir, string socketName, string mainProcessPath)
+ {
+ string outputFile = Path.Combine (parsedOptions.OutputDirPath!, parsedOptions.LldbScriptName!);
+ string fullLibsDir = Path.GetFullPath (Path.Combine (parsedOptions.OutputDirPath!, parsedOptions.AppNativeLibrariesDir!));
+ using FileStream fs = OpenScriptStream (outputFile);
+ using StreamWriter sw = OpenScriptWriter (fs);
+
+ // TODO: add support for appending user commands
+ var searchPathsList = new List {
+ $"\"{Path.Combine (fullLibsDir, device.MainAbi)}\""
+ };
+
+ foreach (string abi in device.AvailableAbis) {
+ if (String.Compare (abi, device.MainAbi, StringComparison.Ordinal) == 0) {
+ continue;
+ }
+
+ searchPathsList.Add ($"\"{Path.Combine (fullLibsDir, abi)}\"");
+ }
+
+ string searchPaths = String.Join (" ", searchPathsList);
+ sw.WriteLine ($"settings append target.exec-search-paths {searchPaths}");
+ sw.WriteLine ("platform select remote-android");
+ sw.WriteLine ($"platform connect {socketScheme}-connect://{socketDir}/{socketName}");
+ sw.WriteLine ($"file \"{mainProcessPath}\"");
+
+ sw.Flush ();
+ }
+
+ static void WriteConfigScript (ParsedOptions parsedOptions, AndroidDevice device, AndroidNdk ndk, string socketScheme, string socketDir, string socketName)
+ {
+ bool powershell = Utilities.IsWindows;
+ string outputFile = Path.Combine (parsedOptions.OutputDirPath!, parsedOptions.ConfigScriptName!);
+ using FileStream fs = OpenScriptStream (outputFile);
+ using StreamWriter sw = OpenScriptWriter (fs);
+
+ sw.WriteLine ($"DEVICE_API_LEVEL={device.ApiLevel}");
+ sw.WriteLine ($"DEVICE_AVAILABLE_ABIS={FormatArray (device.AvailableAbis)}");
+ sw.WriteLine ($"DEVICE_AVAILABLE_ARCHES={FormatArray (device.AvailableArches)}");
+ sw.WriteLine ($"DEVICE_DEBUG_SERVER_LAUNCHER=\"{device.DebugServerLauncherScriptPath}\"");
+ sw.WriteLine ($"DEVICE_LLDB_DIR=\"{device.LldbBaseDir}\"");
+ sw.WriteLine ($"DEVICE_MAIN_ABI={device.MainAbi}");
+ sw.WriteLine ($"DEVICE_MAIN_ARCH={device.MainArch}");
+ sw.WriteLine ($"DEVICE_SERIAL=\"{device.SerialNumber}\"");
+ sw.WriteLine ($"LLDB_PATH=\"{ndk.LldbPath}\"");
+ sw.WriteLine ($"SOCKET_DIR={socketDir}");
+ sw.WriteLine ($"SOCKET_NAME={socketName}");
+ sw.WriteLine ($"SOCKET_SCHEME={socketScheme}");
+
+ sw.Flush ();
+
+ string FormatArray (string[] values)
+ {
+ var sb = new StringBuilder ();
+ if (powershell) {
+ sb.Append ('@');
+ }
+ sb.Append ('(');
+
+ bool first = true;
+ foreach (string v in values) {
+ if (first) {
+ first = false;
+ } else {
+ sb.Append (powershell ? ", " : " ");
+ }
+ sb.Append ('"');
+ sb.Append (v);
+ sb.Append ('"');
+ }
+
+ sb.Append (')');
+
+ return sb.ToString ();
+ }
+ }
+
+ static string[]? EnsureSupportedABIs (XamarinLoggingHelper log, string paramName, string? value, ref bool haveOptionErrors)
+ {
+ string? abis = EnsureNonEmptyString (log, paramName, value, ref haveOptionErrors);
+ if (abis == null) {
+ return null;
+ }
+
+ bool haveInvalidAbis = false;
+ var list = new List ();
+ foreach (string s in abis.Split (',')) {
+ string? abi = s?.Trim ();
+ if (String.IsNullOrEmpty (abi)) {
+ continue;
+ }
+
+ if (!SupportedAbiMap.TryGetValue (abi, out string? mappedAbi) || String.IsNullOrEmpty (mappedAbi)) {
+ log.ErrorLine ($"Unsupported ABI: {abi}");
+ haveInvalidAbis = true;
+ }
+
+ list.Add (mappedAbi!);
+ }
+
+ if (haveInvalidAbis) {
+ return null;
+ }
+
+ return list.ToArray ();
+ }
+
+ static string? EnsureNonEmptyString (XamarinLoggingHelper log, string paramName, string? value, ref bool haveOptionErrors)
+ {
+ if (String.IsNullOrEmpty (value)) {
+ haveOptionErrors = true;
+ log.ErrorLine ($"Parameter '{paramName}' requires a non-empty string as its value");
+ return null;
+ }
+
+ return value;
+ }
+
+ static string GetSupportedAbiNames () => String.Join (", ", SupportedAbiMap.Keys);
+}
diff --git a/tools/debug-session-prep/Resources/lldb-debug-session.sh b/tools/debug-session-prep/Resources/lldb-debug-session.sh
new file mode 100755
index 00000000000..b880d883e3d
--- /dev/null
+++ b/tools/debug-session-prep/Resources/lldb-debug-session.sh
@@ -0,0 +1,159 @@
+#!/bin/bash
+
+# Passed on command line
+ADB_DEVICE=""
+ARCH=""
+
+# Values set by the preparation task
+SESSION_LOG_DIR="."
+SESSION_STDERR_LOG_FILE=""
+
+# Detected during run
+DEVICE_API_LEVEL=""
+DEVICE_ABI=""
+DEVICE_ARCH=""
+
+# Constants
+ABI_PROPERTIES=(
+ # new properties
+ "ro.product.cpu.abilist"
+
+ # old properties
+ "ro.product.cpu.abi"
+ "ro.product.cpu.abi2"
+)
+
+function die()
+{
+ echo "$@" >&2
+ exit 1
+}
+
+function die_with_log()
+{
+ local log_file="${1}"
+
+ shift
+
+ echo "$@" >&2
+ if [ -f "${log_file}" ]; then
+ echo >&2
+ cat "${log_file}" >&2
+ echo >&2
+ fi
+
+ exit 1
+}
+
+function run_adb_nocheck()
+{
+ local args=""
+
+ if [ -n "${ADB_DEVICE}" ]; then
+ args="-s ${ADB_DEVICE}"
+ fi
+
+ COMMAND_OUTPUT="$(adb ${args} "$@")"
+}
+
+function run_adb()
+{
+ local command_stderr="${SESSION_LOG_DIR}/adb-cmd-stderr.log"
+
+ run_adb_nocheck "$@" 2> "${command_stderr}"
+ if [ $? -ne 0 ]; then
+ cat "${command_stderr}" >> "${SESSION_STDERR_LOG_FILE}"
+ die_with_log "${command_stderr}" "ADB command failed: " adb "${args}" "$@"
+ fi
+}
+
+function adb_shell()
+{
+ run_adb shell "$@"
+}
+
+function adb_shell_nocheck()
+{
+ run_adb_nocheck shell "$@"
+}
+
+function adb_get_property()
+{
+ adb_shell getprop "$@"
+}
+
+function get_api_level()
+{
+ adb_get_property ro.build.version.sdk
+ if [ $? -ne 0 ]; then
+ die "Unable to determine API level of the connected device"
+ fi
+ DEVICE_API_LEVEL="${COMMAND_OUTPUT}"
+}
+
+function property_is_equal_to()
+{
+ local prop_name="${1}"
+ local expected_value="${2}"
+
+ local prop_value
+ adb_get_property "${prop_name}"
+ prop_value=${COMMAND_OUTPUT}
+
+ if [ -z "${prop_value}" -o "${prop_value}" != "${expected_value}" ]; then
+ false
+ return
+ fi
+
+ true
+}
+
+function warn_old_pixel_c()
+{
+ adb_shell_nocheck cat /proc/sys/kernel/yama/ptrace_scope "2> /dev/null"
+ if [ $? -ne 0 ]; then
+ true
+ return
+ fi
+
+ local yama=${COMMAND_OUTPUT}
+ if [ -z "${yama}" -o "${yama}" == "0" ]; then
+ true
+ return
+ fi
+
+ local prop_value
+ adb_get_property ro.build.product
+ prop_value=${COMMAND_OUTPUT}
+
+ if ! property_is_equal_to "ro.build.product" "dragon"; then
+ true
+ return
+ fi
+
+ if ! property_is_equal_to "ro.product.name" "ryu"; then
+ true
+ return
+ fi
+
+ cat <&2
+
+WARNING: The device uses Yama ptrace_scope to restrict debugging. ndk-gdb will
+ likely be unable to attach to a process. With root access, the restriction
+ can be lifted by writing 0 to /proc/sys/kernel/yama/ptrace_scope. Consider
+ upgrading your Pixel C to MXC89L or newer, where Yama is disabled.
+
+EOF
+}
+
+if [ ! -d "${SESSION_LOG_DIR}" ]; then
+ install -d -m 755 "${SESSION_LOG_DIR}"
+fi
+
+SESSION_STDERR_LOG_FILE="${SESSION_LOG_DIR}/adb-stderr.log"
+rm -f "${SESSION_STDERR_LOG_FILE}"
+
+warn_old_pixel_c
+get_api_level
+
+echo API: ${DEVICE_API_LEVEL}
diff --git a/tools/debug-session-prep/Resources/xa_start_lldb_server.sh b/tools/debug-session-prep/Resources/xa_start_lldb_server.sh
new file mode 100755
index 00000000000..8fa6cdb3141
--- /dev/null
+++ b/tools/debug-session-prep/Resources/xa_start_lldb_server.sh
@@ -0,0 +1,41 @@
+#!/system/bin/sh
+
+# This script launches lldb-server on Android device from application subfolder - /data/data/$packageId/lldb/bin.
+# Native run configuration is expected to push this script along with lldb-server to the device prior to its execution.
+# Following command arguments are expected to be passed - lldb package directory and lldb-server listen port.
+set -x
+umask 0002
+
+LLDB_DIR=$1
+LISTENER_SCHEME=$2
+DOMAINSOCKET_DIR=$3
+PLATFORM_SOCKET=$4
+LOG_CHANNELS="$5"
+
+BIN_DIR=$LLDB_DIR/bin
+LOG_DIR=$LLDB_DIR/log
+TMP_DIR=$LLDB_DIR/tmp
+PLATFORM_LOG_FILE=$LOG_DIR/platform.log
+
+export LLDB_DEBUGSERVER_LOG_FILE=$LOG_DIR/gdb-server.log
+export LLDB_SERVER_LOG_CHANNELS="$LOG_CHANNELS"
+export LLDB_DEBUGSERVER_DOMAINSOCKET_DIR=$DOMAINSOCKET_DIR
+
+# This directory already exists. Make sure it has the right permissions.
+chmod 0775 "$LLDB_DIR"
+
+rm -r $TMP_DIR
+mkdir $TMP_DIR
+export TMPDIR=$TMP_DIR
+
+rm -r $LOG_DIR
+mkdir $LOG_DIR
+
+# LLDB would create these files with more restrictive permissions than our umask above. Make sure
+# it doesn't get a chance.
+# "touch" does not exist on pre API-16 devices. This is a poor man's replacement
+cat "$LLDB_DEBUGSERVER_LOG_FILE" 2>"$PLATFORM_LOG_FILE"
+
+cd $TMP_DIR # change cwd
+
+$BIN_DIR/lldb-server platform --server --listen $LISTENER_SCHEME://$DOMAINSOCKET_DIR/$PLATFORM_SOCKET --log-file "$PLATFORM_LOG_FILE" --log-channels "$LOG_CHANNELS" $LOG_DIR/platform-stdout.log 2>&1
diff --git a/tools/debug-session-prep/Xamarin.Android.Utilities/XamarinLoggingHelper.cs b/tools/debug-session-prep/Xamarin.Android.Utilities/XamarinLoggingHelper.cs
new file mode 100644
index 00000000000..4167c8852b6
--- /dev/null
+++ b/tools/debug-session-prep/Xamarin.Android.Utilities/XamarinLoggingHelper.cs
@@ -0,0 +1,174 @@
+using System;
+using System.IO;
+
+namespace Xamarin.Android.Utilities;
+
+enum LogLevel
+{
+ Error,
+ Warning,
+ Info,
+ Message,
+ Debug
+}
+
+class XamarinLoggingHelper
+{
+ static readonly object consoleLock = new object ();
+
+ public const ConsoleColor ErrorColor = ConsoleColor.Red;
+ public const ConsoleColor DebugColor = ConsoleColor.DarkGray;
+ public const ConsoleColor InfoColor = ConsoleColor.Green;
+ public const ConsoleColor MessageColor = ConsoleColor.Gray;
+ public const ConsoleColor WarningColor = ConsoleColor.Yellow;
+ public const ConsoleColor StatusLabel = ConsoleColor.Cyan;
+ public const ConsoleColor StatusText = ConsoleColor.White;
+
+ public bool Verbose { get; set; }
+
+ public void Message (string? message)
+ {
+ Log (LogLevel.Message, message);
+ }
+
+ public void MessageLine (string? message = null)
+ {
+ Message ($"{message ?? String.Empty}{Environment.NewLine}");
+ }
+
+ public void Warning (string? message)
+ {
+ Log (LogLevel.Warning, message);
+ }
+
+ public void WarningLine (string? message = null)
+ {
+ Warning ($"{message ?? String.Empty}{Environment.NewLine}");
+ }
+
+ public void Error (string? message)
+ {
+ Log (LogLevel.Error, message);
+ }
+
+ public void ErrorLine (string? message = null)
+ {
+ Error ($"{message ?? String.Empty}{Environment.NewLine}");
+ }
+
+ public void Info (string? message)
+ {
+ Log (LogLevel.Info, message);
+ }
+
+ public void InfoLine (string? message = null)
+ {
+ Info ($"{message ?? String.Empty}{Environment.NewLine}");
+ }
+
+ public void Debug (string? message)
+ {
+ Log (LogLevel.Debug, message);
+ }
+
+ public void DebugLine (string? message = null)
+ {
+ Debug ($"{message ?? String.Empty}{Environment.NewLine}");
+ }
+
+ public void Status (string label, string text)
+ {
+ Log (LogLevel.Info, $"{label}: ", StatusLabel);
+ Log (LogLevel.Info, $"{text}", StatusText);
+ }
+
+ public void StatusLine (string label, string text)
+ {
+ Status (label, text);
+ Log (LogLevel.Info, Environment.NewLine);
+ }
+
+ public void Log (LogLevel level, string? message)
+ {
+ if (!Verbose && level == LogLevel.Debug) {
+ return;
+ }
+
+ Log (level, message, ForegroundColor (level));
+ }
+
+ public void LogLine (LogLevel level, string? message, ConsoleColor color)
+ {
+ Log (level, message, color);
+ Log (level, Environment.NewLine, color);
+ }
+
+ public void Log (LogLevel level, string? message, ConsoleColor color)
+ {
+ if (!Verbose && level == LogLevel.Debug) {
+ return;
+ }
+
+ TextWriter writer = level == LogLevel.Error ? Console.Error : Console.Out;
+ message = message ?? String.Empty;
+
+ ConsoleColor fg = ConsoleColor.Gray;
+ try {
+ lock (consoleLock) {
+ fg = Console.ForegroundColor;
+ Console.ForegroundColor = color;
+ }
+
+ writer.Write (message);
+ } finally {
+ Console.ForegroundColor = fg;
+ }
+ }
+
+ ConsoleColor ForegroundColor (LogLevel level) => level switch {
+ LogLevel.Error => ErrorColor,
+ LogLevel.Warning => WarningColor,
+ LogLevel.Info => InfoColor,
+ LogLevel.Debug => DebugColor,
+ LogLevel.Message => MessageColor,
+ _ => MessageColor,
+ };
+
+#region MSBuild compatibility methods
+ public void LogDebugMessage (string message, params object[] messageArgs)
+ {
+ if (messageArgs == null || messageArgs.Length == 0) {
+ DebugLine (message);
+ } else {
+ DebugLine (String.Format (message, messageArgs));
+ }
+ }
+
+ public void LogError (string message, params object[] messageArgs)
+ {
+ if (messageArgs == null || messageArgs.Length == 0) {
+ ErrorLine (message);
+ } else {
+ ErrorLine (String.Format (message, messageArgs));
+ }
+ }
+
+ public void LogMessage (string message, params object[] messageArgs)
+ {
+ if (messageArgs == null || messageArgs.Length == 0) {
+ MessageLine (message);
+ } else {
+ MessageLine (String.Format (message, messageArgs));
+ }
+ }
+
+ public void LogWarning (string message, params object[] messageArgs)
+ {
+ if (messageArgs == null || messageArgs.Length == 0) {
+ WarningLine (message);
+ } else {
+ WarningLine (String.Format (message, messageArgs));
+ }
+ }
+#endregion
+}
diff --git a/tools/debug-session-prep/debug-session-prep.csproj b/tools/debug-session-prep/debug-session-prep.csproj
new file mode 100644
index 00000000000..5bca33d124f
--- /dev/null
+++ b/tools/debug-session-prep/debug-session-prep.csproj
@@ -0,0 +1,35 @@
+
+
+
+
+ Exe
+ False
+ $(MicrosoftAndroidSdkOutDir)
+ net7.0
+ debug_session_prep
+ enable
+ NO_MSBUILD
+
+
+
+
+
+
+
+
+
+
+
+
+ xa_start_lldb_server.sh
+
+
+ lldb-debug-session.sh
+
+
+
+
+
+
+
+
diff --git a/tools/xadebug/LICENSE b/tools/xadebug/LICENSE
new file mode 100644
index 00000000000..cc166e065e9
--- /dev/null
+++ b/tools/xadebug/LICENSE
@@ -0,0 +1,24 @@
+The MIT License (MIT)
+
+Copyright (c) .NET Foundation Contributors
+
+All rights reserved.
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+
diff --git a/tools/xadebug/Main.cs b/tools/xadebug/Main.cs
new file mode 100644
index 00000000000..c3f8a53b954
--- /dev/null
+++ b/tools/xadebug/Main.cs
@@ -0,0 +1,477 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Text;
+using System.Xml;
+
+using Microsoft.Build.Logging.StructuredLogger;
+using Mono.Options;
+using Xamarin.Android.Utilities;
+using Xamarin.Tools.Zip;
+
+namespace Xamarin.Android.Debug;
+
+sealed class ParsedOptions
+{
+ public bool ShowHelp;
+ public bool Verbose;
+ public string Configuration = "Debug";
+ public string? PackageName;
+ public string? Activity;
+ public string DotNetCommand = "dotnet";
+ public string WorkDirectory = "xadebug-data";
+ public string AdbPath = "adb";
+ public string? TargetDevice;
+ public string? NdkDirPath;
+}
+
+class XADebug
+{
+ const string AndroidManifestZipPath = "AndroidManifest.xml";
+ const string DefaultMinSdkVersion = "21";
+
+ static readonly string[] NdkEnvvars = {
+ "ANDROID_NDK_PATH",
+ "ANDROID_NDK_ROOT",
+ };
+
+ static XamarinLoggingHelper log = new XamarinLoggingHelper ();
+
+ static int Main (string[] args)
+ {
+ bool haveOptionErrors = false;
+ var parsedOptions = new ParsedOptions ();
+ log.Verbose = parsedOptions.Verbose;
+
+ var opts = new OptionSet {
+ "Usage: dotnet xadebug [OPTIONS] ",
+ "",
+ { "p|package-name=", "name of the application package", v => parsedOptions.PackageName = EnsureNonEmptyString (log, "-p|--package-name", v, ref haveOptionErrors) },
+ { "c|configuration=", $"{{CONFIGURATION}} in which to build the application. Ignored when running in APK-only mode. Default: {parsedOptions.Configuration}", v => parsedOptions.Configuration = v },
+ { "a|activity=", "Name of the {ACTIVITY} to start the application. Default: determined from AndroidManifest.xml inside the APK", v => parsedOptions.Activity = v },
+ { "d|dotnet=", $"Name of the dotnet {{COMMAND}} to use when building a project. Defaults to {parsedOptions.DotNetCommand}", v => parsedOptions.DotNetCommand = v },
+ { "w|work-dir=", $"{{DIRECTORY}} in which xadebug will store build and debug logs, as well as shared libraries with symbols. Default: {parsedOptions.WorkDirectory}", v => parsedOptions.WorkDirectory = v },
+ "",
+ { "s|adb=", "{PATH} to adb to use for this session", v => parsedOptions.AdbPath = EnsureNonEmptyString (log, "-s|--adb", v, ref haveOptionErrors) },
+ { "e|device=", "ID of {DEVICE} to target for this session", v => parsedOptions.TargetDevice = EnsureNonEmptyString (log, "-e|--device", v, ref haveOptionErrors) },
+ { "n|ndk-dir=", "{PATH} to to the Android NDK root directory", v => parsedOptions.NdkDirPath = v },
+ "",
+ { "v|verbose", "Show debug messages", v => parsedOptions.Verbose = true },
+ { "h|help|?", "Show this help screen", v => parsedOptions.ShowHelp = true },
+ };
+
+ List rest = opts.Parse (args);
+ log.Verbose = parsedOptions.Verbose;
+
+ DateTime now = DateTime.Now;
+ log.LogFilePath = Path.Combine (Path.GetFullPath (parsedOptions.WorkDirectory), $"session-{now.Year}-{now.Month:00}-{now.Day:00}-{now.Hour:00}:{now.Minute:00}:{now.Second:00}.log");
+ log.StatusLine ("Session log file", log.LogFilePath);
+
+ if (parsedOptions.ShowHelp || rest.Count == 0) {
+ int ret = 0;
+ if (!parsedOptions.ShowHelp) {
+ log.ErrorLine ("Path to application APK or directory with a C# project must be specified");
+ log.ErrorLine ();
+ ret = 1;
+ }
+
+ opts.WriteOptionDescriptions (Console.Out);
+ return ret;
+ }
+
+ if (String.IsNullOrEmpty (parsedOptions.DotNetCommand)) {
+ log.ErrorLine ("Empty string passed in the `-d|--dotnet` parameter. It must be a non-empty string.");
+ haveOptionErrors = true;
+ }
+
+ if (String.IsNullOrEmpty (parsedOptions.NdkDirPath)) {
+ string? ndk = null;
+
+ foreach (string envvar in NdkEnvvars) {
+ log.DebugLine ($"Trying to read NDK path environment variable '{envvar}'");
+ ndk = Environment.GetEnvironmentVariable (envvar);
+ if (!String.IsNullOrEmpty (ndk)) {
+ log.DebugLine ($"Potential NDK location: {ndk}");
+ break;
+ }
+ }
+
+ if (String.IsNullOrEmpty (ndk)) {
+ log.ErrorLine ("Unable to locate Android NDK from environment variables");
+ log.MessageLine ("Please provide path to the NDK using the '-n|--ndk' argument");
+ haveOptionErrors = true;
+ } else {
+ parsedOptions.NdkDirPath = ndk;
+ }
+ }
+
+ if (!Directory.Exists (parsedOptions.NdkDirPath)) {
+ log.ErrorLine ($"NDK directory '{parsedOptions.NdkDirPath}' does not exist");
+ return 1;
+ }
+
+ if (haveOptionErrors) {
+ return 1;
+ }
+
+ log.StatusLine ("Using NDK", parsedOptions.NdkDirPath);
+
+ string aPath = rest[0];
+ string? apkFilePath = null;
+ string? buildLogPath = null;
+ ZipArchive? apk = null;
+
+ if (Directory.Exists (aPath)) {
+ (apkFilePath, buildLogPath) = BuildApp (aPath, parsedOptions, projectPathIsDirectory: true);
+ } else if (File.Exists (aPath)) {
+ if (String.Compare (".csproj", Path.GetExtension (aPath), StringComparison.OrdinalIgnoreCase) == 0) {
+ // Let's see if we can trust the file name...
+ (apkFilePath, buildLogPath) = BuildApp (aPath, parsedOptions, projectPathIsDirectory: false);
+ } else if (IsAndroidPackageFile (aPath, out apk)) {
+ apkFilePath = aPath;
+ } else {
+ log.ErrorLine ($"File '{aPath}' is not an Android APK package");
+ log.ErrorLine ();
+ }
+ } else {
+ log.ErrorLine ($"Neither directory nor file '{aPath}' exist");
+ log.ErrorLine ();
+ }
+
+ if (!String.IsNullOrEmpty (buildLogPath)) {
+ log.StatusLine ("Build log", buildLogPath);
+ }
+
+ if (String.IsNullOrEmpty (apkFilePath)) {
+ return 1;
+ }
+
+ log.StatusLine ("Input APK", apkFilePath);
+
+ if (apk == null) {
+ apk = OpenApk (apkFilePath);
+ }
+
+ // Extract app information fromn the embedded manifest
+ ApplicationInfo? appInfo = ReadManifest (apk, parsedOptions);
+ if (appInfo == null) {
+ return 1;
+ }
+
+ if (!appInfo.Debuggable) {
+ log.ErrorLine ($"Application {apkFilePath} is not debuggable.");
+ log.MessageLine ();
+ log.MessageLine ("Please rebuild the aplication either in `Debug` configuration or with appropriate properties set in `Release` configuration:");
+ log.MessageLine ("TODO: fill in instructions");
+ log.MessageLine ();
+ return 1;
+ }
+
+ var debugSession = new DebugSession (log, appInfo, apkFilePath, apk, parsedOptions);
+ if (!debugSession.Prepare ()) {
+ return 1;
+ }
+
+ if (!debugSession.Run ()) {
+ return 1;
+ }
+
+ return 0;
+ }
+
+ static ApplicationInfo? ReadManifest (ZipArchive apk, ParsedOptions parsedOptions)
+ {
+ ZipEntry entry = apk.ReadEntry (AndroidManifestZipPath);
+
+ using var manifestData = new MemoryStream ();
+ entry.Extract (manifestData);
+ manifestData.Seek (0, SeekOrigin.Begin);
+
+ // TODO: make provisions for plain XML AndroidManifest.xml, perhaps? Although not sure if it's really necesary these days anymore as the APKs should all have the
+ // binary version of the manifest.
+ var axml = new AXMLParser (manifestData, log);
+ XmlDocument? manifest = axml.Parse ();
+ if (manifest == null) {
+ log.ErrorLine ("Unable to parse Android manifest from the apk");
+ return null;
+ }
+
+ var writerSettings = new XmlWriterSettings {
+ Encoding = new UTF8Encoding (false),
+ Indent = true,
+ IndentChars = "\t",
+ NewLineOnAttributes = false,
+ OmitXmlDeclaration = false,
+ WriteEndDocumentOnClose = true,
+ };
+
+ var manifestXml = new StringBuilder ();
+ using var writer = XmlWriter.Create (manifestXml, writerSettings);
+ manifest.WriteTo (writer);
+ writer.Flush ();
+ log.DebugLine ("Android manifest from the apk: START");
+ log.DebugLine (manifestXml.ToString ());
+ log.DebugLine ("Android manifest from the apk: END");
+
+ string? packageName = null;
+ XmlNode? node;
+
+ node = manifest.SelectSingleNode ("//manifest");
+ if (node == null) {
+ log.ErrorLine ("Unable to find root element 'manifest' of AndroidManifest.xml");
+ return null;
+ }
+
+ var nsManager = new XmlNamespaceManager (manifest.NameTable);
+ if (node.Attributes != null) {
+ const string nsPrefix = "xmlns:";
+
+ foreach (XmlAttribute attr in node.Attributes) {
+ if (!attr.Name.StartsWith (nsPrefix, StringComparison.Ordinal)) {
+ continue;
+ }
+
+ nsManager.AddNamespace (attr.Name.Substring (nsPrefix.Length), attr.Value);
+ }
+ }
+
+ if (String.IsNullOrEmpty (parsedOptions.PackageName)) {
+ packageName = GetAttributeValue (node, "package");
+ } else {
+ packageName = parsedOptions.PackageName;
+ }
+
+ if (String.IsNullOrEmpty (packageName)) {
+ log.ErrorLine ("Unable to determine the package name");
+ return null;
+ }
+
+ node = manifest.SelectSingleNode ("//manifest/uses-sdk");
+ string? minSdkVersion = GetAttributeValue (node, "android:minSdkVersion");
+ if (String.IsNullOrEmpty (minSdkVersion)) {
+ log.WarningLine ($"Android manifest doesn't specify the minimum SDK version supported by the application, assuming the default of {DefaultMinSdkVersion}");
+ minSdkVersion = DefaultMinSdkVersion;
+ }
+
+ ApplicationInfo? ret;
+ try {
+ ret = new ApplicationInfo (packageName, minSdkVersion);
+ } catch (Exception ex) {
+ log.ErrorLine ($"Exception {ex.GetType ()} thrown while constructing application info: {ex.Message}");
+ return null;
+ }
+
+ if (String.IsNullOrEmpty (parsedOptions.Activity)) {
+ node = manifest.SelectSingleNode ("//manifest/application");
+ string? debuggable = GetAttributeValue (node, "android:debuggable");
+ if (!String.IsNullOrEmpty (debuggable)) {
+ ret.Debuggable = String.Compare ("true", debuggable, StringComparison.OrdinalIgnoreCase) == 0;
+ }
+
+ node = manifest.SelectSingleNode ("//manifest/application/activity[./intent-filter/action[@android:name='android.intent.action.MAIN']]", nsManager);
+ if (node != null) {
+ ret.Activity = GetAttributeValue (node, "android:name");
+ log.DebugLine ($"Detected main activity: {ret.Activity}");
+ }
+ } else {
+ ret.Activity = parsedOptions.Activity;
+ }
+
+ return ret;
+ }
+
+ static string? GetAttributeValue (XmlNode? node, string prefixedAttributeName)
+ {
+ if (node?.Attributes == null) {
+ return null;
+ }
+
+ foreach (XmlAttribute attr in node.Attributes) {
+ if (String.Compare (prefixedAttributeName, attr.Name, StringComparison.Ordinal) == 0) {
+ return attr.Value;
+ }
+ }
+
+ return null;
+ }
+
+ static string EnsureNonEmptyString (XamarinLoggingHelper log, string paramName, string? value, ref bool haveOptionErrors)
+ {
+ if (String.IsNullOrEmpty (value)) {
+ haveOptionErrors = true;
+ log.ErrorLine ($"Parameter '{paramName}' requires a non-empty string as its value");
+ return String.Empty;
+ }
+
+ return value;
+ }
+
+ static ZipArchive OpenApk (string filePath) => ZipArchive.Open (filePath, FileMode.Open);
+
+ static bool IsAndroidPackageFile (string filePath, out ZipArchive? apk)
+ {
+ try {
+ apk = OpenApk (filePath);
+ } catch (ZipIOException ex) {
+ log.DebugLine ($"Failed to open '{filePath}' as ZIP archive: {ex.Message}");
+ apk = null;
+ return false;
+ }
+
+ return apk.ContainsEntry (AndroidManifestZipPath);
+ }
+
+ static (string? apkPath, string? buildLogPath) BuildApp (string projectPath, ParsedOptions parsedOptions, bool projectPathIsDirectory)
+ {
+ log.MessageLine ();
+
+ var dotnet = new DotNetRunner (log, parsedOptions.DotNetCommand, parsedOptions.WorkDirectory);
+ string? logPath = dotnet.Build (
+ projectPath,
+ parsedOptions.Configuration,
+ "-p:AndroidCreatePackagePerAbi=False",
+ "-p:AndroidPackageFormat=apk",
+ "-p:_AndroidAotStripLibraries=False",
+ "-p:_AndroidEnableNativeDebugging=True",
+ "-p:_AndroidStripNativeLibraries=False"
+ ).Result;
+
+ if (String.IsNullOrEmpty (logPath)) {
+ return FinishAndReturn (null, null);
+ }
+
+ string projectDir = projectPathIsDirectory ? projectPath : Path.GetDirectoryName (projectPath) ?? ".";
+ string? apkPath = FindApkPathFromLog (projectDir, logPath);
+
+ if (String.IsNullOrEmpty (apkPath)) {
+ log.DebugLine ("Could not get APK path from build log, trying to guess");
+ apkPath = TryToGuessApkPath (projectDir, parsedOptions);
+ }
+
+ if (String.IsNullOrEmpty (apkPath)) {
+ log.ErrorLine ("Unable to determine path to the application APK file after build.");
+ log.MessageLine ();
+ log.MessageLine ("Please run `xadebug` again, passing it path to the produced APK file");
+ log.MessageLine ();
+ return FinishAndReturn (null, logPath);
+ }
+
+ if (!File.Exists (apkPath)) {
+ log.ErrorLine ($"APK file '{apkPath}' not found after build");
+ return FinishAndReturn (null, logPath);
+ };
+
+ return FinishAndReturn (apkPath, logPath);
+
+ (string? apkPath, string? buildLogPath) FinishAndReturn (string? apkPath, string? buildLogPath)
+ {
+ log.MessageLine ();
+ return (apkPath, buildLogPath);
+ }
+ }
+
+ static string? TryToGuessApkPath (string projectDir, ParsedOptions parsedOptions)
+ {
+ log.DebugLine ("Trying to find application APK in {projectDir}");
+
+ string binDir = Path.Combine (projectDir, "bin", parsedOptions.Configuration);
+ if (!Directory.Exists (binDir)) {
+ log.WarningLine ($"Bin output directory '{binDir}' does not exist. Unable to determine path to the produced APK");
+ return null;
+ }
+
+ const string ApkSuffix = "-Signed.apk";
+ string apkName;
+ bool apkNameIsGlob;
+
+ if (!String.IsNullOrEmpty (parsedOptions.PackageName)) {
+ apkName = $"{parsedOptions.PackageName}{ApkSuffix}";
+ apkNameIsGlob = false;
+ } else {
+ apkName = $"*{ApkSuffix}";
+ apkNameIsGlob = true;
+ }
+
+ log.StatusLine ("Looking for APK with name", apkName);
+
+ if (!apkNameIsGlob) {
+ string apkPath = Path.Combine (binDir, apkName);
+ LogPotentialPath (apkPath);
+
+ if (File.Exists (apkPath)) {
+ return LogFoundApkPathAndReturn (apkPath);
+ }
+ }
+
+ // Find subdirectories named netX.Y-android and the apk files inside them
+ var apkFiles = new List ();
+ foreach (string dir in Directory.EnumerateDirectories (binDir, "net*-android")) {
+ if (apkNameIsGlob) {
+ foreach (string file in Directory.EnumerateFiles (dir, apkName)) {
+ // We know it exists, but the method also logs paths
+ AddApkIfExists (file, apkFiles);
+ }
+ } else {
+ AddApkIfExists (Path.Combine (dir, apkName), apkFiles);
+ }
+ }
+
+ if (apkFiles.Count == 0) {
+ return null;
+ }
+
+ string selectedApkPath;
+ if (apkFiles.Count > 1) {
+ // TODO: ask the user to select one
+ throw new NotImplementedException ("Support for multiple APK files not implemented yet");
+ } else {
+ selectedApkPath = apkFiles[0];
+ }
+
+ return LogFoundApkPathAndReturn (selectedApkPath);
+
+ void AddApkIfExists (string apkPath, List apkFiles)
+ {
+ LogPotentialPath (apkPath);
+ if (File.Exists (apkPath)) {
+ log.DebugLine ($"Found APK: {apkPath}");
+ apkFiles.Add (apkPath);
+ }
+ }
+
+ void LogPotentialPath (string path)
+ {
+ log.DebugLine ($"Trying path: {path}");
+ }
+ }
+
+ static string? FindApkPathFromLog (string projectDir, string? logPath)
+ {
+ if (String.IsNullOrEmpty (logPath)) {
+ return null;
+ }
+
+ log.DebugLine ($"Trying to find APK file path in the build log ('{logPath}')");
+
+ Build build = BinaryLog.ReadBuild (logPath);
+ foreach (Property prop in build.FindChildrenRecursive ()) {
+ if (String.Compare ("ApkFileSigned", prop.Name, StringComparison.Ordinal) != 0) {
+ continue;
+ }
+
+ if (Path.IsPathRooted (prop.Value)) {
+ return LogFoundApkPathAndReturn (prop.Value);
+ }
+
+ return LogFoundApkPathAndReturn (Path.Combine (projectDir, prop.Value));
+ }
+
+ return null;
+ }
+
+ static string LogFoundApkPathAndReturn (string path)
+ {
+ log.DebugLine ($"Returning APK path: {path}");
+ return path;
+ }
+}
diff --git a/tools/xadebug/README.md b/tools/xadebug/README.md
new file mode 100644
index 00000000000..30404ce4c54
--- /dev/null
+++ b/tools/xadebug/README.md
@@ -0,0 +1 @@
+TODO
\ No newline at end of file
diff --git a/tools/xadebug/Resources/xa_start_lldb_server.sh b/tools/xadebug/Resources/xa_start_lldb_server.sh
new file mode 100755
index 00000000000..d024cced9c7
--- /dev/null
+++ b/tools/xadebug/Resources/xa_start_lldb_server.sh
@@ -0,0 +1,41 @@
+#!/system/bin/sh
+
+# This script launches lldb-server on Android device from application subfolder - /data/data/$packageId/lldb/bin.
+# Native run configuration is expected to push this script along with lldb-server to the device prior to its execution.
+# Following command arguments are expected to be passed - lldb package directory and lldb-server listen port.
+set -x
+umask 0002
+
+LLDB_DIR="$1"
+LISTENER_SCHEME="$2"
+DOMAINSOCKET_DIR="$3"
+PLATFORM_SOCKET="$4"
+LOG_CHANNELS="$5"
+
+BIN_DIR="$LLDB_DIR/bin"
+LOG_DIR="$LLDB_DIR/log"
+TMP_DIR="$LLDB_DIR/tmp"
+PLATFORM_LOG_FILE="$LOG_DIR/platform.log"
+
+export LLDB_DEBUGSERVER_LOG_FILE="$LOG_DIR/gdb-server.log"
+export LLDB_SERVER_LOG_CHANNELS="$LOG_CHANNELS"
+export LLDB_DEBUGSERVER_DOMAINSOCKET_DIR="$DOMAINSOCKET_DIR"
+
+# This directory already exists. Make sure it has the right permissions.
+chmod 0775 "$LLDB_DIR"
+
+rm -r "$TMP_DIR"
+mkdir "$TMP_DIR"
+export TMPDIR="$TMP_DIR"
+
+rm -r "$LOG_DIR"
+mkdir "$LOG_DIR"
+
+# LLDB would create these files with more restrictive permissions than our umask above. Make sure
+# it doesn't get a chance.
+# "touch" does not exist on pre API-16 devices. This is a poor man's replacement
+cat "$LLDB_DEBUGSERVER_LOG_FILE" 2> "$PLATFORM_LOG_FILE"
+
+cd $TMP_DIR # change cwd
+
+"$BIN_DIR/lldb-server" platform --server --listen "$LISTENER_SCHEME://$DOMAINSOCKET_DIR/$PLATFORM_SOCKET" --log-file "$PLATFORM_LOG_FILE" --log-channels "$LOG_CHANNELS" "$LOG_DIR/platform-stdout.log" 2>&1
diff --git a/tools/xadebug/Xamarin.Android.Debug/AXMLParser.cs b/tools/xadebug/Xamarin.Android.Debug/AXMLParser.cs
new file mode 100644
index 00000000000..90b6f088e0f
--- /dev/null
+++ b/tools/xadebug/Xamarin.Android.Debug/AXMLParser.cs
@@ -0,0 +1,688 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Text;
+using System.Xml;
+
+using Xamarin.Android.Utilities;
+
+namespace Xamarin.Android.Debug;
+
+enum ChunkType : ushort
+{
+ Null = 0x0000,
+ StringPool = 0x0001,
+ Table = 0x0002,
+ Xml = 0x0003,
+
+ XmlFirstChunk = 0x0100,
+ XmlStartNamespace = 0x0100,
+ XmlEndNamespace = 0x0101,
+ XmlStartElement = 0x0102,
+ XmlEndElement = 0x0103,
+ XmlCData = 0x0104,
+ XmlLastChunk = 0x017f,
+ XmlResourceMap = 0x0180,
+
+ TablePackage = 0x0200,
+ TableType = 0x0201,
+ TableTypeSpec = 0x0202,
+ TableLibrary = 0x0203,
+}
+
+enum AttributeType : uint
+{
+ // The 'data' field is either 0 or 1, specifying this resource is either undefined or empty, respectively.
+ Null = 0x00,
+
+ // The 'data' field holds a ResTable_ref, a reference to another resource
+ Reference = 0x01,
+
+ // The 'data' field holds an attribute resource identifier.
+ Attribute = 0x02,
+
+ // The 'data' field holds an index into the containing resource table's global value string pool.
+ String = 0x03,
+
+ // The 'data' field holds a single-precision floating point number.
+ Float = 0x04,
+
+ // The 'data' holds a complex number encoding a dimension value such as "100in".
+ Dimension = 0x05,
+
+ // The 'data' holds a complex number encoding a fraction of a container.
+ Fraction = 0x06,
+
+ // The 'data' holds a dynamic ResTable_ref, which needs to be resolved before it can be used like a Reference
+ DynamicReference = 0x07,
+
+ // The 'data' holds an attribute resource identifier, which needs to be resolved before it can be used like a Attribute.
+ DynamicAttribute = 0x08,
+
+ // The 'data' is a raw integer value of the form n..n.
+ IntDec = 0x10,
+
+ // The 'data' is a raw integer value of the form 0xn..n.
+ IntHex = 0x11,
+
+ // The 'data' is either 0 or 1, for input "false" or "true" respectively.
+ IntBoolean = 0x12,
+
+ // The 'data' is a raw integer value of the form #aarrggbb.
+ IntColorARGB8 = 0x1c,
+
+ // The 'data' is a raw integer value of the form #rrggbb.
+ IntColorRGB8 = 0x1d,
+
+ // The 'data' is a raw integer value of the form #argb.
+ IntColorARGB4 = 0x1e,
+
+ // The 'data' is a raw integer value of the form #rgb.
+ IntColorRGB4 = 0x1f,
+}
+
+//
+// Based on https://github.com/androguard/androguard/tree/832104db3eb5dc3cc66b30883fa8ce8712dfa200/androguard/core/axml
+//
+class AXMLParser
+{
+ // Position of fields inside an attribute
+ const int ATTRIBUTE_IX_NAMESPACE_URI = 0;
+ const int ATTRIBUTE_IX_NAME = 1;
+ const int ATTRIBUTE_IX_VALUE_STRING = 2;
+ const int ATTRIBUTE_IX_VALUE_TYPE = 3;
+ const int ATTRIBUTE_IX_VALUE_DATA = 4;
+ const int ATTRIBUTE_LENGHT = 5;
+
+ const long MinimumDataSize = 8;
+ const long MaximumDataSize = (long)UInt32.MaxValue;
+
+ const uint ComplexUnitMask = 0x0f;
+
+ static readonly float[] RadixMultipliers = {
+ 0.00390625f,
+ 3.051758E-005f,
+ 1.192093E-007f,
+ 4.656613E-010f,
+ };
+
+ static readonly string[] DimensionUnits = {
+ "px",
+ "dip",
+ "sp",
+ "pt",
+ "in",
+ "mm",
+ };
+
+ static readonly string[] FractionUnits = {
+ "%",
+ "%p",
+ };
+
+ readonly XamarinLoggingHelper log;
+
+ Stream data;
+ long dataSize;
+ ARSCHeader axmlHeader;
+ uint fileSize;
+ StringBlock stringPool;
+ bool valid = true;
+ long initialPosition;
+
+ public bool IsValid => valid;
+
+ public AXMLParser (Stream data, XamarinLoggingHelper logger)
+ {
+ log = logger;
+
+ this.data = data;
+ dataSize = data.Length;
+
+ // Minimum is a single ARSCHeader, which would be a strange edge case...
+ if (dataSize < MinimumDataSize) {
+ throw new InvalidDataException ($"Input data size too small for it to be valid AXML content ({dataSize} < {MinimumDataSize})");
+ }
+
+ // This would be even stranger, if an AXML file is larger than 4GB...
+ // But this is not possible as the maximum chunk size is a unsigned 4 byte int.
+ if (dataSize > MaximumDataSize) {
+ throw new InvalidDataException ($"Input data size too large for it to be a valid AXML content ({dataSize} > {MaximumDataSize})");
+ }
+
+ try {
+ axmlHeader = new ARSCHeader (data);
+ } catch (Exception) {
+ log.ErrorLine ("Error parsing the first data header");
+ throw;
+ }
+
+ if (axmlHeader.HeaderSize != 8) {
+ throw new InvalidDataException ($"This does not look like AXML data. header size does not equal 8. header size = {axmlHeader.Size}");
+ }
+
+ fileSize = axmlHeader.Size;
+ if (fileSize > dataSize) {
+ throw new InvalidDataException ($"This does not look like AXML data. Declared data size does not match real size: {fileSize} vs {dataSize}");
+ }
+
+ if (fileSize < dataSize) {
+ log.WarningLine ($"Declared data size ({fileSize}) is smaller than total data size ({dataSize}). Was something appended to the file? Trying to parse it anyways.");
+ }
+
+ if (axmlHeader.Type != ChunkType.Xml) {
+ log.WarningLine ($"AXML file has an unusual resource type, trying to parse it anyways. Resource Type: 0x{(ushort)axmlHeader.Type:04x}");
+ }
+
+ ARSCHeader stringPoolHeader = new ARSCHeader (data, ChunkType.StringPool);
+ if (stringPoolHeader.HeaderSize != 28) {
+ throw new InvalidDataException ($"This does not look like an AXML file. String chunk header size does not equal 28. Header size = {stringPoolHeader.Size}");
+ }
+
+ stringPool = new StringBlock (logger, data, stringPoolHeader);
+ initialPosition = data.Position;
+ }
+
+ public XmlDocument? Parse ()
+ {
+ // Reset position in case we're called more than once, for whatever reason
+ data.Seek (initialPosition, SeekOrigin.Begin);
+ valid = true;
+
+ XmlDocument ret = new XmlDocument ();
+ XmlDeclaration declaration = ret.CreateXmlDeclaration ("1.0", stringPool.IsUTF8 ? "UTF-8" : "UTF-16", null);
+ ret.InsertBefore (declaration, ret.DocumentElement);
+
+ using var reader = new BinaryReader (data, Encoding.UTF8, leaveOpen: true);
+ ARSCHeader? header;
+ string? nsPrefix = null;
+ string? nsUri = null;
+ uint prefixIndex = 0;
+ uint uriIndex = 0;
+ var nsUriToPrefix = new Dictionary (StringComparer.Ordinal);
+ XmlNode? currentNode = ret.DocumentElement;
+
+ while (data.Position < dataSize) {
+ header = new ARSCHeader (data);
+
+ // Special chunk: Resource Map. This chunk might follow the string pool.
+ if (header.Type == ChunkType.XmlResourceMap) {
+ if (!SkipOverResourceMap (header, reader)) {
+ valid = false;
+ break;
+ }
+ continue;
+ }
+
+ // XML chunks
+
+ // Skip over unknown types
+ if (!Enum.IsDefined (typeof(ChunkType), header.TypeRaw)) {
+ log.WarningLine ($"Unknown chunk type 0x{header.TypeRaw:x} at offset {data.Position}. Skipping over {header.Size} bytes");
+ data.Seek (header.Size, SeekOrigin.Current);
+ continue;
+ }
+
+ // Check that we read a correct header
+ if (header.HeaderSize != 16) {
+ log.WarningLine ($"XML chunk header size is not 16. Chunk type {header.Type} (0x{header.TypeRaw:x}), chunk size {header.Size}");
+ data.Seek (header.Size, SeekOrigin.Current);
+ continue;
+ }
+
+ // Line Number of the source file, only used as meta information
+ uint lineNumber = reader.ReadUInt32 ();
+
+ // Comment_Index (usually 0xffffffff)
+ uint commentIndex = reader.ReadUInt32 ();
+
+ if (commentIndex != 0xffffffff && (header.Type == ChunkType.XmlStartNamespace || header.Type == ChunkType.XmlEndNamespace)) {
+ log.WarningLine ($"Unhandled Comment at namespace chunk: {commentIndex}");
+ }
+
+ if (header.Type == ChunkType.XmlStartNamespace) {
+ prefixIndex = reader.ReadUInt32 ();
+ uriIndex = reader.ReadUInt32 ();
+
+ nsPrefix = stringPool.GetString (prefixIndex);
+ nsUri = stringPool.GetString (uriIndex);
+
+ if (!String.IsNullOrEmpty (nsUri)) {
+ nsUriToPrefix[nsUri] = nsPrefix ?? String.Empty;
+ }
+
+ log.DebugLine ($"Start of Namespace mapping: prefix {prefixIndex}: '{nsPrefix}' --> uri {uriIndex}: '{nsUri}'");
+
+ if (String.IsNullOrEmpty (nsUri)) {
+ log.WarningLine ($"Namespace prefix '{nsPrefix}' resolves to empty URI.");
+ }
+
+ continue;
+ }
+
+ if (header.Type == ChunkType.XmlEndNamespace) {
+ // Namespace handling is **really** simplified, since we expect to deal only with AndroidManifest.xml which should have just one namespace.
+ // There should be no problems with that. Famous last words.
+ uint endPrefixIndex = reader.ReadUInt32 ();
+ uint endUriIndex = reader.ReadUInt32 ();
+
+ log.DebugLine ($"End of Namespace mapping: prefix {endPrefixIndex}, uri {endUriIndex}");
+ if (endPrefixIndex != prefixIndex) {
+ log.WarningLine ($"Prefix index of Namespace end doesn't match the last Namespace prefix index: {prefixIndex} != {endPrefixIndex}");
+ }
+
+ if (endUriIndex != uriIndex) {
+ log.WarningLine ($"URI index of Namespace end doesn't match the last Namespace URI index: {uriIndex} != {endUriIndex}");
+ }
+
+ string? endUri = stringPool.GetString (endUriIndex);
+ if (!String.IsNullOrEmpty (endUri) && nsUriToPrefix.ContainsKey (endUri)) {
+ nsUriToPrefix.Remove (endUri);
+ }
+
+ nsPrefix = null;
+ nsUri = null;
+ prefixIndex = 0;
+ uriIndex = 0;
+
+ continue;
+ }
+
+ uint tagNsUriIndex;
+ uint tagNameIndex;
+ string? tagName;
+// string? tagNs; // TODO: implement
+
+ if (header.Type == ChunkType.XmlStartElement) {
+ // The TAG consists of some fields:
+ // * (chunk_size, line_number, comment_index - we read before)
+ // * namespace_uri
+ // * name
+ // * flags
+ // * attribute_count
+ // * class_attribute
+ // After that, there are two lists of attributes, 20 bytes each
+ tagNsUriIndex = reader.ReadUInt32 ();
+ tagNameIndex = reader.ReadUInt32 ();
+ uint tagFlags = reader.ReadUInt32 ();
+ uint attributeCount = reader.ReadUInt32 () & 0xffff;
+ uint classAttribute = reader.ReadUInt32 ();
+
+ // Tag name is, of course, required but instead of throwing an exception should we find none, we use a fake name in hope that we can still salvage
+ // the document.
+ tagName = stringPool.GetString (tagNameIndex) ?? "unnamedTag";
+ log.DebugLine ($"Start of tag '{tagName}', NS URI index {tagNsUriIndex}");
+ log.DebugLine ($"Reading tag attributes ({attributeCount}):");
+
+ string? tagNsUri = tagNsUriIndex != 0xffffffff ? stringPool.GetString (tagNsUriIndex) : null;
+ string? tagNsPrefix;
+
+ if (String.IsNullOrEmpty (tagNsUri) || !nsUriToPrefix.TryGetValue (tagNsUri, out tagNsPrefix)) {
+ tagNsPrefix = null;
+ }
+
+ XmlElement element = ret.CreateElement (tagNsPrefix, tagName, tagNsUri);
+ if (currentNode == null) {
+ ret.AppendChild (element);
+ if (!String.IsNullOrEmpty (nsPrefix) && !String.IsNullOrEmpty (nsUri)) {
+ ret.DocumentElement!.SetAttribute ($"xmlns:{nsPrefix}", nsUri);
+ }
+ } else {
+ currentNode.AppendChild (element);
+ }
+ currentNode = element;
+
+ for (uint i = 0; i < attributeCount; i++) {
+ uint attrNsIdx = reader.ReadUInt32 (); // string index
+ uint attrNameIdx = reader.ReadUInt32 (); // string index
+ uint attrValue = reader.ReadUInt32 ();
+ uint attrType = reader.ReadUInt32 () >> 24;
+ uint attrData = reader.ReadUInt32 ();
+
+ string? attrNs = attrNsIdx != 0xffffffff ? stringPool.GetString (attrNsIdx) : String.Empty;
+ string? attrName = stringPool.GetString (attrNameIdx);
+
+ if (String.IsNullOrEmpty (attrName)) {
+ log.WarningLine ($"Attribute without name, ignoring. Offset: {data.Position}");
+ continue;
+ }
+
+ log.DebugLine ($" '{attrName}': ns == '{attrNs}'; value == 0x{attrValue:x}; type == 0x{attrType:x}; data == 0x{attrData:x}");
+ XmlAttribute attr;
+
+ if (!String.IsNullOrEmpty (attrNs)) {
+ attr = ret.CreateAttribute (nsUriToPrefix[attrNs], attrName, attrNs);
+ } else {
+ attr = ret.CreateAttribute (attrName!);
+ }
+ attr.Value = GetAttributeValue (attrValue, attrType, attrData);
+ element.SetAttributeNode (attr);
+ }
+ continue;
+ }
+
+ if (header.Type == ChunkType.XmlEndElement) {
+ tagNsUriIndex = reader.ReadUInt32 ();
+ tagNameIndex = reader.ReadUInt32 ();
+
+ tagName = stringPool.GetString (tagNameIndex);
+ log.DebugLine ($"End of tag '{tagName}', NS URI index {tagNsUriIndex}");
+ currentNode = currentNode?.ParentNode!;
+ continue;
+ }
+
+ // TODO: add support for CDATA
+ }
+
+ return ret;
+ }
+
+ string GetAttributeValue (uint attrValue, uint attrType, uint attrData)
+ {
+ if (!Enum.IsDefined (typeof(AttributeType), attrType)) {
+ log.WarningLine ($"Unknown attribute type value 0x{attrType:x}, returning empty attribute value (data == 0x{attrData:x}). Offset: {data.Position}");
+ return String.Empty;
+ }
+
+ switch ((AttributeType)attrType) {
+ case AttributeType.Null:
+ return attrData == 0 ? "?NULL?" : String.Empty;
+
+ case AttributeType.Reference:
+ return $"@{MaybePrefix()}{attrData:x08}";
+
+ case AttributeType.Attribute:
+ return $"?{MaybePrefix()}{attrData:x08}";
+
+ case AttributeType.String:
+ return stringPool.GetString (attrData) ?? String.Empty;
+
+ case AttributeType.Float:
+ return $"{(float)attrData}";
+
+ case AttributeType.Dimension:
+ return $"{ComplexToFloat(attrData)}{DimensionUnits[attrData & ComplexUnitMask]}";
+
+ case AttributeType.Fraction:
+ return $"{ComplexToFloat(attrData) * 100.0f}{FractionUnits[attrData & ComplexUnitMask]}";
+
+ case AttributeType.IntDec:
+ return attrData.ToString ();
+
+ case AttributeType.IntHex:
+ return $"0x{attrData:X08}";
+
+ case AttributeType.IntBoolean:
+ return attrData == 0 ? "false" : "true";
+
+ case AttributeType.IntColorARGB8:
+ case AttributeType.IntColorRGB8:
+ case AttributeType.IntColorARGB4:
+ case AttributeType.IntColorRGB4:
+ return $"#{attrData:X08}";
+ }
+
+ return String.Empty;
+
+ string MaybePrefix ()
+ {
+ if (attrData >> 24 == 1) {
+ return "android:";
+ }
+ return String.Empty;
+ }
+
+ float ComplexToFloat (uint value)
+ {
+ return (float)(value & 0xffffff00) * RadixMultipliers[(value >> 4) & 3];
+ }
+ }
+
+ bool SkipOverResourceMap (ARSCHeader header, BinaryReader reader)
+ {
+ log.DebugLine ("AXML contains a resource map");
+
+ // Check size: < 8 bytes mean that the chunk is not complete
+ // Should be aligned to 4 bytes.
+ if (header.Size < 8 || (header.Size % 4) != 0) {
+ log.ErrorLine ("Invalid chunk size in chunk XML_RESOURCE_MAP");
+ return false;
+ }
+
+ // Since our main interest is in reading AndroidManifest.xml, we're going to skip over the table
+ for (int i = 0; i < (header.Size - header.HeaderSize) / 4; i++) {
+ reader.ReadUInt32 ();
+ }
+
+ return true;
+ }
+}
+
+class StringBlock
+{
+ const uint FlagSorted = 1 << 0;
+ const uint FlagUTF8 = 1 << 0;
+
+ XamarinLoggingHelper log;
+ ARSCHeader header;
+ uint stringCount;
+ uint stringsOffset;
+ uint flags;
+ bool isUTF8;
+ List stringOffsets;
+ byte[] chars;
+ Dictionary stringCache;
+
+ public uint StringCount => stringCount;
+ public bool IsUTF8 => isUTF8;
+
+ public StringBlock (XamarinLoggingHelper logger, Stream data, ARSCHeader stringPoolHeader)
+ {
+ log = logger;
+ header = stringPoolHeader;
+
+ using var reader = new BinaryReader (data, Encoding.UTF8, leaveOpen: true);
+
+ stringCount = reader.ReadUInt32 ();
+ uint styleCount = reader.ReadUInt32 ();
+
+ flags = reader.ReadUInt32 ();
+ isUTF8 = (flags & FlagUTF8) == FlagUTF8;
+
+ stringsOffset = reader.ReadUInt32 ();
+ uint stylesOffset = reader.ReadUInt32 ();
+
+ if (styleCount == 0 && stylesOffset > 0) {
+ log.InfoLine ("Styles Offset given, but styleCount is zero. This is not a problem but could indicate packers.");
+ }
+
+ stringOffsets = new List ();
+
+ for (uint i = 0; i < stringCount; i++) {
+ stringOffsets.Add (reader.ReadUInt32 ());
+ }
+
+ // We're not interested in styles, skip over their offsets
+ for (uint i = 0; i < styleCount; i++) {
+ reader.ReadUInt32 ();
+ }
+
+ bool haveStyles = stylesOffset != 0 && styleCount != 0;
+ uint size = header.Size - stringsOffset;
+ if (haveStyles) {
+ size = stylesOffset - stringsOffset;
+ }
+
+ if (size % 4 != 0) {
+ log.WarningLine ("Size of strings is not aligned on four bytes.");
+ }
+
+ chars = new byte[size];
+ reader.Read (chars, 0, (int)size);
+
+ if (haveStyles) {
+ size = header.Size - stylesOffset;
+
+ if (size % 4 != 0) {
+ log.WarningLine ("Size of styles is not aligned on four bytes.");
+ }
+
+ // Not interested in them, skip
+ for (uint i = 0; i < size / 4; i++) {
+ reader.ReadUInt32 ();
+ }
+ }
+
+ stringCache = new Dictionary ();
+ }
+
+ public string? GetString (uint idx)
+ {
+ if (stringCache.TryGetValue (idx, out string? ret)) {
+ return ret;
+ }
+
+ if (idx < 0 || idx > stringOffsets.Count || stringOffsets.Count == 0) {
+ return null;
+ }
+
+ uint offset = stringOffsets[(int)idx];
+ if (isUTF8) {
+ ret = DecodeUTF8 (offset);
+ } else {
+ ret = DecodeUTF16 (offset);
+ }
+ stringCache[idx] = ret;
+
+ return ret;
+ }
+
+ string DecodeUTF8 (uint offset)
+ {
+ // UTF-8 Strings contain two lengths, as they might differ:
+ // 1) the string length in characters
+ (uint length, uint nbytes) = DecodeLength (offset, sizeOfChar: 1);
+ offset += nbytes;
+
+ // 2) the number of bytes the encoded string occupies
+ (uint encodedBytes, nbytes) = DecodeLength (offset, sizeOfChar: 1);
+ offset += nbytes;
+
+ if (chars[offset + encodedBytes] != 0) {
+ throw new InvalidDataException ($"UTF-8 string is not NUL-terminated. Offset: offset");
+ }
+
+ return Encoding.UTF8.GetString (chars, (int)offset, (int)encodedBytes);
+ }
+
+ string DecodeUTF16 (uint offset)
+ {
+ (uint length, uint nbytes) = DecodeLength (offset, sizeOfChar: 2);
+ offset += nbytes;
+
+ uint encodedBytes = length * 2;
+ if (chars[offset + encodedBytes] != 0 && chars[offset + encodedBytes + 1] != 0) {
+ throw new InvalidDataException ($"UTF-16 string is not NUL-terminated. Offset: offset");
+ }
+
+ return Encoding.Unicode.GetString (chars, (int)offset, (int)encodedBytes);
+ }
+
+ (uint length, uint nbytes) DecodeLength (uint offset, uint sizeOfChar)
+ {
+ uint sizeOfTwoChars = sizeOfChar << 1;
+ uint highBit = 0x80u << (8 * ((int)sizeOfChar - 1));
+ uint length1, length2;
+
+ // Length is tored as 1 or 2 characters of `sizeofChar` size
+ if (sizeOfChar == 1) {
+ // UTF-8 encoding, each character is a byte
+ length1 = chars[offset];
+ length2 = chars[offset + 1];
+ } else {
+ // UTF-16 encoding, each character is a short
+ length1 = (uint)((chars[offset]) | (chars[offset + 1] << 8));
+ length2 = (uint)((chars[offset + 2]) | (chars[offset + 3] << 8));
+ }
+
+ uint length;
+ uint nbytes;
+ if ((length1 & highBit) != 0) {
+ length = ((length1 & ~highBit) << (8 * (int)sizeOfChar)) | length2;
+ nbytes = sizeOfTwoChars;
+ } else {
+ length = length1;
+ nbytes = sizeOfChar;
+ }
+
+ // 8 bit strings: maximum of 0x7FFF bytes, http://androidxref.com/9.0.0_r3/xref/frameworks/base/libs/androidfw/ResourceTypes.cpp#692
+ // 16 bit strings: maximum of 0x7FFFFFF bytes, http://androidxref.com/9.0.0_r3/xref/frameworks/base/libs/androidfw/ResourceTypes.cpp#670
+ if (sizeOfChar == 1) {
+ if (length > 0x7fff) {
+ throw new InvalidDataException ("UTF-8 string is too long. Offset: {offset}");
+ }
+ } else {
+ if (length > 0x7fffffff) {
+ throw new InvalidDataException ("UTF-16 string is too long. Offset: {offset}");
+ }
+ }
+
+ return (length, nbytes);
+ }
+}
+
+class ARSCHeader
+{
+ // This is the minimal size such a header must have. There might be other header data too!
+ const long MinimumSize = 2 + 2 + 4;
+
+ long start;
+ uint size;
+ ushort type;
+ ushort headerSize;
+ bool unknownType;
+
+ public ChunkType Type => unknownType ? ChunkType.Null : (ChunkType)type;
+ public ushort TypeRaw => type;
+ public ushort HeaderSize => headerSize;
+ public uint Size => size;
+ public long End => start + (long)size;
+
+ public ARSCHeader (Stream data, ChunkType? expectedType = null)
+ {
+ start = data.Position;
+ if (data.Length < start + MinimumSize) {
+ throw new InvalidDataException ($"Input data not large enough. Offset: {start}");
+ }
+
+ // Data in AXML is little-endian, which is fortuitous as that's the only format BinaryReader understands.
+ using BinaryReader reader = new BinaryReader (data, Encoding.UTF8, leaveOpen: true);
+
+ // ushort: type
+ // ushort: header_size
+ // uint: size
+ type = reader.ReadUInt16 ();
+ headerSize = reader.ReadUInt16 ();
+
+ // Total size of the chunk, including the header
+ size = reader.ReadUInt32 ();
+
+ if (expectedType != null && type != (ushort)expectedType) {
+ throw new InvalidOperationException ($"Header type is not equal to the expected type ({expectedType}): got 0x{type:x}, expected 0x{(ushort)expectedType:x}");
+ }
+
+ unknownType = !Enum.IsDefined (typeof(ChunkType), type);
+
+ if (headerSize < MinimumSize) {
+ throw new InvalidDataException ($"Declared header size is smaller than required size of {MinimumSize}. Offset: {start}");
+ }
+
+ if (size < MinimumSize) {
+ throw new InvalidDataException ($"Declared chunk size is smaller than required size of {MinimumSize}. Offset: {start}");
+ }
+
+ if (size < headerSize) {
+ throw new InvalidDataException ($"Declared chunk size ({size}) is smaller than header size ({headerSize})! Offset: {start}");
+ }
+ }
+}
diff --git a/tools/xadebug/Xamarin.Android.Debug/AndroidDevice.cs b/tools/xadebug/Xamarin.Android.Debug/AndroidDevice.cs
new file mode 100644
index 00000000000..ad444409cc6
--- /dev/null
+++ b/tools/xadebug/Xamarin.Android.Debug/AndroidDevice.cs
@@ -0,0 +1,426 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+
+using Xamarin.Android.Tasks;
+using Xamarin.Android.Utilities;
+
+namespace Xamarin.Android.Debug;
+
+class AndroidDevice
+{
+ const string ServerLauncherScriptName = "xa_start_lldb_server.sh";
+
+ static readonly string[] abiProperties = {
+ // new properties
+ "ro.product.cpu.abilist",
+
+ // old properties
+ "ro.product.cpu.abi",
+ "ro.product.cpu.abi2",
+ };
+
+ static readonly string[] serialNumberProperties = {
+ "ro.serialno",
+ "ro.boot.serialno",
+ };
+
+ string packageName;
+ string adbPath;
+ List supportedAbis;
+ int apiLevel = -1;
+ string? appDataDir;
+ string? appLldbBaseDir;
+ string? appLldbBinDir;
+ string? appLldbLogDir;
+ string? appLldbTmpDir;
+ string? mainAbi;
+ string? mainArch;
+ string[]? deviceABIs;
+ string[]? availableAbis;
+ string[]? availableArches;
+ string? serialNumber;
+ bool appIs64Bit;
+ string? deviceLdd;
+ string? deviceDebugServerPath;
+ string? deviceDebugServerScriptPath;
+ string outputDir;
+
+ XamarinLoggingHelper log;
+ AdbRunner2 adb;
+ AndroidNdk ndk;
+
+ public int ApiLevel => apiLevel;
+ public string[] AvailableAbis => availableAbis ?? new string[] {};
+ public string[] AvailableArches => availableArches ?? new string[] {};
+ public string[] DeviceAbis => deviceABIs ?? new string[] {};
+ public string MainArch => mainArch ?? String.Empty;
+ public string MainAbi => mainAbi ?? String.Empty;
+ public string SerialNumber => serialNumber ?? String.Empty;
+ public string DebugServerLauncherScriptPath => deviceDebugServerScriptPath ?? String.Empty;
+ public string DebugServerPath => deviceDebugServerPath ?? String.Empty;
+ public string LldbBaseDir => appLldbBaseDir ?? String.Empty;
+ public string AppDataDir => appDataDir ?? String.Empty;
+ public string? DeviceLddPath => deviceLdd;
+ public AdbRunner2 AdbRunner => adb;
+
+ public AndroidDevice (XamarinLoggingHelper log, IProcessOutputLogger processLogger, AndroidNdk ndk, string outputDir, string adbPath, string packageName, List supportedAbis, string? adbTargetDevice = null)
+ {
+ this.adbPath = adbPath;
+ this.log = log;
+ this.packageName = packageName;
+ this.supportedAbis = supportedAbis;
+ this.ndk = ndk;
+ this.outputDir = outputDir;
+
+ adb = new AdbRunner2 (log, processLogger, adbPath, adbTargetDevice);
+ }
+
+ // TODO: implement manual error checking on API 21, since `adb` won't ever return any error code other than 0 - we need to look at the output of any command to determine
+ // whether or not it was successful. Ugh.
+ public bool GatherInfo ()
+ {
+ (bool success, string output) = adb.GetPropertyValue ("ro.build.version.sdk").Result;
+ if (!success || String.IsNullOrEmpty (output) || !Int32.TryParse (output, out apiLevel)) {
+ log.ErrorLine ("Unable to determine connected device's API level");
+ return false;
+ }
+
+ // Warn on old Pixel C firmware (b/29381985). Newer devices may have Yama
+ // enabled but still work with ndk-gdb (b/19277529).
+ (success, output) = adb.Shell ("cat", "/proc/sys/kernel/yama/ptrace_scope", "2>/dev/null").Result;
+ if (success &&
+ YamaOK (output.Trim ()) &&
+ PropertyIsEqualTo (adb.GetPropertyValue ("ro.build.product").Result, "dragon") &&
+ PropertyIsEqualTo (adb.GetPropertyValue ("ro.product.name").Result, "ryu")
+ ) {
+ log.WarningLine ("WARNING: The device uses Yama ptrace_scope to restrict debugging. ndk-gdb will");
+ log.WarningLine (" likely be unable to attach to a process. With root access, the restriction");
+ log.WarningLine (" can be lifted by writing 0 to /proc/sys/kernel/yama/ptrace_scope. Consider");
+ log.WarningLine (" upgrading your Pixel C to MXC89L or newer, where Yama is disabled.");
+ log.WarningLine ();
+ }
+
+ if (!DetermineArchitectureAndABI ()) {
+ return false;
+ }
+
+ if (!DetermineAppDataDirectory ()) {
+ return false;
+ }
+
+ serialNumber = GetFirstFoundPropertyValue (serialNumberProperties);
+ if (String.IsNullOrEmpty (serialNumber)) {
+ log.WarningLine ("Unable to determine device serial number");
+ }
+
+ return true;
+
+ bool YamaOK (string output)
+ {
+ return !String.IsNullOrEmpty (output) && String.Compare ("0", output, StringComparison.Ordinal) != 0;
+ }
+
+ bool PropertyIsEqualTo ((bool haveProperty, string value) result, string expected)
+ {
+ return
+ result.haveProperty &&
+ !String.IsNullOrEmpty (result.value) &&
+ String.Compare (result.value, expected, StringComparison.Ordinal) == 0;
+ }
+ }
+
+ public bool Prepare (out string? mainProcessPath)
+ {
+ mainProcessPath = null;
+ if (!DetectTools ()) {
+ return false;
+ }
+
+ if (!PushDebugServer ()) {
+ return false;
+ }
+
+ if (!PullLibraries (out mainProcessPath)) {
+ return false;
+ }
+
+ return true;
+ }
+
+ bool PullLibraries (out string? mainProcessPath)
+ {
+ mainProcessPath = null;
+ DeviceLibraryCopier copier;
+
+ if (String.IsNullOrEmpty (deviceLdd)) {
+ copier = new NoLddDeviceLibraryCopier (
+ log,
+ adb,
+ appIs64Bit,
+ outputDir,
+ this
+ );
+ } else {
+ copier = new LddDeviceLibraryCopier (
+ log,
+ adb,
+ appIs64Bit,
+ outputDir,
+ this
+ );
+ }
+
+ return copier.Copy (out mainProcessPath);
+ }
+
+ bool PushDebugServer ()
+ {
+ string? debugServerPath = ndk.GetDebugServerPath (mainAbi!);
+ if (String.IsNullOrEmpty (debugServerPath)) {
+ return false;
+ }
+
+ if (!CreateLldbDir (appLldbBinDir!) || !CreateLldbDir (appLldbLogDir) || !CreateLldbDir (appLldbTmpDir)) {
+ return false;
+ }
+
+ //string serverName = $"xa-{context.arch}-{Path.GetFileName (debugServerPath)}";
+ string serverName = Path.GetFileName (debugServerPath);
+ deviceDebugServerPath = $"{appLldbBinDir}/{serverName}";
+
+ KillDebugServer (deviceDebugServerPath);
+
+ // Always push the server binary, as we don't know what version might already be there
+ if (!PushServerExecutable (debugServerPath, deviceDebugServerPath)) {
+ return false;
+ }
+
+ string? launcherScript = Utilities.ReadManifestResource (log, ServerLauncherScriptName);
+ if (String.IsNullOrEmpty (launcherScript)) {
+ return false;
+ }
+
+ string launcherScriptPath = Path.Combine (outputDir, ServerLauncherScriptName);
+ Directory.CreateDirectory (Path.GetDirectoryName (launcherScriptPath)!);
+ File.WriteAllText (launcherScriptPath, launcherScript, Utilities.UTF8NoBOM);
+
+ deviceDebugServerScriptPath = $"{appLldbBinDir}/{Path.GetFileName (launcherScriptPath)}";
+ if (!PushServerExecutable (launcherScriptPath, deviceDebugServerScriptPath)) {
+ return false;
+ }
+
+ return true;
+
+ bool CreateLldbDir (string? dir)
+ {
+ if (String.IsNullOrEmpty (dir)) {
+ return false;
+ }
+
+ if (!adb.CreateDirectoryAs (packageName, dir).Result.success) {
+ log.ErrorLine ($"Failed to create debug server destination directory on device, {dir}");
+ return false;
+ }
+
+ return true;
+ }
+ }
+
+ bool PushServerExecutable (string hostSource, string deviceDestination)
+ {
+ string executableName = Path.GetFileName (deviceDestination);
+
+ // Always push the executable, as we don't know what version might already be there
+ log.DebugLine ($"Uploading {hostSource} to device");
+
+ // First upload to temporary path, as it's writable for everyone
+ string remotePath = $"/data/local/tmp/{executableName}";
+ if (!adb.Push (hostSource, remotePath).Result) {
+ log.ErrorLine ($"Failed to upload debug server {hostSource} to device path {remotePath}");
+ return false;
+ }
+
+ // Next, copy it to the app dir, with run-as
+ (bool success, string output) = adb.Shell (
+ "cat", remotePath, "|",
+ "run-as", packageName,
+ "sh", "-c", $"'cat > {deviceDestination}'"
+ ).Result;
+
+ if (!success) {
+ log.ErrorLine ($"Failed to copy debug executable to device, from {hostSource} to {deviceDestination}");
+ return false;
+ }
+
+ (success, output) = adb.RunAs (packageName, "chmod", "700", deviceDestination).Result;
+ if (!success) {
+ log.ErrorLine ($"Failed to make debug server executable on device, at {deviceDestination}");
+ return false;
+ }
+
+ return true;
+ }
+
+ // TODO: handle multiple pids
+ bool KillDebugServer (string debugServerPath)
+ {
+ long serverPID = GetDeviceProcessID (debugServerPath, quiet: true);
+ if (serverPID <= 0) {
+ return true;
+ }
+
+ log.DebugLine ("Killing previous instance of the debug server");
+ (bool success, string _) = adb.RunAs (packageName, "kill", "-9", $"{serverPID}").Result;
+ return success;
+ }
+
+ long GetDeviceProcessID (string processName, bool quiet = false)
+ {
+ (bool success, string output) = adb.Shell ("pidof", Path.GetFileName (processName)).Result;
+ if (!success) {
+ Action logger = quiet ? log.DebugLine : log.ErrorLine;
+ logger ($"Failed to obtain PID of process '{processName}'");
+ logger (output);
+ return -1;
+ }
+
+ output = output.Trim ();
+ if (!UInt32.TryParse (output, out uint pid)) {
+ if (!quiet) {
+ log.ErrorLine ($"Unable to parse string '{output}' as the package's PID");
+ }
+ return -1;
+ }
+
+ return pid;
+ }
+
+ bool DetectTools ()
+ {
+ // Not all versions of Android have the `which` utility, all of them have `whence`
+ // Also, API 21 adbd will not return an error code to us... But since we know that 21
+ // doesn't have LDD, we'll cheat
+ deviceLdd = null;
+ if (apiLevel > 21) {
+ (bool success, string output) = adb.Shell ("whence", "ldd").Result;
+ if (success) {
+ log.DebugLine ($"Found `ldd` on device at '{output}'");
+ deviceLdd = output;
+ }
+ }
+
+ if (String.IsNullOrEmpty (deviceLdd)) {
+ log.DebugLine ("`ldd` not found on device");
+ }
+
+ return true;
+ }
+
+ bool DetermineAppDataDirectory ()
+ {
+ (bool success, string output) = adb.GetAppDataDirectory (packageName).Result;
+ if (!AppDataDirFound (success, output)) {
+ log.ErrorLine ($"Unable to determine data directory for package '{packageName}'");
+ return false;
+ }
+
+ appDataDir = output.Trim ();
+
+ appLldbBaseDir = $"{appDataDir}/lldb";
+ appLldbBinDir = $"{appLldbBaseDir}/bin";
+ appLldbLogDir = $"{appLldbBaseDir}/log";
+ appLldbTmpDir = $"{appLldbBaseDir}/tmp";
+
+ // Applications with minSdkVersion >= 24 will have their data directories
+ // created with rwx------ permissions, preventing adbd from forwarding to
+ // the gdbserver socket. To be safe, if we're on a device >= 24, always
+ // chmod the directory.
+ if (apiLevel >= 24) {
+ (success, output) = adb.RunAs (packageName, "/system/bin/chmod", "a+x", appDataDir).Result;
+ if (!success) {
+ log.ErrorLine ("Failed to make application data directory world executable");
+ return false;
+ }
+ }
+
+ return true;
+
+ bool AppDataDirFound (bool success, string output)
+ {
+ if (apiLevel > 21) {
+ return success;
+ }
+
+ if (output.IndexOf ("run-as: Package", StringComparison.OrdinalIgnoreCase) >= 0 &&
+ output.IndexOf ("is unknown", StringComparison.OrdinalIgnoreCase) >= 0)
+ {
+ return false;
+ }
+
+ return true;
+ }
+ }
+
+ bool DetermineArchitectureAndABI ()
+ {
+ string? propValue = GetFirstFoundPropertyValue (abiProperties);
+ deviceABIs = propValue?.Split (',');
+
+ if (deviceABIs == null || deviceABIs.Length == 0) {
+ log.ErrorLine ("Unable to determine device ABI");
+ return false;
+ }
+
+ bool gotValidAbi = false;
+ var possibleAbis = new List ();
+ var possibleArches = new List ();
+
+ foreach (string deviceABI in deviceABIs) {
+ foreach (string appABI in supportedAbis) {
+ if (String.Compare (appABI, deviceABI, StringComparison.OrdinalIgnoreCase) == 0) {
+ string arch = AbiToArch (deviceABI);
+
+ if (!gotValidAbi) {
+ mainAbi = deviceABI;
+ mainArch = arch;
+ appIs64Bit = mainAbi.IndexOf ("64", StringComparison.Ordinal) >= 0;
+ gotValidAbi = true;
+ }
+
+ possibleAbis.Add (deviceABI);
+ possibleArches.Add (arch);
+ }
+ }
+ }
+
+ if (!gotValidAbi) {
+ log.ErrorLine ($"Application cannot run on the selected device: no matching ABI found");
+ }
+
+ availableAbis = possibleAbis.ToArray ();
+ availableArches = possibleArches.ToArray ();
+ return gotValidAbi;
+
+ string AbiToArch (string abi) => abi switch {
+ "armeabi" => "arm",
+ "armeabi-v7a" => "arm",
+ "arm64-v8a" => "arm64",
+ _ => abi,
+ };
+ }
+
+ string? GetFirstFoundPropertyValue (string[] propertyNames)
+ {
+ foreach (string prop in propertyNames) {
+ (bool success, string value) = adb.GetPropertyValue (prop).Result;
+ if (!success) {
+ continue;
+ }
+
+ return value;
+ }
+
+ return null;
+ }
+}
diff --git a/tools/xadebug/Xamarin.Android.Debug/AndroidNdk.cs b/tools/xadebug/Xamarin.Android.Debug/AndroidNdk.cs
new file mode 100644
index 00000000000..b75c188d388
--- /dev/null
+++ b/tools/xadebug/Xamarin.Android.Debug/AndroidNdk.cs
@@ -0,0 +1,101 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+
+using Xamarin.Android.Utilities;
+using Xamarin.Android.Tasks;
+
+namespace Xamarin.Android.Debug;
+
+class AndroidNdk
+{
+ // We want the shell/batch scripts first, since they set up Python environment for the debugger
+ static readonly string[] lldbNames = {
+ "lldb.sh",
+ "lldb",
+ "lldb.cmd",
+ "lldb.exe",
+ };
+
+ Dictionary hostLldbServerPaths;
+ XamarinLoggingHelper log;
+ string? lldbPath;
+
+ public string LldbPath => lldbPath ?? String.Empty;
+
+ public AndroidNdk (XamarinLoggingHelper log, string ndkRootPath, List supportedAbis)
+ {
+ this.log = log;
+ hostLldbServerPaths = new Dictionary (StringComparer.Ordinal);
+
+ if (!FindTools (ndkRootPath, supportedAbis)) {
+ throw new InvalidOperationException ("Failed to find all the required NDK tools");
+ }
+ }
+
+ public string? GetDebugServerPath (string abi)
+ {
+ if (!hostLldbServerPaths.TryGetValue (abi, out string? debugServerPath) || String.IsNullOrEmpty (debugServerPath)) {
+ log.ErrorLine ($"Debug server for abi '{abi}' not found.");
+ return null;
+ }
+
+ return debugServerPath;
+ }
+
+ bool FindTools (string ndkRootPath, List supportedAbis)
+ {
+ string toolchainDir = Path.Combine (ndkRootPath, NdkHelper.RelativeToolchainDir);
+ string toolchainBinDir = Path.Combine (toolchainDir, "bin");
+ string? path = null;
+
+ foreach (string lldb in lldbNames) {
+ path = Path.Combine (toolchainBinDir, lldb);
+ if (File.Exists (path)) {
+ break;
+ }
+ }
+
+ if (String.IsNullOrEmpty (path)) {
+ log.ErrorLine ($"Unable to locate lldb executable in '{toolchainBinDir}'");
+ return false;
+ }
+ lldbPath = path;
+
+ hostLldbServerPaths = new Dictionary (StringComparer.OrdinalIgnoreCase);
+ string llvmVersion = GetLlvmVersion (toolchainDir);
+ foreach (string abi in supportedAbis) {
+ string llvmAbi = NdkHelper.TranslateAbiToLLVM (abi);
+ path = Path.Combine (ndkRootPath, NdkHelper.RelativeToolchainDir, "lib64", "clang", llvmVersion, "lib", "linux", llvmAbi, "lldb-server");
+ if (!File.Exists (path)) {
+ log.ErrorLine ($"LLVM lldb server component for ABI '{abi}' not found at '{path}'");
+ return false;
+ }
+
+ hostLldbServerPaths.Add (abi, path);
+ }
+
+ if (hostLldbServerPaths.Count == 0) {
+ log.ErrorLine ("Unable to find any lldb-server executables, debugging not possible");
+ return false;
+ }
+
+ return true;
+ }
+
+ string GetLlvmVersion (string toolchainDir)
+ {
+ string path = Path.Combine (toolchainDir, "AndroidVersion.txt");
+ if (!File.Exists (path)) {
+ throw new InvalidOperationException ($"LLVM version file not found at '{path}'");
+ }
+
+ string[] lines = File.ReadAllLines (path);
+ string? line = lines.Length >= 1 ? lines[0].Trim () : null;
+ if (String.IsNullOrEmpty (line)) {
+ throw new InvalidOperationException ($"Unable to read LLVM version from '{path}'");
+ }
+
+ return line;
+ }
+}
diff --git a/tools/xadebug/Xamarin.Android.Debug/ApplicationInfo.cs b/tools/xadebug/Xamarin.Android.Debug/ApplicationInfo.cs
new file mode 100644
index 00000000000..1cfbe3e00e1
--- /dev/null
+++ b/tools/xadebug/Xamarin.Android.Debug/ApplicationInfo.cs
@@ -0,0 +1,21 @@
+using System;
+
+namespace Xamarin.Android.Debug;
+
+class ApplicationInfo
+{
+ public string PackageName { get; }
+ public uint MinSdkVersion { get; }
+ public bool Debuggable { get; set; }
+ public string? Activity { get; set; }
+
+ public ApplicationInfo (string packageName, string minSdkVersion)
+ {
+ PackageName = packageName;
+
+ if (!UInt32.TryParse (minSdkVersion, out uint ver)) {
+ throw new ArgumentException ($"Unable to parse minimum SDK version from '{minSdkVersion}'", nameof (minSdkVersion));
+ }
+ MinSdkVersion = ver;
+ }
+}
diff --git a/tools/xadebug/Xamarin.Android.Debug/DebugSession.cs b/tools/xadebug/Xamarin.Android.Debug/DebugSession.cs
new file mode 100644
index 00000000000..d817d5ac851
--- /dev/null
+++ b/tools/xadebug/Xamarin.Android.Debug/DebugSession.cs
@@ -0,0 +1,241 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+
+using Xamarin.Android.Utilities;
+using Xamarin.Tools.Zip;
+
+namespace Xamarin.Android.Debug;
+
+class DebugSession
+{
+ static readonly Dictionary KnownAbiDirs = new Dictionary (StringComparer.Ordinal) {
+ { "lib/arm64-v8a", "arm64-v8a" },
+ { "lib/armeabi-v7a", "armeabi-v7a" },
+ { "lib/x86_64", "x86_64" },
+ { "lib/x86", "x86" },
+ };
+
+ readonly string apkPath;
+ readonly ParsedOptions parsedOptions;
+ readonly XamarinLoggingHelper log;
+ readonly ZipArchive apk;
+ readonly string workDirectory;
+ readonly ApplicationInfo appInfo;
+
+ const string socketScheme = "unix-abstract";
+ string? socketDir = null;
+ string? socketName = null;
+ string? lldbScriptPath = null;
+ AndroidDevice? device = null;
+ AndroidNdk? ndk = null;
+
+ public DebugSession (XamarinLoggingHelper logger, ApplicationInfo appInfo, string apkPath, ZipArchive apk, ParsedOptions parsedOptions)
+ {
+ log = logger;
+ this.apkPath = apkPath;
+ this.parsedOptions = parsedOptions;
+ this.apk = apk;
+ this.appInfo = appInfo;
+ workDirectory = Path.Combine (parsedOptions.WorkDirectory, Utilities.StringHash (apkPath));
+ }
+
+ public bool Prepare ()
+ {
+ List supportedAbis = DetectSupportedAbis ();
+ if (supportedAbis.Count == 0) {
+ log.ErrorLine ("Unable to detect ABIs supported by the application");
+ return false;
+ }
+
+ ndk = new AndroidNdk (log, parsedOptions.NdkDirPath!, supportedAbis);
+ device = new AndroidDevice (
+ log, // general logger
+ log, // process output logger
+ ndk,
+ workDirectory,
+ parsedOptions.AdbPath,
+ appInfo.PackageName,
+ supportedAbis,
+ parsedOptions.TargetDevice
+ );
+
+ // Install first, since there might already be a version of this application on device that is not debuggable
+ if (!device.AdbRunner.Install (apkPath, apkIsDebuggable: true).Result) {
+ log.ErrorLine ($"Failed to install package '{apkPath}' to device");
+ return false;
+ }
+
+ if (!device.GatherInfo ()) {
+ return false;
+ }
+
+ string? mainProcessPath;
+ if (!device.Prepare (out mainProcessPath) || String.IsNullOrEmpty (mainProcessPath)) {
+ return false;
+ }
+
+ string appLibsDirectory = Path.Combine (workDirectory, "lib", device.MainAbi);
+ CopyAppLibs (device, appLibsDirectory);
+
+ log.MessageLine ();
+
+ if (supportedAbis.Count > 0) {
+ log.StatusLine ("All supported ABIs", String.Join (", ", supportedAbis));
+ }
+
+ socketDir = $"/xa-{appInfo.PackageName}";
+
+ var rnd = new Random ();
+ socketName = $"xa-platform-{rnd.NextInt64 ()}.sock";
+ lldbScriptPath = WriteLldbScript (appLibsDirectory, device, socketScheme, socketDir, socketName, mainProcessPath);
+
+ LogABIs ("Application", supportedAbis);
+ LogABIs (" Device", device.DeviceAbis);
+ log.StatusLine (" Selected ABI", $"{device.MainAbi} (architecture: {device.MainArch})");
+ log.MessageLine ();
+ log.StatusLine ("Application data directory on device", device.AppDataDir);
+ log.StatusLine ("Device serial number", device.SerialNumber);
+ log.StatusLine ("Debug server path on device", device.DebugServerPath);
+ log.StatusLine ("Debug server launcher script path on device", device.DebugServerLauncherScriptPath);
+ log.MessageLine ();
+
+ return true;
+
+ void LogABIs (string which, IEnumerable abis)
+ {
+ log.StatusLine ($"{which} ABIs", String.Join (", ", abis));
+ }
+ }
+
+ public bool Run ()
+ {
+ if (!EnsureValidState (nameof (socketDir), socketDir) ||
+ !EnsureValidState (nameof (socketName), socketName) ||
+ !EnsureValidState (nameof (lldbScriptPath), socketName) ||
+ !EnsureValidState (nameof (device), device) ||
+ !EnsureValidState (nameof (ndk), ndk)
+ ) {
+ return false;
+ }
+
+ log.InfoLine ("Starting lldb server on device");
+
+ // TODO: either start the server in the background of the remote shell or keep this `RunAs` in the background
+ (bool success, string output) = device!.AdbRunner.RunAs (
+ appInfo.PackageName,
+ device.DebugServerLauncherScriptPath,
+ device.LldbBaseDir,
+ socketScheme,
+ socketDir!,
+ socketName!,
+ "\\\"lldb process:gdb-remote packets\\\"").Result;
+
+ if (!success) {
+ return false;
+ }
+
+ // TODO: start the app
+ // TODO: start the app so that it waits for the debugger (until monodroid_gdb_wait is set)
+
+ return true;
+
+ bool EnsureValidState (string name, T? variable)
+ {
+ bool valid;
+
+ if (typeof(T) == typeof(string)) {
+ valid = !String.IsNullOrEmpty ((string?)(object?)variable);
+ } else {
+ valid = variable != null;
+ }
+
+ if (valid) {
+ return true;
+ }
+
+ log.ErrorLine ($"Debug session hasn't been initialized properly. Required variable '{name}' not set");
+ return false;
+ }
+ }
+
+ string WriteLldbScript (string appLibsDir, AndroidDevice device, string socketScheme, string socketDir, string socketName, string mainProcessPath)
+ {
+ string outputFile = Path.Combine (workDirectory, "lldb.x");
+ string fullLibsDir = Path.GetFullPath (appLibsDir);
+ using FileStream fs = File.Open (outputFile, FileMode.Create, FileAccess.Write, FileShare.Read);
+ using StreamWriter sw = new StreamWriter (fs, Utilities.UTF8NoBOM);
+
+ // TODO: add support for appending user commands
+ var searchPathsList = new List {
+ $"\"{fullLibsDir}\""
+ };
+
+ string searchPaths = String.Join (" ", searchPathsList);
+ sw.WriteLine ($"settings append target.exec-search-paths {searchPaths}");
+ sw.WriteLine ("platform select remote-android");
+ sw.WriteLine ($"platform connect {socketScheme}-connect://{socketDir}/{socketName}");
+ sw.WriteLine ($"file \"{mainProcessPath}\"");
+
+ log.DebugLine ($"Writing LLDB startup script: {outputFile}");
+ sw.Flush ();
+
+ return outputFile;
+ }
+
+ void CopyAppLibs (AndroidDevice device, string libDir)
+ {
+ log.DebugLine ($"Copying application shared libraries to '{libDir}'");
+ Directory.CreateDirectory (libDir);
+
+ string entryDir = $"lib/{device.MainAbi}/";
+ log.DebugLine ($"Looking for shared libraries inside APK, stored in the {entryDir} directory");
+
+ foreach (ZipEntry entry in apk) {
+ if (entry.IsDirectory) {
+ continue;
+ }
+
+ string dirName = Utilities.GetZipEntryDirName (entry.FullName);
+ if (dirName.Length == 0 || String.Compare (entryDir, dirName, StringComparison.Ordinal) != 0) {
+ continue;
+ }
+
+ string destPath = Path.Combine (libDir, Utilities.GetZipEntryFileName (entry.FullName));
+ log.DebugLine ($"Copying app library '{entry.FullName}' to '{destPath}'");
+
+ using var libraryData = File.Open (destPath, FileMode.Create);
+ entry.Extract (libraryData);
+ libraryData.Seek (0, SeekOrigin.Begin);
+ libraryData.Flush ();
+ libraryData.Close ();
+
+ // TODO: fetch symbols for libs which don't start with `libaot*` and aren't known Xamarin.Android libraries
+ }
+ }
+
+ List DetectSupportedAbis ()
+ {
+ var ret = new List ();
+
+ log.DebugLine ($"Detecting ABIs supported by '{apkPath}'");
+ HashSet seenAbis = new HashSet (StringComparer.Ordinal);
+ foreach (ZipEntry entry in apk) {
+ if (seenAbis.Count == KnownAbiDirs.Count) {
+ break;
+ }
+
+ // There might not be separate entries for lib/{ARCH} directories, so we look for the first file
+ // inside one of them to determine if an ABI is supported
+ string entryDir = Path.GetDirectoryName (entry.FullName) ?? String.Empty;
+ if (!KnownAbiDirs.TryGetValue (entryDir, out string? abi) || seenAbis.Contains (abi)) {
+ continue;
+ }
+
+ seenAbis.Add (abi);
+ ret.Add (abi);
+ }
+
+ return ret;
+ }
+}
diff --git a/tools/xadebug/Xamarin.Android.Debug/DeviceLibrariesCopier.cs b/tools/xadebug/Xamarin.Android.Debug/DeviceLibrariesCopier.cs
new file mode 100644
index 00000000000..73982bfa919
--- /dev/null
+++ b/tools/xadebug/Xamarin.Android.Debug/DeviceLibrariesCopier.cs
@@ -0,0 +1,79 @@
+using System;
+
+using Xamarin.Android.Utilities;
+using Xamarin.Android.Tasks;
+
+namespace Xamarin.Android.Debug;
+
+abstract class DeviceLibraryCopier
+{
+ protected XamarinLoggingHelper Log { get; }
+ protected bool AppIs64Bit { get; }
+ protected string LocalDestinationDir { get; }
+ protected AdbRunner2 Adb { get; }
+ protected AndroidDevice Device { get; }
+
+ protected DeviceLibraryCopier (XamarinLoggingHelper log, AdbRunner2 adb, bool appIs64Bit, string localDestinationDir, AndroidDevice device)
+ {
+ Log = log;
+ Adb = adb;
+ AppIs64Bit = appIs64Bit;
+ LocalDestinationDir = localDestinationDir;
+ Device = device;
+ }
+
+ protected string? FetchZygote ()
+ {
+ string zygotePath;
+ string destination;
+
+ if (AppIs64Bit) {
+ zygotePath = "/system/bin/app_process64";
+ destination = Utilities.MakeLocalPath (LocalDestinationDir, zygotePath);
+
+ Utilities.MakeFileDirectory (destination);
+ if (!Adb.Pull (zygotePath, destination).Result) {
+ return ReportFailureAndReturn ();
+ }
+ } else {
+ // /system/bin/app_process is 32-bit on 32-bit devices, but a symlink to
+ // app_process64 on 64-bit. If we need the 32-bit version, try to pull
+ // app_process32, and if that fails, pull app_process.
+ destination = Utilities.MakeLocalPath (LocalDestinationDir, "/system/bin/app_process");
+ string? source = "/system/bin/app_process32";
+
+ Utilities.MakeFileDirectory (destination);
+ if (!Adb.Pull (source, destination).Result) {
+ source = "/system/bin/app_process";
+ if (!Adb.Pull (source, destination).Result) {
+ source = null;
+ }
+ }
+
+ if (String.IsNullOrEmpty (source)) {
+ return ReportFailureAndReturn ();
+ }
+
+ zygotePath = destination;
+ }
+
+ Log.DebugLine ($"Zygote path: {zygotePath}");
+ return zygotePath;
+
+ string? ReportFailureAndReturn ()
+ {
+ const string appProcess32 = "app_process";
+ const string appProcess64 = appProcess32 + "64";
+
+ string bitness = AppIs64Bit ? "64" : "32";
+ string process = AppIs64Bit ? appProcess64 : appProcess32;
+
+ Log.ErrorLine ($"Failed to copy {bitness}-bit {process}");
+ Log.ErrorLine ("Unable to determine path of the zygote process on device");
+
+ return null;
+ }
+ }
+
+ public abstract bool Copy (out string? zygotePath);
+}
diff --git a/tools/xadebug/Xamarin.Android.Debug/LdConfigParser.cs b/tools/xadebug/Xamarin.Android.Debug/LdConfigParser.cs
new file mode 100644
index 00000000000..983ae60e5d4
--- /dev/null
+++ b/tools/xadebug/Xamarin.Android.Debug/LdConfigParser.cs
@@ -0,0 +1,167 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+
+using Xamarin.Android.Utilities;
+
+namespace Xamarin.Android.Debug;
+
+class LdConfigParser
+{
+ XamarinLoggingHelper log;
+
+ public LdConfigParser (XamarinLoggingHelper log)
+ {
+ this.log = log;
+ }
+
+ // Format: https://android.googlesource.com/platform/bionic/+/master/linker/ld.config.format.md
+ //
+ public (List searchPaths, HashSet permittedPaths) Parse (string localLdConfigPath, string deviceBinDirectory, string libDirName)
+ {
+ var searchPaths = new List ();
+ var permittedPaths = new HashSet ();
+ bool foundSomeSection = false;
+ bool insideMatchingSection = false;
+ string normalizedDeviceBinDirectory = Utilities.NormalizeDirectoryPath (deviceBinDirectory);
+ string? sectionName = null;
+
+ log.DebugLine ($"Parsing LD config file '{localLdConfigPath}'");
+ int lineCounter = 0;
+ var namespaces = new List {
+ "default"
+ };
+
+ foreach (string l in File.ReadLines (localLdConfigPath)) {
+ lineCounter++;
+ string line = l.Trim ();
+ if (line.Length == 0 || line.StartsWith ('#')) {
+ continue;
+ }
+
+ // The `dir.*` entries are before any section, don't waste time looking for them if we've parsed a section already
+ if (!foundSomeSection && sectionName == null) {
+ sectionName = GetMatchingDirMapping (normalizedDeviceBinDirectory, line);
+ if (sectionName != null) {
+ log.DebugLine ($"Found section name on line {lineCounter}: '{sectionName}'");
+ continue;
+ }
+ }
+
+ if (line[0] == '[') {
+ foundSomeSection = true;
+ insideMatchingSection = String.Compare (line, $"[{sectionName}]", StringComparison.Ordinal) == 0;
+ if (insideMatchingSection) {
+ log.DebugLine ($"Found section '{sectionName}' start on line {lineCounter}");
+ }
+ }
+
+ if (!insideMatchingSection) {
+ continue;
+ }
+
+ if (line.StartsWith ("additional.namespaces", StringComparison.Ordinal) && GetVariableAssignmentParts (line, out string? name, out string? value)) {
+ foreach (string v in value!.Split (',')) {
+ string nsName = v.Trim ();
+ if (nsName.Length == 0) {
+ continue;
+ }
+
+ log.DebugLine ($"Adding additional namespace '{nsName}'");
+ namespaces.Add (nsName);
+ }
+ continue;
+ }
+
+ MaybeAddLibraryPath (searchPaths, permittedPaths, namespaces, line, libDirName);
+ }
+
+ return (searchPaths, permittedPaths);
+
+ }
+
+ void MaybeAddLibraryPath (List searchPaths, HashSet permittedPaths, List knownNamespaces, string configLine, string libDirName)
+ {
+ if (!configLine.StartsWith ("namespace.", StringComparison.Ordinal)) {
+ return;
+ }
+
+ // not interested in ASAN libraries
+ if (configLine.IndexOf (".asan.", StringComparison.Ordinal) > 0) {
+ return;
+ }
+
+ foreach (string ns in knownNamespaces) {
+ if (!GetVariableAssignmentParts (configLine, out string? name, out string? value)) {
+ continue;
+ }
+
+ string varName = $"namespace.{ns}.search.paths";
+ if (String.Compare (varName, name, StringComparison.Ordinal) == 0) {
+ AddPath (searchPaths, "search", value!);
+ continue;
+ }
+
+ varName = $"namespace.{ns}.permitted.paths";
+ if (String.Compare (varName, name, StringComparison.Ordinal) == 0) {
+ AddPath (permittedPaths, "permitted", value!, checkIfAlreadyAdded: true);
+ }
+ }
+
+ void AddPath (ICollection list, string which, string value, bool checkIfAlreadyAdded = false)
+ {
+ string path = Utilities.NormalizeDirectoryPath (value.Replace ("${LIB}", libDirName));
+
+ if (checkIfAlreadyAdded && list.Contains (path)) {
+ return;
+ }
+
+ log.DebugLine ($"Adding library {which} path: {path}");
+ list.Add (path);
+ }
+ }
+
+ string? GetMatchingDirMapping (string deviceBinDirectory, string configLine)
+ {
+ const string LinePrefix = "dir.";
+
+ string line = configLine.Trim ();
+ if (line.Length == 0 || !line.StartsWith (LinePrefix, StringComparison.Ordinal)) {
+ return null;
+ }
+
+ if (!GetVariableAssignmentParts (line, out string? name, out string? value)) {
+ return null;
+ }
+
+ string dirPath = Utilities.NormalizeDirectoryPath (value!);
+ if (String.Compare (dirPath, deviceBinDirectory, StringComparison.Ordinal) != 0) {
+ return null;
+ }
+
+ string ns = name!.Substring (LinePrefix.Length).Trim ();
+ if (String.IsNullOrEmpty (ns)) {
+ return null;
+ }
+
+ return ns;
+ }
+
+ bool GetVariableAssignmentParts (string line, out string? name, out string? value)
+ {
+ name = value = null;
+
+ string[] parts = line.Split ("+=", 2);
+ if (parts.Length != 2) {
+ parts = line.Split ('=', 2);
+ if (parts.Length != 2) {
+ return false;
+ }
+ }
+
+ name = parts[0].Trim ();
+ value = parts[1].Trim ();
+
+ return true;
+ }
+}
diff --git a/tools/xadebug/Xamarin.Android.Debug/LddDeviceLibrariesCopier.cs b/tools/xadebug/Xamarin.Android.Debug/LddDeviceLibrariesCopier.cs
new file mode 100644
index 00000000000..166b611ecd1
--- /dev/null
+++ b/tools/xadebug/Xamarin.Android.Debug/LddDeviceLibrariesCopier.cs
@@ -0,0 +1,79 @@
+using System;
+using System.Collections.Generic;
+
+using Xamarin.Android.Utilities;
+
+namespace Xamarin.Android.Debug;
+
+class LddDeviceLibraryCopier : DeviceLibraryCopier
+{
+ public LddDeviceLibraryCopier (XamarinLoggingHelper log, AdbRunner2 adb, bool appIs64Bit, string localDestinationDir, AndroidDevice device)
+ : base (log, adb, appIs64Bit, localDestinationDir, device)
+ {}
+
+ public override bool Copy (out string? zygotePath)
+ {
+ string lddPath = Device.DeviceLddPath ?? throw new InvalidOperationException ("On-device `ldd` binary is required");
+
+ zygotePath = FetchZygote ();
+ if (String.IsNullOrEmpty (zygotePath)) {
+ return false;
+ }
+
+ (bool success, string zygoteLibs) = Adb.Shell (lddPath, zygotePath).Result;
+ if (!success) {
+ Log.ErrorLine ($"On-device ldd ({lddPath}) failed to return list of dependencies for Android application process {zygotePath}");
+ return false;
+ }
+
+ Log.DebugLine ("Zygote libs:");
+ Log.DebugLine (zygoteLibs);
+
+ (List libraryPaths, List libraryNames) = LddOutputToLibraryList (zygoteLibs);
+ if (libraryPaths.Count == 0 || libraryNames.Count == 0) {
+ Log.WarningLine ($"ldd didn't report any shared libraries on-device application process '{zygotePath}' depends on");
+ return true; // Not an error, per se
+ }
+
+ var moduleCache = new LddLldbModuleCache (Log, Device, libraryPaths, libraryNames);
+ moduleCache.Populate (zygotePath);
+
+ return true;
+ }
+
+ (List libraryPaths, List libraryNames) LddOutputToLibraryList (string output)
+ {
+ var libraryPaths = new List ();
+ var libraryNames = new List ();
+ if (String.IsNullOrEmpty (output)) {
+ return (libraryPaths, libraryNames);
+ }
+
+ // Overall line format is: LIBRARY_NAME => LIBRARY_PATH (HEX_ADDRESS)
+ // Lines are split on space, in assumption (and hope) that Android will not use filenames with spaces in them. This way we don't have to worry about the `=>`
+ // separator (which can, in theory, be changed to something else on some version of Android)
+ foreach (string l in output.Split ('\n')) {
+ string line = l.Trim ();
+ if (line.Length == 0) {
+ continue;
+ }
+
+ string[] parts = line.Split (' ');
+ if (parts.Length != 4) {
+ Log.WarningLine ($"ldd line has unsupported format, ignoring: '{line}'");
+ continue;
+ }
+
+ string path = parts[2];
+ if (String.Compare (path, "[vdso]", StringComparison.OrdinalIgnoreCase) == 0) {
+ // virtual library, doesn't exist on disk
+ continue;
+ }
+
+ libraryPaths.Add (path);
+ libraryNames.Add (parts[0]);
+ }
+
+ return (libraryPaths, libraryNames);
+ }
+}
diff --git a/tools/xadebug/Xamarin.Android.Debug/LddLldbModuleCache.cs b/tools/xadebug/Xamarin.Android.Debug/LddLldbModuleCache.cs
new file mode 100644
index 00000000000..edf833cf536
--- /dev/null
+++ b/tools/xadebug/Xamarin.Android.Debug/LddLldbModuleCache.cs
@@ -0,0 +1,24 @@
+using System.Collections.Generic;
+
+using Xamarin.Android.Utilities;
+
+namespace Xamarin.Android.Debug;
+
+class LddLldbModuleCache : LldbModuleCache
+{
+ List dependencyLibraryNames;
+
+ public LddLldbModuleCache (XamarinLoggingHelper log, AndroidDevice device, List deviceSharedLibraryPaths, List dependencyLibraryNames)
+ : base (log, device, deviceSharedLibraryPaths)
+ {
+ this.dependencyLibraryNames = dependencyLibraryNames;
+ }
+
+ protected override void FetchDependencies (HashSet alreadyDownloaded, string localPath)
+ {
+ Log.DebugLine ($"Binary {localPath} references the following libraries:");
+ foreach (string lib in dependencyLibraryNames) {
+ FetchLibrary (lib, alreadyDownloaded);
+ }
+ }
+}
diff --git a/tools/xadebug/Xamarin.Android.Debug/LldbModuleCache.cs b/tools/xadebug/Xamarin.Android.Debug/LldbModuleCache.cs
new file mode 100644
index 00000000000..1d58b0d5c6d
--- /dev/null
+++ b/tools/xadebug/Xamarin.Android.Debug/LldbModuleCache.cs
@@ -0,0 +1,114 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+
+using Xamarin.Android.Utilities;
+
+namespace Xamarin.Android.Debug;
+
+abstract class LldbModuleCache
+{
+ List deviceSharedLibraries;
+ Dictionary libraryCache;
+
+ protected AndroidDevice Device { get; }
+ protected string CacheDirPath { get; }
+ protected XamarinLoggingHelper Log { get; }
+
+ protected LldbModuleCache (XamarinLoggingHelper log, AndroidDevice device, List deviceSharedLibraries)
+ {
+ Device = device;
+ Log = log;
+
+ CacheDirPath = Path.Combine (
+ Environment.GetFolderPath (Environment.SpecialFolder.UserProfile),
+ ".lldb",
+ "module_cache",
+ "remote-android", // "platform" used by LLDB in our case
+ device.SerialNumber
+ );
+
+ this.deviceSharedLibraries = deviceSharedLibraries;
+ libraryCache = new Dictionary (StringComparer.Ordinal);
+ }
+
+ public void Populate (string zygotePath)
+ {
+ string? localPath = FetchFileFromDevice (zygotePath);
+ if (localPath == null) {
+ // TODO: should we perhaps fetch a set of "basic" libraries here, as a fallback?
+ Log.WarningLine ($"Unable to fetch Android application launcher binary ('{zygotePath}') from device. No cache of shared modules will be generated");
+ return;
+ }
+
+ var alreadyDownloaded = new HashSet (StringComparer.Ordinal);
+ FetchDependencies (alreadyDownloaded, localPath);
+ }
+
+ protected abstract void FetchDependencies (HashSet alreadyDownloaded, string localPath);
+
+ protected string? FetchLibrary (string lib, HashSet alreadyDownloaded)
+ {
+ Log.Debug ($" {lib}");
+ if (alreadyDownloaded.Contains (lib)) {
+ Log.DebugLine (" [already downloaded]");
+ return null;
+ }
+
+ string? deviceLibraryPath = GetSharedLibraryPath (lib);
+ if (String.IsNullOrEmpty (deviceLibraryPath)) {
+ Log.DebugLine (" [device path unknown]");
+ Log.WarningLine ($"Referenced libary '{lib}' not found on device");
+ return null;
+ }
+
+ Log.DebugLine (" [downloading]");
+ Log.Status ("Downloading", deviceLibraryPath);
+ string? localLibraryPath = FetchFileFromDevice (deviceLibraryPath);
+ if (String.IsNullOrEmpty (localLibraryPath)) {
+ Log.Log (LogLevel.Info, " [FAILED]", XamarinLoggingHelper.ErrorColor);
+ return null;
+ }
+ Log.LogLine (LogLevel.Info, " [SUCCESS]", XamarinLoggingHelper.InfoColor);
+
+ alreadyDownloaded.Add (lib);
+
+ return localLibraryPath;
+ }
+
+ protected string? FetchFileFromDevice (string deviceFilePath)
+ {
+ string localFilePath = Utilities.MakeLocalPath (CacheDirPath, deviceFilePath);
+ string localTempFilePath = $"{localFilePath}.tmp";
+
+ Directory.CreateDirectory (Path.GetDirectoryName (localFilePath)!);
+
+ if (!Device.AdbRunner.Pull (deviceFilePath, localTempFilePath).Result) {
+ Log.ErrorLine ($"Failed to download {deviceFilePath} from the attached device");
+ return null;
+ }
+
+ File.Move (localTempFilePath, localFilePath, true);
+ return localFilePath;
+ }
+
+ string? GetSharedLibraryPath (string libraryName)
+ {
+ if (libraryCache.TryGetValue (libraryName, out string? libraryPath)) {
+ return libraryPath;
+ }
+
+ foreach (string libPath in deviceSharedLibraries) {
+ string fileName = Utilities.GetZipEntryFileName (libPath);
+
+ if (String.Compare (libraryName, fileName, StringComparison.Ordinal) == 0) {
+ libraryCache.Add (libraryName, libPath);
+ return libPath;
+ }
+ }
+
+ // Cache misses, too, the list isn't going to change
+ libraryCache.Add (libraryName, null);
+ return null;
+ }
+}
diff --git a/tools/xadebug/Xamarin.Android.Debug/NoLddDeviceLibrariesCopier.cs b/tools/xadebug/Xamarin.Android.Debug/NoLddDeviceLibrariesCopier.cs
new file mode 100644
index 00000000000..646891d819a
--- /dev/null
+++ b/tools/xadebug/Xamarin.Android.Debug/NoLddDeviceLibrariesCopier.cs
@@ -0,0 +1,189 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+
+using Xamarin.Android.Utilities;
+using Xamarin.Android.Tasks;
+
+namespace Xamarin.Android.Debug;
+
+class NoLddDeviceLibraryCopier : DeviceLibraryCopier
+{
+ const string LdConfigPath = "/system/etc/ld.config.txt";
+
+ // To make things interesting, it turns out that API29 devices have **both** API 28 and 29 (they report 28) and for that reason they have TWO config files for ld...
+ const string LdConfigPath28 = "/etc/ld.config.28.txt";
+ const string LdConfigPath29 = "/etc/ld.config.29.txt";
+
+ // TODO: We probably need a "provider" for the list of paths, since on ARM devices, /system/lib{64} directories contain x86/x64 binaries, and the ARM binaries are found in
+ // /system/lib{64]/arm{64} (but not on all devices, of course... e.g. Pixel 6 Pro doesn't have these)
+ //
+ // List of directory paths to use when the device has neither ldd nor /system/etc/ld.config.txt
+ static readonly string[] FallbackLibraryDirectories = {
+ "/system/@LIB@",
+ "/system/@LIB@/drm",
+ "/system/@LIB@/egl",
+ "/system/@LIB@/hw",
+ "/system/@LIB@/soundfx",
+ "/system/@LIB@/ssl",
+ "/system/@LIB@/ssl/engines",
+
+ // /system/vendor is a symlink to /vendor on some Android versions, we'll skip the latter then
+ "/system/vendor/@LIB@",
+ "/system/vendor/@LIB@/egl",
+ "/system/vendor/@LIB@/mediadrm",
+ };
+
+ public NoLddDeviceLibraryCopier (XamarinLoggingHelper log, AdbRunner2 adb, bool appIs64Bit, string localDestinationDir, AndroidDevice device)
+ : base (log, adb, appIs64Bit, localDestinationDir, device)
+ {}
+
+ public override bool Copy (out string? zygotePath)
+ {
+ zygotePath = FetchZygote ();
+ if (String.IsNullOrEmpty (zygotePath)) {
+ return false;
+ }
+
+ (List searchPaths, HashSet permittedPaths) = GetLibraryPaths ();
+
+ // Collect file listings for all the search directories
+ var sharedLibraries = new List ();
+ foreach (string path in searchPaths) {
+ AddSharedLibraries (sharedLibraries, path, permittedPaths);
+ }
+
+ var moduleCache = new NoLddLldbModuleCache (Log, Device, sharedLibraries);
+ moduleCache.Populate (zygotePath);
+
+ return true;
+ }
+
+ void AddSharedLibraries (List sharedLibraries, string deviceDirPath, HashSet permittedPaths)
+ {
+ AdbRunner2.OutputLineFilter filterOutErrors = (bool isStdError, string line) => {
+ if (!isStdError) {
+ return false; // don't suppress any lines on stdout
+ }
+
+ // Ignore these, since we don't really care and there's no point in spamming the output with red
+ return
+ line.IndexOf ("Permission denied", StringComparison.OrdinalIgnoreCase) >= 0 ||
+ line.IndexOf ("No such file or directory", StringComparison.OrdinalIgnoreCase) >= 0;
+ };
+
+ (bool success, string output) = Adb.Shell (filterOutErrors, "ls", "-l", deviceDirPath).Result;
+ if (!success) {
+ // We can't rely on `success` because `ls -l` will return an error code if the directory exists but has any entries access to whose is not permitted
+ if (output.IndexOf ("No such file or directory", StringComparison.OrdinalIgnoreCase) >= 0) {
+ Log.DebugLine ($"Shared libraries directory {deviceDirPath} not found on device");
+ return;
+ }
+ }
+
+ Log.DebugLine ($"Adding shared libraries from {deviceDirPath}");
+ foreach (string l in output.Split ('\n')) {
+ string line = l.Trim ();
+ if (line.Length == 0) {
+ continue;
+ }
+
+ // `ls -l` output has 8 columns for filesystem entries
+ string[] parts = line.Split (' ', 8, StringSplitOptions.RemoveEmptyEntries);
+ if (parts.Length != 8) {
+ continue;
+ }
+
+ string permissions = parts[0].Trim ();
+ string name = parts[7].Trim ();
+
+ // First column, permissions: `drwxr-xr-x`, `-rw-r--r--` etc
+ if (permissions[0] == 'd') {
+ // Directory
+ string nestedDirPath = $"{deviceDirPath}{name}/";
+ if (permittedPaths.Count > 0 && !permittedPaths.Contains (nestedDirPath)) {
+ Log.DebugLine ($"Directory '{nestedDirPath}' is not in the list of permitted directories, ignoring");
+ continue;
+ }
+
+ AddSharedLibraries (sharedLibraries, nestedDirPath, permittedPaths);
+ continue;
+ }
+
+ // Ignore entries that aren't regular .so files or symlinks
+ if ((permissions[0] != '-' && permissions[0] != 'l') || !name.EndsWith (".so", StringComparison.Ordinal)) {
+ continue;
+ }
+
+ string libPath;
+ if (permissions[0] == 'l') {
+ // Let's hope there are no libraries with -> in their name :P (if there are, we should use `readlink`)
+ const string SymlinkArrow = "->";
+
+ // Symlink, we'll add the target library instead
+ int idx = name.IndexOf (SymlinkArrow, StringComparison.Ordinal);
+ if (idx > 0) {
+ libPath = name.Substring (idx + SymlinkArrow.Length).Trim ();
+ } else {
+ Log.WarningLine ($"'ls -l' output line contains a symbolic link, but I can't determine the target:");
+ Log.WarningLine ($" '{line}'");
+ Log.WarningLine ("Ignoring this entry");
+ continue;
+ }
+ } else {
+ libPath = $"{deviceDirPath}{name}";
+ }
+
+ Log.DebugLine ($" {libPath}");
+ sharedLibraries.Add (libPath);
+ }
+ }
+
+ (List searchPaths, HashSet permittedPaths) GetLibraryPaths ()
+ {
+ string lib = AppIs64Bit ? "lib64" : "lib";
+
+ if (Device.ApiLevel == 21) {
+ // API21 devices (at least emulators) don't return adb error codes, so to avoid awkward error message parsing, we're going to just skip detection since we
+ // know what API21 has and doesn't have
+ return (GetFallbackDirs (), new HashSet ());
+ }
+
+ string localLdConfigPath = Utilities.MakeLocalPath (LocalDestinationDir, LdConfigPath);
+ Utilities.MakeFileDirectory (localLdConfigPath);
+
+ string deviceLdConfigPath;
+
+ if (Device.ApiLevel == 28) {
+ deviceLdConfigPath = LdConfigPath28;
+ } else if (Device.ApiLevel == 29) {
+ deviceLdConfigPath = LdConfigPath29;
+ } else {
+ deviceLdConfigPath = LdConfigPath;
+ }
+
+ if (!Adb.Pull (deviceLdConfigPath, localLdConfigPath).Result) {
+ Log.DebugLine ($"Device doesn't have {LdConfigPath}");
+ return (GetFallbackDirs (), new HashSet ());
+ } else {
+ Log.DebugLine ($"Downloaded {deviceLdConfigPath} to {localLdConfigPath}");
+ }
+
+ var parser = new LdConfigParser (Log);
+
+ // The app executables (app_process and app_process32) are both in /system/bin, so we can limit our
+ // library search paths to this location.
+ (List searchPaths, HashSet permittedPaths) = parser.Parse (localLdConfigPath, "/system/bin", lib);
+ if (searchPaths.Count == 0) {
+ searchPaths = GetFallbackDirs ();
+ }
+
+ return (searchPaths, permittedPaths);
+
+ List GetFallbackDirs ()
+ {
+ Log.DebugLine ("Using fallback library directories for this device");
+ return FallbackLibraryDirectories.Select (l => l.Replace ("@LIB@", lib)).ToList ();
+ }
+ }
+}
diff --git a/tools/xadebug/Xamarin.Android.Debug/NoLddLldbModuleCache.cs b/tools/xadebug/Xamarin.Android.Debug/NoLddLldbModuleCache.cs
new file mode 100644
index 00000000000..4cd03dcdac7
--- /dev/null
+++ b/tools/xadebug/Xamarin.Android.Debug/NoLddLldbModuleCache.cs
@@ -0,0 +1,100 @@
+using System;
+using System.Collections.Generic;
+
+using ELFSharp.ELF;
+using ELFSharp.ELF.Sections;
+using Xamarin.Android.Utilities;
+
+namespace Xamarin.Android.Debug;
+
+class NoLddLldbModuleCache : LldbModuleCache
+{
+ public NoLddLldbModuleCache (XamarinLoggingHelper log, AndroidDevice device, List deviceSharedLibraries)
+ : base (log, device, deviceSharedLibraries)
+ {}
+
+ protected override void FetchDependencies (HashSet alreadyDownloaded, string localPath)
+ {
+ using IELF? elf = ReadElfFile (localPath);
+ FetchDependencies (elf, alreadyDownloaded, localPath);
+ }
+
+ void FetchDependencies (IELF? elf, HashSet alreadyDownloaded, string localPath)
+ {
+ if (elf == null) {
+ Log.DebugLine ($"Failed to open '{localPath}' as an ELF file. Ignoring.");
+ return;
+ }
+
+ var dynstr = GetSection (elf, ".dynstr") as IStringTable;
+ if (dynstr == null) {
+ Log.DebugLine ($"ELF binary {localPath} has no .dynstr section, unable to read referenced shared library names");
+ return;
+ }
+
+ var needed = new HashSet (StringComparer.Ordinal);
+ foreach (IDynamicSection section in elf.GetSections ()) {
+ foreach (IDynamicEntry entry in section.Entries) {
+ if (entry.Tag != DynamicTag.Needed) {
+ continue;
+ }
+
+ AddNeeded (dynstr, entry);
+ }
+ }
+
+ Log.DebugLine ($"Binary {localPath} references the following libraries:");
+ foreach (string lib in needed) {
+ string? localLibraryPath = FetchLibrary (lib, alreadyDownloaded);
+ if (String.IsNullOrEmpty (localLibraryPath)) {
+ continue;
+ }
+
+ using IELF? libElf = ReadElfFile (localLibraryPath);
+ FetchDependencies (libElf, alreadyDownloaded, localLibraryPath);
+ }
+
+ void AddNeeded (IStringTable stringTable, IDynamicEntry entry)
+ {
+ ulong index;
+ if (entry is DynamicEntry entry64) {
+ index = entry64.Value;
+ } else if (entry is DynamicEntry entry32) {
+ index = (ulong)entry32.Value;
+ } else {
+ Log.WarningLine ($"DynamicEntry neither 32 nor 64 bit? Weird");
+ return;
+ }
+
+ string name = stringTable[(long)index];
+ if (needed.Contains (name)) {
+ return;
+ }
+
+ needed.Add (name);
+ }
+ }
+
+ IELF? ReadElfFile (string path)
+ {
+ try {
+ if (ELFReader.TryLoad (path, out IELF ret)) {
+ return ret;
+ }
+ } catch (Exception ex) {
+ Log.WarningLine ($"{path} may not be a valid ELF binary.");
+ Log.WarningLine (ex.ToString ());
+ }
+
+ return null;
+ }
+
+ ISection? GetSection (IELF elf, string sectionName)
+ {
+ if (!elf.TryGetSection (sectionName, out ISection section)) {
+ return null;
+ }
+
+ return section;
+ }
+}
diff --git a/tools/xadebug/Xamarin.Android.Debug/Utilities.cs b/tools/xadebug/Xamarin.Android.Debug/Utilities.cs
new file mode 100644
index 00000000000..acda6cd6ff2
--- /dev/null
+++ b/tools/xadebug/Xamarin.Android.Debug/Utilities.cs
@@ -0,0 +1,118 @@
+using System;
+using System.IO;
+using System.Reflection;
+using System.Runtime.InteropServices;
+using System.Text;
+
+using Java.Interop.Tools.JavaCallableWrappers;
+using Xamarin.Android.Utilities;
+
+namespace Xamarin.Android.Debug;
+
+static class Utilities
+{
+ public static readonly UTF8Encoding UTF8NoBOM = new UTF8Encoding (false);
+
+ public static bool IsMacOS { get; private set; }
+ public static bool IsLinux { get; private set; }
+ public static bool IsWindows { get; private set; }
+
+ static Utilities ()
+ {
+ if (RuntimeInformation.IsOSPlatform (OSPlatform.Windows)) {
+ IsWindows = true;
+ } else if (RuntimeInformation.IsOSPlatform (OSPlatform.OSX)) {
+ IsMacOS = true;
+ } else if (RuntimeInformation.IsOSPlatform (OSPlatform.Linux)) {
+ IsLinux = true;
+ }
+ }
+
+ public static void MakeFileDirectory (string filePath)
+ {
+ if (String.IsNullOrEmpty (filePath)) {
+ return;
+ }
+
+ string? dirName = Path.GetDirectoryName (filePath);
+ if (String.IsNullOrEmpty (dirName)) {
+ return;
+ }
+
+ Directory.CreateDirectory (dirName);
+ }
+
+ public static string? ReadManifestResource (XamarinLoggingHelper log, string resourceName)
+ {
+ using (var from = Assembly.GetExecutingAssembly ().GetManifestResourceStream (resourceName)) {
+ if (from == null) {
+ log.ErrorLine ($"Manifest resource '{resourceName}' cannot be loaded");
+ return null;
+ }
+
+ using (var sr = new StreamReader (from)) {
+ return sr.ReadToEnd ();
+ }
+ }
+ }
+
+ public static string NormalizeDirectoryPath (string dirPath)
+ {
+ if (dirPath.EndsWith ('/')) {
+ return dirPath;
+ }
+
+ return $"{dirPath}/";
+ }
+
+ public static string ToLocalPathFormat (string path) => IsWindows ? path.Replace ("/", "\\") : path;
+
+ public static string MakeLocalPath (string localDirectory, string remotePath)
+ {
+ string remotePathLocalFormat = ToLocalPathFormat (remotePath);
+ if (remotePath[0] == '/') {
+ return $"{localDirectory}{remotePathLocalFormat}";
+ }
+
+ return Path.Combine (localDirectory, remotePathLocalFormat);
+ }
+
+ public static string StringHash (string input, Encoding? encoding = null)
+ {
+ if (encoding == null) {
+ encoding = UTF8NoBOM;
+ }
+
+ byte[] hash = Crc64Helper.Compute (encoding.GetBytes (input));
+ if (hash.Length == 0) {
+ return input.GetHashCode ().ToString ("x");
+ }
+
+ var sb = new StringBuilder ();
+ foreach (byte b in hash) {
+ sb.Append (b.ToString ("x02"));
+ }
+
+ return sb.ToString ();
+ }
+
+ public static string GetZipEntryFileName (string zipEntryName)
+ {
+ int idx = zipEntryName.LastIndexOf ('/');
+ if (idx >= 0 && idx != zipEntryName.Length - 1) {
+ return zipEntryName.Substring (idx + 1);
+ }
+
+ return zipEntryName;
+ }
+
+ public static string GetZipEntryDirName (string zipEntryName)
+ {
+ int idx = zipEntryName.LastIndexOf ('/');
+ if (idx < 0) {
+ return String.Empty;
+ }
+
+ return zipEntryName.Substring (0, idx + 1);
+ }
+}
diff --git a/tools/xadebug/Xamarin.Android.Utilities/AdbRunner.cs b/tools/xadebug/Xamarin.Android.Utilities/AdbRunner.cs
new file mode 100644
index 00000000000..87d8a9aa57b
--- /dev/null
+++ b/tools/xadebug/Xamarin.Android.Utilities/AdbRunner.cs
@@ -0,0 +1,265 @@
+using System;
+using System.Collections.Generic;
+using System.Threading.Tasks;
+
+namespace Xamarin.Android.Utilities;
+
+class AdbRunner2 : ToolRunner2
+{
+ public delegate bool OutputLineFilter (bool isStdErr, string line);
+
+ sealed class CaptureOutputState
+ {
+ public OutputLineFilter? LineFilter;
+ public CaptureProcessOutputLogger? Logger;
+ }
+
+ sealed class CaptureProcessOutputLogger : IProcessOutputLogger
+ {
+ IProcessOutputLogger? wrappedLogger;
+ OutputLineFilter? lineFilter;
+ List lines;
+ string? stderrPrefix;
+ string? stdoutPrefix;
+
+ public List Lines => lines;
+
+ public IProcessOutputLogger? WrappedLogger => wrappedLogger;
+
+ public string? StdoutPrefix {
+ get => stdoutPrefix ?? wrappedLogger?.StdoutPrefix ?? String.Empty;
+ set => stdoutPrefix = value;
+ }
+
+ public string? StderrPrefix {
+ get => stderrPrefix ?? wrappedLogger?.StderrPrefix ?? String.Empty;
+ set => stderrPrefix = value;
+ }
+
+ public CaptureProcessOutputLogger (IProcessOutputLogger? wrappedLogger, OutputLineFilter? lineFilter = null)
+ {
+ this.wrappedLogger = wrappedLogger;
+ this.lineFilter = lineFilter;
+
+ lines = new List ();
+ }
+
+ public void WriteStderr (string text, bool writeLine = true)
+ {
+ if (LineFiltered (text, isStdError: true)) {
+ return;
+ }
+
+ wrappedLogger?.WriteStderr (text, writeLine);
+ }
+
+ public void WriteStdout (string text, bool writeLine = true)
+ {
+ if (LineFiltered (text, isStdError: false)) {
+ return;
+ }
+
+ lines.Add (text);
+ }
+
+ bool LineFiltered (string text, bool isStdError)
+ {
+ if (lineFilter == null) {
+ return false;
+ }
+
+ return lineFilter (isStdError, text);
+ }
+ }
+
+ string[]? initialParams;
+
+ public AdbRunner2 (ILogger logger, IProcessOutputLogger processOutputLogger, string adbPath, string? deviceSerial = null)
+ : base (adbPath, logger, processOutputLogger)
+ {
+ if (!String.IsNullOrEmpty (deviceSerial)) {
+ initialParams = new string[] { "-s", deviceSerial };
+ }
+ }
+
+ public async Task Pull (string remotePath, string localPath)
+ {
+ var runner = CreateAdbRunner ();
+ runner.AddArgument ("pull");
+ runner.AddArgument (remotePath);
+ runner.AddArgument (localPath);
+
+ return await RunAdbAsync (runner);
+ }
+
+ public async Task Push (string localPath, string remotePath)
+ {
+ var runner = CreateAdbRunner ();
+ runner.AddArgument ("push");
+ runner.AddArgument (localPath);
+ runner.AddArgument (remotePath);
+
+ return await RunAdbAsync (runner);
+ }
+
+ public async Task Install (string apkPath, bool apkIsDebuggable = false, bool replaceExisting = true, bool noStreaming = true)
+ {
+ var runner = CreateAdbRunner ();
+ runner.AddArgument ("install");
+
+ if (replaceExisting) {
+ runner.AddArgument ("-r");
+ }
+
+ if (apkIsDebuggable) {
+ runner.AddArgument ("-d"); // Allow version code downgrade
+ }
+
+ if (noStreaming) {
+ runner.AddArgument ("--no-streaming");
+ }
+
+ runner.AddQuotedArgument (apkPath);
+
+ return await RunAdbAsync (runner);
+ }
+
+ public async Task<(bool success, string output)> GetAppDataDirectory (string packageName)
+ {
+ return await RunAs (packageName, "/system/bin/sh", "-c", "pwd");
+ }
+
+ public async Task<(bool success, string output)> CreateDirectoryAs (string packageName, string directoryPath)
+ {
+ return await RunAs (packageName, "mkdir", "-p", directoryPath);
+ }
+
+ public async Task<(bool success, string output)> GetPropertyValue (string propertyName)
+ {
+ var runner = CreateAdbRunner ();
+ return await Shell ("getprop", propertyName);
+ }
+
+ public async Task<(bool success, string output)> RunAs (string packageName, string command, params string[] args)
+ {
+ return await Shell (
+ "run-as",
+ RunAsPrepareArgs (packageName, command, args),
+ lineFilter: null
+ );
+ }
+
+ public void RunAsInBackground (BackgroundProcessManager processManager, string packageName, string command, params string[] args)
+ {
+ RunAsInBackground (processManager, null, null, packageName, command, args);
+ }
+
+ public void RunAsInBackground (BackgroundProcessManager processManager, ProcessRunner2.BackgroundActionCompletionHandler? completionHandler, string packageName, string command, params string[] args)
+ {
+ RunAsInBackground (processManager, completionHandler, null, packageName, command, args);
+ }
+
+ public void RunAsInBackground (BackgroundProcessManager processManager, ProcessRunner2.BackgroundActionCompletionHandler? completionHandler, TimeSpan? processTimeout, string packageName, string command, params string[] args)
+ {
+ ShellInBackground (
+ processManager,
+ completionHandler,
+ processTimeout,
+ "run-as",
+ RunAsPrepareArgs (packageName, command, args),
+ lineFilter: null
+ );
+ }
+
+ IEnumerable RunAsPrepareArgs (string packageName, string command, params string[] args)
+ {
+ if (String.IsNullOrEmpty (packageName)) {
+ throw new ArgumentException ("must not be null or empty", nameof (packageName));
+ }
+
+ var shellArgs = new List {
+ packageName,
+ command,
+ };
+
+ if (args != null && args.Length > 0) {
+ shellArgs.AddRange (args);
+ }
+
+ return shellArgs;
+ }
+
+ public async Task<(bool success, string output)> Shell (string command, List args, OutputLineFilter? lineFilter = null)
+ {
+ return await Shell (command, (IEnumerable)args, lineFilter);
+ }
+
+ public async Task<(bool success, string output)> Shell (string command, params string[] args)
+ {
+ return await Shell (command, (IEnumerable)args, lineFilter: null);
+ }
+
+ public async Task<(bool success, string output)> Shell (OutputLineFilter lineFilter, string command, params string[] args)
+ {
+ return await Shell (command, (IEnumerable)args, lineFilter);
+ }
+
+ async Task<(bool success, string output)> Shell (string command, IEnumerable? args, OutputLineFilter? lineFilter)
+ {
+ if (String.IsNullOrEmpty (command)) {
+ throw new ArgumentException ("must not be null or empty", nameof (command));
+ }
+
+ var captureState = new CaptureOutputState {
+ LineFilter = lineFilter,
+ };
+
+ var runner = CreateAdbRunner (captureState);
+
+ runner.AddArgument ("shell");
+ runner.AddArgument (command);
+ runner.AddArguments (args);
+
+ return await CaptureAdbOutput (runner, captureState);
+ }
+
+ void ShellInBackground (BackgroundProcessManager processManager, ProcessRunner2.BackgroundActionCompletionHandler? completionHandler, TimeSpan? processTimeout,
+ string command, IEnumerable? args, OutputLineFilter? lineFilter)
+ {
+ }
+
+ async Task RunAdbAsync (ProcessRunner2 runner)
+ {
+ ProcessStatus status = await runner.RunAsync ();
+ return status.Success;
+ }
+
+ async Task<(bool success, string output)> CaptureAdbOutput (ProcessRunner2 runner, CaptureOutputState captureState)
+ {
+ ProcessStatus status = await runner.RunAsync ();
+
+ string output = captureState.Logger != null ? String.Join (Environment.NewLine, captureState.Logger.Lines) : String.Empty;
+ return (status.Success, output);
+ }
+
+ ProcessRunner2 CreateAdbRunner (CaptureOutputState? state = null) => InitProcessRunner (state, initialParams);
+
+ protected override ProcessRunner2 CreateProcessRunner (IProcessOutputLogger consoleProcessLogger, object? state, params string?[]? initialParams)
+ {
+ IProcessOutputLogger outputLogger;
+
+ if (state is CaptureOutputState captureState) {
+ captureState.Logger = new CaptureProcessOutputLogger (consoleProcessLogger, captureState.LineFilter);
+ outputLogger = captureState.Logger;
+ } else {
+ outputLogger = consoleProcessLogger;
+ }
+
+ outputLogger.StderrPrefix = "adb> ";
+ ProcessRunner2 ret = base.CreateProcessRunner (outputLogger, initialParams);
+
+ // Let's make sure all the messages we get are in English, since we need to parse some of them to detect problems
+ ret.Environment["LANG"] = "C";
+ return ret;
+ }
+}
diff --git a/tools/xadebug/Xamarin.Android.Utilities/BackgroundProcessManager.cs b/tools/xadebug/Xamarin.Android.Utilities/BackgroundProcessManager.cs
new file mode 100644
index 00000000000..3cc81af3f5b
--- /dev/null
+++ b/tools/xadebug/Xamarin.Android.Utilities/BackgroundProcessManager.cs
@@ -0,0 +1,61 @@
+using System;
+using System.Collections.Generic;
+using System.Threading.Tasks;
+
+using Xamarin.Android.Tasks;
+
+namespace Xamarin.Android.Utilities;
+
+class BackgroundProcessManager : IDisposable
+{
+ readonly object runnersLock = new object ();
+ readonly List runners;
+
+ bool disposed;
+
+ public BackgroundProcessManager ()
+ {
+ runners = new List ();
+ Console.CancelKeyPress += ConsoleCanceled;
+ }
+
+ // TODO: override finalizer only if 'Dispose(bool disposing)' has code to free unmanaged resources
+ ~BackgroundProcessManager ()
+ {
+ Dispose (disposing: false);
+ }
+
+ protected virtual void Dispose (bool disposing)
+ {
+ if (!disposed) {
+ if (disposing) {
+ // TODO: dispose managed state (managed objects)
+ }
+
+ FinishAllTasks ();
+ disposed = true;
+ }
+ }
+
+ public void Dispose ()
+ {
+ // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
+ Dispose (disposing: true);
+ GC.SuppressFinalize (this);
+ }
+
+ public void RunInBackground (ProcessRunner2 runner, ProcessRunner2.BackgroundActionCompletionHandler? completionHandler)
+ {}
+
+ void TaskFailed (Task task)
+ {
+ }
+
+ void FinishAllTasks ()
+ {
+ }
+
+ void ConsoleCanceled (object? sender, ConsoleCancelEventArgs args)
+ {
+ }
+}
diff --git a/tools/xadebug/Xamarin.Android.Utilities/DotNetRunner.cs b/tools/xadebug/Xamarin.Android.Utilities/DotNetRunner.cs
new file mode 100644
index 00000000000..82b13cff55b
--- /dev/null
+++ b/tools/xadebug/Xamarin.Android.Utilities/DotNetRunner.cs
@@ -0,0 +1,114 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Threading.Tasks;
+
+using Xamarin.Android.Tasks;
+
+namespace Xamarin.Android.Utilities;
+
+using Utils = Xamarin.Android.Debug.Utilities;
+
+class DotNetRunner : ToolRunner
+{
+ class StreamOutputSink : ToolOutputSink
+ {
+ readonly StreamWriter output;
+
+ public StreamOutputSink (XamarinLoggingHelper logger, StreamWriter output)
+ : base (logger)
+ {
+ this.output = output;
+ }
+
+ public override void WriteLine (string? value)
+ {
+ output.WriteLine (value ?? String.Empty);
+ }
+ }
+
+ readonly string workDirectory;
+
+ public DotNetRunner (XamarinLoggingHelper logger, string toolPath, string workDirectory)
+ : base (logger, toolPath)
+ {
+ this.workDirectory = workDirectory;
+ }
+
+ ///
+ /// Build project at (either path to a project directory or a project file) and return full
+ /// path to binary build log, if the build succeded, or null otherwise.
+ ///
+ public async Task Build (string projectPath, string configuration, params string[] extraArgs)
+ {
+ string projectWorkDir = Path.Combine (workDirectory, Utils.StringHash (projectPath));
+
+ Directory.CreateDirectory (projectWorkDir);
+ string binlogPath = Path.Combine (projectWorkDir, "build.binlog");
+
+ var runner = CreateProcessRunner ("build");
+ runner.
+ AddArgument ("-c").AddArgument (configuration).
+ AddArgument ($"-bl:\"{binlogPath}\"");
+
+ if (extraArgs != null && extraArgs.Length > 0) {
+ foreach (string arg in extraArgs) {
+ runner.AddArgument (arg);
+ }
+ }
+ runner.AddQuotedArgument (projectPath);
+
+ if (!await RunDotNet (runner)) {
+ return null;
+ }
+
+ //return await ConvertBinlogToText (binlogPath);
+ return binlogPath;
+ }
+
+ public async Task ConvertBinlogToText (string binlogPath)
+ {
+ if (!File.Exists (binlogPath)) {
+ Logger.ErrorLine ($"Binlog '{binlogPath}' does not exist, cannot convert to text");
+ return null;
+ }
+
+ bool stdoutEchoOrig = EchoStandardOutput;
+ ProcessRunner runner;
+
+ try {
+ EchoStandardOutput = false;
+ runner = CreateProcessRunner ("msbuild");
+ } finally {
+ EchoStandardOutput = stdoutEchoOrig;
+ }
+
+ runner.
+ AddArgument ("-v:diag").
+ AddQuotedArgument (binlogPath);
+
+ string logOutput = Path.ChangeExtension (binlogPath, ".txt");
+ using var fs = File.Open (logOutput, FileMode.Create);
+ using var sw = new StreamWriter (fs, Utils.UTF8NoBOM);
+ using var sink = new StreamOutputSink (Logger, sw);
+
+ try {
+ if (!await RunDotNet (runner, sink)) {
+ return null;
+ }
+ } finally {
+ sw.Flush ();
+ }
+
+ return logOutput;
+ }
+
+ async Task RunDotNet (ProcessRunner runner, ToolOutputSink? outputSink = null)
+ {
+ if (outputSink != null) {
+ SetupOutputSinks (runner, stdoutSink: outputSink, stderrSink: null, ignoreStderr: true);
+ }
+
+ return await RunTool (() => runner.Run ());
+ }
+}
diff --git a/tools/xadebug/Xamarin.Android.Utilities/ILogger.cs b/tools/xadebug/Xamarin.Android.Utilities/ILogger.cs
new file mode 100644
index 00000000000..cd7838781a8
--- /dev/null
+++ b/tools/xadebug/Xamarin.Android.Utilities/ILogger.cs
@@ -0,0 +1,22 @@
+using System;
+
+namespace Xamarin.Android.Utilities;
+
+interface ILogger
+{
+ void Message (string? message);
+ void MessageLine (string? message = null);
+ void Warning (string? message);
+ void WarningLine (string? message = null);
+ void Error (string? message);
+ void ErrorLine (string? message = null);
+ void Info (string? message);
+ void InfoLine (string? message = null);
+ void Debug (string? message);
+ void DebugLine (string? message = null);
+ void Status (string label, string text);
+ void StatusLine (string label, string text);
+ void Log (LogLevel level, string? message);
+ void LogLine (LogLevel level, string? message, ConsoleColor color);
+ void Log (LogLevel level, string? message, ConsoleColor color);
+}
diff --git a/tools/xadebug/Xamarin.Android.Utilities/IProcessOutputLogger.cs b/tools/xadebug/Xamarin.Android.Utilities/IProcessOutputLogger.cs
new file mode 100644
index 00000000000..3ffb3eda893
--- /dev/null
+++ b/tools/xadebug/Xamarin.Android.Utilities/IProcessOutputLogger.cs
@@ -0,0 +1,11 @@
+namespace Xamarin.Android.Utilities;
+
+interface IProcessOutputLogger
+{
+ IProcessOutputLogger? WrappedLogger { get; }
+ string? StdoutPrefix { get; set; }
+ string? StderrPrefix { get; set; }
+
+ void WriteStdout (string text, bool writeLine = true);
+ void WriteStderr (string text, bool writeLine = true);
+}
diff --git a/tools/xadebug/Xamarin.Android.Utilities/ProcessRunner.cs b/tools/xadebug/Xamarin.Android.Utilities/ProcessRunner.cs
new file mode 100644
index 00000000000..486a8a7a1f4
--- /dev/null
+++ b/tools/xadebug/Xamarin.Android.Utilities/ProcessRunner.cs
@@ -0,0 +1,318 @@
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace Xamarin.Android.Utilities;
+
+class ProcessRunner2 : IDisposable
+{
+ public delegate void BackgroundActionCompletionHandler (ProcessRunner2 runner, ProcessStatus status);
+
+ static readonly TimeSpan DefaultProcessTimeout = TimeSpan.FromMinutes (5);
+ static readonly TimeSpan DefaultOutputTimeout = TimeSpan.FromSeconds (10);
+
+ readonly object runLock = new object ();
+ readonly IProcessOutputLogger? outputLogger;
+ readonly ILogger? logger;
+ readonly string command;
+
+ bool disposed;
+ bool running;
+ List? arguments;
+ Task? backgroundProcess;
+
+ public bool CreateWindow { get; set; }
+ public Dictionary Environment { get; } = new Dictionary (StringComparer.Ordinal);
+ public string? FullCommandLine { get; private set; }
+ public bool LeaveRunning { get; set; }
+ public bool LogRunInfo { get; set; } = true;
+ public bool LogStderr { get; set; }
+ public bool LogStdout { get; set; }
+ public bool MakeProcessGroupLeader { get; set; }
+ public TimeSpan ProcessTimeout { get; set; } = DefaultProcessTimeout;
+ public Encoding StandardOutputEncoding { get; set; } = Encoding.Default;
+ public Encoding StandardErrorEncoding { get; set; } = Encoding.Default;
+ public TimeSpan StandardOutputTimeout { get; set; } = DefaultOutputTimeout;
+ public TimeSpan StandardErrorTimeout { get; set; } = DefaultOutputTimeout;
+ public Action? CustomizeStartInfo { get; set; }
+ public bool UseShell { get; set; }
+ public ProcessWindowStyle WindowStyle { get; set; } = ProcessWindowStyle.Hidden;
+ public string? WorkingDirectory { get; set; }
+
+ public ProcessRunner2 (string command, IProcessOutputLogger? outputLogger = null, ILogger? logger = null)
+ {
+ if (String.IsNullOrEmpty (command)) {
+ throw new ArgumentException ("must not be null or empty", nameof (command));
+ }
+
+ this.command = command;
+ this.outputLogger = outputLogger;
+ this.logger = logger;
+ }
+
+ ~ProcessRunner2 ()
+ {
+ Dispose (disposing: false);
+ }
+
+ public void Kill (bool gracefully = true)
+ {}
+
+ public void AddArguments (IEnumerable? args)
+ {
+ if (args == null) {
+ return;
+ }
+
+ foreach (string? a in args) {
+ if (String.IsNullOrEmpty (a)) {
+ continue;
+ }
+
+ AddArgument (a);
+ }
+ }
+
+ public void AddArgument (string arg)
+ {
+ if (arguments == null) {
+ arguments = new List ();
+ }
+
+ arguments.Add (arg);
+ }
+
+ public void AddQuotedArgument (string arg)
+ {
+ AddArgument ($"\"{arg}\"");
+ }
+
+ ///
+ /// Run process synchronously on the calling thread
+ ///
+ public ProcessStatus Run ()
+ {
+ return Run (ProcessTimeout);
+ }
+
+ public ProcessStatus Run (TimeSpan processTimeout)
+ {
+ MarkRunning ();
+
+ try {
+ return DoRun (processTimeout);
+ } finally {
+ MarkNotRunning ();
+ }
+ }
+
+ ProcessStatus DoRun (TimeSpan processTimeout)
+ {
+ var psi = new ProcessStartInfo (command) {
+ CreateNoWindow = !CreateWindow,
+ RedirectStandardError = LogStderr,
+ RedirectStandardOutput = LogStdout,
+ UseShellExecute = UseShell,
+ WindowStyle = WindowStyle,
+ };
+
+ if (arguments != null && arguments.Count > 0) {
+ psi.Arguments = String.Join (" ", arguments);
+ }
+
+ if (Environment.Count > 0) {
+ foreach (var kvp in Environment) {
+ psi.Environment.Add (kvp.Key, kvp.Value);
+ }
+ }
+
+ if (!String.IsNullOrEmpty (WorkingDirectory)) {
+ psi.WorkingDirectory = WorkingDirectory;
+ }
+
+ if (psi.RedirectStandardError) {
+ StandardErrorEncoding = StandardErrorEncoding;
+ }
+
+ if (psi.RedirectStandardOutput) {
+ StandardOutputEncoding = StandardOutputEncoding;
+ }
+
+ if (CustomizeStartInfo != null) {
+ CustomizeStartInfo (psi);
+ }
+
+ EnsureValidConfig (psi);
+
+ FullCommandLine = $"{psi.FileName} {psi.Arguments}";
+
+ ManualResetEventSlim? stderr_done = null;
+ if (LogStderr) {
+ stderr_done = new ManualResetEventSlim (false);
+ }
+
+ ManualResetEventSlim? stdout_done = null;
+ if (LogStdout) {
+ stdout_done = new ManualResetEventSlim (false);
+ }
+
+ if (LogRunInfo) {
+ logger?.DebugLine ($"Running: {FullCommandLine}");
+ }
+
+ var process = new Process {
+ StartInfo = psi
+ };
+
+ try {
+ process.Start ();
+ } catch (System.ComponentModel.Win32Exception ex) {
+ if (logger != null) {
+ logger.ErrorLine ($"Process failed to start: {ex.Message}");
+ logger.DebugLine (ex.ToString ());
+ }
+
+ return new ProcessStatus ();
+ }
+
+ if (psi.RedirectStandardError) {
+ process.ErrorDataReceived += (object sender, DataReceivedEventArgs e) => {
+ if (e.Data != null) {
+ outputLogger!.WriteStderr (e.Data ?? String.Empty);
+ } else {
+ stderr_done!.Set ();
+ }
+ };
+ process.BeginErrorReadLine ();
+ }
+
+ if (psi.RedirectStandardOutput) {
+ process.OutputDataReceived += (object sender, DataReceivedEventArgs e) => {
+ if (e.Data != null) {
+ outputLogger!.WriteStdout (e.Data ?? String.Empty);
+ } else {
+ stdout_done!.Set ();
+ }
+ };
+ process.BeginOutputReadLine ();
+ }
+
+ int timeout = processTimeout == TimeSpan.MaxValue ? -1 : (int)processTimeout.TotalMilliseconds;
+ bool exited = process.WaitForExit (timeout);
+ if (!exited) {
+ logger?.ErrorLine ($"Process '{FullCommandLine}' timed out after {ProcessTimeout}");
+ process.Kill ();
+ }
+
+ // See: https://docs.microsoft.com/en-us/dotnet/api/system.diagnostics.process.waitforexit?view=netframework-4.7.2#System_Diagnostics_Process_WaitForExit)
+ if (psi.RedirectStandardError || psi.RedirectStandardOutput) {
+ process.WaitForExit ();
+ }
+
+ if (stderr_done != null) {
+ stderr_done.Wait (StandardErrorTimeout);
+ }
+
+ if (stdout_done != null) {
+ stdout_done.Wait (StandardOutputTimeout);
+ }
+
+ return new ProcessStatus (process.ExitCode, exited, process.ExitCode == 0);
+ }
+
+ ///
+ /// Run process in a separate thread. The caller is responsible for awaiting on the returned Task
+ ///
+ public Task RunAsync ()
+ {
+ return Task.Run (() => Run ());
+ }
+
+ ///
+ /// Run process in background, calling the on completion. This is meant to be used for processes which are to run under control of our
+ /// process but without us actively monitoring them or awaiting their completion. By default the process will run without a timeout (the
+ /// property is ignored). Timeout can be changed by setting the parameter to anything other than TimeSpan.MaxValue
+ ///
+ public void RunInBackground (BackgroundActionCompletionHandler completionHandler, TimeSpan? processTimeout = null)
+ {
+ backgroundProcess = new Task (
+ () => Run (processTimeout ?? TimeSpan.MaxValue),
+ TaskCreationOptions.LongRunning
+ ).ContinueWith (
+ (Task task) => {
+ ProcessStatus status;
+ if (task.IsFaulted) {
+ status = new ProcessStatus (task.Exception!);
+ } else {
+ status = new ProcessStatus ();
+ }
+ completionHandler (this, status);
+ return status;
+ }, TaskContinuationOptions.OnlyOnFaulted
+ ).ContinueWith (
+ (Task task) => {
+ completionHandler (this, task.Result);
+ return task.Result;
+ },
+ TaskContinuationOptions.OnlyOnRanToCompletion
+ ).ContinueWith