summaryrefslogtreecommitdiff
path: root/base/android/java/src/org/chromium/base/process_launcher/ChildProcessLauncher.java
blob: 7cdc8528bd85ab07a08738398c660c8064bfcc40 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
// 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;

import android.os.Bundle;
import android.os.Handler;
import android.os.IBinder;
import android.os.Looper;

import org.chromium.base.ContextUtils;
import org.chromium.base.Log;
import org.chromium.base.TraceEvent;

import java.io.IOException;
import java.util.List;

/**
 * This class is used to start a child process by connecting to a ChildProcessService.
 */
public class ChildProcessLauncher {
    private static final String TAG = "ChildProcLauncher";

    /** Delegate that client should use to customize the process launching. */
    public abstract static class Delegate {
        /**
         * Called when the launcher is about to start. Gives the embedder a chance to provide an
         * already bound connection if it has one. (allowing for warm-up connections: connections
         * that are already bound in advance to speed up child process start-up time).
         * Note that onBeforeConnectionAllocated will not be called if this method returns a
         * connection.
         * @param connectionAllocator the allocator the returned connection should have been
         * allocated of.
         * @param serviceCallback the service callback that the connection should use.
         * @return a bound connection to use to connect to the child process service, or null if a
         * connection should be allocated and bound by the launcher.
         */
        public ChildProcessConnection getBoundConnection(
                ChildConnectionAllocator connectionAllocator,
                ChildProcessConnection.ServiceCallback serviceCallback) {
            return null;
        }

        /**
         * Called before a connection is allocated.
         * Note that this is only called if the ChildProcessLauncher is created with
         * {@link #createWithConnectionAllocator}.
         * @param serviceBundle the bundle passed in the service intent. Clients can add their own
         * extras to the bundle.
         */
        public void onBeforeConnectionAllocated(Bundle serviceBundle) {}

        /**
         * Called before setup is called on the connection.
         * @param connectionBundle the bundle passed to the {@link ChildProcessService} in the
         * setup call. Clients can add their own extras to the bundle.
         */
        public void onBeforeConnectionSetup(Bundle connectionBundle) {}

        /**
         * Called when the connection was successfully established, meaning the setup call on the
         * service was successful.
         * @param connection the connection over which the setup call was made.
         */
        public void onConnectionEstablished(ChildProcessConnection connection) {}

        /**
         * Called when a connection has been disconnected. Only invoked if onConnectionEstablished
         * was called, meaning the connection was already established.
         * @param connection the connection that got disconnected.
         */
        public void onConnectionLost(ChildProcessConnection connection) {}
    }

    // Represents an invalid process handle; same as base/process/process.h kNullProcessHandle.
    private static final int NULL_PROCESS_HANDLE = 0;

    // The handle for the thread we were created on and on which all methods should be called.
    private final Handler mLauncherHandler;

    private final Delegate mDelegate;

    private final String[] mCommandLine;
    private final FileDescriptorInfo[] mFilesToBeMapped;

    // The allocator used to create the connection.
    private final ChildConnectionAllocator mConnectionAllocator;

    // The IBinder interfaces provided to the created service.
    private final List<IBinder> mClientInterfaces;

    // The actual service connection. Set once we have connected to the service.
    private ChildProcessConnection mConnection;

    /**
     * Constructor.
     *
     * @param launcherHandler the handler for the thread where all operations should happen.
     * @param delegate the delagate that gets notified of the launch progress.
     * @param commandLine the command line that should be passed to the started process.
     * @param filesToBeMapped the files that should be passed to the started process.
     * @param connectionAllocator the allocator used to create connections to the service.
     * @param clientInterfaces the interfaces that should be passed to the started process so it can
     * communicate with the parent process.
     */
    public ChildProcessLauncher(Handler launcherHandler, Delegate delegate, String[] commandLine,
            FileDescriptorInfo[] filesToBeMapped, ChildConnectionAllocator connectionAllocator,
            List<IBinder> clientInterfaces) {
        assert connectionAllocator != null;
        mLauncherHandler = launcherHandler;
        isRunningOnLauncherThread();
        mCommandLine = commandLine;
        mConnectionAllocator = connectionAllocator;
        mDelegate = delegate;
        mFilesToBeMapped = filesToBeMapped;
        mClientInterfaces = clientInterfaces;
    }

