Changeset 81 for trunk/src/helpers/dosh.c
- Timestamp:
- Jun 23, 2001, 11:12:49 AM (24 years ago)
- File:
-
- 1 edited
Legend:
- Unmodified
- Added
- Removed
-
trunk/src/helpers/dosh.c
r73 r81 251 251 252 252 /* 253 *@@ doshDevIOCtl: 254 * 255 * Works with those IOCtls where the buffer 256 * size parameters are always the same anyway, 257 * which applies to all IOCtls I have seen 258 * so far. 259 * 260 *@@added V0.9.13 (2001-06-14) [umoeller] 261 */ 262 263 APIRET doshDevIOCtl(HFILE hf, 264 ULONG ulCategory, 265 ULONG ulFunction, 266 PVOID pvParams, 267 ULONG cbParams, 268 PVOID pvData, 269 ULONG cbData) 270 { 271 return (DosDevIOCtl(hf, 272 ulCategory, 273 ulFunction, 274 pvParams, cbParams, &cbParams, 275 pvData, cbData, &cbData)); 276 } 277 278 /* 253 279 *@@category: Helpers\Control program helpers\Shared memory management 254 280 * helpers for allocating and requesting shared memory. … … 285 311 { 286 312 PVOID pvrc = NULL; 287 APIRET arc = DosAllocSharedMem((PVOID*) (&pvrc),313 APIRET arc = DosAllocSharedMem((PVOID*)&pvrc, 288 314 (PSZ)pcszName, 289 315 ulSize, … … 312 338 { 313 339 PVOID pvrc = NULL; 314 APIRET arc = DosGetNamedSharedMem((PVOID*) (pvrc),340 APIRET arc = DosGetNamedSharedMem((PVOID*)pvrc, 315 341 (PSZ)pcszName, 316 342 PAG_READ | PAG_WRITE); … … 332 358 * 333 359 ********************************************************************/ 360 361 /* 362 *@@ doshIsFixedDisk: 363 * checks whether a disk is fixed or removeable. 364 * ulLogicalDrive must be 1 for drive A:, 2 for B:, ... 365 * The result is stored in *pfFixed. 366 * Returns DOS error code. 367 * 368 * From my testing, this function does _not_ provoke 369 * "drive not ready" popups, even if the disk is not 370 * ready. 371 * 372 * Warning: This uses DosDevIOCtl, which has proved 373 * to cause problems with some device drivers for 374 * removeable disks. 375 */ 376 377 APIRET doshIsFixedDisk(ULONG ulLogicalDrive, // in: 1 for A:, 2 for B:, 3 for C:, ... 378 PBOOL pfFixed) // out: TRUE for fixed disks 379 { 380 APIRET arc = ERROR_INVALID_DRIVE; 381 382 if (ulLogicalDrive) 383 { 384 // parameter packet 385 #pragma pack(1) 386 struct { 387 UCHAR command, 388 drive; 389 } parms; 390 #pragma pack() 391 392 // data packet 393 UCHAR ucNonRemoveable; 394 395 parms.drive = (UCHAR)(ulLogicalDrive-1); 396 arc = doshDevIOCtl((HFILE)-1, 397 IOCTL_DISK, 398 DSK_BLOCKREMOVABLE, 399 &parms, sizeof(parms), 400 &ucNonRemoveable, sizeof(ucNonRemoveable)); 401 402 if (arc == NO_ERROR) 403 *pfFixed = (BOOL)ucNonRemoveable; 404 } 405 406 return (arc); 407 } 408 409 /* 410 *@@ doshQueryDiskParams: 411 * this retrieves more information about a given drive, 412 * which is stored in the specified BIOSPARAMETERBLOCK 413 * structure. 414 * 415 * BIOSPARAMETERBLOCK is defined in the Toolkit headers, 416 * and from my testing, it's the same with the Toolkits 417 * 3 and 4.5. 418 * 419 * If NO_ERROR is returned, the bDeviceType field can 420 * be one of the following (according to CPREF): 421 * 422 * -- 0: 48 TPI low-density diskette drive 423 * -- 1: 96 TPI high-density diskette drive 424 * -- 2: 3.5-inch 720KB diskette drive 425 * -- 3: 8-Inch single-density diskette drive 426 * -- 4: 8-Inch double-density diskette drive 427 * -- 5: Fixed disk 428 * -- 6: Tape drive 429 * -- 7: Other (includes 1.44MB 3.5-inch diskette drive) 430 * -- 8: R/W optical disk 431 * -- 9: 3.5-inch 4.0MB diskette drive (2.88MB formatted) 432 * 433 * From my testing, this function does _not_ provoke 434 * "drive not ready" popups, even if the disk is not 435 * ready. 436 * 437 * Warning: This uses DosDevIOCtl, which has proved 438 * to cause problems with some device drivers for 439 * removeable disks. 440 * 441 * This returns the DOS error code of DosDevIOCtl. 442 * 443 *@@added V0.9.0 [umoeller] 444 *@@changed V0.9.13 (2001-06-14) [umoeller]: changed prototype to use BIOSPARAMETERBLOCK directly 445 *@@changed V0.9.13 (2001-06-14) [umoeller]: now querying standard media, no redetermine 446 */ 447 448 APIRET doshQueryDiskParams(ULONG ulLogicalDrive, // in: 1 for A:, 2 for B:, 3 for C:, ... 449 PBIOSPARAMETERBLOCK pdp) // out: drive parameters 450 { 451 APIRET arc = ERROR_INVALID_DRIVE; 452 453 if (ulLogicalDrive) 454 { 455 #pragma pack(1) 456 // parameter packet 457 struct { 458 UCHAR ucCommand, 459 ucDrive; 460 } parms; 461 #pragma pack() 462 463 parms.ucCommand = 0; // 0 = return standard media, 464 // 1 = read currently inserted media 465 // (1 doesn't work any more, returns arc 87 466 // V0.9.13 (2001-06-14) [umoeller]) 467 parms.ucDrive=(UCHAR)(ulLogicalDrive-1); 468 469 // zero the structure V0.9.13 (2001-06-14) [umoeller] 470 memset(pdp, 0, sizeof(BIOSPARAMETERBLOCK)); 471 472 arc = doshDevIOCtl((HFILE)-1, 473 IOCTL_DISK, 474 DSK_GETDEVICEPARAMS, 475 &parms, sizeof(parms), 476 pdp, sizeof(BIOSPARAMETERBLOCK)); 477 478 if (!arc) 479 { 480 _Pmpf((" bDeviceType: %d", pdp->bDeviceType)); 481 _Pmpf((" bytes per sector: %d", pdp->usBytesPerSector)); 482 _Pmpf((" sectors per track: %d", pdp->usSectorsPerTrack)); 483 } 484 } 485 486 return (arc); 487 } 488 489 /* 490 *@@ doshIsCDROM: 491 * tests the specified BIOSPARAMETERBLOCK 492 * for whether it represents a CD-ROM drive. 493 * 494 * The BIOSPARAMETERBLOCK must be filled 495 * first using doshQueryDiskParams. 496 * 497 *@@added V0.9.13 (2001-06-14) [umoeller] 498 */ 499 500 BOOL doshIsCDROM(PBIOSPARAMETERBLOCK pdp) 501 { 502 return ( (pdp) 503 && (pdp->bDeviceType == 7) // "other" 504 && (pdp->usBytesPerSector == 2048) 505 && (pdp->usSectorsPerTrack == (USHORT)-1) 506 ); 507 } 334 508 335 509 /* … … 376 550 while (ulLogicalDrive <= 26) 377 551 { 378 UCHAR nonRemovable=0;379 ULONG parmSize=2;380 ULONG dataLen=1;381 382 552 #pragma pack(1) 383 553 struct … … 387 557 #pragma pack() 388 558 559 // data packet 560 UCHAR nonRemovable=0; 561 389 562 parms.drive=(UCHAR)(ulLogicalDrive-1); 390 arc = DosDevIOCtl((HFILE)-1, 391 IOCTL_DISK, 392 DSK_BLOCKREMOVABLE, 393 &parms, 394 parmSize, 395 &parmSize, 396 &nonRemovable, 397 1, 398 &dataLen); 563 arc = doshDevIOCtl((HFILE)-1, 564 IOCTL_DISK, 565 DSK_BLOCKREMOVABLE, 566 &parms, sizeof(parms), 567 &nonRemovable, sizeof(nonRemovable)); 399 568 400 569 if ( // fixed disk and non-removeable … … 468 637 * those ugly white "Drive not ready" popups. 469 638 * 639 * "fl" can specify additional flags for testing 640 * and can be any combination of: 641 * 642 * -- ASSERTFL_MIXEDMODECD: whether to allow 643 * mixed-mode CD-ROMs. See error codes below. 644 * 470 645 * This returns (from my testing): 646 * 471 647 * -- NO_ERROR: drive is available. 648 * 472 649 * -- ERROR_INVALID_DRIVE (15): drive letter does not exist. 473 * -- ERROR_NOT_READY (21): drive exists, but is not ready 474 * (e.g. CD-ROM drive without CD inserted). 475 * -- ERROR_NOT_SUPPORTED (50): this is returned by the RAMFS.IFS 476 * file system; apparently, the IFS doesn't support 477 * DASD opening. 650 * 651 * -- ERROR_NOT_READY (21): drive exists, but is not ready. 652 * This is produced by floppies and CD-ROM drives 653 * which do not have valid media inserted. 654 * 655 * -- ERROR_AUDIO_CD_ROM (10000): special error code returned 656 * only by this function if a CD-ROM drive has audio 657 * media inserted. 658 * 659 * If ASSERTFL_MIXEDMODECD was specified, ERROR_AUDIO_CD_ROM 660 * is returned _only_ if _no_ data tracks are 661 * present on a CD-ROM. Since OS/2 is not very 662 * good at handling mixed-mode CDs, this might not 663 * be desireable. 664 * 665 * If ASSERTFL_MIXEDMODECD was not set, ERROR_AUDIO_CD_ROM 666 * will be returned already if _one_ audio track is present. 478 667 * 479 668 *@@changed V0.9.1 (99-12-13) [umoeller]: rewritten, prototype changed. Now using DosOpen on the drive instead of DosError. … … 484 673 *@@changed V0.9.9 (2001-03-19) [pr]: validate drive number 485 674 *@@changed V0.9.11 (2001-04-23) [umoeller]: added an extra check for floppies 486 */ 487 488 APIRET doshAssertDrive(ULONG ulLogicalDrive) // in: 1 for A:, 2 for B:, 3 for C:, ... 489 { 490 CHAR szDrive[3] = "C:"; 675 *@@changed V0.9.13 (2001-06-14) [umoeller]: added "fl" parameter and lots of CD-ROM checks 676 */ 677 678 APIRET doshAssertDrive(ULONG ulLogicalDrive, // in: 1 for A:, 2 for B:, 3 for C:, ... 679 ULONG fl) // in: ASSERTFL_* flags 680 { 491 681 HFILE hfDrive = 0; 492 682 ULONG ulTemp = 0; 493 APIRET arc; 683 APIRET arc = NO_ERROR; 684 BOOL fFixed = FALSE, 685 fCDROM = FALSE; 494 686 495 687 if ((ulLogicalDrive < 1) || (ulLogicalDrive > 26)) 496 688 return(ERROR_PATH_NOT_FOUND); 497 689 498 szDrive[0] = 'A' + ulLogicalDrive - 1; 499 500 arc = DosOpen(szDrive, // "C:", "D:", ... 501 &hfDrive, 502 &ulTemp, 503 0, 504 FILE_NORMAL, 505 OPEN_ACTION_FAIL_IF_NEW 506 | OPEN_ACTION_OPEN_IF_EXISTS, 507 OPEN_FLAGS_DASD 508 | OPEN_FLAGS_FAIL_ON_ERROR 509 | OPEN_FLAGS_NOINHERIT // V0.9.6 (2000-11-25) [pr] 510 | OPEN_ACCESS_READONLY 511 | OPEN_SHARE_DENYNONE, 512 NULL); 513 514 // _Pmpf((__FUNCTION__ ": DosOpen(OPEN_FLAGS_DASD) returned %d", arc)); 690 arc = doshIsFixedDisk(ulLogicalDrive, 691 &fFixed); // V0.9.13 (2001-06-14) [umoeller] 692 693 _Pmpf((__FUNCTION__ ": doshIsFixedDisk returned %d for disk %d", arc, ulLogicalDrive)); 694 _Pmpf((" fFixed is %d", fFixed)); 695 696 if (!arc) 697 if (!fFixed) 698 { 699 // removeable disk: 700 // check if it's a CD-ROM 701 BIOSPARAMETERBLOCK bpb; 702 arc = doshQueryDiskParams(ulLogicalDrive, 703 &bpb); 704 _Pmpf((" doshQueryDiskParams returned %d", arc)); 705 706 if ( (!arc) 707 && (doshIsCDROM(&bpb)) 708 ) 709 { 710 _Pmpf((" --> is CD-ROM")); 711 fCDROM = TRUE; 712 } 713 } 714 715 if (!arc) 716 { 717 CHAR szDrive[3] = "C:"; 718 szDrive[0] = 'A' + ulLogicalDrive - 1; 719 arc = DosOpen(szDrive, // "C:", "D:", ... 720 &hfDrive, 721 &ulTemp, 722 0, 723 FILE_NORMAL, 724 OPEN_ACTION_FAIL_IF_NEW 725 | OPEN_ACTION_OPEN_IF_EXISTS, 726 OPEN_FLAGS_DASD 727 | OPEN_FLAGS_FAIL_ON_ERROR 728 | OPEN_FLAGS_NOINHERIT // V0.9.6 (2000-11-25) [pr] 729 // | OPEN_ACCESS_READONLY // V0.9.13 (2001-06-14) [umoeller] 730 | OPEN_SHARE_DENYNONE, 731 NULL); 732 733 _Pmpf((" DosOpen(OPEN_FLAGS_DASD) returned %d", arc)); 734 735 // this still returns NO_ERROR for audio CDs in a 736 // CD-ROM drive... 737 // however, the WPS then attempts to read in the 738 // root directory for audio CDs, which produces 739 // a "sector not found" error box... 740 741 if (!arc && hfDrive && fCDROM) // determined above 742 { 743 ULONG ulAudioTracks = 0, 744 ulDataTracks = 0; 745 746 CHAR cds1[4] = { 'C', 'D', '0', '1' }; 747 CHAR cds2[4]; 748 // check for proper driver signature 749 if (!(arc = doshDevIOCtl(hfDrive, 750 IOCTL_CDROMDISK, 751 CDROMDISK_GETDRIVER, 752 &cds1, sizeof(cds1), 753 &cds2, sizeof(cds2)))) 754 { 755 if (memcmp(&cds1, &cds2, 4)) 756 // this is not a CD-ROM then: 757 arc = NO_ERROR; 758 else 759 { 760 struct { 761 UCHAR ucFirstTrack, 762 ucLastTrack; 763 ULONG ulLeadOut; 764 } cdat; 765 766 // get track count 767 if (!(arc = doshDevIOCtl(hfDrive, 768 IOCTL_CDROMAUDIO, 769 CDROMAUDIO_GETAUDIODISK, 770 &cds1, sizeof(cds1), 771 &cdat, sizeof(cdat)))) 772 { 773 // still no error: build the audio TOC 774 ULONG i; 775 for (i = cdat.ucFirstTrack; 776 i <= cdat.ucLastTrack; 777 i++) 778 { 779 BYTE cdtp[5] = 780 { 'C', 'D', '0', '1', (UCHAR)i }; 781 782 struct { 783 ULONG ulTrackAddress; 784 BYTE bFlags; 785 } trackdata; 786 787 if (!(arc = doshDevIOCtl(hfDrive, 788 IOCTL_CDROMAUDIO, 789 CDROMAUDIO_GETAUDIOTRACK, 790 &cdtp, sizeof(cdtp), 791 &trackdata, sizeof(trackdata)))) 792 { 793 if (trackdata.bFlags & 64) 794 ulDataTracks++; 795 else 796 { 797 ulAudioTracks++; 798 799 if (!(fl & ASSERTFL_MIXEDMODECD)) 800 { 801 // caller doesn't want mixed mode: 802 // stop here 803 ulDataTracks = 0; 804 break; 805 } 806 } 807 } 808 } 809 810 _Pmpf((" got %d audio, %d data tracks", 811 ulAudioTracks, ulDataTracks)); 812 813 if (!ulDataTracks) 814 arc = ERROR_AUDIO_CD_ROM; // special private error code (10000) 815 } 816 else 817 { 818 // not audio disk: 819 // go on then 820 _Pmpf((" CDROMAUDIO_GETAUDIODISK returned %d", arc)); 821 arc = NO_ERROR; 822 } 823 } 824 } 825 else 826 { 827 // not CD-ROM: go on then 828 _Pmpf((" CDROMDISK_GETDRIVER returned %d", arc)); 829 arc = NO_ERROR; 830 } 831 } 832 } 515 833 516 834 switch (arc) … … 544 862 &fsa, 545 863 sizeof(fsa)); 864 _Pmpf((" re-checked, DosQueryFSInfo returned %d", arc)); 546 865 } 547 866 break; 548 549 case NO_ERROR: 550 DosClose(hfDrive); 551 break; 552 } 867 } 868 869 if (hfDrive) 870 DosClose(hfDrive); 553 871 554 872 return (arc); … … 597 915 paramsize = sizeof(param); 598 916 datasize = sizeof(data); 599 rc = DosDevIOCtl(fd,600 IOCTL_DISK, DSK_SETLOGICALMAP,601 ¶m, paramsize, ¶msize,602 &data, datasize, &datasize);917 rc = doshDevIOCtl(fd, 918 IOCTL_DISK, DSK_SETLOGICALMAP, 919 ¶m, sizeof(param), 920 &data, sizeof(data)); 603 921 DosClose(fd); 604 922 } … … 700 1018 (CHAR*)(&pfsqBuffer->szName) + pfsqBuffer->cbName + 1); 701 1019 } 702 }703 704 return (arc);705 }706 707 /*708 *@@ doshIsFixedDisk:709 * checks whether a disk is fixed or removeable.710 * ulLogicalDrive must be 1 for drive A:, 2 for B:, ...711 * The result is stored in *pfFixed.712 * Returns DOS error code.713 *714 * Warning: This uses DosDevIOCtl, which has proved715 * to cause problems with some device drivers for716 * removeable disks.717 */718 719 APIRET doshIsFixedDisk(ULONG ulLogicalDrive, // in: 1 for A:, 2 for B:, 3 for C:, ...720 PBOOL pfFixed) // out: TRUE for fixed disks721 {722 APIRET arc = ERROR_INVALID_DRIVE;723 724 if (ulLogicalDrive)725 {726 // parameter packet727 #pragma pack(1)728 struct {729 UCHAR command, drive;730 } parms;731 #pragma pack()732 733 // data packet734 UCHAR ucNonRemoveable;735 736 ULONG ulParmSize = sizeof(parms);737 ULONG ulDataSize = sizeof(ucNonRemoveable);738 739 parms.drive = (UCHAR)(ulLogicalDrive-1);740 arc = DosDevIOCtl((HFILE)-1,741 IOCTL_DISK,742 DSK_BLOCKREMOVABLE,743 &parms,744 ulParmSize,745 &ulParmSize,746 &ucNonRemoveable,747 ulDataSize,748 &ulDataSize);749 750 if (arc == NO_ERROR)751 *pfFixed = (BOOL)ucNonRemoveable;752 }753 754 return (arc);755 }756 757 /*758 *@@ doshQueryDiskParams:759 * this retrieves more information about a given drive,760 * which is stored in the specified DRIVEPARAMS structure761 * (dosh.h).762 *763 * Warning: This uses DosDevIOCtl, which has proved764 * to cause problems with some device drivers for765 * removeable disks.766 *767 * This returns the DOS error code of DosDevIOCtl.768 *769 *@@added V0.9.0 [umoeller]770 */771 772 APIRET doshQueryDiskParams(ULONG ulLogicalDrive, // in: 1 for A:, 2 for B:, 3 for C:, ...773 PDRIVEPARAMS pdp) // out: drive parameters774 {775 APIRET arc = ERROR_INVALID_DRIVE;776 777 if (ulLogicalDrive)778 {779 #pragma pack(1)780 // parameter packet781 struct {782 UCHAR command, drive;783 } parms;784 #pragma pack()785 786 ULONG ulParmSize = sizeof(parms);787 ULONG ulDataSize = sizeof(DRIVEPARAMS);788 789 parms.command = 1; // read currently inserted media790 parms.drive=(UCHAR)(ulLogicalDrive-1);791 792 arc = DosDevIOCtl((HFILE)-1,793 IOCTL_DISK,794 DSK_GETDEVICEPARAMS,795 // parameter packet:796 &parms, ulParmSize, &ulParmSize,797 // data packet: DRIVEPARAMS structure798 pdp, ulDataSize, &ulDataSize);799 1020 } 800 1021 … … 1109 1330 } 1110 1331 return (rc); 1332 } 1333 1334 /* 1335 *@@ doshOpenExisting: 1336 * opens an existing file for read-write access. Does 1337 * not create a new file if the file doesn't exist. 1338 * 1339 * This is just a simple wrapper around DosOpen. 1340 * 1341 * ulOpenFlags is passed to DosOpen. Should be one 1342 * of: 1343 * 1344 * -- for read-only access: 1345 * 1346 + OPEN_SHARE_DENYNONE | OPEN_ACCESS_READONLY 1347 * 1348 * -- for read-write access: 1349 * 1350 + OPEN_SHARE_DENYREADWRITE | OPEN_ACCESS_READWRITE 1351 * 1352 * In addition, you can specify 1353 * 1354 + OPEN_FLAGS_FAIL_ON_ERROR | OPEN_FLAGS_RANDOM 1355 + | OPEN_FLAGS_NOINHERIT 1356 * 1357 *@@added V0.9.13 (2001-06-14) [umoeller] 1358 */ 1359 1360 APIRET doshOpenExisting(const char *pcszFilename, // in: file name 1361 ULONG ulOpenFlags, // in: open flags 1362 HFILE *phf) // out: OS/2 file handle 1363 { 1364 ULONG ulAction; 1365 return (DosOpen((PSZ)pcszFilename, 1366 phf, 1367 &ulAction, 1368 0, // cbFile 1369 0, // attributes 1370 OPEN_ACTION_FAIL_IF_NEW | OPEN_ACTION_OPEN_IF_EXISTS, 1371 ulOpenFlags, 1372 NULL)); // EAs 1373 } 1374 1375 /* 1376 *@@ doshWriteAt: 1377 * writes cb bytes (pointed to by pbData) to the 1378 * position specified by ulMethod and lOffset into 1379 * the file specified by hf. 1380 * 1381 * If ulMethod is FILE_BEGIN, lOffset specifies the 1382 * offset from the beginning of the file. With 1383 * FILE_CURRENT, lOffset is considered from the 1384 * current file pointer, and with FILE_END, it is 1385 * considered from the end of the file. 1386 * 1387 *@@added V0.9.13 (2001-06-14) [umoeller] 1388 */ 1389 1390 APIRET doshWriteAt(HFILE hf, // in: OS/2 file handle 1391 LONG lOffset, // in: offset to write at (depends on ulMethod) 1392 ULONG ulMethod, // in: one of FILE_BEGIN, FILE_CURRENT, FILE_END 1393 ULONG cb, // in: bytes to write 1394 PBYTE pbData) // in: ptr to bytes to write (must be cb bytes) 1395 { 1396 APIRET arc; 1397 ULONG ulDummy; 1398 if (!(arc = DosSetFilePtr(hf, 1399 lOffset, 1400 ulMethod, 1401 &ulDummy))) 1402 arc = DosWrite(hf, 1403 pbData, 1404 cb, 1405 &ulDummy); 1406 1407 return (arc); 1408 } 1409 1410 /* 1411 *@@ doshReadAt: 1412 * reads cb bytes from the position specified by 1413 * ulMethod and lOffset into the buffer pointed to 1414 * by pbData, which should be cb bytes in size. 1415 * 1416 * Use lOffset and ulMethod as with doshWriteAt. 1417 * 1418 *@@added V0.9.13 (2001-06-14) [umoeller] 1419 */ 1420 1421 APIRET doshReadAt(HFILE hf, // in: OS/2 file handle 1422 LONG lOffset, // in: offset to write at (depends on ulMethod) 1423 ULONG ulMethod, // in: one of FILE_BEGIN, FILE_CURRENT, FILE_END 1424 ULONG cb, // in: bytes to write 1425 PBYTE pbData) // out: read buffer (must be cb bytes) 1426 { 1427 APIRET arc; 1428 ULONG ulDummy; 1429 if (!(arc = DosSetFilePtr(hf, 1430 lOffset, 1431 ulMethod, 1432 &ulDummy))) 1433 arc = DosRead(hf, 1434 pbData, 1435 cb, 1436 &ulDummy); 1437 1438 return (arc); 1111 1439 } 1112 1440
Note:
See TracChangeset
for help on using the changeset viewer.