/***************************************************************************** * * sdview.cpp * * Lame SD Viewer app. * *****************************************************************************/ #include "sdview.h" HINSTANCE g_hinst; HCURSOR g_hcurWait; HCURSOR g_hcurArrow; HCURSOR g_hcurAppStarting; LONG g_lThreads; UINT g_wShowWindow; CGlobals GlobalSettings; /***************************************************************************** * * Stubs - will be filled in with goodies eventually * *****************************************************************************/ DWORD CALLBACK CFileOut_ThreadProc(LPVOID lpParameter) { MessageBox(NULL, RECAST(LPTSTR, lpParameter), TEXT("fileout"), MB_OK); return EndThreadTask(0); } #if 0 DWORD CALLBACK COpened_ThreadProc(LPVOID lpParameter) { MessageBox(NULL, RECAST(LPTSTR, lpParameter), TEXT("opened"), MB_OK); return EndThreadTask(0); } #endif /***************************************************************************** * * Eschew the C runtime. Also, bonus-initialize memory to zero. * *****************************************************************************/ void * __cdecl operator new(size_t cb) { return RECAST(LPVOID, LocalAlloc(LPTR, cb)); } void __cdecl operator delete(void *pv) { LocalFree(RECAST(HLOCAL, pv)); } int __cdecl _purecall(void) { return 0; } /***************************************************************************** * * Assertion goo * *****************************************************************************/ #ifdef DEBUG void AssertFailed(char *psz, char *pszFile, int iLine) { static BOOL fAsserting = FALSE; if (!fAsserting) { fAsserting = TRUE; String strTitle(TEXT("Assertion failed - ")); strTitle << pszFile << TEXT(" - line ") << iLine; MessageBox(NULL, psz, strTitle, MB_OK); fAsserting = FALSE; } } #endif /***************************************************************************** * * LaunchThreadTask * *****************************************************************************/ BOOL LaunchThreadTask(LPTHREAD_START_ROUTINE pfn, LPCTSTR pszArgs) { BOOL fSuccess = FALSE; LPTSTR psz = StrDup(pszArgs); if (psz) { InterlockedIncrement(&g_lThreads); if (_QueueUserWorkItem(pfn, CCAST(LPTSTR, psz), WT_EXECUTELONGFUNCTION)) { fSuccess = TRUE; } else { LocalFree(psz); InterlockedDecrement(&g_lThreads); } } return fSuccess; } /***************************************************************************** * * EndThreadTask * * When a task finishes, exit with "return EndThreadTask(dwExitCode)". * This decrements the count of active thread tasks and terminates * the process if this is the last one. * *****************************************************************************/ DWORD EndThreadTask(DWORD dwExitCode) { if (InterlockedDecrement(&g_lThreads) <= 0) { ExitProcess(dwExitCode); } return dwExitCode; } /***************************************************************************** * * Listview stuff * *****************************************************************************/ int ListView_GetCurSel(HWND hwnd) { return ListView_GetNextItem(hwnd, -1, LVNI_FOCUSED); } void ListView_SetCurSel(HWND hwnd, int iIndex) { ListView_SetItemState(hwnd, iIndex, LVIS_SELECTED | LVIS_FOCUSED, LVIS_SELECTED | LVIS_FOCUSED); } int ListView_GetSubItemText(HWND hwnd, int iItem, int iSubItem, LPTSTR pszBuf, int cch) { LVITEM lvi; lvi.iSubItem = iSubItem; lvi.pszText= pszBuf; lvi.cchTextMax = cch; return (int)::SendMessage(hwnd, LVM_GETITEMTEXT, iItem, RECAST(LPARAM, &lvi)); } void ChangeTabsToSpaces(LPTSTR psz) { while ((psz = StrChr(psz, TEXT('\t'))) != NULL) *psz = TEXT(' '); } /***************************************************************************** * * LoadPopupMenu * *****************************************************************************/ HMENU LoadPopupMenu(LPCTSTR pszMenu) { HMENU hmenuParent = LoadMenu(g_hinst, pszMenu); if (hmenuParent) { HMENU hmenuPopup = GetSubMenu(hmenuParent, 0); RemoveMenu(hmenuParent, 0, MF_BYPOSITION); DestroyMenu(hmenuParent); return hmenuPopup; } else { return NULL; } } /***************************************************************************** * * EnableDisableOrRemoveMenuItem * * Enable, disable or remove, accordingly. * *****************************************************************************/ void EnableDisableOrRemoveMenuItem(HMENU hmenu, UINT id, BOOL fEnable, BOOL fDelete) { if (fEnable) { EnableMenuItem(hmenu, id, MF_BYCOMMAND | MF_ENABLED); } else if (fDelete) { DeleteMenu(hmenu, id, MF_BYCOMMAND); } else { EnableMenuItem(hmenu, id, MF_BYCOMMAND | MF_DISABLED | MF_GRAYED); } } /***************************************************************************** * * MakeMenuPretty * * Remove separators at the top and at the bottom, and collapse * multiple consecutive separators. * *****************************************************************************/ void MakeMenuPretty(HMENU hmenu) { BOOL fPrevSep = TRUE; int iCount = GetMenuItemCount(hmenu); for (int iItem = 0; iItem < iCount; iItem++) { UINT uiState = GetMenuState(hmenu, 0, MF_BYPOSITION); if (uiState & MF_SEPARATOR) { if (fPrevSep) { DeleteMenu(hmenu, iItem, MF_BYPOSITION); iCount--; iItem--; // Will be incremented by loop control } fPrevSep = TRUE; } else { fPrevSep = FALSE; } } if (iCount && fPrevSep) { DeleteMenu(hmenu, iCount - 1, MF_BYPOSITION); } } /***************************************************************************** * * JiggleMouse * * * Jiggle the mouse to force a cursor recomputation. * *****************************************************************************/ void JiggleMouse() { POINT pt; if (GetCursorPos(&pt)) { SetCursorPos(pt.x, pt.y); } } /***************************************************************************** * * BGTask * *****************************************************************************/ BGTask::~BGTask() { if (_hDone) { /* * Theoretically we don't need to pump messages because * we destroyed all the windows we created so our thread * should be clear of any windows. Except that Cicero will * secretly create a window on our thread, so we have * to pump messages anyway... */ while (MsgWaitForMultipleObjects(1, &_hDone, FALSE, INFINITE, QS_ALLINPUT) == WAIT_OBJECT_0+1) { MSG msg; while (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE)) { TranslateMessage(&msg); DispatchMessage(&msg); } } CloseHandle(_hDone); } } BOOL BGTask::BGStartTask(LPTHREAD_START_ROUTINE pfn, LPVOID Context) { ASSERT(!_fPending); if (BGConstructed()) { /* * Must reset before queueing the work item to avoid a race where * the work item completes before we return from the Queue call. */ ResetEvent(_hDone); _fPending = QueueUserWorkItem(pfn, Context, WT_EXECUTELONGFUNCTION); if (_fPending) { JiggleMouse(); } else { BGEndTask(); // pretend task completed (because it never started) } } return _fPending; } void BGTask::BGEndTask() { SetEvent(_hDone); _fPending = FALSE; JiggleMouse(); } LRESULT BGTask::BGFilterSetCursor(LRESULT lres) { if (BGTaskPending()) { if (GetCursor() == g_hcurArrow) { SetCursor(g_hcurAppStarting); lres = TRUE; } } return lres; } /***************************************************************************** * * PremungeFileSpec * * Due to complex view specifications this can be led astray when "..." * gets involved. As a workaround (HACK!) we change "..." to "???", * do the mapping, then map back. * * We choose "???" because it has so many magical properties... * * - not a valid filename, so cannot match a local file specification. * - not a valid Source Depot wildcard, so cannot go wild on the server, * - not a single question mark, which SD treats as equivalent to "help". * - same length as "..." so can be updated in place. * * Any revision specifiers remain attached to the string. * *****************************************************************************/ void _ChangeTo(LPTSTR psz, LPCTSTR pszFrom, LPCTSTR pszTo) { ASSERT(lstrlen(pszFrom) == lstrlen(pszTo)); while ((psz = StrStr(psz, pszFrom)) != NULL) { memcpy(psz, pszTo, lstrlen(pszTo) * sizeof(pszTo[0])); } } void PremungeFilespec(LPTSTR psz) { _ChangeTo(psz, TEXT("..."), TEXT("???")); } void PostmungeFilespec(LPTSTR psz) { _ChangeTo(psz, TEXT("???"), TEXT("...")); } /***************************************************************************** * * MapToXPath * *****************************************************************************/ BOOL MapToXPath(LPCTSTR pszSD, String& strOut, MAPTOX X) { if (X == MAPTOX_DEPOT) { // // Early-out: Is it already a full depot path? // if (pszSD[0] == TEXT('/')) { strOut = pszSD; return TRUE; } } // // Borrow strOut to compose the query string. // Substring ssPath; strOut.Reset(); if (Parse(TEXT("$p"), pszSD, &ssPath) && ssPath.Length() > 0) { strOut << ssPath; } else { return FALSE; } PremungeFilespec(strOut); String str; str << TEXT("where ") << QuoteSpaces(strOut); WaitCursor wait; SDChildProcess proc(str); IOBuffer buf(proc.Handle()); while (buf.NextLine(str)) { str.Chomp(); Substring rgss[3]; if (rgss[2].SetStart(Parse(TEXT("$P $P "), str, rgss))) { PostmungeFilespec(str); rgss[2].SetEnd(str + str.Length()); strOut.Reset(); strOut << rgss[X] << ssPath._pszMax; return TRUE; } } return FALSE; } /***************************************************************************** * * MapToLocalPath * * MapToXPath does most of the work, but then we have to do some * magic munging if we are running from a fake directory. * *****************************************************************************/ BOOL MapToLocalPath(LPCTSTR pszSD, String& strOut) { BOOL fSuccess = MapToXPath(pszSD, strOut, MAPTOX_LOCAL); if (fSuccess && !GlobalSettings.GetFakeDir().IsEmpty()) { if (strOut.BufferLength() < MAX_PATH) { if (!strOut.Grow(MAX_PATH - strOut.BufferLength())) { return FALSE; // Out of memory } } LPCTSTR pszRest = strOut + lstrlen(GlobalSettings.GetFakeDir()); if (*pszRest == TEXT('\\')) { pszRest++; } PathCombine(strOut.Buffer(), GlobalSettings.GetLocalRoot(), pszRest); fSuccess = TRUE; } return fSuccess; } /***************************************************************************** * * SpawnProcess * *****************************************************************************/ BOOL SpawnProcess(LPTSTR pszCommand) { STARTUPINFO si = { 0 }; PROCESS_INFORMATION pi; BOOL fSuccess = CreateProcess(NULL, pszCommand, NULL, NULL, FALSE, 0, NULL, NULL, &si, &pi); if (fSuccess) { CloseHandle(pi.hThread); CloseHandle(pi.hProcess); } return fSuccess; } /***************************************************************************** * * WindiffChangelist * *****************************************************************************/ void WindiffChangelist(int iChange) { if (iChange > 0) { String str; str << TEXT("windiff.exe -ld") << iChange; SpawnProcess(str); } } /***************************************************************************** * * WindiffOneChange * *****************************************************************************/ void WindiffOneChange(LPTSTR pszPath) { Substring rgss[2]; if (Parse(TEXT("$P#$d$e"), pszPath, rgss)) { String str; str << TEXT("windiff.exe "); rgss[0].Finalize(); int iVersion = StrToInt(rgss[1].Start()); if (iVersion > 1) { /* Edit is easy */ str << QuoteSpaces(rgss[0].Start()) << TEXT("#") << (iVersion - 1); } else { /* Add uses NUL as the base file */ str << TEXT("NUL"); } str << TEXT(' '); str << QuoteSpaces(rgss[0].Start()) << TEXT("#") << iVersion; SpawnProcess(str); } } /***************************************************************************** * * ParseBugNumber * * See if there's a bug number in there. * * Digits at the beginning - bug number. * Digits after a space or punctuation mark - bug number. * Digits after the word "bug" or the letter "B" - bug number. * * A valid bug number must begin with a nonzero digit. * *****************************************************************************/ int ParseBugNumber(LPCTSTR psz) { Substring ss; LPCTSTR pszStart = psz; while (*psz) { if (IsDigit(*psz)) { if (*psz == TEXT('0')) { // Nope, cannot begin with zero } else if (psz == pszStart) { return StrToInt(psz); // woo-hoo! } else switch (psz[-1]) { case 'B': case 'g': case 'G': return StrToInt(psz); // Comes after a B or a G default: if (!IsAlpha(psz[-1])) { return StrToInt(psz); // Comes after a space or punctuation } } // Phooey, a digit string beginning with 0; not a bug. while (IsDigit(*psz)) psz++; } else { psz++; } } return 0; } /***************************************************************************** * * ParseBugNumberFromSubItem * * Sometimes we use this just to parse regular numbers since regular * numbers pass the Bug Number Test. * *****************************************************************************/ int ParseBugNumberFromSubItem(HWND hwnd, int iItem, int iSubItem) { TCHAR sz[MAX_PATH]; sz[0] = TEXT('\0'); if (iItem >= 0) { ListView_GetSubItemText(hwnd, iItem, iSubItem, sz, ARRAYSIZE(sz)); } return ParseBugNumber(sz); } /***************************************************************************** * * AdjustBugMenu * *****************************************************************************/ inline void _TrimAtTab(LPTSTR psz) { psz = StrChr(psz, TEXT('\t')); if (psz) *psz = TEXT('\0'); } void AdjustBugMenu(HMENU hmenu, int iBug, BOOL fContextMenu) { TCHAR sz[MAX_PATH]; String str; if (iBug) { str << StringResource(IDS_VIEWBUG_FORMAT); wnsprintf(sz, ARRAYSIZE(sz), str, iBug); if (fContextMenu) { _TrimAtTab(sz); } ModifyMenu(hmenu, IDM_VIEWBUG, MF_BYCOMMAND, IDM_VIEWBUG, sz); } else { str << StringResource(IDS_VIEWBUG_NONE); ModifyMenu(hmenu, IDM_VIEWBUG, MF_BYCOMMAND, IDM_VIEWBUG, str); } EnableDisableOrRemoveMenuItem(hmenu, IDM_VIEWBUG, iBug, fContextMenu); } /***************************************************************************** * * OpenBugWindow * *****************************************************************************/ void OpenBugWindow(HWND hwnd, int iBug) { String str; GlobalSettings.FormatBugUrl(str, iBug); LPCTSTR pszArgs = PathGetArgs(str); PathRemoveArgs(str); PathUnquoteSpaces(str); _AllowSetForegroundWindow(-1); ShellExecute(hwnd, NULL, str, pszArgs, 0, SW_NORMAL); } /***************************************************************************** * * SetClipboardText * *****************************************************************************/ #ifdef UNICODE #define CF_TSTR CF_UNICODETEXT #else #define CF_TSTR CF_TEXT #endif void SetClipboardText(HWND hwnd, LPCTSTR psz) { if (OpenClipboard(hwnd)) { EmptyClipboard(); int cch = lstrlen(psz) + 1; HGLOBAL hglob = GlobalAlloc(GMEM_MOVEABLE, cch * sizeof(*psz)); if (hglob) { LPTSTR pszCopy = RECAST(LPTSTR, GlobalLock(hglob)); if (pszCopy) { lstrcpy(pszCopy, psz); GlobalUnlock(hglob); if (SetClipboardData(CF_TSTR, hglob)) { hglob = NULL; // ownership transfer } } if (hglob) { GlobalFree(hglob); } } CloseClipboard(); } } /***************************************************************************** * * ContainsWildcards * * The SD wildcards are * * * (asterisk) * ... (ellipsis) * %n (percent sign followed by anything) * (null) (null string -- shorthand for "//...") * *****************************************************************************/ BOOL ContainsWildcards(LPCTSTR psz) { if (*psz == TEXT('#') || *psz == TEXT('@') || *psz == TEXT('\0')) { return TRUE; // Null string wildcard } for (; *psz; psz++) { if (*psz == TEXT('*') || *psz == TEXT('%')) { return TRUE; } if (psz[0] == TEXT('.') && psz[1] == TEXT('.') && psz[2] == TEXT('.')) { return TRUE; } } return FALSE; } /***************************************************************************** * * Downlevel support * *****************************************************************************/ #ifdef SUPPORT_DOWNLEVEL /* * If there is no thread pool, then chew an entire thread. */ BOOL WINAPI Emulate_QueueUserWorkItem(LPTHREAD_START_ROUTINE pfn, LPVOID Context, ULONG Flags) { DWORD dwId; HANDLE hThread = CreateThread(NULL, 0, pfn, Context, 0, &dwId); if (hThread) { CloseHandle(hThread); return TRUE; } return FALSE; } BOOL WINAPI Emulate_AllowSetForegroundWindow(DWORD dwProcessId) { return FALSE; } QUEUEUSERWORKITEM _QueueUserWorkItem; ALLOWSETFOREGROUNDWINDOW _AllowSetForegroundWindow; template T GetProcFromModule(LPCTSTR pszModule, LPCSTR pszProc, T Default) { T t; HMODULE hmod = GetModuleHandle(pszModule); if (pszModule) { t = RECAST(T, GetProcAddress(hmod, pszProc)); if (!t) { t = Default; } } else { t = Default; } return t; } #define GetProc(mod, fn) \ _##fn = GetProcFromModule(TEXT(mod), #fn, Emulate_##fn) void InitDownlevel() { GetProc("KERNEL32", QueueUserWorkItem); GetProc("USER32", AllowSetForegroundWindow); } #undef GetProc #else #define InitDownlevel() #endif /***************************************************************************** * * Main program stuff * *****************************************************************************/ LONG GetDllVersion(LPCTSTR pszDll) { HINSTANCE hinst = LoadLibrary(pszDll); DWORD dwVersion = 0; if (hinst) { DLLGETVERSIONPROC DllGetVersion; DllGetVersion = (DLLGETVERSIONPROC)GetProcAddress(hinst, "DllGetVersion"); if (DllGetVersion) { DLLVERSIONINFO dvi; dvi.cbSize = sizeof(dvi); if (SUCCEEDED(DllGetVersion(&dvi))) { dwVersion = MAKELONG(dvi.dwMinorVersion, dvi.dwMajorVersion); } } // Leak the DLL - we're going to use him anyway } return dwVersion; } /***************************************************************************** * * Globals * *****************************************************************************/ BOOL InitGlobals() { g_hinst = GetModuleHandle(0); g_hcurWait = LoadCursor(NULL, IDC_WAIT); g_hcurArrow = LoadCursor(NULL, IDC_ARROW); g_hcurAppStarting = LoadCursor(NULL, IDC_APPSTARTING); if (GetDllVersion(TEXT("Comctl32.dll")) < MAKELONG(71, 4) || GetDllVersion(TEXT("Shlwapi.dll")) < MAKELONG(71, 4)) { TCHAR sz[MAX_PATH]; LoadString(g_hinst, IDS_IE4, sz, ARRAYSIZE(sz)); //$$//BUGBUG// MessageBox(NULL, sz, g_szTitle, MB_OK); return FALSE; } InitDownlevel(); InitCommonControls(); /* * Get the SW_ flag for the first window. */ STARTUPINFOA si; si.cb = sizeof(si); si.dwFlags = 0; GetStartupInfoA(&si); if (si.dwFlags & STARTF_USESHOWWINDOW) { g_wShowWindow = si.wShowWindow; } else { g_wShowWindow = SW_SHOWDEFAULT; } return TRUE; } void TermGlobals() { } /***************************************************************************** * * Help * *****************************************************************************/ void Help(HWND hwnd, LPCTSTR pszAnchor) { TCHAR szSelf[MAX_PATH]; GetModuleFileName(g_hinst, szSelf, ARRAYSIZE(szSelf)); String str; str << TEXT("res://") << szSelf << TEXT("/tips.htm"); if (pszAnchor) { str << pszAnchor; } _AllowSetForegroundWindow(-1); ShellExecute(hwnd, NULL, str, 0, 0, SW_NORMAL); } /***************************************************************************** * * CGlobals::Initialize * *****************************************************************************/ void CGlobals::Initialize() { /* * The order of these three steps is important. * * - We have to get the path before we can call sd. * * - We need the "sd info" in order to determine what * the proper fake directory is. */ _InitSdPath(); _InitInfo(); _InitFakeDir(); _InitServerVersion(); _InitBugPage(); } /***************************************************************************** * * CGlobals::_InitSdPath * * The environment variable "SD" provides the path to the program to use. * The default is "sd", but for debugging, you can set it to "fakesd", * or if you're using that other company's product, you might even want * to set it to that other company's program... * *****************************************************************************/ void CGlobals::_InitSdPath() { TCHAR szSd[MAX_PATH]; LPTSTR pszSdExe; DWORD cb = GetEnvironmentVariable(TEXT("SD"), szSd, ARRAYSIZE(szSd)); if (cb == 0 || cb > ARRAYSIZE(szSd)) { pszSdExe = TEXT("SD.EXE"); // Default value } else { pszSdExe = szSd; } cb = SearchPath(NULL, pszSdExe, TEXT(".exe"), ARRAYSIZE(_szSd), _szSd, NULL); if (cb == 0 || cb > ARRAYSIZE(_szSd)) { /* * Not found on path, eek! Just use sd.exe and wait for the * fireworks. */ lstrcpyn(_szSd, TEXT("SD.EXE"), ARRAYSIZE(_szSd)); } } /***************************************************************************** * * CGlobals::_InitInfo * * Collect the results of the "sd info" command. * *****************************************************************************/ void CGlobals::_InitInfo() { static const LPCTSTR s_rgpsz[] = { TEXT("User name: "), TEXT("Client name: "), TEXT("Client root: "), TEXT("Current directory: "), TEXT("Server version: "), }; COMPILETIME_ASSERT(ARRAYSIZE(s_rgpsz) == ARRAYSIZE(_rgpszSettings)); WaitCursor wait; SDChildProcess proc(TEXT("info")); IOBuffer buf(proc.Handle()); String str; while (buf.NextLine(str)) { str.Chomp(); int i; for (i = 0; i < ARRAYSIZE(s_rgpsz); i++) { LPTSTR pszRest = Parse(s_rgpsz[i], str, NULL); if (pszRest) { _rgpszSettings[i] = pszRest; } } } } /***************************************************************************** * * CGlobals::_InitFakeDir * * See if the user is borrowing another person's enlistment. * If so, then virtualize the directory (by walking the tree * looking for an sd.ini file) to keep sd happy. * * DO NOT WHINE if anything is wrong. Magical resolution of * borrowed directories is just a nicety. * *****************************************************************************/ void CGlobals::_InitFakeDir() { /* * If the client root is not a prefix of the current directory, * then cook up a virtual current directory that will keep sd happy. */ _StringCache& pszClientRoot = _rgpszSettings[SETTING_CLIENTROOT]; _StringCache& pszLocalDir = _rgpszSettings[SETTING_LOCALDIR]; if (!pszClientRoot.IsEmpty() && !pszLocalDir.IsEmpty() && !PathIsPrefix(pszClientRoot, pszLocalDir)) { TCHAR szDir[MAX_PATH]; TCHAR szOriginalDir[MAX_PATH]; TCHAR szSdIni[MAX_PATH]; szDir[0] = TEXT('\0'); GetCurrentDirectory(ARRAYSIZE(szDir), szDir); if (!szDir[0]) return; // Freaky lstrcpyn(szOriginalDir, szDir, ARRAYSIZE(szOriginalDir)); do { PathCombine(szSdIni, szDir, TEXT("sd.ini")); if (PathFileExists(szSdIni)) { _pszLocalRoot = szDir; // // Now work from the root back to the current directory. // LPTSTR pszSuffix = szOriginalDir + lstrlen(szDir); if (pszSuffix[0] == TEXT('\\')) { pszSuffix++; } PathCombine(szSdIni, _rgpszSettings[SETTING_CLIENTROOT], pszSuffix); _pszFakeDir = szSdIni; break; } } while (PathRemoveFileSpec(szDir)); } } /***************************************************************************** * * CGlobals::_InitServerVersion * *****************************************************************************/ void CGlobals::_InitServerVersion() { Substring rgss[5]; if (Parse(TEXT("$w $d.$d.$d.$d"), _rgpszSettings[SETTING_SERVERVERSION], rgss)) { for (int i = 0; i < VERSION_MAX; i++) { _rguiVer[i] = StrToInt(rgss[1+i].Start()); } } } /***************************************************************************** * * CGlobals::_InitBugPage * *****************************************************************************/ void CGlobals::_InitBugPage() { TCHAR szRaid[MAX_PATH]; DWORD cb = GetEnvironmentVariable(TEXT("SDVRAID"), szRaid, ARRAYSIZE(szRaid)); if (cb == 0 || cb > ARRAYSIZE(szRaid)) { LoadString(g_hinst, IDS_DEFAULT_BUGPAGE, szRaid, ARRAYSIZE(szRaid)); } LPTSTR pszSharp = StrChr(szRaid, TEXT('#')); if (pszSharp) { *pszSharp++ = TEXT('\0'); } _pszBugPagePre = szRaid; _pszBugPagePost = pszSharp; } /***************************************************************************** * * CommandLineParser * *****************************************************************************/ class CommandLineParser { public: CommandLineParser() : _tok(GetCommandLine()) {} BOOL ParseCommandLine(); void Invoke(); private: BOOL ParseMetaParam(); BOOL TokenWithUndo(); void UndoToken() { _tok.Restart(_pszUndo); } private: Tokenizer _tok; LPCTSTR _pszUndo; LPTHREAD_START_ROUTINE _pfn; String _str; }; BOOL CommandLineParser::TokenWithUndo() { _pszUndo = _tok.Unparsed(); return _tok.Token(_str); } BOOL CommandLineParser::ParseMetaParam() { switch (_str[2]) { case TEXT('s'): if (_str[3] == TEXT('\0')) { _tok.Token(_str); GlobalSettings.SetSdOpts(_str); } else { GlobalSettings.SetSdOpts(_str+3); } break; case TEXT('#'): switch (_str[3]) { case TEXT('+'): case TEXT('\0'): GlobalSettings.SetChurn(TRUE); break; case TEXT('-'): GlobalSettings.SetChurn(FALSE); break; default: return FALSE; } break; default: return FALSE; } return TRUE; } BOOL CommandLineParser::ParseCommandLine() { _tok.Token(_str); // Throw away program name /* * First collect the meta-parameters. These begin with two dashes. */ while (TokenWithUndo()) { if (_str[0] == TEXT('-') && _str[1] == TEXT('-')) { if (!ParseMetaParam()) { return FALSE; } } else { break; } } /* * Next thing had better be a command! */ if (lstrcmpi(_str, TEXT("changes")) == 0) { _pfn = CChanges_ThreadProc; } else if (lstrcmpi(_str, TEXT("describe")) == 0) { _pfn = CDescribe_ThreadProc; } else if (lstrcmpi(_str, TEXT("filelog")) == 0) { _pfn = CFileLog_ThreadProc; } else if (lstrcmpi(_str, TEXT("fileout")) == 0) { _pfn = CFileOut_ThreadProc; } else if (lstrcmpi(_str, TEXT("opened")) == 0) { _pfn = COpened_ThreadProc; } else { /* * Eek! Must use psychic powers! */ Substring ss; if (_str[0] == TEXT('\0')) { /* * If no args, then it's "changes". */ _pfn = CChanges_ThreadProc; } else if (_str[0] == TEXT('-')) { /* * If it begins with a dash, then it's "changes". */ _pfn = CChanges_ThreadProc; } else if (Parse(TEXT("$d$e"), _str, &ss)) { /* * If first word is all digits, then it's "describe". */ _pfn = CDescribe_ThreadProc; } else if (_tok.Finished() && !ContainsWildcards(_str)) { /* * If only one argument that contains no wildcards, * then it's "filelog". */ _pfn = CFileLog_ThreadProc; } else { /* * If all else fails, assume "changes". */ _pfn = CChanges_ThreadProc; } UndoToken(); /* Undo all the tokens we accidentally ate */ } return TRUE; } void CommandLineParser::Invoke() { LPTSTR psz = StrDup(_tok.Unparsed()); if (psz) { InterlockedIncrement(&g_lThreads); ExitThread(_pfn(psz)); } } /***************************************************************************** * * Entry * * Program entry point. * *****************************************************************************/ EXTERN_C void PASCAL Entry(void) { if (InitGlobals()) { CommandLineParser parse; if (!parse.ParseCommandLine()) { Help(NULL, NULL); } else { GlobalSettings.Initialize(); parse.Invoke(); } } ExitProcess(0); }