| OLD | NEW |
| 1 // Copyright (c) 2012 The Chromium Authors. All rights reserved. | 1 // Copyright (c) 2012 The Chromium Authors. All rights reserved. |
| 2 // Use of this source code is governed by a BSD-style license that can be | 2 // Use of this source code is governed by a BSD-style license that can be |
| 3 // found in the LICENSE file. | 3 // found in the LICENSE file. |
| 4 | 4 |
| 5 #include "base/test/multiprocess_test.h" | 5 #include "base/test/multiprocess_test.h" |
| 6 | 6 |
| 7 #include <errno.h> | |
| 8 #include <string.h> | 7 #include <string.h> |
| 9 #include <sys/types.h> | |
| 10 #include <sys/socket.h> | |
| 11 #include <unistd.h> | |
| 12 | |
| 13 #include <memory> | |
| 14 #include <utility> | |
| 15 #include <vector> | 8 #include <vector> |
| 16 | 9 |
| 10 #include "base/android/context_utils.h" |
| 11 #include "base/android/jni_android.h" |
| 12 #include "base/android/jni_array.h" |
| 13 #include "base/android/scoped_java_ref.h" |
| 17 #include "base/base_switches.h" | 14 #include "base/base_switches.h" |
| 18 #include "base/command_line.h" | 15 #include "base/command_line.h" |
| 19 #include "base/containers/hash_tables.h" | |
| 20 #include "base/lazy_instance.h" | |
| 21 #include "base/logging.h" | 16 #include "base/logging.h" |
| 22 #include "base/macros.h" | 17 #include "jni/MainReturnCodeResult_jni.h" |
| 23 #include "base/pickle.h" | 18 #include "jni/MultiprocessTestClientLauncher_jni.h" |
| 24 #include "base/posix/global_descriptors.h" | |
| 25 #include "base/posix/unix_domain_socket_linux.h" | |
| 26 #include "testing/multiprocess_func_list.h" | |
| 27 | 19 |
| 28 namespace base { | 20 namespace base { |
| 29 | 21 |
| 30 namespace { | |
| 31 | |
| 32 const int kMaxMessageSize = 1024 * 1024; | |
| 33 const int kFragmentSize = 4096; | |
| 34 | |
| 35 // Message sent between parent process and helper child process. | |
| 36 enum class MessageType : uint32_t { | |
| 37 START_REQUEST, | |
| 38 START_RESPONSE, | |
| 39 WAIT_REQUEST, | |
| 40 WAIT_RESPONSE, | |
| 41 }; | |
| 42 | |
| 43 struct MessageHeader { | |
| 44 uint32_t size; | |
| 45 MessageType type; | |
| 46 }; | |
| 47 | |
| 48 struct StartProcessRequest { | |
| 49 MessageHeader header = | |
| 50 {sizeof(StartProcessRequest), MessageType::START_REQUEST}; | |
| 51 | |
| 52 uint32_t num_args = 0; | |
| 53 uint32_t num_fds = 0; | |
| 54 }; | |
| 55 | |
| 56 struct StartProcessResponse { | |
| 57 MessageHeader header = | |
| 58 {sizeof(StartProcessResponse), MessageType::START_RESPONSE}; | |
| 59 | |
| 60 pid_t child_pid; | |
| 61 }; | |
| 62 | |
| 63 struct WaitProcessRequest { | |
| 64 MessageHeader header = | |
| 65 {sizeof(WaitProcessRequest), MessageType::WAIT_REQUEST}; | |
| 66 | |
| 67 pid_t pid; | |
| 68 uint64_t timeout_ms; | |
| 69 }; | |
| 70 | |
| 71 struct WaitProcessResponse { | |
| 72 MessageHeader header = | |
| 73 {sizeof(WaitProcessResponse), MessageType::WAIT_RESPONSE}; | |
| 74 | |
| 75 bool success = false; | |
| 76 int32_t exit_code = 0; | |
| 77 }; | |
| 78 | |
| 79 // Helper class that implements an alternate test child launcher for | |
| 80 // multi-process tests. The default implementation doesn't work if the child is | |
| 81 // launched after starting threads. However, for some tests (i.e. Mojo), this | |
| 82 // is necessary. This implementation works around that issue by forking a helper | |
| 83 // process very early in main(), before any real work is done. Then, when a | |
| 84 // child needs to be spawned, a message is sent to that helper process, which | |
| 85 // then forks and returns the result to the parent. The forked child then calls | |
| 86 // main() and things look as though a brand new process has been fork/exec'd. | |
| 87 class LaunchHelper { | |
| 88 public: | |
| 89 using MainFunction = int (*)(int, char**); | |
| 90 | |
| 91 LaunchHelper() {} | |
| 92 | |
| 93 // Initialise the alternate test child implementation. | |
| 94 void Init(MainFunction main); | |
| 95 | |
| 96 // Starts a child test helper process. | |
| 97 Process StartChildTestHelper(const std::string& procname, | |
| 98 const CommandLine& base_command_line, | |
| 99 const LaunchOptions& options); | |
| 100 | |
| 101 // Waits for a child test helper process. | |
| 102 bool WaitForChildExitWithTimeout(const Process& process, TimeDelta timeout, | |
| 103 int* exit_code); | |
| 104 | |
| 105 bool IsReady() const { return child_fd_ != -1; } | |
| 106 bool IsChild() const { return is_child_; } | |
| 107 | |
| 108 private: | |
| 109 // Wrappers around sendmsg/recvmsg that supports message fragmentation. | |
| 110 void Send(int fd, const MessageHeader* msg, const std::vector<int>& fds); | |
| 111 ssize_t Recv(int fd, void* buf, std::vector<ScopedFD>* fds); | |
| 112 | |
| 113 // Parent process implementation. | |
| 114 void DoParent(int fd); | |
| 115 // Helper process implementation. | |
| 116 void DoHelper(int fd); | |
| 117 | |
| 118 void StartProcessInHelper(const StartProcessRequest* request, | |
| 119 std::vector<ScopedFD> fds); | |
| 120 void WaitForChildInHelper(const WaitProcessRequest* request); | |
| 121 | |
| 122 bool is_child_ = false; | |
| 123 | |
| 124 // Parent vars. | |
| 125 int child_fd_ = -1; | |
| 126 | |
| 127 // Helper vars. | |
| 128 int parent_fd_ = -1; | |
| 129 MainFunction main_ = nullptr; | |
| 130 | |
| 131 DISALLOW_COPY_AND_ASSIGN(LaunchHelper); | |
| 132 }; | |
| 133 | |
| 134 void LaunchHelper::Init(MainFunction main) { | |
| 135 main_ = main; | |
| 136 | |
| 137 // Create a communication channel between the parent and child launch helper. | |
| 138 // fd[0] belongs to the parent, fd[1] belongs to the child. | |
| 139 int fds[2] = {-1, -1}; | |
| 140 int rv = socketpair(AF_UNIX, SOCK_SEQPACKET, 0, fds); | |
| 141 PCHECK(rv == 0); | |
| 142 CHECK_NE(-1, fds[0]); | |
| 143 CHECK_NE(-1, fds[1]); | |
| 144 | |
| 145 pid_t pid = fork(); | |
| 146 PCHECK(pid >= 0) << "Fork failed"; | |
| 147 if (pid) { | |
| 148 // Parent. | |
| 149 rv = close(fds[1]); | |
| 150 PCHECK(rv == 0); | |
| 151 DoParent(fds[0]); | |
| 152 } else { | |
| 153 // Helper. | |
| 154 rv = close(fds[0]); | |
| 155 PCHECK(rv == 0); | |
| 156 DoHelper(fds[1]); | |
| 157 NOTREACHED(); | |
| 158 _exit(0); | |
| 159 } | |
| 160 } | |
| 161 | |
| 162 void LaunchHelper::Send( | |
| 163 int fd, const MessageHeader* msg, const std::vector<int>& fds) { | |
| 164 uint32_t bytes_remaining = msg->size; | |
| 165 const char* buf = reinterpret_cast<const char*>(msg); | |
| 166 while (bytes_remaining) { | |
| 167 size_t send_size = | |
| 168 (bytes_remaining > kFragmentSize) ? kFragmentSize : bytes_remaining; | |
| 169 bool success = UnixDomainSocket::SendMsg( | |
| 170 fd, buf, send_size, | |
| 171 (bytes_remaining == msg->size) ? fds : std::vector<int>()); | |
| 172 CHECK(success); | |
| 173 bytes_remaining -= send_size; | |
| 174 buf += send_size; | |
| 175 } | |
| 176 } | |
| 177 | |
| 178 ssize_t LaunchHelper::Recv(int fd, void* buf, std::vector<ScopedFD>* fds) { | |
| 179 ssize_t size = UnixDomainSocket::RecvMsg(fd, buf, kFragmentSize, fds); | |
| 180 if (size <= 0) | |
| 181 return size; | |
| 182 | |
| 183 const MessageHeader* header = reinterpret_cast<const MessageHeader*>(buf); | |
| 184 CHECK(header->size < kMaxMessageSize); | |
| 185 uint32_t bytes_remaining = header->size - size; | |
| 186 char* buffer = reinterpret_cast<char*>(buf); | |
| 187 buffer += size; | |
| 188 while (bytes_remaining) { | |
| 189 std::vector<ScopedFD> dummy_fds; | |
| 190 size = UnixDomainSocket::RecvMsg(fd, buffer, kFragmentSize, &dummy_fds); | |
| 191 if (size <= 0) | |
| 192 return size; | |
| 193 | |
| 194 CHECK(dummy_fds.empty()); | |
| 195 CHECK(size == kFragmentSize || | |
| 196 static_cast<size_t>(size) == bytes_remaining); | |
| 197 bytes_remaining -= size; | |
| 198 buffer += size; | |
| 199 } | |
| 200 return header->size; | |
| 201 } | |
| 202 | |
| 203 void LaunchHelper::DoParent(int fd) { | |
| 204 child_fd_ = fd; | |
| 205 } | |
| 206 | |
| 207 void LaunchHelper::DoHelper(int fd) { | |
| 208 parent_fd_ = fd; | |
| 209 is_child_ = true; | |
| 210 std::unique_ptr<char[]> buf(new char[kMaxMessageSize]); | |
| 211 while (true) { | |
| 212 // Wait for a message from the parent. | |
| 213 std::vector<ScopedFD> fds; | |
| 214 ssize_t size = Recv(parent_fd_, buf.get(), &fds); | |
| 215 if (size == 0 || (size < 0 && errno == ECONNRESET)) { | |
| 216 _exit(0); | |
| 217 } | |
| 218 PCHECK(size > 0); | |
| 219 | |
| 220 const MessageHeader* header = | |
| 221 reinterpret_cast<const MessageHeader*>(buf.get()); | |
| 222 CHECK_EQ(static_cast<ssize_t>(header->size), size); | |
| 223 switch (header->type) { | |
| 224 case MessageType::START_REQUEST: | |
| 225 StartProcessInHelper( | |
| 226 reinterpret_cast<const StartProcessRequest*>(buf.get()), | |
| 227 std::move(fds)); | |
| 228 break; | |
| 229 case MessageType::WAIT_REQUEST: | |
| 230 WaitForChildInHelper( | |
| 231 reinterpret_cast<const WaitProcessRequest*>(buf.get())); | |
| 232 break; | |
| 233 default: | |
| 234 LOG(FATAL) << "Unsupported message type: " | |
| 235 << static_cast<uint32_t>(header->type); | |
| 236 } | |
| 237 } | |
| 238 } | |
| 239 | |
| 240 void LaunchHelper::StartProcessInHelper(const StartProcessRequest* request, | |
| 241 std::vector<ScopedFD> fds) { | |
| 242 pid_t pid = fork(); | |
| 243 PCHECK(pid >= 0) << "Fork failed"; | |
| 244 if (pid) { | |
| 245 // Helper. | |
| 246 StartProcessResponse resp; | |
| 247 resp.child_pid = pid; | |
| 248 Send(parent_fd_, reinterpret_cast<const MessageHeader*>(&resp), | |
| 249 std::vector<int>()); | |
| 250 } else { | |
| 251 // Child. | |
| 252 PCHECK(close(parent_fd_) == 0); | |
| 253 parent_fd_ = -1; | |
| 254 CommandLine::Reset(); | |
| 255 | |
| 256 Pickle serialised_extra(reinterpret_cast<const char*>(request + 1), | |
| 257 request->header.size - sizeof(StartProcessRequest)); | |
| 258 PickleIterator iter(serialised_extra); | |
| 259 std::vector<std::string> args; | |
| 260 for (size_t i = 0; i < request->num_args; i++) { | |
| 261 std::string arg; | |
| 262 CHECK(iter.ReadString(&arg)); | |
| 263 args.push_back(std::move(arg)); | |
| 264 } | |
| 265 | |
| 266 CHECK_EQ(request->num_fds, fds.size()); | |
| 267 for (size_t i = 0; i < request->num_fds; i++) { | |
| 268 int new_fd; | |
| 269 CHECK(iter.ReadInt(&new_fd)); | |
| 270 int old_fd = fds[i].release(); | |
| 271 if (new_fd != old_fd) { | |
| 272 if (dup2(old_fd, new_fd) < 0) { | |
| 273 PLOG(FATAL) << "dup2"; | |
| 274 } | |
| 275 PCHECK(close(old_fd) == 0); | |
| 276 } | |
| 277 } | |
| 278 | |
| 279 // argv has argc+1 elements, where the last element is NULL. | |
| 280 std::unique_ptr<char*[]> argv(new char*[args.size() + 1]); | |
| 281 for (size_t i = 0; i < args.size(); i++) { | |
| 282 argv[i] = const_cast<char*>(args[i].c_str()); | |
| 283 } | |
| 284 argv[args.size()] = nullptr; | |
| 285 _exit(main_(args.size(), argv.get())); | |
| 286 NOTREACHED(); | |
| 287 } | |
| 288 } | |
| 289 | |
| 290 void LaunchHelper::WaitForChildInHelper(const WaitProcessRequest* request) { | |
| 291 Process process(request->pid); | |
| 292 TimeDelta timeout = TimeDelta::FromMilliseconds(request->timeout_ms); | |
| 293 int exit_code = -1; | |
| 294 bool success = process.WaitForExitWithTimeout(timeout, &exit_code); | |
| 295 | |
| 296 WaitProcessResponse resp; | |
| 297 resp.exit_code = exit_code; | |
| 298 resp.success = success; | |
| 299 Send(parent_fd_, reinterpret_cast<const MessageHeader*>(&resp), | |
| 300 std::vector<int>()); | |
| 301 } | |
| 302 | |
| 303 Process LaunchHelper::StartChildTestHelper(const std::string& procname, | |
| 304 const CommandLine& base_command_line, | |
| 305 const LaunchOptions& options) { | |
| 306 | |
| 307 CommandLine command_line(base_command_line); | |
| 308 if (!command_line.HasSwitch(switches::kTestChildProcess)) | |
| 309 command_line.AppendSwitchASCII(switches::kTestChildProcess, procname); | |
| 310 | |
| 311 StartProcessRequest request; | |
| 312 Pickle serialised_extra; | |
| 313 const CommandLine::StringVector& argv = command_line.argv(); | |
| 314 for (const auto& arg : argv) | |
| 315 CHECK(serialised_extra.WriteString(arg)); | |
| 316 request.num_args = argv.size(); | |
| 317 | |
| 318 std::vector<int> fds_to_send; | |
| 319 if (options.fds_to_remap) { | |
| 320 for (auto p : *options.fds_to_remap) { | |
| 321 CHECK(serialised_extra.WriteInt(p.second)); | |
| 322 fds_to_send.push_back(p.first); | |
| 323 } | |
| 324 request.num_fds = options.fds_to_remap->size(); | |
| 325 } | |
| 326 | |
| 327 size_t buf_size = sizeof(StartProcessRequest) + serialised_extra.size(); | |
| 328 request.header.size = buf_size; | |
| 329 std::unique_ptr<char[]> buffer(new char[buf_size]); | |
| 330 memcpy(buffer.get(), &request, sizeof(StartProcessRequest)); | |
| 331 memcpy(buffer.get() + sizeof(StartProcessRequest), serialised_extra.data(), | |
| 332 serialised_extra.size()); | |
| 333 | |
| 334 // Send start message. | |
| 335 Send(child_fd_, reinterpret_cast<const MessageHeader*>(buffer.get()), | |
| 336 fds_to_send); | |
| 337 | |
| 338 // Synchronously get response. | |
| 339 StartProcessResponse response; | |
| 340 std::vector<ScopedFD> recv_fds; | |
| 341 ssize_t resp_size = Recv(child_fd_, &response, &recv_fds); | |
| 342 PCHECK(resp_size == sizeof(StartProcessResponse)); | |
| 343 | |
| 344 return Process(response.child_pid); | |
| 345 } | |
| 346 | |
| 347 bool LaunchHelper::WaitForChildExitWithTimeout( | |
| 348 const Process& process, TimeDelta timeout, int* exit_code) { | |
| 349 | |
| 350 WaitProcessRequest request; | |
| 351 request.pid = process.Handle(); | |
| 352 request.timeout_ms = timeout.InMilliseconds(); | |
| 353 | |
| 354 Send(child_fd_, reinterpret_cast<const MessageHeader*>(&request), | |
| 355 std::vector<int>()); | |
| 356 | |
| 357 WaitProcessResponse response; | |
| 358 std::vector<ScopedFD> recv_fds; | |
| 359 ssize_t resp_size = Recv(child_fd_, &response, &recv_fds); | |
| 360 PCHECK(resp_size == sizeof(WaitProcessResponse)); | |
| 361 | |
| 362 if (!response.success) | |
| 363 return false; | |
| 364 | |
| 365 *exit_code = response.exit_code; | |
| 366 return true; | |
| 367 } | |
| 368 | |
| 369 LazyInstance<LaunchHelper>::Leaky g_launch_helper; | |
| 370 | |
| 371 } // namespace | |
| 372 | |
| 373 void InitAndroidMultiProcessTestHelper(int (*main)(int, char**)) { | |
| 374 DCHECK(main); | |
| 375 // Don't allow child processes to themselves create new child processes. | |
| 376 if (g_launch_helper.Get().IsChild()) | |
| 377 return; | |
| 378 g_launch_helper.Get().Init(main); | |
| 379 } | |
| 380 | |
| 381 bool AndroidIsChildProcess() { | |
| 382 return g_launch_helper.Get().IsChild(); | |
| 383 } | |
| 384 | |
| 385 bool AndroidWaitForChildExitWithTimeout( | |
| 386 const Process& process, TimeDelta timeout, int* exit_code) { | |
| 387 CHECK(g_launch_helper.Get().IsReady()); | |
| 388 return g_launch_helper.Get().WaitForChildExitWithTimeout( | |
| 389 process, timeout, exit_code); | |
| 390 } | |
| 391 | |
| 392 // A very basic implementation for Android. On Android tests can run in an APK | 22 // A very basic implementation for Android. On Android tests can run in an APK |
| 393 // and we don't have an executable to exec*. This implementation does the bare | 23 // and we don't have an executable to exec*. This implementation does the bare |
| 394 // minimum to execute the method specified by procname (in the child process). | 24 // minimum to execute the method specified by procname (in the child process). |
| 395 // - All options except |fds_to_remap| are ignored. | 25 // - All options except |fds_to_remap| are ignored. |
| 26 // |
| 27 // NOTE: This MUST NOT run on the main thread of the NativeTest application. |
| 396 Process SpawnMultiProcessTestChild(const std::string& procname, | 28 Process SpawnMultiProcessTestChild(const std::string& procname, |
| 397 const CommandLine& base_command_line, | 29 const CommandLine& base_command_line, |
| 398 const LaunchOptions& options) { | 30 const LaunchOptions& options) { |
| 399 if (g_launch_helper.Get().IsReady()) { | 31 JNIEnv* env = android::AttachCurrentThread(); |
| 400 return g_launch_helper.Get().StartChildTestHelper( | 32 DCHECK(env); |
| 401 procname, base_command_line, options); | 33 |
| 34 std::vector<int> fd_keys; |
| 35 std::vector<int> fd_fds; |
| 36 if (options.fds_to_remap) { |
| 37 for (auto& iter : *options.fds_to_remap) { |
| 38 fd_keys.push_back(iter.second); |
| 39 fd_fds.push_back(iter.first); |
| 40 } |
| 402 } | 41 } |
| 403 | 42 |
| 404 // TODO(viettrungluu): The FD-remapping done below is wrong in the presence of | 43 android::ScopedJavaLocalRef<jobjectArray> fds = |
| 405 // cycles (e.g., fd1 -> fd2, fd2 -> fd1). crbug.com/326576 | 44 android::Java_MultiprocessTestClientLauncher_makeFdInfoArray( |
| 406 FileHandleMappingVector empty; | 45 env, base::android::ToJavaIntArray(env, fd_keys), |
| 407 const FileHandleMappingVector* fds_to_remap = | 46 base::android::ToJavaIntArray(env, fd_fds)); |
| 408 options.fds_to_remap ? options.fds_to_remap : ∅ | |
| 409 | 47 |
| 410 pid_t pid = fork(); | 48 CommandLine command_line(base_command_line); |
| 49 if (!command_line.HasSwitch(switches::kTestChildProcess)) { |
| 50 command_line.AppendSwitchASCII(switches::kTestChildProcess, procname); |
| 51 } |
| 411 | 52 |
| 412 if (pid < 0) { | 53 android::ScopedJavaLocalRef<jobjectArray> j_argv = |
| 413 PLOG(ERROR) << "fork"; | 54 android::ToJavaArrayOfStrings(env, command_line.argv()); |
| 414 return Process(); | 55 jint pid = android::Java_MultiprocessTestClientLauncher_launchClient( |
| 56 env, android::GetApplicationContext(), j_argv, fds); |
| 57 return Process(pid); |
| 58 } |
| 59 |
| 60 bool WaitForMultiprocessTestChildExit(const Process& process, |
| 61 TimeDelta timeout, |
| 62 int* exit_code) { |
| 63 JNIEnv* env = android::AttachCurrentThread(); |
| 64 DCHECK(env); |
| 65 |
| 66 base::android::ScopedJavaLocalRef<jobject> result_code = |
| 67 android::Java_MultiprocessTestClientLauncher_waitForMainToReturn( |
| 68 env, android::GetApplicationContext(), process.Pid(), |
| 69 static_cast<int32_t>(timeout.InMilliseconds())); |
| 70 if (result_code.is_null() || |
| 71 Java_MainReturnCodeResult_hasTimedOut(env, result_code)) { |
| 72 return false; |
| 415 } | 73 } |
| 416 if (pid > 0) { | 74 if (exit_code) { |
| 417 // Parent process. | 75 *exit_code = Java_MainReturnCodeResult_getReturnCode(env, result_code); |
| 418 return Process(pid); | |
| 419 } | 76 } |
| 420 // Child process. | 77 return true; |
| 421 base::hash_set<int> fds_to_keep_open; | 78 } |
| 422 for (FileHandleMappingVector::const_iterator it = fds_to_remap->begin(); | |
| 423 it != fds_to_remap->end(); ++it) { | |
| 424 fds_to_keep_open.insert(it->first); | |
| 425 } | |
| 426 // Keep standard FDs (stdin, stdout, stderr, etc.) open since this | |
| 427 // is not meant to spawn a daemon. | |
| 428 int base = GlobalDescriptors::kBaseDescriptor; | |
| 429 for (int fd = base; fd < sysconf(_SC_OPEN_MAX); ++fd) { | |
| 430 if (fds_to_keep_open.find(fd) == fds_to_keep_open.end()) { | |
| 431 close(fd); | |
| 432 } | |
| 433 } | |
| 434 for (FileHandleMappingVector::const_iterator it = fds_to_remap->begin(); | |
| 435 it != fds_to_remap->end(); ++it) { | |
| 436 int old_fd = it->first; | |
| 437 int new_fd = it->second; | |
| 438 if (dup2(old_fd, new_fd) < 0) { | |
| 439 PLOG(FATAL) << "dup2"; | |
| 440 } | |
| 441 close(old_fd); | |
| 442 } | |
| 443 CommandLine::Reset(); | |
| 444 CommandLine::Init(0, nullptr); | |
| 445 CommandLine* command_line = CommandLine::ForCurrentProcess(); | |
| 446 command_line->InitFromArgv(base_command_line.argv()); | |
| 447 if (!command_line->HasSwitch(switches::kTestChildProcess)) | |
| 448 command_line->AppendSwitchASCII(switches::kTestChildProcess, procname); | |
| 449 | 79 |
| 450 _exit(multi_process_function_list::InvokeChildProcessTest(procname)); | 80 bool TerminateMultiProcessTestChild(const Process& process, |
| 451 return Process(); | 81 int exit_code, |
| 82 bool wait) { |
| 83 JNIEnv* env = android::AttachCurrentThread(); |
| 84 DCHECK(env); |
| 85 |
| 86 return android::Java_MultiprocessTestClientLauncher_terminate( |
| 87 env, android::GetApplicationContext(), process.Pid(), exit_code, wait); |
| 452 } | 88 } |
| 453 | 89 |
| 454 } // namespace base | 90 } // namespace base |
| OLD | NEW |