/*++ Copyright (c) 1995 Microsoft Corporation Module Name: LlsUtil.c Abstract: Author: Arthur Hanson (arth) Dec 07, 1994 Environment: Revision History: Jeff Parham (jeffparh) 12-Jan-1996 o Added WinNtBuildNumberGet() to ascertain the Windows NT build number running on a given machine. o Enhanced output of TimeToString(). --*/ #include #include #include #include #include #include #include #include #include #include "debug.h" #include "llssrv.h" #include "llsevent.h" // // NB : Keep this define in sync with client\llsrpc.rc. // #define IDS_LICENSEWARNING 1501 const char HeaderString[] = "License Logging System Data File\x01A"; #define HEADER_SIZE 34 typedef struct _LLS_FILE_HEADER { char Header[HEADER_SIZE]; DWORD Version; DWORD DataSize; } LLS_FILE_HEADER, *PLLS_FILE_HEADER; extern HANDLE gLlsDllHandle; static HANDLE ghWarningDlgThreadHandle = NULL; VOID WarningDlgThread( PVOID ThreadParameter ); ///////////////////////////////////////////////////////////////////////// NTSTATUS EBlock( PVOID Data, ULONG DataSize ) /*++ Routine Description: Arguments: Return Value: --*/ { NTSTATUS Status = STATUS_SUCCESS; DATA_KEY PublicKey; CRYPT_BUFFER CryptBuffer; // // Init our public key // PublicKey.Length = 4; PublicKey.MaximumLength = 4; PublicKey.Buffer = LocalAlloc(LPTR, 4); if (PublicKey.Buffer != NULL) { ((char *) (PublicKey.Buffer))[0] = '7'; ((char *) (PublicKey.Buffer))[1] = '7'; ((char *) (PublicKey.Buffer))[2] = '7'; ((char *) (PublicKey.Buffer))[3] = '7'; CryptBuffer.Length = DataSize; CryptBuffer.MaximumLength = DataSize; CryptBuffer.Buffer = (PVOID) Data; Status = RtlEncryptData2(&CryptBuffer, &PublicKey); LocalFree(PublicKey.Buffer); } else Status = STATUS_NO_MEMORY; return Status; } // EBlock ///////////////////////////////////////////////////////////////////////// NTSTATUS DeBlock( PVOID Data, ULONG DataSize ) /*++ Routine Description: Arguments: Return Value: --*/ { NTSTATUS Status = STATUS_SUCCESS; DATA_KEY PublicKey; CRYPT_BUFFER CryptBuffer; // // Init our public key // PublicKey.Length = 4; PublicKey.MaximumLength = 4; PublicKey.Buffer = LocalAlloc(LPTR, 4); if (PublicKey.Buffer != NULL) { ((char *) (PublicKey.Buffer))[0] = '7'; ((char *) (PublicKey.Buffer))[1] = '7'; ((char *) (PublicKey.Buffer))[2] = '7'; ((char *) (PublicKey.Buffer))[3] = '7'; CryptBuffer.Length = DataSize; CryptBuffer.MaximumLength = DataSize; CryptBuffer.Buffer = (PVOID) Data; Status = RtlDecryptData2(&CryptBuffer, &PublicKey); LocalFree(PublicKey.Buffer); } else Status = STATUS_NO_MEMORY; return Status; } // DeBlock ///////////////////////////////////////////////////////////////////////// BOOL FileExists( LPTSTR FileName ) /*++ Routine Description: Arguments: Return Value: --*/ { return (BOOL) RtlDoesFileExists_U(FileName); } // FileExists ///////////////////////////////////////////////////////////////////////// VOID lsplitpath( const TCHAR *path, TCHAR *drive, TCHAR *dir, TCHAR *fname, TCHAR *ext ) /*++ Routine Description: Splits a path name into its individual components Took the _splitpath and _makepath routines and converted them to be NT (long file name) and Unicode friendly. Arguments: Entry: path - pointer to path name to be parsed drive - pointer to buffer for drive component, if any dir - pointer to buffer for subdirectory component, if any fname - pointer to buffer for file base name component, if any ext - pointer to buffer for file name extension component, if any Exit: drive - pointer to drive string. Includes ':' if a drive was given. dir - pointer to subdirectory string. Includes leading and trailing '/' or '\', if any. fname - pointer to file base name ext - pointer to file extension, if any. Includes leading '.'. Return Value: --*/ { TCHAR *p; TCHAR *last_slash = NULL, *dot = NULL; SIZE_T len; // init these so we don't exit with bogus values drive[0] = TEXT('\0'); dir[0] = TEXT('\0'); fname[0] = TEXT('\0'); ext[0] = TEXT('\0'); if (path[0] == TEXT('\0')) return; /*+---------------------------------------------------------------------+ | Assume that the path argument has the following form, where any or | | all of the components may be missing. | | | | | | | | drive: | | 0 to MAX_DRIVE-1 characters, the last of which, if any, is a | | ':' or a '\' in the case of a UNC path. | | dir: | | 0 to _MAX_DIR-1 characters in the form of an absolute path | | (leading '/' or '\') or relative path, the last of which, if | | any, must be a '/' or '\'. E.g - | | | | absolute path: | | \top\next\last\ ; or | | /top/next/last/ | | relative path: | | top\next\last\ ; or | | top/next/last/ | | Mixed use of '/' and '\' within a path is also tolerated | | fname: | | 0 to _MAX_FNAME-1 characters not including the '.' character | | ext: | | 0 to _MAX_EXT-1 characters where, if any, the first must be a | | '.' | +---------------------------------------------------------------------+*/ // extract drive letter and :, if any if ( path[0] && (path[1] == TEXT(':')) ) { if (drive) { drive[0] = path[0]; drive[1] = path[1]; drive[2] = TEXT('\0'); } path += 2; } // if no drive then check for UNC pathname if (drive[0] == TEXT('\0')) if ((path[0] == TEXT('\\')) && (path[1] == TEXT('\\'))) { // got a UNC path so put server-sharename into drive drive[0] = path[0]; drive[1] = path[1]; path += 2; p = &drive[2]; while ((*path != TEXT('\0')) && (*path != TEXT('\\'))) *p++ = *path++; if (*path == TEXT('\0')) return; // now sitting at the share - copy this as well (copy slash first) *p++ = *path++; while ((*path != TEXT('\0')) && (*path != TEXT('\\'))) *p++ = *path++; // tack on terminating NULL *p = TEXT('\0'); } /*+---------------------------------------------------------------------+ | extract path string, if any. Path now points to the first character| | of the path, if any, or the filename or extension, if no path was | | specified. Scan ahead for the last occurence, if any, of a '/' or | | '\' path separator character. If none is found, there is no path. | | We will also note the last '.' character found, if any, to aid in | | handling the extension. | +---------------------------------------------------------------------+*/ for (last_slash = NULL, p = (TCHAR *)path; *p; p++) { if (*p == TEXT('/') || *p == TEXT('\\')) // point to one beyond for later copy last_slash = p + 1; else if (*p == TEXT('.')) dot = p; } if (last_slash) { // found a path - copy up through last_slash or max. characters allowed, // whichever is smaller if (dir) { len = __min((last_slash - path), (_MAX_DIR - 1)); lstrcpyn(dir, path, (int)len + 1); dir[len] = TEXT('\0'); } path = last_slash; } /*+---------------------------------------------------------------------+ | extract file name and extension, if any. Path now points to the | | first character of the file name, if any, or the extension if no | | file name was given. Dot points to the '.' beginning the extension,| | if any. | +---------------------------------------------------------------------+*/ if (dot && (dot >= path)) { // found the marker for an extension - copy the file name up to the // '.'. if (fname) { len = __min((dot - path), (_MAX_FNAME - 1)); lstrcpyn(fname, path, (int)len + 1); *(fname + len) = TEXT('\0'); } // now we can get the extension - remember that p still points to the // terminating nul character of path. if (ext) { len = __min((p - dot), (_MAX_EXT - 1)); lstrcpyn(ext, dot, (int)len + 1); ext[len] = TEXT('\0'); } } else { // found no extension, give empty extension and copy rest of string // into fname. if (fname) { len = __min((p - path), (_MAX_FNAME - 1)); lstrcpyn(fname, path, (int)len + 1); fname[len] = TEXT('\0'); } if (ext) { *ext = TEXT('\0'); } } } // lsplitpath ///////////////////////////////////////////////////////////////////////// VOID lmakepath( TCHAR *path, const TCHAR *drive, const TCHAR *dir, const TCHAR *fname, const TCHAR *ext ) /*++ Routine Description: Create a path name from its individual components. Arguments: Entry: char *path - pointer to buffer for constructed path char *drive - pointer to drive component, may or may not contain trailing ':' char *dir - pointer to subdirectory component, may or may not include leading and/or trailing '/' or '\' characters char *fname - pointer to file base name component char *ext - pointer to extension component, may or may not contain a leading '.'. Exit: path - pointer to constructed path name Return Value: --*/ { const TCHAR *p; /*+---------------------------------------------------------------------+ | we assume that the arguments are in the following form (although we | | do not diagnose invalid arguments or illegal filenames (such as | | names longer than 8.3 or with illegal characters in them) | | | | drive: | | A or A: | | dir: | | \top\next\last\ ; or | | /top/next/last/ ; or | | | | either of the above forms with either/both the leading and | | trailing / or \ removed. Mixed use of '/' and '\' is also | | tolerated | | fname: | | any valid file name | | ext: | | any valid extension (none if empty or null ) | +---------------------------------------------------------------------+*/ // copy drive if (drive && *drive) while (*drive) *path++ = *drive++; // copy dir if ((p = dir) && *p) { do { *path++ = *p++; } while (*p); if ((*(p-1) != TEXT('/')) && (*(p-1) != TEXT('\\'))) { *path++ = TEXT('\\'); } } // copy fname if (p = fname) { while (*p) { *path++ = *p++; } } // copy ext, including 0-terminator - check to see if a '.' needs to be // inserted. if (p = ext) { if (*p && *p != TEXT('.')) { *path++ = TEXT('.'); } while (*path++ = *p++) ; } else { // better add the 0-terminator *path = TEXT('\0'); } } // lmakepath ///////////////////////////////////////////////////////////////////////// VOID FileBackupCreate( LPTSTR Path ) /*++ Routine Description: Arguments: Return Value: --*/ { HANDLE hFile = NULL; DWORD dwFileNumber = 0; TCHAR Drive[_MAX_DRIVE + 1]; TCHAR Dir[_MAX_DIR + 1]; TCHAR FileName[_MAX_FNAME + 1]; TCHAR Ext[_MAX_EXT + 1]; TCHAR NewExt[_MAX_EXT + 1]; TCHAR NewPath[MAX_PATH + 1]; // // Make sure file exists // if (!FileExists(FileName)) return; // // Split name into constituent parts... // lsplitpath(Path, Drive, Dir, FileName, Ext); // Find next backup number... // Files are backed up as .xxx where xxx is a number in the form .001, // the first backup is stored as .001, second as .002, etc... do { // // Create new file name with backup extension // dwFileNumber++; wsprintf(NewExt, TEXT("%03u"), dwFileNumber); lmakepath(NewPath, Drive, Dir, FileName, NewExt); } while ( FileExists(NewPath) ); MoveFile( Path, NewPath ); } // FileBackupCreate ///////////////////////////////////////////////////////////////////////// HANDLE LlsFileInit( LPTSTR FileName, DWORD Version, DWORD DataSize ) /*++ Routine Description: Arguments: Return Value: --*/ { HANDLE hFile = NULL; LLS_FILE_HEADER Header; DWORD BytesWritten; #ifdef DEBUG if (TraceFlags & (TRACE_FUNCTION_TRACE | TRACE_DATABASE)) dprintf(TEXT("LLS TRACE: LlsFileInit\n")); #endif if (FileName == NULL) return NULL; strcpy(Header.Header, HeaderString); Header.Version = Version; Header.DataSize = DataSize; SetFileAttributes(FileName, FILE_ATTRIBUTE_NORMAL); hFile = CreateFile(FileName, GENERIC_WRITE, 0, NULL, OPEN_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL); if (hFile != INVALID_HANDLE_VALUE) { if (!WriteFile(hFile, &Header, sizeof(LLS_FILE_HEADER), &BytesWritten, NULL)) { CloseHandle(hFile); hFile = NULL; } } else { return NULL; } return hFile; } // LlsFileInit ///////////////////////////////////////////////////////////////////////// HANDLE LlsFileCheck( LPTSTR FileName, LPDWORD Version, LPDWORD DataSize ) /*++ Routine Description: Arguments: Return Value: --*/ { BOOL FileOK = FALSE; HANDLE hFile = NULL; LLS_FILE_HEADER Header; DWORD FileSize; DWORD BytesRead; #ifdef DEBUG if (TraceFlags & (TRACE_FUNCTION_TRACE | TRACE_DATABASE)) dprintf(TEXT("LLS TRACE: LlsFileCheck\n")); #endif if (FileName == NULL) return NULL; // // We are assuming the file exists // SetFileAttributes(FileName, FILE_ATTRIBUTE_NORMAL); hFile = CreateFile(FileName, GENERIC_READ, 0, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL); if (hFile != INVALID_HANDLE_VALUE) { FileSize = GetFileSize(hFile, NULL); // // Make sure there is enough data there to read // if (FileSize > (sizeof(LLS_FILE_HEADER) + 1)) { if (ReadFile(hFile, &Header, sizeof(LLS_FILE_HEADER), &BytesRead, NULL)) { if ( !_strcmpi(Header.Header, HeaderString) ) { // // Data checks out - so return datalength // *Version = Header.Version; *DataSize = Header.DataSize; FileOK = TRUE; } } } // // If we opened the file and something was wrong - close it. // if (!FileOK) { CloseHandle(hFile); hFile = NULL; } } else { return NULL; } return hFile; } // LlsFileCheck ///////////////////////////////////////////////////////////////////////// DWORD DateSystemGet( ) /*++ Routine Description: Arguments: Return Value: Seconds since midnight. --*/ { DWORD Seconds = 0; LARGE_INTEGER SysTime; NtQuerySystemTime(&SysTime); RtlTimeToSecondsSince1980(&SysTime, &Seconds); return Seconds; } // DateSystemGet ///////////////////////////////////////////////////////////////////////// DWORD DateLocalGet( ) /*++ Routine Description: Arguments: Return Value: Seconds since midnight. --*/ { DWORD Seconds = 0; LARGE_INTEGER SysTime, LocalTime; NtQuerySystemTime(&SysTime); RtlSystemTimeToLocalTime(&SysTime, &LocalTime); RtlTimeToSecondsSince1980(&LocalTime, &Seconds); return Seconds; } // DateLocalGet ///////////////////////////////////////////////////////////////////////// DWORD InAWorkgroup( VOID ) /*++ Routine Description: This function determines whether we are a member of a domain, or of a workgroup. First it checks to make sure we're running on a Windows NT system (otherwise we're obviously in a domain) and if so, queries LSA to get the Primary domain SID, if this is NULL, we're in a workgroup. If we fail for some random unexpected reason, we'll pretend we're in a workgroup (it's more restrictive). Arguments: None Return Value: TRUE - We're in a workgroup FALSE - We're in a domain --*/ { NT_PRODUCT_TYPE ProductType; OBJECT_ATTRIBUTES ObjectAttributes; LSA_HANDLE Handle; NTSTATUS Status; PPOLICY_PRIMARY_DOMAIN_INFO PolicyPrimaryDomainInfo = NULL; DWORD Result = FALSE; Status = RtlGetNtProductType(&ProductType); if (!NT_SUCCESS(Status)) { #if DBG dprintf(TEXT("ERROR LLS Could not get Product type\n")); #endif return TRUE; } if (ProductType == NtProductLanManNt) { return(FALSE); } InitializeObjectAttributes(&ObjectAttributes, NULL, 0, 0, NULL); Status = LsaOpenPolicy(NULL, &ObjectAttributes, POLICY_VIEW_LOCAL_INFORMATION, &Handle); if (!NT_SUCCESS(Status)) { #if DBG dprintf(TEXT("ERROR LLS: Could not open LSA Policy Database\n")); #endif return TRUE; } Status = LsaQueryInformationPolicy(Handle, PolicyPrimaryDomainInformation, (PVOID *) &PolicyPrimaryDomainInfo); if (NT_SUCCESS(Status)) { if (PolicyPrimaryDomainInfo->Sid == NULL) { Result = TRUE; } else { Result = FALSE; } } if (PolicyPrimaryDomainInfo) { LsaFreeMemory((PVOID)PolicyPrimaryDomainInfo); } LsaClose(Handle); return(Result); } // InAWorkgroup ///////////////////////////////////////////////////////////////////////// VOID LogEvent( DWORD MessageId, DWORD NumberOfSubStrings, LPWSTR *SubStrings, DWORD ErrorCode ) { HANDLE LogHandle; WORD wEventType; LogHandle = RegisterEventSourceW ( NULL, TEXT("LicenseService") ); if (LogHandle == NULL) { #if DBG dprintf(TEXT("LLS RegisterEventSourceW failed %lu\n"), GetLastError()); #endif return; } switch ( MessageId >> 30 ) { case STATUS_SEVERITY_INFORMATIONAL: case STATUS_SEVERITY_SUCCESS: wEventType = EVENTLOG_INFORMATION_TYPE; break; case STATUS_SEVERITY_WARNING: wEventType = EVENTLOG_WARNING_TYPE; break; case STATUS_SEVERITY_ERROR: default: wEventType = EVENTLOG_ERROR_TYPE; break; } if (ErrorCode == ERROR_SUCCESS) { // // No error codes were specified // (void) ReportEventW( LogHandle, wEventType, 0, // event category MessageId, NULL, (WORD)NumberOfSubStrings, 0, SubStrings, (PVOID) NULL ); } else { // // Log the error code specified // (void) ReportEventW( LogHandle, wEventType, 0, // event category MessageId, NULL, (WORD)NumberOfSubStrings, sizeof(DWORD), SubStrings, (PVOID) &ErrorCode ); } DeregisterEventSource(LogHandle); } // LogEvent #define THROTTLE_WRAPAROUND 24 // // Reduce the frequency of logging // No need for the limit to be exact // ///////////////////////////////////////////////////////////////////////// VOID ThrottleLogEvent( DWORD MessageId, DWORD NumberOfSubStrings, LPWSTR *SubStrings, DWORD ErrorCode ) { static LONG lLogged = THROTTLE_WRAPAROUND; LONG lResult; lResult = InterlockedIncrement(&lLogged); if (THROTTLE_WRAPAROUND <= lResult) { LogEvent( MessageId, NumberOfSubStrings, SubStrings, ErrorCode ); lLogged = 0; } } ///////////////////////////////////////////////////////////////////////// VOID LicenseCapacityWarningDlg(DWORD dwCapacityState) { // // NB : The ServiceLock critical section is entered for the duration // of this routine. No serialization issues. // if (ghWarningDlgThreadHandle == NULL) { // // No action necessary if this fails. // DWORD Ignore; DWORD * pWarningMessageID; pWarningMessageID = LocalAlloc(LPTR, sizeof(DWORD)); if ( pWarningMessageID != NULL ) { switch( dwCapacityState ) { case LICENSE_CAPACITY_NEAR_MAXIMUM: *pWarningMessageID = LLS_EVENT_NOTIFY_LICENSES_NEAR_MAX; break; case LICENSE_CAPACITY_AT_MAXIMUM: *pWarningMessageID = LLS_EVENT_NOTIFY_LICENSES_AT_MAX; break; case LICENSE_CAPACITY_EXCEEDED: *pWarningMessageID = LLS_EVENT_NOTIFY_LICENSES_EXCEEDED; break; default: *pWarningMessageID = LLS_EVENT_NOTIFY_LICENSES_EXCEEDED; }; ghWarningDlgThreadHandle = CreateThread(NULL, 0L, (LPTHREAD_START_ROUTINE) WarningDlgThread, pWarningMessageID, 0L, &Ignore); if (ghWarningDlgThreadHandle == NULL) { // // CreateThread failed // LocalFree(pWarningMessageID); } } } } ///////////////////////////////////////////////////////////////////////// VOID WarningDlgThread( PVOID ThreadParameter) { LPTSTR pszWarningMessage = NULL; DWORD * pWarningMessageID; HANDLE hThread; TCHAR szWarningTitle[256] = TEXT(""); ASSERT(ThreadParameter != NULL); pWarningMessageID = (DWORD *)ThreadParameter; // // NB : The .dll should already have been loaded in MasterServiceListInit // on service startup. This logic exists here for the case where // the code invoked on initialization should fail. // // It is OK if another thread should simulataneously initialize // gLlsDllHandle. Worst case, there will be an orphaned handle // to the .dll. But the .dll is loaded for the lifetime of this // .exe, so no big deal. // if ( gLlsDllHandle == NULL ) { gLlsDllHandle = LoadLibrary(TEXT("LLSRPC.DLL")); } if ( gLlsDllHandle != NULL) { DWORD ccWarningMessage; // // Fetch the dialog title. // LoadString(gLlsDllHandle, IDS_LICENSEWARNING, szWarningTitle, sizeof(szWarningTitle)/sizeof(TCHAR)); // // Fetch the dialog message. // ccWarningMessage = FormatMessage(FORMAT_MESSAGE_IGNORE_INSERTS | FORMAT_MESSAGE_FROM_HMODULE | FORMAT_MESSAGE_ALLOCATE_BUFFER, gLlsDllHandle, *pWarningMessageID, 0, (LPTSTR)&pszWarningMessage, 0, NULL); if ( ccWarningMessage > 2 ) { // // Strip the trailing format message always adds. // pszWarningMessage[ccWarningMessage - 2] = TEXT('\0'); MessageBox(NULL, pszWarningMessage, szWarningTitle, MB_ICONWARNING | MB_OK | MB_SYSTEMMODAL | MB_SERVICE_NOTIFICATION); } if ( pszWarningMessage != NULL) { LocalFree(pszWarningMessage); } } LocalFree(pWarningMessageID); // // By closing the handle, we allow the system to remove all remaining // traces of this thread. // hThread = ghWarningDlgThreadHandle; ghWarningDlgThreadHandle = NULL; CloseHandle(hThread); } ///////////////////////////////////////////////////////////////////////// DWORD WinNtBuildNumberGet( LPTSTR pszServerName, LPDWORD pdwBuildNumber ) /*++ Routine Description: Retrieve the build number of Windows NT running on a given machine. Arguments: pszServerName (LPTSTR) Name of the server to check. pdwBuildNumber (LPDWORD) On return, holds the build number of the server (e.g., 1057 for the release version of Windows NT 3.51). Return Value: ERROR_SUCCESS or Win error code. --*/ { LONG lError; HKEY hKeyLocalMachine; lError = RegConnectRegistry( pszServerName, HKEY_LOCAL_MACHINE, &hKeyLocalMachine ); if ( ERROR_SUCCESS != lError ) { #if DBG dprintf( TEXT("WinNtBuildNumberGet(): Could not connect to remote registry, error %ld.\n"), lError ); #endif } else { HKEY hKeyCurrentVersion; lError = RegOpenKeyEx( hKeyLocalMachine, TEXT( "Software\\Microsoft\\Windows NT\\CurrentVersion" ), 0, KEY_READ, &hKeyCurrentVersion ); if ( ERROR_SUCCESS != lError ) { #if DBG dprintf( TEXT("WinNtBuildNumberGet(): Could not open key, error %ld.\n"), lError ); #endif } else { DWORD dwType; TCHAR szWinNtBuildNumber[ 16 ]; DWORD cbWinNtBuildNumber = sizeof( szWinNtBuildNumber ); lError = RegQueryValueEx( hKeyCurrentVersion, TEXT( "CurrentBuildNumber" ), NULL, &dwType, (LPBYTE) &szWinNtBuildNumber, &cbWinNtBuildNumber ); if ( ERROR_SUCCESS != lError ) { #if DBG dprintf( TEXT("WinNtBuildNumberGet(): Could not query value, error %ld.\n"), lError ); #endif } else { *pdwBuildNumber = (DWORD) _wtol( szWinNtBuildNumber ); } RegCloseKey( hKeyCurrentVersion ); } RegCloseKey( hKeyLocalMachine ); } return (DWORD) lError; } #if DBG ///////////////////////////////////////////////////////////////////////// LPTSTR TimeToString( ULONG Seconds ) { TIME_FIELDS tf; LARGE_INTEGER Time, LTime; static TCHAR TimeString[100]; if ( 0 == Seconds ) { lstrcpy(TimeString, TEXT("None")); } else { RtlSecondsSince1980ToTime(Seconds, &Time); RtlSystemTimeToLocalTime(&Time, <ime); RtlTimeToTimeFields(<ime, &tf); wsprintf(TimeString, TEXT("%02hd/%02hd/%04hd @ %02hd:%02hd:%02hd"), tf.Month, tf.Day, tf.Year, tf.Hour, tf.Minute, tf.Second); } return TimeString; } // TimeToString #endif