1 /* 2 * Copyright (c) 2008, 2013, Oracle and/or its affiliates. All rights reserved. 3 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. 4 * 5 * This code is free software; you can redistribute it and/or modify it 6 * under the terms of the GNU General Public License version 2 only, as 7 * published by the Free Software Foundation. Oracle designates this 8 * particular file as subject to the "Classpath" exception as provided 9 * by Oracle in the LICENSE file that accompanied this code. 10 * 11 * This code is distributed in the hope that it will be useful, but WITHOUT 12 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 13 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License 14 * version 2 for more details (a copy is included in the LICENSE file that 15 * accompanied this code). 16 * 17 * You should have received a copy of the GNU General Public License version 18 * 2 along with this work; if not, write to the Free Software Foundation, 19 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. 20 * 21 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA 22 * or visit www.oracle.com if you need additional information or have any 23 * questions. 24 */ 25 26 package sun.nio.fs; 27 28 import java.nio.file.*; 29 import java.security.AccessController; 30 import java.security.PrivilegedAction; 31 import java.util.*; 32 import java.io.IOException; 33 import jdk.internal.misc.Unsafe; 34 35 import static sun.nio.fs.UnixConstants.*; 36 37 /** 38 * Solaris implementation of WatchService based on file events notification 39 * facility. 40 */ 41 42 class SolarisWatchService 43 extends AbstractWatchService 44 { 45 private static final Unsafe unsafe = Unsafe.getUnsafe(); 46 private static int addressSize = unsafe.addressSize(); 47 48 private static int dependsArch(int value32, int value64) { 49 return (addressSize == 4) ? value32 : value64; 50 } 51 52 /* 53 * typedef struct port_event { 54 * int portev_events; 55 * ushort_t portev_source; 56 * ushort_t portev_pad; 57 * uintptr_t portev_object; 58 * void *portev_user; 59 * } port_event_t; 60 */ 61 private static final int SIZEOF_PORT_EVENT = dependsArch(16, 24); 62 private static final int OFFSETOF_EVENTS = 0; 63 private static final int OFFSETOF_SOURCE = 4; 64 private static final int OFFSETOF_OBJECT = 8; 65 66 /* 67 * typedef struct file_obj { 68 * timestruc_t fo_atime; 69 * timestruc_t fo_mtime; 70 * timestruc_t fo_ctime; 71 * uintptr_t fo_pad[3]; 72 * char *fo_name; 73 * } file_obj_t; 74 */ 75 private static final int SIZEOF_FILEOBJ = dependsArch(40, 80); 76 private static final int OFFSET_FO_NAME = dependsArch(36, 72); 77 78 // port sources 79 private static final short PORT_SOURCE_USER = 3; 80 private static final short PORT_SOURCE_FILE = 7; 81 82 // user-watchable events 83 private static final int FILE_MODIFIED = 0x00000002; 84 private static final int FILE_ATTRIB = 0x00000004; 85 private static final int FILE_NOFOLLOW = 0x10000000; 86 87 // exception events 88 private static final int FILE_DELETE = 0x00000010; 89 private static final int FILE_RENAME_TO = 0x00000020; 90 private static final int FILE_RENAME_FROM = 0x00000040; 91 private static final int UNMOUNTED = 0x20000000; 92 private static final int MOUNTEDOVER = 0x40000000; 93 94 // background thread to read change events 95 private final Poller poller; 96 97 SolarisWatchService(UnixFileSystem fs) throws IOException { 98 int port = -1; 99 try { 100 port = portCreate(); 101 } catch (UnixException x) { 102 throw new IOException(x.errorString()); 103 } 104 105 this.poller = new Poller(fs, this, port); 106 this.poller.start(); 107 } 108 109 @Override 110 WatchKey register(Path dir, 111 WatchEvent.Kind<?>[] events, 112 WatchEvent.Modifier... modifiers) 113 throws IOException 114 { 115 // delegate to poller 116 return poller.register(dir, events, modifiers); 117 } 118 119 @Override 120 void implClose() throws IOException { 121 // delegate to poller 122 poller.close(); 123 } 124 125 /** 126 * WatchKey implementation 127 */ 128 private class SolarisWatchKey extends AbstractWatchKey 129 implements DirectoryNode 130 { 131 private final UnixFileKey fileKey; 132 133 // pointer to native file_obj object 134 private final long object; 135 136 // events (may be changed). set to null when watch key is invalid 137 private volatile Set<? extends WatchEvent.Kind<?>> events; 138 139 // map of entries in directory; created lazily; accessed only by 140 // poller thread. 141 private Map<Path,EntryNode> children = new HashMap<>(); 142 143 SolarisWatchKey(SolarisWatchService watcher, 144 UnixPath dir, 145 UnixFileKey fileKey, 146 long object, 147 Set<? extends WatchEvent.Kind<?>> events) 148 { 149 super(dir, watcher); 150 this.fileKey = fileKey; 151 this.object = object; 152 this.events = events; 153 } 154 155 UnixPath getDirectory() { 156 return (UnixPath)watchable(); 157 } 158 159 UnixFileKey getFileKey() { 160 return fileKey; 161 } 162 163 @Override 164 public long object() { 165 return object; 166 } 167 168 void invalidate() { 169 events = null; 170 } 171 172 Set<? extends WatchEvent.Kind<?>> events() { 173 return events; 174 } 175 176 void setEvents(Set<? extends WatchEvent.Kind<?>> events) { 177 this.events = events; 178 } 179 180 Map<Path,EntryNode> children() { 181 return children; 182 } 183 184 @Override 185 public boolean isValid() { 186 return events != null; 187 } 188 189 @Override 190 public void cancel() { 191 if (isValid()) { 192 // delegate to poller 193 poller.cancel(this); 194 } 195 } 196 197 @Override 198 public void addChild(Path name, EntryNode node) { 199 children.put(name, node); 200 } 201 202 @Override 203 public void removeChild(Path name) { 204 children.remove(name); 205 } 206 207 @Override 208 public EntryNode getChild(Path name) { 209 return children.get(name); 210 } 211 } 212 213 /** 214 * Background thread to read from port 215 */ 216 private class Poller extends AbstractPoller { 217 218 // maximum number of events to read per call to port_getn 219 private static final int MAX_EVENT_COUNT = 128; 220 221 // events that map to ENTRY_DELETE 222 private static final int FILE_REMOVED = 223 (FILE_DELETE|FILE_RENAME_TO|FILE_RENAME_FROM); 224 225 // events that tell us not to re-associate the object 226 private static final int FILE_EXCEPTION = 227 (FILE_REMOVED|UNMOUNTED|MOUNTEDOVER); 228 229 // address of event buffers (used to receive events with port_getn) 230 private final long bufferAddress; 231 232 private final SolarisWatchService watcher; 233 234 // the I/O port 235 private final int port; 236 237 // maps file key (dev/inode) to WatchKey 238 private final Map<UnixFileKey,SolarisWatchKey> fileKey2WatchKey; 239 240 // maps file_obj object to Node 241 private final Map<Long,Node> object2Node; 242 243 /** 244 * Create a new instance 245 */ 246 Poller(UnixFileSystem fs, SolarisWatchService watcher, int port) { 247 this.watcher = watcher; 248 this.port = port; 249 this.bufferAddress = 250 unsafe.allocateMemory(SIZEOF_PORT_EVENT * MAX_EVENT_COUNT); 251 this.fileKey2WatchKey = new HashMap<UnixFileKey,SolarisWatchKey>(); 252 this.object2Node = new HashMap<Long,Node>(); 253 } 254 255 @Override 256 void wakeup() throws IOException { 257 // write to port to wakeup polling thread 258 try { 259 portSend(port, 0); 260 } catch (UnixException x) { 261 throw new IOException(x.errorString()); 262 } 263 } 264 265 @Override 266 Object implRegister(Path obj, 267 Set<? extends WatchEvent.Kind<?>> events, 268 WatchEvent.Modifier... modifiers) 269 { 270 // no modifiers supported at this time 271 if (modifiers.length > 0) { 272 for (WatchEvent.Modifier modifier: modifiers) { 273 if (modifier == null) 274 return new NullPointerException(); 275 if (modifier instanceof com.sun.nio.file.SensitivityWatchEventModifier) 276 continue; // ignore 277 return new UnsupportedOperationException("Modifier not supported"); 278 } 279 } 280 281 UnixPath dir = (UnixPath)obj; 282 283 // check file is directory 284 UnixFileAttributes attrs = null; 285 try { 286 attrs = UnixFileAttributes.get(dir, true); 287 } catch (UnixException x) { 288 return x.asIOException(dir); 289 } 290 if (!attrs.isDirectory()) { 291 return new NotDirectoryException(dir.getPathForExceptionMessage()); 292 } 293 294 // if already registered then update the events and return existing key 295 UnixFileKey fileKey = attrs.fileKey(); 296 SolarisWatchKey watchKey = fileKey2WatchKey.get(fileKey); 297 if (watchKey != null) { 298 try { 299 updateEvents(watchKey, events); 300 } catch (UnixException x) { 301 return x.asIOException(dir); 302 } 303 return watchKey; 304 } 305 306 // register directory 307 long object = 0L; 308 try { 309 object = registerImpl(dir, (FILE_MODIFIED | FILE_ATTRIB)); 310 } catch (UnixException x) { 311 return x.asIOException(dir); 312 } 313 314 // create watch key and insert it into maps 315 watchKey = new SolarisWatchKey(watcher, dir, fileKey, object, events); 316 object2Node.put(object, watchKey); 317 fileKey2WatchKey.put(fileKey, watchKey); 318 319 // register all entries in directory 320 registerChildren(dir, watchKey, false, false); 321 322 return watchKey; 323 } 324 325 // release resources for single entry 326 void releaseChild(EntryNode node) { 327 long object = node.object(); 328 if (object != 0L) { 329 object2Node.remove(object); 330 releaseObject(object, true); 331 node.setObject(0L); 332 } 333 } 334 335 // release resources for entries in directory 336 void releaseChildren(SolarisWatchKey key) { 337 for (EntryNode node: key.children().values()) { 338 releaseChild(node); 339 } 340 } 341 342 // cancel single key 343 @Override 344 void implCancelKey(WatchKey obj) { 345 SolarisWatchKey key = (SolarisWatchKey)obj; 346 if (key.isValid()) { 347 fileKey2WatchKey.remove(key.getFileKey()); 348 349 // release resources for entries 350 releaseChildren(key); 351 352 // release resources for directory 353 long object = key.object(); 354 object2Node.remove(object); 355 releaseObject(object, true); 356 357 // and finally invalidate the key 358 key.invalidate(); 359 } 360 } 361 362 // close watch service 363 @Override 364 void implCloseAll() { 365 // release all native resources 366 for (Long object: object2Node.keySet()) { 367 releaseObject(object, true); 368 } 369 370 // invalidate all keys 371 for (Map.Entry<UnixFileKey,SolarisWatchKey> entry: fileKey2WatchKey.entrySet()) { 372 entry.getValue().invalidate(); 373 } 374 375 // clean-up 376 object2Node.clear(); 377 fileKey2WatchKey.clear(); 378 379 // free global resources 380 unsafe.freeMemory(bufferAddress); 381 UnixNativeDispatcher.close(port); 382 } 383 384 /** 385 * Poller main loop. Blocks on port_getn waiting for events and then 386 * processes them. 387 */ 388 @Override 389 public void run() { 390 try { 391 for (;;) { 392 int n = portGetn(port, bufferAddress, MAX_EVENT_COUNT); 393 assert n > 0; 394 395 long address = bufferAddress; 396 for (int i=0; i<n; i++) { 397 boolean shutdown = processEvent(address); 398 if (shutdown) 399 return; 400 address += SIZEOF_PORT_EVENT; 401 } 402 } 403 } catch (UnixException x) { 404 x.printStackTrace(); 405 } 406 } 407 408 /** 409 * Process a single port_event 410 * 411 * Returns true if poller thread is requested to shutdown. 412 */ 413 boolean processEvent(long address) { 414 // pe->portev_source 415 short source = unsafe.getShort(address + OFFSETOF_SOURCE); 416 // pe->portev_object 417 long object = unsafe.getAddress(address + OFFSETOF_OBJECT); 418 // pe->portev_events 419 int events = unsafe.getInt(address + OFFSETOF_EVENTS); 420 421 // user event is trigger to process pending requests 422 if (source != PORT_SOURCE_FILE) { 423 if (source == PORT_SOURCE_USER) { 424 // process any pending requests 425 boolean shutdown = processRequests(); 426 if (shutdown) 427 return true; 428 } 429 return false; 430 } 431 432 // lookup object to get Node 433 Node node = object2Node.get(object); 434 if (node == null) { 435 // should not happen 436 return false; 437 } 438 439 // As a workaround for 6642290 and 6636438/6636412 we don't use 440 // FILE_EXCEPTION events to tell use not to register the file. 441 // boolean reregister = (events & FILE_EXCEPTION) == 0; 442 boolean reregister = true; 443 444 // If node is EntryNode then event relates to entry in directory 445 // If node is a SolarisWatchKey (DirectoryNode) then event relates 446 // to a watched directory. 447 boolean isDirectory = (node instanceof SolarisWatchKey); 448 if (isDirectory) { 449 processDirectoryEvents((SolarisWatchKey)node, events); 450 } else { 451 boolean ignore = processEntryEvents((EntryNode)node, events); 452 if (ignore) 453 reregister = false; 454 } 455 456 // need to re-associate to get further events 457 if (reregister) { 458 try { 459 events = FILE_MODIFIED | FILE_ATTRIB; 460 if (!isDirectory) events |= FILE_NOFOLLOW; 461 portAssociate(port, 462 PORT_SOURCE_FILE, 463 object, 464 events); 465 } catch (UnixException x) { 466 // unable to re-register 467 reregister = false; 468 } 469 } 470 471 // object is not re-registered so release resources. If 472 // object is a watched directory then signal key 473 if (!reregister) { 474 // release resources 475 object2Node.remove(object); 476 releaseObject(object, false); 477 478 // if watch key then signal it 479 if (isDirectory) { 480 SolarisWatchKey key = (SolarisWatchKey)node; 481 fileKey2WatchKey.remove( key.getFileKey() ); 482 key.invalidate(); 483 key.signal(); 484 } else { 485 // if entry then remove it from parent 486 EntryNode entry = (EntryNode)node; 487 SolarisWatchKey key = (SolarisWatchKey)entry.parent(); 488 key.removeChild(entry.name()); 489 } 490 } 491 492 return false; 493 } 494 495 /** 496 * Process directory events. If directory is modified then re-scan 497 * directory to register any new entries 498 */ 499 void processDirectoryEvents(SolarisWatchKey key, int mask) { 500 if ((mask & (FILE_MODIFIED | FILE_ATTRIB)) != 0) { 501 registerChildren(key.getDirectory(), key, 502 key.events().contains(StandardWatchEventKinds.ENTRY_CREATE), 503 key.events().contains(StandardWatchEventKinds.ENTRY_DELETE)); 504 } 505 } 506 507 /** 508 * Process events for entries in registered directories. Returns {@code 509 * true} if events are ignored because the watch key has been cancelled. 510 */ 511 boolean processEntryEvents(EntryNode node, int mask) { 512 SolarisWatchKey key = (SolarisWatchKey)node.parent(); 513 Set<? extends WatchEvent.Kind<?>> events = key.events(); 514 if (events == null) { 515 // key has been cancelled so ignore event 516 return true; 517 } 518 519 // entry modified 520 if (((mask & (FILE_MODIFIED | FILE_ATTRIB)) != 0) && 521 events.contains(StandardWatchEventKinds.ENTRY_MODIFY)) 522 { 523 key.signalEvent(StandardWatchEventKinds.ENTRY_MODIFY, node.name()); 524 } 525 526 527 return false; 528 } 529 530 /** 531 * Registers all entries in the given directory 532 * 533 * The {@code sendCreateEvents} and {@code sendDeleteEvents} parameters 534 * indicates if ENTRY_CREATE and ENTRY_DELETE events should be queued 535 * when new entries are found. When initially registering a directory 536 * they will always be false. When re-scanning a directory then it 537 * depends on if the events are enabled or not. 538 */ 539 void registerChildren(UnixPath dir, 540 SolarisWatchKey parent, 541 boolean sendCreateEvents, 542 boolean sendDeleteEvents) 543 { 544 boolean isModifyEnabled = 545 parent.events().contains(StandardWatchEventKinds.ENTRY_MODIFY) ; 546 547 // reset visited flag on entries so that we can detect file deletes 548 for (EntryNode node: parent.children().values()) { 549 node.setVisited(false); 550 } 551 552 try (DirectoryStream<Path> stream = Files.newDirectoryStream(dir)) { 553 for (Path entry: stream) { 554 Path name = entry.getFileName(); 555 556 // skip entry if already registered 557 EntryNode node = parent.getChild(name); 558 if (node != null) { 559 node.setVisited(true); 560 continue; 561 } 562 563 // new entry found 564 565 long object = 0L; 566 int errno = 0; 567 boolean addNode = false; 568 569 // if ENTRY_MODIFY enabled then we register the entry for events 570 if (isModifyEnabled) { 571 try { 572 UnixPath path = (UnixPath)entry; 573 int events = (FILE_NOFOLLOW | FILE_MODIFIED | FILE_ATTRIB); 574 object = registerImpl(path, events); 575 addNode = true; 576 } catch (UnixException x) { 577 errno = x.errno(); 578 } 579 } else { 580 addNode = true; 581 } 582 583 if (addNode) { 584 // create node 585 node = new EntryNode(object, (UnixPath)entry.getFileName(), parent); 586 node.setVisited(true); 587 // tell the parent about it 588 parent.addChild(entry.getFileName(), node); 589 if (object != 0L) 590 object2Node.put(object, node); 591 } 592 593 // send ENTRY_CREATE event for the new file 594 // send ENTRY_DELETE event for files that were deleted immediately 595 boolean deleted = (errno == ENOENT); 596 if (sendCreateEvents && (addNode || deleted)) 597 parent.signalEvent(StandardWatchEventKinds.ENTRY_CREATE, name); 598 if (sendDeleteEvents && deleted) 599 parent.signalEvent(StandardWatchEventKinds.ENTRY_DELETE, name); 600 601 } 602 } catch (DirectoryIteratorException | IOException x) { 603 // queue OVERFLOW event so that user knows to re-scan directory 604 parent.signalEvent(StandardWatchEventKinds.OVERFLOW, null); 605 return; 606 } 607 608 // clean-up and send ENTRY_DELETE events for any entries that were 609 // not found 610 Iterator<Map.Entry<Path,EntryNode>> iterator = 611 parent.children().entrySet().iterator(); 612 while (iterator.hasNext()) { 613 Map.Entry<Path,EntryNode> entry = iterator.next(); 614 EntryNode node = entry.getValue(); 615 if (!node.isVisited()) { 616 long object = node.object(); 617 if (object != 0L) { 618 object2Node.remove(object); 619 releaseObject(object, true); 620 } 621 if (sendDeleteEvents) 622 parent.signalEvent(StandardWatchEventKinds.ENTRY_DELETE, node.name()); 623 iterator.remove(); 624 } 625 } 626 } 627 628 /** 629 * Update watch key's events. If ENTRY_MODIFY changes to be enabled 630 * then register each file in the directory; If ENTRY_MODIFY changed to 631 * be disabled then unregister each file. 632 */ 633 void updateEvents(SolarisWatchKey key, Set<? extends WatchEvent.Kind<?>> events) 634 throws UnixException 635 { 636 637 // update events, remembering if ENTRY_MODIFY was previously 638 // enabled or disabled. 639 boolean oldModifyEnabled = key.events() 640 .contains(StandardWatchEventKinds.ENTRY_MODIFY); 641 key.setEvents(events); 642 643 // check if ENTRY_MODIFY has changed 644 boolean newModifyEnabled = events 645 .contains(StandardWatchEventKinds.ENTRY_MODIFY); 646 if (newModifyEnabled != oldModifyEnabled) { 647 UnixException ex = null; 648 for (EntryNode node: key.children().values()) { 649 if (newModifyEnabled) { 650 // register 651 UnixPath path = key.getDirectory().resolve(node.name()); 652 int ev = (FILE_NOFOLLOW | FILE_MODIFIED | FILE_ATTRIB); 653 try { 654 long object = registerImpl(path, ev); 655 object2Node.put(object, node); 656 node.setObject(object); 657 } catch (UnixException x) { 658 // if file has been deleted then it will be detected 659 // as a FILE_MODIFIED event on the directory 660 if (x.errno() != ENOENT) { 661 ex = x; 662 break; 663 } 664 } 665 } else { 666 // unregister 667 releaseChild(node); 668 } 669 } 670 671 // an error occurred 672 if (ex != null) { 673 releaseChildren(key); 674 throw ex; 675 } 676 } 677 } 678 679 /** 680 * Calls port_associate to register the given path. 681 * Returns pointer to fileobj structure that is allocated for 682 * the registration. 683 */ 684 long registerImpl(UnixPath dir, int events) 685 throws UnixException 686 { 687 // allocate memory for the path (file_obj->fo_name field) 688 byte[] path = dir.getByteArrayForSysCalls(); 689 int len = path.length; 690 long name = unsafe.allocateMemory(len+1); 691 unsafe.copyMemory(path, Unsafe.ARRAY_BYTE_BASE_OFFSET, null, 692 name, (long)len); 693 unsafe.putByte(name + len, (byte)0); 694 695 // allocate memory for filedatanode structure - this is the object 696 // to port_associate 697 long object = unsafe.allocateMemory(SIZEOF_FILEOBJ); 698 unsafe.setMemory(null, object, SIZEOF_FILEOBJ, (byte)0); 699 unsafe.putAddress(object + OFFSET_FO_NAME, name); 700 701 // associate the object with the port 702 try { 703 portAssociate(port, 704 PORT_SOURCE_FILE, 705 object, 706 events); 707 } catch (UnixException x) { 708 // debugging 709 if (x.errno() == EAGAIN) { 710 System.err.println("The maximum number of objects associated "+ 711 "with the port has been reached"); 712 } 713 714 unsafe.freeMemory(name); 715 unsafe.freeMemory(object); 716 throw x; 717 } 718 return object; 719 } 720 721 /** 722 * Frees all resources for an file_obj object; optionally remove 723 * association from port 724 */ 725 void releaseObject(long object, boolean dissociate) { 726 // remove association 727 if (dissociate) { 728 try { 729 portDissociate(port, PORT_SOURCE_FILE, object); 730 } catch (UnixException x) { 731 // ignore 732 } 733 } 734 735 // free native memory 736 long name = unsafe.getAddress(object + OFFSET_FO_NAME); 737 unsafe.freeMemory(name); 738 unsafe.freeMemory(object); 739 } 740 } 741 742 /** 743 * A node with native (file_obj) resources 744 */ 745 private static interface Node { 746 long object(); 747 } 748 749 /** 750 * A directory node with a map of the entries in the directory 751 */ 752 private static interface DirectoryNode extends Node { 753 void addChild(Path name, EntryNode node); 754 void removeChild(Path name); 755 EntryNode getChild(Path name); 756 } 757 758 /** 759 * An implementation of a node that is an entry in a directory. 760 */ 761 private static class EntryNode implements Node { 762 private long object; 763 private final UnixPath name; 764 private final DirectoryNode parent; 765 private boolean visited; 766 767 EntryNode(long object, UnixPath name, DirectoryNode parent) { 768 this.object = object; 769 this.name = name; 770 this.parent = parent; 771 } 772 773 @Override 774 public long object() { 775 return object; 776 } 777 778 void setObject(long ptr) { 779 this.object = ptr; 780 } 781 782 UnixPath name() { 783 return name; 784 } 785 786 DirectoryNode parent() { 787 return parent; 788 } 789 790 boolean isVisited() { 791 return visited; 792 } 793 794 void setVisited(boolean v) { 795 this.visited = v; 796 } 797 } 798 799 // -- native methods -- 800 801 private static native void init(); 802 803 private static native int portCreate() throws UnixException; 804 805 private static native void portAssociate(int port, int source, long object, int events) 806 throws UnixException; 807 808 private static native void portDissociate(int port, int source, long object) 809 throws UnixException; 810 811 private static native void portSend(int port, int events) 812 throws UnixException; 813 814 private static native int portGetn(int port, long address, int max) 815 throws UnixException; 816 817 static { 818 AccessController.doPrivileged(new PrivilegedAction<Void>() { 819 public Void run() { 820 System.loadLibrary("nio"); 821 return null; 822 }}); 823 init(); 824 } 825 }