Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Languages / XML

Adventures in Debugging: Hosting Processes, Windows Subsystems, and Other Magical Things

5.00/5 (16 votes)
1 Aug 2021BSD22 min read 18.7K   247  
Though useful and mostly harmless, pitfalls can lead unwary developers astray.

Introduction

A couple of years ago, when I was developing a library of helper classes to further expedite my approach to the way software communicates to its users, I discovered the first of several surprises about the way that debugging with the Visual Studio Hosting Process enabled can cause your program to produce unexpected results. The first of these discoveries arose when I integrated an unmanaged function that reported the execution subsystem of a program, upon which I base decisions about which features of my exception logger class to enable by default. Since I almost always implement such test programs as console applications, I was shocked when the first attempt to use the new routine reported that my test program ran in the Windows GUI subsystem!

Further investigation uncovered the cause, which is that debugging with the Visual Studio hosting process enabled, which it is by default, causes the program that you are debugging to run as a child of the Visual Studio hosting process, a graphical mode program that runs in the Windows GUI subsystem. Further complicating matters, System.Reflection.Assembly.GetEntryAssembly identifies the hosting process assembly as the entry assembly.

More recently, another, more subtle issue came to my attention, when I noticed that my my console programs displayed incorrect startup times, often minutes, or even hours, before I actually hit F5 to start debugging. Further investigation over the last few days led to the discovery that you have relatively little control over when the Visual Studio hosting process starts, meaning that the System.Diagnostics.Process.StartTime reports the time when the Visual Studio hosting process started, which may be long before you started debugging.

Beware: Since the default project configuration enables the Visual Studio hosting process, you are using it, unless you disable it. I'll show you how to disable it in the demonstration section, towards the end of the article.

Update: From Visual Studio 2017 onwards, the Visual Studio Hosting Process has been retired. See Debugging and the Hosting Process. Unless you still use Visual Studio 2015 or earlier the sole remaining values of this article are academic and historical. This author won't miss the Visual Studio Hosting Process.

Background

The discussion that follows touches on the following namespaces in the Base Class Library, all of which are exposed by its core libraries.

  • System.Reflection
  • System.Diagnostics
  • System.IO

When I created the helper class that virtually all of my character mode programs use to generate their startup banners and keep track of their wall clock running times, I wanted the startup time to be the time when the process actually started, which might be considerably earlier than when the initial display was formatted and written, especially when you single step the startup. It so happens that the startup and exit times are two of the many properties that Windows keeps in the massive object that springs into being when a new process starts, and stays with it until Windows restarts or assigns the same ID to another process, whichever comes first. Most of these details are accessible to managed code through the System.Diagnostics.Process object that follows your code for its managed lifetime.

I decided to use the System.Diagnostics.Process.StartTime as my startup time, and wrote my standard startup routine to save its value into a private System.DateTime structure, for use both as the startup time in the initial display (the logo or banner), and for eventual use as the starting point for computing the running time of the program, which goes into the last message written on the console. I had not yet discovered that, when the Visual Studio hosting process is enabled, it owns the Process object, which springs into existence long before your debugging session gets underway, and it usurps the role of Entry Assembly.

What Is the Visual Studio Hosting Process?

Each time you ask the Visual Studio IDE to build a Visual C# or Visual Basic program, it creates two programs.

  1. MyProgram.exe, where MyProgram is the name given in the Assembly Name text box on the Application tab of the project property sheet.
  2. MyProgram.vshost.exe is a simple stub program that implements the Visual Studio hosting process for your debugging sessions. For example, Figure 1 shows that the name of the demonstration assembly is VSHostingProcess_Demo, which becomes VSHostingProcess_Demo.exe (the demonstration program) and VSHostingProcess_Demo.vshost.exe (the hosting process assembly), respectively, shown in Figure 2..

Figure 1

Figure 1 is the Application tab of the project property sheet for the demonstration project.

aImage 2

Figure 2 is the Debug build output directory, which contains the demonstration assembly, its Visual Studio hosting process assembly, and their configuration files.

The file explorer window shown in Figure 2 shows the files that were created when the project shown in Figure 1 was built.

