April 1999
Code for this article: April99Win32.exe (8KB)
Jeffrey Richter wrote Advanced Windows, Third Edition (Microsoft Press, 1998) and Windows 95: A Developer's Guide (M&T Books, 1995). Jeff is a consultant and teaches Win32 programming courses (www.solsem.com). He can be reached at www.JeffreyRichter.com.
Q How can I make a CD-ROM load an HTML file automatically when the disk is inserted into the drive? So many people,
I lost count
A This is by far the most popular question I've ever been asked. This month I'll offer three slightly different solutions, each with some pros and cons. But before I dive in, here's some quick background information. The mechanism that Windows® uses to automatically execute a process when a CD-ROM disk is inserted in the drive is called AutoPlay. Simply stated, when a CD-ROM disk is inserted in a drive, the system broadcasts a WM_DEVICECHANGE notification. If Explorer.exe is running, it examines the newly inserted disk, looking for a file called AUTORUN.INF. This file is a simple ASCII text file (often created with Notepad) that contains statements telling Explorer what icon to show for the drive when it appears in the My Computer folder. The statements also tell Explorer what menu items should appear in the drive's context menu. Lastly, if the AUTORUN.INF file contains an "OPEN=" statement (and most do), then Explorer peels off the string to the right of the equal sign and passes this string to the Windows CreateProcess function. I'm not going to explain everything that you can do in this file because I've already discussed it in previous MSJ articles. So here's a simple AUTORUN.INF file: |
[AutoRun]
ICON=SomeProcess.exe
OPEN=SomeProcess.exe
This file tells the Explorer to show SomeProcess's icon for the CD-ROM disk in the My Computer folder. When the disk is inserted (or when the user attempts to open the disk), the Explorer grabs the OPEN statement's command line and passes it to CreateProcess.To make the Explorer display an HTML file, I first tried to create an AUTORUN.INF file as follows:
[AutoRun]
ICON=SomeProcess.exe
OPEN=Welcome.html
I had hoped that Explorer would see that HTML files were associated with the user's default browser and that Explorer would run AUTORUN.INF, passing it the path of my HTML file. The beauty of this is that the system would automatically determine which browser the user has set up as their default. Unfortunately, this AUTORUN.INF file does not work. Explorer calls CreateProcess internally, which instructs the system to run an executable file named Welcome.html. Since this file is not an executable, CreateProcess fails and nothing runs. Had the Explorer team at Microsoft decided to call ShellExecute instead of CreateProcess, my AUTORUN.INF file would have worked just fine. ShellExecute looks up file associations that are in the registry, constructs a proper command line automatically, and passes the resulting command line to CreateProcess. So ShellExecute would have seen that files ending with an HTML extension are associated with IExplore.exe (or whatever) and called CreateProcess, passing it something like "IExplore.exe Welcome.html". This would have been an elegant solution.
OK, so my first attempt didn't work. What next? Well, the next step seems obvious: write a super simple application that calls ShellExecute. This application will be placed on the CD-ROM disk with the Welcome.html file. Figure 1 shows the code for this simple application. This application takes its command-line parameter and passes it off to ShellExecute to do the work. To prepare the CD-ROM disk for shipping, I create a Welcome.html file that has what I want the user to see when they insert the disk. Then I create the following AUTORUN.INF file:
[AutoRun]
ICON=ShellExecute.exe
OPEN=ShellExecute Welcome.html
This is the first solution to the problem. But I wondered if there was a way to accomplish the same thing without having to write my own code. Basically, the problem now is how to run some application that calls ShellExecute, passing it a command-line parameter that I specify.
It dawned on me that the command shell has a START command that does just this. So I created the following AUTORUN.INF file:
[AutoRun]
ICON=SomeIcon.ico
OPEN=CMD.EXE /c "start Welcome.html"
This worked great, but it has two problems. First, when the command shell executes, the system creates a console window that gets destroyed almost immediately. This causes some unpleasant flashing, and I'd prefer my CD-ROMs to have a more polished look. Second, CMD.EXE exists on Windows NT®, but does not exist on Windows 9x.Windows 9x, of course, uses COMMAND.COM as its command shell processor. I fixed this problem by changing my AUTORUN.INF file to read as follows:
[AutoRun]
ICON=SomeIcon.ico
OPEN=ShellExecute.bat Welcome.html
When "ShellExecute.bat" is passed to CreateProcess, it knows to spawn the correct command shell for the system you're on (CMD.EXE or COMMAND.COM). By the way, here is my ShellExecute.bat file:
Start %1
Pretty simple, huh? The Start command causes the command processor to call ShellExecute internally. Even though I got this technique working on both OS platforms, I wasn't happy because of the flashing console window. What I really need is a GUI application that calls ShellExecute. Incidentally, this is why my code in Figure 1 uses a WinMain function instead of a main function; this application should be a GUI app. After racking my brain for a while, I remembered that Windows now ships with a scripting host. In fact, there are two scripting host processes: a GUI (WSCRIPT.EXE) and a CUI (CSCRIPT.EXE). I, of course, wanted to use the GUI host to avoid the unpleasant flashing.
The following JScript®
// Create an instance of the scripting Shell Object
WshShell = WScript.CreateObject("WScript.Shell");
// Have the Shell Object call ShellExecute on our HTML // file
WshShell.Run("Welcome.html", 1, 0);
// Destroy the Shell Object
WScript.DisconnectObject(WshShell);
simply takes its command-line arguments and passes them to the Shell object's Run method. This method, in turn, calls the Windows ShellExecute function. Voilà! So now my CD-ROM has a Welcome.html file, a ShellExecute.js file, and the following AUTORUN.INF file:
[AutoRun]
ICON=SomeIcon.ico
OPEN=Wscript ShellExecute.js //B //nologo Welcome.html
This is my favorite solution because it doesn't involve writing, compiling, and linking any code. Therefore, it was the easiest to test and modify. So what is the downside of using the scripting host? Some Windows platforms don't have the scripting host installed. For example, Windows 95 and Windows NT 4.0 didn't ship with the Wscript.exe application, so if your users are running on these platforms, your HTML file won't be displayed. For these platforms, you'll need to use one of the other two methods. Of course, Windows 98 ships with the scripting host (and Windows 2000 is expected to), so if your application requires these platforms (or later), feel free to use the scripting host method.
While I'm on the subject, let me tell you how I tested all these mechanisms. First, I did not burn a new CD-ROM disk to try out each idea I had. Instead, there is a way to test AUTORUN.INF files using a floppy disk or a hard drive directory. To do this you must edit the NoDriveTypeAutoRun value in the following registry subkey:
HKEY_CURRENT_USER\Software\Microsoft\Windows\
CurrentVersion\Policies\Explorer\
This value indicates which drives should have AutoPlay enabled. When Explorer receives the WM_DEVICECHANGE message, it first checks this registry value to see if it should parse the disk's AUTORUN.INF file at all. Knowledge Base article Q136214 explains how to set this value correctly for testing. After you change this registry value, don't forget to shut down the Explorer and restart it so that the change takes effect. The easy way to do this is to choose Shutdown from the Start menu. But when the Shutdown confirmation dialog box appears, don't just click a button. Instead, hold down the Control+Alt+Shift keys and click No. This tells the Explorer to terminate itself without shutting down the whole machine.
Now you can start another instance of Explorer to do your testing. On Windows 2000, press Control+Alt+Del and select the Task Manager. When the Task Manager appears, choose the New Task (Run…) option from the File menu and enter "Explorer." On Windows 9x, start a command shell before you terminate the Explorer. After the Explorer terminates, go to the command shell and run Explorer. You can do this on Windows NT too.
Now that a new instance of Explorer is running and has read the new value from the registry, you can test your AUTORUN.INF file by finding the drive in the My Computer folder and trying to open it. Instead of Explorer opening the folder, it will parse the AUTORUN.INF file and try to execute its "OPEN=" statement.
Q In your article, "A File System for the 21st Century: Previewing the Windows NT 5.0 File System" (MSJ, November 1998), you mentioned the new sparse files feature. I was wondering how (or if) you could use this feature to create growable memory-mapped files.
A Yes, when I first started using sparse files, I immediately thought about how they could be used for growable memory-mapped files (GMMFs). Memory-mapped files (MMFs) are one of my favorite Windows features. For those of you who need a refresher, MMFs allow you to use memory addressing techniques to access the contents of your disk files. This makes it incredibly easy to manipulate the contents of a file, since accessing memory is always easier than doing buffered file I/O. A big problem with MMFs is that they are not growable. In other words, when you call CreateFileMapping to create an MMF, you must tell it exactly how much disk space to allocate.
Let's say that you want to create an MMF to store recorded audio data. As the user speaks, you want to write the digital audio data into a memory buffer and have that buffer backed by a file on the disk. There is a really easy and very efficient way to implement this in your code. The problem is that you don't know how long the user will speak before pressing the Stop button. You may need a file large enough for 5 minutes of dataor 5 hours. So, if you really want to use MMFs in your application, you must call CreateFileMapping, passing the maximum size you think you'll ever need and write the audio data as it comes in. When the user presses the Stop button, you would then close the file-mapping object and shrink the size of the file down by calling SetFilePointer followed by SetEndOfFile.
As you probably guessed, most applications (hopefully) will not use MMFs to implement this because it means an enormous amount of disk space is allocated while the audio data is being written. The likelihood of the data file staying its maximum size once the user stops recording is very slim. This storage problem is well-known to people who use MMFs. Way back when, I created my own GMMF mechanism that allowed a developer to work with MMFs, allocating disk storage only as needed. (See "Add Growable Memory-Mapped Files to Your App with Our Exclusive GMMF API," in the October 1995 issue of MSJ.) The code that I published in that article is still appropriate if you are working with Windows 9x or Windows NT 4.0 or earlier. In fact, my GMMF code is good for all file systems other than Windows NT File System (NTFS) 5.0. If you're designing an application for Windows 2000 and you know that your customer will be saving their data on an NTFS 5.0 partition, then you can use the new sparse file feature to make GMMFs simple to implement.
Figure 2 shows how to create a GMMF using sparse files. To make things easier, I created a CGMMF class that encapsulates the details. The class member functions do all the sparse file stuff that I explained in the November 1998 NTFS article, so I won't go into the fine points here. Instead, I'll just concentrate on the WinMain function.
When the process starts running, I first call CreateFile to create a new file on my NTFS 5.0 disk partition. This is a normal, ordinary file. Then I construct a CGMMF object, passing its constructor the file handle and the maximum size to which I'll allow this file to grow. The constructor tells NTFS 5.0 to make the file a sparse file and creates a file-mapping object that is the maximum size. Note that my code allows the file to grow to 10MB, but since the file is sparse, the file on the disk will actually contain zero bytes! If this were not a sparse file, the file on the disk would actually be 10MB.
Once the CGMMF object is constructed, I try to read some bytes from within the file. I do this simply by creating a PBYTE cast operator method in my CGMMF class. When you read bytes from a sparse file, the file system automatically knows to return 0s for bytes that don't actually exist in the file. So, my For loop doesn't generate any access violations, and when the loop completes, the file on the disk still contains zero bytes.
After the reading is finished, I have another loop that writes 100 bytes, starting 8MB into the disk file. Again, no access violations occur when I execute this loop. When the loop completes, the file will no longer be zero bytes in length; instead, it will now have just enough disk storage allocated to it to hold the 100 bytes. Note that the first 8MB of this file still requires no disk storage at all. This is extremely efficient use of disk space.
The next few lines of code show how to get the logical and physical size of the file. Simply examine the results in the debugger to get a better understanding of what's going on.
Toward the end of WinMain, you see another concept demonstrated. Sometimes the user may want to delete part of a file. For example, the user may choose to delete the first 10 minutes of the audio data file. To delete this data, you can call my CGMMF class's SetToZero method. This method is static because it actually doesn't work on a GMMF at all. To delete a portion of the file, you must work with file handles, not file-mapping handles. In fact, to delete a portion of a sparse file, you must not have any file-mapping objects backed by that file or attempting to delete a portion of the file will fail.
So to delete a portion of the file, you must first call CGMMF's ForceClose method explicitly. This closes the file-mapping object so that the static SetToZero method can be called to delete a portion of the file, freeing up some disk space. At this point, if you want to continue working with the file as a GMMF, you can create another CGMMF object, passing the file handle just like I did at the beginning of WinMain. If you do not have a need to delete a portion of the file, then the CGMMF's destructor calls the ForceClose method when the object goes out of scope.
One last thing. Remember the audio data example mentioned earlier? When the user presses the Stop button, you may want to cut the file's size back down to what you actually need. Trimming the end of a sparse file that contains zero bytes doesn't actually affect the disk space, but it is still a nice thing to do so that the Explorer and the command shell's DIR command report a more accurate file size to the user. To set the end of file marker for a file, you must call SetFilePointer and SetEndOfFile. Just like the call to SetToZero, these functions succeed only if there are no file-mapping objects for the file you're manipulating. This is another place where ForceClose comes in handy.
Have a question about programming in Win32? Contact Jeffrey Richter at http://www.JeffreyRichter.com
From the April 1999 issue of Microsoft Systems Journal