Ganz frisch aus dem Kundenforum für CoCreate Modeling (aka PTC Creo Elements/Direct Modeling): Wie startet man aus CoCreate Modeling heraus programmatisch eine interaktive Instanz von cmd.exe,
also ein cmd.exe mitsamt DOS-Prompt-Fenster?
Es liegt nahe, das zunächst mit
(oli:sd-sys-exec "cmd.exe")
zu versuchen. Das führt dann aber dazu, dass CoCreate Modeling scheinbar hängt und nichts Erkennbares passiert.
cmd.exe ist ein Kommandozeilenprogramm. Deswegen ist es völlig normal, dass ("grafisch") nichts passiert, wenn man cmd.exe als externes Programm ohne Parameter startet, zum Beispiel per sd-sys-exec. Dann wartet cmd.exe nämlich einfach im Hintergrund auf weitere Eingaben und tut sonst nichts.
Will man cmd.exe in einem eigenen Terminalfenster (landläufig "DOS-Fenster" oder "command shell" oder "command prompt") starten und interaktiv laufen lassen, kann man das so erreichen:
(oli:sd-sys-exec "start cmd.exe")
(Zu den Kommandozeilenparametern und Besonderheiten des Helferleins start siehe http://ss64.com/nt/start.html.)
Bonusfrage: Wenn cmd.exe ein Kommandozeilenprogramm ohne grafische Oberfläche ist, wieso öffnet sich denn ein Terminalfenster, wenn man cmd.exe aus Windows Explorer heraus startet?
Antwort: Weil Explorer entsprechend vorkonfiguriert ist - intern wird in so einem Fall nicht einfach nur cmd.exe ausgeführt, sondern das moralische Äquivalent zu start cmd.exe.
Bonusfrage 2: Woher weiss Windows eigentlich, wo cmd.exe liegt? Muss man da nicht einen Pfad wie C:\Windows\System32\cmd.exe angeben?
Hintergrund: In der Forumsfrage wurde ein solcher hartkodierter Pfad verwendet.
Antwort: Das Verzeichnis, in dem cmd.exe liegt, taucht im Inhalt der Umgebungsvariablen PATH auf, die Windows beim Starten von Programmen konsultiert. Damit ist die explizite Angabe eines Pfades unnötig. Mehr noch, sie ist sogar kontraproduktiv und fehlerträchtig - denn nicht auf jedem Rechner liegt das Windows-Verzeichnis unter C:\Windows.
Bonusfrage 3: Wozu ist das eigentlich gut, so eine interaktive Instanz von cmd.exe aus einer CAD-Applikation heraus zu starten?
Kopfkratzende erste Antwort: Für sachdienliche Hinweise dankbar
Zweite Antwort nach Eintreffen der angeforderten sachdienlichen Hinweise: Ziel war es offenbar letztlich, ein kleines interaktives Kommandozeilenprogramm zu starten - der Start von cmd.exe war nur erster Test dafür.
Like many other companies, my company provides VPN access to its employees so that we can stay
connected from our home offices or on the road. Most of the time, I connect to the company network
through a web portal which downloads, installs and runs
Juniper's "Network Connect" software on the Windows
client system. That's all fine and dandy, except that I am a command-line guy and find it way too
clumsy to fire up a web browser just in order to "dial in".
Fortunately, Juniper's Network Connect client has a command-line interface, and so here is a trivial
DOS batch script which can be used to establish a connection in "I-don't-need-no-stinkin'-buttons" mode.
The script assumes that the Network Connect client has been installed and run in the usual manner
(i.e. from the web portal) at least once. It will attempt to auto-detect the VPN host and user name,
so in most cases all you have to specify is password information. Oh, and the script assumes you want
to connect to the "SecurID(Network Connect)" realm by default, which requires entering a PIN and
a number displayed on your RSA SecurID token.
@echo offREM Launch Juniper Network Connect client from the command lineREM Written by Claus Brod in 2011, seeREMhttp://www.clausbrod.de/Blog/DefinePrivatePublic20110820JuniperNetworkConnectREM --------------------------------------------------------setlocal enableextensions
call :find_juniper_client NCCLIENTDIR
if"x%NCCLIENTDIR%"=="x" (
echo ERROR: Cannot find Network Connect client.goto :end
)
rem CONFIGURE: Set your preferred VPN host here.set url=define-your-vpn-host-here
ping -n 1%url% >nul
if not errorlevel1goto :validhostrem Try to auto-detect the VPN host from the config fileset NCCLIENTCONFIG="%NCCLIENTDIR%\..\Common Files\config.ini"if exist%NCCLIENTCONFIG%for/f"delims=[]"%%A in ('findstr [[a-z0-9]\. %NCCLIENTCONFIG% ^| findstr /V "Network Connect"') do set url=%%A
ping -n 1%url% >nul
if errorlevel1 (
echo ERROR: Host %url% does not ping. Please check your configuration.goto :end
)
:validhostcall :read_no_history url %url%"VPN host"set user=guestcall :read_no_history user %user%"Username"rem CONFIGURE: Set your preferred realm here. By default, the scriptrem assumes two-stage authentication using a PIN and RSA SecurID.set realm="SecurID(Network Connect)"call :read_no_history realm %realm%"Realm"REMTODO: Hide password inputset password=""call :read_no_history password %password%"Enter PIN + token value for user %user%:"ifx%password%==x (
echo ERROR: No password specifiedgoto :end
)
clsecho Launching Juniper Network Connect client inecho%NCCLIENTDIR%..."%NCCLIENTDIR%\nclauncher.exe" -url %url% -u %user% -p %password% -r %realm%goto :endREM --------------------------------------------------------:find_juniper_clientsetlocalset CLIENT=rem search registry firstfor/f"tokens=1* delims= "%%A in ('reg query "HKLM\SOFTWARE\Juniper Networks"2^>nul') do set LATESTVERSION="%%A"ifx%LATESTVERSION%==x""goto :eoffor/f"tokens=2* delims= "%%A in ('reg query %LATESTVERSION% /v InstallPath 2^>nul ^| findstr InstallPath') do set CLIENT=%%B
rem if nothing found, check filesystemif"x%CLIENT%"=="x"for/d%%A in ("%ProgramFiles(x86)%\Juniper Networks\Network Connect*") do set CLIENT=%%A
if"x%CLIENT%"=="x"for/d%%A in ("%ProgramFiles%\Juniper Networks\Network Connect*") do set CLIENT=%%A
endlocal & set"%~1=%CLIENT%"goto :eofREM --------------------------------------------------------REM read_no_history promptvar default promptmessage:read_no_historysetlocalset msg=%~3if not "x%~2"=="x" (
set msg="%~3 (default: %~2): "
)
set/P RNH_TEMP=%msg% <nul
set RNH_TEMP=REM call external script to avoid adding to our own command historyset RNH_CMDFILE=%TEMP%\temp$$$.cmd
(
echo @echo offecho set var_=%2echo set /p var_=echo echo %%var_%%
)> "%RNH_CMDFILE%"for/f"delims=,"%%A in ('%RNH_CMDFILE%') do set RNH_TEMP=%%A
del%RNH_CMDFILE%endlocal & if not x%RNH_TEMP%==x set"%~1=%RNH_TEMP%"goto :eofREM --------------------------------------------------------:endendlocal
Let’s suppose you had written this test case for low-level
DDE communication in your product, and that this test talks to Internet Explorer via DDE.
Let’s assume you’d do this by sending a URL to IE via DDE, and that you’d then verify the result by asking IE which page it actually loaded.
Let’s say that you’d use the URL of your company’s website, http://www.cocreate.com.
The day your QA people start yelling at you because the test fails miserably, you know
that your company has been acquired, and that all accesses to http://www.cocreate.com have been automatically redirected to http://www.ptc.com
A co-worker needed to convert a Cygwin-dependent script to something that runs on a bare-bones
Windows system. The interesting part of the task was finding a replacement for the good ol'
head command-line utility.
Fortunately, this is fairly simple using a few lines of VBScript and the
Windows Scripting Host.
First, here's the VBScript code:
lines = WScript.Arguments(0)
Do Until WScript.stdin.AtEndOfStream Or lines=0
WScript.Echo WScript.stdin.ReadLine
lines = lines-1
Loop
This is an extremely stripped-down version of head's original functionality, of course. For
example, the code above can only read from standard input, and things like command-line argument
validation and error handling are left as an exercise for the reader
Assuming you'd save the above into a file called head.vbs, this is how you can
display the first three lines of a text file called someinputfile.txt:
type someinputfile.txt | cscript /nologo head.vbs 3
A few months ago, I blogged about crash reporting under Windows XP and Vista. The protocol
between Windows and Microsoft's Watson servers is undocumented, but contains useful hints
on how the mechanism works. Back then, I hinted that I had intercepted the crash reporting
traffic to learn more about it, but I didn't fully describe how I did that.
If only I could remember, that is
Well, I remember the basic approach, and I even took some notes back then which I will regurgitate
here and now. However, don't expect step-by-step instructions.
My goal was to take control of the crash reporting process in my application.
When an application crashes, Microsoft's official recommendation is that it should not
try to catch the fatal exception, but instead simply bail out, let Windows perform
its crash reporting rites, and then terminate. For various reasons, we needed more
control over the process, and so I set out on a discovery tour through
the Windows Error Reporting APIs which Microsoft introduced in Windows Vista. The
details of this epic saga can be found at:
For a long time, I simply couldn't get crash reporting to work as I expected it to.
I had pretty much exhausted most of the official documentation, so I needed to dig deeper.
When an application crashes and the user agrees to send the crash data to Microsoft, Windows
contacts Microsoft's Watson servers. For some reason, my crash reports weren't accepted there,
while the usual plain vanilla crashme.exe could successfully dump its debris to Microsoft's
servers. Hence, the idea was to look at the network traffic and find the differences between
my crash reports and those produced by the proverbial crashme.exe.
I could have run the usual network sniffing suspects to decode LAN traffic including all the email exchanged
between my boss and his boss, of course. But there is an even easier and less controversial approach:
For corporate environments, where admins often need more control over the crash reporting
process, Microsoft introduced corporate error reporting (CER), where crashing systems can contact
a local server rather than sending all those confidential access violations to Microsoft.
There are registry entries
to set the server name and port for corporate error reporting:
HKEY_LOCAL_MACHINE\Software\Microsoft\Windows\Windows Error Reporting\CorporateWERServer:
Name of the local crash reporting server
HKEY_LOCAL_MACHINE\Software\Microsoft\Windows\Windows Error Reporting\CorporateWERPortNumber:
Port number to be used for communication
On my Vista system, I modified CorporateWERServer so that it referred to my laptop. I did not set
the port number explicitly; using procmon,
I found that the default port is 1273.
On my laptop, I installed netcat and had it listen to
input from port 1273 (nc -l273 or something like that). Once the port was open, Vista started to
send HTTP POST requests to it - so the CER server really is a specialized HTTP server listening
to port 1273! Here's a typical request (slightly polished and anonymized) following a crash
in a sample app I was writing back then:
By comparing this kind of payload with the traffic generated by a plain vanilla crashme.exe program,
I could experiment with the various WER APIs and settings until I had finally figured out how to
use them. Without crash report sniffing, I'd probably still be experimenting...
When an application crashes under Windows Vista, it will contact Microsoft's
Windows Error Reporting servers; only if those servers request more data
about the problem, crashdump files will be generated. Bad luck if you're
a developer talking to an enraged customer who just lost his data, and you
first have to go through a lengthy process of registering your application
on Winqual, mapping
the right application versions, and waiting until the first customer
crashdumps show up on the server. Chances are that your customer may
have taken his money elsewhere in the meantime while waiting for you
to fix his problem.
In Think globally, dump locally,
I listed the following workarounds:
Using the ForceQueue registry entry to force WER to always produce a crashdump (at the price of losing UI interaction)
Disable the Internet connection before the crash occurs
(Ab-)Using the CorporateWERServer registry entry
Deploy Microsoft Operations Manager 2007, including
Agentless Exception Monitoring
(which replaces the older "Corporate Error Reporting" servers)
Copy Dr. Watson from XP to the Vista system and install it as system JIT debugger
Install a top-level crash filter in your application using SetUnhandledExceptionFilter
In Don't dump Vista just yet...,
I presented a little-known Task Manager option which creates a user dump file from any
running process.
And in this installment of the seemingly never-ending series on Vista crashdumps,
we'll explore yet another option. What if we had a system JIT debugger which
can be easily installed on a customer system and automatically produces minidump files
for any crashing application? Basically a homebrew version of good ol' Dr. Watson, stripped
down to bare essentials?
The following code illustrates this approach. This skeleton application is
called mydearwatson and installs as a JIT debugger. When an app crashes,
it attaches to it and asks the user what kind of minidump (normal or full dump)
should be generated. The resulting crashdump file goes to the current user's
desktop folder, readily available for sending it off to the developer of
the application.
// mydearwatson
//
// Minimal JIT debugger; attaches to a crashing process
// and generates minidump information. Intended to be used
// as the rough skeleton for a poor man's Dr. Watson
// replacement on Vista.
//
// Written by Claus Brod, http://www.clausbrod.de/Blog
#include <windows.h>
#include <DbgHelp.h>
#pragma comment(lib, "DbgHelp.lib")
#include <Psapi.h>
#include <shlobj.h>
#include <atlbase.h>
#include <stdio.h>
#include <string.h>
#define MSGHDR "mydearwatson\n" \
"(C) 2007 Claus Brod, http://www.clausbrod.de/Blog\n\n"
bool uninstall(void)
{
CRegKey key;
if (ERROR_SUCCESS != key.Open(HKEY_LOCAL_MACHINE,
"Software\\Microsoft\\Windows NT\\CurrentVersion\\AeDebug\\",
KEY_READ | KEY_WRITE))
return false;
// check for old debugger registration
char debuggerCommandLine[MAX_PATH+256];
ULONG nChars = _countof(debuggerCommandLine);
LONG ret = key.QueryStringValue("PreMydearwatsonDebugger",
debuggerCommandLine, &nChars);
if (ret == ERROR_SUCCESS) {
ret = key.SetStringValue("Debugger", debuggerCommandLine);
if (ret == ERROR_SUCCESS) {
ret = key.DeleteValue("PreMydearwatsonDebugger");
}
}
return ret == ERROR_SUCCESS;
}
bool install(char *programName)
{
CRegKey key;
if (ERROR_SUCCESS != key.Open(HKEY_LOCAL_MACHINE,
"Software\\Microsoft\\Windows NT\\CurrentVersion\\AeDebug\\",
KEY_READ | KEY_WRITE))
return false;
char debuggerCommandLine[MAX_PATH+256];
ULONG nChars = _countof(debuggerCommandLine);
if (ERROR_SUCCESS == key.QueryStringValue("Debugger", debuggerCommandLine, &nChars)) {
_strlwr_s(debuggerCommandLine, _countof(debuggerCommandLine));
if (!strstr(debuggerCommandLine, "mydearwatson")) {
// save command line for previously installed debugger
key.SetStringValue("PreMydearwatsonDebugger", debuggerCommandLine);
}
}
char debuggerPath[MAX_PATH];
strcpy_s(debuggerPath, programName); // preset with default
::GetModuleFileName(GetModuleHandle(0), debuggerPath, _countof(debuggerPath));
_snprintf_s(debuggerCommandLine, _countof(debuggerCommandLine), _TRUNCATE,
"\"%s\" -p %%ld -e %%ld", debuggerPath);
return ERROR_SUCCESS == key.SetStringValue("Debugger", debuggerCommandLine);
}
char *getMinidumpPath(void)
{
staticchar dumpPath[MAX_PATH];
if (dumpPath[0] == 0) {
SHGetSpecialFolderPath(NULL, dumpPath, CSIDL_DESKTOPDIRECTORY, FALSE);
}
return dumpPath;
}
char *getMinidumpFilename(DWORD pid)
{
staticchar minidumpFilename[MAX_PATH];
if (!minidumpFilename[0]) {
_snprintf_s(minidumpFilename, MAX_PATH, _TRUNCATE,
"%s\\mydearwatson_pid%d.mdmp", getMinidumpPath(), pid);
}
return minidumpFilename;
}
bool dumpHelper(HANDLE hDumpFile,
HANDLE processHandle, DWORD pid,
HANDLE threadHandle, DWORD tid,
EXCEPTION_RECORD *exc_record, MINIDUMP_TYPE miniDumpType)
{
bool ret = false;
CONTEXT threadContext;
threadContext.ContextFlags = CONTEXT_ALL;
if (::GetThreadContext(threadHandle, &threadContext)) {
__try {
MINIDUMP_EXCEPTION_INFORMATION exceptionInfo;
exceptionInfo.ThreadId = tid;
EXCEPTION_POINTERS exc_ptr;
exc_ptr.ExceptionRecord = exc_record;
exc_ptr.ContextRecord = &threadContext;
exceptionInfo.ExceptionPointers = &exc_ptr;
exceptionInfo.ClientPointers = FALSE;
if (MiniDumpWriteDump(processHandle,
pid, hDumpFile, miniDumpType, &exceptionInfo, NULL, NULL))
ret = true;
} __except(EXCEPTION_EXECUTE_HANDLER) { }
}
return ret;
}
struct HandleOnStack
{
HANDLE m_h;
HandleOnStack(HANDLE h) : m_h(h) { }
~HandleOnStack() { if (m_h && m_h != INVALID_HANDLE_VALUE) CloseHandle(m_h); }
operator HANDLE() { return m_h; }
};
bool createMinidump(HANDLE processHandle, DWORD pid, DWORD tid,
EXCEPTION_RECORD *exc_record)
{
HandleOnStack hDumpFile(CreateFile(getMinidumpFilename(pid), GENERIC_WRITE, 0, NULL,
CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL));
if (hDumpFile == INVALID_HANDLE_VALUE)
return false;
HandleOnStack
threadHandle(OpenThread(THREAD_GET_CONTEXT | THREAD_SUSPEND_RESUME, 0, tid));
if (!threadHandle)
return false;
if (-1 == SuspendThread(threadHandle))
return false;
MINIDUMP_TYPE miniDumpType = MiniDumpNormal;
if (IDYES == MessageBox(NULL, MSGHDR
"By default, minimal crashdump information is generated.\n"
"Do you want full crashdump information instead?",
"mydearwatson", MB_YESNO|MB_ICONQUESTION|MB_DEFBUTTON2)) {
miniDumpType = MiniDumpWithFullMemory;
}
bool ret = dumpHelper(hDumpFile, processHandle, pid,
threadHandle, tid, exc_record, miniDumpType);
if (ret) {
char buf[1024];
_snprintf_s(buf, _countof(buf), _TRUNCATE, MSGHDR
"Minidump information has been written to\n%s.\n",
getMinidumpFilename(pid));
MessageBox(NULL, buf, "mydearwatson", MB_OK|MB_ICONINFORMATION);
}
ResumeThread(threadHandle);
return ret;
}
int debuggerLoop(DWORD pid, HANDLE eventHandle)
{
// attach to debuggee
if (!DebugActiveProcess(pid)) {
fprintf(stderr, "Could not attach to process %d\n", pid);
return 1;
}
HANDLE processHandle = 0;
while (1) {
DEBUG_EVENT de;
if (WaitForDebugEvent(&de, INFINITE)) {
switch(de.dwDebugEventCode)
{
case CREATE_PROCESS_DEBUG_EVENT:
processHandle = de.u.CreateProcessInfo.hProcess;
printf("Attaching to process %x...\n", processHandle);
break;
case EXCEPTION_DEBUG_EVENT:
printf("Exception reported: code=%x, dwFirstChance=%d\n",
de.u.Exception.ExceptionRecord.ExceptionCode, de.u.Exception.dwFirstChance);
if (de.u.Exception.ExceptionRecord.ExceptionCode == EXCEPTION_BREAKPOINT) {
SetEvent(eventHandle);
} else {
createMinidump(processHandle, de.dwProcessId, de.dwThreadId,
&de.u.Exception.ExceptionRecord);
ContinueDebugEvent(de.dwProcessId, de.dwThreadId,
DBG_EXCEPTION_NOT_HANDLED); // required?
DebugActiveProcessStop(pid);
printf("Detached from process, terminating debugger...\n");
return 0;
}
break;
default:
// printf("debug event code = %d\n", de.dwDebugEventCode);
break;
}
ContinueDebugEvent(de.dwProcessId, de.dwThreadId, DBG_CONTINUE);
}
} // while (1)
return 1;
}
void usage(char *programName)
{
fprintf(stderr, "To install as JIT debugger:\n");
fprintf(stderr, " %s -i\n", programName);
fprintf(stderr, " %s\n", programName);
fprintf(stderr, "To uninstall:\n");
fprintf(stderr, " %s -u\n", programName);
fprintf(stderr, "Call as JIT debugger:\n");
fprintf(stderr, " %s -p pid -e eventhandle\n", programName);
}
int main(int argc, char *argv[])
{
DWORD pid = 0;
HANDLE eventHandle = (HANDLE)0;
bool uninstallationMode = false;
bool installationMode = false;
if (argc == 1) {
if (IDYES == MessageBox(NULL, MSGHDR
"Do you want to install mydearwatson as the system JIT debugger?",
"mydearwatson", MB_YESNO|MB_ICONQUESTION)) {
installationMode = true;
}
}
for (int i=1; i<argc; i++) {
if (!_stricmp(argv[i], "-p")) {
pid = atol(argv[i+1]);
}
if (!_stricmp(argv[i], "-e")) {
eventHandle = (HANDLE)atol(argv[i+1]);
}
if (!_stricmp(argv[i], "-i")) {
installationMode = true;
uninstallationMode = false;
}
if (!_stricmp(argv[i], "-u")) {
uninstallationMode = true;
installationMode = false;
}
}
if (installationMode) {
if (!install(argv[0])) {
fprintf(stderr, "Could not register as a JIT debugger.\n");
return 1;
}
return 0;
}
if (uninstallationMode) {
if (!uninstall()) {
fprintf(stderr, "Could not uninstall.\n");
return 1;
}
return 0;
}
if (!pid || !eventHandle) {
usage(argv[0]);
return 2;
}
return debuggerLoop(pid, eventHandle);
}
To compile and build this code, open a Visual Studio command prompt and enter
cl mydearwatson.cpp
To install mydearwatson, run the executable in elevated mode and confirm
the installation message box. Now configure Windows Error Reporting to
always ask the user before submitting crash data:
"Problem Reports and Solutions/Change Settings/Ask me to check if a problem occurs".
Alternatively, create a registry value called Auto (REG_SZ) in HKEY_LOCAL_MACHINE\Software\Microsoft\Windows NT\CurrentVersion\AeDebug
and set its value to "1".
The next time an application crashes, the usual WER dialog will appear;
click the "Debug" option in that dialog. Another message box will
be displayed asking what kind of crashdump information should be
written. Make your choice, and the crashdump file will magically
appear on your desktop.
To uninstall, run mydearwatson with the -u option. mydearwatson
tries to remember which JIT debugger was installed before, and will
reinstall that JIT debugger. The mechanism for doing this is far from
perfect, though.
If you look at the code, you'll notice that it basically implements
a minimal debugger, using Win32 debugging APIs such as DebugActiveProcess
or WaitForDebugEvent. I've never written a debugger before, so
I'd assume there are a few subtleties and bugs hidden in this code,
but it did work for me on both XP and Vista systems. Test results
most welcome.
The other day, I was contemplating various approaches to
retrofit a feature into Vista which its new implementation of Windows Error Reporting
apparently took away from us. What I and a couple of folks in the
Windows Error Reporting discussion forum were desperately missing were crashdumps.
On Vista, WER only produces minidumps if Microsoft's Winqual servers ask it to.
So if a customer reports a crash, and your application isn't registered with Winqual,
it becomes a lot more difficult than on XP systems to get a crash dump file
to analyse the problem.
I already suggested a couple of workarounds, but they call came with their own
quirks or inconveniences. None of them was 100% satisfying.
Which actually was good news to me, as this inspired me to develop a ruthless,
but lucrative plot: I would sue the living daylights out of the poor folks in Redmond,
based on a charge of malicious removal of vital features from their operating
system, win the trial with fanfare, and then retreat to my newly-acquired beach
villa on a remote island, my only connection to my former life being the
ultra-hyper-gargantomanically-fast direct Internet connection via my own
private satellite parked in stationary orbit 36000 km above Brod mansion.
It wasn't meant to be.
The bubble burst after a mouse click at the right time in the wrong place.
For all eternity, cursed shall be the day I was taught to use the right mouse button.
Once more, I had run my crashme.exe test application which causes an
access violation on purpose. I had the Windows Task Manager
running. The WER dialog popped up, but for some reason, instead of using
that dialog, I right-clicked the process entry in the Task Manager window.
And there it was: The "Create Dump File" option, which I had not noticed
anytime before.
And indeed, this option does what it promises: It produces
a dump file which can be loaded into the debugger to inspect the cause of the
crash.
So if an application crashes on Vista, and you want to create crashdump information
and send it to the developer of the application right away, here's how:
Configure Windows Error Reporting so that it asks you explicitly
what to do when a crash occurs
("Problem Reports and Solutions/Change Settings/Ask me to check if a problem occurs")
Run and test the app until it crashes
Do not click any of the options in the WER dialog just yet. Instead, open
the Task Manager and go to the "Processes" tab. Right-click the entry
for the crashing app, and select "Create Dump File".
There's even a Knowledge Base article
on this feature.
Sigh. There goes the beach villa.
These days, I spend quite some time in Microsoft's Windows Error Reporting
forum, which is where David Ching, who is a Microsoft MVP, posed
an interesting problem this week.
On Vista, Windows Error Reporting will create and transmit minidump files only if the WER
servers request them. At least this seems to be the default behavior which both David and I have
observed on Vista systems. David, however, wanted to make sure that whenever an application
crashes, a minidump file is generated which the user or tester can then send directly
to the developers of the application for analysis - even if Microsoft's WER servers
never actually request the minidumps, which, as far as I can tell, is the default
for applications which have not been explicitly registered with and mapped
at Winqual.
My first idea was to force the system into queuing mode. When crash reports are queued,
minidumps are always generated and stored locally, so that they can be transmitted to
the error reporting server later on. Queuing is enabled by setting
HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\Windows Error Reporting\ForceQueue
(DWORD) to 1. (See
WER Settings
for documentation on this and other WER-related registry keys.)
Crash report data will be stored in directories such as
c:\Users\someusername\AppData\Local\temp and C:\ProgramData\Microsoft\Windows\WER\ReportQueue.
That works, but it also suppresses the WER UI, which isn't ideal either. Isn't
there some way to have the cake and eat it, too?
Let's see: A variation of the above approach is to disable the Internet connection before
the crash occurs. You'll get the dialogs, but WER won't be able to connect to the Microsoft
servers, and so it should then also queue the crash information. Alternatively,
and this is something that I have tried myself a few times, you could set
HKEY_LOCAL_MACHINE\Software\Microsoft\Windows\Windows Error Reporting\CorporateWERServer
(string) to the name of some non-existing system. When a crash occurs, WER will try
to contact that server, find that it's not responding, and then store all crash data
locally so that it can be re-sent when the connection is later established.
Or you could go all the way and actually install such a Corporate Error Reporting server
on one of your systems. Probably one of the best solutions, since this gives you
direct access to minidump files within your organization.
But this blog isn't about IT, it's about hacking and coding Here's an idea
how David's goals could be accomplished without implementing a full-blown
crash handler:
In that exception filter, call
MiniDumpWriteDump()
to create a local minidump file.
Return from the filter with EXCEPTION_CONTINUE_SEARCH.
And here's the demo code which demonstrate this technique:
// Demo program using SetUnhandledExceptionFilter() and
// MiniDumpWriteDump().
//
// Claus Brod, http://www.clausbrod.de/Blog
#include <windows.h>
#include <DbgHelp.h>
#pragma comment(lib, "DbgHelp.lib")
#include <stdio.h>
static LONG WINAPI myfilter(_EXCEPTION_POINTERS *exc_ptr)
{
staticconstchar *minidumpFilename = "myminidump.mdmp";
HANDLE hDumpFile = CreateFile(minidumpFilename, GENERIC_WRITE, 0, NULL,
CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
if (hDumpFile != INVALID_HANDLE_VALUE) {
__try {
MINIDUMP_EXCEPTION_INFORMATION exceptionInfo;
exceptionInfo.ThreadId = GetCurrentThreadId();
exceptionInfo.ExceptionPointers = exc_ptr;
exceptionInfo.ClientPointers = false;
BOOL ret = MiniDumpWriteDump(GetCurrentProcess(),
GetCurrentProcessId(), hDumpFile, MiniDumpNormal, &exceptionInfo, NULL, NULL);
if (ret) {
printf("Minidump information has been written to %s.\n", minidumpFilename);
}
} __except(EXCEPTION_EXECUTE_HANDLER) { }
CloseHandle(hDumpFile);
}
return EXCEPTION_CONTINUE_SEARCH;
}
staticint wedding_crasher(int *pp)
{
*pp = 42;
return 42;
}
int main(void)
{
SetUnhandledExceptionFilter(myfilter);
wedding_crasher(0);
return 0;
}
And finally, here's a really weird idea from Dmitry Vostokov:
Resurrecting Dr. Watson on Vista If you're into exception handling and crash analysis, Dmitry's
http://www.dumpanalysis.org/ web site is a fantastic resource. This guy lives
in an exception filter
In the first part of this mini-series, I
demonstrated the ReportFault API
and why it didn't fit my needs on Vista. Last time around, I discussed
my first attempt to use the new
Windows Error Reporting (WER) APIs instead, which failed to produce
any crash reports on Microsoft's Winqual site.
When the curtain fell last time, I had a WER test application which,
on the surface, appeared to work, but didn't manage to get any crash
reports through to Winqual. Also, entries for crash reports produced
by this application looked a little funny in Vista's Problem History window:
In particular, the Bucket ID value stands out. What are bucket IDs? Essentially,
the Winqual site combines various attributes of the crash report (application,
signatures, crash address etc.) and creates a unique integer value from them,
which then becomes an identifier for this particular type of crash.
All my WER-induced crash reports submitted from Vista clients always had a
bucket ID of 8, regardless of which test application I used and how exactly I provoked the
crash. Also, I knew from earlier, successful attempts to talk to the Winqual
servers how real bucket IDs usually look like (much larger integers).
Something fishy was going on here.
The application I tested was properly registered, signed and mapped
at the Winqual site, and crash reports submitted from XP systems made it to
the Winqual servers just fine. Hence, registration issues could be ruled
out. I posted to the
Windows Error Reporting forum
and asked for help and clarification. Saar Picker responded: "We filter out
unknown event types. Since your report is not of a recognized event type, it
is being rejected. The Bucket ID 8 event is reporting the rejection to us."
So my crash reports were not of a recognized event type. What's a poor
crash report supposed to do to be recognized?
The first parameter for WerReportCreate
is an event type. The documentations says: "wzEventType - A pointer to a
Unicode string that specifies the name of the event." Hmmm, so maybe this is
the event type that Saar mentioned. If so, what kind of event are we talking
about? Win32 events? Events like the ones captured in the Windows event log?
None of those, as it turns out. Instead, error reporting servers can define
types of error events that they want to capture.
Microsoft's Winqual servers, for example, are configured to accept event types
which represent application or operating system crashes.
So what is the magic event type which represents an application crash?
Hint 1: The werapi.h header file defines an undocumented macro constant called
APPCRASH_EVENT.
#define APPCRASH_EVENT L"APPCRASH"
Hint 2: When a crash report is submitted using WerReportSubmit, this API tries to
contact the error reporting server. In Vista, the protocol is based
on XML snippets which the client sends to the server via HTTP. One of
the attributes in the initial XML that is transmitted is called eventtype,
and for applications which do not try to handle fatal crashes themselves,
the value of that attribute is indeed "APPCRASH".
So I modified my WER code to use "APPCRASH" instead of some arbitrary
string. And indeed, this made a difference, although not the one I had hoped for:
With the new event type, WerReportSubmit() now returned an error
(E_FAIL), where it previously succeeded...
To debug the problem, I intercepted the XML exchange between the client
and the server, and looked at the differences between a non-WER client
and my own test code. (If you're interested in the interception details,
drop me a line.) The non-WER client transmitted
additional data (so-called "signature parameters"), and it also
specified a "report type" of 2 instead of 1. So my strategy was
to eliminate the differences one by one by working the WER APIs.
The extra parameters sent by the non-WER client were things like the
application's name, version and timestamp; the faulting module's name,
version and typestamp; and the exception code and address offset.
And now, finally, I understood the purpose of the
underdocumented WerReportSetParameter
API - depending on the server's setup, it expects certain extra
parameters to safely identify an event, and those can be set using
WerReportSetParameter:
The other significant change was to use the undocumented WerReportApplicationCrash constant
as the "report type" parameter for WerReportCreate. After these changes,
the Winqual servers finally started talking to me: I received bucket IDs, sometimes also
requests to transmit minidump data - and after a few days, the crash reports appeared
on the Winqual site! Whoopee!
The full demo code is attached. To build, open
a Visual Studio command prompt and run the compiler:
cl werapitest.cpp
My special thanks to Saar Picker and Jason Hardester at Microsoft for their help!
Now that I've achieved my original goal (reporting crashes using the WER APIs
under Vista), let me spoil the fun by warning you to ever use this approach. Why?
Because this is clearly not the way Microsoft recommends to handle application
crashes. Now, while I'm not sure whether Microsoft as a whole has an official
recommendation, the documentation or the postings in newsgroups in blogs clearly
suggest that an application shouldn't actually even try to handle a crash
explicitly - instead, it should just crash and let the OS do the reporting.
The basic rationale behind this is that an application is probably already
deeply confused when a crash occurs, and some of its data may already have been
damaged. This makes crash recovery a difficult and unreliable endeavor.
There are circumstances where an application needs to keep control of the reporting
process, but Microsoft expects such cases to be very rare. Which explains
a lot of the initial communication disconnects that I experienced while discussing
my case with Saar and Jason.
There's a reason why it's called "WER" (Windows Error Reporting)
and not "WCR" (Windows Crash Reporting). Apparently, Microsoft doesn't
expect us to use those APIs for crash reporting, but rather for more generic
"error" or "event" reporting. For example,
this U.S. patent claim
discusses how the WER APIs can be used to report failures in handwriting recognition.
(By the way, there's also a patent for WER itself, see http://www.freepatentsonline.com/20060271591.html.)
A few days ago, I reported about the
peculiarities of the ReportFault API,
particularly on Windows Vista, and how those peculiarities drove me to
give in to Microsoft's sound advice and use the new and shiny
Windows Error Reporting (WER) APIs
on Vista.ReportFault() is a great
one-stop shopping API: A one-liner will display
all required dialogs, ask the user if he wants to contact Microsoft,
create report data (including minidumps)
if required, and send the whole report off to Microsoft.
The new WER APIs
in Vista are slightly more complex, but also provide more control for
the details of error reporting. Well, if you know how to handle the APIs,
that is. Apparently, I do not know how to handle them since I still
haven't solved all the problems around them.
More on this in a moment. Let's first take a look at the core of a test application I wrote:
The WER APIs do indeed solve a problem that I found with ReportFault
on Vista: They don't force the calling process to be terminated, and
allow me to proceed as I see fit. That's really good news.
The problem I haven't resolved yet is this: Even though I call
WerReportAddDump, I have no idea whether minidump data are
actually generated and sent. In fact, from the feedback
provided by the system, it seems likely that those data are not
generated.
To illustrate my uncertainties, I wrote a test program called werapitest.
The code is attached as a ZIP file; unpack it into a directory, open
a Visual Studio command prompt window, and build the code as follows:
cl werapitest.cpp
Run the resulting executable, then open up the "Problem Reports and Solutions"
control panel and click on "View problem history". On my system, I get
something like this:
Double-clicking on the report entry leads to this:
The problem history entry does not mention any attached files, such as
minidump data!
When a crash occurs, the system also writes entries into the event log;
those log entries claim there are additional data in paths such as
C:\Users\clausb\AppData\Local\Microsoft\Windows\WER\ReportArchive\Report0f8918ad,
and indeed, such directories exist and each contain a file called Report.wer,
which holds data such as:
Version=1
EventType=werapitest (eventType)
EventTime=128266502225896608
ReportType=1
Consent=1
UploadTime=128266502257542112
Response.BucketId=8
Response.BucketTable=5
Response.type=4
DynamicSig[1].Name=OS Version
DynamicSig[1].Value=6.0.6000.2.0.0.256.16
DynamicSig[2].Name=Locale ID
DynamicSig[2].Value=1033
UI[3]=werapitest.exe has stopped working
UI[4]=Windows can check online for a solution to the problem.
UI[5]=Check online for a solution and close the program
UI[6]=Check online for a solution later and close the program
UI[7]=Close the program
State[0].Key=Transport.DoneStage1
State[0].Value=1
State[1].Key=DataRequest
State[1].Value=Bucket=8/nBucketTable=5/nResponse=1/n
FriendlyEventName=werapitest (friendly event name)
ConsentKey=werapitest (eventType)
AppName=werapitest.exe
AppPath=C:\tmp\werapitest.exe
ReportDescription=Critical runtime problem
So again, the minidump is not mentioned anywhere.
Now let's try some minimal code which uses neither ReportFault nor
the new WER API:
int main(void)
{
int *p = (int *)0;
*p = 42;
return 0;
}
After running this code and letting it crash and report to Microsoft, I get the following
problem history entry:
This problem report contains a lot more data than the one for werapitest,
and it even refers to a minidump file which was apparently generated by the system
and probably also sent to Microsoft.
So the lazy code which doesn't do anything about crashes gets full and proper
service from the OS, while the application which tries to deal with a crash in
an orderly manner and elaborately goes through all the trouble of using the
proper APIs doesn't get its message across to Microsoft. I call this unfair
Oh, and in case you're wondering: Yes, we've registered with Microsoft's Winqual site
where the crash reports are supposed to be sent to, and we established
"product mappings" there, and the whole process seems to work for XP clients
just fine.
I'm pretty sure that I'm just missing a couple of details with the new
APIs, or maybe I'm misinterpreting the feedback from the system.
I ran numerous experiments and umpteen variations, I've searched the web
high and low, read the docs, consulted newsgroups here
and there -
and now I'm running out of ideas. Any hints most welcome...
PS: I did indeed receive some hints. For updated WER code, along with
an explanation on why the above failed, see
Crashing with style on Vista, part II.
How can you tell that you're the control freak type of Windows programmer?
Easy: You feel that irresistible urge to install top-level exception handlers which
report application crashes to the end user and provide useful options on how to
proceed, such as to report the issue to the software vendor, save the currently
loaded data, inspect the issue in more detail, or call the police.
In fact, this is pretty much what
Windows Error Reporting
is all about, only that the crash reports are sent to Microsoft
first (to their Winqual site, that is), from where
ISVs can then download them for further analysis. Oh, and the other difference is that
Microsoft dropped the "call the police" feature in order to get Vista done in time.
One of the applications that I'm working on already had its own top-level crash handler
which performed some of the services also provided by Windows Error Reporting. It
was about time to investigate Microsoft's offerings in this area and see how they
can replace or augment the existing crash handler code.
The first option I looked at was the ReportFault
API. Microsoft's documentation says that the function is obsolete, and we should rather use a different
set of APIs collectively called the "WER functions". However, understanding them requires a lot more
brain calories than the trivial ReportFault call which you can simply drop into an
exception filter,
and you're done.
The required code is pretty trivial and looks roughly like this:
int filter_exception(EXCEPTION_POINTERS *exc_ptr)
{
EFaultRepRetVal repret = ReportFault(exc_ptr, 0);
switch (repret)
{
// decode return value...
//
}
return EXCEPTION_EXECUTE_HANDLER;
}
void main(void)
{
__try {
int *foo = (int *)0;
*foo = 42;
} __except(filter_exception(GetExceptionInformation())) {
_tprintf(__T("Nothing to see here, move on, process is still alive!\n"));
}
Sleep(5000);
}
Sequence of events:
A structured exception block is established using __try and __except.
Code provokes an access violation.
The exception filterfilter_exception is consulted by the exception
handling infrastructure to find out how to proceed with the exception.
The filter calls ReportFault to display the crash dialog as shown above,
and to give the user options to debug the problem, ignore it, or report
it to Microsoft.
After performing its menial reporting duties, the exception filter
returns EXCEPTION_EXECUTE_HANDLER to indicate that its associated
exception handler should be called.
That exception handler is, in fact, essentially the _tprintf statement
which spreads the good news about the process still being alive.
On XP, that is. On Vista, the _tprintf statement may actually never execute.
You'll still get a nice reporting dialog, such as the one in the screenshot
to the right, but when you click the "Close program" button, the calling process
will be terminated immediately, i.e. ReportFault never really
returns to the caller!
I debugged into ReportFault on my Vista machine and found that ReportFault
spawns off a process called wermgr.exe which performs the actual work.
My current hypothesis is that it is wermgr.exe which terminates
the calling process if the user chooses "Close program".
If you want to try it yourself, click here
to download the demo code. To compile, simply run it through cl.exe:
cl.exe reportfault.cpp
Now, can we complain about this, really? After all, you can't call it surprising
if a program closes after hitting the "Close program" button.
Still, the behavior differs from the old XP dialog - and it is inconsistent
even on Vista. What I just described is the behavior that I found with the default
error reporting settings in Vista. By default, Vista "checks for solutions
automatically" and doesn't ask the user what to do when a crash occurs.
This can be configured in the "Problem Reports and Solutions" control panel:
After changing the report settings as shown above ("Ask me") and then running the test
application again, the error reporting dialog looks like this:
When I click on "Close program" now, guess what happens - the process does not terminate,
and the _tprintf statement in my exception handler is executed, just like on XP!
So that "Close program" button can mean two different things on Vista...
It's not just this inconsistency which bugged me. I also don't like the idea of
letting the error reporting dialog pull the rug from under my feet. Sure, I'd like to
use the dialog's services, but when it returns, I want to make my own decisions
about how to proceed. For example, I could try and save the currently loaded data
in my application, or I could add my own special reporting. Or call the cops.
ReportFault won't let me do that on Vista. And so I set out to
burn those extra brain calories anyway and learn about the new WER APIs
which were introduced with Windows Vista.
And burn calories I did, oh yes. More on this hopefully soon.
Every now and then, some tool on my system runs berserk and starts to generate
files called nul. This is a clear indication that there's something going
wrong with output redirection in a script, but I still have to figure out
exactly what's going on. Until then, I need at least a way to get rid of those
files.
Yes, that's right, you cannot delete a file called nul that easily - neither
using Windows Explorer nor via the DOS prompt. nul is a very special filename for
Windows - it is an alias for the null device, i.e. the bit bucket where
all the redirected output goes, all those cries for help from software
which we are guilty of ignoring all the time.
UNC path notation to the rescue: To remove a file called nul in, say c:\temp,
you can use the DOS del command as follows:
del \\.\c:\temp\nul
Works great for me. But since I rarely use UNC syntax, I sometimes forget
how it looks like. Worse, the syntax requires to specify the full path
of the nul file, and I hate typing those long paths. So I came up
with the following naïve batch file which does the job for me.
It takes one argument which specifies the relative or absolute path of
the nul file. Examples:
rem remove nul file in current dir
delnul.bat nul
rem remove nul file in subdir
delnul.bat foo\nul
rem remove nul file in tempdir
delnul.bat c:\temp\nul
For the path completion magic, I'm using the
for
command which has so many options that my brain hurts whenever I read its
documentation. I'm pretty sure one could build a Turing-complete language using
just for...
@echo off
set fullpath=
for %%i IN (%1x) DO set fullpath=%%~di%%~pi
set filename=
for %%i IN (%1x) DO set filename=%%~ni
if not "x%filename%" == "xnulx" (echo Usage: %0 [somepath\]nul && goto :eof)
echo Deleting %fullpath%nul...
del \\.\%fullpath%nul
DelinvFile
takes this a lot further; it has a Windows UI and can delete many other
otherwise pretty sticky files - nul is not the only dangerous file
name; there's con, aux, prn and probably a couple of other
magic names which had a special meaning for DOS, and hence also
for Windows.
In the epic "dogfood" series, the hero
comes to realize he needs systematic tests
for clipboard code. On his quest, he briefly
gives in to the sweet song
of the Scripting Sirens, but escapes and
makes it to the safer shores of the Win32 API - but
only to realize that fate has even more trials in place for him.
When we left our hero the last time, he had just figured out that not
all Win32 handles are made alike. Data for most clipboard formats is held
in a global memory buffer (allocated via GlobalAlloc); GetClipboardData
returns a handle to the memory block, and all you need to do in order to
decode the data is to interpret the handle as a memory handle and then read
from that block of memory.
However, there are some formats which won't reveal their inner selfs that
easily, such as bitmaps. Hence, it's about time we form a circle,
take each other by the hands, and meditate over clipboard formats.
Microsoft lists clipboard formats here
and distinguishes the following main classes of clipboard formats:
Standard clipboard formats, i.e. predefined formats such as
CF_BITMAP, CF_TEXT, CF_ENHMETAFILE, CF_HDROP,
CF_PALETTE, CF_WAVE etc.
Registered clipboard formats, i.e. application-defined
formats which are registered at runtime. RTF is a
prominent example for such a format.
Private clipboard formats: Another special kind of
application-defined formats.
For the purpose of code which tries to retrieve arbitrary data from the
clipboard, however, it is more useful to use a different classification.
Memory-based clipboard formats
These are all the formats for which the handle returned from GetClipboardData
can be interpreted as a memory handle.
The text formats (CF_TEXT and cousins) are in this class, and probably
most application-defined formats ("registered formats"), although it
is entirely up to the application to decide on how the data in the clipboard
needs to be decoded. Other examples: CF_LOCALE, CF_WAVE, CF_TIFF.
These formats can be posted to the clipboard and read from there using
code similar to what I posted last time.
Handle-based clipboard formats
Examples for such formats:
CF_ENHMETAFILE, CF_DSPENHMETAFILE
CF_METAFILEPICT, CF_DSPMETAFILEPICT
CF_BITMAP, CF_DSPBITMAP
CF_PALETTE
I found that I had to provide format-specific code to interpret these
formats.
Metafiles
CF_ENHMETAFILE and CF_DSPENHMETAFILE data can be copied by interpreting
the clipboard handle as a metafile handle (HENHMETAFILE) and using
the CopyEnhMetaFile API in Win32 to directly create a file on the disk.
CF_METAFILEPICT and CF_DSPMETAFILEPICT differ slightly from this.
The clipboard handle is a memory handle to a METAFILEPICT
data structure. That structure has a member called hMF which is
the actual metafile handle; pass this handle to CopyMetaFile, and
you'll get a metafile on the disk.
Bitmaps
The clipboard handle really is a bitmap handle of type HBITMAP.
HBITMAPs refer to device-dependent bitmap data (DDB), which
first need to be converted into device-independent format (DIB);
then you can add a bitmap header and write the whole shebang to the disk
in a format which can be read as *.BMP by image viewers.
Palettes
The clipboard handle must be interpreted as a HPALETTE handle.
The GetPaletteEntries API can be used to retrieve the data behind
such a handle; then we can dump the palette entries in the
data structure to a file in any format we choose; for example,
a simple integer specifying the number of entries, followed by PALETTEENTRY structures.
Other formats
When dragging and dropping files (or copying them in Explorer), information about
these files is transmitted in CF_HDROP format. The handle returned by GetClipboardData
can be interpreted as an HDROP, which can be passed to DragQueryFile
to learn more about the files in the clipboard.
(I tried to come up with testcases for formats such as CF_PENDATA
and CF_DSPTEXT, but could not find any. If anyone comes across
these formats, please let me know.)
Finally: New toys!
With the above findings, I was ready to extend my very simplistic
original test code. The result: Two useful tools which can be used to copy data
from the clipboard into files (ClipboardToFile.exe), and to copy data from
files into the clipboard (FileToClipboard.exe). And I'm even sharing this
code .-)
Source code and executables can be downloaded
here - and here are
some hints on how to use the toolset.
ClipboardToFile
ClipboardToFile does exactly what the name hints at: It enumerates the
formats which are currently in the clipboard, and writes files containing
the clipboard data in that format.
So to create a set of test files,
simply run your favorite apps on any system and use them to copy data
in various formats to the clipboard. For example:
Run Paint, load a file and copy it to the clipboard
Run ClipboardToFile to save whatever Paint added to the
clipboard
Run Word, type some text, and copy it to the clipboard
Run ClipboardToFile again to get clipboard extracts
in various text formats.
An example of how to run ClipboardToFile:
ClipboardToFile c:\temp\clipbboarddata
With the above command line, ClipboardToFile will produce files
in the directory c:\temp\clipboarddata. Those files are named
after the clipboard format from which they were produced.
Typical names are "CF_TEXT", "CF_BITMAP", "CF_DIB" and so on.
Repeat the process with other apps on your system until you have a
library of clipboard data files which you can use for unit tests!
FileToClipboard
FileToClipboard is a command-line tool which
takes any file you throw at it, reads the file's contents and copies
them to the clipboard in (almost) any format you specify:
So the basic idea is to prepare a couple of test files (here: foo.bmp,
foo.wmf and foo.txt), dump them into your unit test directory, and use them
to prepare the clipboard. Then you run your application's "Edit/Paste"
functionality and verify that it works as expected. Since FileToClipboard
is a command-line utility, you can automate such tests easily; also,
the executable is very small and can be installed everywhere simply
by copying the exe file.
In the case of text and bitmap files, it is easy to see where you can get
sample test data. However, some formats are only used for clipboard transfer
and are never persisted to files. As an example, the CF_LOCALE
format indicates that locale data is in the clipboard. In the case of
CF_LOCALE, it would be easy to fudge a binary file: A single
integer is used to encode a locale ID. So you could create such a file
with a hex editor or by writing a one-liner C program or whatever, and
then feed it into FileToClipboard in CF_LOCALE mode.
However, there are many other formats which are not quite that simple.
Worse, any application out there can define its own undocumented clipboard
formats at any time. Fortunately, we already have a tool which fills this
gap: ClipboardToFile.
The end of the saga? Not quite!
These tools cover a wide variety of clipboard formats, including many
registered formats - most of them seem to be memory-based. For the most
part, I'm a happy camper now. I can automate all my tests, and move on
to greener pastures.
Well, I could, except that the whole Win32 approach which I took is
still fundamentally flawed, really.
When I fire up Paint, then select an image area and copy it to
the clipboard, ClipboardToFile will report the following:
ClipboardToFile - (C) 2006 Claus Brod, http://www.clausbrod.de
Clipboard format 49161 successfully written to DataObject.
Clipboard format 49163 successfully written to Embed Source.
Clipboard format 49156 successfully written to Native.
ERROR: Cannot write data to OwnerLink
Clipboard format 49166 successfully written to Object Descriptor.
Clipboard format 3 successfully written to CF_METAFILEPICT.
Clipboard format 8 successfully written to CF_DIB.
Clipboard format 49171 successfully written to Ole Private Data.
Clipboard format 14 successfully written to CF_ENHMETAFILE.
Clipboard format 2 successfully written to CF_BITMAP.
Clipboard format 17 successfully written to CF_DIBV5.
ERROR: Failure to enumerate clipboard and store all formats.
Apparently, there's a funny format on the clipboard called OwnerLink
which, for some reason, ClipboardToFile cannot read properly. When debugging
into this case, it turns out that GetClipboardData returns a null handle
for this format. Hmmm... what is this format used for? Why doesn't it contain
any data? Or are there ways to retrieve the data than
GetClipboardData?
And what are these other formats such as DataObject, Embed Source,
Native, Object Descriptor and Ole Private Data for?
Indeed, there is much more to the clipboard than was dreamt of in my philosophy.
More on this (hopefully) soon.
Last time around, I discussed a slightly
kludgy approach for automatic testing of clipboard code, which
was based on clipbrd.exe and some VBscript code.
That wasn't bad, but I wasn't really satisfied. After all,
the original goal was to write rock-solid unit tests for clipboard code.
I wanted a more reliable tool to copy data from and to the clipboard in arbitrary
formats. I needed more control. And, most important of all, I was in the mood for
reinventing wheels (really fancy ones, of course).
The key Win32 APIs for clipboard handling are
SetClipboardData and GetClipboardData. Their signatures are as follows:
So when you post to the clipboard, all you need to specify is a format and a memory
handle, as it seems! This looks so trivial that I had my strategy
laid out almost immediately: I would allocate a global memory block using GlobalAlloc.
Then I would read some clipboard data from a file into that block, and finally call
SetClipboardData - like in the following code:
HANDLE ReadFileToMemory(const TCHAR *filename)
{
FILE *f;
errno_t error = _tfopen_s(&f, filename, _T("rb"));
if (error) {
_ftprintf(stderr, _T("ERROR: Cannot open %s\n"), filename);
return 0;
}
// get size of file
fseek(f, 0, SEEK_END);
long size=ftell(f);
fseek(f, 0, SEEK_SET);
// allocate memory
HANDLE hMem = ::GlobalAlloc(GMEM_MOVEABLE, size);
if (!hMem) {
fclose(f);
return 0;
}
LPVOID mem = ::GlobalLock(hMem);
if (!mem) {
fclose(f);
::GlobalFree(hMem);
return 0;
}
// read the file into memory
size_t bytes_read = fread(mem, 1, size, f);
fclose(f);
::GlobalUnlock(mem);
if (bytes_read != size) {
::GlobalFree(hMem);
return 0;
}
return hMem;
}
bool FileToClipboard(TCHAR *filename, UINT clipid, HWND ownerWindow)
{
if (::OpenClipboard(ownerWindow)) {
HANDLE hClip = ReadFileToMemory(filename);
::EmptyClipboard();
HANDLE h = ::SetClipboardData(clipid, hClip);
::CloseClipboard();
::DestroyWindow(owner);
return true;
}
return false;
}
And the reverse code to get data from the clipboard and save it to
a file would be just as simple:
FILE *f = fopen(filename, "wb");
if (f) {
HANDLE hClip = GetClipboardData(clipID);
// clipID culled from looping over
// available formats using EnumClipboardFormats
void *pData = (void*)GlobalLock(hClip);
if (pData) {
SIZE_T sz = ::GlobalSize(pData);
if (sz) {
size_t written = fwrite(pData, 1, sz, f);
ret = (written == sz);
}
}
::GlobalUnlock(hClip);
fclose(f);
}
Piece of cake! Mission accomplished! I slapped the usual boilerplate code for a
console app onto the above, and ran my first successful tests: I could read
text from the clipboard, and post text to it just fine.
However, several formats stubbornly refused to cooperate. In particular, the really
useful stuff, like metafiles. Or bitmaps. What was going on?
When calling GetClipboardData for these formats, the code that
interprets the returned handle as a global memory handle flatly
falls on its face. It turns out that I had jumped to conclusions way
too early when I read the first few lines of the SetClipboardData
documentation - some of those clipboard handles are actually anything but
memory handles! Examples for such formats:
CF_ENHMETAFILE, CF_DSPENHMETAFILE
CF_METAFILEPICT, CF_DSPMETAFILEPICT
CF_BITMAP, CF_DSPBITMAP
CF_PALETTE
And then, of course, there are whole classes of clipboard formats
which I had not even explored yet, such as application-defined formats.
Next time: How I learnt to peacefully coexist with all the various
classes of clipboard formats.
Yesterday, I introduced the problem of
how to automatically test Windows clipboard code in applications.
The idea is to move from manual and error-prone clickety-click style
testing to an automatic process which produces reliable results.
Unbeknownst to many, Windows ships with a fairly interesting tool
called the ClipBook Viewer (clipbrd.exe), which monitors
what the clipboard contains, and will even display the formats
it knows about.
This is quite helpful while developing and debugging clipboard code.
However, ClipBook Viewer can even help with test automation since it
can save the current clipboard contents to *.CLP files and load them
back into the clipboard later.
Which, in fact, is almost all we need to thoroughly and reliably
test clipboard code: We run some apps which produce
a good variety of clipboard formats which our own application needs
to deal with. We select some data, copy them to the clipboard, then
save the clipboard contents as a *.CLP file from ClipBook Viewer.
Once we have created a reasonably-sized clipboard file library,
we run ClipBook viewer and load each one of those clipboard
files in turn. After loading, we switch to our own app,
paste the data and check whether the incoming data makes sense to us.
Not bad at all!
But alas, I could not find a way to automate
the ClipBook Viewer via the command-line or COM interfaces. If someone
knows about such interfaces, I'm certainly most interested to hear about
them.
Luck would have it that only recently, I blogged about poor man's automation via SendKeys. The idea is to write a small shell script
which runs the target application (here: clipbrd.exe), and then simulate how a user
presses keys to use the application.
clipbrd.exe can be started with the name of a *.CLP file in its
command line, and will then automatically load this file. However,
before it pushes the contents of the file to clipboard, it will
ask the user for confirmation in a message box. Well, in fact, first
it will try to establish NetDDE connections, and will usually waste
quite a bit of time for this. The following script tries to take this
into account:
Set WshShell = WScript.CreateObject("WScript.Shell")
WshShell.Run("clipbrd.exe c:\temp\clip.CLP")
WScript.Sleep 5000 ' Wait for "Clear clipboard (yes/no)?" message box
WshShell.SendKeys "{ENTER}"
Now we could add some more scripting glue code to control our own
application, have it execute its "Paste" functionality and verify
that the data arrives in all its expected glory.
The above code is not quite that useful if we need to run
a set of tests in a loop; the following modified version is
better suited for that purpose. It assumes that all *.CLP
files are stored in c:\temp\clipfiles.
Set WshShell = WScript.CreateObject("WScript.Shell")
WshShell.Run("clipbrd.exe")
WScript.Sleep 20000
startFolder="c:\temp\clipfiles"
set folder=CreateObject("Scripting.FileSystemObject").GetFolder(startFolder)foreach file in folder.files
WScript.Echo "Now testing " & file.Path
OpenClipFile(file.Path)
' Add here:
' - Activate application under test
' - Have it paste data from the clipboard
' - Verify that the data comes in as expected
next
' Close clipbrd.exe
WshShell.AppActivate("ClipBook Viewer")
WshShell.SendKeys "%F"
WScript.Sleep 1000
WshShell.SendKeys "x"
Sub OpenClipFile(filename)
WshShell.AppActivate("ClipBook Viewer")
WshShell.SendKeys "%W" ' ALT-W for Windows menu
WScript.Sleep 500
WshShell.SendKeys "1" ' Activate Clipboard window
WScript.Sleep 500
WshShell.SendKeys "%F" ' ALT-F for File menu
WScript.Sleep 1000
WshShell.SendKeys "O"
WScript.Sleep 1000
WshShell.SendKeys filenam9e
WScript.Sleep 1000
WshShell.SendKeys "{ENTER}"
WScript.Sleep 1000 ' Wait for "Clear clipboard (yes/no)?"
WshShell.SendKeys "{ENTER}"
EndSub
I'm sure a VBscript hacker could tidy this up considerably and use it
to form a complete test suite. However, while this approach finally gives
us some degree of automation, it is still lacking in several ways:
The format for the *.CLP file is undocumented, so
we cannot add clipboard data of our own, unless we first
copy it to the clipboard, then save it from there using
ClipBook Viewer.
Automation via sending keys is a very brittle approach.
For instance, the above code was written for the English version
of clipbrd.exe. The German or French or Lithuanian versions
of clipbrd.exe might have completely different keyboard
shortcuts.
I shudder when looking at those magic delay
time values which the code is ridden with - what if we
run on a really slow system? On a system which has even
more networking problems than the one which I tested the
code with?
Any process (or user) stealing the window focus while the test is running will break the test.
Simplicity often breeds success. The Windows clipboard is undoubtedly an example
for this. Even though fairly limited and brittle, it is probably
the most popular mechanism of data exchange in a computer user's
daily life - every time I copy and paste some text from here to there,
I'm using the clipboard.
(This is a tempting opportunity to gripe about clipboard inheritance.
In my time as a programmer, I have certainly found way more jaw-dropping
instances of boneheaded copy-paste programming than I'd ever wish for.
But then, considering all the stuff I've written and forgotten about, who
knows if I'm really in the right position to cast the first stone! And
today doesn't feel like soapbox day, anyway. No, today I'll try to be
constructive, just for a change!)
Pretty much every application under the sun supports the clipboard,
and if you want to write a new application, you'll also want to
be able to export application-specific data to the clipboard in some
popular formats, or to import data from Office, your favorite audio
ripper software, or simply from Notepad.
Clipboard code isn't too tricky to write. However, it isn't
all that obvious how you can test it effectively.
You can of course test clipboard functionality in your application
by mousing around: Run your app, select some data, hit CTRL-C to paste
data to the clipboard, hit CTRL-V to paste it back into your own
application again, select some other data, rinse and repeat. While this
roundtrip test certainly covers a lot of ground, it has two fundamental
weaknesses: First, it assumes that your application understands the same
clipboard input formats which it produces for output, which is often not the
case. Second, this approach only verifies that your "copy" code is just
as broken as your "paste" code, i.e. that they make the same
assumptions about the clipboard and the format of the data stored there.
If the transfer works as expected, it either means that both copy and
paste code are correct, or it means that both code areas have symmetric bugs!
So to fully test clipboard functionality in an application, you better try
to interact with other applications. After all, this is the whole
original purpose of the clipboard. If exchanging data with other apps works,
then you know that you interpret certain clipboard formats
the same way other applications do, and can claim with confidence that
your application is interoperable.
However, running other applications as part of clipboard unit tests
poses its own challenges: For example, the remote application might be difficult
to automate because it does not have an automation API. Also, to run the unit tests
on any given test system, you'd have to install the remote application
on that test system first. Not exactly a tempting thought if the Windows
Installer file for that application fills 100 MB, or if the installation
process requires you to enter license codes.
This is the kind of situation I found myself in recently. In the next few blog
entries, I'll discuss a few ideas on how to tackle this problem.
Let us assume that I'm a little backward and have a peculiar fondness for the
DOS command shell. Let us further assume that I also like blank characters in pathnames.
Let us conclude that therefore I'm hosed.
But maybe others out there are hosed, too. Blank characters in pathnames are not exactly my
exclusive fetish; others have joined in as well (C:\Program Files,
C:\Documents and Settings). And when using software, you might be running
cmd.exe without even knowing it. Many applications can run external helper
programs upon user request, be it through the UI or through the application's
macro language.
The test environment is a directory c:\temp\foo bar which contains
write.exe (copied from the Windows system directory) and two text files, one of
them with a blank in its filename.
Now we open a DOS shell:
C:\>dir c:\temp\foo bar
Volume in drive C is IBM_PRELOAD
Volume Serial Number is C081-0CE2
Directory of c:\temp
File Not Found
Directory of C:\
File Not Found
C:\>dir "c:\temp\foo bar"
Volume in drive C is IBM_PRELOAD
Volume Serial Number is C081-0CE2
Directory of c:\temp\foo bar
03/18/2006 03:08 PM <DIR> .
03/18/2006 03:08 PM <DIR> ..
01/24/2006 11:19 PM 1,516 foo bar.txt
01/24/2006 11:19 PM 1,516 foo.txt
03/17/2006 09:44 AM 5,632 write.exe
3 File(s) 8,664 bytes
2 Dir(s) 17,448,394,752 bytes free
Note that we had to quote the pathname to make the DIR command work.
Nothing unusual here; quoting is a fact of life for anyone out there
who ever used a DOS or UNIX shell.
Trying to start write.exe by entering c:\temp\foo bar\write.exe in the
DOS shell fails; again, we need to quote:
C:\>"c:\temp\foo bar\write.exe"
And if we want to load foo bar.txt into the editor, we need to quote
the filename as well:
Still no surprises here.
But let's suppose we want to run an arbitrary command from our application
rather than from the command prompt. The C runtime library provides the
system()
function for this purpose. It is well-known that under the hood
system actually runs cmd.exe to do its job.
When running this code, it reports that system() returned 0, and write.exe
never starts, even though we quoted both the name of the executable and
the text file name.
What's going on here? system() internally runs cmd.exe like this:
Try entering the above in the command prompt: No editor to be seen anywhere!
So when we run cmd.exe programmatically, apparently it parses its input
differently than when we use it in an interactive fashion.
I remember this problem drove me the up the freakin' wall when I first encountered
it roughly two years ago. With a lot of experimentation, I found the right
magic incantation:
Note that I quoted the whole command string another time! Now the executable
actually starts. Let's verify this in the command prompt window: Yes, something
like cmd.exe /c ""c:\temp\foo bar\write.exe" "c:\temp\foo bar\foo bar.txt""
does what we want.
I was reminded of this weird behavior when John Scheffel, long-time user of our flagship
product OneSpace Designer Modeling and maintainer of the international
CoCreate user forum, reported funny
quoting problems when trying to run executables from our app's built-in Lisp interpreter.
John also found the solution and documented it in a Lisp version.
Our Lisp implementation provides a function called sd-sys-exec, and you need to
invoke it thusly:
Kudos to John for figuring out the Lisp solution. Let's try to decipher all those quotes
and backslashes in the format statement.
Originally, I modified his solution slightly
by using ~S instead of ~A in the format call and thereby saving one level
of explicit quoting in the code:
(format nil "\"~S ~S\"" exe path))
This is much easier on the eyes, yet I overlooked that the ~S format specifier
not only produces enclosing quotes, but also escapes any backslash characters
in the argument that it processes. So if path contains a backslash (not quite
unlikely on a Windows machine), the backslash will be doubled. This works
surprisingly well for some time, until you hit a UNC path which already starts
with two backslashes. As an example, \\backslash\lashes\back turns into
\\\\backslash\\lashes\\back, which no DOS shell will be able to grok anymore.
John spotted this issue as well. Maybe he should be writing these blog entries,
don't you think?
From those Lisp subtleties back to the original problem:
I never quite understood why the extra level of quoting is necessary for
cmd.exe, but apparently, others have been in the same mess before. For example,
check out
this XEmacs code
to see how complex correct quoting can be. See also an online version of
the help pages for CMD.EXE
for more information on the involved quoting heuristics applied by the shell.
PS: A very similar situation occurs in OneSpace Designer Drafting as well
(which is our 2D CAD application). To start an executable write.exe in a directory
c:\temp\foo bar and have it open the text file c:\temp\foo bar\foo bar.txt,
you'll need macro code like this:
LET Cmd '"C:\temp\foo bar\write.exe"'
LET File '"C:\temp\foo bar\foo bar.txt"'
LET Fullcmd (Cmd + " " + File)
LET Fullcmd ('"' + Fullcmd + '"') { This is the important line }
RUN Fullcmd
The company I work for is called CoCreate.
The name was chosen because the company's mission is all about collaboratively
creating things. That's all nice and dandy, but I guess the team who
picked the name didn't include a programmer, and so they overlooked
something pretty obvious which causes mild confusion every now and then.
Most programmers, when confronted with our company name, think of COM.
After all, one of the most important functions in all of the COM libraries
prominently displays our company name:
CoCreateInstance.
Now, if a programmer thinks about COM (and hence software)
when she hears about us, that's probably fine, because, after all,
we're in the business to make and sell software.
However, customers are not necessarily that technology-savvy,
nor should they have to be.
A while ago, a customer complained that our software was sloppy because
it wouldn't uninstall itself properly and leave behind traces in the system.
Our installer/uninstaller tests didn't seem to confirm that.
So we asked the customer why he thought we were messing with his
system. "Well", he said, "even after I uninstall your stuff,
I still get those CoCreate error messages."
The customer sent a screenshot - it showed a message box,
displayed by an application which shall remain unnamed, saying
that "CoCreateInstance failed" and mumbling some COM error
codes!
It took us a while to explain to the customer that no, we
did not install this CoCreateInstance thing on the system,
and that it is a system function, and if we actually tried to
uninstall it along with our application as he requested (kind of),
he wouldn't be terribly happy with his system any longer, and
that the other app was actually trying to report to the customer that
it had found a problem with its COM registration, and that this should
be looked after, not our uninstaller. Phew.
Now if only we had the time-warping powers of the publishers
of "The Hitchhiker's Guide To The Galaxy", we'd send our
company marketing materials back into time before Microsoft invented
COM, and then sue the living daylights out of them. Well, if we were
evil, that is
My memory took a little longer to swap back in, but while writing
the above, it dawned on me that this incident wasn't the only one of
its kind: Somebody had upgraded to a new PC and installed all applications
except CoCreate's. Then, while syncing to his Palm Pilot, he got
an "OLE CoCreateInstance Failed" error message, and started to
search high and low on his shiny new PC for traces of CoCreate
applications or components.
Puzzled, he posted to a newsgroup, and I replied with tongue-in-cheek:
Let me explain: When we kicked off CoCreate as a company,
we sat together and thought about awareness strategies for the new
company. So we called our buddies from Microsoft and asked them to
name some API functions after us, and in exchange we would port our
software to Windows NT. Neat scheme, and as you discovered on your
system, the cooperation between the two companies worked just fine.
[... skipping explanation of the technical issue and hints on
how to fix registry issue on the system ...]
The next step for CoCreate towards world domination will be to
talk to some of our buddies in, say, Portugal, and offer them to
develop a Portugese version of our application if they name
their country after us.
Would I get away with a response like this if I was a support engineer?
Maybe not. One more thing to like about being a software developer
(Everybody in the newsgroup had a good chuckle back then.)
Chances are that - by looking at my earlier blog entry on batch files -
you think I'm a DOS lamer. Nothing could be further from the truth, because I'm really a UNIX
lamer. (OK, so what really shaped my thinking even before that was the phrase
"38911 bytes free".
But I digress.)
So I still write little one-off scripts using bash,
typically in a Cygwin environment. One of these scripts recently
ran berserk, reporting lots of errors like this one:
./foo.sh: line 42: /usr/bin/find: Resource temporarily unavailable
I couldn't really figure out what resources the shell was talking about. Memory? Certainly
not - the test system had ample memory, and was hardly using any. Files or disk space?
Nope, lots of free disk space everywhere, and noone was fighting over access to
shared files or so. Too many processes? Process Explorer wouldn't think so. Hmmm...
This test script then revealed the truth:
typeset -i limit=2200
# Create a file with 2200 environment variable definitions
rm -f exportlist
typeset -i i=0
while [ $i -lt $limit ]
do
echo "export FOO$i=$i" >>exportlist
let i=i+1
done
# Import the environment definitions
source ./exportlist
# Are we still alive?
env | wc
find . -name exportlist
Run this script and watch it balk miserably about unavailable resources. So it's the
environment which filled up and caused my scripts to fail! And indeed,
the system for which the problem was originally reported uses a lot of environment
variables for some reason, and this broke my script.
Once I had found out that much, it was easy to google for the right search terms
and learn more:
In this Cygwin mailing list discussion,
Mike Sieweke explains that we are actually suffering from a Windows limitation here -
apparently, the environment cannot grow larger than 32K. Christopher Faylor,
chief maintainer of Cygwin, even
recommends a workaround,
but I haven't tested that one yet; instead, I helped to clean up the polluted
environment on the affected PC, and henceforth, no waldsterben anymore on that
system.
32K - this would have filled almost all of those 38911 memory bytes assigned
for BASIC programs on my good ol' 64...
Movie podcasts are the next big thing after blogging and podcasting.
For the time being, I'll stick to my blog, thanks very much for asking, but
I do listen to a lot of podcasts while commuting or exercising. Occasionally,
I also watch some of the Channel 9 videos where Microsoft engineers
and employees talk about their work. No matter what you think about the company
in general, everybody knows that Microsoft hires smart people, so
there is a lot to learn from them.
Many of those videos contain demos or at least feature casually-dressed geeks
scribbling frantically on whiteboards, which, of course, is a must-see (ahem).
But quite a few videos could be enjoyed almost just
as well in pure audio format. Unfortunately, most of the Channel 9 content is in video
format (*.wmv) only, which will neither fit nor play on my 512 MB MP3 player.
I'm pretty much a newbie in all things video, and so I was glad that
Minh Truong suggested a way to convert WMV to WMA
using Windows Media Encoder.
This actually works fine, but it's a lot of settings to remember (see the screenshots below), and
it produces WMA instead of MP3 or OGG format which I'd prefer.
Fortunately, I found that Windows Media Encoder actually ships with a script called
WMCmd.vbs
which takes a gazillion parameters and automates the conversion process!
And indeed, the following trivial command line produces a WMA audio file
from a WMV video:
cd c:\Program Files\Windows Media Components\Encoder
cscript.exe WMCmd.vbs -input c:\temp\foo.wmv -output c:\temp\foo.wma -audioonly
There are a number of options to control the quality and encoding of the output which I haven't explored at all.
So now I only need to find a reasonable WMA-to-MP3 converter which can be used
from the command line. batchenc
and dBpowerAMP Music Converter look like they could help with that part of the job, but I'm not sure. Sounds like I have a plan for next weekend
So here I confess, not without a certain sense of pride: Sometimes I boldly go
where few programmers like to go - and then I write a few lines in DOS batch language.
Most of the time, it's not as bad as many people think. Its bad reputation
mostly stems from the days of DOS and Windows 95, but since the
advent of Windows NT, the command processor has learnt quite a few new tricks.
While its syntax remains absurd enough to drive some programmers out of their
profession, you can now actually accomplish most typical scripting tasks with it.
In particular, the for statement
is quite powerful.
Anyway - a while ago, one of my batchfiles started to act up. The error message
was "The system cannot find the batch label specified - copyfile".
The batch file in question had a structure like this:
@echo off
rem copy all pdb files in the current directory into a backup directory
set pdbdir=c:\temp\pdbfiles
for /r %%c in (*.pdb) do call :copyfile "%%c" %pdbdir%
if errorlevel 1 echo Error occurred while copying pdb files
echo All pdb files copied.
goto :eof
rem copyfile subroutine
:copyfile
echo Copying %1 to %2...
copy /Y %1 %2 >nul
goto :eof
I know what you're thinking - no, this is not the problem. This is how you write
subroutines in DOS batch files. Seriously. And yes, the above script can
of course be replaced by a single copy command. The original script couldn't;
it performed a few extra checks for each and every file to be copied in
the :copyfile subroutine, but it also contained a lot of extra fluff which
distracts from the actual problem, so what you're seeing here is a stripped-down
version.
The error message complained that the label copyfile could not be found. Funny,
because the label is of course there. (The leading colon identifies it as a label.)
And in fact, the very same subroutine could be called just fine from elsewhere in
the same batch file!
For debugging, I removed the @echo off statement so that the command processor
would log all commands it executes; this usually helps to find most batch file
problems. But not this one - removing the echo "fixed" the bug. I added the
statement again - now I got the error again. Removed the echo statement - all is
fine.
Oh great. It's a Heisenbug.
So I added the echo statement back in again and stared at the script hoping to find the
problem by the old-fashioned method of "flash of inspiration".
No inspiration in sight, though. Not knowing what to do, I added a few empty
lines between the for and the if errorlevel statement and ran the script
again - no error message! Many attempts later, I concluded that it's
the sheer length of the script file which made the difference between
smooth sailing and desperation. By the way, the above demo script works
just fine, of course, because I stripped it down for publication.
Google confirmed my suspicion: Apparently, there are cases where
labels cannot be found even though they are most certainly in the batch file.
Maybe the length of the label names matters -
Microsoft Knowledge Base Article 63071
suggests that only the first eight characters of the label are significant.
However, copyfile has exactly eight characters!
I still haven't solved this puzzle. If you're a seasoned batch file
programmer sent to this place by Google and can shed some light on this,
I could finally trust that script again...
-- ClausBrod - 27 Jan 2006
"How bad is the Windows command line really?"
-- ClausBrod - 01 Apr 2016
Thanks a lot, Reinder!
-- ClausBrod - 05 Apr 2015
From http://help.wugnet.com/windows/system-find-batch-label-ftopict615555.html, I tentatively conclude that
you need two preconditions for this to hit you:
the batch file must not use CRLF line endings
the label you jump to must span a block boundary
As to your remark "And in fact, the very same subroutine could be called just fine from elsewhere in the same batch file": in my experience, the subroutine gets called just fine when you get this error.
Regards,
Reinder
-- Reinder - 20 Jun 2008
A few days ago, I dissed good ol' aCC on the HP-UX platform,
but for political correctness, here's an amusing quirk in Microsoft's compiler as well.
Consider the following code:
typedef struct foobar gazonk;
struct gazonk;
The C++ compiler which ships with VS.NET 2003 is quite impressed with
those two lines:
fatal error C1001: INTERNAL COMPILER ERROR
(compiler file 'msc1.cpp', line 2701)
Please choose the Technical Support command on the Visual C++
Help menu, or open the Technical Support help file for more information
What the compiler really wants to tell me is that it does not want me to redefine
gazonk. The C++ compiler in VS 2005 gets this right.
If you refer back to the previous blog entry, you'll find
that it took me only one line to crash aCC on HP-UX. It took two lines in the above
example to crash Microsoft's compiler. Hence, I conclude that their compiler
is only half as bad as the HP-UX compiler.
If you want to argue with my reasoning, let me tell you that out there in the wild,
I have rarely seen a platform-vs-platform discussion based on
facts which were any better than that. Ahem...
I've been meaning to install and tame Minimo
on my PDA for some time. Finally, I can tap my way through my first blog posting from this cute little browser. I had to switch off the SSR (Small Screen Rendering) feature, but now, at last, my PocketPC displays web pages in a manner that is suitable for human consumption. I never understood why IE, after quite a number of releases of the Windows Mobile/CE platform, still sucks that badly as a browser.
Typing with the stylus is a pain in the youknowwhere, so I probably won't be blogging
from my PDA that often But still, I love the Minimo browser, even though it is in its
early infancy. It seems to do a much better job at displaying most web sites than IE;
it groks the CSS-based layout of my own web site; it makes use of the VGA screen on
my PDA; and I can now even use TWiki's direct editing facilities from my organizer.
This project really makes me wonder what it would take to start developing for the PocketPC platform...
And then there is Rory Blyth's
Tiny Things podcast which is also
wetting my appetite for those little gadgets. But even if you couldn't care less
about mobile platforms, I hereby guarantee that listening to the intro section
of each of the episodes will give you a good chuckle. Well, let's say that I'll guarantee
a chuckle only if you promise that you won't sue me for making such potentially
groundless claims. You'll know what I mean once you've listened in to Rory's show.
Man, I long for a real keyboard now.
-- ClausBrod - 21 Jan 2006
Now this was funny because that code had not changed in ages, and so far had compiled
just fine. At first, I couldn't explain what was going on. Hmmmm... note the
funny u-umlauts in the comment. Why would someone use a comment like that?
Well, the above code was inherited from source code originally written on an HP-UX
system. For long years, the default character encoding on HP-UX systems has been
Roman8. In that encoding, the
above comment looked like this:
foobar(text); //■■■
(If your browser cannot interpret the Unicode codepoint U+25A0, it represents a
filled box.)
So the original programmer used this special character for graphically highlighting the
line. In Roman8, the filled box has a character code of 0xFC. On a Windows system
in the US or Europe, which defaults to displaying characters according to ISO8859-1 (aka Latin1), 0xFC will
be interpreted as the German u-umlaut ü.
So far, so good, but why the compilation errors?
On the affected system, I ran the code through the C preprocessor (cpp), and ended
up with this preprocessed version:
if (!strncmp(text, "FOO", 3))
{
foobar(text);
else
{
gazonk();
}
Wow - the preprocessor threw away the comment, as expected, but also the closing
parenthesis } on the next line! Hence, the parentheses in the code are now
unbalanced, which the compiler complains bitterly about.
But why would the preprocessor misbehave so badly on this system? Shortly before,
I had installed the Windows multi-language UI pack
(MUI) to run tests in Japanese; because of that, the system defaulted to a Japanese
locale. In the default Japanese locale, Windows assumes that all strings are
encoding according to the Shift-JIS
standard, which is a multi-byte character set (MBCS).
Shift-JIS tries to tackle the problem of representing the several thousands of
Japanese characters. The code positions 0-127 are identical with US ASCII.
In the range from 128-255, some byte values indicate "first byte of a
two-byte sequence" - and 0xFC is indeed one of those indicator bytes.
So the preprocessor reads the line until it finds the // comment indicators.
The preprocessor changes into "comment mode" and reads all characters until
the end of the line, only to discard them. (The compiler doesn't care about
the comments, so why bother it with them?)
Now the preprocessor finds the first 0xFC character, and - according to
the active Japanese locale - assumes that it is
the first byte of a two-byte character. Hence, it reads the next byte (also 0xFC,
the second "box"), converts the sequence 0xFC 0xFC into a
Japanese Kanji character, and throws that character away.
Then the next byte is read, which again is 0xFC
(the third "box" in the comment), and so the preprocessor will slurp
another byte, interpreting it as the second byte of a two-byte character.
But the next byte in the file after the third "box" is a 0x0A, i.e. the
line-feed character which indicates the end of the line. The preprocessor
reads that byte, forms a two-byte character from it and its predecessor (0xFC),
discards the character - and misses the end of the line.
The preprocessor doesn't have a choice now but to continue searching for the next LF,
which it finds in the next line, but only after the closing parenthesis. Which is
why that closing parenthesis never makes it to the compiler. Hocus, pocus, leavenotracus.
So special characters in comments are not a particularly brilliant idea; not just because
they might be misinterpreted (in our case, displayed as ü instead of
the originally intended box), but because they can actually cause the compiler to
fail.
If you think this could only happen in a Roman8 context, consider this variation
of the original code:
Here, we're simply using the German translation for menu in the comment;
we're not even trying to be "graphical" and draw boxes in our comments.
But even this is enough to cause the same compilation issue as with my original example.
Now, in my particular case, the affected code isn't likely to be compiled in Japan or China
anytime soon, except in that non-standard situation when I performed my experiments with
the MUI pack and a Japanese UI. But what if your next open-source
project attracts hundreds of volunteers around the world who want to refine the code, and
some of those volunteers happen to be from Japan? If you're trying to be too clever
(or too patriotic) in your comments, they might have to spend more time on finding
out why the code won't compile than on adding new features to your code.
Yesterday, I explained how easy it is to inadvertedly
load the same executable twice into the same process address space - you simply
run it using its short DOS-ish filename (like Sample~1.exe)
instead of its original long filename (such as SampleApplication.exe).
For details, please consult the original blog entry.
I mentioned that one fine day I might report how exactly this happened
to us, i.e. why in the world our app was started using its short filename.
Seems like today is such a fine day
Said application registered itself as a COM server, and it does so using
the services of the ATL Registrar.
Upon calling RegisterServer, the registrar will kindly create all the required
registry entries for a COM server, including the LocalServer entry which
contains the path and filename of the server. Internally, this will call the
following code in atlbase.h:
inline HRESULT WINAPI CComModule::UpdateRegistryFromResourceS(UINT nResID,
BOOL bRegister, struct _ATL_REGMAP_ENTRY* pMapEntries)
{
USES_CONVERSION;
ATL::CRegObject ro;
TCHAR szModule[_MAX_PATH];
GetModuleFileName(_pModule->GetModuleInstance(), szModule, _MAX_PATH);
// Convert to short path to work around bug in NT4's CreateProcess
TCHAR szModuleShort[_MAX_PATH];
GetShortPathName(szModule, szModuleShort, _MAX_PATH);
LPOLESTR pszModule = T2OLE(szModuleShort);
...
Aha! So ATL deliberately converts the module name (something like SampleApplication.exe)
into its short-name equivalent (Sample~1.exe) to work around an issue in the
CreateProcess implementation of Windows NT.
MSKB:179690
describes this problem: CreateProcess could not always handle blanks in pathnames
correctly, and so the ATL designers had to convert the path into its short-path
version which converts everything into an 8+3 filename and hence guarantees that
the filename contains no blanks.
Adding insult to injury, MSKB:201318
shows that this NT-specific bug fix in ATL has a bug itself... and, of course, our problem is,
in fact, caused by yet another bug in the bug fix (see earlier blog entry).
For my application, the first workaround was to use a modified version of atlbase.h which checks the
OS version; if it is Windows 2000 or later, no short-path conversion
takes place. Under Windows NT, however, we're caught in a pickle: Either we
use the original ATL version of the registration code and thus map the executable
twice into the address space, or we apply the same fix as for Windows 2000,
and will suffer from the bug in CreateProcess if the application is installed
in a path which has blanks in the pathname.
In my case, this was not a showstopper issue because the application is targeting Windows 2000 and XP
only, so I simply left it at that.
Another approach is to use the AddReplacement
and ClearReplacements APIs of the ATL registrar to set our own conversion rules
for the module name and thereby override ATL's own rules for the module name:
Windows, even in its latest incarnations, still exhibits quite a bit of quirky behavior
which is due to its DOS roots, or at least due to the attempt to remain compatible
with code which was created for DOS. Most of the time, I am not even surprised anymore
when I come across 16-bit limitations or similar reminiscences of the past.
But sometimes, I only become aware of them when my code crashes.
This happened some time ago with an application I am working on. When I started
the app in a certain way, it would simply crash very early during startup. It took
a while to break this down into the following trivial code example which consists
of a main executable and a DLL which is loaded into the executable via
LoadLibrary,
i.e. dynamically. Here is the code for the main executable, SampleApp.cpp:
#include <stdio.h>
#include <conio.h>
#include <windows.h>
#include <psapi.h>
static void EnumModules(const char *msg)
{
printf("\n==========================================================\n");
printf("List of modules in the current process %s:\n", msg);
HMODULE hMods[1024];
DWORD cbNeeded;
HANDLE hProcess = GetCurrentProcess();
// inquire modules loaded into process
if( EnumProcessModules(hProcess, hMods, sizeof(hMods), &cbNeeded)) {
// print name and handle for each module
for ( unsigned int i = 0; i < (cbNeeded/sizeof(HMODULE)); i++ ) {
char szModName[MAX_PATH];
if ( GetModuleFileNameEx( hProcess, hMods[i], szModName, sizeof(szModName))) {
printf(" %s (0x%08X)\n", szModName, hMods[i] );
}
}
}
CloseHandle( hProcess );
}
extern "C" __declspec(dllexport) int functionInExe(void)
{
printf("Now in functionInExe()\n");
return 42;
}
int main(void)
{
EnumModules("before loading DLL");
HMODULE hmod = LoadLibrary("SampleDLL.dll");
EnumModules("after loading DLL");
printf("\nPress key to exit.\n");
_getch();
return 0;
}
This code loads a DLL called SampleDLL.dll. Before and after loading
the DLL, it enumerates the modules which are currently loaded into the process;
this is only to demonstrate the effect which led to the crash in the other app I
was working on.
SampleDLL.dll is built from this code (SampleDLL.cpp):
extern "C" __declspec(dllimport) int functionInExe(void);
extern "C" __declspec(dllexport) void gazonk(void)
{
int i = functionInExe();
}
The main executable exports a function called functionInExe, and the DLL
calls this function, and so it has an explicit reference to the main executable
which the linker needs to resolve. This is an important piece of the puzzle.
And here is a simple makefile which shows how to build the two modules:
Let's assume that the above files (SampleApp.cpp, SampleDLL.cpp and makefile)
are all in a directory c:\temp\dupemod, and that we built the code by
running nmake in that directory. Now let's run the code as shown in
the screenshots below.
Long filename
Short filename
Take a close look at the command shell window on the right: After loading the DLL,
the process maps both c:\temp\dupemod\Sample~1.exe and
c:\temp\dupemod\SampleApplication.exe into its address space.
Both refer, of course, to the same file, which means that we have loaded
the executable twice!
This happens only if we run the executable using its 8+3 DOS name, i.e. Sample~1.exe.
When run with a long filename, everything works as expected. So when we load
SampleDLL.dll, the OS loader tries to resolve the references which this
DLL makes to other modules. One of those modules is SampleApplication.exe.
The OS loader should be able to map this reference to the instance of the
executable which is already mapped into the address space. However, it seems
that the OS loader cannot figure out that Sample~1.exe and SampleApplication.exe are
actually the same file, and therefore loads another instance of the executable!
BTW, this happens both on Windows 2000 and Windows XP systems.
In this trivial example, the only damage done is probably just that the main
executable consumes twice the virtual address space. In
a large application, the consequences can be more severe, and in our case
they were.
Microsoft also documents some effects of this issue in Knowledge Base articles,
for example KB218475
and KB193513.
The only workarounds I see are:
Rename the executable to that it uses a name which
fits into the 8+3 format.
Make sure that nobody ever runs the executable
using its short name.
We basically went with the latter approach - by making sure that end users
always run the application by double-clicking shortcuts which contain
the full executable name, and by fixing an interesting related bug in ATL
which, hopefully, I may have the time and the nerves to describe in
more detail one fine day...