I noticed early on that the Visual Studio hosting process assembly that went into the output directory of every project appeared to be quite similar. Researching this article prompted me to dig a good bit deeper. Choosing another project that was built at about the same time, and targets the same framework, I started with the side by side directory listings shown in Figure 3, which shows that the two files are exactly the same size.

Figure 3

Figure 3 is a side by side directory listing of the Visual Studio hosting process assemblies from two projects, both targeting Microsoft .NET Framework, Version 3.5 Client Profile.

Both fc.exe, the byte for byte file comparer that ships with Windows, and IDM Computer Solutions' UltraCompare revealed that, although they are the same size, subtle differences exist in the files.

Comparing them side by side in ILDAsm.exe yielded the expected result that both assemblies have identical object models and manifests. Figure 4 shows the object models side by side. Digging deeper revealed that all of the generated IL is identical. The proof is left as an exercise for insatiably curious readers, for whom I have provided the necessary resources in VSHostingProcess_SideBySide_20170401_154841.ZIP.

Figure 4

Figure 4 shows both hosting process assemblies side by side in ILDAsm windows. The second assembly listed in Figure 3 is on the left. while the demonstration file is on the right.

Satisfied that the two assemblies are substantially identical, I set them aside; perhaps I'll dig deeper, to precisely identify the differences, but that would be a digression from the objective of this article.

A Robust Solution

To address an unrelated issue, I recently began investigating the role of Application Domains, and did a good bit of spelunking in that class and its many members, but that is a subject for another day, and maybe another article.

To understand how I solved the problem, there are two things about Application Domains that you must know.

  1. Every process has at least one Application Domain, which is exposed by the static CurrentDomain property on the System.AppDomain class.
  2. The default Application Domain of every process that runs under a Visual Studio hosting process has a DomainManager property that exposes an EntryAssembly property, which identifies the real entry assembly (the one you are debugging).

Application domains offer a world of possibilities, and I'll leave it at that. Consider your appetite whetted.

Every Visual Studio hosting process identifies its entry assembly through the CurrentDomain property of its default AppDomain. What do you do when the CurrentDomain.DomainManager property is null? You use the other property, about which I have known for years, and originally used, the Assembly object returned by the static System.Reflection.Assembly.GetEntryAssembly method.

  • For a hosted process, Assembly.GetEntryAssembly returns a reference to the host assembly, e. g., VSHostingProcess_Demo.vshost.exe.
  • For a freestanding process, Assembly.GetEntryAssembly returns a reference to the expected assembly, the one you are debugging.

I brought up application domains for one very specific reason; they paved the way for the next stage of my research, and for a straightforward solution to the problem, which is enshrined in the private InitializeInstance method of the PESubsystemInfo class, which implements the Singleton design pattern, and is shown next, in Listing 1.

C#
if ( _intDefaultAppDomainSubsystemID == IMAGE_SUBSYSTEM_UNKNOWN )
{    // Use the work done by the first invocation.
    Assembly asmDomainEntryAssembly = AppDomain.CurrentDomain.DomainManager != null
                                      ? AppDomain.CurrentDomain.DomainManager.EntryAssembly
                                      : Assembly.GetEntryAssembly ( );
    _asmDefaultDomainEntryAssemblyName = asmDomainEntryAssembly.GetName ( );
    _strDomainEntryAssemblyLocation = asmDomainEntryAssembly.Location;
    _intDefaultAppDomainSubsystemID = GetPESubsystemID ( _strDomainEntryAssemblyLocation );
}    // if ( _intProcessSubsystemID == IMAGE_SUBSYSTEM_UNKNOWN )

Listing 1 is the entire InitializeInstance method of the PESubsystemInfo class.

The solution boils down to the first statement inside the IF block shown in Listing 1, which needs a tad of explanation.

  • The current (default) application domain of an application that runs in the Visual Studio hosting process has a  DomainManager property, and its EntryAssembly property points to the "real" entry assembly, the one you built, and are testing.
  • The default domain of an application that starts on its own, without the benefit of a hosting process, has no DomainManager, but you can get a reference to its entry assembly by calling the static Assembly.GetEntryAssembly method.

