1 /* 2 * Copyright (C) 2008 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 package com.android.server; 18 19 import android.app.IActivityController; 20 import android.os.Binder; 21 import android.os.Build; 22 import android.os.RemoteException; 23 import android.system.ErrnoException; 24 import android.system.OsConstants; 25 import android.system.StructRlimit; 26 import com.android.internal.os.ZygoteConnectionConstants; 27 import com.android.server.am.ActivityManagerService; 28 29 import android.content.BroadcastReceiver; 30 import android.content.ContentResolver; 31 import android.content.Context; 32 import android.content.Intent; 33 import android.content.IntentFilter; 34 import android.hidl.manager.V1_0.IServiceManager; 35 import android.os.Debug; 36 import android.os.Handler; 37 import android.os.IPowerManager; 38 import android.os.Looper; 39 import android.os.Process; 40 import android.os.ServiceManager; 41 import android.os.SystemClock; 42 import android.os.SystemProperties; 43 import android.util.EventLog; 44 import android.util.Log; 45 import android.util.Slog; 46 47 import java.io.File; 48 import java.io.FileWriter; 49 import java.io.IOException; 50 import java.util.ArrayList; 51 import java.util.Arrays; 52 import java.util.Collections; 53 import java.util.HashSet; 54 import java.util.List; 55 56 /** This class calls its monitor every minute. Killing this process if they don't return **/ 57 public class Watchdog extends Thread { 58 static final String TAG = "Watchdog"; 59 60 // Set this to true to use debug default values. 61 static final boolean DB = false; 62 63 // Note 1: Do not lower this value below thirty seconds without tightening the invoke-with 64 // timeout in com.android.internal.os.ZygoteConnection, or wrapped applications 65 // can trigger the watchdog. 66 // Note 2: The debug value is already below the wait time in ZygoteConnection. Wrapped 67 // applications may not work with a debug build. CTS will fail. 68 static final long DEFAULT_TIMEOUT = DB ? 10*1000 : 60*1000; 69 static final long CHECK_INTERVAL = DEFAULT_TIMEOUT / 2; 70 71 // These are temporally ordered: larger values as lateness increases 72 static final int COMPLETED = 0; 73 static final int WAITING = 1; 74 static final int WAITED_HALF = 2; 75 static final int OVERDUE = 3; 76 77 // Which native processes to dump into dropbox's stack traces 78 public static final String[] NATIVE_STACKS_OF_INTEREST = new String[] { 79 "/system/bin/audioserver", 80 "/system/bin/cameraserver", 81 "/system/bin/drmserver", 82 "/system/bin/mediadrmserver", 83 "/system/bin/mediaserver", 84 "/system/bin/sdcard", 85 "/system/bin/surfaceflinger", 86 "media.extractor", // system/bin/mediaextractor 87 "media.metrics", // system/bin/mediametrics 88 "media.codec", // vendor/bin/hw/android.hardware.media.omx@1.0-service 89 "com.android.bluetooth", // Bluetooth service 90 "statsd", // Stats daemon 91 }; 92 93 public static final List<String> HAL_INTERFACES_OF_INTEREST = Arrays.asList( 94 "android.hardware.audio@2.0::IDevicesFactory", 95 "android.hardware.audio@4.0::IDevicesFactory", 96 "android.hardware.bluetooth@1.0::IBluetoothHci", 97 "android.hardware.camera.provider@2.4::ICameraProvider", 98 "android.hardware.graphics.composer@2.1::IComposer", 99 "android.hardware.media.omx@1.0::IOmx", 100 "android.hardware.media.omx@1.0::IOmxStore", 101 "android.hardware.sensors@1.0::ISensors", 102 "android.hardware.vr@1.0::IVr" 103 ); 104 105 static Watchdog sWatchdog; 106 107 /* This handler will be used to post message back onto the main thread */ 108 final ArrayList<HandlerChecker> mHandlerCheckers = new ArrayList<>(); 109 final HandlerChecker mMonitorChecker; 110 ContentResolver mResolver; 111 ActivityManagerService mActivity; 112 113 int mPhonePid; 114 IActivityController mController; 115 boolean mAllowRestart = true; 116 final OpenFdMonitor mOpenFdMonitor; 117 118 /** 119 * Used for checking status of handle threads and scheduling monitor callbacks. 120 */ 121 public final class HandlerChecker implements Runnable { 122 private final Handler mHandler; 123 private final String mName; 124 private final long mWaitMax; 125 private final ArrayList<Monitor> mMonitors = new ArrayList<Monitor>(); 126 private boolean mCompleted; 127 private Monitor mCurrentMonitor; 128 private long mStartTime; 129 HandlerChecker(Handler handler, String name, long waitMaxMillis)130 HandlerChecker(Handler handler, String name, long waitMaxMillis) { 131 mHandler = handler; 132 mName = name; 133 mWaitMax = waitMaxMillis; 134 mCompleted = true; 135 } 136 addMonitor(Monitor monitor)137 public void addMonitor(Monitor monitor) { 138 mMonitors.add(monitor); 139 } 140 scheduleCheckLocked()141 public void scheduleCheckLocked() { 142 if (mMonitors.size() == 0 && mHandler.getLooper().getQueue().isPolling()) { 143 // If the target looper has recently been polling, then 144 // there is no reason to enqueue our checker on it since that 145 // is as good as it not being deadlocked. This avoid having 146 // to do a context switch to check the thread. Note that we 147 // only do this if mCheckReboot is false and we have no 148 // monitors, since those would need to be executed at this point. 149 mCompleted = true; 150 return; 151 } 152 153 if (!mCompleted) { 154 // we already have a check in flight, so no need 155 return; 156 } 157 158 mCompleted = false; 159 mCurrentMonitor = null; 160 mStartTime = SystemClock.uptimeMillis(); 161 mHandler.postAtFrontOfQueue(this); 162 } 163 isOverdueLocked()164 public boolean isOverdueLocked() { 165 return (!mCompleted) && (SystemClock.uptimeMillis() > mStartTime + mWaitMax); 166 } 167 getCompletionStateLocked()168 public int getCompletionStateLocked() { 169 if (mCompleted) { 170 return COMPLETED; 171 } else { 172 long latency = SystemClock.uptimeMillis() - mStartTime; 173 if (latency < mWaitMax/2) { 174 return WAITING; 175 } else if (latency < mWaitMax) { 176 return WAITED_HALF; 177 } 178 } 179 return OVERDUE; 180 } 181 getThread()182 public Thread getThread() { 183 return mHandler.getLooper().getThread(); 184 } 185 getName()186 public String getName() { 187 return mName; 188 } 189 describeBlockedStateLocked()190 public String describeBlockedStateLocked() { 191 if (mCurrentMonitor == null) { 192 return "Blocked in handler on " + mName + " (" + getThread().getName() + ")"; 193 } else { 194 return "Blocked in monitor " + mCurrentMonitor.getClass().getName() 195 + " on " + mName + " (" + getThread().getName() + ")"; 196 } 197 } 198 199 @Override run()200 public void run() { 201 final int size = mMonitors.size(); 202 for (int i = 0 ; i < size ; i++) { 203 synchronized (Watchdog.this) { 204 mCurrentMonitor = mMonitors.get(i); 205 } 206 mCurrentMonitor.monitor(); 207 } 208 209 synchronized (Watchdog.this) { 210 mCompleted = true; 211 mCurrentMonitor = null; 212 } 213 } 214 } 215 216 final class RebootRequestReceiver extends BroadcastReceiver { 217 @Override onReceive(Context c, Intent intent)218 public void onReceive(Context c, Intent intent) { 219 if (intent.getIntExtra("nowait", 0) != 0) { 220 rebootSystem("Received ACTION_REBOOT broadcast"); 221 return; 222 } 223 Slog.w(TAG, "Unsupported ACTION_REBOOT broadcast: " + intent); 224 } 225 } 226 227 /** Monitor for checking the availability of binder threads. The monitor will block until 228 * there is a binder thread available to process in coming IPCs to make sure other processes 229 * can still communicate with the service. 230 */ 231 private static final class BinderThreadMonitor implements Watchdog.Monitor { 232 @Override monitor()233 public void monitor() { 234 Binder.blockUntilThreadAvailable(); 235 } 236 } 237 238 public interface Monitor { monitor()239 void monitor(); 240 } 241 getInstance()242 public static Watchdog getInstance() { 243 if (sWatchdog == null) { 244 sWatchdog = new Watchdog(); 245 } 246 247 return sWatchdog; 248 } 249 Watchdog()250 private Watchdog() { 251 super("watchdog"); 252 // Initialize handler checkers for each common thread we want to check. Note 253 // that we are not currently checking the background thread, since it can 254 // potentially hold longer running operations with no guarantees about the timeliness 255 // of operations there. 256 257 // The shared foreground thread is the main checker. It is where we 258 // will also dispatch monitor checks and do other work. 259 mMonitorChecker = new HandlerChecker(FgThread.getHandler(), 260 "foreground thread", DEFAULT_TIMEOUT); 261 mHandlerCheckers.add(mMonitorChecker); 262 // Add checker for main thread. We only do a quick check since there 263 // can be UI running on the thread. 264 mHandlerCheckers.add(new HandlerChecker(new Handler(Looper.getMainLooper()), 265 "main thread", DEFAULT_TIMEOUT)); 266 // Add checker for shared UI thread. 267 mHandlerCheckers.add(new HandlerChecker(UiThread.getHandler(), 268 "ui thread", DEFAULT_TIMEOUT)); 269 // And also check IO thread. 270 mHandlerCheckers.add(new HandlerChecker(IoThread.getHandler(), 271 "i/o thread", DEFAULT_TIMEOUT)); 272 // And the display thread. 273 mHandlerCheckers.add(new HandlerChecker(DisplayThread.getHandler(), 274 "display thread", DEFAULT_TIMEOUT)); 275 276 // Initialize monitor for Binder threads. 277 addMonitor(new BinderThreadMonitor()); 278 279 mOpenFdMonitor = OpenFdMonitor.create(); 280 281 // See the notes on DEFAULT_TIMEOUT. 282 assert DB || 283 DEFAULT_TIMEOUT > ZygoteConnectionConstants.WRAPPED_PID_TIMEOUT_MILLIS; 284 } 285 init(Context context, ActivityManagerService activity)286 public void init(Context context, ActivityManagerService activity) { 287 mResolver = context.getContentResolver(); 288 mActivity = activity; 289 290 context.registerReceiver(new RebootRequestReceiver(), 291 new IntentFilter(Intent.ACTION_REBOOT), 292 android.Manifest.permission.REBOOT, null); 293 } 294 processStarted(String name, int pid)295 public void processStarted(String name, int pid) { 296 synchronized (this) { 297 if ("com.android.phone".equals(name)) { 298 mPhonePid = pid; 299 } 300 } 301 } 302 setActivityController(IActivityController controller)303 public void setActivityController(IActivityController controller) { 304 synchronized (this) { 305 mController = controller; 306 } 307 } 308 setAllowRestart(boolean allowRestart)309 public void setAllowRestart(boolean allowRestart) { 310 synchronized (this) { 311 mAllowRestart = allowRestart; 312 } 313 } 314 addMonitor(Monitor monitor)315 public void addMonitor(Monitor monitor) { 316 synchronized (this) { 317 if (isAlive()) { 318 throw new RuntimeException("Monitors can't be added once the Watchdog is running"); 319 } 320 mMonitorChecker.addMonitor(monitor); 321 } 322 } 323 addThread(Handler thread)324 public void addThread(Handler thread) { 325 addThread(thread, DEFAULT_TIMEOUT); 326 } 327 addThread(Handler thread, long timeoutMillis)328 public void addThread(Handler thread, long timeoutMillis) { 329 synchronized (this) { 330 if (isAlive()) { 331 throw new RuntimeException("Threads can't be added once the Watchdog is running"); 332 } 333 final String name = thread.getLooper().getThread().getName(); 334 mHandlerCheckers.add(new HandlerChecker(thread, name, timeoutMillis)); 335 } 336 } 337 338 /** 339 * Perform a full reboot of the system. 340 */ rebootSystem(String reason)341 void rebootSystem(String reason) { 342 Slog.i(TAG, "Rebooting system because: " + reason); 343 IPowerManager pms = (IPowerManager)ServiceManager.getService(Context.POWER_SERVICE); 344 try { 345 pms.reboot(false, reason, false); 346 } catch (RemoteException ex) { 347 } 348 } 349 evaluateCheckerCompletionLocked()350 private int evaluateCheckerCompletionLocked() { 351 int state = COMPLETED; 352 for (int i=0; i<mHandlerCheckers.size(); i++) { 353 HandlerChecker hc = mHandlerCheckers.get(i); 354 state = Math.max(state, hc.getCompletionStateLocked()); 355 } 356 return state; 357 } 358 getBlockedCheckersLocked()359 private ArrayList<HandlerChecker> getBlockedCheckersLocked() { 360 ArrayList<HandlerChecker> checkers = new ArrayList<HandlerChecker>(); 361 for (int i=0; i<mHandlerCheckers.size(); i++) { 362 HandlerChecker hc = mHandlerCheckers.get(i); 363 if (hc.isOverdueLocked()) { 364 checkers.add(hc); 365 } 366 } 367 return checkers; 368 } 369 describeCheckersLocked(List<HandlerChecker> checkers)370 private String describeCheckersLocked(List<HandlerChecker> checkers) { 371 StringBuilder builder = new StringBuilder(128); 372 for (int i=0; i<checkers.size(); i++) { 373 if (builder.length() > 0) { 374 builder.append(", "); 375 } 376 builder.append(checkers.get(i).describeBlockedStateLocked()); 377 } 378 return builder.toString(); 379 } 380 getInterestingHalPids()381 private ArrayList<Integer> getInterestingHalPids() { 382 try { 383 IServiceManager serviceManager = IServiceManager.getService(); 384 ArrayList<IServiceManager.InstanceDebugInfo> dump = 385 serviceManager.debugDump(); 386 HashSet<Integer> pids = new HashSet<>(); 387 for (IServiceManager.InstanceDebugInfo info : dump) { 388 if (info.pid == IServiceManager.PidConstant.NO_PID) { 389 continue; 390 } 391 392 if (!HAL_INTERFACES_OF_INTEREST.contains(info.interfaceName)) { 393 continue; 394 } 395 396 pids.add(info.pid); 397 } 398 return new ArrayList<Integer>(pids); 399 } catch (RemoteException e) { 400 return new ArrayList<Integer>(); 401 } 402 } 403 getInterestingNativePids()404 private ArrayList<Integer> getInterestingNativePids() { 405 ArrayList<Integer> pids = getInterestingHalPids(); 406 407 int[] nativePids = Process.getPidsForCommands(NATIVE_STACKS_OF_INTEREST); 408 if (nativePids != null) { 409 pids.ensureCapacity(pids.size() + nativePids.length); 410 for (int i : nativePids) { 411 pids.add(i); 412 } 413 } 414 415 return pids; 416 } 417 418 @Override run()419 public void run() { 420 boolean waitedHalf = false; 421 while (true) { 422 final List<HandlerChecker> blockedCheckers; 423 final String subject; 424 final boolean allowRestart; 425 int debuggerWasConnected = 0; 426 synchronized (this) { 427 long timeout = CHECK_INTERVAL; 428 // Make sure we (re)spin the checkers that have become idle within 429 // this wait-and-check interval 430 for (int i=0; i<mHandlerCheckers.size(); i++) { 431 HandlerChecker hc = mHandlerCheckers.get(i); 432 hc.scheduleCheckLocked(); 433 } 434 435 if (debuggerWasConnected > 0) { 436 debuggerWasConnected--; 437 } 438 439 // NOTE: We use uptimeMillis() here because we do not want to increment the time we 440 // wait while asleep. If the device is asleep then the thing that we are waiting 441 // to timeout on is asleep as well and won't have a chance to run, causing a false 442 // positive on when to kill things. 443 long start = SystemClock.uptimeMillis(); 444 while (timeout > 0) { 445 if (Debug.isDebuggerConnected()) { 446 debuggerWasConnected = 2; 447 } 448 try { 449 wait(timeout); 450 } catch (InterruptedException e) { 451 Log.wtf(TAG, e); 452 } 453 if (Debug.isDebuggerConnected()) { 454 debuggerWasConnected = 2; 455 } 456 timeout = CHECK_INTERVAL - (SystemClock.uptimeMillis() - start); 457 } 458 459 boolean fdLimitTriggered = false; 460 if (mOpenFdMonitor != null) { 461 fdLimitTriggered = mOpenFdMonitor.monitor(); 462 } 463 464 if (!fdLimitTriggered) { 465 final int waitState = evaluateCheckerCompletionLocked(); 466 if (waitState == COMPLETED) { 467 // The monitors have returned; reset 468 waitedHalf = false; 469 continue; 470 } else if (waitState == WAITING) { 471 // still waiting but within their configured intervals; back off and recheck 472 continue; 473 } else if (waitState == WAITED_HALF) { 474 if (!waitedHalf) { 475 // We've waited half the deadlock-detection interval. Pull a stack 476 // trace and wait another half. 477 ArrayList<Integer> pids = new ArrayList<Integer>(); 478 pids.add(Process.myPid()); 479 ActivityManagerService.dumpStackTraces(true, pids, null, null, 480 getInterestingNativePids()); 481 waitedHalf = true; 482 } 483 continue; 484 } 485 486 // something is overdue! 487 blockedCheckers = getBlockedCheckersLocked(); 488 subject = describeCheckersLocked(blockedCheckers); 489 } else { 490 blockedCheckers = Collections.emptyList(); 491 subject = "Open FD high water mark reached"; 492 } 493 allowRestart = mAllowRestart; 494 } 495 496 // If we got here, that means that the system is most likely hung. 497 // First collect stack traces from all threads of the system process. 498 // Then kill this process so that the system will restart. 499 EventLog.writeEvent(EventLogTags.WATCHDOG, subject); 500 501 ArrayList<Integer> pids = new ArrayList<>(); 502 pids.add(Process.myPid()); 503 if (mPhonePid > 0) pids.add(mPhonePid); 504 // Pass !waitedHalf so that just in case we somehow wind up here without having 505 // dumped the halfway stacks, we properly re-initialize the trace file. 506 final File stack = ActivityManagerService.dumpStackTraces( 507 !waitedHalf, pids, null, null, getInterestingNativePids()); 508 509 // Give some extra time to make sure the stack traces get written. 510 // The system's been hanging for a minute, another second or two won't hurt much. 511 SystemClock.sleep(2000); 512 513 // Trigger the kernel to dump all blocked threads, and backtraces on all CPUs to the kernel log 514 doSysRq('w'); 515 doSysRq('l'); 516 517 // Try to add the error to the dropbox, but assuming that the ActivityManager 518 // itself may be deadlocked. (which has happened, causing this statement to 519 // deadlock and the watchdog as a whole to be ineffective) 520 Thread dropboxThread = new Thread("watchdogWriteToDropbox") { 521 public void run() { 522 mActivity.addErrorToDropBox( 523 "watchdog", null, "system_server", null, null, 524 subject, null, stack, null); 525 } 526 }; 527 dropboxThread.start(); 528 try { 529 dropboxThread.join(2000); // wait up to 2 seconds for it to return. 530 } catch (InterruptedException ignored) {} 531 532 IActivityController controller; 533 synchronized (this) { 534 controller = mController; 535 } 536 if (controller != null) { 537 Slog.i(TAG, "Reporting stuck state to activity controller"); 538 try { 539 Binder.setDumpDisabled("Service dumps disabled due to hung system process."); 540 // 1 = keep waiting, -1 = kill system 541 int res = controller.systemNotResponding(subject); 542 if (res >= 0) { 543 Slog.i(TAG, "Activity controller requested to coninue to wait"); 544 waitedHalf = false; 545 continue; 546 } 547 } catch (RemoteException e) { 548 } 549 } 550 551 // Only kill the process if the debugger is not attached. 552 if (Debug.isDebuggerConnected()) { 553 debuggerWasConnected = 2; 554 } 555 if (debuggerWasConnected >= 2) { 556 Slog.w(TAG, "Debugger connected: Watchdog is *not* killing the system process"); 557 } else if (debuggerWasConnected > 0) { 558 Slog.w(TAG, "Debugger was connected: Watchdog is *not* killing the system process"); 559 } else if (!allowRestart) { 560 Slog.w(TAG, "Restart not allowed: Watchdog is *not* killing the system process"); 561 } else { 562 Slog.w(TAG, "*** WATCHDOG KILLING SYSTEM PROCESS: " + subject); 563 WatchdogDiagnostics.diagnoseCheckers(blockedCheckers); 564 Slog.w(TAG, "*** GOODBYE!"); 565 Process.killProcess(Process.myPid()); 566 System.exit(10); 567 } 568 569 waitedHalf = false; 570 } 571 } 572 doSysRq(char c)573 private void doSysRq(char c) { 574 try { 575 FileWriter sysrq_trigger = new FileWriter("/proc/sysrq-trigger"); 576 sysrq_trigger.write(c); 577 sysrq_trigger.close(); 578 } catch (IOException e) { 579 Slog.w(TAG, "Failed to write to /proc/sysrq-trigger", e); 580 } 581 } 582 583 public static final class OpenFdMonitor { 584 /** 585 * Number of FDs below the soft limit that we trigger a runtime restart at. This was 586 * chosen arbitrarily, but will need to be at least 6 in order to have a sufficient number 587 * of FDs in reserve to complete a dump. 588 */ 589 private static final int FD_HIGH_WATER_MARK = 12; 590 591 private final File mDumpDir; 592 private final File mFdHighWaterMark; 593 create()594 public static OpenFdMonitor create() { 595 // Only run the FD monitor on debuggable builds (such as userdebug and eng builds). 596 if (!Build.IS_DEBUGGABLE) { 597 return null; 598 } 599 600 // Don't run the FD monitor on builds that have a global ANR trace file. We're using 601 // the ANR trace directory as a quick hack in order to get these traces in bugreports 602 // and we wouldn't want to overwrite something important. 603 final String dumpDirStr = SystemProperties.get("dalvik.vm.stack-trace-dir", ""); 604 if (dumpDirStr.isEmpty()) { 605 return null; 606 } 607 608 final StructRlimit rlimit; 609 try { 610 rlimit = android.system.Os.getrlimit(OsConstants.RLIMIT_NOFILE); 611 } catch (ErrnoException errno) { 612 Slog.w(TAG, "Error thrown from getrlimit(RLIMIT_NOFILE)", errno); 613 return null; 614 } 615 616 // The assumption we're making here is that FD numbers are allocated (more or less) 617 // sequentially, which is currently (and historically) true since open is currently 618 // specified to always return the lowest-numbered non-open file descriptor for the 619 // current process. 620 // 621 // We do this to avoid having to enumerate the contents of /proc/self/fd in order to 622 // count the number of descriptors open in the process. 623 final File fdThreshold = new File("/proc/self/fd/" + (rlimit.rlim_cur - FD_HIGH_WATER_MARK)); 624 return new OpenFdMonitor(new File(dumpDirStr), fdThreshold); 625 } 626 OpenFdMonitor(File dumpDir, File fdThreshold)627 OpenFdMonitor(File dumpDir, File fdThreshold) { 628 mDumpDir = dumpDir; 629 mFdHighWaterMark = fdThreshold; 630 } 631 dumpOpenDescriptors()632 private void dumpOpenDescriptors() { 633 try { 634 File dumpFile = File.createTempFile("anr_fd_", "", mDumpDir); 635 java.lang.Process proc = new ProcessBuilder() 636 .command("/system/bin/lsof", "-p", String.valueOf(Process.myPid())) 637 .redirectErrorStream(true) 638 .redirectOutput(dumpFile) 639 .start(); 640 641 int returnCode = proc.waitFor(); 642 if (returnCode != 0) { 643 Slog.w(TAG, "Unable to dump open descriptors, lsof return code: " 644 + returnCode); 645 dumpFile.delete(); 646 } 647 } catch (IOException | InterruptedException ex) { 648 Slog.w(TAG, "Unable to dump open descriptors: " + ex); 649 } 650 } 651 652 /** 653 * @return {@code true} if the high water mark was breached and a dump was written, 654 * {@code false} otherwise. 655 */ monitor()656 public boolean monitor() { 657 if (mFdHighWaterMark.exists()) { 658 dumpOpenDescriptors(); 659 return true; 660 } 661 662 return false; 663 } 664 } 665 } 666