Mage.exe deployment problems

Nathan Wheeler picture Nathan Wheeler · Feb 22, 2010 · Viewed 7.3k times · Source

I have a config file that needs changed on a per-server basis, so that once a server has our software installed on it, the config file for a client installer is set up to match that server's particular settings, and then copied to a public folder on the web for deployment.

Since I'm changing the config file, I also have to rebuild the *.manifest and *.application files, and as I understand it, my only real option for this is to use Mage.exe from the Win7 SDK. In order to fix the *.manifest file with the proper hash from the modified config file, I run:

mage -new Application -fd ".\Application Files\<appName>_1_0_0_0" -ToFile ".\Application Files\_1_0_0_0\<appName>.exe.manifest" -Name "<appName>" -Version "1.0.0.0" -CertFile "key.pfx" -password "<password>"

and then, to fix the *.application file with the proper hash from the modified *.manifest file, I run:

mage -new Deployment -I t -t "<appName>.application" -v "1.0.0.0" -appManifest ".\Application Files\<appName>_1_0_0_0\<appName>.exe.manifest" -pu "http://<hostaddress>/<path>/Application Files/<appName>_1_0_0_0/<appName>.exe.manifest" -CertFile "key.pfx" -password ""

Now, this all works, and I get the message that the files were successfully signed. When I try to install the client app though, it's obvious that something has gone awry when I get an error log with the message:

+ Deployment manifest is not semantically valid.
+ Deployment manifest requires <deployment> section.

In looking at the *.application file, it has some additional information under the "deployment" node, which the same file directly from the publish feature of VS2008 does not have:

<deployment install="true">
  <subscription>
    <update>
      <expiration maximumAge="0" unit="days" />
    </update>
  </subscription>
  <deploymentProvider codebase="http://<hostaddress>/<path>/Application Files/<appName>_1_0_0_0/<appName>.exe.manifest" />
</deployment>

The VS2008 publish version simply has:

<deployment install="true" />

When I remove the additional information and set the deployment node to a self terminating node, then re-sign the file, everything works as expected.

Is this a known issue and is there any way to get Mage to create the file without the extra information in the deployment node so that it will work properly?

EDIT: As a temporary solution, I am loading the files into an XmlDocument and modifying them to suit, then re-signing the files. Additionally, I'm now facing the issue of being as yet unable to determine how to add an icon to the deployment, so the Start menu item gets an icon other than the generic icon.

Answer

Nathan Wheeler picture Nathan Wheeler · Feb 26, 2010

Here is my implementation. I've spent a lot of time on this little bit of code, and I still haven't found all the right options to have Mage handle all of the generation of the .application file without intervention. I'm going to say there are probably a lot of optimizations that could be made to this code. However, this can still be used as a springboard to help someone.

In order for the following method to work, you have to deploy it at least once from ClickOnce in VS, and then just keep the .application file from that deployment. You MUST delete the .application and .manifest IN the deploy folder.

After I've moved all the application files to Config.Instance.ServerSettings.ClientLocation + "<AppName>_<version>":

DirectoryInfo filedir = new DirectoryInfo(Config.Instance.ServerSettings.ClientLocation);

