Introduction
Imagine that you could open a document in a Visual SourceSafe database with one mouse click.
No more opening the SourceSafe client, browsing to the correct project tree and choosing 'view latest version' from the document's context menu. In the screenshot above, you see three links to project documentation in a Word document; the links have the new VSS: protocol prefix. Clicking a VSS: link will fire up the handler presented below and open the designated document using shell association (.doc -> Word). In other words: "VSS://$/Projects/VSS ProtHandler/TD-VSSProtocolHandler.doc" will start Visual SourceSafe, find the TD-VSSProtocolHandler.doc in project "$/Projects/VSS ProtHandler" and attempt to open it using "shellexec". This is all done using Automation, you won't see the SourceSafe client at all!
You are of course familiar with the use of HTTP:, FTP: and other protocols. The VSS: protocol can be used in almost the same manner in a (intranet) webpage, on the Windows Run box, in a Word document or in a shortcut. All protocol requests are handled by URL.dll (a DLL installed by Internet Explorer). URL.dll will attempt to find a registration for the protocol and call the registered handler. For HTTP: it is most likely Internet Explorer, for VSS: it is VSSProtocolHandler.exe.
Please note that the user must have the SourceSafe client installed (to use its automation objects) and his/her machine must be able to reach the VSS database.
This article presents two principles:
- Registering & hooking up the "VSS:" protocol to the handler VSSProtocolHandler.exe,
- Handling the protocol request: letting the handler open the indicated document on the protocol line from the Visual SourceSafe database, using automation.
Registering & hooking the "VSS:" protocol
Actually, the "VSS:" protocol is similar to the "MAILTO:" protocol. Researching this protocol is how I found out how to hook the protocol! A few lines in the registry and the protocol is registered.
[HKEY_CLASSES_ROOT]
[vss]
(Default) = "URL:VSS Protocol"
URL Protocol = ""
[DefaultIcon]
(Default) = "VSSProtocolHandler.exe"
[shell]
[open]
[command]
(Default) = "c:\whatever\VSSProtocolHandler.exe "%1""
The secret is the string entry URL Protocol
with an empty string. Now URL.dll recognizes "VSS" as a protocol.
Handling the protocol request
When the hook is registered, URL.dll will call VSSProtocolHandler.exe whenever it encounters "VSS:xxxx". The argument must be parsed by the handler.
VSS://$/Projects/VSS ProtHandler/TD-VSSProtocolHandler.doc
will eventually yield the next command line:
c:\whatever\VSSProtocolHandler.exe
"VSS://$/Projects/VSS ProtHandler/TD-VSSProtocolHandler.doc"
Note that the protocol prefix is still present in the argument. The handler needs to parse it and check for the "VSS:" or "VSS://" prefix. The remaining part of the argument is used to look up the document in the SourceSafe database.
The source archive contains a full Visual C++ 6 project for the handler. It's plain Win32 & Automation (no MFC). Note that the project settings automatically register the "VSS:" protocol once the project has compiled successfully, just like a COM component project does.
The code was developed on Windows 2000 and only tested on Windows 2000.
The first part is parsing the argument:
int APIENTRY WinMain(HINSTANCE hInstance,
HINSTANCE hPrevInstance,
LPSTR lpCmdLine,
int nCmdShow)
{
char szArgument[1024] = {0};
LPSTR pszVSSFile = NULL;
StrNCpy(szArgument, lpCmdLine, 1023);
PathUnquoteSpaces(szArgument);
char *pszTemp = szArgument;
while (*pszTemp)
{
if (*pszTemp == '\\')
*pszTemp = '/';
pszTemp++;
}
if (strlen(szArgument) == 0)
{
ReportError(pszErrorNoArgumentSpecified);
exit(1);
}
else if (_strnicmp(szArgument, "vss://$/", 8) != 0)
{
if (_strnicmp(szArgument, "vss:$/", 6) != 0)
{
if (_strnicmp(szArgument, "/register", 9) == 0)
{
RegisterVSSProtocol(_strnicmp(szArgument,
"/registerq", 10) == 0? true: false);
exit(0);
}
else if (_strnicmp(szArgument, "/?", 2) == 0)
{
ShowHelp();
exit(0);
}
else
{
ReportError(pszErrorProtocolInvalidArgument,
szArgument, pszAbout);
exit(1);
}
}
else
pszVSSFile = szArgument + 4;
}
else
pszVSSFile = szArgument + 6;
if (strlen(pszVSSFile) == 0)
{
MessageBox(NULL, pszErrorNoVSSFileSpecified,
pszAppName, MB_ICONERROR);
return 1;
}
Next is firing up COM, instantiating a Visual SourceSafe Object instance, finding the current SourceSafe database in the registry and calling ViewVSSFile()
to get the intended file:
HRESULT hr;
if (FAILED(hr=CoInitialize(NULL)))
{
ReportError(pszErrorFailedToInitCOM, hr);
exit(1);
}
CLSID clsid;
if(SUCCEEDED(hr=CLSIDFromProgID(L"SourceSafe", &clsid)))
{
IVSSDatabase *pVSSDBObject = NULL;
if (SUCCEEDED(hr=CoCreateInstance(clsid, NULL,
CLSCTX_ALL, IID_IVSSDatabase, (void**)&pVSSDBObject)))
{
long lErr = 0;
HKEY hKey;
if ((lErr = RegOpenKeyEx(HKEY_LOCAL_MACHINE,
_T("SOFTWARE\\Microsoft\\SourceSafe"),
0,
KEY_READ,
&hKey)) == ERROR_SUCCESS)
{
DWORD dwType = REG_SZ;
char szVSSDatabasePath[512] = {0};
DWORD dwSize = 511;
if ((lErr = RegQueryValueEx(hKey,
_T("Current Database"),
NULL,
&dwType,
(LPBYTE)szVSSDatabasePath,
&dwSize)) == ERROR_SUCCESS && dwType == REG_SZ)
{
if (!ViewVSSFile(pVSSDBObject,
szVSSDatabasePath, pszVSSFile))
{
}
}
else
ReportError(pszErrorFindingVSSCurrentDatabase);
RegCloseKey(hKey);
}
else
ReportError(pszErrorOpeningVSSRegKey, lErr);
pVSSDBObject->Release();
}
else
ReportError(pszErrorCreatingVSSAutomationObject, hr);
}
else
ReportError(pszErrorFindingVSSAutomationObject, hr);
CoUninitialize();
ViewVSSFile()
will open the designated database (srcsafe.ini), look up the designated item (file), test it if it is a file, which is not marked 'Deleted', and performs a 'Get' command on it, resulting in a file in the Temporary directory. This file is executed using ShellExecuteW
(UNICODE version because the arguments (widestrings) are UNICODE and I was lazy at the time).
bool ViewVSSFile(IVSSDatabase *pVSSDBObject,
char *pszVSSDatabasePath, char *pszVSSFile)
{
bool bViewSucceeded = false;
HRESULT hr = S_OK;
PathRemoveBackslash(pszVSSDatabasePath);
strcat(pszVSSDatabasePath, "\\srcsafe.ini");
CComBSTR bsVSSIniFile(pszVSSDatabasePath);
if(SUCCEEDED(hr=pVSSDBObject->Open(bsVSSIniFile, L"", L"")))
{
IVSSItem *pIVSSItem = NULL;
CComBSTR bsVSSFile(pszVSSFile);
pszAppName, MB_ICONINFORMATION);
if(SUCCEEDED(hr=pVSSDBObject->get_VSSItem(bsVSSFile,
0, &pIVSSItem)))
{
int nItemType = -1;
if (SUCCEEDED(hr = pIVSSItem->get_Type(&nItemType)))
{
if (nItemType == VSSITEM_FILE)
{
VARIANT_BOOL vbDeleted;
if (SUCCEEDED(hr =
pIVSSItem->get_Deleted(&vbDeleted)))
{
if (vbDeleted == VARIANT_FALSE)
{
char *pszTargetFileName=
StrRChrA(pszVSSFile, NULL, '/');
pszTargetFileName++;
char szTempFile[_MAX_PATH] = {0};
if (GetTempPath(_MAX_PATH, szTempFile) > 0)
{
StrCat(szTempFile, pszTargetFileName);
CComBSTR bsLocal(szTempFile);
if (SUCCEEDED(hr=pIVSSItem->Get(&bsLocal,
VSSFLAG_REPREPLACE)))
{
int hInst = (int)ShellExecuteW(NULL, L"open",
(BSTR)bsLocal, NULL, NULL, SW_SHOWMAXIMIZED);
if (hInst <= 32)
ReportError(pszErrorOpeningTempDocument,
hInst, szTempFile, pszVSSFile);
else
bViewSucceeded = true;
}
else
ReportError(pszErrorGettingVSSDocument, hr,
pszVSSFile, pszVSSDatabasePath, szTempFile);
}
else
ReportError(pszErrorCantDetermineTempDir);
}
else
ReportError(pszErrorVSSFileMarkedDeleted, pszVSSFile);
}
else
ReportError(pszErrorRetrievingVSSFileDeletedState, hr);
}
else
ReportError(pszErrorVSSItemIsNotAFile, pszVSSFile);
}
else
ReportError(pszErrorRetrievingVSSFileType, hr);
pIVSSItem->Release();
}
else
ReportError(pszErrorFindingVSSDocument,
hr, pszVSSFile, pszVSSDatabasePath);
}
else
ReportError(pszErrorOpeningVSSDatabase, hr, pszVSSDatabasePath);
return bViewSucceeded;
}
There is some extra code to handle registration of the protocol and handle error messaging; they're pretty straight forward.
Demo executable
The demo archive contains the (release version) executable of the project. Extract it somewhere and run it through the Windows Run box (Start | Run) using argument "/register", e.g.: "C:\path\VSSProtocolHandler.exe /register". The handler will register the "VSS:" protocol and set itself up to be the handler. You are now ready to use the protocol. You may want to hardcode your SourceSafe username and password in VSS Open command (pVSSDBObject->Open(bsVSSIniFile, L"", L"")
) instead of the empty strings. Empty strings make SourceSafe use the current Windows username and cached password. You must also setup an argument line ("VSS://$/path_to_existing_file_in_VSS_database") in the project's Settings "Debug" tab before debugging.
Further
This project was a 'proof-of-concept', and was never intended to be an idiot proof, user ready application. I won't be doing any more development on it, but I still have some ideas that you might find interesting to implement:
- Better "logging on" code for the SourceSafe database. The current code assumes that you are using your Windows logon name to log on into the SourceSafe database. It also assumes that you have logged on to the database at least once; the logon/password is cached by Windows and is provided automatically in this case.
- If one uses more than one SourceSafe database, and the document cannot be found in the current SourceSafe database, then the handler could open up the other databases until the file is found (or not). SourceSafe stores all used databases in the registry, so this shouldn't be hard to find.
- Search for srcsafe.ini in the directory where ssapi.dll is located.
- Search for srcsafe.ini in each directory of the path to ssapi.dll. In other words, if ssapi.dll is located in C:\Folder1\Folder2\Folder3\SSAPI.DLL, then Folder3, Folder2, Folder1 and C:\ are searched (in that order).
- Search for srcsafe.ini in the location indicated by the named value "API Current Database" in the registry key HKEY_LOCAL_MACHINE\Software\Microsoft\SourceSafe.
- Search for srcsafe.ini in the location indicated by the named value "SCCProviderPath" in the registry key HKEY_LOCAL_MACHINE\Software\Microsoft\SourceSafe.
By the way: MS Office applications do not recognize the "VSS:" protocol as a hyperlink and underline it automatically. You will have to use the "Insert Hyperlink" (CTRL+K) command to enter a Visual SourceSafe hyperlink.
An MSDN article describes the "Note:" protocol, which works in the same manner as the "VSS" protocol, but is hooked to notepad.exe. However, this example doesn't work, because notepad.exe receives "Note://file.ext" as argument, but can't handle the prefix "Note://" and fails to open the document.
References
- Q167134 - HOWTO Open Visual SourceSafe to a Specific Project
- Q169928 - HOWTO Open a SourceSafe Database with OLE Automation
- Q175758 - HOWTO Trap Visual SourceSafe OLE Errors
- Q176350 - HOWTO Open Visual SourceSafe to a Specific Database
- Q201431 - HOWTO Write Automation for Visual SourceSafe 5_0-6_0
- Microsoft Visual SourceSafe OLE Automation (Ken Felder, October 1995)
- ssauterr - SourceSafe Automation errors
- ssauto - Visual C++ Header File for Visual SourceSafe
- Visual SourceSafe Frequently Asked Questions
- vsstree - VSS OLE Sample Code Written in Visual C