The remaining statements in the block save the AssemblyName and Location properties of the entry assembly into private object variables for immediate use and future reference.

  • The _strDomainEntryAssemblyLocation property, a simple string, is the fully qualified path (name) of the file from which the entry assembly was loaded, which is put to immediate use to derive its Windows subsystem ID.
  • The _asmDefaultDomainEntryAssemblyName property is an AssemblyName instance, which exposes the parts of an assembly's full name, which are useful for startup banner strings, window captions, among other things.

Since InitializeInstance is called when GetTheSingleInstance is called upon to return a reference to the PESubsystemInfo singleton, and Reflection calls are relatively expensive, InitializeInstance short circuits after the first call by taking advantage of the fact that IMAGE_SUBSYSTEM_UNKNOWN is an invalid value for _intDefaultAppDomainSubsystemID, and GetPESubsystemID should reset it.

Identifying the Subsystem in which an Assembly Runs

At last, it's time to turn our attention to the issue that started this expedition, is this a character mode program or a full fledged graphical Windows program?

Every assembly loads from a Windows Portable Executable (PE) file, as does every other program that runs on a Windows operating system. The only exception that is of any significant interest is old 16 bit MS-DOS programs that can still run on 32 bit versions of Microsoft Windows. Strictly speaking, DOS programs don't run under Windows, but use a virtualized MS-DOS machine. Everything else, including command line utilities such as fc.exe (mentioned above), Xcopy.exe, RoboCopy.exe (its successor), cmd.exe, and its nominal successor, PowerShell.exe, is a Portable Executable. Henceforth, I shall referee to them as PE files.

The first thousand bytes or so of every PE fie is its PE Header, which has a rather complex format comprised of variable length tables and pointers to their starting locations, augmented by a collection of flags that tell Windows what kind of file it is, how it should be loaded, whether it is a debug build, and lots of other information that is useful to know when a program loads into memory and while it runs. The PE header is fairly well documented in the Windows Platform SDK, and the structures that comprise it are defined in WinNT.h.

Anyone who has explored the innards of a PE file almost certainly owes Matt Pietrek a debt of gratitude for two articles, "Peering Inside the PE: A Tour of the Win32 Portable Executable File Format," "An In-Depth Look into the Win32 Portable Executable File Format," and "An In-Depth Look into the Win32 Portable Executable File Format, Part 2.," and his famous PEDump.exe, which is thoroughly described and documented in the original 1994 article (the first of the three cited articles). I certainly do, and most of the code of the original unmanaged (straight C) version of the routine that I wrote to gather this information is adapted from code that comprised a small portion of PEDump.exe. I also owe my thanks to a former neighbor, Allan Winston, for unearthing the articles, which have moved several times since their initial publication.

After a review of the original C code, I decided that including it here would seriously confuse the story, so I left it out. Anyone who wants to learn how it's done in C can examine PEDump, which is still available on Matt's Web site.

