/*++ Copyright (c) 1989-1999 Microsoft Corporation Module Name: fspyLog.c Abstract: This module contains functions used to retrieve and see the log records recorded by filespy.sys. // @@BEGIN_DDKSPLIT Author: Molly Brown (MollyBro) 21-Apr-1999 // @@END_DDKSPLIT Environment: User mode // @@BEGIN_DDKSPLIT Revision History: // @@END_DDKSPLIT --*/ #include #include #include #include #include "ioTestLog.h" #include "ioTestLib.h" #define TIME_BUFFER_LENGTH 20 #define TIME_ERROR L"time error" #define FlagOn(F,SF) ( \ (((F) & (SF))) \ ) VOID DumpLogRecord ( PLOG_RECORD pLogRecord, PLOG_CONTEXT context ) { PRECORD_IRP pRecordIrp; PRECORD_FASTIO pRecordFastIo; PRECORD_FS_FILTER_OPERATION pRecordFsFilterOp; ULONG nameLength; // // Calculate the length of the name in the log record. // nameLength = pLogRecord->Length - SIZE_OF_LOG_RECORD; // // A LOG_RECORD could have Irp or FastIo data in it. This // is denoted in the low-order byte of the RecordType flag. // switch (GET_RECORD_TYPE(pLogRecord)) { case RECORD_TYPE_IRP: // // We've got an Irp record, so output this data correctly. // pRecordIrp = &(pLogRecord->Record.RecordIrp); if (context->LogToScreen) { IrpScreenDump( pLogRecord->DeviceType, pLogRecord->SequenceNumber, pLogRecord->Name, nameLength, pRecordIrp, context->VerbosityFlags); } if (context->LogToFile) { IrpFileDump( pLogRecord->DeviceType, pLogRecord->SequenceNumber, pLogRecord->Name, nameLength, pRecordIrp, context->OutputFile, context->VerbosityFlags); } break; case RECORD_TYPE_FASTIO: // // We've got a FastIo record, so output this data correctly. // pRecordFastIo = &(pLogRecord->Record.RecordFastIo); if (context->LogToScreen) { FastIoScreenDump( pLogRecord->DeviceType, pLogRecord->SequenceNumber, pLogRecord->Name, nameLength, pRecordFastIo); } if (context->LogToFile) { FastIoFileDump( pLogRecord->DeviceType, pLogRecord->SequenceNumber, pLogRecord->Name, nameLength, pRecordFastIo, context->OutputFile); } break; case RECORD_TYPE_FS_FILTER_OP: // // We've got a FsFilter operation record, so output this // data correctly. // pRecordFsFilterOp = &(pLogRecord->Record.RecordFsFilterOp); if (context->LogToScreen) { FsFilterOperationScreenDump( pLogRecord->DeviceType, pLogRecord->SequenceNumber, pLogRecord->Name, nameLength, pRecordFsFilterOp ); } if (context->LogToFile) { FsFilterOperationFileDump( pLogRecord->DeviceType, pLogRecord->SequenceNumber, pLogRecord->Name, nameLength, pRecordFsFilterOp, context->OutputFile ); } break; default: printf("Filmon: Unknown log record type\n"); } // // The RecordType could also designate that we are out of memory // or hit our program defined memory limit, so check for these // cases. // if (pLogRecord->RecordType & RECORD_TYPE_OUT_OF_MEMORY) { if (context->LogToScreen) { printf("M %08X SYSTEM OUT OF MEMORY\n", pLogRecord->SequenceNumber); } if (context->LogToFile) { fprintf(context->OutputFile, "M:\t%u", pLogRecord->SequenceNumber); } } else if (pLogRecord->RecordType & RECORD_TYPE_EXCEED_MEMORY_ALLOWANCE) { if (context->LogToScreen) { printf("M %08X EXCEEDED MEMORY ALLOWANCE\n", pLogRecord->SequenceNumber); } if (context->LogToFile) { fprintf(context->OutputFile, "M:\t%u", pLogRecord->SequenceNumber); } } } DWORD WINAPI VerifyCurrentLogRecords ( PLOG_CONTEXT Context, PEXPECTED_OPERATION ExpectedOps ) { CHAR buffer[BUFFER_SIZE]; DWORD bytesReturned = 0; BOOL bResult; DWORD result; PLOG_RECORD pLogRecord; BOOL askForMore = TRUE; BOOL testPassed = FALSE; ULONG currentOp = 0; #ifdef USE_DO_HINT BOOL keepVerifying = TRUE; #else BOOL seenFirstOp = FALSE; #endif /* USE_DO_HINT */ while (askForMore) { // // Check to see if we should shut down // if (Context->CleaningUp) { break; } // // Request log data from filespy // bResult = DeviceIoControl( Context->Device, IOTEST_GetLog, NULL, 0, buffer, BUFFER_SIZE, &bytesReturned, NULL); if (!bResult) { result = GetLastError(); printf("ERROR controlling device: 0x%x\n", result); } // // Buffer is filled with a series of LOG_RECORD structures, one // right after another. Each LOG_RECORD says how long it is, so // we know where the next LOG_RECORD begins. // pLogRecord = (PLOG_RECORD) buffer; // // Logic to write record to screen and/or file // while ((BYTE *) pLogRecord < buffer + bytesReturned) { PRECORD_IRP pRecordIrp; DumpLogRecord( pLogRecord, Context ); switch (GET_RECORD_TYPE(pLogRecord)) { case RECORD_TYPE_IRP: pRecordIrp = &(pLogRecord->Record.RecordIrp); #ifdef USE_DO_HINT if (keepVerifying) { if (pRecordIrp->IrpMajor == ExpectedOps[currentOp].Op) { if(pLogRecord->DeviceType != ExpectedOps[currentOp].Device) { keepVerifying = FALSE; } else { currentOp ++; } } } #else // // If we are not using the DeviceObject hint, we expect to // see log entries from both the TOP and BOTTOM filters // for each expected operation. So that we don't have to // redefine the ExpectedOperation array, just ignore the // device in the array and check to make sure that you see // a log record for the TOP_FILTER then the BOTTOM_FILTER for // each operation in the array. // if (!seenFirstOp) { if ((pLogRecord->DeviceType == TOP_FILTER) && (pRecordIrp->IrpMajor == ExpectedOps[currentOp].Op)) { seenFirstOp = TRUE; } } else { if ((pLogRecord->DeviceType == BOTTOM_FILTER) && (pRecordIrp->IrpMajor == ExpectedOps[currentOp].Op)) { seenFirstOp = FALSE; currentOp ++; } } #endif /* USE_DO_HINT */ break; default: // // ignore // ; } // // Move to next LOG_RECORD // pLogRecord = (PLOG_RECORD) (((BYTE *) pLogRecord) + pLogRecord->Length); } if (bytesReturned == 0) { askForMore = FALSE; } } if (ExpectedOps[currentOp].Op == IRP_MJ_MAXIMUM_FUNCTION + 1) { testPassed = TRUE; } if (testPassed) { printf( "User log verification:\tPASSED - Expected operations seen.\n" ); } else { printf( "User log verification:\tFAILED - Expected operations NOT seen.\n" ); } return testPassed; } DWORD WINAPI RetrieveLogRecords ( LPVOID lpParameter ) { PLOG_CONTEXT context = (PLOG_CONTEXT)lpParameter; CHAR buffer[BUFFER_SIZE]; DWORD bytesReturned = 0; BOOL bResult; DWORD result; PLOG_RECORD pLogRecord; printf("Log: Starting up\n"); while (TRUE) { // // Check to see if we should shut down // if (context->CleaningUp) { break; } // // Request log data from filespy // bResult = DeviceIoControl( context->Device, IOTEST_GetLog, NULL, 0, buffer, BUFFER_SIZE, &bytesReturned, NULL); if (!bResult) { result = GetLastError(); printf("ERROR controlling device: 0x%x\n", result); } // // Buffer is filled with a series of LOG_RECORD structures, one // right after another. Each LOG_RECORD says how long it is, so // we know where the next LOG_RECORD begins. // pLogRecord = (PLOG_RECORD) buffer; // // Logic to write record to screen and/or file // while ((BYTE *) pLogRecord < buffer + bytesReturned) { DumpLogRecord( pLogRecord, context ); // // Move to next LOG_RECORD // pLogRecord = (PLOG_RECORD) (((BYTE *) pLogRecord) + pLogRecord->Length); } if (bytesReturned == 0) { Sleep( 500 ); } } printf("Log: Shutting down\n"); ReleaseSemaphore(context->ShutDown, 1, NULL); printf("Log: All done\n"); return 0; } VOID PrintIrpCode ( UCHAR MajorCode, UCHAR MinorCode, ULONG FsControlCode, FILE *OutputFile, BOOLEAN PrintMajorCode ) { CHAR irpMajorString[OPERATION_NAME_BUFFER_SIZE]; CHAR irpMinorString[OPERATION_NAME_BUFFER_SIZE]; CHAR formatBuf[OPERATION_NAME_BUFFER_SIZE*2]; GetIrpName(MajorCode,MinorCode,FsControlCode,irpMajorString,irpMinorString); if (OutputFile) { sprintf(formatBuf, "%s %s", irpMajorString, irpMinorString); fprintf(OutputFile, "\t%-50s", formatBuf); } else { if (PrintMajorCode) { printf("%-31s ", irpMajorString); } else { if (irpMinorString[0] != 0) { printf(" %-35s\n", irpMinorString); } } } } VOID PrintFastIoType ( FASTIO_TYPE Code, FILE *OutputFile ) { CHAR outputString[OPERATION_NAME_BUFFER_SIZE]; GetFastioName(Code,outputString); if (OutputFile) { fprintf(OutputFile, "%-50s", outputString); } else { printf("%-31s ", outputString); } } VOID PrintFsFilterOperation ( UCHAR Operation, FILE *OutputFile ) { CHAR outputString[OPERATION_NAME_BUFFER_SIZE]; GetFsFilterOperationName(Operation,outputString); if (OutputFile) { fprintf( OutputFile, "%-50s", outputString ); } else { printf( "%-31s ", outputString ); } } ULONG FormatSystemTime ( SYSTEMTIME *SystemTime, PWCHAR Buffer, ULONG BufferLength ) /*++ Routine Name: FormatSystemTime Routine Description: Formats the values in a SystemTime struct into the buffer passed in. The resulting string is NULL terminated. The format for the time is: hours:minutes:seconds:milliseconds Arguments: SystemTime - the struct to format Buffer - the buffer to place the formatted time in BufferLength - the size of the buffer in characters Return Value: The number of characters returned in Buffer. --*/ { PWCHAR writePosition; ULONG returnLength = 0; writePosition = Buffer; if (BufferLength < TIME_BUFFER_LENGTH) { // // Buffer is too short so exit // return 0; } returnLength = swprintf( Buffer, L"%02d:%02d:%02d:%03d", SystemTime->wHour, SystemTime->wMinute, SystemTime->wSecond, SystemTime->wMilliseconds); return returnLength; } VOID IrpFileDump ( IOTEST_DEVICE_TYPE DeviceType, ULONG SequenceNumber, PWCHAR Name, ULONG NameLength, PRECORD_IRP RecordIrp, FILE *File, ULONG VerbosityFlags ) /*++ Routine Name: IrpFileDump Routine Description: Prints a Irp log record to the specified file. The output is in a tab delimited format with the fields in the following order: SequenceNumber, OriginatingTime, CompletionTime, IrpMajor, IrpMinor, IrpFlags, NoCache, Paging I/O, Synchronous, Synchronous paging, FileName, ReturnStatus, FileName Arguments: SequenceNumber - the sequence number for this log record Name - the name of the file that this Irp relates to NameLength - the length of Name in bytes RecordIrp - the Irp record to print File - the file to print to Return Value: None. --*/ { FILETIME localTime; SYSTEMTIME systemTime; WCHAR time[TIME_BUFFER_LENGTH]; switch (DeviceType) { case TOP_FILTER: fprintf(File, "TOP\tI\t%08X", SequenceNumber); break; case BOTTOM_FILTER: fprintf(File, "BOT\tI\t%08X", SequenceNumber); break; default: fprintf(File, "UNK\tI\t%08X", SequenceNumber); break; } // // Convert originating time // FileTimeToLocalFileTime( (FILETIME *)&(RecordIrp->OriginatingTime), &localTime ); FileTimeToSystemTime( &localTime, &systemTime ); if (FormatSystemTime( &systemTime, time, TIME_BUFFER_LENGTH )) { fprintf( File, "\t%-12S", time ); } else { fprintf( File, "\t%-12S", TIME_ERROR ); } fprintf( File, "\t%8x.%-4x ", RecordIrp->ProcessId, RecordIrp->ThreadId ); PrintIrpCode( RecordIrp->IrpMajor, RecordIrp->IrpMinor, (ULONG)(ULONG_PTR)RecordIrp->Argument3, File, TRUE ); fprintf( File, "\t%08p", RecordIrp->FileObject ); // // Interpret set flags // fprintf( File, "\t%08lx ", RecordIrp->IrpFlags ); fprintf( File, "%s", (RecordIrp->IrpFlags & IRP_NOCACHE) ? "N":"-" ); fprintf( File, "%s", (RecordIrp->IrpFlags & IRP_PAGING_IO) ? "P":"-" ); fprintf( File, "%s", (RecordIrp->IrpFlags & IRP_SYNCHRONOUS_API) ? "S":"-" ); fprintf( File, "%s", (RecordIrp->IrpFlags & IRP_SYNCHRONOUS_PAGING_IO) ? "Y":"-" ); if (FlagOn( VerbosityFlags, FS_VF_DUMP_PARAMETERS )) { fprintf( File, "%p %p %p %p ", RecordIrp->Argument1, RecordIrp->Argument2, RecordIrp->Argument3, RecordIrp->Argument4 ); if (IRP_MJ_CREATE == RecordIrp->IrpMajor) { fprintf( File, "DesiredAccess->%08lx ", RecordIrp->DesiredAccess ); } } fprintf( File, "\t%.*S", NameLength/sizeof(WCHAR), Name ); fprintf( File, "\n" ); } VOID IrpScreenDump ( IOTEST_DEVICE_TYPE DeviceType, ULONG SequenceNumber, PWCHAR Name, ULONG NameLength, PRECORD_IRP RecordIrp, ULONG VerbosityFlags ) /*++ Routine Name: IrpScreenDump Routine Description: Prints a Irp log record to the screen in the following order: SequenceNumber, OriginatingTime, CompletionTime, IrpMajor, IrpMinor, IrpFlags, NoCache, Paging I/O, Synchronous, Synchronous paging, FileName, ReturnStatus, FileName Arguments: SequenceNumber - the sequence number for this log record Name - the file name to which this Irp relates NameLength - the length of Name in bytes RecordIrp - the Irp record to print Return Value: None. --*/ { FILETIME localTime; SYSTEMTIME systemTime; WCHAR time[TIME_BUFFER_LENGTH]; switch (DeviceType) { case TOP_FILTER: printf("TOP I %08X", SequenceNumber); break; case BOTTOM_FILTER: printf("BOT I %08X", SequenceNumber); break; default: printf("UNK I %08X", SequenceNumber); break; } // // Convert originating time // FileTimeToLocalFileTime( (FILETIME *)&(RecordIrp->OriginatingTime), &localTime ); FileTimeToSystemTime( &localTime, &systemTime ); if (FormatSystemTime( &systemTime, time, TIME_BUFFER_LENGTH )) { printf( "%-12S ", time ); } else { printf( "%-12S ", TIME_ERROR ); } printf( "%8x.%-4x ", RecordIrp->ProcessId, RecordIrp->ThreadId ); PrintIrpCode( RecordIrp->IrpMajor, RecordIrp->IrpMinor, (ULONG)(ULONG_PTR)RecordIrp->Argument3, NULL, TRUE ); printf( "%08p ", RecordIrp->FileObject ); // // Interpret set flags // printf( "%08lx ", RecordIrp->IrpFlags ); printf( "%s", (RecordIrp->IrpFlags & IRP_NOCACHE) ? "N":"-" ); printf( "%s", (RecordIrp->IrpFlags & IRP_PAGING_IO) ? "P":"-" ); printf( "%s", (RecordIrp->IrpFlags & IRP_SYNCHRONOUS_API) ? "S":"-" ); printf( "%s ", (RecordIrp->IrpFlags & IRP_SYNCHRONOUS_PAGING_IO) ? "Y":"-" ); if (FlagOn( VerbosityFlags, FS_VF_DUMP_PARAMETERS )) { printf( "%p %p %p %p ", RecordIrp->Argument1, RecordIrp->Argument2, RecordIrp->Argument3, RecordIrp->Argument4 ); if (IRP_MJ_CREATE == RecordIrp->IrpMajor) { printf( "DesiredAccess->%08lx ", RecordIrp->DesiredAccess ); } } printf( "%.*S", NameLength/sizeof(WCHAR), Name ); printf( "\n" ); PrintIrpCode( RecordIrp->IrpMajor, RecordIrp->IrpMinor, (ULONG)(ULONG_PTR)RecordIrp->Argument3, NULL, FALSE ); } VOID FastIoFileDump ( IOTEST_DEVICE_TYPE DeviceType, ULONG SequenceNumber, PWCHAR Name, ULONG NameLength, PRECORD_FASTIO RecordFastIo, FILE *File ) /*++ Routine Name: FastIoFileDump Routine Description: Prints a FastIo log record to the specified file. The output is in a tab delimited format with the fields in the following order: SequenceNumber, StartTime, CompletionTime, Fast I/O Type, FileName, Length, Wait, ReturnStatus, FileName Arguments: SequenceNumber - the sequence number for this log record Name - the name of the file referenced by this Fast I/O operation NameLength - the length of name in bytes RecordFastIo - the FastIo record to print File - the file to print to Return Value: None. --*/ { SYSTEMTIME systemTime; FILETIME localTime; WCHAR time[TIME_BUFFER_LENGTH]; switch (DeviceType) { case TOP_FILTER: fprintf(File, "TOP\tF\t%08X", SequenceNumber); break; case BOTTOM_FILTER: fprintf(File, "BOT\tF\t%08X", SequenceNumber); break; default: fprintf(File, "UNK\tF\t%08X", SequenceNumber); break; } // // Convert start time // FileTimeToLocalFileTime( (FILETIME *)&(RecordFastIo->StartTime), &localTime ); FileTimeToSystemTime( &localTime, &systemTime ); if (FormatSystemTime( &systemTime, time, TIME_BUFFER_LENGTH )) { fprintf( File, "\t%-12S", time ); } else { fprintf( File, "\t%-12S", TIME_ERROR ); } fprintf( File, "\t%8x.%-4x ", RecordFastIo->ProcessId, RecordFastIo->ThreadId ); fprintf( File, "\t" ); PrintFastIoType( RecordFastIo->Type, File ); fprintf( File, "\t%08p", RecordFastIo->FileObject ); fprintf( File, "\t%s", (RecordFastIo->Wait)?"T":"F" ); fprintf( File, "\t%08x", RecordFastIo->Length ); fprintf( File, "\t%016I64x ", RecordFastIo->FileOffset ); fprintf( File, "\t%.*S", NameLength/sizeof(WCHAR), Name ); fprintf( File, "\n" ); } VOID FastIoScreenDump ( IOTEST_DEVICE_TYPE DeviceType, ULONG SequenceNumber, PWCHAR Name, ULONG NameLength, PRECORD_FASTIO RecordFastIo ) /*++ Routine Name: FastIoScreenDump Routine Description: Prints a FastIo log record to the screen in the following order: SequenceNumber, StartTime, CompletionTime, Fast I/O Type, FileName, Length, Wait, ReturnStatus, FileName Arguments: SequenceNumber - the sequence number for this log record Name - the name of the file referenced by this Fast I/O operation NameLength - the length of name in bytes RecordIrp - the Irp record to print Return Value: None. --*/ { SYSTEMTIME systemTime; FILETIME localTime; WCHAR time[TIME_BUFFER_LENGTH]; switch (DeviceType) { case TOP_FILTER: printf("TOP F %08X", SequenceNumber); break; case BOTTOM_FILTER: printf("BOT F %08X", SequenceNumber); break; default: printf("UNK F %08X", SequenceNumber); break; } // // Convert start time // FileTimeToLocalFileTime( (FILETIME *)&(RecordFastIo->StartTime), &localTime ); FileTimeToSystemTime( &localTime, &systemTime ); if (FormatSystemTime( &systemTime, time, TIME_BUFFER_LENGTH )) { printf( "%-12S ", time ); } else { printf( "%-12S ", TIME_ERROR ); } printf( "%8x.%-4x ", RecordFastIo->ProcessId, RecordFastIo->ThreadId ); PrintFastIoType( RecordFastIo->Type, NULL ); printf( "%08p ", RecordFastIo->FileObject ); printf( "%s ", (RecordFastIo->Wait)?"T":"F" ); printf( "%08x ", RecordFastIo->Length ); printf( "%016I64x ", RecordFastIo->FileOffset ); printf( "%.*S", NameLength/sizeof(WCHAR), Name ); printf ("\n" ); } VOID FsFilterOperationFileDump ( IOTEST_DEVICE_TYPE DeviceType, ULONG SequenceNumber, PWCHAR Name, ULONG NameLength, PRECORD_FS_FILTER_OPERATION RecordFsFilterOp, FILE *File ) /*++ Routine Name: FsFilterOperationFileDump Routine Description: Prints a FsFilterOperation log record to the specified file. The output is in a tab delimited format with the fields in the following order: SequenceNumber, OriginatingTime, CompletionTime, ProcessId, ThreadId, Operation, FileObject, ReturnStatus, FileName Arguments: SequenceNumber - the sequence number for this log record Name - the name of the file that this operation relates to NameLength - the length of Name in bytes RecordFsFilterOp - the FsFilter operation record to print File - the file to print to Return Value: None. --*/ { FILETIME localTime; SYSTEMTIME systemTime; WCHAR time[TIME_BUFFER_LENGTH]; switch (DeviceType) { case TOP_FILTER: fprintf(File, "TOP\tO\t%08X", SequenceNumber); break; case BOTTOM_FILTER: fprintf(File, "BOT\tO\t%08X", SequenceNumber); break; default: fprintf(File, "UNK\tO\t%08X", SequenceNumber); break; } // // Convert originating time // FileTimeToLocalFileTime( (FILETIME *)&(RecordFsFilterOp->OriginatingTime), &localTime ); FileTimeToSystemTime( &localTime, &systemTime ); if (FormatSystemTime( &systemTime, time, TIME_BUFFER_LENGTH )) { fprintf( File, "\t%-12S", time ); } else { fprintf( File, "\t%-12S", TIME_ERROR ); } // // Output the process and thread id // fprintf( File, "\t%8x.%-4x ", RecordFsFilterOp->ProcessId, RecordFsFilterOp->ThreadId ); // // Output the FsFilter operation parameters // PrintFsFilterOperation( RecordFsFilterOp->FsFilterOperation, File ); fprintf( File, "\t%08p", RecordFsFilterOp->FileObject ); fprintf( File, "\t%.*S", NameLength/sizeof(WCHAR), Name ); fprintf( File, "\n" ); } VOID FsFilterOperationScreenDump ( IOTEST_DEVICE_TYPE DeviceType, ULONG SequenceNumber, PWCHAR Name, ULONG NameLength, PRECORD_FS_FILTER_OPERATION RecordFsFilterOp ) /*++ Routine Name: FsFilterOperationScreenDump Routine Description: Prints a FsFilterOperation log record to the screen in the following order: SequenceNumber, OriginatingTime, CompletionTime, ProcessId, ThreadId, Operation, FileObject, ReturnStatus, FileName Arguments: SequenceNumber - the sequence number for this log record Name - the file name to which this Irp relates NameLength - the length of name in bytes RecordFsFilterOp - the FsFilterOperation record to print Return Value: None. --*/ { FILETIME localTime; SYSTEMTIME systemTime; WCHAR time[TIME_BUFFER_LENGTH]; switch (DeviceType) { case TOP_FILTER: printf("TOP O %08X", SequenceNumber); break; case BOTTOM_FILTER: printf("BOT O %08X", SequenceNumber); break; default: printf("UNK O %08X", SequenceNumber); break; } // // Convert originating time // FileTimeToLocalFileTime( (FILETIME *)&(RecordFsFilterOp->OriginatingTime), &localTime ); FileTimeToSystemTime( &localTime, &systemTime ); if (FormatSystemTime( &systemTime, time, TIME_BUFFER_LENGTH )) { printf( "%-12S ", time ); } else { printf( "%-12S ", TIME_ERROR ); } printf( "%8x.%-4x ", RecordFsFilterOp->ProcessId, RecordFsFilterOp->ThreadId ); PrintFsFilterOperation( RecordFsFilterOp->FsFilterOperation, NULL ); // // Print FsFilter operation specific values. // printf( "%08p ", RecordFsFilterOp->FileObject ); printf( "%.*S", NameLength/sizeof(WCHAR),Name ); printf( "\n" ); }