Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

ProcRunner - a generic tool runner add-in for Visual Studio .NET

0.00/5 (No votes)
2 Oct 2002 1  
Runs different tools on a per-project basis. Multiple command lines are supported.

Introduction

Well, after the release of Visual Studio .NET it's hard to say that developing full-featured add-ins is a nightmare. Microsoft has really done a great job in its attempt to provide add-in developers with a more powerful interface to the development environment. Even more functionality is available via the Visual Studio .NET Integration Program. However, the new add-in interface is completely incompatible with the 6.0 one, which makes porting existing add-ins somewhat difficult.

Add-in description

ProcRunner add-in has grown out of my earlier SppMk add-in, which was primarily aimed as a tool for building and unit testing Visual Studio 6.0 projects using GNU make. ProcRunner provides a way for executing different tools in a way similar to that of External Tools in Visual Studio. Unlike External Tools in Visual Studio, ProcRunner allows to group several command lines and run them as a single command in a convenient way - from a popup menu of the project list window. The next figure shows ProcRunner project list window and the output of project's unit tests executed by the nunit-console.exe utility from the NUnit test framework.

The output from the running tool is sent to the "Build" tab of the output window. This allows for easy location of compilation errors, unit test failures, etc. if they are logged in an appropriate manner (i.e. file(line):text).

ProcRunner is configured using a special page of the Visual Studio Options dialog. This page is shown in the next figure.

Each command consists of one or more command lines. Command lines may contain macros, which are constructs of the form $(Name) resolved at runtime. For example, a $(ProjectDir) macro will be replaced by project's full directory name. ProcRunner is able to figure out the correct project directory if multiple projects are selected. Two kinds of commands are supported: commands which are executed once and commands which are executed for every project selected in the project list window.

Add-in implementation

ProcRunner, being a successor of the SppMk add-in, offers similar functionality, but its implementation is significantly different. When developing SppMk, I had to use a lot of undocumented techniques to provide the required features. On the other hand, ProcRunner has an almost 100% 'clear' implementation as it does not make use of hooks or other 'tricky' things. The only exception is the vcspawn utility used to control the processes being executed, as this utility is not documented.

I've chosen to write ProcRunner in C#, as working with the extensibility objects in C++ is not that pleasant.

I've started development of ProcRunner by first porting SppMk to C#. Then I had to completely rewrite most of the code, partly due to changes in the add-in's functionality, and partly due to new extensibility interface. However, the work already done still had great influence, so I always tried first to do something the way it was in SppMk before writing it from scratch.

Running a process from within Visual Studio IDE

Running a process is the main thing ProcRunner does. Besides creating a process with the correct command lines, ProcRunner also waits for its exit and prints its output (both standard output and standard error) to the "Build" tab of the output window. It is possible to stop the running process upon user request.

SppMk used a hook on CreateProcess API to substitute the required command line. The necessary CreateProcess call was made by Visual Studio in response to the "RebuildAll" command. So, my first idea was to make ProcRunner do the same thing. But, with so many supported languages and configurations, build process in Visual Studio .NET is much more complicated than just a single call to the CreateProcess as it is used to be in Visual Studio 6.0. Furthermore, some projects may be built without invoking the CreateProcess API. Next, running an arbitrary tool in a context of a project build is not an obvious thing.

The second idea I had was to run a tool via one of External Tools available under Tools menu of the Visual Studio. The result of choosing one of the tool menu items is again a CreateProcess call which can also be hooked. However, after some investigation I've found that I was unable to hook this CreateProcess call by patching the import table of the importing module, msenv.dll. The reason is this API is not imported from kernel32.dll, as running depends on msenv.dll readily shows. Of course, there were other ways of hooking to consider, but all this was becoming too ugly to proceed.

The final idea regarding running a process from within Visual Studio was just as simple: manually run a process and read its redirected output. It worked fine and did not require any tricks.

Commands for the projects selected in the project list are placed in a temporary batch file and executed in turn. An error in any of the commands aborts the execution. Below is a simple batch file generated for the make command.

@echo off
goto body

:exit
exit %ERRORLEVEL%

:body

make -s clean
if not %ERRORLEVEL%==0 goto exit

make -s
if not %ERRORLEVEL%==0 goto exit


goto exit

The commands run may spawn additional processes, and additional care is required to be able to stop all of them upon user request. Windows does not fully support parent/child relationships between processes, so killing the root process won't kill any of the processes it spawned. Jeffrey Richter describes this in detail in his article. He also provides sample code for a small application which is able to stop its child processes, and mentions the vcspawn utility which is part of the Visual Studio distribution and actually does the same thing. Using vcspawn for running ProcRunner's commands seemed a natural choice.

