Commit a7cc5d0d authored by Clark DuVall's avatar Clark DuVall Committed by Commit Bot

Fix android crash reporting for child processes that exit cleanly

Some services that run in a utility process such as the Data Decoder
Service will shut themselves down cleanly if they do not have requests
for a period of time. These were being treated as OOM kills previously.

This uses the approach Bo outlined in http://crbug.com/872343.

Bug: 872343
Change-Id: Iaf371520893076981d44429a7ec1c444b637c93d
Reviewed-on: https://chromium-review.googlesource.com/c/1298024Reviewed-by: default avatarRobert Sesek <rsesek@chromium.org>
Reviewed-by: default avatarAlex Moshchuk <alexmos@chromium.org>
Reviewed-by: default avatarMark Mentovai <mark@chromium.org>
Reviewed-by: default avatarYaron Friedman <yfriedman@chromium.org>
Reviewed-by: default avatarBo <boliu@chromium.org>
Commit-Queue: Clark DuVall <cduvall@chromium.org>
Cr-Commit-Position: refs/heads/master@{#605768}
parent 4a7d0521
...@@ -2239,6 +2239,7 @@ test("base_unittests") { ...@@ -2239,6 +2239,7 @@ test("base_unittests") {
"allocator/tcmalloc_unittest.cc", "allocator/tcmalloc_unittest.cc",
"android/android_image_reader_compat_unittest.cc", "android/android_image_reader_compat_unittest.cc",
"android/application_status_listener_unittest.cc", "android/application_status_listener_unittest.cc",
"android/child_process_unittest.cc",
"android/content_uri_utils_unittest.cc", "android/content_uri_utils_unittest.cc",
"android/jni_android_unittest.cc", "android/jni_android_unittest.cc",
"android/jni_array_unittest.cc", "android/jni_array_unittest.cc",
...@@ -3022,8 +3023,8 @@ if (is_android) { ...@@ -3022,8 +3023,8 @@ if (is_android) {
android_aidl("base_java_aidl") { android_aidl("base_java_aidl") {
import_include = [ "android/java/src" ] import_include = [ "android/java/src" ]
sources = [ sources = [
"android/java/src/org/chromium/base/process_launcher/ICallbackInt.aidl",
"android/java/src/org/chromium/base/process_launcher/IChildProcessService.aidl", "android/java/src/org/chromium/base/process_launcher/IChildProcessService.aidl",
"android/java/src/org/chromium/base/process_launcher/IParentProcess.aidl",
] ]
} }
......
// Copyright 2018 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "base/android/jni_android.h"
#include "base/run_loop.h"
#include "base/test/multiprocess_test.h"
#include "base/test/test_timeouts.h"
#include "testing/multiprocess_func_list.h"
namespace base {
namespace android {
MULTIPROCESS_TEST_MAIN(BasicMain) {
return 0;
}
MULTIPROCESS_TEST_MAIN(WaitingMain) {
base::RunLoop().Run();
return 0;
}
class ChildProcessTest : public MultiProcessTest {};
TEST_F(ChildProcessTest, ChildHasCleanExit) {
Process process = SpawnChild("BasicMain");
int exit_code = 0;
EXPECT_TRUE(WaitForMultiprocessTestChildExit(
process, TestTimeouts::action_timeout(), &exit_code));
EXPECT_EQ(exit_code, 0);
EXPECT_TRUE(MultiProcessTestChildHasCleanExit(process));
}
TEST_F(ChildProcessTest, ChildTerminated) {
Process process = SpawnChild("WaitingMain");
EXPECT_TRUE(TerminateMultiProcessTestChild(process, 0, true));
EXPECT_FALSE(MultiProcessTestChildHasCleanExit(process));
}
} // namespace android
} // namespace base
...@@ -255,6 +255,10 @@ public class ChildProcessConnection { ...@@ -255,6 +255,10 @@ public class ChildProcessConnection {
private MemoryPressureCallback mMemoryPressureCallback; private MemoryPressureCallback mMemoryPressureCallback;
// Whether the process exited cleanly or not.
@GuardedBy("sBindingStateLock")
private boolean mCleanExit;
public ChildProcessConnection(Context context, ComponentName serviceName, boolean bindToCaller, public ChildProcessConnection(Context context, ComponentName serviceName, boolean bindToCaller,
boolean bindAsExternalService, Bundle serviceBundle) { boolean bindAsExternalService, Bundle serviceBundle) {
this(context, serviceName, bindToCaller, bindAsExternalService, serviceBundle, this(context, serviceName, bindToCaller, bindAsExternalService, serviceBundle,
...@@ -502,6 +506,10 @@ public class ChildProcessConnection { ...@@ -502,6 +506,10 @@ public class ChildProcessConnection {
} }
private void onSetupConnectionResult(int pid) { private void onSetupConnectionResult(int pid) {
if (mPid != 0) {
Log.e(TAG, "sendPid was called more than once: pid=%d", mPid);
return;
}
mPid = pid; mPid = pid;
assert mPid != 0 : "Child service claims to be run by a process of pid=0."; assert mPid != 0 : "Child service claims to be run by a process of pid=0.";
...@@ -522,9 +530,9 @@ public class ChildProcessConnection { ...@@ -522,9 +530,9 @@ public class ChildProcessConnection {
assert mServiceConnectComplete && mService != null; assert mServiceConnectComplete && mService != null;
assert mConnectionParams != null; assert mConnectionParams != null;
ICallbackInt pidCallback = new ICallbackInt.Stub() { IParentProcess parentProcess = new IParentProcess.Stub() {
@Override @Override
public void call(final int pid) { public void sendPid(final int pid) {
mLauncherHandler.post(new Runnable() { mLauncherHandler.post(new Runnable() {
@Override @Override
public void run() { public void run() {
...@@ -532,9 +540,22 @@ public class ChildProcessConnection { ...@@ -532,9 +540,22 @@ public class ChildProcessConnection {
} }
}); });
} }
@Override
public void reportCleanExit() {
synchronized (sBindingStateLock) {
mCleanExit = true;
}
mLauncherHandler.post(new Runnable() {
@Override
public void run() {
unbind();
}
});
}
}; };
try { try {
mService.setupConnection(mConnectionParams.mConnectionBundle, pidCallback, mService.setupConnection(mConnectionParams.mConnectionBundle, parentProcess,
mConnectionParams.mClientInterfaces); mConnectionParams.mClientInterfaces);
} catch (RemoteException re) { } catch (RemoteException re) {
Log.e(TAG, "Failed to setup connection.", re); Log.e(TAG, "Failed to setup connection.", re);
...@@ -675,6 +696,15 @@ public class ChildProcessConnection { ...@@ -675,6 +696,15 @@ public class ChildProcessConnection {
} }
} }
/**
* @return true if the process exited cleanly.
*/
public boolean hasCleanExit() {
synchronized (sBindingStateLock) {
return mCleanExit;
}
}
/** /**
* Returns the binding state of remaining processes, excluding the current connection. * Returns the binding state of remaining processes, excluding the current connection.
* *
......
...@@ -89,6 +89,9 @@ public abstract class ChildProcessService extends Service { ...@@ -89,6 +89,9 @@ public abstract class ChildProcessService extends Service {
private final Semaphore mActivitySemaphore = new Semaphore(1); private final Semaphore mActivitySemaphore = new Semaphore(1);
// Interface to send notifications to the parent process.
private IParentProcess mParentProcess;
// These values are persisted to logs. Entries should not be renumbered and numeric values // These values are persisted to logs. Entries should not be renumbered and numeric values
// should never be reused. // should never be reused.
@IntDef({SplitApkWorkaroundResult.NOT_RUN, SplitApkWorkaroundResult.NO_ENTRIES, @IntDef({SplitApkWorkaroundResult.NOT_RUN, SplitApkWorkaroundResult.NO_ENTRIES,
...@@ -137,18 +140,19 @@ public abstract class ChildProcessService extends Service { ...@@ -137,18 +140,19 @@ public abstract class ChildProcessService extends Service {
} }
@Override @Override
public void setupConnection(Bundle args, ICallbackInt pidCallback, List<IBinder> callbacks) public void setupConnection(Bundle args, IParentProcess parentProcess,
throws RemoteException { List<IBinder> callbacks) throws RemoteException {
assert mServiceBound; assert mServiceBound;
synchronized (mBinderLock) { synchronized (mBinderLock) {
if (mBindToCallerCheck && mBoundCallingPid == 0) { if (mBindToCallerCheck && mBoundCallingPid == 0) {
Log.e(TAG, "Service has not been bound with bindToCaller()"); Log.e(TAG, "Service has not been bound with bindToCaller()");
pidCallback.call(-1); parentProcess.sendPid(-1);
return; return;
} }
} }
pidCallback.call(Process.myPid()); parentProcess.sendPid(Process.myPid());
mParentProcess = parentProcess;
processConnectionBundle(args, callbacks); processConnectionBundle(args, callbacks);
} }
...@@ -272,6 +276,11 @@ public abstract class ChildProcessService extends Service { ...@@ -272,6 +276,11 @@ public abstract class ChildProcessService extends Service {
} }
if (mActivitySemaphore.tryAcquire()) { if (mActivitySemaphore.tryAcquire()) {
mDelegate.runMain(); mDelegate.runMain();
try {
mParentProcess.reportCleanExit();
} catch (RemoteException e) {
Log.e(TAG, "Failed to call clean exit callback.", e);
}
nativeExitChildProcess(); nativeExitChildProcess();
} }
} catch (InterruptedException e) { } catch (InterruptedException e) {
......
// Copyright 2017 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package org.chromium.base.process_launcher;
oneway interface ICallbackInt {
void call(int value);
}
...@@ -6,7 +6,7 @@ package org.chromium.base.process_launcher; ...@@ -6,7 +6,7 @@ package org.chromium.base.process_launcher;
import android.os.Bundle; import android.os.Bundle;
import org.chromium.base.process_launcher.ICallbackInt; import org.chromium.base.process_launcher.IParentProcess;
interface IChildProcessService { interface IChildProcessService {
// On the first call to this method, the service will record the calling PID // On the first call to this method, the service will record the calling PID
...@@ -15,8 +15,8 @@ interface IChildProcessService { ...@@ -15,8 +15,8 @@ interface IChildProcessService {
boolean bindToCaller(); boolean bindToCaller();
// Sets up the initial IPC channel. // Sets up the initial IPC channel.
oneway void setupConnection(in Bundle args, ICallbackInt pidCallback, oneway void setupConnection(in Bundle args, IParentProcess parentProcess,
in List<IBinder> clientInterfaces); in List<IBinder> clientInterfaces);
// Forcefully kills the child process. // Forcefully kills the child process.
oneway void forceKill(); oneway void forceKill();
......
// Copyright 2018 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package org.chromium.base.process_launcher;
interface IParentProcess {
// Sends the child pid to the parent process. This will be called before any
// third-party code is loaded, and will be a no-op after the first call.
oneway void sendPid(int pid);
// Tells the parent proces the child exited cleanly. Not oneway to ensure
// the browser receives the message before child exits.
void reportCleanExit();
}
...@@ -116,7 +116,7 @@ public class ChildProcessConnectionTest { ...@@ -116,7 +116,7 @@ public class ChildProcessConnectionTest {
// Parameters captured from the IChildProcessService.setupConnection() call // Parameters captured from the IChildProcessService.setupConnection() call
private Bundle mConnectionBundle; private Bundle mConnectionBundle;
private ICallbackInt mConnectionPidCallback; private IParentProcess mConnectionParentProcess;
private IBinder mConnectionIBinderCallback; private IBinder mConnectionIBinderCallback;
@Before @Before
...@@ -129,7 +129,7 @@ public class ChildProcessConnectionTest { ...@@ -129,7 +129,7 @@ public class ChildProcessConnectionTest {
@Override @Override
public Void answer(InvocationOnMock invocation) { public Void answer(InvocationOnMock invocation) {
mConnectionBundle = (Bundle) invocation.getArgument(0); mConnectionBundle = (Bundle) invocation.getArgument(0);
mConnectionPidCallback = (ICallbackInt) invocation.getArgument(1); mConnectionParentProcess = (IParentProcess) invocation.getArgument(1);
mConnectionIBinderCallback = (IBinder) invocation.getArgument(2); mConnectionIBinderCallback = (IBinder) invocation.getArgument(2);
return null; return null;
} }
...@@ -305,11 +305,29 @@ public class ChildProcessConnectionTest { ...@@ -305,11 +305,29 @@ public class ChildProcessConnectionTest {
verify(mConnectionCallback, never()).onConnected(any()); verify(mConnectionCallback, never()).onConnected(any());
mFirstServiceConnection.notifyServiceConnected(mChildProcessServiceBinder); mFirstServiceConnection.notifyServiceConnected(mChildProcessServiceBinder);
ShadowLooper.runUiThreadTasks(); ShadowLooper.runUiThreadTasks();
assertNotNull(mConnectionPidCallback); assertNotNull(mConnectionParentProcess);
mConnectionPidCallback.call(34 /* pid */); mConnectionParentProcess.sendPid(34);
verify(mConnectionCallback, times(1)).onConnected(connection); verify(mConnectionCallback, times(1)).onConnected(connection);
} }
@Test
public void testSendPidOnlyWorksOnce() throws RemoteException {
ChildProcessConnection connection = createDefaultTestConnection();
assertNotNull(mFirstServiceConnection);
connection.start(false /* useStrongBinding */, null /* serviceCallback */);
connection.setupConnection(
null /* connectionBundle */, null /* callback */, mConnectionCallback);
verify(mConnectionCallback, never()).onConnected(any());
mFirstServiceConnection.notifyServiceConnected(mChildProcessServiceBinder);
ShadowLooper.runUiThreadTasks();
assertNotNull(mConnectionParentProcess);
mConnectionParentProcess.sendPid(34);
assertEquals(34, connection.getPid());
mConnectionParentProcess.sendPid(543);
assertEquals(34, connection.getPid());
}
@Test @Test
public void testSetupConnectionAfterServiceConnected() throws RemoteException { public void testSetupConnectionAfterServiceConnected() throws RemoteException {
ChildProcessConnection connection = createDefaultTestConnection(); ChildProcessConnection connection = createDefaultTestConnection();
...@@ -320,8 +338,8 @@ public class ChildProcessConnectionTest { ...@@ -320,8 +338,8 @@ public class ChildProcessConnectionTest {
null /* connectionBundle */, null /* callback */, mConnectionCallback); null /* connectionBundle */, null /* callback */, mConnectionCallback);
verify(mConnectionCallback, never()).onConnected(any()); verify(mConnectionCallback, never()).onConnected(any());
ShadowLooper.runUiThreadTasks(); ShadowLooper.runUiThreadTasks();
assertNotNull(mConnectionPidCallback); assertNotNull(mConnectionParentProcess);
mConnectionPidCallback.call(34 /* pid */); mConnectionParentProcess.sendPid(34);
verify(mConnectionCallback, times(1)).onConnected(connection); verify(mConnectionCallback, times(1)).onConnected(connection);
} }
...@@ -335,8 +353,8 @@ public class ChildProcessConnectionTest { ...@@ -335,8 +353,8 @@ public class ChildProcessConnectionTest {
null /* connectionBundle */, null /* callback */, mConnectionCallback); null /* connectionBundle */, null /* callback */, mConnectionCallback);
verify(mConnectionCallback, never()).onConnected(any()); verify(mConnectionCallback, never()).onConnected(any());
ShadowLooper.runUiThreadTasks(); ShadowLooper.runUiThreadTasks();
assertNotNull(mConnectionPidCallback); assertNotNull(mConnectionParentProcess);
mConnectionPidCallback.call(34 /* pid */); mConnectionParentProcess.sendPid(34);
verify(mConnectionCallback, times(1)).onConnected(connection); verify(mConnectionCallback, times(1)).onConnected(connection);
// Add strong binding so that connection is oom protected. // Add strong binding so that connection is oom protected.
......
...@@ -321,6 +321,26 @@ public final class MultiprocessTestClientLauncher { ...@@ -321,6 +321,26 @@ public final class MultiprocessTestClientLauncher {
} }
} }
@CalledByNative
private static boolean hasCleanExit(final int pid) {
return runOnLauncherAndGetResult(new Callable<Boolean>() {
@Override
public Boolean call() {
return hasCleanExitOnLauncherThread(pid);
}
});
}
private static boolean hasCleanExitOnLauncherThread(int pid) {
assert isRunningOnLauncherThread();
MultiprocessTestClientLauncher launcher = sPidToLauncher.get(pid);
if (launcher == null) {
return false;
}
return launcher.mLauncher.getConnection().hasCleanExit();
}
/** Does not take ownership of of fds. */ /** Does not take ownership of of fds. */
@CalledByNative @CalledByNative
private static FileDescriptorInfo[] makeFdInfoArray(int[] keys, int[] fds) { private static FileDescriptorInfo[] makeFdInfoArray(int[] keys, int[] fds) {
......
...@@ -81,6 +81,11 @@ bool TerminateMultiProcessTestChild(const Process& process, ...@@ -81,6 +81,11 @@ bool TerminateMultiProcessTestChild(const Process& process,
int exit_code, int exit_code,
bool wait); bool wait);
#if defined(OS_ANDROID)
// Returns whether the child process exited cleanly from the main runloop.
bool MultiProcessTestChildHasCleanExit(const Process& process);
#endif
// MultiProcessTest ------------------------------------------------------------ // MultiProcessTest ------------------------------------------------------------
// A MultiProcessTest is a test class which makes it easier to // A MultiProcessTest is a test class which makes it easier to
......
...@@ -83,4 +83,12 @@ bool TerminateMultiProcessTestChild(const Process& process, ...@@ -83,4 +83,12 @@ bool TerminateMultiProcessTestChild(const Process& process,
env, process.Pid(), exit_code, wait); env, process.Pid(), exit_code, wait);
} }
bool MultiProcessTestChildHasCleanExit(const Process& process) {
JNIEnv* env = android::AttachCurrentThread();
DCHECK(env);
return android::Java_MultiprocessTestClientLauncher_hasCleanExit(
env, process.Pid());
}
} // namespace base } // namespace base
...@@ -171,6 +171,7 @@ void ChildExitObserver::BrowserChildProcessKilled( ...@@ -171,6 +171,7 @@ void ChildExitObserver::BrowserChildProcessKilled(
info.pid = data.GetProcess().Pid(); info.pid = data.GetProcess().Pid();
info.process_type = static_cast<content::ProcessType>(data.process_type); info.process_type = static_cast<content::ProcessType>(data.process_type);
info.app_state = base::android::ApplicationStatusListener::GetState(); info.app_state = base::android::ApplicationStatusListener::GetState();
info.normal_termination = content_info.clean_exit;
PopulateTerminationInfo(content_info, &info); PopulateTerminationInfo(content_info, &info);
browser_child_process_info_.emplace(data.id, info); browser_child_process_info_.emplace(data.id, info);
// Subsequent BrowserChildProcessHostDisconnected will call OnChildExit. // Subsequent BrowserChildProcessHostDisconnected will call OnChildExit.
......
...@@ -157,6 +157,22 @@ TEST_F(CrashMetricsReporterTest, UtilityProcessOOM) { ...@@ -157,6 +157,22 @@ TEST_F(CrashMetricsReporterTest, UtilityProcessOOM) {
nullptr); nullptr);
} }
TEST_F(CrashMetricsReporterTest, NormalTerminationIsNotOOMUtilityProcess) {
ChildExitObserver::TerminationInfo termination_info;
termination_info.process_host_id = 1;
termination_info.pid = base::kNullProcessHandle;
termination_info.process_type = content::PROCESS_TYPE_UTILITY;
termination_info.app_state =
base::android::APPLICATION_STATE_HAS_RUNNING_ACTIVITIES;
termination_info.normal_termination = true;
termination_info.binding_state = base::android::ChildBindingState::STRONG;
termination_info.was_killed_intentionally_by_browser = false;
termination_info.was_oom_protected_status = true;
termination_info.renderer_has_visible_clients = true;
TestOomCrashProcessing(termination_info, {}, nullptr);
}
TEST_F(CrashMetricsReporterTest, UtilityProcessAll) { TEST_F(CrashMetricsReporterTest, UtilityProcessAll) {
ChildExitObserver::TerminationInfo termination_info; ChildExitObserver::TerminationInfo termination_info;
termination_info.process_host_id = 1; termination_info.process_host_id = 1;
......
...@@ -185,6 +185,7 @@ static void JNI_ChildProcessLauncherHelperImpl_SetTerminationInfo( ...@@ -185,6 +185,7 @@ static void JNI_ChildProcessLauncherHelperImpl_SetTerminationInfo(
jlong termination_info_ptr, jlong termination_info_ptr,
jint binding_state, jint binding_state,
jboolean killed_by_us, jboolean killed_by_us,
jboolean clean_exit,
jint remaining_process_with_strong_binding, jint remaining_process_with_strong_binding,
jint remaining_process_with_moderate_binding, jint remaining_process_with_moderate_binding,
jint remaining_process_with_waived_binding) { jint remaining_process_with_waived_binding) {
...@@ -193,6 +194,7 @@ static void JNI_ChildProcessLauncherHelperImpl_SetTerminationInfo( ...@@ -193,6 +194,7 @@ static void JNI_ChildProcessLauncherHelperImpl_SetTerminationInfo(
info->binding_state = info->binding_state =
static_cast<base::android::ChildBindingState>(binding_state); static_cast<base::android::ChildBindingState>(binding_state);
info->was_killed_intentionally_by_browser = killed_by_us; info->was_killed_intentionally_by_browser = killed_by_us;
info->clean_exit = clean_exit;
info->remaining_process_with_strong_binding = info->remaining_process_with_strong_binding =
remaining_process_with_strong_binding; remaining_process_with_strong_binding;
info->remaining_process_with_moderate_binding = info->remaining_process_with_moderate_binding =
......
...@@ -413,8 +413,9 @@ public final class ChildProcessLauncherHelperImpl { ...@@ -413,8 +413,9 @@ public final class ChildProcessLauncherHelperImpl {
int bindingCounts[] = connection.remainingBindingStateCountsCurrentOrWhenDied(); int bindingCounts[] = connection.remainingBindingStateCountsCurrentOrWhenDied();
nativeSetTerminationInfo(terminationInfoPtr, connection.bindingStateCurrentOrWhenDied(), nativeSetTerminationInfo(terminationInfoPtr, connection.bindingStateCurrentOrWhenDied(),
connection.isKilledByUs(), bindingCounts[ChildBindingState.STRONG], connection.isKilledByUs(), connection.hasCleanExit(),
bindingCounts[ChildBindingState.MODERATE], bindingCounts[ChildBindingState.WAIVED]); bindingCounts[ChildBindingState.STRONG], bindingCounts[ChildBindingState.MODERATE],
bindingCounts[ChildBindingState.WAIVED]);
} }
@CalledByNative @CalledByNative
...@@ -644,6 +645,6 @@ public final class ChildProcessLauncherHelperImpl { ...@@ -644,6 +645,6 @@ public final class ChildProcessLauncherHelperImpl {
} }
private static native void nativeSetTerminationInfo(long termiantionInfoPtr, private static native void nativeSetTerminationInfo(long termiantionInfoPtr,
@ChildBindingState int bindingState, boolean killedByUs, int remainingStrong, @ChildBindingState int bindingState, boolean killedByUs, boolean cleanExit,
int remainingModerate, int remainingWaived); int remainingStrong, int remainingModerate, int remainingWaived);
} }
...@@ -37,6 +37,9 @@ struct ChildProcessTerminationInfo { ...@@ -37,6 +37,9 @@ struct ChildProcessTerminationInfo {
// True if child service was explicitly killed by browser. // True if child service was explicitly killed by browser.
bool was_killed_intentionally_by_browser = false; bool was_killed_intentionally_by_browser = false;
// True if the child shut itself down cleanly by quitting the main runloop.
bool clean_exit = false;
// Counts of remaining child processes with corresponding binding. // Counts of remaining child processes with corresponding binding.
int remaining_process_with_strong_binding = 0; int remaining_process_with_strong_binding = 0;
int remaining_process_with_moderate_binding = 0; int remaining_process_with_moderate_binding = 0;
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment