Multiple InfoPath interop automation instances
Asked Answered
E

2

7

I am trying to automate multiple parallel instances of Office InfoPath 2010 via a windows service. I understand automating Office from a service is not supported however it is a requirement of my customer.

I can automate other Office applications in a parallel fashion, however InfoPath behaves differently.

What I have found is that there will only ever be one instance of the INFOPATH.EXE process created, no matter how many parallel calls to CreateObject("InfoPath.Application") are made. In contrast to this, multiple instances of WINWORD.EXE can be created via the similar mechanism CreateObject("Word.Application")

To reproduce this issue, a simple console application can be used.

static void Main(string[] args) {
    // Create two instances of word in parallel
    ThreadPool.QueueUserWorkItem(Word1);
    ThreadPool.QueueUserWorkItem(Word2);

    System.Threading.Thread.Sleep(5000);

    // Attempt to create two instances of infopath in parallel
    ThreadPool.QueueUserWorkItem(InfoPath1);
    ThreadPool.QueueUserWorkItem(InfoPath2);
}

static void Word1(object context) {
    OfficeInterop.WordTest word = new OfficeInterop.WordTest();
    word.Test();
}

static void Word2(object context) {
    OfficeInterop.WordTest word = new OfficeInterop.WordTest();
    word.Test();
}

static void InfoPath1(object context) {
    OfficeInterop.InfoPathTest infoPath = new OfficeInterop.InfoPathTest();
    infoPath.Test();
}

static void InfoPath2(object context) {
    OfficeInterop.InfoPathTest infoPath = new OfficeInterop.InfoPathTest();
    infoPath.Test();
}

The InfoPathTest and WordTest classes (VB) are in another project.

Public Class InfoPathTest
    Public Sub Test()
        Dim ip As Microsoft.Office.Interop.InfoPath.Application
        ip = CreateObject("InfoPath.Application")
        System.Threading.Thread.Sleep(5000)
        ip.Quit(False)
    End Sub
End Class

Public Class WordTest
    Public Sub Test()
        Dim app As Microsoft.Office.Interop.Word.Application
        app = CreateObject("Word.Application") 
        System.Threading.Thread.Sleep(5000)
        app.Quit(False)
    End Sub
End Class

The interop classes simply create the automation objects, sleep and then quit (although in the case of Word, I have completed more complex tests).

When running the console app, I can see (via Task Manager) two WINWORD.EXE processes created in parallel, and only a single INFOPATH.EXE process created. In fact when the first instance of InfoPathTest calls ip.Quit, the INFOPATH.EXE process terminates. When the second instance of InfoPathTest calls ip.Quit, a DCOM timeout exception is thrown - it appears as though the two instances were sharing the same underlying automation object, and that object no longer exists after the first call to ip.Quit.

At this stage my thoughts were only a single INFOPATH.EXE is supported per user login. I expanded the windows service to start two new processes (a console application called InfoPathTest), each running under a different user account. These new processes would then attempt to automate INFOPATH.EXE

Here's where it gets interesting, this actually works, but only on some machines, and I cannot figure out why that is the case.

And the service code (with help from AsproLock):

public partial class InfoPathService : ServiceBase {
    private Thread _mainThread;
    private bool isStopping = false;

    public InfoPathService() {
        InitializeComponent();
    }

    protected override void OnStart(string[] args) {
        if (_mainThread == null || _mainThread.IsAlive == false) {
            _mainThread = new Thread(ProcessController);
            _mainThread.Start();
        }
    }

    protected override void OnStop() {
        isStopping = true;
    }        

    public void ProcessController() {
        while (isStopping == false) {
            try {

                IntPtr hWinSta = GetProcessWindowStation();
                WindowStationSecurity ws = new WindowStationSecurity(hWinSta, System.Security.AccessControl.AccessControlSections.Access);
                ws.AddAccessRule(new WindowStationAccessRule("user1", WindowStationRights.AllAccess, System.Security.AccessControl.AccessControlType.Allow));
                ws.AddAccessRule(new WindowStationAccessRule("user2", WindowStationRights.AllAccess, System.Security.AccessControl.AccessControlType.Allow));
                ws.AcceptChanges();

                IntPtr hDesk = GetThreadDesktop(GetCurrentThreadId());
                DesktopSecurity ds = new DesktopSecurity(hDesk, System.Security.AccessControl.AccessControlSections.Access);
                ds.AddAccessRule(new DesktopAccessRule("user1", DesktopRights.AllAccess, System.Security.AccessControl.AccessControlType.Allow));
                ds.AddAccessRule(new DesktopAccessRule("user2", DesktopRights.AllAccess, System.Security.AccessControl.AccessControlType.Allow));
                ds.AcceptChanges();

                ThreadPool.QueueUserWorkItem(Process1);
                ThreadPool.QueueUserWorkItem(Process2);

            } catch (Exception ex) {
                System.Diagnostics.Debug.WriteLine(String.Format("{0}: Process Controller Error {1}", System.Threading.Thread.CurrentThread.ManagedThreadId, ex.Message));
            }

            Thread.Sleep(15000);
        }
    }

