0

I've got a Deferred Custom Action DLL written in DTF that publishes a set of .RDL files to the SQL Server Reporting Web Service. All is working well and I can trap most of the error conditions in various Try Catch blocks.

The only thing I am having trouble with is if the user presses the Cancel button in the installer while the publish is happening. It does immediately pop up a message asking if I want to Cancel the install, but if I answer Yes then it throws a message :

Exception of type Microsoft.Deployment.WindowsInstaller.InstallCanceledException was thrown

and just an OK button.

I've tried adding a special Exception handler of

catch (InstallCanceledException ex)
{
}

prior to other exceptions, but it just doesn't seem to capture this one particular exception.

Any suggestions how to handle the InstallCanceledException during a Cancel of a long-running Deferred Custom Action?

The product team looked at using one of the applications but normal users run the applications and they wouldn't necessarily know the web service URL or have permissions to publish the reports to the web service. The installer I have put this in is usually used for running SQL Scripts and I'm adding a second Feature to the installer to Publish the reports. It's actually working too well to abandon it now. Product has seen what I've done already and they love it. The MSI Progress Bar is updating with the name of each report as they are published. The MSI prompts for the URI and user credentials and it already knows what folder the .RDL files are in. I run a Validation on the URI when they click the next button so by the time I run the Deferred action in the Execution Sequence it has a good URI and credentials. I've even gone so far as while the publish is occurring I disconnect from VPN and it fails with a proper error. It is literally only when the user presses Cancel that I can't seem to trap that one, but it is also not a showstopper for this work to go out.

Hiding the Cancel button is not an appropriate option since it is fine if they Cancel at any time.

