The Android Open Source Project | adc854b | 2009-03-03 19:28:47 -0800 | [diff] [blame] | 1 | /* |
| 2 | * Copyright (C) 2007 The Android Open Source Project |
| 3 | * |
| 4 | * Licensed under the Apache License, Version 2.0 (the "License"); |
| 5 | * you may not use this file except in compliance with the License. |
| 6 | * You may obtain a copy of the License at |
| 7 | * |
| 8 | * http://www.apache.org/licenses/LICENSE-2.0 |
| 9 | * |
| 10 | * Unless required by applicable law or agreed to in writing, software |
| 11 | * distributed under the License is distributed on an "AS IS" BASIS, |
| 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 13 | * See the License for the specific language governing permissions and |
| 14 | * limitations under the License. |
| 15 | */ |
| 16 | |
| 17 | #define LOG_TAG "ProcessManager" |
| 18 | |
crazybob | 410530d | 2009-06-18 17:19:04 -0500 | [diff] [blame] | 19 | #include <sys/resource.h> |
The Android Open Source Project | adc854b | 2009-03-03 19:28:47 -0800 | [diff] [blame] | 20 | #include <sys/types.h> |
| 21 | #include <sys/wait.h> |
| 22 | #include <unistd.h> |
| 23 | #include <fcntl.h> |
| 24 | #include <signal.h> |
| 25 | #include <stdlib.h> |
| 26 | #include <string.h> |
| 27 | #include <errno.h> |
The Android Open Source Project | adc854b | 2009-03-03 19:28:47 -0800 | [diff] [blame] | 28 | |
| 29 | #include "jni.h" |
| 30 | #include "JNIHelp.h" |
Elliott Hughes | a9f5c16 | 2010-06-16 16:32:18 -0700 | [diff] [blame] | 31 | #include "JniConstants.h" |
Elliott Hughes | e9f1204 | 2011-05-11 11:48:46 -0700 | [diff] [blame^] | 32 | #include "ScopedLocalRef.h" |
The Android Open Source Project | adc854b | 2009-03-03 19:28:47 -0800 | [diff] [blame] | 33 | #include "utils/Log.h" |
The Android Open Source Project | adc854b | 2009-03-03 19:28:47 -0800 | [diff] [blame] | 34 | |
The Android Open Source Project | adc854b | 2009-03-03 19:28:47 -0800 | [diff] [blame] | 35 | /* |
| 36 | * These are constants shared with the higher level code in |
| 37 | * ProcessManager.java. |
| 38 | */ |
| 39 | #define WAIT_STATUS_UNKNOWN (-1) // unknown child status |
| 40 | #define WAIT_STATUS_NO_CHILDREN (-2) // no children to wait for |
| 41 | #define WAIT_STATUS_STRANGE_ERRNO (-3) // observed an undocumented errno |
| 42 | |
The Android Open Source Project | adc854b | 2009-03-03 19:28:47 -0800 | [diff] [blame] | 43 | /** |
The Android Open Source Project | adc854b | 2009-03-03 19:28:47 -0800 | [diff] [blame] | 44 | * Loops indefinitely and calls ProcessManager.onExit() when children exit. |
| 45 | */ |
Elliott Hughes | a5fb706 | 2011-04-28 15:36:47 -0700 | [diff] [blame] | 46 | static void ProcessManager_watchChildren(JNIEnv* env, jclass processManagerClass, jobject processManager) { |
| 47 | static jmethodID onExitMethod = env->GetMethodID(processManagerClass, "onExit", "(II)V"); |
The Android Open Source Project | adc854b | 2009-03-03 19:28:47 -0800 | [diff] [blame] | 48 | if (onExitMethod == NULL) { |
Elliott Hughes | a5fb706 | 2011-04-28 15:36:47 -0700 | [diff] [blame] | 49 | return; |
The Android Open Source Project | adc854b | 2009-03-03 19:28:47 -0800 | [diff] [blame] | 50 | } |
| 51 | |
Elliott Hughes | a5fb706 | 2011-04-28 15:36:47 -0700 | [diff] [blame] | 52 | while (true) { |
| 53 | // Wait for children in our process group. |
The Android Open Source Project | adc854b | 2009-03-03 19:28:47 -0800 | [diff] [blame] | 54 | int status; |
Andy McFadden | a327fd2 | 2009-04-08 12:51:10 -0700 | [diff] [blame] | 55 | pid_t pid = waitpid(0, &status, 0); |
The Android Open Source Project | adc854b | 2009-03-03 19:28:47 -0800 | [diff] [blame] | 56 | |
| 57 | if (pid >= 0) { |
| 58 | // Extract real status. |
| 59 | if (WIFEXITED(status)) { |
| 60 | status = WEXITSTATUS(status); |
| 61 | } else if (WIFSIGNALED(status)) { |
| 62 | status = WTERMSIG(status); |
| 63 | } else if (WIFSTOPPED(status)) { |
| 64 | status = WSTOPSIG(status); |
| 65 | } else { |
| 66 | status = WAIT_STATUS_UNKNOWN; |
| 67 | } |
| 68 | } else { |
| 69 | /* |
| 70 | * The pid should be -1 already, but force it here just in case |
| 71 | * we somehow end up with some other negative value. |
| 72 | */ |
| 73 | pid = -1; |
| 74 | |
| 75 | switch (errno) { |
| 76 | case ECHILD: { |
| 77 | /* |
| 78 | * Expected errno: There are no children to wait() |
| 79 | * for. The callback will sleep until it is |
| 80 | * informed of another child coming to life. |
| 81 | */ |
| 82 | status = WAIT_STATUS_NO_CHILDREN; |
| 83 | break; |
| 84 | } |
| 85 | case EINTR: { |
| 86 | /* |
| 87 | * An unblocked signal came in while waiting; just |
| 88 | * retry the wait(). |
| 89 | */ |
| 90 | continue; |
| 91 | } |
| 92 | default: { |
| 93 | /* |
| 94 | * Unexpected errno, so squawk! Note: Per the |
| 95 | * Linux docs, there are no errnos defined for |
| 96 | * wait() other than the two that are handled |
| 97 | * immediately above. |
| 98 | */ |
Elliott Hughes | a5fb706 | 2011-04-28 15:36:47 -0700 | [diff] [blame] | 99 | LOGE("Error %d calling wait(): %s", errno, strerror(errno)); |
The Android Open Source Project | adc854b | 2009-03-03 19:28:47 -0800 | [diff] [blame] | 100 | status = WAIT_STATUS_STRANGE_ERRNO; |
| 101 | break; |
| 102 | } |
| 103 | } |
| 104 | } |
| 105 | |
Elliott Hughes | a5fb706 | 2011-04-28 15:36:47 -0700 | [diff] [blame] | 106 | env->CallVoidMethod(processManager, onExitMethod, pid, status); |
Elliott Hughes | 94367e0 | 2009-10-23 18:10:44 -0700 | [diff] [blame] | 107 | if (env->ExceptionOccurred()) { |
The Android Open Source Project | adc854b | 2009-03-03 19:28:47 -0800 | [diff] [blame] | 108 | /* |
| 109 | * The callback threw, so break out of the loop and return, |
| 110 | * letting the exception percolate up. |
| 111 | */ |
| 112 | break; |
| 113 | } |
| 114 | } |
| 115 | } |
| 116 | |
crazybob | 410530d | 2009-06-18 17:19:04 -0500 | [diff] [blame] | 117 | /** Close all open fds > 2 (i.e. everything but stdin/out/err), != skipFd. */ |
Elliott Hughes | a5fb706 | 2011-04-28 15:36:47 -0700 | [diff] [blame] | 118 | static void closeNonStandardFds(int skipFd1, int skipFd2) { |
Elliott Hughes | 94367e0 | 2009-10-23 18:10:44 -0700 | [diff] [blame] | 119 | // TODO: rather than close all these non-open files, we could look in /proc/self/fd. |
Elliott Hughes | e22935d | 2010-08-12 17:27:27 -0700 | [diff] [blame] | 120 | rlimit rlimit; |
crazybob | 410530d | 2009-06-18 17:19:04 -0500 | [diff] [blame] | 121 | getrlimit(RLIMIT_NOFILE, &rlimit); |
Elliott Hughes | 94367e0 | 2009-10-23 18:10:44 -0700 | [diff] [blame] | 122 | const int max_fd = rlimit.rlim_max; |
| 123 | for (int fd = 3; fd < max_fd; ++fd) { |
Elliott Hughes | a5fb706 | 2011-04-28 15:36:47 -0700 | [diff] [blame] | 124 | if (fd != skipFd1 && fd != skipFd2) { |
The Android Open Source Project | adc854b | 2009-03-03 19:28:47 -0800 | [diff] [blame] | 125 | close(fd); |
crazybob | 410530d | 2009-06-18 17:19:04 -0500 | [diff] [blame] | 126 | } |
The Android Open Source Project | adc854b | 2009-03-03 19:28:47 -0800 | [diff] [blame] | 127 | } |
The Android Open Source Project | adc854b | 2009-03-03 19:28:47 -0800 | [diff] [blame] | 128 | } |
| 129 | |
| 130 | #define PIPE_COUNT (4) // number of pipes used to communicate with child proc |
| 131 | |
| 132 | /** Closes all pipes in the given array. */ |
| 133 | static void closePipes(int pipes[], int skipFd) { |
Elliott Hughes | a5fb706 | 2011-04-28 15:36:47 -0700 | [diff] [blame] | 134 | for (int i = 0; i < PIPE_COUNT * 2; i++) { |
The Android Open Source Project | adc854b | 2009-03-03 19:28:47 -0800 | [diff] [blame] | 135 | int fd = pipes[i]; |
| 136 | if (fd == -1) { |
| 137 | return; |
| 138 | } |
| 139 | if (fd != skipFd) { |
| 140 | close(pipes[i]); |
| 141 | } |
| 142 | } |
| 143 | } |
| 144 | |
| 145 | /** Executes a command in a child process. */ |
| 146 | static pid_t executeProcess(JNIEnv* env, char** commands, char** environment, |
| 147 | const char* workingDirectory, jobject inDescriptor, |
Elliott Hughes | 1805727 | 2009-10-27 13:39:09 -0700 | [diff] [blame] | 148 | jobject outDescriptor, jobject errDescriptor, |
| 149 | jboolean redirectErrorStream) { |
Elliott Hughes | a5fb706 | 2011-04-28 15:36:47 -0700 | [diff] [blame] | 150 | |
| 151 | // Keep track of the system properties fd so we don't close it. |
| 152 | int androidSystemPropertiesFd = -1; |
| 153 | char* fdString = getenv("ANDROID_PROPERTY_WORKSPACE"); |
| 154 | if (fdString) { |
| 155 | androidSystemPropertiesFd = atoi(fdString); |
| 156 | } |
The Android Open Source Project | adc854b | 2009-03-03 19:28:47 -0800 | [diff] [blame] | 157 | |
| 158 | // Create 4 pipes: stdin, stdout, stderr, and an exec() status pipe. |
| 159 | int pipes[PIPE_COUNT * 2] = { -1, -1, -1, -1, -1, -1, -1, -1 }; |
Elliott Hughes | a5fb706 | 2011-04-28 15:36:47 -0700 | [diff] [blame] | 160 | for (int i = 0; i < PIPE_COUNT; i++) { |
The Android Open Source Project | adc854b | 2009-03-03 19:28:47 -0800 | [diff] [blame] | 161 | if (pipe(pipes + i * 2) == -1) { |
| 162 | jniThrowIOException(env, errno); |
| 163 | closePipes(pipes, -1); |
| 164 | return -1; |
| 165 | } |
| 166 | } |
| 167 | int stdinIn = pipes[0]; |
| 168 | int stdinOut = pipes[1]; |
| 169 | int stdoutIn = pipes[2]; |
| 170 | int stdoutOut = pipes[3]; |
| 171 | int stderrIn = pipes[4]; |
| 172 | int stderrOut = pipes[5]; |
| 173 | int statusIn = pipes[6]; |
| 174 | int statusOut = pipes[7]; |
| 175 | |
| 176 | pid_t childPid = fork(); |
| 177 | |
| 178 | // If fork() failed... |
| 179 | if (childPid == -1) { |
| 180 | jniThrowIOException(env, errno); |
| 181 | closePipes(pipes, -1); |
| 182 | return -1; |
| 183 | } |
| 184 | |
| 185 | // If this is the child process... |
| 186 | if (childPid == 0) { |
crazybob | 410530d | 2009-06-18 17:19:04 -0500 | [diff] [blame] | 187 | /* |
| 188 | * Note: We cannot malloc() or free() after this point! |
| 189 | * A no-longer-running thread may be holding on to the heap lock, and |
| 190 | * an attempt to malloc() or free() would result in deadlock. |
| 191 | */ |
| 192 | |
The Android Open Source Project | adc854b | 2009-03-03 19:28:47 -0800 | [diff] [blame] | 193 | // Replace stdin, out, and err with pipes. |
| 194 | dup2(stdinIn, 0); |
| 195 | dup2(stdoutOut, 1); |
Elliott Hughes | 1805727 | 2009-10-27 13:39:09 -0700 | [diff] [blame] | 196 | if (redirectErrorStream) { |
| 197 | dup2(stdoutOut, 2); |
| 198 | } else { |
| 199 | dup2(stderrOut, 2); |
| 200 | } |
The Android Open Source Project | adc854b | 2009-03-03 19:28:47 -0800 | [diff] [blame] | 201 | |
| 202 | // Close all but statusOut. This saves some work in the next step. |
| 203 | closePipes(pipes, statusOut); |
| 204 | |
| 205 | // Make statusOut automatically close if execvp() succeeds. |
| 206 | fcntl(statusOut, F_SETFD, FD_CLOEXEC); |
| 207 | |
Elliott Hughes | a5fb706 | 2011-04-28 15:36:47 -0700 | [diff] [blame] | 208 | // Close remaining unwanted open fds. |
| 209 | closeNonStandardFds(statusOut, androidSystemPropertiesFd); |
The Android Open Source Project | adc854b | 2009-03-03 19:28:47 -0800 | [diff] [blame] | 210 | |
| 211 | // Switch to working directory. |
| 212 | if (workingDirectory != NULL) { |
| 213 | if (chdir(workingDirectory) == -1) { |
| 214 | goto execFailed; |
| 215 | } |
| 216 | } |
| 217 | |
| 218 | // Set up environment. |
| 219 | if (environment != NULL) { |
Elliott Hughes | a5fb706 | 2011-04-28 15:36:47 -0700 | [diff] [blame] | 220 | extern char** environ; // Standard, but not in any header file. |
The Android Open Source Project | adc854b | 2009-03-03 19:28:47 -0800 | [diff] [blame] | 221 | environ = environment; |
| 222 | } |
| 223 | |
| 224 | // Execute process. By convention, the first argument in the arg array |
| 225 | // should be the command itself. In fact, I get segfaults when this |
| 226 | // isn't the case. |
| 227 | execvp(commands[0], commands); |
| 228 | |
| 229 | // If we got here, execvp() failed or the working dir was invalid. |
| 230 | execFailed: |
Elliott Hughes | a5fb706 | 2011-04-28 15:36:47 -0700 | [diff] [blame] | 231 | int error = errno; |
The Android Open Source Project | adc854b | 2009-03-03 19:28:47 -0800 | [diff] [blame] | 232 | write(statusOut, &error, sizeof(int)); |
| 233 | close(statusOut); |
| 234 | exit(error); |
| 235 | } |
| 236 | |
| 237 | // This is the parent process. |
| 238 | |
| 239 | // Close child's pipe ends. |
| 240 | close(stdinIn); |
| 241 | close(stdoutOut); |
| 242 | close(stderrOut); |
| 243 | close(statusOut); |
| 244 | |
| 245 | // Check status pipe for an error code. If execvp() succeeds, the other |
| 246 | // end of the pipe should automatically close, in which case, we'll read |
| 247 | // nothing. |
Elliott Hughes | a5fb706 | 2011-04-28 15:36:47 -0700 | [diff] [blame] | 248 | int result; |
The Android Open Source Project | adc854b | 2009-03-03 19:28:47 -0800 | [diff] [blame] | 249 | int count = read(statusIn, &result, sizeof(int)); |
| 250 | close(statusIn); |
| 251 | if (count > 0) { |
| 252 | jniThrowIOException(env, result); |
| 253 | |
| 254 | close(stdoutIn); |
| 255 | close(stdinOut); |
| 256 | close(stderrIn); |
| 257 | |
| 258 | return -1; |
| 259 | } |
| 260 | |
| 261 | // Fill in file descriptor wrappers. |
| 262 | jniSetFileDescriptorOfFD(env, inDescriptor, stdoutIn); |
| 263 | jniSetFileDescriptorOfFD(env, outDescriptor, stdinOut); |
| 264 | jniSetFileDescriptorOfFD(env, errDescriptor, stderrIn); |
| 265 | |
| 266 | return childPid; |
| 267 | } |
| 268 | |
| 269 | /** Converts a Java String[] to a 0-terminated char**. */ |
| 270 | static char** convertStrings(JNIEnv* env, jobjectArray javaArray) { |
| 271 | if (javaArray == NULL) { |
| 272 | return NULL; |
| 273 | } |
| 274 | |
Elliott Hughes | 94367e0 | 2009-10-23 18:10:44 -0700 | [diff] [blame] | 275 | jsize length = env->GetArrayLength(javaArray); |
Elliott Hughes | 583ce47 | 2010-07-22 10:23:42 -0700 | [diff] [blame] | 276 | char** array = new char*[length + 1]; |
The Android Open Source Project | adc854b | 2009-03-03 19:28:47 -0800 | [diff] [blame] | 277 | array[length] = 0; |
Elliott Hughes | e9f1204 | 2011-05-11 11:48:46 -0700 | [diff] [blame^] | 278 | for (jsize i = 0; i < length; ++i) { |
| 279 | ScopedLocalRef<jstring> javaEntry(env, reinterpret_cast<jstring>(env->GetObjectArrayElement(javaArray, i))); |
Elliott Hughes | 583ce47 | 2010-07-22 10:23:42 -0700 | [diff] [blame] | 280 | // We need to pass these strings to const-unfriendly code. |
Elliott Hughes | e9f1204 | 2011-05-11 11:48:46 -0700 | [diff] [blame^] | 281 | char* entry = const_cast<char*>(env->GetStringUTFChars(javaEntry.get(), NULL)); |
| 282 | array[i] = entry; |
The Android Open Source Project | adc854b | 2009-03-03 19:28:47 -0800 | [diff] [blame] | 283 | } |
| 284 | |
| 285 | return array; |
| 286 | } |
| 287 | |
| 288 | /** Frees a char** which was converted from a Java String[]. */ |
| 289 | static void freeStrings(JNIEnv* env, jobjectArray javaArray, char** array) { |
| 290 | if (javaArray == NULL) { |
| 291 | return; |
| 292 | } |
| 293 | |
Elliott Hughes | 94367e0 | 2009-10-23 18:10:44 -0700 | [diff] [blame] | 294 | jsize length = env->GetArrayLength(javaArray); |
Elliott Hughes | e9f1204 | 2011-05-11 11:48:46 -0700 | [diff] [blame^] | 295 | for (jsize i = 0; i < length; ++i) { |
| 296 | ScopedLocalRef<jstring> javaEntry(env, reinterpret_cast<jstring>(env->GetObjectArrayElement(javaArray, i))); |
| 297 | env->ReleaseStringUTFChars(javaEntry.get(), array[i]); |
The Android Open Source Project | adc854b | 2009-03-03 19:28:47 -0800 | [diff] [blame] | 298 | } |
| 299 | |
Elliott Hughes | 583ce47 | 2010-07-22 10:23:42 -0700 | [diff] [blame] | 300 | delete[] array; |
The Android Open Source Project | adc854b | 2009-03-03 19:28:47 -0800 | [diff] [blame] | 301 | } |
| 302 | |
| 303 | /** |
| 304 | * Converts Java String[] to char** and delegates to executeProcess(). |
| 305 | */ |
Elliott Hughes | a5fb706 | 2011-04-28 15:36:47 -0700 | [diff] [blame] | 306 | static pid_t ProcessManager_exec(JNIEnv* env, jclass, jobjectArray javaCommands, |
The Android Open Source Project | adc854b | 2009-03-03 19:28:47 -0800 | [diff] [blame] | 307 | jobjectArray javaEnvironment, jstring javaWorkingDirectory, |
Elliott Hughes | 1805727 | 2009-10-27 13:39:09 -0700 | [diff] [blame] | 308 | jobject inDescriptor, jobject outDescriptor, jobject errDescriptor, |
| 309 | jboolean redirectErrorStream) { |
The Android Open Source Project | adc854b | 2009-03-03 19:28:47 -0800 | [diff] [blame] | 310 | |
| 311 | // Copy commands into char*[]. |
| 312 | char** commands = convertStrings(env, javaCommands); |
| 313 | |
| 314 | // Extract working directory string. |
| 315 | const char* workingDirectory = NULL; |
| 316 | if (javaWorkingDirectory != NULL) { |
Elliott Hughes | 94367e0 | 2009-10-23 18:10:44 -0700 | [diff] [blame] | 317 | workingDirectory = env->GetStringUTFChars(javaWorkingDirectory, NULL); |
The Android Open Source Project | adc854b | 2009-03-03 19:28:47 -0800 | [diff] [blame] | 318 | } |
| 319 | |
| 320 | // Convert environment array. |
| 321 | char** environment = convertStrings(env, javaEnvironment); |
| 322 | |
Elliott Hughes | a5fb706 | 2011-04-28 15:36:47 -0700 | [diff] [blame] | 323 | pid_t result = executeProcess(env, commands, environment, workingDirectory, |
Elliott Hughes | 1805727 | 2009-10-27 13:39:09 -0700 | [diff] [blame] | 324 | inDescriptor, outDescriptor, errDescriptor, redirectErrorStream); |
The Android Open Source Project | adc854b | 2009-03-03 19:28:47 -0800 | [diff] [blame] | 325 | |
| 326 | // Temporarily clear exception so we can clean up. |
Elliott Hughes | 94367e0 | 2009-10-23 18:10:44 -0700 | [diff] [blame] | 327 | jthrowable exception = env->ExceptionOccurred(); |
| 328 | env->ExceptionClear(); |
The Android Open Source Project | adc854b | 2009-03-03 19:28:47 -0800 | [diff] [blame] | 329 | |
| 330 | freeStrings(env, javaEnvironment, environment); |
| 331 | |
| 332 | // Clean up working directory string. |
| 333 | if (javaWorkingDirectory != NULL) { |
Elliott Hughes | 94367e0 | 2009-10-23 18:10:44 -0700 | [diff] [blame] | 334 | env->ReleaseStringUTFChars(javaWorkingDirectory, workingDirectory); |
The Android Open Source Project | adc854b | 2009-03-03 19:28:47 -0800 | [diff] [blame] | 335 | } |
| 336 | |
| 337 | freeStrings(env, javaCommands, commands); |
| 338 | |
| 339 | // Re-throw exception if present. |
| 340 | if (exception != NULL) { |
Elliott Hughes | 94367e0 | 2009-10-23 18:10:44 -0700 | [diff] [blame] | 341 | if (env->Throw(exception) < 0) { |
The Android Open Source Project | adc854b | 2009-03-03 19:28:47 -0800 | [diff] [blame] | 342 | LOGE("Error rethrowing exception!"); |
| 343 | } |
| 344 | } |
| 345 | |
| 346 | return result; |
| 347 | } |
| 348 | |
The Android Open Source Project | adc854b | 2009-03-03 19:28:47 -0800 | [diff] [blame] | 349 | static JNINativeMethod methods[] = { |
Elliott Hughes | a5fb706 | 2011-04-28 15:36:47 -0700 | [diff] [blame] | 350 | NATIVE_METHOD(ProcessManager, watchChildren, "(Ljava/lang/ProcessManager;)V"), |
Elliott Hughes | e22935d | 2010-08-12 17:27:27 -0700 | [diff] [blame] | 351 | NATIVE_METHOD(ProcessManager, exec, "([Ljava/lang/String;[Ljava/lang/String;Ljava/lang/String;Ljava/io/FileDescriptor;Ljava/io/FileDescriptor;Ljava/io/FileDescriptor;Z)I"), |
The Android Open Source Project | adc854b | 2009-03-03 19:28:47 -0800 | [diff] [blame] | 352 | }; |
The Android Open Source Project | adc854b | 2009-03-03 19:28:47 -0800 | [diff] [blame] | 353 | int register_java_lang_ProcessManager(JNIEnv* env) { |
Elliott Hughes | c08f9fb | 2010-04-16 17:44:12 -0700 | [diff] [blame] | 354 | return jniRegisterNativeMethods(env, "java/lang/ProcessManager", methods, NELEM(methods)); |
The Android Open Source Project | adc854b | 2009-03-03 19:28:47 -0800 | [diff] [blame] | 355 | } |