Skip to content

Commit

Permalink
JNDCrash API changes to improve usability: out-of-process initializat…
Browse files Browse the repository at this point in the history
…ion method now starts a service itself.
  • Loading branch information
ivanarh committed Apr 27, 2018
1 parent 5182850 commit a0e0041
Show file tree
Hide file tree
Showing 3 changed files with 106 additions and 45 deletions.
54 changes: 24 additions & 30 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
To add a library to a project please add this line to your application's build.gradle, `dependencies` section:

```
compile 'ru.ivanarh.ndcrash:jndcrash-libunwind:0.1'
compile 'ru.ivanarh.ndcrash:jndcrash-libunwind:0.2'
```

Also make sure that `jcenter()` is included to `repositories` section (it's already done in default project template). Run "Sync" operation and verify that no error has occured.
Expand Down Expand Up @@ -87,36 +87,30 @@ First you need to declare a new service that will work in a parallel process, pl

Note that ":reportprocess" is just string used for a process name, it doesn't affect library work but should be set.

Next you need to add some code that initializes a signal handler and starts out service. A recommended place for it is `onCreate` method of Application subclass. **One important detail:** Code that initializes a signal handler must be run only from main application process, not from background service process. The problem is that `onCreate` method is called for both processes and we need a way to determine if it's being run in a main process. For this purpose `NDCrashUtils.isMainProcess` method may be used.

For a signal handler initialization please call `NDCrash.initializeOutOfProcess` method. You need to pass a Context instance and don't need to specify a socket name, a package name with additional ".ndcrash" suffix will be used for this.
To specify a crash report output file path and an unwinder you need to pass this data to extras of Intent that will be used to start a service. A report passed as a string, an unwinder should be passed as an ordinal integer value of `NDCrashUnwinder` enum.

An example initialization code is below, assuming it's located in `onCreate` method of Application subclass.
Next you need to add some code that initializes a signal handler and starts a service. You should add this code to `onCreate` method of your Application subclass. It will register a signal handler and will start a background service that will use specified unwinder and report path. A class of starting background service should be provided in this point (it should be the same with declared in AndroidManifest.xml). This is an example:

```
@Override
public void onCreate() {
super.onCreate();
...
if (NDCrashUtils.isMainProcess(this)) {
final NDCrashError error = NDCrash.initializeOutOfProcess(this);
if (error == NDCrashError.ok) {
} else {
}
final Intent serviceIntent = new Intent(context, NDCrashService.class);
final String reportPath = getFilesDir().getAbsolutePath() + "/crash.txt"; // Example.
serviceIntent.putExtra(NDCrashService.EXTRA_REPORT_FILE, reportPath);
serviceIntent.putExtra(NDCrashService.EXTRA_UNWINDER, NDCrashUnwinder.libunwind.ordinal());
startService(serviceIntent);
}
...
@Override
public void onCreate() {
super.onCreate();
final String reportPath = getFilesDir().getAbsolutePath() + "/crash.txt"; // Example.
final NDCrashError error = NDCrash.initializeOutOfProcess(
this,
reportPath,
NDCrashUnwinder.libunwind,
NDCrashService.class);
if (error == NDCrashError.ok) {
// Initialization is successful.
} else {
// Initialization failed, check error value.
}
}
```

Some important details: `onCreate()` method is run for all processes of an application including background crash service process. The `NDCrash.initializeOutOfProcess` method checks if it's run from crash service process. If yes it doesn't do anything and return NDCrashError.ok value, we don't need to register a signal handler for background process.

If your application has a lot of processes you can add additional check and initialize a library only for processes that use NDK code (for optimization). But keep in mind that a library must be initialized from main process of an application anyway. It should be done because a service is started only from the main process of an application. You can use `NDCrashUtils.isMainProcess` to check this situation.

### Immediate crash handling ###

You can access a crash report immediately, for example you can send it to your server straight after it's generated. It's supported **only in Out-of-process mode**. To do this you need to subclass `NDCrashService` class and override `onCrash` method:
Expand All @@ -135,12 +129,12 @@ Also a service declaration in AndroidManifest.xml should be updated:
<service android:name=".CrashService" android:process=":reportprocess"/>
```

And, or course, you need to update a service starting code:
And, or course, you need to update a library initialization code, it should provide actual `serviceClass` argument:

```
final Intent serviceIntent = new Intent(context, CrashService.class);
// Set extras...
startService(serviceIntent);
final NDCrashError error = NDCrash.initializeOutOfProcess(
...
CrashService.class);
```

Please keep in mind that onCrash is run from background thread created by *pthread*. It means it doesn't have a Looper instance. Also note that when `onCrash` method is running other crash report can't be created, it means a very long blocking operation in it is unwanted.
Expand Down
63 changes: 51 additions & 12 deletions src/main/java/ru/ivanarh/jndcrash/NDCrash.java
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package ru.ivanarh.jndcrash;

import android.content.Context;
import android.content.Intent;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;

Expand Down Expand Up @@ -36,12 +37,38 @@ public static boolean deInitializeInProcess() {
private static native boolean nativeDeInitializeInProcess();

/**
* Initializes NDCrash library signal handler using out-of-process mode.
* Initializes NDCrash library signal handler using out-of-process mode. Should be called from
* onCreate() method of your subclass of Application.
*
* @param context Context instance. Used to determine a socket name.
* @param context Context instance. Used to determine a socket name and start a service.
* @param crashReportPath Path where a crash report is saved.
* @param unwinder Used unwinder. See ndcrash_unwinder type in ndcrash.h.
* @param serviceClass Class of background service. Used when we need to use a custom subclass
* of NDCrashUnwinder to use as a background service. If you didn't subclass
* NDCrashUnwinder, please pass NDCrashUnwinder.class.
* @return Error status.
*/
public static NDCrashError initializeOutOfProcess(Context context) {
public static NDCrashError initializeOutOfProcess(
@NonNull Context context,
@Nullable String crashReportPath,
@NonNull NDCrashUnwinder unwinder,
@NonNull Class<? extends NDCrashService> serviceClass) {
if (NDCrashUtils.isCrashServiceProcess(context, serviceClass)) {
// If it's a background crash service process we don't need to initialize anything,
// we treat this situation as no error because this method is designed to call from
// Application.onCreate().
return NDCrashError.ok;
}
// Saving service class, we should be able to stop it on de-initialization.
mServiceClass = serviceClass;
// Starting crash reporting service. Only from main process.
if (NDCrashUtils.isMainProcess(context)) {
final Intent serviceIntent = new Intent(context, serviceClass);
serviceIntent.putExtra(NDCrashService.EXTRA_REPORT_FILE, crashReportPath);
serviceIntent.putExtra(NDCrashService.EXTRA_UNWINDER, unwinder.ordinal());
context.startService(serviceIntent);
}
// Initializing signal handler.
return NDCrashError.values()[nativeInitializeOutOfProcess(getSocketName(context))];
}

Expand All @@ -51,9 +78,14 @@ public static NDCrashError initializeOutOfProcess(Context context) {
/**
* De-initializes NDCrash library signal handler using out-of-process mode.
*
* @param context Context instance. Used to stop a service.
* @return Flag whether de-initialization was successful.
*/
public static boolean deInitializeOutOfProcess() {
public static boolean deInitializeOutOfProcess(@NonNull Context context) {
if (mServiceClass != null) {
context.stopService(new Intent(context, mServiceClass));
mServiceClass = null;
}
return nativeDeInitializeOutOfProcess();
}

Expand All @@ -62,17 +94,18 @@ public static boolean deInitializeOutOfProcess() {

/**
* Starts NDCrash out-of-process unwinding daemon. This is necessary for out of process crash
* handling. This method should be run from a service that works in separate process.
* handling. This method is run from a service that works in separate process.
*
* @param context Context instance. Used to determine a socket name.
* @param context Context instance. Used to determine a socket name.
* @param crashReportPath Path where to save a crash report.
* @param unwinder Unwinder to use.
* @param unwinder Unwinder to use.
* @param callback Callback to execute when a crash has occurred.
* @return Error status.
*/
public static NDCrashError startOutOfProcessDaemon(
Context context,
static NDCrashError startOutOfProcessDaemon(
@NonNull Context context,
@Nullable String crashReportPath,
NDCrashUnwinder unwinder,
@NonNull NDCrashUnwinder unwinder,
@Nullable OnCrashCallback callback) {
if (NDCrashUtils.isMainProcess(context)) {
return NDCrashError.error_wrong_process;
Expand All @@ -96,7 +129,7 @@ private static native int nativeStartOutOfProcessDaemon(
*
* @return Flag whether daemon stopping was successful.
*/
public static boolean stopOutOfProcessDaemon() {
static boolean stopOutOfProcessDaemon() {
final boolean result = nativeStopOutOfProcessDaemon();
mOnCrashCallback = null;
return result;
Expand All @@ -111,6 +144,12 @@ public static boolean stopOutOfProcessDaemon() {
@Nullable
private static volatile OnCrashCallback mOnCrashCallback = null;

/**
* Background service class for out-of-process mode.
*/
@Nullable
private static Class<? extends NDCrashService> mServiceClass = null;

/**
* Runs on crash callback if it was set. This method is called from native code.
*
Expand All @@ -130,7 +169,7 @@ private static void runOnCrashCallback(String reportPath) {
* @param context Context to use.
* @return Socket name.
*/
private static String getSocketName(Context context) {
private static String getSocketName(@NonNull Context context) {
return context.getPackageName() + ".ndcrash";
}

Expand Down
34 changes: 31 additions & 3 deletions src/main/java/ru/ivanarh/jndcrash/NDCrashUtils.java
Original file line number Diff line number Diff line change
@@ -1,22 +1,25 @@
package ru.ivanarh.jndcrash;

import android.app.ActivityManager;
import android.content.ComponentName;
import android.content.Context;
import android.content.pm.PackageManager;
import android.content.pm.ServiceInfo;
import android.os.Process;
import android.support.annotation.NonNull;

/**
* Contains some utility code.
*/
public class NDCrashUtils {

/**
* Checks if a current process is a main process of application. Returns false if this code
* is running in a service with android:process attribute in manifest.
* Checks if a current process is a main process of application.
*
* @param context Current context.
* @return Flag whether it's a main process.
*/
public static boolean isMainProcess(Context context) {
public static boolean isMainProcess(@NonNull Context context) {
final int pid = Process.myPid();
final String packageName = context.getPackageName();
final ActivityManager manager = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
Expand All @@ -30,4 +33,29 @@ public static boolean isMainProcess(Context context) {
return true;
}

/**
* Checks if a current process is a background crash service process.
*
* @param context Current context.
* @param serviceClass Class of background crash reporting service.
* @return Flag whether a current process is a background crash service process.
*/
public static boolean isCrashServiceProcess(@NonNull Context context, @NonNull Class<? extends NDCrashService> serviceClass) {
final ServiceInfo serviceInfo;
try {
serviceInfo = context.getPackageManager().getServiceInfo(new ComponentName(context, serviceClass), 0);
} catch (PackageManager.NameNotFoundException ignored) {
return false;
}
final int pid = Process.myPid();
final ActivityManager manager = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
if (manager != null) {
for (final ActivityManager.RunningAppProcessInfo info : manager.getRunningAppProcesses()) {
if (info.pid == pid) {
return serviceInfo.processName.equals(info.processName);
}
}
}
return false;
}
}

0 comments on commit a0e0041

Please sign in to comment.