C#
public static Int16 GetPESubsystemID (
    string pstrFileName )
{
    //    ----------------------------------------------------------------
    //    Everything that remotely smacks of being a magic number is
    //    defined as a constant, everything that is the object of an
    //    integer comparison is the same size as the integer against which
    //    it is compared, and every integer that is involved in a compare
    //    against data read from the file has its size specified as an
    //    integer of the appropriate number of bits (16 for the EXE magic
    //    and the subsystem ID, and 32 for the NT header magic. The goal
    //    of this much precision is to enable the program to perform
    //    correctly on both 32 and 64 bit processors.
    //    ----------------------------------------------------------------

    const int BEGINNING_OF_BUFFER = 0;
    const int INVALID_POINTER = 0;
    const int MINIMUM_FILE_LENGTH = 384;
    const int NOTHING_READ = 0;
    const int PE_HEADER_BUFFER = 1024;

    const int PE_HDR_OFFSET_E_LFANEW = 60;
    const int PE_HDR_OFFSET_SUBSYSTEM = 92;

    const Int16 IMAGE_DOS_SIGNATURE = 23117;
    const Int32 IMAGE_NT_SIGNATURE = 17744;

    const char QUOTE_CHAR = '"';

    //    ----------------------------------------------------------------
    //    Verify that the string that is expected to contain the file name
    //    meets a few minimum requirements before we incur the overhead of
    //    opening a binary stream.
    //    ----------------------------------------------------------------

    if ( string.IsNullOrEmpty ( pstrFileName ) )
        throw new ArgumentException (
            pstrFileName == null                                                            // Differentiate between null reference and empty string.
                ? Properties.Resources.MSG_GETSUBSYST_NULL_FILENAME_POINTER                    // Display this message if the pointer is null.
                : Properties.Resources.MSG_GETSUBSYST_FILENAME_POINTER_EMPTY_STRING );        // Display this message if the pointer is the empty string.

    FileInfo fiCandidate = null;

    if ( File.Exists ( pstrFileName ) )
    {    // File exists. Check its length.
        fiCandidate = new FileInfo ( pstrFileName );

        if ( fiCandidate.Length < MINIMUM_FILE_LENGTH )
        {    // File is too small to contain a complete PE header.
            throw new ArgumentException (
                string.Format (
                    Properties.Resources.MSG_GETSUBSYST_FILE_TOO_SMALL ,                    // Format control string
                    new object [ ]
                    {
                        pstrFileName ,                                                        // Format Item 0 = File name, as fed into the method
                        QUOTE_CHAR ,                                                        // Format Item 1 = Double Quote character to enclose file name
                        fiCandidate.Length ,                                                // Format Item 2 = Actual length (size) of file
                        MINIMUM_FILE_LENGTH    ,                                                // Format Item 3 = Minimum file length
                        Environment.NewLine                                                    // Format Item 4 = Embedded Newline
                    } ) );
        }    // if ( fiCandidate.Length < MINIMUM_FILE_LENGTH )
    }    // TRUE (anticipated outcome) block, if ( File.Exists ( pstrFileName ) )
    else
    {    // The specified file cannot be found in the current security context.
        throw new ArgumentException (
            string.Format (
                Properties.Resources.MSG_GETSUBSYST_FILE_NOT_FOUND ,                        // Format control string
                pstrFileName ,                                                                // Format Item 0 = File name, as fed into the method
                QUOTE_CHAR ) );                                                                // Format Item 1 = Double Quote character to enclose file name
    }    // FALSE (Unanticipated outcome) block, if ( File.Exists ( pstrFileName ) )

    //    ----------------------------------------------------------------
    //    Since the file name string passed the smell test, open the file.
    //    read up to the first kilobyte into memory, and search for the
    //    magic flags.
    //    ----------------------------------------------------------------

    Int16 rintSubystemID = IMAGE_SUBSYSTEM_UNKNOWN;

    try
    {
        int intBytesToRead =
            ( fiCandidate.Length >= PE_HEADER_BUFFER )
            ? PE_HEADER_BUFFER
            : ( int ) fiCandidate.Length;
        int intBytesRead = NOTHING_READ;
        byte [ ] abytPeHeaderBuf;

        //    ------------------------------------------------------------
        //    Since the file I/O happens within a Using block guarded by a
        //    try/catch block, proper disposal of its unmanaged resources
        //    is guaranteed by the runtime engine.
        //
        //    Before the buffer is processed, the number of bytes actually
        //    read is compared against the expected count, which is the
        //    lesser of 1024 (1 KB) or the length of the file.
        //    ------------------------------------------------------------

        using ( FileStream fsCandidate = new FileStream (
            pstrFileName ,
            FileMode.Open ,
            FileAccess.Read ,
            FileShare.Read ) )
        {
            abytPeHeaderBuf = new byte [ intBytesToRead ];

            intBytesRead = fsCandidate.Read ( abytPeHeaderBuf ,                                // Store bytes read into this array.
                                              BEGINNING_OF_BUFFER ,                            // Start copying at this offset in the buffer, which happens to be its beginning.
                                              intBytesToRead );                                // Store up to this many bytes, which happens to be the size of array abytPeHeaderBuf.

            if ( intBytesRead < intBytesToRead )
            {    // An I/O error occurred while reading input file {0}{3}Only {1} of the expected {2} bytes were read.
                throw new Exception ( string.Format (
                    Properties.Resources.MSG_GETSUBSYST_FILE_READ_SHORT ,                    // Format Control String
                    new object [ ]
                    {
                        pstrFileName ,                                                        // Format Item 0 = File Name, as tendered for processing
                        intBytesRead ,                                                        // Format Item 1 = Count of bytes actually read from file
                        intBytesToRead ,                                                    // Format Item 2 = Count of bytes expected to be read
                        Environment.NewLine                                                    // Format Item 3 = Embedded newline
                    } ) );
            }    // if ( intBytesRead < intBytesToRead )

            //    --------------------------------------------------------
            //    Though it could be moved outside the using block, or the
            //    enclosing try block, for that matter, I chose to leave
            //    the testing inline, which makes the program flow very
            //    clean. Since it's all over in a matter of nanoseconds,
            //    leaving the file open won't make that much difference.
            //    --------------------------------------------------------

            Int16 intPEMagic = BitConverter.ToInt16 ( abytPeHeaderBuf , BEGINNING_OF_BUFFER );

            if ( intPEMagic == IMAGE_DOS_SIGNATURE)
            {    // Checking for the presence of the magic WORD is the very first task.
                Int32 intPEOffsetNTHeader = BitConverter.ToInt32 (
                    abytPeHeaderBuf ,
                    PE_HDR_OFFSET_E_LFANEW );

                if ( intPEOffsetNTHeader > INVALID_POINTER && intPEOffsetNTHeader <= intBytesToRead )
                {    // The location of the NT header is variable, but the DOS header has a pointer, relative to its own start, at a fixed location.
                    Int32 intNTHeaderMagic = BitConverter.ToInt32 (
                        abytPeHeaderBuf ,
                        intPEOffsetNTHeader );

                    if ( intNTHeaderMagic == IMAGE_NT_SIGNATURE )
                    {    // Though the Subsystem is at a fixed offset within the NT header, the location of the start of said header is variable, but known.
                        rintSubystemID = BitConverter.ToInt16 (
                            abytPeHeaderBuf ,
                            intPEOffsetNTHeader + PE_HDR_OFFSET_SUBSYSTEM );
                    }    // TRUE (anticipated outcome) block, if ( intNTHeaderMagic == IMAGE_NT_SIGNATURE )
                    else
                    {    // The NT header magic DWORD is missing.
                        throw new Exception (
                            string.Format (
                            Properties.Resources.MSG_GETSUBSYST_NO_NT_MAGIC ,                // Format control string
                            pstrFileName ) );                                                // Format Item 0 = File Name as submitted
                    }    // FALSE (unanticipated outcome) block, if ( intNTHeaderMagic == IMAGE_NT_SIGNATURE )
                }    // TRUE (anticipated outcome) if ( intPEOffsetNTHeader > INVALID_POINTER && intPEOffsetNTHeader <= intBytesToRead )
                else
                {    // The pointer to the NT header is missing.
                    throw new Exception (
                        string.Format (
                            Properties.Resources.MSG_GETSUBSYST_NO_NT_SIGNATURE ,            // Format control string
                            pstrFileName ,                                                    // Format Item 0 = File Name as submitted
                            intPEOffsetNTHeader ,                                            // Format Item 1 = Offset of PE header
                            Environment.NewLine ) );                                        // Format Item 2 = Embedded newline
                }    // FALSE (unanticipated outcome) block, if ( intPEOffsetNTHeader > INVALID_POINTER )
            }    // TRUE (anticipated outcome) block, if ( intPEOffsetNTHeader > INVALID_POINTER && intPEOffsetNTHeader <= intBytesToRead )
            else
            {    // The PE header magic WORD is absent.
                throw new Exception (
                    string.Format (
                        Properties.Resources.MSG_GETSUBSYST_NO_MAGIC ,                        // Format control string
                        pstrFileName ) );                                                    // Format Item 0 = File Name as submitted
            }    // FALSE (unanticipated outcome) block, if ( intPEMagic == IMAGE_DOS_SIGNATURE)
        }    // using ( FileStream Candidate = new FileStream ( pstrFileName , FileMode.Open , FileAccess.Read , FileShare.Read ) )
    }    // The normal flow of control falls through to the return statement at the very end of the function block.
    catch ( IOException exIO )
    {
        throw new Exception (
            string.Format (
                Properties.Resources.MSG_GETSUBSYST_FILE_READ_ERROR ,                        // Format control string
                exIO.GetType ( ).FullName ,                                                    // Format Item 0 = Fully qualified Type of the exception
                pstrFileName ,                                                                // Format Item 1 = File name, as fed into the method
                Environment.NewLine ) ,                                                        // Format Item 2 = Embedded Newline
            exIO );                                                                            // The original exception gets the InnerException seat.
    }
    catch ( Exception exMisc )
    {    // If the exception is our own, it contains the file name; pass it up the call stack. Otherwise, wrap a new exception around it that can pass the file name up the call stack.
        if ( exMisc.TargetSite.Name == System.Reflection.MethodBase.GetCurrentMethod ( ).Name )
            throw;
        else
            throw new Exception (
                string.Format (
                    Properties.Resources.MSG_GETSUBSYST_GENERAL_EXCEPTION ,                    // Format control string
                    exMisc.GetType ( ).FullName ,                                            // Format Item 0 = Fully qualified Type of the exception
                    pstrFileName ,                                                            // Format Item 1 = File name, as fed into the method
                    Environment.NewLine ) ,                                                    // Format Item 2 = Embedded Newline
                exMisc );                                                                    // The original exception gets the InnerException seat.
    }

    //    ----------------------------------------------------------------
    //    Leaving the return here, and letting execution fall through is
    //    easier than arguing with the compiler about whether all code
    //    paths return a value.
    //    ----------------------------------------------------------------

    return rintSubystemID;
}    // public static Int16 GetPESubsystemID

