MediaWiki
REL1_19
|
00001 <?php 00027 $originalDir = getcwd(); 00028 00029 require_once( dirname( __FILE__ ) . '/commandLine.inc' ); 00030 require_once( 'backup.inc' ); 00031 00035 class TextPassDumper extends BackupDumper { 00036 var $prefetch = null; 00037 var $input = "php://stdin"; 00038 var $history = WikiExporter::FULL; 00039 var $fetchCount = 0; 00040 var $prefetchCount = 0; 00041 var $prefetchCountLast = 0; 00042 var $fetchCountLast = 0; 00043 00044 var $failures = 0; 00045 var $maxFailures = 5; 00046 var $failedTextRetrievals = 0; 00047 var $maxConsecutiveFailedTextRetrievals = 200; 00048 var $failureTimeout = 5; // Seconds to sleep after db failure 00049 00050 var $php = "php"; 00051 var $spawn = false; 00052 var $spawnProc = false; 00053 var $spawnWrite = false; 00054 var $spawnRead = false; 00055 var $spawnErr = false; 00056 00057 var $xmlwriterobj = false; 00058 00059 // when we spend more than maxTimeAllowed seconds on this run, we continue 00060 // processing until we write out the next complete page, then save output file(s), 00061 // rename it/them and open new one(s) 00062 var $maxTimeAllowed = 0; // 0 = no limit 00063 var $timeExceeded = false; 00064 var $firstPageWritten = false; 00065 var $lastPageWritten = false; 00066 var $checkpointJustWritten = false; 00067 var $checkpointFiles = array(); 00068 00072 protected $db; 00073 00074 function initProgress( $history ) { 00075 parent::initProgress(); 00076 $this->timeOfCheckpoint = $this->startTime; 00077 } 00078 00079 function dump( $history, $text = WikiExporter::TEXT ) { 00080 // This shouldn't happen if on console... ;) 00081 header( 'Content-type: text/html; charset=UTF-8' ); 00082 00083 // Notice messages will foul up your XML output even if they're 00084 // relatively harmless. 00085 if ( ini_get( 'display_errors' ) ) 00086 ini_set( 'display_errors', 'stderr' ); 00087 00088 $this->initProgress( $this->history ); 00089 00090 $this->db = $this->backupDb(); 00091 00092 $this->egress = new ExportProgressFilter( $this->sink, $this ); 00093 00094 // it would be nice to do it in the constructor, oh well. need egress set 00095 $this->finalOptionCheck(); 00096 00097 // we only want this so we know how to close a stream :-P 00098 $this->xmlwriterobj = new XmlDumpWriter(); 00099 00100 $input = fopen( $this->input, "rt" ); 00101 $result = $this->readDump( $input ); 00102 00103 if ( WikiError::isError( $result ) ) { 00104 throw new MWException( $result->getMessage() ); 00105 } 00106 00107 if ( $this->spawnProc ) { 00108 $this->closeSpawn(); 00109 } 00110 00111 $this->report( true ); 00112 } 00113 00114 function processOption( $opt, $val, $param ) { 00115 global $IP; 00116 $url = $this->processFileOpt( $val, $param ); 00117 00118 switch( $opt ) { 00119 case 'prefetch': 00120 require_once "$IP/maintenance/backupPrefetch.inc"; 00121 $this->prefetch = new BaseDump( $url ); 00122 break; 00123 case 'stub': 00124 $this->input = $url; 00125 break; 00126 case 'maxtime': 00127 $this->maxTimeAllowed = intval($val)*60; 00128 break; 00129 case 'checkpointfile': 00130 $this->checkpointFiles[] = $val; 00131 break; 00132 case 'current': 00133 $this->history = WikiExporter::CURRENT; 00134 break; 00135 case 'full': 00136 $this->history = WikiExporter::FULL; 00137 break; 00138 case 'spawn': 00139 $this->spawn = true; 00140 if ( $val ) { 00141 $this->php = $val; 00142 } 00143 break; 00144 } 00145 } 00146 00147 function processFileOpt( $val, $param ) { 00148 $fileURIs = explode(';',$param); 00149 foreach ( $fileURIs as $URI ) { 00150 switch( $val ) { 00151 case "file": 00152 $newURI = $URI; 00153 break; 00154 case "gzip": 00155 $newURI = "compress.zlib://$URI"; 00156 break; 00157 case "bzip2": 00158 $newURI = "compress.bzip2://$URI"; 00159 break; 00160 case "7zip": 00161 $newURI = "mediawiki.compress.7z://$URI"; 00162 break; 00163 default: 00164 $newURI = $URI; 00165 } 00166 $newFileURIs[] = $newURI; 00167 } 00168 $val = implode( ';', $newFileURIs ); 00169 return $val; 00170 } 00171 00175 function showReport() { 00176 if ( !$this->prefetch ) { 00177 parent::showReport(); 00178 return; 00179 } 00180 00181 if ( $this->reporting ) { 00182 $now = wfTimestamp( TS_DB ); 00183 $nowts = wfTime(); 00184 $deltaAll = wfTime() - $this->startTime; 00185 $deltaPart = wfTime() - $this->lastTime; 00186 $this->pageCountPart = $this->pageCount - $this->pageCountLast; 00187 $this->revCountPart = $this->revCount - $this->revCountLast; 00188 00189 if ( $deltaAll ) { 00190 $portion = $this->revCount / $this->maxCount; 00191 $eta = $this->startTime + $deltaAll / $portion; 00192 $etats = wfTimestamp( TS_DB, intval( $eta ) ); 00193 if ( $this->fetchCount ) { 00194 $fetchRate = 100.0 * $this->prefetchCount / $this->fetchCount; 00195 } else { 00196 $fetchRate = '-'; 00197 } 00198 $pageRate = $this->pageCount / $deltaAll; 00199 $revRate = $this->revCount / $deltaAll; 00200 } else { 00201 $pageRate = '-'; 00202 $revRate = '-'; 00203 $etats = '-'; 00204 $fetchRate = '-'; 00205 } 00206 if ( $deltaPart ) { 00207 if ( $this->fetchCountLast ) { 00208 $fetchRatePart = 100.0 * $this->prefetchCountLast / $this->fetchCountLast; 00209 } else { 00210 $fetchRatePart = '-'; 00211 } 00212 $pageRatePart = $this->pageCountPart / $deltaPart; 00213 $revRatePart = $this->revCountPart / $deltaPart; 00214 00215 } else { 00216 $fetchRatePart = '-'; 00217 $pageRatePart = '-'; 00218 $revRatePart = '-'; 00219 } 00220 $this->progress( sprintf( "%s: %s (ID %d) %d pages (%0.1f|%0.1f/sec all|curr), %d revs (%0.1f|%0.1f/sec all|curr), %0.1f%%|%0.1f%% prefetched (all|curr), ETA %s [max %d]", 00221 $now, wfWikiID(), $this->ID, $this->pageCount, $pageRate, $pageRatePart, $this->revCount, $revRate, $revRatePart, $fetchRate, $fetchRatePart, $etats, $this->maxCount ) ); 00222 $this->lastTime = $nowts; 00223 $this->revCountLast = $this->revCount; 00224 $this->prefetchCountLast = $this->prefetchCount; 00225 $this->fetchCountLast = $this->fetchCount; 00226 } 00227 } 00228 00229 function setTimeExceeded() { 00230 $this->timeExceeded = True; 00231 } 00232 00233 function checkIfTimeExceeded() { 00234 if ( $this->maxTimeAllowed && ( $this->lastTime - $this->timeOfCheckpoint > $this->maxTimeAllowed ) ) { 00235 return true; 00236 } 00237 return false; 00238 } 00239 00240 function finalOptionCheck() { 00241 if ( ( $this->checkpointFiles && ! $this->maxTimeAllowed ) || 00242 ( $this->maxTimeAllowed && !$this->checkpointFiles ) ) { 00243 throw new MWException("Options checkpointfile and maxtime must be specified together.\n"); 00244 } 00245 foreach ($this->checkpointFiles as $checkpointFile) { 00246 $count = substr_count ( $checkpointFile,"%s" ); 00247 if ( $count != 2 ) { 00248 throw new MWException("Option checkpointfile must contain two '%s' for substitution of first and last pageids, count is $count instead, file is $checkpointFile.\n"); 00249 } 00250 } 00251 00252 if ( $this->checkpointFiles ) { 00253 $filenameList = (array)$this->egress->getFilenames(); 00254 if ( count( $filenameList ) != count( $this->checkpointFiles ) ) { 00255 throw new MWException("One checkpointfile must be specified for each output option, if maxtime is used.\n"); 00256 } 00257 } 00258 } 00259 00260 function readDump( $input ) { 00261 $this->buffer = ""; 00262 $this->openElement = false; 00263 $this->atStart = true; 00264 $this->state = ""; 00265 $this->lastName = ""; 00266 $this->thisPage = 0; 00267 $this->thisRev = 0; 00268 00269 $parser = xml_parser_create( "UTF-8" ); 00270 xml_parser_set_option( $parser, XML_OPTION_CASE_FOLDING, false ); 00271 00272 xml_set_element_handler( $parser, array( &$this, 'startElement' ), array( &$this, 'endElement' ) ); 00273 xml_set_character_data_handler( $parser, array( &$this, 'characterData' ) ); 00274 00275 $offset = 0; // for context extraction on error reporting 00276 $bufferSize = 512 * 1024; 00277 do { 00278 if ($this->checkIfTimeExceeded()) { 00279 $this->setTimeExceeded(); 00280 } 00281 $chunk = fread( $input, $bufferSize ); 00282 if ( !xml_parse( $parser, $chunk, feof( $input ) ) ) { 00283 wfDebug( "TextDumpPass::readDump encountered XML parsing error\n" ); 00284 return new WikiXmlError( $parser, 'XML import parse failure', $chunk, $offset ); 00285 } 00286 $offset += strlen( $chunk ); 00287 } while ( $chunk !== false && !feof( $input ) ); 00288 if ($this->maxTimeAllowed) { 00289 $filenameList = (array)$this->egress->getFilenames(); 00290 // we wrote some stuff after last checkpoint that needs renamed 00291 if (file_exists($filenameList[0])) { 00292 $newFilenames = array(); 00293 # we might have just written the header and footer and had no 00294 # pages or revisions written... perhaps they were all deleted 00295 # there's no pageID 0 so we use that. the caller is responsible 00296 # for deciding what to do with a file containing only the 00297 # siteinfo information and the mw tags. 00298 if (! $this->firstPageWritten) { 00299 $firstPageID = str_pad(0,9,"0",STR_PAD_LEFT); 00300 $lastPageID = str_pad(0,9,"0",STR_PAD_LEFT); 00301 } 00302 else { 00303 $firstPageID = str_pad($this->firstPageWritten,9,"0",STR_PAD_LEFT); 00304 $lastPageID = str_pad($this->lastPageWritten,9,"0",STR_PAD_LEFT); 00305 } 00306 for ( $i = 0; $i < count( $filenameList ); $i++ ) { 00307 $checkpointNameFilledIn = sprintf( $this->checkpointFiles[$i], $firstPageID, $lastPageID ); 00308 $fileinfo = pathinfo($filenameList[$i]); 00309 $newFilenames[] = $fileinfo['dirname'] . '/' . $checkpointNameFilledIn; 00310 } 00311 $this->egress->closeAndRename( $newFilenames ); 00312 } 00313 } 00314 xml_parser_free( $parser ); 00315 00316 return true; 00317 } 00318 00319 function getText( $id ) { 00320 $this->fetchCount++; 00321 if ( isset( $this->prefetch ) ) { 00322 $text = $this->prefetch->prefetch( $this->thisPage, $this->thisRev ); 00323 if ( $text !== null ) { // Entry missing from prefetch dump 00324 $dbr = wfGetDB( DB_SLAVE ); 00325 $revID = intval( $this->thisRev ); 00326 $revLength = $dbr->selectField( 'revision', 'rev_len', array( 'rev_id' => $revID ) ); 00327 // if length of rev text in file doesn't match length in db, we reload 00328 // this avoids carrying forward broken data from previous xml dumps 00329 if( strlen( $text ) == $revLength ) { 00330 $this->prefetchCount++; 00331 return $text; 00332 } 00333 } 00334 } 00335 return $this->doGetText( $id ); 00336 } 00337 00338 private function doGetText( $id ) { 00339 $id = intval( $id ); 00340 $this->failures = 0; 00341 $ex = new MWException( "Graceful storage failure" ); 00342 while (true) { 00343 if ( $this->spawn ) { 00344 if ($this->failures) { 00345 // we don't know why it failed, could be the child process 00346 // borked, could be db entry busted, could be db server out to lunch, 00347 // so cover all bases 00348 $this->closeSpawn(); 00349 $this->openSpawn(); 00350 } 00351 $text = $this->getTextSpawned( $id ); 00352 } else { 00353 $text = $this->getTextDbSafe( $id ); 00354 } 00355 if ( $text === false ) { 00356 $this->failures++; 00357 if ( $this->failures > $this->maxFailures) { 00358 $this->progress( "Failed to retrieve revision text for text id ". 00359 "$id after $this->maxFailures tries, giving up" ); 00360 // were there so many bad retrievals in a row we want to bail? 00361 // at some point we have to declare the dump irretrievably broken 00362 $this->failedTextRetrievals++; 00363 if ($this->failedTextRetrievals > $this->maxConsecutiveFailedTextRetrievals) { 00364 throw $ex; 00365 } else { 00366 // would be nice to return something better to the caller someday, 00367 // log what we know about the failure and about the revision 00368 return ""; 00369 } 00370 } else { 00371 $this->progress( "Error $this->failures " . 00372 "of allowed $this->maxFailures retrieving revision text for text id $id! " . 00373 "Pausing $this->failureTimeout seconds before retry..." ); 00374 sleep( $this->failureTimeout ); 00375 } 00376 } else { 00377 $this->failedTextRetrievals= 0; 00378 return $text; 00379 } 00380 } 00381 return ''; 00382 } 00383 00391 private function getTextDbSafe( $id ) { 00392 while ( true ) { 00393 try { 00394 $text = $this->getTextDb( $id ); 00395 } catch ( DBQueryError $ex ) { 00396 $text = false; 00397 } 00398 return $text; 00399 } 00400 } 00401 00407 private function getTextDb( $id ) { 00408 global $wgContLang; 00409 $row = $this->db->selectRow( 'text', 00410 array( 'old_text', 'old_flags' ), 00411 array( 'old_id' => $id ), 00412 __METHOD__ ); 00413 $text = Revision::getRevisionText( $row ); 00414 if ( $text === false ) { 00415 return false; 00416 } 00417 $stripped = str_replace( "\r", "", $text ); 00418 $normalized = $wgContLang->normalize( $stripped ); 00419 return $normalized; 00420 } 00421 00422 private function getTextSpawned( $id ) { 00423 wfSuppressWarnings(); 00424 if ( !$this->spawnProc ) { 00425 // First time? 00426 $this->openSpawn(); 00427 } 00428 $text = $this->getTextSpawnedOnce( $id ); 00429 wfRestoreWarnings(); 00430 return $text; 00431 } 00432 00433 function openSpawn() { 00434 global $IP; 00435 00436 if ( file_exists( "$IP/../multiversion/MWScript.php" ) ) { 00437 $cmd = implode( " ", 00438 array_map( 'wfEscapeShellArg', 00439 array( 00440 $this->php, 00441 "$IP/../multiversion/MWScript.php", 00442 "fetchText.php", 00443 '--wiki', wfWikiID() ) ) ); 00444 } 00445 else { 00446 $cmd = implode( " ", 00447 array_map( 'wfEscapeShellArg', 00448 array( 00449 $this->php, 00450 "$IP/maintenance/fetchText.php", 00451 '--wiki', wfWikiID() ) ) ); 00452 } 00453 $spec = array( 00454 0 => array( "pipe", "r" ), 00455 1 => array( "pipe", "w" ), 00456 2 => array( "file", "/dev/null", "a" ) ); 00457 $pipes = array(); 00458 00459 $this->progress( "Spawning database subprocess: $cmd" ); 00460 $this->spawnProc = proc_open( $cmd, $spec, $pipes ); 00461 if ( !$this->spawnProc ) { 00462 // shit 00463 $this->progress( "Subprocess spawn failed." ); 00464 return false; 00465 } 00466 list( 00467 $this->spawnWrite, // -> stdin 00468 $this->spawnRead, // <- stdout 00469 ) = $pipes; 00470 00471 return true; 00472 } 00473 00474 private function closeSpawn() { 00475 wfSuppressWarnings(); 00476 if ( $this->spawnRead ) 00477 fclose( $this->spawnRead ); 00478 $this->spawnRead = false; 00479 if ( $this->spawnWrite ) 00480 fclose( $this->spawnWrite ); 00481 $this->spawnWrite = false; 00482 if ( $this->spawnErr ) 00483 fclose( $this->spawnErr ); 00484 $this->spawnErr = false; 00485 if ( $this->spawnProc ) 00486 pclose( $this->spawnProc ); 00487 $this->spawnProc = false; 00488 wfRestoreWarnings(); 00489 } 00490 00491 private function getTextSpawnedOnce( $id ) { 00492 global $wgContLang; 00493 00494 $ok = fwrite( $this->spawnWrite, "$id\n" ); 00495 // $this->progress( ">> $id" ); 00496 if ( !$ok ) return false; 00497 00498 $ok = fflush( $this->spawnWrite ); 00499 // $this->progress( ">> [flush]" ); 00500 if ( !$ok ) return false; 00501 00502 // check that the text id they are sending is the one we asked for 00503 // this avoids out of sync revision text errors we have encountered in the past 00504 $newId = fgets( $this->spawnRead ); 00505 if ( $newId === false ) { 00506 return false; 00507 } 00508 if ( $id != intval( $newId ) ) { 00509 return false; 00510 } 00511 00512 $len = fgets( $this->spawnRead ); 00513 // $this->progress( "<< " . trim( $len ) ); 00514 if ( $len === false ) return false; 00515 00516 $nbytes = intval( $len ); 00517 // actual error, not zero-length text 00518 if ($nbytes < 0 ) return false; 00519 00520 $text = ""; 00521 00522 // Subprocess may not send everything at once, we have to loop. 00523 while ( $nbytes > strlen( $text ) ) { 00524 $buffer = fread( $this->spawnRead, $nbytes - strlen( $text ) ); 00525 if ( $buffer === false ) break; 00526 $text .= $buffer; 00527 } 00528 00529 $gotbytes = strlen( $text ); 00530 if ( $gotbytes != $nbytes ) { 00531 $this->progress( "Expected $nbytes bytes from database subprocess, got $gotbytes " ); 00532 return false; 00533 } 00534 00535 // Do normalization in the dump thread... 00536 $stripped = str_replace( "\r", "", $text ); 00537 $normalized = $wgContLang->normalize( $stripped ); 00538 return $normalized; 00539 } 00540 00541 function startElement( $parser, $name, $attribs ) { 00542 $this->checkpointJustWritten = false; 00543 00544 $this->clearOpenElement( null ); 00545 $this->lastName = $name; 00546 00547 if ( $name == 'revision' ) { 00548 $this->state = $name; 00549 $this->egress->writeOpenPage( null, $this->buffer ); 00550 $this->buffer = ""; 00551 } elseif ( $name == 'page' ) { 00552 $this->state = $name; 00553 if ( $this->atStart ) { 00554 $this->egress->writeOpenStream( $this->buffer ); 00555 $this->buffer = ""; 00556 $this->atStart = false; 00557 } 00558 } 00559 00560 if ( $name == "text" && isset( $attribs['id'] ) ) { 00561 $text = $this->getText( $attribs['id'] ); 00562 $this->openElement = array( $name, array( 'xml:space' => 'preserve' ) ); 00563 if ( strlen( $text ) > 0 ) { 00564 $this->characterData( $parser, $text ); 00565 } 00566 } else { 00567 $this->openElement = array( $name, $attribs ); 00568 } 00569 } 00570 00571 function endElement( $parser, $name ) { 00572 $this->checkpointJustWritten = false; 00573 00574 if ( $this->openElement ) { 00575 $this->clearOpenElement( "" ); 00576 } else { 00577 $this->buffer .= "</$name>"; 00578 } 00579 00580 if ( $name == 'revision' ) { 00581 $this->egress->writeRevision( null, $this->buffer ); 00582 $this->buffer = ""; 00583 $this->thisRev = ""; 00584 } elseif ( $name == 'page' ) { 00585 if (! $this->firstPageWritten) { 00586 $this->firstPageWritten = trim($this->thisPage); 00587 } 00588 $this->lastPageWritten = trim($this->thisPage); 00589 if ($this->timeExceeded) { 00590 $this->egress->writeClosePage( $this->buffer ); 00591 // nasty hack, we can't just write the chardata after the 00592 // page tag, it will include leading blanks from the next line 00593 $this->egress->sink->write("\n"); 00594 00595 $this->buffer = $this->xmlwriterobj->closeStream(); 00596 $this->egress->writeCloseStream( $this->buffer ); 00597 00598 $this->buffer = ""; 00599 $this->thisPage = ""; 00600 // this could be more than one file if we had more than one output arg 00601 00602 $filenameList = (array)$this->egress->getFilenames(); 00603 $newFilenames = array(); 00604 $firstPageID = str_pad($this->firstPageWritten,9,"0",STR_PAD_LEFT); 00605 $lastPageID = str_pad($this->lastPageWritten,9,"0",STR_PAD_LEFT); 00606 for ( $i = 0; $i < count( $filenameList ); $i++ ) { 00607 $checkpointNameFilledIn = sprintf( $this->checkpointFiles[$i], $firstPageID, $lastPageID ); 00608 $fileinfo = pathinfo($filenameList[$i]); 00609 $newFilenames[] = $fileinfo['dirname'] . '/' . $checkpointNameFilledIn; 00610 } 00611 $this->egress->closeRenameAndReopen( $newFilenames ); 00612 $this->buffer = $this->xmlwriterobj->openStream(); 00613 $this->timeExceeded = false; 00614 $this->timeOfCheckpoint = $this->lastTime; 00615 $this->firstPageWritten = false; 00616 $this->checkpointJustWritten = true; 00617 } 00618 else { 00619 $this->egress->writeClosePage( $this->buffer ); 00620 $this->buffer = ""; 00621 $this->thisPage = ""; 00622 } 00623 00624 } elseif ( $name == 'mediawiki' ) { 00625 $this->egress->writeCloseStream( $this->buffer ); 00626 $this->buffer = ""; 00627 } 00628 } 00629 00630 function characterData( $parser, $data ) { 00631 $this->clearOpenElement( null ); 00632 if ( $this->lastName == "id" ) { 00633 if ( $this->state == "revision" ) { 00634 $this->thisRev .= $data; 00635 } elseif ( $this->state == "page" ) { 00636 $this->thisPage .= $data; 00637 } 00638 } 00639 // have to skip the newline left over from closepagetag line of 00640 // end of checkpoint files. nasty hack!! 00641 if ($this->checkpointJustWritten) { 00642 if ($data[0] == "\n") { 00643 $data = substr($data,1); 00644 } 00645 $this->checkpointJustWritten = false; 00646 } 00647 $this->buffer .= htmlspecialchars( $data ); 00648 } 00649 00650 function clearOpenElement( $style ) { 00651 if ( $this->openElement ) { 00652 $this->buffer .= Xml::element( $this->openElement[0], $this->openElement[1], $style ); 00653 $this->openElement = false; 00654 } 00655 } 00656 } 00657 00658 00659 $dumper = new TextPassDumper( $argv ); 00660 00661 if ( !isset( $options['help'] ) ) { 00662 $dumper->dump( true ); 00663 } else { 00664 $dumper->progress( <<<ENDS 00665 This script postprocesses XML dumps from dumpBackup.php to add 00666 page text which was stubbed out (using --stub). 00667 00668 XML input is accepted on stdin. 00669 XML output is sent to stdout; progress reports are sent to stderr. 00670 00671 Usage: php dumpTextPass.php [<options>] 00672 Options: 00673 --stub=<type>:<file> To load a compressed stub dump instead of stdin 00674 --prefetch=<type>:<file> Use a prior dump file as a text source, to save 00675 pressure on the database. 00676 (Requires the XMLReader extension) 00677 --maxtime=<minutes> Write out checkpoint file after this many minutes (writing 00678 out complete page, closing xml file properly, and opening new one 00679 with header). This option requires the checkpointfile option. 00680 --checkpointfile=<filenamepattern> Use this string for checkpoint filenames, 00681 substituting first pageid written for the first %s (required) and the 00682 last pageid written for the second %s if it exists. 00683 --quiet Don't dump status reports to stderr. 00684 --report=n Report position and speed after every n pages processed. 00685 (Default: 100) 00686 --server=h Force reading from MySQL server h 00687 --current Base ETA on number of pages in database instead of all revisions 00688 --spawn Spawn a subprocess for loading text records 00689 --help Display this help message 00690 ENDS 00691 ); 00692 } 00693 00694