if (filedir.Exists)
{
    FileInfo[] files = filedir.GetFiles();

    // Find the current .application file.
    FileInfo appinfo = null;
    foreach (FileInfo fi in files)
    {
        if (fi.Name == "<AppName>.application")
        {
            appinfo = fi;
            break;
        }
    }

    if (appinfo != null)
    {
        XmlDocument applocinfo = new XmlDocument();
        applocinfo.Load(appinfo.FullName);

        // Get the location of the files from the .application file.
        string codebase = applocinfo["asmv1:assembly"]["dependency"]["dependentAssembly"].Attributes["codebase"].Value.Replace("AppName.exe.manifest", "");

        XmlDocument xDoc = new XmlDocument();
        xDoc.Load(Path.Combine(Path.Combine(filedir.FullName, codebase), "AppName.exe.config"));

        foreach (XmlNode xn in xDoc["configuration"]["appSettings"].ChildNodes)
        {
            if (xn.Attributes != null && xn.Attributes["key"] != null && xn.Attributes["key"].Value == "Clnt_Host")
            {
                // Here is where I'm modifying my config file, the whole purpose in this wretched deployment process.
                xn.Attributes["value"].Value = Config.Instance.ClientSettings.Host;
                break;
            }
        }

        xDoc.Save(Path.Combine(Path.Combine(filedir.FullName, codebase), "<AppName>.exe.config"));

        Process p = new Process();
        p.StartInfo = new ProcessStartInfo(Path.Combine(filedir.FullName, "Mage.exe"));
        p.StartInfo.WorkingDirectory = filedir.FullName;

        FileInfo fi = new FileInfo(Path.Combine(Path.Combine(filedir.FullName, codebase.TrimStart('.')), "<AppName>.exe.manifest"));
        if (fi.Exists)
            fi.Delete();

        // Write a new .manifest file as an Application file. (-new Application -ToFile ".\codebase\<AppName.exe.manifest")
        // Include the files from the codebase directory in the manifest (-fd ".\codebase\")
        // Give the application a name to use in the start menu (-name "<AppName>")
        // Assign a version number to the deployment (-Version "<version>")
        // Give the application an icon to use in the start menu (-IconFile "64x64.ico")
        // Sign the manifest (-CertFile "<KeyName>.pfx -Password <password>)
        p.StartInfo.Arguments = "-new Application -fd \".\\" + codebase.TrimEnd('\\') + "\" -ToFile \".\\" + Path.Combine(codebase, "<AppName>.exe.manifest") + "\" -Name \"<AppName>\" -Version \"" + codebase.Substring(codebase.IndexOf('_') + 1, codebase.Length - (codebase.IndexOf('_') + 1)).Replace('_', '.').TrimEnd('\\') + "\" -CertFile \"<KeyName>.pfx\" -Password <Password> -IconFile \"64x64.ico\"";

        while (p.StartInfo.Arguments.Contains(".\\.\\"))
            p.StartInfo.Arguments = p.StartInfo.Arguments.Replace(".\\.\\", ".\\");

        Logger.Instance.LogInfo("Starting application: " + p.StartInfo.FileName + "\n\tWith arguments: " + p.StartInfo.Arguments, Logger.InfoType.Information);

        p.Start();

        while (!p.HasExited)
        {
            Thread.Sleep(100);
        }

        // Make a new deployment manifest (-new Deployment -t "<AppName>.application")
        // Make the application available offline (-I t)
        // Use the files from the .manifest we just made (-AppManifest ".\codebase\<AppName>.exe.manifest")
        p.StartInfo.Arguments = "-new Deployment -I t -t \"<AppName>.application\" -v \"" + codebase.Substring(codebase.IndexOf('_') + 1, codebase.Length - (codebase.IndexOf('_') + 1)).Replace('_', '.').TrimEnd('\\') + "\" -AppManifest \".\\" + codebase + "<AppName>.exe.manifest\" -pu \"http://" + Config.Instance.ClientSettings.Host + "/client/" + codebase.Replace('\\', '/') + "<AppName>.exe.manifest\"";

                    while (p.StartInfo.Arguments.Contains(".\\.\\"))
            p.StartInfo.Arguments = p.StartInfo.Arguments.Replace(".\\.\\", ".\\");

        Logger.Instance.LogInfo("Starting application: " + p.StartInfo.FileName + "\n\tWith arguments: " + p.StartInfo.Arguments, Logger.InfoType.Information);

        p.Start();

        while (!p.HasExited)
        {
            Thread.Sleep(100);
        }

        xDoc = new XmlDocument();
        xDoc.Load(Path.Combine(filedir.FullName, "<AppName>.application"));

        // Add to the Deployment manifest (.application) to make the application 
        // have a minimum required version of the current version,and makes a 
        // subscription so that the application will always check for updates before 
        // running.
        if (xDoc["asmv1:assembly"]["deployment"]["subscription"] != null)
        {
            xDoc["asmv1:assembly"]["deployment"].RemoveChild(xDoc["asmv1:assembly"]["deployment"]["subscription"]);
            xDoc["asmv1:assembly"]["deployment"].RemoveChild(xDoc["asmv1:assembly"]["deployment"]["deploymentProvider"]);
            XmlAttribute node = xDoc.CreateAttribute("minimumRequiredVersion");
            node.Value = codebase.Substring(codebase.IndexOf('_') + 1, codebase.Length - (codebase.IndexOf('_') + 1)).Replace('_', '.').TrimEnd('\\');
            xDoc["asmv1:assembly"]["deployment"].Attributes.Append(node);

            xDoc["asmv1:assembly"]["deployment"].InnerXml = "<subscription><update><beforeApplicationStartup /></update></subscription>";
        }

        xDoc.Save(Path.Combine(filedir.FullName, "<AppName>.application"));

        // Sign the deployment manifest (.application) (-Sign "\<AppName>.application" -CertFile "<AppName>.key" -Password <password>
        p.StartInfo.Arguments = "-Sign \"<AppName>.application\" -CertFile \"<AppName>.pfx\" -Password <password>";

        while (p.StartInfo.Arguments.Contains(".\\.\\"))
            p.StartInfo.Arguments = p.StartInfo.Arguments.Replace(".\\.\\", ".\\");

        Logger.Instance.LogInfo("Starting application: " + p.StartInfo.FileName + "\n\tWith arguments: " + p.StartInfo.Arguments, Logger.InfoType.Information);

        p.Start();

        while (!p.HasExited)
        {
            Thread.Sleep(100);
        }
    }
}