1""" 2BitBake 'Event' implementation 3 4Classes and functions for manipulating 'events' in the 5BitBake build tools. 6""" 7 8# Copyright (C) 2003, 2004 Chris Larson 9# 10# SPDX-License-Identifier: GPL-2.0-only 11# 12 13import ast 14import atexit 15import collections 16import logging 17import pickle 18import sys 19import threading 20import traceback 21 22import bb.exceptions 23import bb.utils 24 25# This is the pid for which we should generate the event. This is set when 26# the runqueue forks off. 27worker_pid = 0 28worker_fire = None 29 30logger = logging.getLogger('BitBake.Event') 31 32class Event(object): 33 """Base class for events""" 34 35 def __init__(self): 36 self.pid = worker_pid 37 38 39class HeartbeatEvent(Event): 40 """Triggered at regular time intervals of 10 seconds. Other events can fire much more often 41 (runQueueTaskStarted when there are many short tasks) or not at all for long periods 42 of time (again runQueueTaskStarted, when there is just one long-running task), so this 43 event is more suitable for doing some task-independent work occassionally.""" 44 def __init__(self, time): 45 Event.__init__(self) 46 self.time = time 47 48Registered = 10 49AlreadyRegistered = 14 50 51def get_class_handlers(): 52 return _handlers 53 54def set_class_handlers(h): 55 global _handlers 56 _handlers = h 57 58def clean_class_handlers(): 59 return collections.OrderedDict() 60 61# Internal 62_handlers = clean_class_handlers() 63_ui_handlers = {} 64_ui_logfilters = {} 65_ui_handler_seq = 0 66_event_handler_map = {} 67_catchall_handlers = {} 68_eventfilter = None 69_uiready = False 70_thread_lock = threading.Lock() 71_thread_lock_enabled = False 72 73if hasattr(__builtins__, '__setitem__'): 74 builtins = __builtins__ 75else: 76 builtins = __builtins__.__dict__ 77 78def enable_threadlock(): 79 global _thread_lock_enabled 80 _thread_lock_enabled = True 81 82def disable_threadlock(): 83 global _thread_lock_enabled 84 _thread_lock_enabled = False 85 86def execute_handler(name, handler, event, d): 87 event.data = d 88 addedd = False 89 if 'd' not in builtins: 90 builtins['d'] = d 91 addedd = True 92 try: 93 ret = handler(event) 94 except (bb.parse.SkipRecipe, bb.BBHandledException): 95 raise 96 except Exception: 97 etype, value, tb = sys.exc_info() 98 logger.error("Execution of event handler '%s' failed" % name, 99 exc_info=(etype, value, tb.tb_next)) 100 raise 101 except SystemExit as exc: 102 if exc.code != 0: 103 logger.error("Execution of event handler '%s' failed" % name) 104 raise 105 finally: 106 del event.data 107 if addedd: 108 del builtins['d'] 109 110def fire_class_handlers(event, d): 111 if isinstance(event, logging.LogRecord): 112 return 113 114 eid = str(event.__class__)[8:-2] 115 evt_hmap = _event_handler_map.get(eid, {}) 116 for name, handler in list(_handlers.items()): 117 if name in _catchall_handlers or name in evt_hmap: 118 if _eventfilter: 119 if not _eventfilter(name, handler, event, d): 120 continue 121 if d and not name in (d.getVar("__BBHANDLERS_MC") or []): 122 continue 123 execute_handler(name, handler, event, d) 124 125ui_queue = [] 126@atexit.register 127def print_ui_queue(): 128 global ui_queue 129 """If we're exiting before a UI has been spawned, display any queued 130 LogRecords to the console.""" 131 logger = logging.getLogger("BitBake") 132 if not _uiready: 133 from bb.msg import BBLogFormatter 134 # Flush any existing buffered content 135 sys.stdout.flush() 136 sys.stderr.flush() 137 stdout = logging.StreamHandler(sys.stdout) 138 stderr = logging.StreamHandler(sys.stderr) 139 formatter = BBLogFormatter("%(levelname)s: %(message)s") 140 stdout.setFormatter(formatter) 141 stderr.setFormatter(formatter) 142 143 # First check to see if we have any proper messages 144 msgprint = False 145 msgerrs = False 146 147 # Should we print to stderr? 148 for event in ui_queue[:]: 149 if isinstance(event, logging.LogRecord) and event.levelno >= logging.WARNING: 150 msgerrs = True 151 break 152 153 if msgerrs: 154 logger.addHandler(stderr) 155 else: 156 logger.addHandler(stdout) 157 158 for event in ui_queue[:]: 159 if isinstance(event, logging.LogRecord): 160 if event.levelno > logging.DEBUG: 161 logger.handle(event) 162 msgprint = True 163 164 # Nope, so just print all of the messages we have (including debug messages) 165 if not msgprint: 166 for event in ui_queue[:]: 167 if isinstance(event, logging.LogRecord): 168 logger.handle(event) 169 if msgerrs: 170 logger.removeHandler(stderr) 171 else: 172 logger.removeHandler(stdout) 173 ui_queue = [] 174 175def fire_ui_handlers(event, d): 176 global _thread_lock 177 global _thread_lock_enabled 178 179 if not _uiready: 180 # No UI handlers registered yet, queue up the messages 181 ui_queue.append(event) 182 return 183 184 if _thread_lock_enabled: 185 _thread_lock.acquire() 186 187 errors = [] 188 for h in _ui_handlers: 189 #print "Sending event %s" % event 190 try: 191 if not _ui_logfilters[h].filter(event): 192 continue 193 # We use pickle here since it better handles object instances 194 # which xmlrpc's marshaller does not. Events *must* be serializable 195 # by pickle. 196 if hasattr(_ui_handlers[h].event, "sendpickle"): 197 _ui_handlers[h].event.sendpickle((pickle.dumps(event))) 198 else: 199 _ui_handlers[h].event.send(event) 200 except: 201 errors.append(h) 202 for h in errors: 203 del _ui_handlers[h] 204 205 if _thread_lock_enabled: 206 _thread_lock.release() 207 208def fire(event, d): 209 """Fire off an Event""" 210 211 # We can fire class handlers in the worker process context and this is 212 # desired so they get the task based datastore. 213 # UI handlers need to be fired in the server context so we defer this. They 214 # don't have a datastore so the datastore context isn't a problem. 215 216 fire_class_handlers(event, d) 217 if worker_fire: 218 worker_fire(event, d) 219 else: 220 # If messages have been queued up, clear the queue 221 global _uiready, ui_queue 222 if _uiready and ui_queue: 223 for queue_event in ui_queue: 224 fire_ui_handlers(queue_event, d) 225 ui_queue = [] 226 fire_ui_handlers(event, d) 227 228def fire_from_worker(event, d): 229 fire_ui_handlers(event, d) 230 231noop = lambda _: None 232def register(name, handler, mask=None, filename=None, lineno=None, data=None): 233 """Register an Event handler""" 234 235 if data and data.getVar("BB_CURRENT_MC"): 236 mc = data.getVar("BB_CURRENT_MC") 237 name = '%s%s' % (mc.replace('-', '_'), name) 238 239 # already registered 240 if name in _handlers: 241 return AlreadyRegistered 242 243 if handler is not None: 244 # handle string containing python code 245 if isinstance(handler, str): 246 tmp = "def %s(e):\n%s" % (name, handler) 247 try: 248 code = bb.methodpool.compile_cache(tmp) 249 if not code: 250 if filename is None: 251 filename = "%s(e)" % name 252 code = compile(tmp, filename, "exec", ast.PyCF_ONLY_AST) 253 if lineno is not None: 254 ast.increment_lineno(code, lineno-1) 255 code = compile(code, filename, "exec") 256 bb.methodpool.compile_cache_add(tmp, code) 257 except SyntaxError: 258 logger.error("Unable to register event handler '%s':\n%s", name, 259 ''.join(traceback.format_exc(limit=0))) 260 _handlers[name] = noop 261 return 262 env = {} 263 bb.utils.better_exec(code, env) 264 func = bb.utils.better_eval(name, env) 265 _handlers[name] = func 266 else: 267 _handlers[name] = handler 268 269 if not mask or '*' in mask: 270 _catchall_handlers[name] = True 271 else: 272 for m in mask: 273 if _event_handler_map.get(m, None) is None: 274 _event_handler_map[m] = {} 275 _event_handler_map[m][name] = True 276 277 if data: 278 bbhands_mc = (data.getVar("__BBHANDLERS_MC") or []) 279 bbhands_mc.append(name) 280 data.setVar("__BBHANDLERS_MC", bbhands_mc) 281 282 return Registered 283 284def remove(name, handler, data=None): 285 """Remove an Event handler""" 286 if data: 287 if data.getVar("BB_CURRENT_MC"): 288 mc = data.getVar("BB_CURRENT_MC") 289 name = '%s%s' % (mc.replace('-', '_'), name) 290 291 _handlers.pop(name) 292 if name in _catchall_handlers: 293 _catchall_handlers.pop(name) 294 for event in _event_handler_map.keys(): 295 if name in _event_handler_map[event]: 296 _event_handler_map[event].pop(name) 297 298 if data: 299 bbhands_mc = (data.getVar("__BBHANDLERS_MC") or []) 300 if name in bbhands_mc: 301 bbhands_mc.remove(name) 302 data.setVar("__BBHANDLERS_MC", bbhands_mc) 303 304def get_handlers(): 305 return _handlers 306 307def set_handlers(handlers): 308 global _handlers 309 _handlers = handlers 310 311def set_eventfilter(func): 312 global _eventfilter 313 _eventfilter = func 314 315def register_UIHhandler(handler, mainui=False): 316 bb.event._ui_handler_seq = bb.event._ui_handler_seq + 1 317 _ui_handlers[_ui_handler_seq] = handler 318 level, debug_domains = bb.msg.constructLogOptions() 319 _ui_logfilters[_ui_handler_seq] = UIEventFilter(level, debug_domains) 320 if mainui: 321 global _uiready 322 _uiready = _ui_handler_seq 323 return _ui_handler_seq 324 325def unregister_UIHhandler(handlerNum, mainui=False): 326 if mainui: 327 global _uiready 328 _uiready = False 329 if handlerNum in _ui_handlers: 330 del _ui_handlers[handlerNum] 331 return 332 333def get_uihandler(): 334 if _uiready is False: 335 return None 336 return _uiready 337 338# Class to allow filtering of events and specific filtering of LogRecords *before* we put them over the IPC 339class UIEventFilter(object): 340 def __init__(self, level, debug_domains): 341 self.update(None, level, debug_domains) 342 343 def update(self, eventmask, level, debug_domains): 344 self.eventmask = eventmask 345 self.stdlevel = level 346 self.debug_domains = debug_domains 347 348 def filter(self, event): 349 if isinstance(event, logging.LogRecord): 350 if event.levelno >= self.stdlevel: 351 return True 352 if event.name in self.debug_domains and event.levelno >= self.debug_domains[event.name]: 353 return True 354 return False 355 eid = str(event.__class__)[8:-2] 356 if self.eventmask and eid not in self.eventmask: 357 return False 358 return True 359 360def set_UIHmask(handlerNum, level, debug_domains, mask): 361 if not handlerNum in _ui_handlers: 362 return False 363 if '*' in mask: 364 _ui_logfilters[handlerNum].update(None, level, debug_domains) 365 else: 366 _ui_logfilters[handlerNum].update(mask, level, debug_domains) 367 return True 368 369def getName(e): 370 """Returns the name of a class or class instance""" 371 if getattr(e, "__name__", None) is None: 372 return e.__class__.__name__ 373 else: 374 return e.__name__ 375 376class OperationStarted(Event): 377 """An operation has begun""" 378 def __init__(self, msg = "Operation Started"): 379 Event.__init__(self) 380 self.msg = msg 381 382class OperationCompleted(Event): 383 """An operation has completed""" 384 def __init__(self, total, msg = "Operation Completed"): 385 Event.__init__(self) 386 self.total = total 387 self.msg = msg 388 389class OperationProgress(Event): 390 """An operation is in progress""" 391 def __init__(self, current, total, msg = "Operation in Progress"): 392 Event.__init__(self) 393 self.current = current 394 self.total = total 395 self.msg = msg + ": %s/%s" % (current, total); 396 397class ConfigParsed(Event): 398 """Configuration Parsing Complete""" 399 400class MultiConfigParsed(Event): 401 """Multi-Config Parsing Complete""" 402 def __init__(self, mcdata): 403 self.mcdata = mcdata 404 Event.__init__(self) 405 406class RecipeEvent(Event): 407 def __init__(self, fn): 408 self.fn = fn 409 Event.__init__(self) 410 411class RecipePreFinalise(RecipeEvent): 412 """ Recipe Parsing Complete but not yet finalised""" 413 414class RecipePostKeyExpansion(RecipeEvent): 415 """ Recipe Parsing Complete but not yet finalised""" 416 417 418class RecipeTaskPreProcess(RecipeEvent): 419 """ 420 Recipe Tasks about to be finalised 421 The list of tasks should be final at this point and handlers 422 are only able to change interdependencies 423 """ 424 def __init__(self, fn, tasklist): 425 self.fn = fn 426 self.tasklist = tasklist 427 Event.__init__(self) 428 429class RecipeParsed(RecipeEvent): 430 """ Recipe Parsing Complete """ 431 432class BuildBase(Event): 433 """Base class for bitbake build events""" 434 435 def __init__(self, n, p, failures = 0): 436 self._name = n 437 self._pkgs = p 438 Event.__init__(self) 439 self._failures = failures 440 441 def getPkgs(self): 442 return self._pkgs 443 444 def setPkgs(self, pkgs): 445 self._pkgs = pkgs 446 447 def getName(self): 448 return self._name 449 450 def setName(self, name): 451 self._name = name 452 453 def getFailures(self): 454 """ 455 Return the number of failed packages 456 """ 457 return self._failures 458 459 pkgs = property(getPkgs, setPkgs, None, "pkgs property") 460 name = property(getName, setName, None, "name property") 461 462class BuildInit(BuildBase): 463 """buildFile or buildTargets was invoked""" 464 def __init__(self, p=[]): 465 name = None 466 BuildBase.__init__(self, name, p) 467 468class BuildStarted(BuildBase, OperationStarted): 469 """Event when builds start""" 470 def __init__(self, n, p, failures = 0): 471 OperationStarted.__init__(self, "Building Started") 472 BuildBase.__init__(self, n, p, failures) 473 474class BuildCompleted(BuildBase, OperationCompleted): 475 """Event when builds have completed""" 476 def __init__(self, total, n, p, failures=0, interrupted=0): 477 if not failures: 478 OperationCompleted.__init__(self, total, "Building Succeeded") 479 else: 480 OperationCompleted.__init__(self, total, "Building Failed") 481 self._interrupted = interrupted 482 BuildBase.__init__(self, n, p, failures) 483 484class DiskFull(Event): 485 """Disk full case build aborted""" 486 def __init__(self, dev, type, freespace, mountpoint): 487 Event.__init__(self) 488 self._dev = dev 489 self._type = type 490 self._free = freespace 491 self._mountpoint = mountpoint 492 493class DiskUsageSample: 494 def __init__(self, available_bytes, free_bytes, total_bytes): 495 # Number of bytes available to non-root processes. 496 self.available_bytes = available_bytes 497 # Number of bytes available to root processes. 498 self.free_bytes = free_bytes 499 # Total capacity of the volume. 500 self.total_bytes = total_bytes 501 502class MonitorDiskEvent(Event): 503 """If BB_DISKMON_DIRS is set, then this event gets triggered each time disk space is checked. 504 Provides information about devices that are getting monitored.""" 505 def __init__(self, disk_usage): 506 Event.__init__(self) 507 # hash of device root path -> DiskUsageSample 508 self.disk_usage = disk_usage 509 510class NoProvider(Event): 511 """No Provider for an Event""" 512 513 def __init__(self, item, runtime=False, dependees=None, reasons=None, close_matches=None): 514 Event.__init__(self) 515 self._item = item 516 self._runtime = runtime 517 self._dependees = dependees 518 self._reasons = reasons 519 self._close_matches = close_matches 520 521 def getItem(self): 522 return self._item 523 524 def isRuntime(self): 525 return self._runtime 526 527 def __str__(self): 528 msg = '' 529 if self._runtime: 530 r = "R" 531 else: 532 r = "" 533 534 extra = '' 535 if not self._reasons: 536 if self._close_matches: 537 extra = ". Close matches:\n %s" % '\n '.join(sorted(set(self._close_matches))) 538 539 if self._dependees: 540 msg = "Nothing %sPROVIDES '%s' (but %s %sDEPENDS on or otherwise requires it)%s" % (r, self._item, ", ".join(self._dependees), r, extra) 541 else: 542 msg = "Nothing %sPROVIDES '%s'%s" % (r, self._item, extra) 543 if self._reasons: 544 for reason in self._reasons: 545 msg += '\n' + reason 546 return msg 547 548 549class MultipleProviders(Event): 550 """Multiple Providers""" 551 552 def __init__(self, item, candidates, runtime = False): 553 Event.__init__(self) 554 self._item = item 555 self._candidates = candidates 556 self._is_runtime = runtime 557 558 def isRuntime(self): 559 """ 560 Is this a runtime issue? 561 """ 562 return self._is_runtime 563 564 def getItem(self): 565 """ 566 The name for the to be build item 567 """ 568 return self._item 569 570 def getCandidates(self): 571 """ 572 Get the possible Candidates for a PROVIDER. 573 """ 574 return self._candidates 575 576 def __str__(self): 577 msg = "Multiple providers are available for %s%s (%s)" % (self._is_runtime and "runtime " or "", 578 self._item, 579 ", ".join(self._candidates)) 580 rtime = "" 581 if self._is_runtime: 582 rtime = "R" 583 msg += "\nConsider defining a PREFERRED_%sPROVIDER entry to match %s" % (rtime, self._item) 584 return msg 585 586class ParseStarted(OperationStarted): 587 """Recipe parsing for the runqueue has begun""" 588 def __init__(self, total): 589 OperationStarted.__init__(self, "Recipe parsing Started") 590 self.total = total 591 592class ParseCompleted(OperationCompleted): 593 """Recipe parsing for the runqueue has completed""" 594 def __init__(self, cached, parsed, skipped, masked, virtuals, errors, total): 595 OperationCompleted.__init__(self, total, "Recipe parsing Completed") 596 self.cached = cached 597 self.parsed = parsed 598 self.skipped = skipped 599 self.virtuals = virtuals 600 self.masked = masked 601 self.errors = errors 602 self.sofar = cached + parsed 603 604class ParseProgress(OperationProgress): 605 """Recipe parsing progress""" 606 def __init__(self, current, total): 607 OperationProgress.__init__(self, current, total, "Recipe parsing") 608 609 610class CacheLoadStarted(OperationStarted): 611 """Loading of the dependency cache has begun""" 612 def __init__(self, total): 613 OperationStarted.__init__(self, "Loading cache Started") 614 self.total = total 615 616class CacheLoadProgress(OperationProgress): 617 """Cache loading progress""" 618 def __init__(self, current, total): 619 OperationProgress.__init__(self, current, total, "Loading cache") 620 621class CacheLoadCompleted(OperationCompleted): 622 """Cache loading is complete""" 623 def __init__(self, total, num_entries): 624 OperationCompleted.__init__(self, total, "Loading cache Completed") 625 self.num_entries = num_entries 626 627class TreeDataPreparationStarted(OperationStarted): 628 """Tree data preparation started""" 629 def __init__(self): 630 OperationStarted.__init__(self, "Preparing tree data Started") 631 632class TreeDataPreparationProgress(OperationProgress): 633 """Tree data preparation is in progress""" 634 def __init__(self, current, total): 635 OperationProgress.__init__(self, current, total, "Preparing tree data") 636 637class TreeDataPreparationCompleted(OperationCompleted): 638 """Tree data preparation completed""" 639 def __init__(self, total): 640 OperationCompleted.__init__(self, total, "Preparing tree data Completed") 641 642class DepTreeGenerated(Event): 643 """ 644 Event when a dependency tree has been generated 645 """ 646 647 def __init__(self, depgraph): 648 Event.__init__(self) 649 self._depgraph = depgraph 650 651class TargetsTreeGenerated(Event): 652 """ 653 Event when a set of buildable targets has been generated 654 """ 655 def __init__(self, model): 656 Event.__init__(self) 657 self._model = model 658 659class ReachableStamps(Event): 660 """ 661 An event listing all stamps reachable after parsing 662 which the metadata may use to clean up stale data 663 """ 664 665 def __init__(self, stamps): 666 Event.__init__(self) 667 self.stamps = stamps 668 669class FilesMatchingFound(Event): 670 """ 671 Event when a list of files matching the supplied pattern has 672 been generated 673 """ 674 def __init__(self, pattern, matches): 675 Event.__init__(self) 676 self._pattern = pattern 677 self._matches = matches 678 679class ConfigFilesFound(Event): 680 """ 681 Event when a list of appropriate config files has been generated 682 """ 683 def __init__(self, variable, values): 684 Event.__init__(self) 685 self._variable = variable 686 self._values = values 687 688class ConfigFilePathFound(Event): 689 """ 690 Event when a path for a config file has been found 691 """ 692 def __init__(self, path): 693 Event.__init__(self) 694 self._path = path 695 696class MsgBase(Event): 697 """Base class for messages""" 698 699 def __init__(self, msg): 700 self._message = msg 701 Event.__init__(self) 702 703class MsgDebug(MsgBase): 704 """Debug Message""" 705 706class MsgNote(MsgBase): 707 """Note Message""" 708 709class MsgWarn(MsgBase): 710 """Warning Message""" 711 712class MsgError(MsgBase): 713 """Error Message""" 714 715class MsgFatal(MsgBase): 716 """Fatal Message""" 717 718class MsgPlain(MsgBase): 719 """General output""" 720 721class LogExecTTY(Event): 722 """Send event containing program to spawn on tty of the logger""" 723 def __init__(self, msg, prog, sleep_delay, retries): 724 Event.__init__(self) 725 self.msg = msg 726 self.prog = prog 727 self.sleep_delay = sleep_delay 728 self.retries = retries 729 730class LogHandler(logging.Handler): 731 """Dispatch logging messages as bitbake events""" 732 733 def emit(self, record): 734 if record.exc_info: 735 etype, value, tb = record.exc_info 736 if hasattr(tb, 'tb_next'): 737 tb = list(bb.exceptions.extract_traceback(tb, context=3)) 738 # Need to turn the value into something the logging system can pickle 739 record.bb_exc_info = (etype, value, tb) 740 record.bb_exc_formatted = bb.exceptions.format_exception(etype, value, tb, limit=5) 741 value = str(value) 742 record.exc_info = None 743 fire(record, None) 744 745 def filter(self, record): 746 record.taskpid = worker_pid 747 return True 748 749class MetadataEvent(Event): 750 """ 751 Generic event that target for OE-Core classes 752 to report information during asynchrous execution 753 """ 754 def __init__(self, eventtype, eventdata): 755 Event.__init__(self) 756 self.type = eventtype 757 self._localdata = eventdata 758 759class ProcessStarted(Event): 760 """ 761 Generic process started event (usually part of the initial startup) 762 where further progress events will be delivered 763 """ 764 def __init__(self, processname, total): 765 Event.__init__(self) 766 self.processname = processname 767 self.total = total 768 769class ProcessProgress(Event): 770 """ 771 Generic process progress event (usually part of the initial startup) 772 """ 773 def __init__(self, processname, progress): 774 Event.__init__(self) 775 self.processname = processname 776 self.progress = progress 777 778class ProcessFinished(Event): 779 """ 780 Generic process finished event (usually part of the initial startup) 781 """ 782 def __init__(self, processname): 783 Event.__init__(self) 784 self.processname = processname 785 786class SanityCheck(Event): 787 """ 788 Event to run sanity checks, either raise errors or generate events as return status. 789 """ 790 def __init__(self, generateevents = True): 791 Event.__init__(self) 792 self.generateevents = generateevents 793 794class SanityCheckPassed(Event): 795 """ 796 Event to indicate sanity check has passed 797 """ 798 799class SanityCheckFailed(Event): 800 """ 801 Event to indicate sanity check has failed 802 """ 803 def __init__(self, msg, network_error=False): 804 Event.__init__(self) 805 self._msg = msg 806 self._network_error = network_error 807 808class NetworkTest(Event): 809 """ 810 Event to run network connectivity tests, either raise errors or generate events as return status. 811 """ 812 def __init__(self, generateevents = True): 813 Event.__init__(self) 814 self.generateevents = generateevents 815 816class NetworkTestPassed(Event): 817 """ 818 Event to indicate network test has passed 819 """ 820 821class NetworkTestFailed(Event): 822 """ 823 Event to indicate network test has failed 824 """ 825 826class FindSigInfoResult(Event): 827 """ 828 Event to return results from findSigInfo command 829 """ 830 def __init__(self, result): 831 Event.__init__(self) 832 self.result = result 833