Listing 2 is all of GetPESubsystemID, which is implemented as a static method on PESubsystemInfo, and is used internally by its instance initializer.

Extracting information from a PE header is fairly straightforward, especially when you have the PEDump source and the Windows API header files as references. Writing a PE header parser in C# is pretty much the same as it is in C, with two major differences.

  1. My C# mplementation fills a byte array from a System.IO.FileStream object, while the C implementation used a memory-mapped file created from a conventional file handle, which manifests itself as an array of bytes.
  2. Since it didn't have access to the structures defined in WinNT.h, my implementation used offsets into the byte array, which were computed manually from the structures defined in WinNT.h, coupled with calls to static BitConverter methods to convert the bytes into 16 and 32 bit integers required for testing the magic values that serve as landmarks, and to recover the subsystem ID.

Since this method is exposed to the general public, in a manner of speaking, there are several sanity checks on the filename string that Matt didn't apply to PEDump.

  1. If the string is a null reference or the empty string, an ArgumentException is thrown, using one of two messages that differentiate between a null reference and the empty string.
  2. The string is fed to System.IO.File.Exists, which must return TRUE, or an ArgumentException is thrown. Note that the exception message includes the string that caused it. One of my pet peeves is exceptions that omit such important details. While there are situations when this is too much information, or disclosing it poses a security risk, I would rather supply the information, and leave that decision in the hands of the caller, than risk it becoming lost because the string is the output of a nested method call.
  3. If the file exists, a System.IO.FileInfo object is created around it, and its length is tested. If the file contains fewer than MINIMUM_FILE_LENGTH (384) bytes, another ArgumentException arises, which reports the length along with the file name and the threshold that it didn't exceed.

