How do I trap Cancelling of a long-running deferred DTF Custom Action?

166 views Asked by At

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";
1

There are 1 answers

3
David Tarulli On BEST ANSWER

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;
}