vcspawn utility is not documented, but it's command line syntax is straightforward and can be derived from its invocations made by Visual Studio. When calling vcspawn, Visual Studio uses at least two variants of its command line syntax.

  • vcspawn -e Event "<command line>"
  • vcspawn -e Event -m [\n~vcecho!<message>]\n<command line>[\n...n]

As can be easily guessed, the -e argument specifies an event handle which will be checked by vcspawn to determine whether it should exit. Once the event is in signaled state, vcspawn will stop all the processes it created and terminate. The second variant of the command line syntax allows to run multiple commands optionally preceded by text messages.

Here's the code that runs a process executing the ProcRunner command.

public void Run(string Filename, 
    string Arguments, bool DeleteAfterRun, 
    IPrint Printer)
{
    if(null == Printer)
        Error.Throw("ProcessRunner::Run(): no printer");
        
    if(IsRunning())
        Error.Throw("ProcessRunner::Run(): already running");
        
    m_StopEvent.Reset();
    string SpawnArgs = " -e " + m_StopEvent.Handle;
    SpawnArgs += " \"" + Filename + "\" " + Arguments;
    
    ProcessStartInfo Info = new ProcessStartInfo();
    Info.FileName               = "vcspawn.exe";
    Info.Arguments              = SpawnArgs;
    Info.WorkingDirectory       = "";
    Info.CreateNoWindow         = true;
    StringDictionary ParentEvnironment = Process.GetCurrentProcess().
        StartInfo.EnvironmentVariables;
    Info.EnvironmentVariables.Clear();
    foreach(string Key in ParentEvnironment.Keys)
    {
        Info.EnvironmentVariables.Add(Key, ParentEvnironment[Key]);    
    }
    Info.UseShellExecute        = false;
    Info.RedirectStandardOutput = true;
    Info.RedirectStandardError  = true;

    Process Proc = Process.Start(Info);
    
    m_StdOutReader = new RedirOutReader(Proc, Printer, 
        Proc.StandardOutput);
    m_StdOutReader.Start();
    
    m_StdErrReader = new RedirOutReader(Proc, Printer, 
        Proc.StandardError);
    m_StdErrReader.Start();            
    
    m_StopWaiter = new ProcessStopWaiter(Proc, Printer, 
        Filename, DeleteAfterRun, m_StopEvent);
    m_StopWaiter.Start();
    Logger.GetInstance().Log(Logger.Level.Info, "ProcessRunner::Run(): "
        "started new process " + Proc.Id);
}

The simple form of vcspawn command line syntax is used. Three threads are started after the process is launched: two for reading its standard output and standard error, and one for printing a message in case a process was terminated by the user request. m_StopEvent is the controlling event whose handle gets passed to the vcspawn process.

There's only one thing worth to mention regarding running a process in such a way. The event passed to the vcspawn utility via the -e parameter must be inheritable so that vcspawn can see it. A process must also be created with the bInheritHandles parameter of CreateProcess set to TRUE. Fortunately, the latter parameter has the desired value when creating a process via the Process class. But, standard framework event objects such as AutoResetEvent are created with inherit flag set to FALSE. To overcome this, I've created a small helper class making a direct call to the CreateEvent API.

[StructLayout(LayoutKind.Sequential)]
class SECURITY_ATTRIBUTES
{
    public int     nLength              = 12; 
    public IntPtr  lpSecurityDescriptor = IntPtr.Zero; 
    public int     bInheritHandle       = 1; 
}

class InheritableEvent
{
    private ManualResetEvent m_Event = null;
    [DllImport("KERNEL32.DLL", 
         EntryPoint="CreateEventW", SetLastError=true,
         CharSet=CharSet.Unicode, ExactSpelling=true,
         CallingConvention=CallingConvention.StdCall)]
    public static extern int CreateEventW(
        SECURITY_ATTRIBUTES lpEventAttributes,
        bool                bManualReset,
        bool                bInitialState,                      
        string              lpName);

    public InheritableEvent(bool ManualReset, bool InitialState)
    {
        m_Event = new ManualResetEvent(InitialState);
        SECURITY_ATTRIBUTES Attrs = new SECURITY_ATTRIBUTES();        
        int EventHandle = CreateEventW(Attrs, 
                             ManualReset, InitialState, null);
        m_Event.Handle = new IntPtr(EventHandle);
    }
    
    public IntPtr Handle
    {
        get { return m_Event.Handle; }
    }
    public bool Set()
    {
        return m_Event.Set();
    }
    
