diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.ProjectTools/Resources/Base/BuildReleaseArm64SimpleDotNet.CoreCLR.R8.apkdesc b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.ProjectTools/Resources/Base/BuildReleaseArm64SimpleDotNet.CoreCLR.R8.apkdesc index c183f227e34..af3524b0b6c 100644 --- a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.ProjectTools/Resources/Base/BuildReleaseArm64SimpleDotNet.CoreCLR.R8.apkdesc +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.ProjectTools/Resources/Base/BuildReleaseArm64SimpleDotNet.CoreCLR.R8.apkdesc @@ -8,16 +8,16 @@ "Size": 402716 }, "lib/arm64-v8a/libassembly-store.so": { - "Size": 2522592 + "Size": 2529368 }, "lib/arm64-v8a/libclrjit.so": { - "Size": 2817680 + "Size": 2818032 }, "lib/arm64-v8a/libcoreclr.so": { - "Size": 4843904 + "Size": 4844000 }, "lib/arm64-v8a/libmonodroid.so": { - "Size": 537592 + "Size": 203592 }, "lib/arm64-v8a/libSystem.Globalization.Native.so": { "Size": 72432 @@ -59,5 +59,5 @@ "Size": 1904 } }, - "PackageSize": 6829499 + "PackageSize": 6751675 } \ No newline at end of file diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.ProjectTools/Resources/Base/BuildReleaseArm64SimpleDotNet.CoreCLR.apkdesc b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.ProjectTools/Resources/Base/BuildReleaseArm64SimpleDotNet.CoreCLR.apkdesc index 57f3525deca..96198aec85a 100644 --- a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.ProjectTools/Resources/Base/BuildReleaseArm64SimpleDotNet.CoreCLR.apkdesc +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.ProjectTools/Resources/Base/BuildReleaseArm64SimpleDotNet.CoreCLR.apkdesc @@ -8,16 +8,16 @@ "Size": 405040 }, "lib/arm64-v8a/libassembly-store.so": { - "Size": 2522592 + "Size": 2529336 }, "lib/arm64-v8a/libclrjit.so": { - "Size": 2817680 + "Size": 2818032 }, "lib/arm64-v8a/libcoreclr.so": { - "Size": 4843904 + "Size": 4844000 }, "lib/arm64-v8a/libmonodroid.so": { - "Size": 537592 + "Size": 203592 }, "lib/arm64-v8a/libSystem.Globalization.Native.so": { "Size": 72432 @@ -59,5 +59,5 @@ "Size": 1904 } }, - "PackageSize": 6829499 + "PackageSize": 6751675 } \ No newline at end of file diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.ProjectTools/Resources/Base/BuildReleaseArm64XFormsDotNet.CoreCLR.R8.apkdesc b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.ProjectTools/Resources/Base/BuildReleaseArm64XFormsDotNet.CoreCLR.R8.apkdesc index 0c51fae48bb..72bebe67617 100644 --- a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.ProjectTools/Resources/Base/BuildReleaseArm64XFormsDotNet.CoreCLR.R8.apkdesc +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.ProjectTools/Resources/Base/BuildReleaseArm64XFormsDotNet.CoreCLR.R8.apkdesc @@ -29,16 +29,16 @@ "Size": 2396 }, "lib/arm64-v8a/libassembly-store.so": { - "Size": 9438128 + "Size": 9453656 }, "lib/arm64-v8a/libclrjit.so": { - "Size": 2817680 + "Size": 2818032 }, "lib/arm64-v8a/libcoreclr.so": { - "Size": 4843904 + "Size": 4844000 }, "lib/arm64-v8a/libmonodroid.so": { - "Size": 537592 + "Size": 203592 }, "lib/arm64-v8a/libSystem.Globalization.Native.so": { "Size": 72432 @@ -2231,5 +2231,5 @@ "Size": 794696 } }, - "PackageSize": 16133651 + "PackageSize": 16064019 } \ No newline at end of file diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.ProjectTools/Resources/Base/BuildReleaseArm64XFormsDotNet.CoreCLR.apkdesc b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.ProjectTools/Resources/Base/BuildReleaseArm64XFormsDotNet.CoreCLR.apkdesc index 1fdabc8649f..46a4d4edb84 100644 --- a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.ProjectTools/Resources/Base/BuildReleaseArm64XFormsDotNet.CoreCLR.apkdesc +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.ProjectTools/Resources/Base/BuildReleaseArm64XFormsDotNet.CoreCLR.apkdesc @@ -32,16 +32,16 @@ "Size": 2396 }, "lib/arm64-v8a/libassembly-store.so": { - "Size": 9438312 + "Size": 9453632 }, "lib/arm64-v8a/libclrjit.so": { - "Size": 2817680 + "Size": 2818032 }, "lib/arm64-v8a/libcoreclr.so": { - "Size": 4843904 + "Size": 4844000 }, "lib/arm64-v8a/libmonodroid.so": { - "Size": 537592 + "Size": 203592 }, "lib/arm64-v8a/libSystem.Globalization.Native.so": { "Size": 72432 @@ -2234,5 +2234,5 @@ "Size": 794696 } }, - "PackageSize": 18284109 + "PackageSize": 18214477 } \ No newline at end of file diff --git a/src/native/clr/host/assembly-store.cc b/src/native/clr/host/assembly-store.cc index 37cc279fc9a..90280f8cf0a 100644 --- a/src/native/clr/host/assembly-store.cc +++ b/src/native/clr/host/assembly-store.cc @@ -1,10 +1,10 @@ #include #include +#include +#include #include #include -#include -#include -#include +#include #include #include @@ -67,9 +67,10 @@ namespace { struct WriteRequest final { - std::string path; - std::unique_ptr data; - size_t size; + WriteRequest *next; + uint8_t *payload; + size_t size; + uint32_t descriptor_index; }; enum class WriteResult @@ -79,9 +80,10 @@ namespace { }; pthread_mutex_t state_lock = PTHREAD_MUTEX_INITIALIZER; - std::deque write_queue; - std::string cache_dir; - std::unique_ptr tracking; + WriteRequest *write_queue_head = nullptr; + WriteRequest *write_queue_tail = nullptr; + char *cache_dir = nullptr; + uint8_t **tracking = nullptr; size_t queued_bytes = 0; uint64_t store_id = 0; bool initialized = false; @@ -89,6 +91,23 @@ namespace { bool writes_enabled = false; bool writer_running = false; + auto allocate_write_request (size_t payload_size) noexcept -> WriteRequest* + { + auto *request = static_cast(std::malloc (sizeof (WriteRequest))); + if (request == nullptr) [[unlikely]] { + return nullptr; + } + + request->payload = static_cast(std::malloc (payload_size)); + if (request->payload == nullptr) [[unlikely]] { + std::free (request); + return nullptr; + } + + request->next = nullptr; + return request; + } + auto hash_payload (const uint8_t *data, size_t size) noexcept -> uint64_t { return static_cast(crc32_hash (reinterpret_cast(data), size)); @@ -114,27 +133,98 @@ namespace { return true; } - void log_file_error (std::string_view operation, std::string const& path, int error) noexcept + void log_file_error (const char *operation, const char *path, int error) noexcept { - log_debugf (LOG_ASSEMBLY, "Decompressed-assembly cache %.*s failed for '%s': %s", static_cast(operation.length ()), operation.data (), path.c_str (), std::strerror (error)); + log_debugf (LOG_ASSEMBLY, "Decompressed-assembly cache %s failed for '%s': %s", operation, path, std::strerror (error)); } - auto write_cache_file (WriteRequest const& req) noexcept -> WriteResult + // Unlike Util::format_with_retry, cache path allocation must not abort the application on failure. + class CachePath final { - std::string tmp_path = req.path; - tmp_path.append (".tmp."sv); - tmp_path.append (std::to_string (getpid ())); + public: + template + CachePath (const char *operation, const char *source, TFormatter formatter) noexcept + { + int length = formatter (stack_buffer, sizeof (stack_buffer)); + if (length < 0) [[unlikely]] { + log_file_error (operation, source, errno); + return; + } + if (static_cast(length) < sizeof (stack_buffer)) { + path = stack_buffer; + return; + } + + size_t capacity = static_cast(length) + 1uz; + char *heap_buffer = static_cast(std::malloc (capacity)); + if (heap_buffer == nullptr) [[unlikely]] { + log_file_error (operation, source, ENOMEM); + return; + } + + length = formatter (heap_buffer, capacity); + if (length < 0 || static_cast(length) >= capacity) [[unlikely]] { + int error = length < 0 ? errno : ENAMETOOLONG; + std::free (heap_buffer); + log_file_error (operation, source, error); + return; + } + path = heap_buffer; + } + + // `cache_dir` is immutable once enabled, including on the writer thread. + explicit CachePath (uint32_t descriptor_index) noexcept + : CachePath ("path formatting", cache_dir, [descriptor_index](char *buffer, size_t size) noexcept { + return snprintf (buffer, size, "%s/%u.bin", cache_dir, descriptor_index); + }) + {} + + CachePath (CachePath const&) = delete; + CachePath (CachePath&&) = delete; + auto operator= (CachePath const&) -> CachePath& = delete; + auto operator= (CachePath&&) -> CachePath& = delete; + + ~CachePath () noexcept + { + if (path != stack_buffer) { + std::free (path); + } + } + + auto get () const noexcept -> const char* + { + return path; + } + + private: + char stack_buffer[Util::LocalPathBufferSize]; + char *path = nullptr; + }; + + auto write_cache_file (WriteRequest *req) noexcept -> WriteResult + { + CachePath path { req->descriptor_index }; + if (path.get () == nullptr) [[unlikely]] { + return WriteResult::Failed; + } + + CachePath tmp_path { "temporary-file path formatting", path.get (), [&path](char *buffer, size_t size) noexcept { + return snprintf (buffer, size, "%s.tmp.%d", path.get (), getpid ()); + }}; + if (tmp_path.get () == nullptr) [[unlikely]] { + return WriteResult::Failed; + } int fd; do { - fd = open (tmp_path.c_str (), O_WRONLY | O_CREAT | O_TRUNC | O_CLOEXEC | O_NOFOLLOW, 0600); + fd = open (tmp_path.get (), O_WRONLY | O_CREAT | O_TRUNC | O_CLOEXEC | O_NOFOLLOW, 0600); } while (fd < 0 && errno == EINTR); if (fd < 0) { - log_file_error ("temporary-file creation"sv, req.path, errno); + log_file_error ("temporary-file creation", path.get (), errno); return WriteResult::Failed; } - bool ok = write_fully (fd, req.data.get (), req.size); + bool ok = write_fully (fd, req->payload, req->size); int error = ok ? 0 : errno; if (close (fd) != 0 && ok) { ok = false; @@ -142,53 +232,65 @@ namespace { } if (!ok) { - log_file_error ("write"sv, req.path, error); - unlink (tmp_path.c_str ()); + log_file_error ("write", path.get (), error); + unlink (tmp_path.get ()); return WriteResult::Failed; } int rename_result; do { - rename_result = rename (tmp_path.c_str (), req.path.c_str ()); + rename_result = rename (tmp_path.get (), path.get ()); } while (rename_result != 0 && errno == EINTR); if (rename_result != 0) { error = errno; - log_file_error ("publish"sv, req.path, error); - unlink (tmp_path.c_str ()); + log_file_error ("publish", path.get (), error); + unlink (tmp_path.get ()); return WriteResult::Failed; } return WriteResult::Succeeded; } + // Discards every queued request without writing it. Must be called with `state_lock` held. void clear_write_queue_locked () noexcept { - for (WriteRequest const& request : write_queue) { - queued_bytes -= request.size; + WriteRequest *request = write_queue_head; + write_queue_head = nullptr; + write_queue_tail = nullptr; + + while (request != nullptr) { + WriteRequest *next = request->next; + queued_bytes -= request->size; + std::free (request->payload); + std::free (request); + request = next; } - write_queue.clear (); } [[gnu::cold]] auto writer_loop ([[maybe_unused]] void *arg) noexcept -> void* { while (true) { - WriteRequest request; + WriteRequest *request; { lock_guard lock (state_lock); - if (write_queue.empty ()) { + request = write_queue_head; + if (request == nullptr) { writer_running = false; return nullptr; } - request = std::move (write_queue.front ()); - write_queue.pop_front (); + write_queue_head = request->next; + if (write_queue_head == nullptr) { + write_queue_tail = nullptr; + } } - size_t request_size = request.size; + size_t request_size = request->size; WriteResult write_result = write_cache_file (request); - request.data.reset (); + std::free (request->payload); + std::free (request); { lock_guard lock (state_lock); @@ -229,25 +331,25 @@ namespace { return true; } - bool ensure_directory (std::string const& path) noexcept + bool ensure_directory (const char *path) noexcept { - if (mkdir (path.c_str (), 0700) == 0) { + if (mkdir (path, 0700) == 0) { return true; } int error = errno; if (error != EEXIST) { - log_file_error ("directory creation"sv, path, error); + log_file_error ("directory creation", path, error); return false; } struct stat st {}; - if (lstat (path.c_str (), &st) != 0) { - log_file_error ("directory validation"sv, path, errno); + if (lstat (path, &st) != 0) { + log_file_error ("directory validation", path, errno); return false; } if (!S_ISDIR (st.st_mode)) { - log_file_error ("directory validation"sv, path, ENOTDIR); + log_file_error ("directory validation", path, ENOTDIR); return false; } @@ -257,24 +359,24 @@ namespace { // Best-effort removal of staging files left behind by a previous process whose writer was // killed (e.g. by Android) between creating a `.tmp.` file and renaming it into place. // Such files are never reclaimed otherwise and would accumulate outside the queue bound. - void remove_stale_temp_files (std::string const& dir) noexcept + void remove_stale_temp_files (const char *dir) noexcept { - DIR *handle = opendir (dir.c_str ()); + DIR *handle = opendir (dir); if (handle == nullptr) { return; } - std::string prefix = dir; - prefix.append ("/"); for (dirent *entry = readdir (handle); entry != nullptr; entry = readdir (handle)) { - std::string_view name { entry->d_name }; - if (name.find (".tmp."sv) == std::string_view::npos) { + if (strstr (entry->d_name, ".tmp.") == nullptr) { continue; } - std::string path = prefix; - path.append (name); - unlink (path.c_str ()); + CachePath path { "stale temporary-file path formatting", dir, [dir, entry](char *buffer, size_t size) noexcept { + return snprintf (buffer, size, "%s/%s", dir, entry->d_name); + }}; + if (path.get () != nullptr) { + unlink (path.get ()); + } } closedir (handle); @@ -316,33 +418,51 @@ namespace { return; } - cache_dir.assign (code_cache_dir); - cache_dir.append ("/"); - cache_dir.append (CACHE_DIR_NAME); - if (!ensure_directory (cache_dir)) { + // The cache lives at `//`, with both levels created in turn. + CachePath root { "cache-directory path formatting", code_cache_dir, [code_cache_dir](char *buffer, size_t size) noexcept { + return snprintf (buffer, size, "%s/%.*s", code_cache_dir, static_cast(CACHE_DIR_NAME.length ()), CACHE_DIR_NAME.data ()); + }}; + if (root.get () == nullptr) [[unlikely]] { + return; + } + + if (!ensure_directory (root.get ())) { return; } store_id = assembly_store_id; - cache_dir.append ("/"); - char store_id_text[(sizeof (store_id) * 2) + 1]; - snprintf (store_id_text, sizeof (store_id_text), "%" PRIx64, store_id); - cache_dir.append (store_id_text); - if (!ensure_directory (cache_dir)) { + CachePath path { "store-directory path formatting", root.get (), [&root](char *buffer, size_t size) noexcept { + return snprintf (buffer, size, "%s/%" PRIx64, root.get (), store_id); + }}; + if (path.get () == nullptr) [[unlikely]] { + return; + } + + if (!ensure_directory (path.get ())) { return; } - remove_stale_temp_files (cache_dir); + remove_stale_temp_files (path.get ()); - if (compressed_assembly_count > 0) { - tracking.reset (new (std::nothrow) uint8_t*[compressed_assembly_count]()); + if (compressed_assembly_count == 0) { + return; } - enabled = (tracking != nullptr); - if (!enabled) { + // Neither allocation is ever freed: both live for as long as the process does. + cache_dir = strdup (path.get ()); + if (cache_dir == nullptr) [[unlikely]] { + return; + } + + tracking = static_cast(std::calloc (compressed_assembly_count, sizeof (uint8_t*))); + if (tracking == nullptr) [[unlikely]] { + std::free (cache_dir); + cache_dir = nullptr; return; } + enabled = true; + { lock_guard lock (state_lock); writes_enabled = true; @@ -351,29 +471,24 @@ namespace { log_debugf ( LOG_ASSEMBLY, "Enabled decompressed-assembly cache at '%s'; store ID 0x%" PRIx64 "; write queue limit %zu bytes", - cache_dir.c_str (), + cache_dir, store_id, MAX_QUEUED_BYTES ); } - auto build_path (uint32_t descriptor_index) noexcept -> std::string - { - std::string path = cache_dir; - path.append ("/"); - path.append (std::to_string (descriptor_index)); - path.append (".bin"sv); - return path; - } - auto try_load (uint32_t descriptor_index, std::string_view name, uint32_t expected_size) noexcept -> uint8_t* { if (!enabled) { return nullptr; } - std::string path = build_path (descriptor_index); - int fd = open (path.c_str (), O_RDONLY | O_CLOEXEC | O_NOFOLLOW); + CachePath path { descriptor_index }; + if (path.get () == nullptr) [[unlikely]] { + return nullptr; + } + + int fd = open (path.get (), O_RDONLY | O_CLOEXEC | O_NOFOLLOW); if (fd < 0) { return nullptr; } @@ -460,40 +575,52 @@ namespace { return; } - auto snapshot = std::unique_ptr (new (std::nothrow) uint8_t[total]); - if (snapshot == nullptr) { + WriteRequest *req = allocate_write_request (total); + if (req == nullptr) [[unlikely]] { + log_debugf ( + LOG_ASSEMBLY, + "Skipping decompressed-assembly cache write for '%.*s': unable to allocate the request or payload", + static_cast(name.length ()), + name.data () + ); lock_guard lock (state_lock); queued_bytes -= total; return; } + + req->size = total; + req->descriptor_index = descriptor_index; + // The runtime can modify the shared decompression buffer after this // method returns, so the background writer needs an immutable copy. - memcpy (snapshot.get (), data, size); + memcpy (req->payload, data, size); CacheFileFooter footer { .magic = CACHE_FILE_MAGIC, .version = CACHE_FILE_FORMAT_VERSION, .store_id = store_id, - .payload_hash = hash_payload (snapshot.get (), size), + .payload_hash = hash_payload (req->payload, size), .descriptor_index = descriptor_index, .payload_size = static_cast(size), }; - memcpy (snapshot.get () + size, &footer, sizeof (footer)); - - WriteRequest req { - .path = build_path (descriptor_index), - .data = std::move (snapshot), - .size = total, - }; + memcpy (req->payload + size, &footer, sizeof (footer)); { lock_guard lock (state_lock); if (!writes_enabled) { queued_bytes -= total; + std::free (req->payload); + std::free (req); return; } - write_queue.push_back (std::move (req)); + if (write_queue_tail == nullptr) { + write_queue_head = req; + } else { + write_queue_tail->next = req; + } + write_queue_tail = req; + if (!writer_running) { writer_running = true; if (!start_writer_locked ()) { @@ -817,12 +944,16 @@ void AssemblyStore::configure_from_payload (const void *payload_start, const cha // Build a lookup of assembly names indexed by descriptor index, used to disambiguate CRC32 hash // collisions during lookup. The names section follows the descriptor table and consists of // `entry_count` length-prefixed (uint32 length followed by the UTF-8 bytes) records, stored in - // descriptor-index order. `delete[]` guards against a leak should the (single) store ever be + // descriptor-index order. The `free` guards against a leak should the (single) store ever be // re-mapped; `assembly_store_names` is nullptr on first call, for which it is a no-op. const uint8_t *names_cursor = assembly_store.data_start + header_size + header->index_size + (static_cast(header->entry_count) * sizeof (AssemblyStoreEntryDescriptor)); - delete[] assembly_store_names; - assembly_store_names = new std::string_view[header->entry_count]; + std::free (assembly_store_names); + assembly_store_names = static_cast(std::calloc (header->entry_count, sizeof (std::string_view))); + if (assembly_store_names == nullptr) [[unlikely]] { + Helpers::abort_application (LOG_ASSEMBLY, "Unable to allocate memory for the assembly store name table"); + } + for (uint32_t i = 0; i < header->entry_count; i++) { uint32_t name_length; memcpy (&name_length, names_cursor, sizeof (name_length)); diff --git a/tests/MSBuildDeviceIntegration/Tests/InstallAndRunTests.cs b/tests/MSBuildDeviceIntegration/Tests/InstallAndRunTests.cs index ccfa33128df..a69c7be831e 100644 --- a/tests/MSBuildDeviceIntegration/Tests/InstallAndRunTests.cs +++ b/tests/MSBuildDeviceIntegration/Tests/InstallAndRunTests.cs @@ -734,13 +734,13 @@ public void DeployToDevice ([Values] bool isRelease, [Values (AndroidRuntime.Cor } [Test] - public void AssemblyStoreDecompressionCacheMapsPersistedAssemblies () + public void AssemblyStoreDecompressionCacheMapsPersistedAssemblies ([Values] bool longCachePath) { if (IgnoreUnsupportedConfiguration (AndroidRuntime.CoreCLR, release: true)) { return; } - var app = new XamarinAndroidApplicationProject (packageName: PackageUtils.MakePackageName (AndroidRuntime.CoreCLR, "assemblycache")) { + var app = new XamarinAndroidApplicationProject (packageName: PackageUtils.MakePackageName (AndroidRuntime.CoreCLR, longCachePath ? "assemblycachelong" : "assemblycache")) { IsRelease = true, }; app.SetRuntime (AndroidRuntime.CoreCLR); @@ -748,6 +748,35 @@ public void AssemblyStoreDecompressionCacheMapsPersistedAssemblies () app.SetProperty ("AndroidEnableAssemblyStoreDecompressionCache", "true"); app.AndroidManifest = app.AndroidManifest.Replace (" $$""" + package com.test; + + public class CachePathApplication extends android.app.Application { + @Override + public java.io.File getCodeCacheDir () { + java.io.File directory = new java.io.File (super.getCodeCacheDir (), "{{subdirectory}}"); + if (!directory.isDirectory () && !directory.mkdirs ()) { + throw new IllegalStateException ("Unable to create the long code cache directory"); + } + return directory; + } + } + """, + }); + } + string cacheRoot = cacheDirectory + "/decompressed-assembly-cache-v1"; + using var appBuilder = CreateApkBuilder (); Assert.IsTrue (appBuilder.Install (app), "Install should have succeeded."); @@ -767,15 +796,22 @@ public void AssemblyStoreDecompressionCacheMapsPersistedAssemblies () for (int attempt = 0; attempt < 40 && cacheFiles.Length < 2; attempt++) { Thread.Sleep (250); cacheFiles = RunAdbCommand ( - $"shell run-as {app.PackageName} find code_cache/decompressed-assembly-cache-v1 -type f -name '*.bin'" + $"shell run-as {app.PackageName} find {cacheRoot} -type f -name '*.bin'" ) .Split (new [] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries) .Where (line => line.EndsWith (".bin", StringComparison.Ordinal)) .ToArray (); } Assert.That (cacheFiles.Length, Is.GreaterThanOrEqualTo (2), "The first launch should persist multiple decompressed assemblies."); + if (longCachePath) { + Assert.IsTrue (cacheFiles.All (path => path.Length > 1024), "Cache paths should exceed the native stack buffer size."); + } RunAdbCommand ($"shell am force-stop --user all {app.PackageName}"); + string staleTempFile = cacheFiles.Last () + ".tmp.stale"; + RunAdbCommand ($"shell run-as {app.PackageName} touch {staleTempFile}"); + StringAssert.Contains (staleTempFile, RunAdbCommand ($"shell run-as {app.PackageName} ls {staleTempFile}"), + "The stale temporary file should exist before restarting."); string cacheFileToCorrupt = cacheFiles.First (); string ValidFileHash () => RunAdbCommand ( $"shell run-as {app.PackageName} md5sum {cacheFileToCorrupt}" @@ -810,6 +846,11 @@ string ValidFileHash () => RunAdbCommand ( rewritten = ValidFileHash () == validHash; } Assert.IsTrue (rewritten, $"The corrupted cache file '{cacheFileToCorrupt}' should be rewritten with valid contents after fallback."); + Assert.That ( + RunAdbCommand ($"shell run-as {app.PackageName} find {cacheRoot} -type f -name '*.tmp.stale'").Trim (), + Is.Empty, + "The second launch should remove stale temporary files." + ); string [] pids = RunAdbCommand ($"shell pidof {app.PackageName}") .Split (new [] { ' ', '\r', '\n', '\t' }, StringSplitOptions.RemoveEmptyEntries); @@ -819,7 +860,7 @@ string ValidFileHash () => RunAdbCommand ( maps.Append (RunAdbCommand ($"shell run-as {app.PackageName} cat /proc/{pid}/maps")); } StringAssert.Contains ( - "/code_cache/decompressed-assembly-cache-v1/", + "/" + cacheRoot + "/", maps.ToString (), "The second launch should map persisted decompressed assemblies." );