    /**
     * Starts the child process and calls setup on it if {@param setupConnection} is true.
     * @param setupConnection whether the setup should be performed on the connection once
     * established
     * @param queueIfNoFreeConnection whether to queue that request if no service connection is
     * available. If the launcher was created with a connection provider, this parameter has no
     * effect.
     * @return true if the connection was started or was queued.
     */
    public boolean start(final boolean setupConnection, final boolean queueIfNoFreeConnection) {
        assert isRunningOnLauncherThread();
        try {
            TraceEvent.begin("ChildProcessLauncher.start");
            ChildProcessConnection.ServiceCallback serviceCallback =
                    new ChildProcessConnection.ServiceCallback() {
                        @Override
                        public void onChildStarted() {}

                        @Override
                        public void onChildStartFailed(ChildProcessConnection connection) {
                            assert isRunningOnLauncherThread();
                            assert mConnection == connection;
                            Log.e(TAG, "ChildProcessConnection.start failed, trying again");
                            mLauncherHandler.post(new Runnable() {
                                @Override
                                public void run() {
                                    // The child process may already be bound to another client
                                    // (this can happen if multi-process WebView is used in more
                                    // than one process), so try starting the process again.
                                    // This connection that failed to start has not been freed,
                                    // so a new bound connection will be allocated.
                                    mConnection = null;
                                    start(setupConnection, queueIfNoFreeConnection);
                                }
                            });
                        }

                        @Override
                        public void onChildProcessDied(ChildProcessConnection connection) {
                            assert isRunningOnLauncherThread();
                            assert mConnection == connection;
                            ChildProcessLauncher.this.onChildProcessDied();
                        }
                    };
            mConnection = mDelegate.getBoundConnection(mConnectionAllocator, serviceCallback);
            if (mConnection != null) {
                assert mConnectionAllocator.isConnectionFromAllocator(mConnection);
                setupConnection();
                return true;
            }
            if (!allocateAndSetupConnection(
                        serviceCallback, setupConnection, queueIfNoFreeConnection)
                    && !queueIfNoFreeConnection) {
                return false;
            }
            return true;
        } finally {
            TraceEvent.end("ChildProcessLauncher.start");
        }
    }

    public ChildProcessConnection getConnection() {
        return mConnection;
    }

    public ChildConnectionAllocator getConnectionAllocator() {
        return mConnectionAllocator;
    }

    private boolean allocateAndSetupConnection(
            final ChildProcessConnection.ServiceCallback serviceCallback,
            final boolean setupConnection, final boolean queueIfNoFreeConnection) {
        assert mConnection == null;
        Bundle serviceBundle = new Bundle();
        mDelegate.onBeforeConnectionAllocated(serviceBundle);

        mConnection = mConnectionAllocator.allocate(
                ContextUtils.getApplicationContext(), serviceBundle, serviceCallback);
        if (mConnection == null) {
            if (!queueIfNoFreeConnection) {
                Log.d(TAG, "Failed to allocate a child connection (no queuing).");
                return false;
            }
            mConnectionAllocator.queueAllocation(
                    () -> allocateAndSetupConnection(
                                    serviceCallback, setupConnection, queueIfNoFreeConnection));
            return false;
        }

        if (setupConnection) {
            setupConnection();
        }
        return true;
    }

    private void setupConnection() {
        ChildProcessConnection.ConnectionCallback connectionCallback =
                new ChildProcessConnection.ConnectionCallback() {
                    @Override
                    public void onConnected(ChildProcessConnection connection) {
                        assert mConnection == connection;
                        onServiceConnected();
                    }
                };
        Bundle connectionBundle = createConnectionBundle();
        mDelegate.onBeforeConnectionSetup(connectionBundle);
        mConnection.setupConnection(connectionBundle, getClientInterfaces(), connectionCallback);
    }

    private void onServiceConnected() {
        assert isRunningOnLauncherThread();

        Log.d(TAG, "on connect callback, pid=%d", mConnection.getPid());

        mDelegate.onConnectionEstablished(mConnection);

        // Proactively close the FDs rather than waiting for the GC to do it.
        try {
            for (FileDescriptorInfo fileInfo : mFilesToBeMapped) {
                fileInfo.fd.close();
            }
        } catch (IOException ioe) {
            Log.w(TAG, "Failed to close FD.", ioe);
        }
    }

    public int getPid() {
        assert isRunningOnLauncherThread();
        return mConnection == null ? NULL_PROCESS_HANDLE : mConnection.getPid();
    }

    public List<IBinder> getClientInterfaces() {
        return mClientInterfaces;
    }

    private boolean isRunningOnLauncherThread() {
        return mLauncherHandler.getLooper() == Looper.myLooper();
    }

    private Bundle createConnectionBundle() {
        Bundle bundle = new Bundle();
        bundle.putStringArray(ChildProcessConstants.EXTRA_COMMAND_LINE, mCommandLine);
        bundle.putParcelableArray(ChildProcessConstants.EXTRA_FILES, mFilesToBeMapped);
        return bundle;
    }

    private void onChildProcessDied() {
        assert isRunningOnLauncherThread();
        if (getPid() != 0) {
            mDelegate.onConnectionLost(mConnection);
        }
    }

    public void stop() {
        assert isRunningOnLauncherThread();
        Log.d(TAG, "stopping child connection: pid=%d", mConnection.getPid());
        mConnection.stop();
    }
}