diff --git a/leakcanary/leakcanary-android-core/src/main/java/leakcanary/RemoteWorkManagerHeapAnalyzer.kt b/leakcanary/leakcanary-android-core/src/main/java/leakcanary/RemoteWorkManagerHeapAnalyzer.kt index 50f534fa4..787d27909 100644 --- a/leakcanary/leakcanary-android-core/src/main/java/leakcanary/RemoteWorkManagerHeapAnalyzer.kt +++ b/leakcanary/leakcanary-android-core/src/main/java/leakcanary/RemoteWorkManagerHeapAnalyzer.kt @@ -1,20 +1,27 @@ package leakcanary +import androidx.lifecycle.Observer import androidx.work.Data import androidx.work.OneTimeWorkRequest +import androidx.work.WorkInfo import androidx.work.WorkManager import androidx.work.multiprocess.RemoteListenableWorker.ARGUMENT_CLASS_NAME import androidx.work.multiprocess.RemoteListenableWorker.ARGUMENT_PACKAGE_NAME +import java.io.File import leakcanary.EventListener.Event +import leakcanary.EventListener.Event.HeapAnalysisDone import leakcanary.EventListener.Event.HeapDump import leakcanary.internal.HeapAnalyzerWorker.Companion.asWorkerInputData import leakcanary.internal.InternalLeakCanary import leakcanary.internal.RemoteHeapAnalyzerWorker +import leakcanary.internal.Serializables +import leakcanary.internal.friendly.mainHandler import shark.SharkLog /** * When receiving a [HeapDump] event, starts a WorkManager worker that performs heap analysis in - * a dedicated :leakcanary process + * a dedicated :leakcanary process. When the analysis completes, the result is sent back to the + * main process and dispatched to all configured event listeners. */ object RemoteWorkManagerHeapAnalyzer : EventListener { @@ -29,6 +36,35 @@ object RemoteWorkManagerHeapAnalyzer : EventListener { } } + /** + * Cleans up orphaned event files from previous remote worker runs. + * If the main process died after a remote worker completed, the result file + * would never be read or deleted. This dispatches any recoverable events + * and deletes all leftover files. + * + * Called from [InternalLeakCanary.invoke] on startup. + */ + internal fun dispatchAndCleanupOrphanedEventFiles() { + val application = InternalLeakCanary.application + val filesDir = application.filesDir + val orphanedFiles = filesDir.listFiles { file -> + file.name.startsWith(RemoteHeapAnalyzerWorker.EVENT_FILE_PREFIX) + } ?: return + for (eventFile in orphanedFiles) { + try { + val doneEvent = Serializables.fromByteArray>(eventFile.readBytes()) + if (doneEvent != null) { + SharkLog.d { "Recovering orphaned remote event from ${eventFile.name}" } + InternalLeakCanary.sendEvent(doneEvent) + } + } catch (e: Throwable) { + SharkLog.d(e) { "Failed to recover orphaned event file ${eventFile.name}" } + } finally { + eventFile.delete() + } + } + } + override fun onEvent(event: Event) { if (event is HeapDump) { val application = InternalLeakCanary.application @@ -45,6 +81,47 @@ object RemoteWorkManagerHeapAnalyzer : EventListener { SharkLog.d { "Enqueuing heap analysis for ${event.file} on WorkManager remote worker" } val workManager = WorkManager.getInstance(application) workManager.enqueue(heapAnalysisRequest) + + // Observe the remote worker's completion from the main process so we can + // re-dispatch the HeapAnalysisDone event to listeners running here. + val workInfoLiveData = workManager.getWorkInfoByIdLiveData(heapAnalysisRequest.id) + mainHandler.post { + workInfoLiveData.observeForever(object : Observer { + override fun onChanged(workInfo: WorkInfo) { + if (workInfo.state.isFinished) { + workInfoLiveData.removeObserver(this) + if (workInfo.state == WorkInfo.State.SUCCEEDED) { + dispatchEventFromRemoteWorker(workInfo.outputData) + } + } + } + }) + } + } + } + + private fun dispatchEventFromRemoteWorker(outputData: Data) { + val eventFilePath = outputData.getString(RemoteHeapAnalyzerWorker.EVENT_FILE_KEY) + if (eventFilePath == null) { + SharkLog.d { "Remote worker completed but no event file path in output data" } + return + } + val eventFile = File(eventFilePath) + if (!eventFile.exists()) { + SharkLog.d { "Remote worker event file does not exist: $eventFilePath" } + return + } + try { + val doneEvent = Serializables.fromByteArray>(eventFile.readBytes()) + if (doneEvent != null) { + InternalLeakCanary.sendEvent(doneEvent) + } else { + SharkLog.d { "Failed to deserialize remote worker event" } + } + } catch (e: Throwable) { + SharkLog.d(e) { "Error reading remote worker event file" } + } finally { + eventFile.delete() } } } diff --git a/leakcanary/leakcanary-android-core/src/main/java/leakcanary/internal/InternalLeakCanary.kt b/leakcanary/leakcanary-android-core/src/main/java/leakcanary/internal/InternalLeakCanary.kt index 8bfc2ca89..b1c704e60 100644 --- a/leakcanary/leakcanary-android-core/src/main/java/leakcanary/internal/InternalLeakCanary.kt +++ b/leakcanary/leakcanary-android-core/src/main/java/leakcanary/internal/InternalLeakCanary.kt @@ -22,6 +22,7 @@ import leakcanary.GcTrigger import leakcanary.LeakCanary import leakcanary.LeakCanaryAndroidInternalUtils import leakcanary.OnObjectRetainedListener +import leakcanary.RemoteWorkManagerHeapAnalyzer import leakcanary.inProcess import leakcanary.internal.HeapDumpControl.ICanHazHeap.Nope import leakcanary.internal.HeapDumpControl.ICanHazHeap.Yup @@ -142,6 +143,14 @@ internal object InternalLeakCanary : (Application) -> Unit, OnObjectRetainedList registerResumedActivityListener(application) LeakCanaryAndroidInternalUtils.addLeakActivityDynamicShortcut(application) + // Recover any heap analysis results from remote workers that completed + // while the main process was dead, and clean up orphaned event files. + if (RemoteWorkManagerHeapAnalyzer.remoteLeakCanaryServiceInClasspath) { + backgroundHandler.post { + RemoteWorkManagerHeapAnalyzer.dispatchAndCleanupOrphanedEventFiles() + } + } + // We post so that the log happens after Application.onCreate() where // the config could be updated. mainHandler.post { diff --git a/leakcanary/leakcanary-android-core/src/main/java/leakcanary/internal/RemoteHeapAnalyzerWorker.kt b/leakcanary/leakcanary-android-core/src/main/java/leakcanary/internal/RemoteHeapAnalyzerWorker.kt index d94593897..6dddaa8a6 100644 --- a/leakcanary/leakcanary-android-core/src/main/java/leakcanary/internal/RemoteHeapAnalyzerWorker.kt +++ b/leakcanary/leakcanary-android-core/src/main/java/leakcanary/internal/RemoteHeapAnalyzerWorker.kt @@ -1,11 +1,13 @@ package leakcanary.internal import android.content.Context +import androidx.work.Data import androidx.work.ForegroundInfo import androidx.work.WorkerParameters import androidx.work.impl.utils.futures.SettableFuture import androidx.work.multiprocess.RemoteListenableWorker import com.google.common.util.concurrent.ListenableFuture +import java.io.File import leakcanary.BackgroundThreadHeapAnalyzer.heapAnalyzerThreadHandler import leakcanary.EventListener.Event.HeapDump import leakcanary.internal.HeapAnalyzerWorker.Companion.asEvent @@ -32,8 +34,22 @@ internal class RemoteHeapAnalyzerWorker( if (result.isCancelled) { SharkLog.d { "Remote heap analysis for ${heapDump.file} was canceled" } } else { + // Dispatch to any listeners configured in the background process. InternalLeakCanary.sendEvent(doneEvent) - result.set(Result.success()) + // Serialize the done event to a file so the main process can re-dispatch it + // to its own listeners. WorkManager output data has a 10 KB limit, so we write + // to a shared file instead and pass the path back. + val outputData = try { + val eventFile = File(applicationContext.filesDir, EVENT_FILE_PREFIX + heapDump.uniqueId) + eventFile.writeBytes(doneEvent.toByteArray()) + Data.Builder() + .putString(EVENT_FILE_KEY, eventFile.absolutePath) + .build() + } catch (e: Throwable) { + SharkLog.d(e) { "Failed to serialize done event for main process" } + Data.EMPTY + } + result.set(Result.success(outputData)) } } return result @@ -44,4 +60,9 @@ internal class RemoteHeapAnalyzerWorker( applicationContext.heapAnalysisForegroundInfo() } } + + companion object { + const val EVENT_FILE_PREFIX = "leakcanary_remote_event_" + const val EVENT_FILE_KEY = "leakcanary.remote.event_file" + } }