Gain Control of Application
Setup and Maintenance with
the New Windows Installer
| The new Windows installer offers your app some cool features that go well beyond anything available now. What if your app could automatically repair itself, restoring missing files from the installation media when they are referenced? Sound pretty neat? Well, read on for all the details.|
This article assumes you're familiar with C++, Win32, COM|
Mike Kelly is a software developer on the Microsoft Office team, who is working to make Office setup make sense by using the new Windows installer. He can be reached at email@example.com..|
Compared to some
of the topics covered in Microsoft Systems Journal, like DirectX® and Microsoft® Transaction Server, an article on setup might seem like pretty dry stuff. But the new Windows® installer offers your application some cool features that go well beyond anything available now. What if your application could automatically repair itself, restoring missing files from the installation media when they are referenced? How about adding your application's icon to the Start menu of thousands of machines in an enterprisewithout having to actually install any of the application's files on a machine until a user selects that icon? What if you could advertise all the COM servers in your app to those machines without actually installing anything, and have the system automatically install the COM server when it is first instantiated by any client?
Sound pretty neat? Well, read on for all the details.
There are a few key ideas behind the Windows installer. It will make application installation and ongoing management part of the basic Windows system services. This enables the system itself to track what is installed and better manage components that are shared by applications. The installer should persuade developers to think of setup not as a one-time process customers run, but as an integrated part of an application. It should support software installation on "locked down" machines, where ordinary users don't have privileges to do the things that many application installation programs need to do. The Windows installer is a key part of the Zero Administration Windows initiative. Finally, the installer should use the integrated Directory Service (expected to be introduced with Windows NT® 5.0) to introduce two new models of application deployment within large, distributed organizations: assignment and publishing.
I'll describe all of these in more detail in this article. First, though, let's review the various setup approaches Windows-based applications use today.
Applications today use a variety of installation technologies. InstallShield, Seagate's WinInstall, or Great Lakes Software's Wise Installation System are commonly used tools for generating application installation packages. ActiveX® controls and other Web-page components use IExpress packages, which are basically self-extracting executables with simple file copy and registry capabilities. Large, complex applications like Microsoft Office and Corel PerfectOffice often use proprietary installation technology. Some simple components like drivers are able to use INF files, which are similar in capability to IExpress packages.
Microsoft Windows currently provides only a very rudimentary way for administrators or users to know which applications they have installed on their machines and to manage these installed applications. The Add/Remove Programs Control Panel applet is driven by a standard set of registry entries that applications are supposed to write. This registration information provides a command line that Windows can invoke to uninstall the product. Higher-end products like Microsoft Systems Management Server can help in centrally managed environments, but are limited by the information and control that individual application setup scripts choose to provide, and do nothing to help home or small business users who are, in effect, their own system administrators.
How the Windows Installer Helps
The new Windows installer aims to improve this situation by providing a standard installation mechanism for Windows-based applications and components. Tools like WinInstall and InstallShield (as well as proprietary tools used by some application developers) will continue to exist, but will become setup authoring environments; the new Windows installer service is the "execution engine" for the setup scripts that the various authoring tools generate. The installer also exposes an API that applications can use to determine what installation choices users made, or even to install missing components. No more "Please exit and run setup to correct the problem" message boxes; the application simply runs setup for the user using the installer API.
Because the installer is a basic system service and tracks information about what is installed, it can provide administrators in managed environments with a powerful set of tools for determining which software is installedand on Windows NT 5.0, remotely managing software installation on user machines.
|Figure 1 Windows Installer Architecture|
Figure 1 illustrates the installer architecture. On Windows NT, the installer consists of two executable components: a Client Install Engine that runs with user privileges and an Install Service that can run with elevated administrative privileges because it is implemented as a Windows NT Service. All changes to the system configuration are done as a single installation transaction by the Install Service. The transaction provides for rollback of a failed or aborted installation. The rollback includes restoring the original contents of files replaced or deleted during the installation and restoring overwritten or deleted registry settings (such as COM class registration). Since this rollback information can take up a significant amount of space, an administrator or user can disable it during installation.
On Windows 95 and Windows 98, the Install Service and the Client Install Engine run as a separate process. The Client Install Engine and the Install Service communicate through secure remote procedure calls.
In addition to the install package for the product being installed, the installer can apply a "transform." This is a method of customizing the package for a specific group of users. Transforms can be used to disable or enable certain installation features, or even add additional items to the installation (for example, to add customer-specific content to the rollout of a product).
Products, Features, and Components
The installer divides applications into a three-level hierarchy. Figure 2 contains the feature hierarchy for a sample application, EasyMail, used later in this article. At the
top of the hierarchy is a productsomething a user can install. A product is composed of multiple features. A feature is the smallest installable unit of functionality. Examples of features for EasyMail are MailReader, MailEditor, and Spell Checker. If you think of common setup user interfaces, a feature maps to a checkbox in the "custom" or "advanced" installation dialog; it is something a user can choose to not install.
Features are collections of components. A component is the smallest unit of sharing among products and features. While features are specific to a product and identified by a name unique only within the product (such as MailReader), components are global across all products installed on a machine and are identified by a GUID. Although a COM component can be encapsulated as an installer componentand most COM components probably will bedon't confuse the terms. An installer component has nothing at all to do with COM.
Components are the actual contents of your product. A single component may be composed of files, registry entries, COM registration information, Visual Basic® type libraries, shortcuts for the Windows Start menu, and so on (see Figure 2). An installer component is monolithic; it is either entirely installed on a machine or not installed at all.
Because installer component identifiers are global, they are shared across products. For example, a number of products ship the Microsoft Visual Basic for Applications runtime. Defining this as an installer component has a couple of advantages. For starters, the installation logic for Visual Basic for Applications can be encapsulated within the component, so all products will install and uninstall it in exactly the same way. Once Visual Basic for Applications is installed on the machine, the installer knows it is there; installations of subsequent products that use this component simply increases the component's installation reference count. All the products share the same copies of the files. This is possible today, but requires some tricky coordination across productsincluding some non-Microsoft product licensees. The new installer makes this coordination unnecessary because Visual Basic for Applications is encapsulated as an installer component.
Since the installer manages all the components, it can also do the right thing when a product is uninstalled: it will not remove components shared by products remaining on the machine. Again, this is possible today, but requires a fair amount of coordination between products. The installer makes it automatic and free for any product it installs.
Remember that components are shared across features. As shown in Figure 2, Component1 is shared by the Reader and the Editor. Only one copy of that file will actually be installed, but it will be installed if either Feature1 or Feature2 is installed.
Features and components for a product are described in the product's installation database. This is a file with a .MSI extension that contains all the installation information for a particular product, including the user interface displayed during the initial user installation of the product. Setup authoring tools will typically be used to create the installation database file. The installation database is actually an OLE-structured storage file that contains a relational database of tablessee Figure 3 for a partial list of installation database tables. There are actually several dozen tables, but Figure 3 only contains descriptions of a few key tables.
The actual product files may be stored in compressed CAB files contained in streams within the installation database file to facilitate Internet download of a single file to install a small product. For a larger product, product files may be external to the installation database in directories on a CD or a network server.
The Windows installer is a registered server for files with a .MSI extension, so it is automatically invoked by the shell when a .MSI file is opened by a user. When invoked in this way, the installer reads product information from the installation database file and determines whether the product is already installed. If the product is not yet installed, it launches the product's installation sequence, which is described in the database. If the product is installed, different logic can be invoked, such as to add and remove features, or uninstall the product.
The installer also exposes an OLE Automation interface to allow administrators or developers to write Visual Basic or VBScript code that controls product installation.
Finally, the MsiExec command line tool can be used in batch scripts to install, uninstall, or change installation options for a product.
You may wonder why the installer makes a distinction between COM server registration information (the Class table) and registry entries (the Registry table), or between files (the File table) and Start menu shortcuts (the Shortcut table). After all, registering a COM server just entails writing a bunch of registry entries to HKEY_CLASSES_
ROOT, and a shortcut is just a file. One reason is to support a key feature of Windows NT 5.0: feature advertisement.
The installer enables any product feature to be in one of four installation states: locally installed, installed to run from the source, absent, or advertised. Figure 4 explains each of these installation states, along with the symbolic names used in installer API calls.
INSTALLSTATE_ABSENT is used for features that are not installed. Features set to INSTALLSTATE_LOCAL or INSTALLSTATE_SOURCE are installed; the distinction is only between where the bits actually live. A feature has an installation affinity, which the setup author sets. For instance, a feature that is a large set of clip art images might have an affinity to run from source installation media to avoid taking up space on the local drive. Most commonly used features will be set to run locally so users don't have to insert installation media when running the application or have a network connection to the installation server. Users or administrators can override the installation affinity of any feature when installing it. In addition, your application can set the installation state of any feature using the APIs I'll describe later. For instance, you could provide a Prepare for Road Trip menu option that installs all necessary features locally on a laptop.
INSTALLSTATE_ADVERTISED is the most interesting installation state. An advertised feature only has the appearance of being installed. COM servers, extension associations, MIME information, and so on are registered. An advertised feature doesn't have a path to the EXE or DLL that implements the server. Instead, the registry contains an MSI Descriptoran opaque blob of data which references the installed product and feature that can provide the actual bits to implement that server. Start menu shortcuts exist for the feature (if the feature is the application executable), but they don't reference a file directly eitherthey also contain an MSI Descriptor. Only a thin veneer of the feature is actually present on the user's machine. Both the Windows Shell and OLE32.DLL have been modified to recognize these new MSI Descriptors and call the installer to get an actual path. To return the path for an advertised component, the installer must install the feature. That's rightif you invoke a COM server (say, by double-clicking on a mail attachment) or click on a Start menu shortcut for an advertised feature, the installer will automatically install all of the feature's components.
Advertisement, through Application Assign and Publish, should be fully supported only on Windows NT 5.0, although shell support is available for Windows NT 4.0, Windows 95, and Windows 98 if Microsoft Internet Explorer 4.01 Service Pack 1 is installed with Active Desktop enabled. (This installs an updated shell that includes the support for advertisement.) Support for advertised COM servers requires Windows NT 5.0.
Since advertisement, through Assign and Publish, requires very few resources on the user's machine (just the space for the registry entries and Start menu shortcuts), it is an easy way to make available a large number of COM servers or applications that most users may never use. If you ever do happen to need it, though, the system knows how to install it without any user intervention. Better yet, advertising utilizes the Windows NT 5.0 Active Directory to make advertising an installation package to a large group of users very easy. All the features in that package will appear on those users' machines the next time they log on, but no bits will actually be installed until the user invokes one of those applications or COM servers.
It should be noted that advertisement will only be supported for COM servers that are registered through the installer Class table; just writing the registry entries from the Registry table or using the SelfReg table doesn't support advertisement. This is one reason that self-registration (through DllRegisterServer or the /regserver command line argument) is now discouraged in favor of using the installer Class table to do COM server registration. This is also why the installer splits out Class registration from other registry entries. Note that a COM server can still support self-registration, but it should support it by invoking the installer through the API to actually write the registry entries rather than doing it directly.
To clarify some of these concepts, let's take a simple application as an example: your boss asks you to take your company's flagship email product, EasyMail, and make it work with the new Windows installer. Where do you start?
First, you need a product codea GUID that the installer uses to uniquely identify your product. If your product is a COM server, you can use the same GUID for product code as you use for your CLSID, but there is no connection between these two GUIDs. The product's friendly name is EasyMail, and I just generated its associated product code GUID using GuidGen. The GUID is:
|EasyMail is composed of an email editor, a reader, support for various protocols (SMTP, IMAP, and so on), and file converters to enable displaying and sending attachments. These features are identified internally with short names such as MailEditor, MailReader, and IMAP. They are identified to users with a display name such as EasyMail Editor. Remember that a feature is the smallest installable unit of functionality; from a user point of view, this maps to a checkbox in the setup dialog.
Now that you've determined how you'll divide your functionality into features, the next step is to divide the files your product installs into components. Remember that components are the smallest unit of sharing. Every change your product makes on a user's machine when it is installed is part of some componentevery registry setting, shortcut, or file. There is a many-to-many relationship between features and components; a feature is composed of one or more components, and a single component may be part of multiple features.
Suppose your editor and reader share EASYSYS.DLL, which provides a set of common functionality used by both features. Because you've separated the editor and the reader into features, either or both can be installed. However, if either one is installed, you'd like this common DLL to be there. And since you're a forward-looking developer, you might want future products to use this handy DLL as well. Make EASYSYS.DLL a component. Both features will be able to use the component and your future products will also be able to benefit from sharing that common DLL with EasyMail if it is already installed. If registry entries need to be written or COM servers need registration, you link that to the component as well. The installer will automatically write those registry entries and register those COM servers when the component is installed. Better yet, the installer will also automatically remove those entries and unregister those servers when the component is uninstalled. One warning, though: components are shared among all installed products, so be sure you think carefully about components' contents as you design your components.
As a forward-thinking developer, you'd probably also like EasyMail to be available in other languages. You've split all your strings and UIs into resources and are careful to use the Windows National Language System APIs in your code rather than assuming things like the format of dates and numbers. The only thing you aren't doing quite right is that your localized resources are in your main executable; it isn't possible for a user to install multiple languages of EasyMail without installing copies of a lot of things that aren't localized, such as all the code pages in your main executable and DLLs. In many countries in Europe and the Far East, installing support for more than one language is common.
The right approach is to put all your localized resources in a separate, per-language DLL. This DLL has no code, only resources. You can load it at runtime using LoadLibrary with the LOAD_LIBRARY_AS_DATAFILE flag. When you do things like LoadResource or LoadString, you refer to the HINSTANCE of the resource DLL rather than your main DLL. Use the locale identifier (LCID) as part of the name for this resource DLL so you can ship support for as many languages as you want and the DLLs can coexist. For instance, your U.S. English resource DLL will be called EASY1033.DLL. The Japanese one, EASY1041.DLL. Installer-qualified components will help you easily find a particular language DLL at runtime.
Using a qualified component is one way to do a single-level indirection. Normal components have a single identifier: the component ID GUID. Qualified components have two identifiers: a category GUID and a qualifier, which is just a text string. Qualified components use the identifier to map to a real, nonqualified component. Another way of describing this is that a qualified component is a dynamic array of components where the name of the array is the category GUID and the array index is a string.
I decided to use a qualified component to represent EasyMail Language Resources, and to use the LCID of the language as the qualifier. I can easily map the LCID to a display name for the UI using GetLocaleInfo. Using the installer API MsiProvideQualifiedComponent, I can find the path to a particular per-language resource DLL, and even have the installer automatically install it if it isn't present. It's also possible to enumerate the qualifiers for a qualified component. In this case, that would provide the available LCIDs (the available languages for which EasyMail can present a UI). Even if I only ship U.S. English support now, I can write code that uses qualified components. If I later decide to ship a language pack that supports other languages, it will populate this qualified component and, suddenly, EasyMail will know how to speak French and German. Cool, eh?
Of course, it's possible to do something like qualified components yourself; you can use the registry to register your language resource DLLs and look up the path to a language DLL in your code using the registry. The installer just makes it easier, and it gives you on-demand installation for free.
Since components are global, so are qualified components. For example, a File Converters qualified component (with an associated, well-known category GUID) that publishes available file format converters could be accessed from multiple products. By agreeing on the qualifier (perhaps some encoding of file type), multiple products could install and share converters. Better yet, the advertisement for the converter can be present without the actual bits being installed.
Installer Programming Interface
I've alluded to some of the installer APIs in the previous sections. This section will provide a more detailed rundown on the APIs the new installer exposes. These are all contained in MSI.DLL. OLE Automation access for Visual Basic-based development is provided through the MsiApi object model. On Windows 95, the Unicode (W) versions of the APIs will convert arguments to ANSI and call the ANSI (A) versions. On Windows NT, both Unicode (W) and ANSI (A) versions of the APIs are provided. You'll probably want to download the latest version of the Microsoft Windows Platform SDK, which contains the files and tools you need to use the installer APIs in your program. Installer APIs are defined in include\msi.h.
Let's start with the main APIs that EasyMail will need to use at startup. There are some basic steps an application installed by the new installer should do when it starts up. First, it should call MsiGetProductCode so the application can identify itself to the installer. This provides the product code you'll need for other installer APIs you'll be using. Second, you should call the MsiGetUserInfo function to get information about the registered user. For example, your application might display this information in its splash screen as a way of deterring piracy of EasyMail. MsiGetUserInfo returns failure if the requested information isn't present; this will be the case the first time it is called after the application is installed if that information wasn't provided during setup. You should then call MsiCollectUserInfo function, which puts up a dialog to collect user information. This brands the installation of your product with the registered user's name and company. Third, while building the basic user interface for your application (the set of menus and toolbars you expose), you may now have code that checks whether certain features are installed. For instance, your EasyMail reader wouldn't show an editor toolbar button (or would disable it) if the EasyMail editor isn't present. Since you can install any feature on demand, you should instead use the installer APIs to determine which features should be enabled in your application. You can enumerate features in a few ways:
- Query the installer feature-by-feature. For example, before the application draws a button or a menu item, the application can call MsiQueryFeatureState to check whether a particular feature is available.
- Enumerate all of the available features at once by calling MsiEnumFeatures.
- Enumerate qualifiers for qualified components using MsiEnumComponentQualifiers. You might use this in EasyMail to determine which file converters are installed, or what UI languages you can support.
There are several ways that Windows-based applications access functionality implemented external to the current executable or DLL. You may use COM (via CoCreateInstance); you might find the path to a DLL using the registry; or you might just assume that a given file is in a particular directory relative to your executable. CoCreateInstance remains unchanged, but for the other two, you'll want to use installer APIs instead of your existing mechanism.
First, let's address COM and CoCreateInstance. Because the new installer is part of Windows, COM has been modified to work with it. When you call CoCreateInstance, you pass the CLSID of the COM class you want to instantiate. Previously, COM would look in the registry to find the absolute path to the executable or DLL that is the server for that class and launch it (for EXE servers) or load it (for in-process servers). Starting with Windows NT 5.0, COM (through OLE32.DLL) first looks for an MSI Descriptor under the CLSID key in the registry. If present, COM knows this class was installed with the Windows installer and invokes the installer to give the path to the COM server. This allows the installer to install the server bits if the server is advertised. If the server is already installed but the user has somehow removed the DLL or EXE supporting the server, the installer will detect that and automatically reinstall it, providing resiliency for COM servers.
So for functionality you access through COM, your application doesn't need to do anything new.
You should figure out other ways to find external components to use the installer interfaces. You could, of course, mimic COM and write some sort of descriptor to the registry in place of paths and have your app continue to look up those descriptors from the registry and translate the descriptor to a path using the installer APIs. But why are you writing to the registry at all? Most applications do this to allow their setup program to communicate with the running application through the registrythe setup program writes the path to the bits in the registry and the application reads the path from there. You can accomplish this goal without using the registry simply by using an installer component. EasyMail can call MsiProvideComponent to find the path to any component given the component ID. The installer will automatically install the component if you pass the flag INSTALLMODE_DEFAULT as the dwInstallMode parameter, or only return the path to an already available component (or an error code if the component is not installed) if you pass INSTALLMODE_
EXISTING. This does three things for your application. First, it reduces your dependency on fragile, registry-based communication between your setup program and your application. It also simplifies your setup since you no longer need to write these registry values. Second, it makes your application more resilient; even if the user inadvertently deletes a necessary file, the installer can automatically reinstall it when your application looks for it. Third, it allows users to make any feature "run from installation source" without any additional work by your application. The user (or administrator) can determine the right tradeoff between local disk usage and speed.
If for some reason you want to allow administrators to override the setup-provided bits for a component, you can still check the registry setting and use the path contained in the registry if it is set. You might want to do this, for example, for a dictionary; you provide the default dictionary, but administrators can substitute their own dictionary by pointing a registry setting to it. If the registry is not set, you fall back on the installer to provide the default bits.
Qualified Component APIs
Qualified components can be used for file converters and translators, dictionaries, templates or standard forms, or any per-language resource.
Qualified components are also helpful for creating third-party wizard or add-in files that your application uses at runtime to augment or modify its behavior. Increasingly, though, this would be through a COM interface that you would just find through the standard COM techniques. Let's look at the definition of the PublishComponent table (see Figure 5) to understand how qualified components work. Figure 6 contains details on the installer APIs that deal with qualified components.
When you author your setup, you decide what the Qualifier and AppData values will be for your qualified components. The Qualifier needs to be a unique identifier within this category. This should be a string that you can generate easily when you are looking for a particular qualified component. For example, if your component is qualified by language, using the LCID as a string is a natural choice. The AppData field is optional (and localizable). This could be a string that you display in your user interface to describe the qualifier. If you are using LCID as the qualifier, chances are you don't want to display LCID numbers to your userit's probably better to display "French dictionary," "German dictionary," or something similar. You can use AppData to contain those strings. When you enumerate the qualifiers for a qualified component, you will receive both the Qualifier and the AppData from the MsiEnumComponentQualifiers installer API.
Figure 7 shows some code that finds a component whose qualifier is the LCID as a string ("1033" for U.S. English) given the LCID and a default LCID. It also tries using the primary language if the preferred language doesn't exist (so that, for instance, if I looked for Brazilian Portuguese but had Portuguese installed, I would find that). This is just one common usage of qualified components.
Installation and Configuration Functions
As you would expect, several installer APIs deal with actually installing products and individual features. It is unlikely that you will need to programmatically install an entire productusers will still run a setup program to install your product. However, if you need to, you can use the MsiInstallProduct API. One use for this would be where a subproduct is shipped with your main product. Remember, though, that running setup does not necessarily copy all your application's files to the local machine.
If you detect that your product's installation is damaged in some critical way, you can reinstall the product using MsiReinstallProduct. This will also rewrite any registry information, including COM class registration, for your product. Applications often have error messages that advise the user to run setup again if an expected COM server cannot be created or some other "should never happen" condition occurs. MsiReinstallProduct lets you do this reinstallation automatically. To get a finer level of control, you can reinstall just a particular featureperhaps the damaged one.
MsiConfigureProduct and MsiConfigureFeature can be used to change the installation state of a featurefor instance, to make a feature that is now running from the installation source be run locally insteadcopying the
files in all the components associated with that feature to
a local drive.
Prior to using a particular feature, your application should call MsiUseFeature. This allows the installer to track feature usage counts and ensure that the feature is installed. This information can be valuable when you install the next version of your application. You can then choose which features to install locally based on the actual usage of features in the prior version of the application.
Most of the time you'll use the installer APIs to determine whether a feature is installed (so you know whether to enable the UI for that feature) and to obtain the file system paths to files you need while running your app. A component has a designated key file. When checking whether a component is installed correctly, the installer uses the existence of the key file as a shortcut. It is possible to get the installer to do a more extensive check, but usually only if a problem is encountered. The installer requires that all files associated with a single component go into a single file system directory, so the key file path also shows where all the files reside.
Use MsiQueryFeatureState to determine the installation state of a particular feature. MsiQueryFeatureState is fast, so it does not validate the feature's components; it only checks whether the feature is marked as installed or not. MsiQueryFeatureState returns the INSTALLSTATE for the feature (see Figure 4).
To obtain the path to the key file for a component, first check the associated feature state. If the feature is not installed, you won't be able to get the path to its components. For installed components, use MsiGetComponentPath to obtain the path to the key file. You can do the normal path factoring to access other files in the component (see Figure 8).
Other Installer APIs
There are many other installer APIs. Authored into the install package is a UI that is presented when running setup. This user interface supports all the Windows common controls and makes localizing your setup to different languages easy. If your application installs a particular feature, you can choose to let the installer put up this UI while installing the feature, or override it and provide UI within your application. With MsiSetExternalUI, you provide the installer with a function that the installer will call with progress information during the installation. You can use this callback function to put up your own user interface. There is also a whole set of installer APIs to access the internal installer database tables; these are used by tools and custom actions. For more information, download the Platform SDK and browse the MSI.HLP help file.
One of the last things on your mind during the development of your product is probably setup. For a new product, you expect setup to be pretty easy: copy a few files, write a few registry entries, and you're done. If you're shipping a new version, you know how tricky and complex doing a proper setup for today's applications can beregistering COM servers, creating shortcuts, handling DLL version mismatches and platform differencesbut you figure you'll just take the last version's setup, tweak it a bit and you're ready to ship. In either case, setup gets thought of late in the game.
With the new Windows installer, you can think of how your product will be installed early in designwhile you are defining the key components of your product and designing the user interface. In exchange for this additional up-front attention, your application gets to tune its disk usage to each user's preference and automatically reinstall missing files. You can also be part of the new Zero Administration for Windows initiative movement by supporting application assignment and publishing.
Because the popular setup tools vendors like InstallShield and Wise are introducing support for the Windows installer, you can use the tools you already know to author more intelligent setups. The Windows installer helps close the circle between the application's initial setup and the running application, hopefully reducing support calls for your product by making it easier for the application to repair minor problems or replace missing files automatically.
From the September 1998 issue of Microsoft Systems Journal.
Get it at your local newsstand, or better yet, subscribe.
For related information see: Windows 95 Application ASetup Guidelines for Independent Software Vendors,
Also check http://www.microsoft.com/msdn for daily updates on developer programs, resources and events.