public static ActionResult PublishSSRSReports(Session session)
    {

        session.Log("Begin PublishSSRSReports");

        bool bFolderExists = false;

        string sCustomActionData;
        sCustomActionData = session["CustomActionData"];

        string INSTALLDIR = Convert.ToString(MsiGetCustomActionDataAttribute(sCustomActionData, "/InstallDir="));
        string SSRSURL = Convert.ToString(MsiGetCustomActionDataAttribute(sCustomActionData, "/SsrsUrl="));
        string USERCREDENTIALS = Convert.ToString(MsiGetCustomActionDataAttribute(sCustomActionData, "/Credentials="));
        string USERNAME = Convert.ToString(MsiGetCustomActionDataAttribute(sCustomActionData, "/Username="));
        string PASSWORD = Convert.ToString(MsiGetCustomActionDataAttribute(sCustomActionData, "/Password="));


        string ReportsFolderPath = INSTALLDIR + "SSRSReports";
        DirectoryInfo directory = new DirectoryInfo(ReportsFolderPath);

        FileInfo[] reports = directory.GetFiles("*.rdl"); //Getting all RDL files

        ResetProgressBar(session, reports.Length);

        CatalogItem[] catalogitem = null;

        using (ReportingService2010 rsc = new ReportingService2010())
        {

            rsc.Url = SSRSURL; 

            if (USERCREDENTIALS == "0")
            {
                rsc.Credentials = System.Net.CredentialCache.DefaultCredentials; //User credential for Reporting Service
                                                                                 //the current logged system user
            }
            if (USERCREDENTIALS == "1")
            {
                string[] userdomain = USERNAME.Split(Convert.ToChar("\\"));
                rsc.Credentials = new System.Net.NetworkCredential(userdomain[1], PASSWORD, userdomain[0]);

            }
            catalogitem = rsc.ListChildren(@"/", false);
            foreach (CatalogItem catalog in catalogitem)
            {
                if (catalog.Name == (DP))
                {
                    EventLog.WriteEntry(AppDomain.CurrentDomain.FriendlyName, DP + " folder already exists");
                    bFolderExists = true;
                }
            }

            if (bFolderExists == false)
            {
                rsc.CreateFolder(DP, @"/", null);
            }

            Warning[] Warnings = null;
            foreach (FileInfo ReportFile in reports)
            {
                Byte[] definition = null;
                Warning[] warnings = null;

                try
                {
                    FileStream stream = ReportFile.OpenRead();
                    definition = new Byte[stream.Length];
                    stream.Read(definition, 0, (int)stream.Length);
                    stream.Close();
                }
                catch (InstallCanceledException ex)
                {
                    //session.Message(InstallMessage.Error, new Record { FormatString = ex.Message });
                    EventLog.WriteEntry(AppDomain.CurrentDomain.FriendlyName, ex.Message);
                    return ActionResult.UserExit;
                }

                catch (IOException ex)
                {
                    session.Message(InstallMessage.Error, new Record { FormatString = ex.Message });
                    EventLog.WriteEntry(AppDomain.CurrentDomain.FriendlyName, ex.Message);
                    return ActionResult.Failure;
                }
                catch (Exception ex)
                {
                    session.Message(InstallMessage.Error, new Record { FormatString = ex.Message });
                    EventLog.WriteEntry(AppDomain.CurrentDomain.FriendlyName, ex.Message);
                    return ActionResult.Failure;
                }

                try
                {
                    CatalogItem report = rsc.CreateCatalogItem("Report", ReportFile.Name, @"/" + DP, true, definition, null, out Warnings);

                    DisplayActionData(session, ReportFile.Name);
                    IncrementProgressBar(session, 1);

                    if (report != null)
                    {
                        EventLog.WriteEntry(AppDomain.CurrentDomain.FriendlyName, ReportFile.Name + " Published Successfully ");
                    }
                    if (warnings != null)
                    {
                        foreach (Warning warning in warnings)
                        {
                            EventLog.WriteEntry(AppDomain.CurrentDomain.FriendlyName, string.Format("Report: {0} has warnings", warning.Message));
                        }
                    }
                    else
                    {
                        EventLog.WriteEntry(AppDomain.CurrentDomain.FriendlyName, string.Format("Report: {0} created successfully with no warnings", ReportFile.Name));
                    }
                }

                catch (InstallCanceledException ex)
                {
                    //session.Message(InstallMessage.Error, new Record { FormatString = ex.Message });
                    EventLog.WriteEntry(AppDomain.CurrentDomain.FriendlyName, ex.Message);
                    return ActionResult.UserExit;
                }

                catch (SoapException ex)
                {
                    session.Message(InstallMessage.Error, new Record { FormatString = ex.Message });
                    EventLog.WriteEntry(AppDomain.CurrentDomain.FriendlyName, ex.Detail.InnerXml.ToString());
                    return ActionResult.Failure;
                }
                catch (Exception ex)
                {
                    session.Message(InstallMessage.Error, new Record { FormatString = ex.Message });
                    EventLog.WriteEntry(AppDomain.CurrentDomain.FriendlyName, ex.Message);
                    return ActionResult.Failure;
                }
            }

        }

        return ActionResult.Success;

I've also got these in the class

private const string SpaceForwardSlash = " /";
    private const string DP = "Test";
TA455HO
  • 3
  • 2
  • Do you need to run this as part of your setup, or can you do this during application launch instead? Much easier debugging and error handling. None of the impersonation-, sequencing-, conditioning- or runtime challenges plaguing custom action implementations. – Stein Åsmul Nov 27 '18 at 23:36
  • If the installation is cancelled while this custom action is running does it cause a specific issue? How is this problematic compared to a cancellation at some other point? – David Tarulli Nov 30 '18 at 14:36
  • @TA455HO Curious if you got this sorted? – Stein Åsmul Dec 01 '18 at 14:01
  • If the install is canceled during the C# code execution I get Exception of type Microsoft.Deployment.WindowsInstaller.InstallCanceledException was thrown, whereas a Cancel at other times does not thrown an Exception. I believe I would have to switch to using C++ so I can use MsiProcessMessage, which doesn't seem to be possible using C# DTF. – TA455HO Dec 01 '18 at 15:09
  • It's certainly possible to call Windows Installer API functions like MsiProcessMessage from C# code with p/invoke. But it's even easier in this case because DTF provides a wrapper for you via Session.Message(). See examples [here](https://stackoverflow.com/questions/11722541/wix-dynamically-changing-the-status-text-during-customaction) and [here](https://stackoverflow.com/questions/16124164/messageboxes-using-dtf). – David Tarulli Dec 01 '18 at 16:22
  • I tested a simple custom action that loops 60 times, writing a message to the log file on each iteration and then sleeping for 1 second. If I cancel while its running I don't get an exception and I see all 60 messages in the log. It appears windows installer waits for the custom action to finish before proceeding with cancellation. Is your code displaying the message box? I don't think DTF does that natively. How are you building the MSI? Can you post your stack trace and custom action code to reproduce the issue? – David Tarulli Dec 02 '18 at 05:36
  • I'm using DTF from WiX for the custom action code, but using InstallShield Premier for the MSI. No MessageBox in my code since it is a DLL it's not allowed. I am using session.Message liberally. I'm not sure how to post the code since it is too long for a comment. Should I edit my original post and put the code there? – TA455HO Dec 02 '18 at 15:18
  • I've added the code above. It does require a Web Reference to a valid SSRS WSDL (SSRSURL variable name) – TA455HO Dec 02 '18 at 19:09
  • We use Installshield as well, but my MSIs seem to behave differently. When I click cancel I don't get a confirmation prompt or a message displaying an exception. The install just starts rolling back immediately (though I can see it waiting for the current custom action to finish). When you add your custom actions in Installshild do you choose "New Managed Code", or "New MSI DLL"? We use "New MSI DLL" so that Windows Installer will think it's an unmanaged C++ dll. I suspect the managed option could be changing the behavior for you. – David Tarulli Dec 02 '18 at 20:32
  • I choose "New MSI DLL" also, making it a Type 1 custom action, but then Deferred sets it to Type 1025. I do like that mine prompt whether or not to Cancel. If I choose No, even after waiting a full day, the publish continues fine. It's only if I choose Yes to the Cancel dialog that I get the Exception. Might be because Microsoft.Deployment.WindowsInstaller.dll has instantiated ReportService2010.dll and it is doing something in the background that is triggering the exception. – TA455HO Dec 03 '18 at 13:00
  • Do you know what's throwing the InstallCanceledException? Do you get a stack trace in your MSI log? It's not clear to me where this exception is coming from or why I don't see it in my projects. It seems like we have things setup pretty much the same way. I'm using WIX 3.11. Maybe you could try attaching the visual studio debugger to your custom action process and step through it while you cancel. You can pop up a message box at the beginning of the CA to give yourself time to attach. Just add a reference to System.Windows.Forms. – David Tarulli Dec 03 '18 at 17:50
  • snippet from log file. I assume the I/O on various threads is causing it. CancelSetup. Dialog created Doing action: ISSetupFilesCleanup I/O on thread 18200 could not be cancelled. Error: 1168 I/O on thread 10084 could not be cancelled. Error: 1168 I/O on thread 15160 could not be cancelled. Error: 1168 I/O on thread 15928 could not be cancelled. Error: 1168 I/O on thread 1508 could not be cancelled. Error: 1168 Exception of type 'Microsoft.Deployment.WindowsInstaller.InstallCanceledException' was thrown. – TA455HO Dec 03 '18 at 19:59

1 Answers1

0

In the DTF source code the only place I see an InstallCanceledException being thrown is in Session.Message(). This is a wrapper for the MsiProcessMessage Windows API function. It looks to me like you would get this exception if you used Session.Message() to display a message box from a managed custom action, and then clicked the 'Cancel' button. DTF sees the message box 'cancel' return code and throws an InstallCanceledException. Perhaps it's then falling into a catch block somewhere (could be a different action?) where you call something similar to

session.Message(InstallMessage.Error, new Record { FormatString = ex.Message })

which displays the second message box containing just the exception.

I can't quite piece everything together 100% without seeing your MSI source or a complete log file, but maybe this will help.

Here's how Session.Message() is defined in the DTF source:

public MessageResult Message(InstallMessage messageType, Record record)
{
    if (record == null)
    {
        throw new ArgumentNullException("record");
    }

    int ret = RemotableNativeMethods.MsiProcessMessage((int) this.Handle, (uint) messageType, (int) record.Handle);
    if (ret < 0)
    {
        throw new InstallerException();
    }
    else if (ret == (int) MessageResult.Cancel)
    {
        throw new InstallCanceledException();
    }
    return (MessageResult) ret;
}
David Tarulli
  • 929
  • 1
  • 10
  • 13
  • David, you nailed it. Once I commented out my own session.Message on my own InstallCanceledException then it started working just as I was hoping. It both prompts if I want to Cancel and does not throw the Exception if I choose Yes. I marked yours as the answer. Thanks for pointing out to me that DTF handles it. – TA455HO Dec 04 '18 at 17:21
  • catch (InstallCanceledException ex) { //session.Message(InstallMessage.Error, new Record { FormatString = ex.Message }); EventLog.WriteEntry(AppDomain.CurrentDomain.FriendlyName, ex.Message); return ActionResult.UserExit; } – TA455HO Dec 04 '18 at 17:32
  • If I remove the return ActionResult.UserExit; then it doesn't work the same so the trapping of the Exception seems important, just don't display your own message if you don't need to. – TA455HO Dec 04 '18 at 17:41