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.pm; 18 19 import android.annotation.AppIdInt; 20 import android.annotation.NonNull; 21 import android.annotation.Nullable; 22 import android.annotation.UserIdInt; 23 import android.content.Context; 24 import android.content.pm.PackageStats; 25 import android.os.Build; 26 import android.os.IBinder; 27 import android.os.IBinder.DeathRecipient; 28 import android.os.IInstalld; 29 import android.os.RemoteException; 30 import android.os.ServiceManager; 31 import android.text.format.DateUtils; 32 import android.util.Slog; 33 34 import com.android.internal.os.BackgroundThread; 35 import com.android.server.SystemService; 36 37 import dalvik.system.VMRuntime; 38 39 import java.io.FileDescriptor; 40 41 public class Installer extends SystemService { 42 private static final String TAG = "Installer"; 43 44 /* *************************************************************************** 45 * IMPORTANT: These values are passed to native code. Keep them in sync with 46 * frameworks/native/cmds/installd/installd.h 47 * **************************************************************************/ 48 /** Application should be visible to everyone */ 49 public static final int DEXOPT_PUBLIC = 1 << 1; 50 /** Application wants to allow debugging of its code */ 51 public static final int DEXOPT_DEBUGGABLE = 1 << 2; 52 /** The system boot has finished */ 53 public static final int DEXOPT_BOOTCOMPLETE = 1 << 3; 54 /** Hint that the dexopt type is profile-guided. */ 55 public static final int DEXOPT_PROFILE_GUIDED = 1 << 4; 56 /** The compilation is for a secondary dex file. */ 57 public static final int DEXOPT_SECONDARY_DEX = 1 << 5; 58 /** Ignore the result of dexoptNeeded and force compilation. */ 59 public static final int DEXOPT_FORCE = 1 << 6; 60 /** Indicates that the dex file passed to dexopt in on CE storage. */ 61 public static final int DEXOPT_STORAGE_CE = 1 << 7; 62 /** Indicates that the dex file passed to dexopt in on DE storage. */ 63 public static final int DEXOPT_STORAGE_DE = 1 << 8; 64 /** Indicates that dexopt is invoked from the background service. */ 65 public static final int DEXOPT_IDLE_BACKGROUND_JOB = 1 << 9; 66 /** Indicates that dexopt should restrict access to private APIs. */ 67 public static final int DEXOPT_ENABLE_HIDDEN_API_CHECKS = 1 << 10; 68 /** Indicates that dexopt should convert to CompactDex. */ 69 public static final int DEXOPT_GENERATE_COMPACT_DEX = 1 << 11; 70 /** Indicates that dexopt should generate an app image */ 71 public static final int DEXOPT_GENERATE_APP_IMAGE = 1 << 12; 72 73 // NOTE: keep in sync with installd 74 public static final int FLAG_CLEAR_CACHE_ONLY = 1 << 8; 75 public static final int FLAG_CLEAR_CODE_CACHE_ONLY = 1 << 9; 76 public static final int FLAG_USE_QUOTA = 1 << 12; 77 public static final int FLAG_FREE_CACHE_V2 = 1 << 13; 78 public static final int FLAG_FREE_CACHE_V2_DEFY_QUOTA = 1 << 14; 79 public static final int FLAG_FREE_CACHE_NOOP = 1 << 15; 80 public static final int FLAG_FORCE = 1 << 16; 81 82 private final boolean mIsolated; 83 84 private volatile IInstalld mInstalld; 85 private volatile Object mWarnIfHeld; 86 Installer(Context context)87 public Installer(Context context) { 88 this(context, false); 89 } 90 91 /** 92 * @param isolated indicates if this object should <em>not</em> connect to 93 * the real {@code installd}. All remote calls will be ignored 94 * unless you extend this class and intercept them. 95 */ Installer(Context context, boolean isolated)96 public Installer(Context context, boolean isolated) { 97 super(context); 98 mIsolated = isolated; 99 } 100 101 /** 102 * Yell loudly if someone tries making future calls while holding a lock on 103 * the given object. 104 */ setWarnIfHeld(Object warnIfHeld)105 public void setWarnIfHeld(Object warnIfHeld) { 106 mWarnIfHeld = warnIfHeld; 107 } 108 109 @Override onStart()110 public void onStart() { 111 if (mIsolated) { 112 mInstalld = null; 113 } else { 114 connect(); 115 } 116 } 117 connect()118 private void connect() { 119 IBinder binder = ServiceManager.getService("installd"); 120 if (binder != null) { 121 try { 122 binder.linkToDeath(new DeathRecipient() { 123 @Override 124 public void binderDied() { 125 Slog.w(TAG, "installd died; reconnecting"); 126 connect(); 127 } 128 }, 0); 129 } catch (RemoteException e) { 130 binder = null; 131 } 132 } 133 134 if (binder != null) { 135 mInstalld = IInstalld.Stub.asInterface(binder); 136 try { 137 invalidateMounts(); 138 } catch (InstallerException ignored) { 139 } 140 } else { 141 Slog.w(TAG, "installd not found; trying again"); 142 BackgroundThread.getHandler().postDelayed(() -> { 143 connect(); 144 }, DateUtils.SECOND_IN_MILLIS); 145 } 146 } 147 148 /** 149 * Do several pre-flight checks before making a remote call. 150 * 151 * @return if the remote call should continue. 152 */ checkBeforeRemote()153 private boolean checkBeforeRemote() { 154 if (mWarnIfHeld != null && Thread.holdsLock(mWarnIfHeld)) { 155 Slog.wtf(TAG, "Calling thread " + Thread.currentThread().getName() + " is holding 0x" 156 + Integer.toHexString(System.identityHashCode(mWarnIfHeld)), new Throwable()); 157 } 158 if (mIsolated) { 159 Slog.i(TAG, "Ignoring request because this installer is isolated"); 160 return false; 161 } else { 162 return true; 163 } 164 } 165 createAppData(String uuid, String packageName, int userId, int flags, int appId, String seInfo, int targetSdkVersion)166 public long createAppData(String uuid, String packageName, int userId, int flags, int appId, 167 String seInfo, int targetSdkVersion) throws InstallerException { 168 if (!checkBeforeRemote()) return -1; 169 try { 170 return mInstalld.createAppData(uuid, packageName, userId, flags, appId, seInfo, 171 targetSdkVersion); 172 } catch (Exception e) { 173 throw InstallerException.from(e); 174 } 175 } 176 restoreconAppData(String uuid, String packageName, int userId, int flags, int appId, String seInfo)177 public void restoreconAppData(String uuid, String packageName, int userId, int flags, int appId, 178 String seInfo) throws InstallerException { 179 if (!checkBeforeRemote()) return; 180 try { 181 mInstalld.restoreconAppData(uuid, packageName, userId, flags, appId, seInfo); 182 } catch (Exception e) { 183 throw InstallerException.from(e); 184 } 185 } 186 migrateAppData(String uuid, String packageName, int userId, int flags)187 public void migrateAppData(String uuid, String packageName, int userId, int flags) 188 throws InstallerException { 189 if (!checkBeforeRemote()) return; 190 try { 191 mInstalld.migrateAppData(uuid, packageName, userId, flags); 192 } catch (Exception e) { 193 throw InstallerException.from(e); 194 } 195 } 196 clearAppData(String uuid, String packageName, int userId, int flags, long ceDataInode)197 public void clearAppData(String uuid, String packageName, int userId, int flags, 198 long ceDataInode) throws InstallerException { 199 if (!checkBeforeRemote()) return; 200 try { 201 mInstalld.clearAppData(uuid, packageName, userId, flags, ceDataInode); 202 } catch (Exception e) { 203 throw InstallerException.from(e); 204 } 205 } 206 destroyAppData(String uuid, String packageName, int userId, int flags, long ceDataInode)207 public void destroyAppData(String uuid, String packageName, int userId, int flags, 208 long ceDataInode) throws InstallerException { 209 if (!checkBeforeRemote()) return; 210 try { 211 mInstalld.destroyAppData(uuid, packageName, userId, flags, ceDataInode); 212 } catch (Exception e) { 213 throw InstallerException.from(e); 214 } 215 } 216 fixupAppData(String uuid, int flags)217 public void fixupAppData(String uuid, int flags) throws InstallerException { 218 if (!checkBeforeRemote()) return; 219 try { 220 mInstalld.fixupAppData(uuid, flags); 221 } catch (Exception e) { 222 throw InstallerException.from(e); 223 } 224 } 225 moveCompleteApp(String fromUuid, String toUuid, String packageName, String dataAppName, int appId, String seInfo, int targetSdkVersion)226 public void moveCompleteApp(String fromUuid, String toUuid, String packageName, 227 String dataAppName, int appId, String seInfo, int targetSdkVersion) 228 throws InstallerException { 229 if (!checkBeforeRemote()) return; 230 try { 231 mInstalld.moveCompleteApp(fromUuid, toUuid, packageName, dataAppName, appId, seInfo, 232 targetSdkVersion); 233 } catch (Exception e) { 234 throw InstallerException.from(e); 235 } 236 } 237 getAppSize(String uuid, String[] packageNames, int userId, int flags, int appId, long[] ceDataInodes, String[] codePaths, PackageStats stats)238 public void getAppSize(String uuid, String[] packageNames, int userId, int flags, int appId, 239 long[] ceDataInodes, String[] codePaths, PackageStats stats) 240 throws InstallerException { 241 if (!checkBeforeRemote()) return; 242 try { 243 final long[] res = mInstalld.getAppSize(uuid, packageNames, userId, flags, 244 appId, ceDataInodes, codePaths); 245 stats.codeSize += res[0]; 246 stats.dataSize += res[1]; 247 stats.cacheSize += res[2]; 248 stats.externalCodeSize += res[3]; 249 stats.externalDataSize += res[4]; 250 stats.externalCacheSize += res[5]; 251 } catch (Exception e) { 252 throw InstallerException.from(e); 253 } 254 } 255 getUserSize(String uuid, int userId, int flags, int[] appIds, PackageStats stats)256 public void getUserSize(String uuid, int userId, int flags, int[] appIds, PackageStats stats) 257 throws InstallerException { 258 if (!checkBeforeRemote()) return; 259 try { 260 final long[] res = mInstalld.getUserSize(uuid, userId, flags, appIds); 261 stats.codeSize += res[0]; 262 stats.dataSize += res[1]; 263 stats.cacheSize += res[2]; 264 stats.externalCodeSize += res[3]; 265 stats.externalDataSize += res[4]; 266 stats.externalCacheSize += res[5]; 267 } catch (Exception e) { 268 throw InstallerException.from(e); 269 } 270 } 271 getExternalSize(String uuid, int userId, int flags, int[] appIds)272 public long[] getExternalSize(String uuid, int userId, int flags, int[] appIds) 273 throws InstallerException { 274 if (!checkBeforeRemote()) return new long[6]; 275 try { 276 return mInstalld.getExternalSize(uuid, userId, flags, appIds); 277 } catch (Exception e) { 278 throw InstallerException.from(e); 279 } 280 } 281 setAppQuota(String uuid, int userId, int appId, long cacheQuota)282 public void setAppQuota(String uuid, int userId, int appId, long cacheQuota) 283 throws InstallerException { 284 if (!checkBeforeRemote()) return; 285 try { 286 mInstalld.setAppQuota(uuid, userId, appId, cacheQuota); 287 } catch (Exception e) { 288 throw InstallerException.from(e); 289 } 290 } 291 dexopt(String apkPath, int uid, @Nullable String pkgName, String instructionSet, int dexoptNeeded, @Nullable String outputPath, int dexFlags, String compilerFilter, @Nullable String volumeUuid, @Nullable String sharedLibraries, @Nullable String seInfo, boolean downgrade, int targetSdkVersion, @Nullable String profileName, @Nullable String dexMetadataPath, @Nullable String compilationReason)292 public void dexopt(String apkPath, int uid, @Nullable String pkgName, String instructionSet, 293 int dexoptNeeded, @Nullable String outputPath, int dexFlags, 294 String compilerFilter, @Nullable String volumeUuid, @Nullable String sharedLibraries, 295 @Nullable String seInfo, boolean downgrade, int targetSdkVersion, 296 @Nullable String profileName, @Nullable String dexMetadataPath, 297 @Nullable String compilationReason) throws InstallerException { 298 assertValidInstructionSet(instructionSet); 299 if (!checkBeforeRemote()) return; 300 try { 301 mInstalld.dexopt(apkPath, uid, pkgName, instructionSet, dexoptNeeded, outputPath, 302 dexFlags, compilerFilter, volumeUuid, sharedLibraries, seInfo, downgrade, 303 targetSdkVersion, profileName, dexMetadataPath, compilationReason); 304 } catch (Exception e) { 305 throw InstallerException.from(e); 306 } 307 } 308 mergeProfiles(int uid, String packageName, String profileName)309 public boolean mergeProfiles(int uid, String packageName, String profileName) 310 throws InstallerException { 311 if (!checkBeforeRemote()) return false; 312 try { 313 return mInstalld.mergeProfiles(uid, packageName, profileName); 314 } catch (Exception e) { 315 throw InstallerException.from(e); 316 } 317 } 318 dumpProfiles(int uid, String packageName, String profileName, String codePath)319 public boolean dumpProfiles(int uid, String packageName, String profileName, String codePath) 320 throws InstallerException { 321 if (!checkBeforeRemote()) return false; 322 try { 323 return mInstalld.dumpProfiles(uid, packageName, profileName, codePath); 324 } catch (Exception e) { 325 throw InstallerException.from(e); 326 } 327 } 328 copySystemProfile(String systemProfile, int uid, String packageName, String profileName)329 public boolean copySystemProfile(String systemProfile, int uid, String packageName, 330 String profileName) throws InstallerException { 331 if (!checkBeforeRemote()) return false; 332 try { 333 return mInstalld.copySystemProfile(systemProfile, uid, packageName, profileName); 334 } catch (Exception e) { 335 throw InstallerException.from(e); 336 } 337 } 338 idmap(String targetApkPath, String overlayApkPath, int uid)339 public void idmap(String targetApkPath, String overlayApkPath, int uid) 340 throws InstallerException { 341 if (!checkBeforeRemote()) return; 342 try { 343 mInstalld.idmap(targetApkPath, overlayApkPath, uid); 344 } catch (Exception e) { 345 throw InstallerException.from(e); 346 } 347 } 348 removeIdmap(String overlayApkPath)349 public void removeIdmap(String overlayApkPath) throws InstallerException { 350 if (!checkBeforeRemote()) return; 351 try { 352 mInstalld.removeIdmap(overlayApkPath); 353 } catch (Exception e) { 354 throw InstallerException.from(e); 355 } 356 } 357 rmdex(String codePath, String instructionSet)358 public void rmdex(String codePath, String instructionSet) throws InstallerException { 359 assertValidInstructionSet(instructionSet); 360 if (!checkBeforeRemote()) return; 361 try { 362 mInstalld.rmdex(codePath, instructionSet); 363 } catch (Exception e) { 364 throw InstallerException.from(e); 365 } 366 } 367 rmPackageDir(String packageDir)368 public void rmPackageDir(String packageDir) throws InstallerException { 369 if (!checkBeforeRemote()) return; 370 try { 371 mInstalld.rmPackageDir(packageDir); 372 } catch (Exception e) { 373 throw InstallerException.from(e); 374 } 375 } 376 clearAppProfiles(String packageName, String profileName)377 public void clearAppProfiles(String packageName, String profileName) throws InstallerException { 378 if (!checkBeforeRemote()) return; 379 try { 380 mInstalld.clearAppProfiles(packageName, profileName); 381 } catch (Exception e) { 382 throw InstallerException.from(e); 383 } 384 } 385 destroyAppProfiles(String packageName)386 public void destroyAppProfiles(String packageName) throws InstallerException { 387 if (!checkBeforeRemote()) return; 388 try { 389 mInstalld.destroyAppProfiles(packageName); 390 } catch (Exception e) { 391 throw InstallerException.from(e); 392 } 393 } 394 createUserData(String uuid, int userId, int userSerial, int flags)395 public void createUserData(String uuid, int userId, int userSerial, int flags) 396 throws InstallerException { 397 if (!checkBeforeRemote()) return; 398 try { 399 mInstalld.createUserData(uuid, userId, userSerial, flags); 400 } catch (Exception e) { 401 throw InstallerException.from(e); 402 } 403 } 404 destroyUserData(String uuid, int userId, int flags)405 public void destroyUserData(String uuid, int userId, int flags) throws InstallerException { 406 if (!checkBeforeRemote()) return; 407 try { 408 mInstalld.destroyUserData(uuid, userId, flags); 409 } catch (Exception e) { 410 throw InstallerException.from(e); 411 } 412 } 413 markBootComplete(String instructionSet)414 public void markBootComplete(String instructionSet) throws InstallerException { 415 assertValidInstructionSet(instructionSet); 416 if (!checkBeforeRemote()) return; 417 try { 418 mInstalld.markBootComplete(instructionSet); 419 } catch (Exception e) { 420 throw InstallerException.from(e); 421 } 422 } 423 freeCache(String uuid, long targetFreeBytes, long cacheReservedBytes, int flags)424 public void freeCache(String uuid, long targetFreeBytes, long cacheReservedBytes, int flags) 425 throws InstallerException { 426 if (!checkBeforeRemote()) return; 427 try { 428 mInstalld.freeCache(uuid, targetFreeBytes, cacheReservedBytes, flags); 429 } catch (Exception e) { 430 throw InstallerException.from(e); 431 } 432 } 433 434 /** 435 * Links the 32 bit native library directory in an application's data 436 * directory to the real location for backward compatibility. Note that no 437 * such symlink is created for 64 bit shared libraries. 438 */ linkNativeLibraryDirectory(String uuid, String packageName, String nativeLibPath32, int userId)439 public void linkNativeLibraryDirectory(String uuid, String packageName, String nativeLibPath32, 440 int userId) throws InstallerException { 441 if (!checkBeforeRemote()) return; 442 try { 443 mInstalld.linkNativeLibraryDirectory(uuid, packageName, nativeLibPath32, userId); 444 } catch (Exception e) { 445 throw InstallerException.from(e); 446 } 447 } 448 createOatDir(String oatDir, String dexInstructionSet)449 public void createOatDir(String oatDir, String dexInstructionSet) 450 throws InstallerException { 451 if (!checkBeforeRemote()) return; 452 try { 453 mInstalld.createOatDir(oatDir, dexInstructionSet); 454 } catch (Exception e) { 455 throw InstallerException.from(e); 456 } 457 } 458 linkFile(String relativePath, String fromBase, String toBase)459 public void linkFile(String relativePath, String fromBase, String toBase) 460 throws InstallerException { 461 if (!checkBeforeRemote()) return; 462 try { 463 mInstalld.linkFile(relativePath, fromBase, toBase); 464 } catch (Exception e) { 465 throw InstallerException.from(e); 466 } 467 } 468 moveAb(String apkPath, String instructionSet, String outputPath)469 public void moveAb(String apkPath, String instructionSet, String outputPath) 470 throws InstallerException { 471 if (!checkBeforeRemote()) return; 472 try { 473 mInstalld.moveAb(apkPath, instructionSet, outputPath); 474 } catch (Exception e) { 475 throw InstallerException.from(e); 476 } 477 } 478 deleteOdex(String apkPath, String instructionSet, String outputPath)479 public void deleteOdex(String apkPath, String instructionSet, String outputPath) 480 throws InstallerException { 481 if (!checkBeforeRemote()) return; 482 try { 483 mInstalld.deleteOdex(apkPath, instructionSet, outputPath); 484 } catch (Exception e) { 485 throw InstallerException.from(e); 486 } 487 } 488 installApkVerity(String filePath, FileDescriptor verityInput, int contentSize)489 public void installApkVerity(String filePath, FileDescriptor verityInput, int contentSize) 490 throws InstallerException { 491 if (!checkBeforeRemote()) return; 492 try { 493 mInstalld.installApkVerity(filePath, verityInput, contentSize); 494 } catch (Exception e) { 495 throw InstallerException.from(e); 496 } 497 } 498 assertFsverityRootHashMatches(String filePath, @NonNull byte[] expectedHash)499 public void assertFsverityRootHashMatches(String filePath, @NonNull byte[] expectedHash) 500 throws InstallerException { 501 if (!checkBeforeRemote()) return; 502 try { 503 mInstalld.assertFsverityRootHashMatches(filePath, expectedHash); 504 } catch (Exception e) { 505 throw InstallerException.from(e); 506 } 507 } 508 reconcileSecondaryDexFile(String apkPath, String packageName, int uid, String[] isas, @Nullable String volumeUuid, int flags)509 public boolean reconcileSecondaryDexFile(String apkPath, String packageName, int uid, 510 String[] isas, @Nullable String volumeUuid, int flags) throws InstallerException { 511 for (int i = 0; i < isas.length; i++) { 512 assertValidInstructionSet(isas[i]); 513 } 514 if (!checkBeforeRemote()) return false; 515 try { 516 return mInstalld.reconcileSecondaryDexFile(apkPath, packageName, uid, isas, 517 volumeUuid, flags); 518 } catch (Exception e) { 519 throw InstallerException.from(e); 520 } 521 } 522 hashSecondaryDexFile(String dexPath, String packageName, int uid, @Nullable String volumeUuid, int flags)523 public byte[] hashSecondaryDexFile(String dexPath, String packageName, int uid, 524 @Nullable String volumeUuid, int flags) throws InstallerException { 525 if (!checkBeforeRemote()) return new byte[0]; 526 try { 527 return mInstalld.hashSecondaryDexFile(dexPath, packageName, uid, volumeUuid, flags); 528 } catch (Exception e) { 529 throw InstallerException.from(e); 530 } 531 } 532 createProfileSnapshot(int appId, String packageName, String profileName, String classpath)533 public boolean createProfileSnapshot(int appId, String packageName, String profileName, 534 String classpath) throws InstallerException { 535 if (!checkBeforeRemote()) return false; 536 try { 537 return mInstalld.createProfileSnapshot(appId, packageName, profileName, classpath); 538 } catch (Exception e) { 539 throw InstallerException.from(e); 540 } 541 } 542 destroyProfileSnapshot(String packageName, String profileName)543 public void destroyProfileSnapshot(String packageName, String profileName) 544 throws InstallerException { 545 if (!checkBeforeRemote()) return; 546 try { 547 mInstalld.destroyProfileSnapshot(packageName, profileName); 548 } catch (Exception e) { 549 throw InstallerException.from(e); 550 } 551 } 552 invalidateMounts()553 public void invalidateMounts() throws InstallerException { 554 if (!checkBeforeRemote()) return; 555 try { 556 mInstalld.invalidateMounts(); 557 } catch (Exception e) { 558 throw InstallerException.from(e); 559 } 560 } 561 isQuotaSupported(String volumeUuid)562 public boolean isQuotaSupported(String volumeUuid) throws InstallerException { 563 if (!checkBeforeRemote()) return false; 564 try { 565 return mInstalld.isQuotaSupported(volumeUuid); 566 } catch (Exception e) { 567 throw InstallerException.from(e); 568 } 569 } 570 prepareAppProfile(String pkg, @UserIdInt int userId, @AppIdInt int appId, String profileName, String codePath, String dexMetadataPath)571 public boolean prepareAppProfile(String pkg, @UserIdInt int userId, @AppIdInt int appId, 572 String profileName, String codePath, String dexMetadataPath) throws InstallerException { 573 if (!checkBeforeRemote()) return false; 574 try { 575 return mInstalld.prepareAppProfile(pkg, userId, appId, profileName, codePath, 576 dexMetadataPath); 577 } catch (Exception e) { 578 throw InstallerException.from(e); 579 } 580 } 581 assertValidInstructionSet(String instructionSet)582 private static void assertValidInstructionSet(String instructionSet) 583 throws InstallerException { 584 for (String abi : Build.SUPPORTED_ABIS) { 585 if (VMRuntime.getInstructionSet(abi).equals(instructionSet)) { 586 return; 587 } 588 } 589 throw new InstallerException("Invalid instruction set: " + instructionSet); 590 } 591 592 public static class InstallerException extends Exception { InstallerException(String detailMessage)593 public InstallerException(String detailMessage) { 594 super(detailMessage); 595 } 596 from(Exception e)597 public static InstallerException from(Exception e) throws InstallerException { 598 throw new InstallerException(e.toString()); 599 } 600 } 601 } 602