    public bool Reset()
    {
        return m_Event.Reset();
    }
    
    public bool WaitOne(int millisecondsTimeout, bool exitContext)
    {
        return m_Event.WaitOne(millisecondsTimeout, exitContext);
    }
}

Perhaps there's a more clear way to solve this problem, if you have one, please let me know.

Creating a project list window

Again, I was tempted to go the same way as I did when working on SppMk, which is manually creating a project list window. But again, there's a better way to do such things, that is, a CreateToolWindow method of the Windows collection. All you have to do is to pass the progid of the ActiveX control, and it will be created inside a tool window indistinguishable from those native to Visual Studio. Here's the code.

public void OnConnection(
    object application, ext_ConnectMode connectMode, 
    object addInInst, ref System.Array custom)
{
    DteRef.GetInstance().SetDte((_DTE)application);
    
    object ToolWinObj = null;           
    m_AddIn = (AddIn)addInInst;
    Logger.GetInstance().Log(Logger.Level.Info, "Connect::OnConnection(): "
        "creating tool window");
    m_ToolWindow = DteRef.GetInstance().GetDte().Windows.CreateToolWindow(
        m_AddIn, 
        "ProcRunner.ToolWin", 
        "ProcRunner", 
        "{DEE1D905-0014-448b-A62E-ADD588912D53}",
        ref ToolWinObj);
    m_ToolWindow.Visible = true;
    
    m_ToolWin = (ToolWin)ToolWinObj;
    if ((connectMode == Extensibility.ext_ConnectMode.ext_cm_UISetup)
      || (connectMode == Extensibility.ext_ConnectMode.ext_cm_AfterStartup))
    {
        Logger.GetInstance().Log(Logger.Level.Info, 
            "Connect::OnConnection(): registering command(s)");

        try
        {
            AddCommand("Stop", "Stop ProcRunner", 
                "Stop currently executing ProcRunner tool");
        }
        catch(System.Exception ex)
        {
            Error.Process(ex);
        }
    }
}

ProcRunner.ToolWin is the programmatic ID of the project list window, which uses ListView to display a list of projects. The implementation of the project list window is trivial and is not shown here.

Implementing the Options dialog

Extensibility interface allows an add-in to provide its configuration options via additional pages of the Visual Studio Options dialog. These additional pages are ActiveX controls supporting the IDTToolsOptionsPage interface. In fact, options pages created in such a way are completely independent of an add-in itself, so it is possible to create standalone options pages. Options pages are configured by writing the appropriate progid to the registry under the Control value. For example, ProcRunner's option page registry item has the following form.

[HKEY_CURRENT_USER\Software\Microsoft\VisualStudio\
              7.0\AddIns\ProcRunner.Connect\Options\ProcRunner\Settings]
"Control"="ProcRunner.Options"

The implementation of the options page is again quite simple. Below shown is an implementation of the IDTToolsOptionsPage interface responsible for loading and saving of the property values.

public void OnAfterCreated(DTE DTEObject)
{
    try
    {
        m_Commands = Config.CommandList.Load();
        EditImpl.Fill(m_CommandList, new CommandOptionsAdapter(m_Commands));
    }
    catch(Exception ex)
    {
        Error.ProcessWithMessage(ex);
    }
}

public void GetProperties(ref object PropertiesObject)
{
    PropertiesObject = null;
}

public void OnOK()
{
    try
    {
        m_Commands.Save();        
    }
    catch(Exception ex)
    {
        Error.ProcessWithMessage(ex);
    }
}

public void OnCancel(){}
public void OnHelp(){}

Note on shim controls

For both tool windows and option pages implemented in C#, MSDN advises using a shim control to host the 'real' control supplied by the application. However, everything works fine without shim controls (although this was not the case with beta releases of VS .NET). Also, using shim controls may be quite awkward. For example, an option page is instantiated by Visual Studio via its programmatic ID, and the shim must take care of creating an appropriate C# control. This means that the universal shim provided with the MSDN ToolWindow sample cannot be used directly, as there's no one to call it's HostUserControl method.

Add-in stability

ProcRunner is still in alpha stage. Please report all problems found, I will fix them as soon as possible.

Conclusion

Visual Studio .NET provides a very powerful extensibility interface, particularly when compared to that exposed by the earlier versions of Visual Studio. Therefore implementing ProcRunner was a much more easy task than implementing SppMk. However, sometimes too much is hidden under the nice interfaces of the framework classes, and getting finer control over the situation presents a real problem.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here