Only after the file has passed all three of the above tests is its first kilobyte read into memory, that being more than enough to ensure that it includes the subsystem ID. Reading the file is the task of a single call to the Read method on a FileStream object, which fills a 1024 byte array, and verifies that it got that many.

The rest of the routine is straightforward, and follows the pattern used by my own C code and PEDump.

Converting the subsystem ID to something meaningful is straightforward. Since there are only 13 values, ranging from zero through 14, with a couple of unassigned values, treating the subsystem ID as an array subscript makes the conversion very straightforward.

  • Static array s_astrShortNames holds the list of short names described in WinNT.h.
  • Static array s_astrLongNames holds the list of long names described in WinNT.h.

To facilitate programmatic testing of subsystem IDs, PESubsystemInfo defines the PESubsystemID enumeration, along with a set of Int16 constants for the most common subsystems. For maximum flexibility, I defined both, and made the integer subsystem ID and PESubsystemID  freely convertible in both directions. Internally, the class uses the integer, but everything is accessible through either the raw integer or the enumeration.

Using the Code

The demonstration program is deceptively simple.

  • Static member s_dtmStartedUtc is a System.DateTime structure, which is initialized to System.DateTime.UtcNow by way of a static initializer.
    • Using a static member with an initializer guarantees that its value is set as soon as possible.
    • Since it must retain its initial value to be usable for computing the total running time of the program, it is marked read only.
    • I use UTC time, rather than local time because UTC is unambiguous, since it completely ignores Daylight Saving Time., yet conversion to local time is as easy as calling the ToLocalTime instance method. 
  • Just inside the main routine, peMainInfo is defined as a PESubsystemInfo, and assigned a reference to the single instance of a PESubsystemInfo.
  • Next, string strLogoBanner is initialized by string.Format, which is noteworthy for three reasons.
    • The same message is written twice, first, to the console, then to the trace log. Otherwise, I would have used the workhorse of character mode output, Console.WriteLine. I'll explain about the trace log shortly.
    • The format items are organized into an array of objects.
      • Though I usually use string arrays, and take complete control over the formatting of each item, I chose to accept the default formats, so that I could forego references to the custom helper classes that I use to handle most of the work, or pulling even more of them into the project, which already has adapted versions of two of my helper classes.
      • Using a parameter array of objects requires writing less code, because the runtime implicitly calls ToString on each object.
    • The labeling scheme evident in the line comments is a very deliberate attempt to document the mapping of array elements to format items, and to ensure that there are as many items in the array as there are format items in the format control string..
  • The use of trace logging is a bit unorthodox, because the last trace record isn't written until after the operator presses the Return key to exit the program. The objective was to document as accurately as possible when the hosting process exits, because the debugger doesn't time stamp its own entries in the output window, and, since the output is visible for only a split second when you run the code in a debugger (when the timing is most interesting), it goes into the trace log.
  • The rest of the demonstration program is unremarkable.

