You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
1191 lines
32 KiB
1191 lines
32 KiB
/*****************************************************************************
|
|
*
|
|
* 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);
|
|
}
|
|
|
|
/*****************************************************************************
|
|
*
|
|
* 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;
|
|
}
|
|
#endif
|
|
|
|
/*****************************************************************************
|
|
*
|
|
* 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<class T>
|
|
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 (_stricmp(_str, TEXT("changes")) == 0) {
|
|
_pfn = CChanges_ThreadProc;
|
|
} else if (_stricmp(_str, TEXT("describe")) == 0) {
|
|
_pfn = CDescribe_ThreadProc;
|
|
} else if (_stricmp(_str, TEXT("filelog")) == 0) {
|
|
_pfn = CFileLog_ThreadProc;
|
|
} else if (_stricmp(_str, TEXT("fileout")) == 0) {
|
|
_pfn = CFileOut_ThreadProc;
|
|
} else if (_stricmp(_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.
|
|
*
|
|
*****************************************************************************/
|
|
|
|
int
|
|
WINAPI
|
|
WinMain(
|
|
IN HINSTANCE hInstance,
|
|
IN HINSTANCE hPrevInstance,
|
|
IN LPSTR lpCmdLine,
|
|
IN int nShowCmd
|
|
)
|
|
{
|
|
if (InitGlobals()) {
|
|
CommandLineParser parse;
|
|
if (!parse.ParseCommandLine()) {
|
|
Help(NULL, NULL);
|
|
} else {
|
|
GlobalSettings.Initialize();
|
|
parse.Invoke();
|
|
}
|
|
}
|
|
|
|
ExitProcess(0);
|
|
}
|