/* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ package org.mozilla.gecko.process; import android.os.DeadObjectException; import android.os.IBinder; import android.os.RemoteException; import android.util.Log; import androidx.annotation.NonNull; import androidx.collection.ArrayMap; import androidx.collection.ArraySet; import androidx.collection.SimpleArrayMap; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.UUID; import org.mozilla.gecko.GeckoAppShell; import org.mozilla.gecko.GeckoNetworkManager; import org.mozilla.gecko.GeckoThread; import org.mozilla.gecko.GeckoThread.FileDescriptors; import org.mozilla.gecko.GeckoThread.ParcelFileDescriptors; import org.mozilla.gecko.IGeckoEditableChild; import org.mozilla.gecko.IGeckoEditableParent; import org.mozilla.gecko.TelemetryUtils; import org.mozilla.gecko.annotation.WrapForJNI; import org.mozilla.gecko.gfx.CompositorSurfaceManager; import org.mozilla.gecko.gfx.ISurfaceAllocator; import org.mozilla.gecko.gfx.RemoteSurfaceAllocator; import org.mozilla.gecko.mozglue.JNIObject; import org.mozilla.gecko.process.ServiceAllocator.PriorityLevel; import org.mozilla.gecko.util.ThreadUtils; import org.mozilla.gecko.util.XPCOMEventTarget; import org.mozilla.geckoview.GeckoResult; public final class GeckoProcessManager extends IProcessManager.Stub { private static final String LOGTAG = "GeckoProcessManager"; private static final GeckoProcessManager INSTANCE = new GeckoProcessManager(); private static final int INVALID_PID = 0; // This id univocally identifies the current process manager instance private final String mInstanceId; public static GeckoProcessManager getInstance() { return INSTANCE; } @WrapForJNI(calledFrom = "gecko") private static void setEditableChildParent( final IGeckoEditableChild child, final IGeckoEditableParent parent) { try { child.transferParent(parent); } catch (final RemoteException e) { Log.e(LOGTAG, "Cannot set parent", e); } } @WrapForJNI(stubName = "GetEditableParent", dispatchTo = "gecko") private static native void nativeGetEditableParent( IGeckoEditableChild child, long contentId, long tabId); @Override // IProcessManager public void getEditableParent( final IGeckoEditableChild child, final long contentId, final long tabId) { nativeGetEditableParent(child, contentId, tabId); } /** * Returns the surface allocator interface to be used by child processes to allocate Surfaces. The * service bound to the returned interface may live in either the GPU process or parent process. */ @Override // IProcessManager public ISurfaceAllocator getSurfaceAllocator() { final GeckoResult gpuEnabled = GeckoAppShell.isGpuProcessEnabled(); try { final GeckoResult allocator = new GeckoResult<>(); if (gpuEnabled.poll(1000)) { // The GPU process is enabled, so look it up and ask it for its surface allocator. XPCOMEventTarget.runOnLauncherThread( () -> { final Selector selector = new Selector(GeckoProcessType.GPU); final GpuProcessConnection conn = (GpuProcessConnection) INSTANCE.mConnections.getExistingConnection(selector); if (conn != null) { allocator.complete(conn.getSurfaceAllocator()); } else { // If we cannot find a GPU process, it has probably been killed and not yet // restarted. Return null here, and allow the caller to try again later. // We definitely do *not* want to return the parent process allocator instead, as // that will result in surfaces being allocated in the parent process, which // therefore won't be usable when the GPU process is eventually launched. allocator.complete(null); } }); } else { // The GPU process is disabled, so return the parent process allocator instance. allocator.complete(RemoteSurfaceAllocator.getInstance(0)); } return allocator.poll(100); } catch (final Throwable e) { Log.e(LOGTAG, "Error in getSurfaceAllocator", e); return null; } } @WrapForJNI public static CompositorSurfaceManager getCompositorSurfaceManager() { final Selector selector = new Selector(GeckoProcessType.GPU); final GpuProcessConnection conn = (GpuProcessConnection) INSTANCE.mConnections.getExistingConnection(selector); if (conn == null) { return null; } return conn.getCompositorSurfaceManager(); } /** Gecko uses this class to uniquely identify a process managed by GeckoProcessManager. */ public static final class Selector { private final GeckoProcessType mType; private final int mPid; @WrapForJNI private Selector(@NonNull final GeckoProcessType type, final int pid) { if (pid == INVALID_PID) { throw new RuntimeException("Invalid PID"); } mType = type; mPid = pid; } @WrapForJNI private Selector(@NonNull final GeckoProcessType type) { mType = type; mPid = INVALID_PID; } public GeckoProcessType getType() { return mType; } public int getPid() { return mPid; } @Override public boolean equals(final Object obj) { if (obj == null) { return false; } if (obj == ((Object) this)) { return true; } final Selector other = (Selector) obj; return mType == other.mType && mPid == other.mPid; } @Override public int hashCode() { return Arrays.hashCode(new Object[] {mType, mPid}); } } private static final class IncompleteChildConnectionException extends RuntimeException { public IncompleteChildConnectionException(@NonNull final String msg) { super(msg); } } /** * Maintains state pertaining to an individual child process. Inheriting from * ServiceAllocator.InstanceInfo enables this class to work with ServiceAllocator. */ private static class ChildConnection extends ServiceAllocator.InstanceInfo { private IChildProcess mChild; private GeckoResult mPendingBind; private int mPid; protected ChildConnection( @NonNull final ServiceAllocator allocator, @NonNull final GeckoProcessType type, @NonNull final PriorityLevel initialPriority) { super(allocator, type, initialPriority); mPid = INVALID_PID; } public int getPid() { XPCOMEventTarget.assertOnLauncherThread(); if (mChild == null) { throw new IncompleteChildConnectionException( "Calling ChildConnection.getPid() on an incomplete connection"); } return mPid; } private GeckoResult completeFailedBind( @NonNull final ServiceAllocator.BindException e) { XPCOMEventTarget.assertOnLauncherThread(); Log.e(LOGTAG, "Failed bind", e); if (mPendingBind == null) { throw new IllegalStateException("Bind failed with null mPendingBind"); } final GeckoResult bindResult = mPendingBind; mPendingBind = null; unbind().accept(v -> bindResult.completeExceptionally(e)); return bindResult; } public GeckoResult bind() { XPCOMEventTarget.assertOnLauncherThread(); if (mChild != null) { // Already bound return GeckoResult.fromValue(mChild); } if (mPendingBind != null) { // Bind in progress return mPendingBind; } mPendingBind = new GeckoResult<>(); try { if (!bindService()) { throw new ServiceAllocator.BindException("Cannot connect to process"); } } catch (final ServiceAllocator.BindException e) { return completeFailedBind(e); } return mPendingBind; } public GeckoResult unbind() { XPCOMEventTarget.assertOnLauncherThread(); if (mPendingBind != null) { // We called unbind() while bind() was still pending completion return mPendingBind.then(child -> unbind()); } if (mChild == null) { // Not bound in the first place return GeckoResult.fromValue(null); } unbindService(); return GeckoResult.fromValue(null); } @Override protected void onBinderConnected(final IBinder service) { XPCOMEventTarget.assertOnLauncherThread(); final IChildProcess child = IChildProcess.Stub.asInterface(service); try { mPid = child.getPid(); onBinderConnected(child); } catch (final DeadObjectException e) { unbindService(); // mPendingBind might be null if a bind was initiated by the system (eg Service Restart) if (mPendingBind != null) { mPendingBind.completeExceptionally(e); mPendingBind = null; } return; } catch (final RemoteException e) { throw new RuntimeException(e); } mChild = child; GeckoProcessManager.INSTANCE.mConnections.onBindComplete(this); // mPendingBind might be null if a bind was initiated by the system (eg Service Restart) if (mPendingBind != null) { mPendingBind.complete(mChild); mPendingBind = null; } } // Subclasses of ChildConnection can override this method to make any IChildProcess calls // specific to their process type immediately after connection. protected void onBinderConnected(@NonNull final IChildProcess child) throws RemoteException {} @Override protected void onReleaseResources() { XPCOMEventTarget.assertOnLauncherThread(); // NB: This must happen *before* resetting mPid! GeckoProcessManager.INSTANCE.mConnections.removeConnection(this); mChild = null; mPid = INVALID_PID; } } private static class NonContentConnection extends ChildConnection { public NonContentConnection( @NonNull final ServiceAllocator allocator, @NonNull final GeckoProcessType type) { super(allocator, type, PriorityLevel.FOREGROUND); if (type == GeckoProcessType.CONTENT) { throw new AssertionError("Attempt to create a NonContentConnection as CONTENT"); } } protected void onAppForeground() { setPriorityLevel(PriorityLevel.FOREGROUND); } protected void onAppBackground() { setPriorityLevel(PriorityLevel.IDLE); } } private static final class GpuProcessConnection extends NonContentConnection { private CompositorSurfaceManager mCompositorSurfaceManager; private ISurfaceAllocator mSurfaceAllocator; // Unique ID used to identify each GPU process instance. Will always be non-zero, // and unlike the process' pid cannot be the same value for successive instances. private int mUniqueGpuProcessId; // Static counter used to initialize each instance's mUniqueGpuProcessId private static int sUniqueGpuProcessIdCounter = 0; public GpuProcessConnection(@NonNull final ServiceAllocator allocator) { super(allocator, GeckoProcessType.GPU); // Initialize the unique ID ensuring we skip 0 (as that is reserved for parent process // allocators). if (sUniqueGpuProcessIdCounter == 0) { sUniqueGpuProcessIdCounter++; } mUniqueGpuProcessId = sUniqueGpuProcessIdCounter++; } @Override protected void onBinderConnected(@NonNull final IChildProcess child) throws RemoteException { mCompositorSurfaceManager = new CompositorSurfaceManager(child.getCompositorSurfaceManager()); mSurfaceAllocator = child.getSurfaceAllocator(mUniqueGpuProcessId); } public CompositorSurfaceManager getCompositorSurfaceManager() { return mCompositorSurfaceManager; } public ISurfaceAllocator getSurfaceAllocator() { return mSurfaceAllocator; } } private static final class SocketProcessConnection extends NonContentConnection { private boolean mIsForeground = true; private boolean mIsNetworkUp = true; public SocketProcessConnection(@NonNull final ServiceAllocator allocator) { super(allocator, GeckoProcessType.SOCKET); GeckoProcessManager.INSTANCE.mConnections.enableNetworkNotifications(); } public void onNetworkStateChange(final boolean isNetworkUp) { mIsNetworkUp = isNetworkUp; prioritize(); } @Override protected void onAppForeground() { mIsForeground = true; prioritize(); } @Override protected void onAppBackground() { mIsForeground = false; prioritize(); } private static final PriorityLevel[][] sPriorityStates = initPriorityStates(); private static PriorityLevel[][] initPriorityStates() { final PriorityLevel[][] states = new PriorityLevel[2][2]; // Background, no network states[0][0] = PriorityLevel.IDLE; // Background, network states[0][1] = PriorityLevel.BACKGROUND; // Foreground, no network states[1][0] = PriorityLevel.IDLE; // Foreground, network states[1][1] = PriorityLevel.FOREGROUND; return states; } private void prioritize() { final PriorityLevel nextPriority = sPriorityStates[mIsForeground ? 1 : 0][mIsNetworkUp ? 1 : 0]; setPriorityLevel(nextPriority); } } private static final class ContentConnection extends ChildConnection { private static final String TELEMETRY_PROCESS_LIFETIME_HISTOGRAM_NAME = "GV_CONTENT_PROCESS_LIFETIME_MS"; private TelemetryUtils.UptimeTimer mLifetimeTimer = null; public ContentConnection( @NonNull final ServiceAllocator allocator, @NonNull final PriorityLevel initialPriority) { super(allocator, GeckoProcessType.CONTENT, initialPriority); } @Override protected void onBinderConnected(final IBinder service) { mLifetimeTimer = new TelemetryUtils.UptimeTimer(TELEMETRY_PROCESS_LIFETIME_HISTOGRAM_NAME); super.onBinderConnected(service); } @Override protected void onReleaseResources() { if (mLifetimeTimer != null) { mLifetimeTimer.stop(); mLifetimeTimer = null; } super.onReleaseResources(); } } /** This class manages the state surrounding existing connections and their priorities. */ private static final class ConnectionManager extends JNIObject { // Connections to non-content processes private final ArrayMap mNonContentConnections; // Mapping of pid to content process private final SimpleArrayMap mContentPids; // Set of initialized content process connections private final ArraySet mContentConnections; // Set of bound but uninitialized content connections private final ArraySet mNonStartedContentConnections; // Allocator for service IDs private final ServiceAllocator mServiceAllocator; private boolean mIsObservingNetwork = false; public ConnectionManager() { mNonContentConnections = new ArrayMap(); mContentPids = new SimpleArrayMap(); mContentConnections = new ArraySet(); mNonStartedContentConnections = new ArraySet(); mServiceAllocator = new ServiceAllocator(); // Attach to native once JNI is ready. if (GeckoThread.isStateAtLeast(GeckoThread.State.JNI_READY)) { attachTo(this); } else { GeckoThread.queueNativeCallUntil( GeckoThread.State.JNI_READY, ConnectionManager.class, "attachTo", this); } } private void enableNetworkNotifications() { if (mIsObservingNetwork) { return; } mIsObservingNetwork = true; // Ensure that GeckoNetworkManager is monitoring network events so that we can // prioritize the socket process. ThreadUtils.runOnUiThread( () -> { GeckoNetworkManager.getInstance().enableNotifications(); }); observeNetworkNotifications(); } @WrapForJNI(dispatchTo = "gecko") private static native void attachTo(ConnectionManager instance); @WrapForJNI(dispatchTo = "gecko") private native void observeNetworkNotifications(); @WrapForJNI(calledFrom = "gecko") private void onBackground() { XPCOMEventTarget.runOnLauncherThread(() -> onAppBackgroundInternal()); } @WrapForJNI(calledFrom = "gecko") private void onForeground() { XPCOMEventTarget.runOnLauncherThread(() -> onAppForegroundInternal()); } @WrapForJNI(calledFrom = "gecko") private void onNetworkStateChange(final boolean isUp) { XPCOMEventTarget.runOnLauncherThread(() -> onNetworkStateChangeInternal(isUp)); } @Override protected native void disposeNative(); private void onAppBackgroundInternal() { XPCOMEventTarget.assertOnLauncherThread(); for (final NonContentConnection conn : mNonContentConnections.values()) { conn.onAppBackground(); } } private void onAppForegroundInternal() { XPCOMEventTarget.assertOnLauncherThread(); for (final NonContentConnection conn : mNonContentConnections.values()) { conn.onAppForeground(); } } private void onNetworkStateChangeInternal(final boolean isUp) { XPCOMEventTarget.assertOnLauncherThread(); final SocketProcessConnection conn = (SocketProcessConnection) mNonContentConnections.get(GeckoProcessType.SOCKET); if (conn == null) { return; } conn.onNetworkStateChange(isUp); } private void removeContentConnection(@NonNull final ChildConnection conn) { if (!mContentConnections.remove(conn)) { throw new RuntimeException("Attempt to remove non-registered connection"); } mNonStartedContentConnections.remove(conn); final int pid; try { pid = conn.getPid(); } catch (final IncompleteChildConnectionException e) { // conn lost its binding before it was able to retrieve its pid. It follows that // mContentPids does not have an entry for this connection, so we can just return. return; } if (pid == INVALID_PID) { return; } final ChildConnection removed = mContentPids.remove(Integer.valueOf(pid)); if (removed != null && removed != conn) { throw new RuntimeException( "Integrity error - connection mismatch for pid " + Integer.toString(pid)); } } public void removeConnection(@NonNull final ChildConnection conn) { XPCOMEventTarget.assertOnLauncherThread(); if (conn.getType() == GeckoProcessType.CONTENT) { removeContentConnection(conn); return; } final ChildConnection removed = mNonContentConnections.remove(conn.getType()); if (removed != conn) { throw new RuntimeException( "Integrity error - connection mismatch for process type " + conn.getType().toString()); } } /** Saves any state information that was acquired upon start completion. */ public void onBindComplete(@NonNull final ChildConnection conn) { if (conn.getType() == GeckoProcessType.CONTENT) { final int pid = conn.getPid(); if (pid == INVALID_PID) { throw new AssertionError( "PID is invalid even though our caller just successfully retrieved it after binding"); } mContentPids.put(pid, (ContentConnection) conn); } } /** Retrieve the ChildConnection for an already running content process. */ private ContentConnection getExistingContentConnection(@NonNull final Selector selector) { XPCOMEventTarget.assertOnLauncherThread(); if (selector.getType() != GeckoProcessType.CONTENT) { throw new IllegalArgumentException("Selector is not for content!"); } return mContentPids.get(selector.getPid()); } /** Unconditionally create a new content connection for the specified priority. */ private ContentConnection getNewContentConnection(@NonNull final PriorityLevel newPriority) { final ContentConnection result = new ContentConnection(mServiceAllocator, newPriority); mContentConnections.add(result); return result; } /** Retrieve the ChildConnection for an already running child process of any type. */ public ChildConnection getExistingConnection(@NonNull final Selector selector) { XPCOMEventTarget.assertOnLauncherThread(); final GeckoProcessType type = selector.getType(); if (type == GeckoProcessType.CONTENT) { return getExistingContentConnection(selector); } return mNonContentConnections.get(type); } /** * Retrieve a ChildConnection for a content process for the purposes of starting. If there are * any preloaded content processes already running, we will use one of those. Otherwise we will * allocate a new ChildConnection. */ private ChildConnection getContentConnectionForStart() { XPCOMEventTarget.assertOnLauncherThread(); if (mNonStartedContentConnections.isEmpty()) { return getNewContentConnection(PriorityLevel.FOREGROUND); } final ChildConnection conn = mNonStartedContentConnections.removeAt(mNonStartedContentConnections.size() - 1); conn.setPriorityLevel(PriorityLevel.FOREGROUND); return conn; } /** Retrieve or create a new child process for the specified non-content process. */ private ChildConnection getNonContentConnection(@NonNull final GeckoProcessType type) { XPCOMEventTarget.assertOnLauncherThread(); if (type == GeckoProcessType.CONTENT) { throw new IllegalArgumentException("Content processes not supported by this method"); } NonContentConnection connection = mNonContentConnections.get(type); if (connection == null) { if (type == GeckoProcessType.SOCKET) { connection = new SocketProcessConnection(mServiceAllocator); } else if (type == GeckoProcessType.GPU) { connection = new GpuProcessConnection(mServiceAllocator); } else { connection = new NonContentConnection(mServiceAllocator, type); } mNonContentConnections.put(type, connection); } return connection; } /** Retrieve a ChildConnection for the purposes of starting a new child process. */ public ChildConnection getConnectionForStart(@NonNull final GeckoProcessType type) { if (type == GeckoProcessType.CONTENT) { return getContentConnectionForStart(); } return getNonContentConnection(type); } /** Retrieve a ChildConnection for the purposes of preloading a new child process. */ public ChildConnection getConnectionForPreload(@NonNull final GeckoProcessType type) { if (type == GeckoProcessType.CONTENT) { final ContentConnection conn = getNewContentConnection(PriorityLevel.BACKGROUND); mNonStartedContentConnections.add(conn); return conn; } return getNonContentConnection(type); } } private final ConnectionManager mConnections; private GeckoProcessManager() { mConnections = new ConnectionManager(); mInstanceId = UUID.randomUUID().toString(); } public void preload(final GeckoProcessType... types) { XPCOMEventTarget.launcherThread() .execute( () -> { for (final GeckoProcessType type : types) { final ChildConnection connection = mConnections.getConnectionForPreload(type); connection.bind(); } }); } public void crashChild(@NonNull final Selector selector) { XPCOMEventTarget.launcherThread() .execute( () -> { final ChildConnection conn = mConnections.getExistingConnection(selector); if (conn == null) { return; } conn.bind() .accept( proc -> { try { proc.crash(); } catch (final RemoteException e) { } }); }); } @WrapForJNI private static void shutdownProcess(final Selector selector) { XPCOMEventTarget.assertOnLauncherThread(); final ChildConnection conn = INSTANCE.mConnections.getExistingConnection(selector); if (conn == null) { return; } conn.unbind(); } @WrapForJNI private static void setProcessPriority( @NonNull final Selector selector, @NonNull final PriorityLevel priorityLevel, final int relativeImportance) { XPCOMEventTarget.runOnLauncherThread( () -> { final ChildConnection conn = INSTANCE.mConnections.getExistingConnection(selector); if (conn == null) { return; } conn.setPriorityLevel(priorityLevel, relativeImportance); }); } @WrapForJNI private static GeckoResult start( final GeckoProcessType type, final String[] args, final int prefsFd, final int prefMapFd, final int ipcFd, final int crashFd) { final GeckoResult result = new GeckoResult<>(); final StartInfo info = new StartInfo( type, GeckoThread.InitInfo.builder() .args(args) .userSerialNumber(System.getenv("MOZ_ANDROID_USER_SERIAL_NUMBER")) .extras(GeckoThread.getActiveExtras()) .flags(filterFlagsForChild(GeckoThread.getActiveFlags())) .fds( FileDescriptors.builder() .prefs(prefsFd) .prefMap(prefMapFd) .ipc(ipcFd) .crashReporter(crashFd) .build()) .build()); XPCOMEventTarget.runOnLauncherThread( () -> { INSTANCE .start(info) .accept(result::complete, result::completeExceptionally) .finally_(info.pfds::close); }); return result; } private static int filterFlagsForChild(final int flags) { return flags & GeckoThread.FLAG_ENABLE_NATIVE_CRASHREPORTER; } private static class StartInfo { final GeckoProcessType type; final String crashHandler; final GeckoThread.InitInfo init; final ParcelFileDescriptors pfds; private StartInfo(final GeckoProcessType type, final GeckoThread.InitInfo initInfo) { this.type = type; this.init = initInfo; crashHandler = GeckoAppShell.getCrashHandlerService() != null ? GeckoAppShell.getCrashHandlerService().getName() : null; // The native side owns the File Descriptors so we cannot call adopt here. pfds = ParcelFileDescriptors.from(initInfo.fds); } } private static final int MAX_RETRIES = 3; private GeckoResult start(final StartInfo info) { return start(info, new ArrayList<>()); } private GeckoResult retry( final StartInfo info, final List retryLog, final Throwable error) { retryLog.add(error); if (error instanceof StartException) { final StartException startError = (StartException) error; if (startError.errorCode == IChildProcess.STARTED_BUSY) { // This process is owned by a different runtime, so we can't use // it. We will keep retrying indefinitely until we find a non-busy process. // Note: this strategy is pretty bad, we go through each process in // sequence until one works, the multiple runtime case is test-only // for now, so that's ok. We can improve on this if we eventually // end up needing something fancier. return start(info, retryLog); } } // If we couldn't unbind there's something very wrong going on and we bail // immediately. if (retryLog.size() >= MAX_RETRIES || error instanceof UnbindException) { return GeckoResult.fromException(fromRetryLog(retryLog)); } return start(info, retryLog); } private String serializeLog(final List retryLog) { if (retryLog == null || retryLog.size() == 0) { return "Empty log."; } final StringBuilder message = new StringBuilder(); for (final Throwable error : retryLog) { if (error instanceof UnbindException) { message.append("Could not unbind: "); } else if (error instanceof StartException) { message.append("Cannot restart child: "); } else { message.append("Error while binding: "); } message.append(error); message.append(";"); } return message.toString(); } private RuntimeException fromRetryLog(final List retryLog) { return new RuntimeException(serializeLog(retryLog), retryLog.get(retryLog.size() - 1)); } private GeckoResult start(final StartInfo info, final List retryLog) { return startInternal(info).then(GeckoResult::fromValue, error -> retry(info, retryLog, error)); } private static class StartException extends RuntimeException { public final int errorCode; public StartException(final int errorCode, final int pid) { super("Could not start process, errorCode: " + errorCode + " PID: " + pid); this.errorCode = errorCode; } } private GeckoResult startInternal(final StartInfo info) { XPCOMEventTarget.assertOnLauncherThread(); final ChildConnection connection = mConnections.getConnectionForStart(info.type); return connection .bind() .map( child -> { final int result = child.start( this, mInstanceId, info.init.args, info.init.extras, info.init.flags, info.init.userSerialNumber, info.crashHandler, info.pfds.prefs, info.pfds.prefMap, info.pfds.ipc, info.pfds.crashReporter); if (result == IChildProcess.STARTED_OK) { return connection.getPid(); } else { throw new StartException(result, connection.getPid()); } }) .then(GeckoResult::fromValue, error -> handleBindError(connection, error)); } private GeckoResult handleBindError( final ChildConnection connection, final Throwable error) { return connection .unbind() .then( unused -> GeckoResult.fromException(error), unbindError -> GeckoResult.fromException(new UnbindException(unbindError))); } private static class UnbindException extends RuntimeException { public UnbindException(final Throwable cause) { super(cause); } } } // GeckoProcessManager