The other three classes deserve a paragraph or so each.

TraceLogger

This static class exports ten methods that cover every imaginable combination of local and UTC time stamps. Only one, WriteWithBothTimesLabeledLocalFirst, is actually used. I wrote the other nine to complete the set, which I expect to move into a library, in which the entire class will be marked public, and incorporated into the library's namespace.

AppDomainDetails

This static class exports two methods, both used in the main routine. I put them into their own class because I expect them to find their way into a library, quite possibly the same one that gets TraceLogger. Defining them in a class of their own simplifies incorporating them into another library, because all that is required is to copy the the module, change the namespace, and mark the class public,

GenericSingletonBase

This class is based on an article that I stumbled upon one day, Base Class for Singleton Pattern with Generics.

Update: On Sunday, 01 August 2021, I discovered that the foregoing article has disappeared. Thankfully, it was superseded by a CodeProject article, A Reusable Base Class for the Singleton Pattern in C#.

My implementation resolves the issue raised in the original, since vanished, article regarding the need to provide derived classes with a default constructor, by fitting my abstract base class with a protected default constructor. Since abstract classes must be inherited, and protected methods come along for the ride, derived classes effectively inherit a do-nothing default constructor.,

Periodically, I have debated whether I should add a public GetTheSingleInstance method, although the static TheOnlyInstance property essentially fills its role a lot more cheaply. So far, I have concluded that any GetTheSingleInstance method that I might write into the base class would almost certainly be overridden by the derived class. Note: Though whether it was part of the initial implementation or was added later is lost to history, the current implementation of GenericSingletonBase in the WizardWrx .NET API class libraries has a public GetTheSingleInstance method.

Points of Interest

Since I've already covered most of the interesting aspects of the code, I'll use this section to tie up a few loose ends.

Disabling the Visual Studio Hosting Process

Near the beginning of this article, I promised that I would show how to disable the Visual Studio Hosting Process. Like so many things about using Microsoft tools, it's quite simple,, once you know where to look. Display the project Properties by selecting the last item on the Project menu, or by way of its accelerator key, which is ALT-F7, unless you've changed it. Since this page isn't especially busy, it is usually completely visible, unless you have seriously shrunk the main Visual Studio window, and you should see the Visual Studio hosting process setting at the bottom of the page. Figure 5 shows the default values, while Figure 6 shows the hosting process disabled.