    private static void Process1(object context) {

        SecureString pwd2;

        Process process2 = new Process();
        process2.StartInfo.FileName = @"c:\debug\InfoPathTest.exe";

        process2.StartInfo.UseShellExecute = false;
        process2.StartInfo.LoadUserProfile = true;
        process2.StartInfo.WorkingDirectory = @"C:\debug\";
        process2.StartInfo.Domain = "DEV01";
        pwd2 = new SecureString(); foreach (char c in "password") { pwd2.AppendChar(c); };
        process2.StartInfo.Password = pwd2;
        process2.StartInfo.UserName = "user1";
        process2.Start();

        process2.WaitForExit();
    }

    private static void Process2(object context) {
        SecureString pwd2;

        Process process2 = new Process();
        process2.StartInfo.FileName = @"c:\debug\InfoPathTest.exe";
        process2.StartInfo.UseShellExecute = false;
        process2.StartInfo.LoadUserProfile = true;
        process2.StartInfo.WorkingDirectory = @"C:\debug\";
        process2.StartInfo.Domain = "DEV01";
        pwd2 = new SecureString(); foreach (char c in "password") { pwd2.AppendChar(c); };
        process2.StartInfo.Password = pwd2;
        process2.StartInfo.UserName = "user2";
        process2.Start();

        process2.WaitForExit();
    }

    [DllImport("user32.dll", SetLastError = true)]
    public static extern IntPtr GetProcessWindowStation();

    [DllImport("user32.dll", SetLastError = true)]
    public static extern IntPtr GetThreadDesktop(int dwThreadId);

    [DllImport("kernel32.dll", SetLastError = true)]
    public static extern int GetCurrentThreadId();

}

The InfoPathTest.exe process simply calls the InfoPathTest.Test() method detailed above.

In summary, this works, but only on certain machines. When it fails, the second INFOPATH.EXE process is actually created, but immediately quits with an exitcode of 0. There is nothing in the event logs, nor any exceptions in the code.

I've looked at many things to try and differentiate between working / non working machines, but I'm now stuck.

Any pointers appreciated, especially if you have other thoughts on how to automate multiple InfoPath instances in parallel.

Errol answered 3/5, 2012 at 0:1 Comment(0)
A
1

I'm guessing you'd get similar behavior if you tried to do the same thing with Outlook, which would mean Microsoft thinks it is a bad idea to run multiple copies.

If that is so, I see two options.

Option one is to make your Infopath automation synchronous, running one instance at a time.

Option two, and I have NO idea if it would even work, would be to see if you can launch virtual machines to accomplish youe InfoPath work.

I hope this can at least spark some new train of though that will lead to success.

Afflatus answered 18/5, 2012 at 13:16 Comment(1)
Our current automation approach is synchronous, however for performance reasons we are trying to allow parallel processes.Errol
S
1

I’ve encountered a very similar issue with Outlook. The restriction of allowing only a single instance of the application to be running does not apply per user, but per interactive login session. You may read more about it in Investigating Outlook's Single-Instance Restriction:

Outlook was determining whether or not another instance was already running in the interactive login session. […] During Outlook's initialization, it checks to see if a window named "Microsoft Outlook" with class name "mspim_wnd32" exists, and if so, it assumes that another instance is already running.

There are ways of hacking around it – there is a tool for launching multiple Outlook instances on the Hammer of God site (scroll down) – but they will probably involve intercepting Win32 calls.

As for your code only working on certain machines: That’s probably due to a race condition. If both processes manage to start up fast enough simultaneously, then they won’t detect each other’s window, and assume that they’re the only instance running. However, if the machine is slow, one process would open its window before the other, thereby causing the second process to detect the first process’s window and shut itself down. To reproduce, try introducing a delay of several seconds between launching the first process and the second – this way, only the first process should ever succeed.

Suannesuarez answered 21/5, 2012 at 18:24 Comment(1)
A race condition is something I've already considered. Adding an artificial delay between starting each process (tried increments up to 10 sec) had no effect, on machines where it works, it still worked, on other machines, it still failed. Thanks for the info though, I might try debugging the processes (as per the Outlook investigation) and see if I come up with anything.Errol

© 2022 - 2024 — McMap. All rights reserved.