As is true with anything else in these property sheets, changing this setting makes the property pages "dirty," and forces a complete project build. Since the setting is stored in the project configuration file, you can also expect your .CPROJ or .VBPROJ file to be updated. If the configuration file is under source code control, it will be checked out for editing, using the default locking rule.

Figure 5

Figure 5 shows the default settings on the Debug tab of a project property sheet. Note the check mark labeled "Enable the Visual Studio hosting process."

Figure 6

Figure 6 shows the Debug tab of a project property sheet with the Visual Studio hosting process disabled.

Figure 7 shows the last part of the display as it appears when you run the project with the Visual Studio hosting process enabled. This single picture demonstrates every issue that I mentioned at the top of this article. Not only does this picture demonstrate all of the issues that I raised, but it demonstrates the solution, in the top two rows of text, which reports from the default application domain.

Figure 7

Figure 7 shows the last part of the console output generated by the demonstration program. Note the disparity between the Process Startup Time and the Current Machine Time. This is due to the fact that Visual Studio ran for over an hour before I tapped the F5 key to generate the output shown here.

Figure 8

Figure 8 shows the last part of the console output generated by the demonstration program when it runs with the Visual Studio hosting process disabled. The two time stamps are almost identical, the program name is the name you would expect to see, and the Windows Subsystem ID is 3, Image runs in the Windows character subsystem.

There is a tad more output than will fit on a standard 24-line screen, but not so much that you cannot easily capture all of it by way of the Control menu. Alt-Space bar, E, S, Enter, captures everything,, without leaving the keyboard. The control menu and its edit fly-out menu are shown in Figure 9. Creating this picture required some screen capturing and image editing gymnastics, aided by my stalwart screen capture and image editing tool, JASC Paint Shop Pro 7.02. The trick was to set a timer, and tell it to capture the whole desktop when it expired. The image shown below was created by cropping the desktop image.

Figure 9

Figure 9 shows the context menu and its Edit fly-out menu, which offers the fastest way to capture all of the output from the demonstration program. I used the screen capture timer in Paint Shop Pro, for the first time ever, to allow enough time for me to set up the picture.

Tracing

This project has a TextWriterTraceListener configured with its output going to an unqualified file, VSHostingProcess_Demo.LOG, which is created in the build output directory, which is also the working directory used by the program. Somewhat to my surprise, the System.Diagnostics.TextWriterTraceListener class treats the directory from which the assembly loaded as its current working directory, which could lead to some nasty surprises when you put a new utility program into production.

Zany Stuff

The article template suggests that you say something about anything zany that you did, so here goes.

  1. When I configured the trace listener, I decided to see whether I could use MSBuild macros to set the output file name. Apparently not, or, at the very least, not without writing a custom build tool to transform the app.config file in the source code directory into VSHostingProcess_Demo.exe.config in the output directory. Oh, well, it was worth a try.
  2. Though not exactly zany, pulling most of the output text from string resources is overkill for a demonstration program. However, since the PESubsystemInfo lookup tables and error messages were already coming from resources, I decided that I might as well put everything else in there.
  3. Speaking of PESubsystemInfo, the first exception report in GetPESubsystemID uses a ternary expression to deliver separate exception messages for a null reference and the empty string, making the code that evaluates whether pstrFileName is either a null reference or the empty string very compact.
  4. I still use my modified Hungarian notation, and I stand by my choice, for two reasons.
    • Scope. The first character of the name identifies it as a parameter. I use two other unconventional prefixes, and one common one, s, for static, underscore for private, and a for array. You'll see all three in this sample.
    • Type. Everything up to the first upper case character succinctly identifies the type, without having to chase down the definition.

That's about it. Happy debugging!

History

Monday, 03 April 2017 - Article published.

Monday, 03 April 2017 - Restore pictures lost by CP sysops.

Monday, 12 June 2017 - Add links to two of the three articles by Matt Pietrek that I cited, giving credit to Allan Winston for finding them, and make a few cosmetic cleanups in the text.

Sunday, 01 August 2021 - Replace the first article cited in the coveage of the GenericSingletonBase class with a citation to the CodeProject article that covers essentially the same material, add an historical note about the demise of the Visual Studio Hosting Process beginning with Visual Studio 2017, and correct a typographical error or two.

License

This article, along with any associated source code and files, is licensed under The BSD License