Problem Generating Html Report Using DbUp during Octopus Deployment - octopus-deploy

Using Octopus Deploy to deploy a simple API.
The first step of our deployment process is to generate an HTML report with the delta of the scripts run vs the scripts required to run. I used this tutorial to create the step.
The relevant code in my console application is:
var reportLocationSection = appConfiguration.GetSection(previewReportCmdLineFlag);
if (reportLocationSection.Value is not null)
{
// Generate a preview file so Octopus Deploy can generate an artifact for approvals
try
{
var report = reportLocationSection.Value;
var fullReportPath = Path.Combine(report, deltaReportName);
Console.WriteLine($"Generating upgrade report at {fullReportPath}");
upgrader.GenerateUpgradeHtmlReport(fullReportPath);
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
return operationError;
}
}
The Powershell which I am using in the script step is:
# Get the extracted path for the package
$packagePath = $OctopusParameters["Octopus.Action.Package[DatabaseUpdater].ExtractedPath"]
$connectionString = $OctopusParameters["Project.Database.ConnectionString"]
$reportPath = $OctopusParameters["Project.HtmlReport.Location"]
Write-Host "Report Path: $($reportPath)"
$exeToRun = "$($packagePath)\DatabaseUpdater.exe"
$generatedReport = "$($reportPath)\UpgradeReport.html"
Write-Host "Generated Report: $($generatedReport)"
if ((test-path $reportPath) -eq $false){
New-Item "Creating new directory..."
} else {
New-Item "Directory already exists."
}
# Run this .NET app, passing in the Connection String and a flag
# which tells the app to create a report, but not update the database
& $exeToRun --connectionString="$($connectionString)" --previewReportPath="$($reportPath)"
New-OctopusArtifact -Path "$($generatedReport)"
The error reported by Octopus is:
'Could not find file 'C:\DeltaReports\Some API\2.9.15-DbUp-Test-9\UpgradeReport.html'.'
I'm guessing that is being thrown when this powershell line is hit: New-OctopusArtifact ...
And that seems to indicate that the report was never created.
I've used a bit of logging to log out certain variables and the values look sound:
Report Path: C:\DeltaReports\Some API\2.9.15-DbUp-Test-9
Generated Report: C:\DeltaReports\Some API\2.9.15-DbUp-Test-9\UpgradeReport.html
Generating upgrade report at C:\DeltaReports\Some API\2.9.15-DbUp-Test-9\UpgradeReport.html
As you can see in the C#, the relevant code is wrapped in a try/catch block, but I'm not sure whether the error is being written out there or at a later point by Octopus (I'd need to do a pull request to add a marker in the code).
Can anyone see a way forward win resolving this? Has anyone else encountered this?
Cheers

I recently redid some of the work from that article for this video up on YouTube. I did run into some issues with the .SQL files not being included in the assembly. I think it was after I upgraded to .NET 6. But that might be a coincidence.
Anyway, because the files weren't being included in the assembly, when I ran the command line app via Octopus, it wouldn't properly generate the file for me. I ended up configuring the project to copy the .SQL files to a folder in the output directory instead of embedding them in the assembly. You can view a sample package here.
One thing that helped me is running the app in a debugger with the same parameters just to make sure it was actually generating the file. I'm sure you already thought of that, but I'd be remiss if I forgot to include it in my answer. :)
FWIW, this is my updated scripts.
First, the Octopus Script:
$packagePath = $OctopusParameters["Octopus.Action.Package[Trident.Database].ExtractedPath"]
$connectionString = $OctopusParameters["Project.Connection.String"]
$environmentName = $OctopusParameters["Octopus.Environment.Name"]
$reportPath = $OctopusParameters["Project.Database.Report.Path"]
cd $packagePath
$appToRun = ".\Octopus.Trident.Database.DbUp"
$generatedReport = "$reportPath\UpgradeReport.html"
& $appToRun --ConnectionString="$connectionString" --PreviewReportPath="$reportPath"
New-OctopusArtifact -Path "$generatedReport" -Name "$environmentName.UpgradeReport.html"
My C# code can be found here but for ease of use, you can see it all here (I'm not proud of how I parse the parameters).
static void Main(string[] args)
{
var connectionString = args.FirstOrDefault(x => x.StartsWith("--ConnectionString", StringComparison.OrdinalIgnoreCase));
connectionString = connectionString.Substring(connectionString.IndexOf("=") + 1).Replace(#"""", string.Empty);
var executingPath = Assembly.GetExecutingAssembly().Location.Replace("Octopus.Trident.Database.DbUp", "").Replace(".dll", "").Replace(".exe", "");
Console.WriteLine($"The execution location is {executingPath}");
var deploymentScriptPath = Path.Combine(executingPath, "DeploymentScripts");
Console.WriteLine($"The deployment script path is located at {deploymentScriptPath}");
var postDeploymentScriptsPath = Path.Combine(executingPath, "PostDeploymentScripts");
Console.WriteLine($"The deployment script path is located at {postDeploymentScriptsPath}");
var upgradeEngineBuilder = DeployChanges.To
.SqlDatabase(connectionString, null)
.WithScriptsFromFileSystem(deploymentScriptPath, new SqlScriptOptions { ScriptType = ScriptType.RunOnce, RunGroupOrder = 1 })
.WithScriptsFromFileSystem(postDeploymentScriptsPath, new SqlScriptOptions { ScriptType = ScriptType.RunAlways, RunGroupOrder = 2 })
.WithTransactionPerScript()
.LogToConsole();
var upgrader = upgradeEngineBuilder.Build();
Console.WriteLine("Is upgrade required: " + upgrader.IsUpgradeRequired());
if (args.Any(a => a.StartsWith("--PreviewReportPath", StringComparison.InvariantCultureIgnoreCase)))
{
// Generate a preview file so Octopus Deploy can generate an artifact for approvals
var report = args.FirstOrDefault(x => x.StartsWith("--PreviewReportPath", StringComparison.OrdinalIgnoreCase));
report = report.Substring(report.IndexOf("=") + 1).Replace(#"""", string.Empty);
if (Directory.Exists(report) == false)
{
Directory.CreateDirectory(report);
}
var fullReportPath = Path.Combine(report, "UpgradeReport.html");
if (File.Exists(fullReportPath) == true)
{
File.Delete(fullReportPath);
}
Console.WriteLine($"Generating the report at {fullReportPath}");
upgrader.GenerateUpgradeHtmlReport(fullReportPath);
}
else
{
var result = upgrader.PerformUpgrade();
// Display the result
if (result.Successful)
{
Console.ForegroundColor = ConsoleColor.Green;
Console.WriteLine("Success!");
}
else
{
Console.ForegroundColor = ConsoleColor.Red;
Console.WriteLine(result.Error);
Console.WriteLine("Failed!");
}
}
}
I hope that helps!

After long and detailed investigation, we discovered the answer was quite obvious.
We assumed the existing deploy process configuration was sound. Because we never had a problem with it (until now). As it transpires, there was a problem which led to the Development deployments being deployed twice.
Hence, the errors like the one above and others which talked about file handles being held by another process.
It was actually obvious in hindsight, but we were blind to it as we thought the existing process was sound 😣

Related

InDesign Server Crashing on Data Merge

I'm trying to do a very basic data merge with InDesign Server and keep getting a crash.
I begin the server with ./InDesignServer -port 18383 starts with no problems.
I call the script with ./sampleclient ./scripts/test.jsx
The .jsx looks like this:
var source = File("/Users/me/Desktop/InDesign Server/example/example.indd")
var destination = File("/Users/me/Desktop/InDesign Server/example/example.pdf")
var sourceData = File("/Users/me/Desktop/InDesign Server/example/example.csv")
var doc = app.open(source);
doc.dataMergeProperties.selectDataSource(sourceData);
doc.dataMergeProperties.dataMergePreferences.recordNumber = 1;
doc.dataMergeProperties.mergeRecords(); // <-- Crashes here
var myPDFExportPreset = app.pdfExportPresets.item(0);
app.documents.item(0).exportFile(ExportFormat.pdfType, destination, false, myPDFExportPreset);
app.documents.item(0).close(SaveOptions.no);
doc.close(SaveOptions.no);
InDesign Server responds with:
Tue Sep 18 09:48:21 2018 INFO [javascript] Executing Script
./InDesignServer: line 13: 30363 Segmentation fault: 11 "$installed_name" "$#"
And crashes. This script runs perfectly fine in InDesign CC Desktop. Server appears to crash on the .mergeRecords() call. Any ideas why?
Edit: I've modified the code to 1) Have no spaces in the file path 2) check that my objects all exist before performing the merge.
var source = File("/Users/me/Desktop/example/example.indd");
var destination = File("/Users/me/Desktop/example/example.pdf");
var sourceData = File("/Users/me/Desktop/example/example.csv");
var doc = app.open(source);
doc.dataMergeProperties.selectDataSource(sourceData);
if (source.exists && destination.exists && sourceData.exists) {
try {
app.consoleout("Performing merge...");
doc.dataMergeProperties.mergeRecords(); // <-- Crashes here
} catch (err) {
app.consoleout(err);
}
} else {
app.consoleout("Something doesn't exist...");
}
It logs "Performing merge..." so my file paths do in fact point to files that exist. What's more, it full on crashes, and does not report any errors.
Edit 2:
It should be noted, this is the error the Terminal window which launched sampleclient gets from IDS: Error -1 fault: SOAP-ENV:Client [no subcode]
"End of file or no input: Operation interrupted or timed out"
Detail: [no detail]
The folks at Adobe took notice, and fixed this issue for the 2019 release of InDesign Server. The same script, with a similar merging document, no longer produces the error.
So, for a solution, update to 2019.
More information:
Adobe Forums Post
Found a solution, if others find themselves in my situation.
Still a mystery why mergeRecords() seems broken in Server.
doc.dataMergeProperties.exportFile()
Props to Colecandoo: https://forums.adobe.com/thread/2478708
My code is now:
var source = File("/Users/me/Desktop/example/example.indd");
var destination = File("/Users/me/Desktop/example/example.pdf");
var sourceData = File("Macintosh HD:Users:me:Desktop:example:example.csv");
var doc = app.open(source);
var myExport = File(doc.filePath + "/" + doc.name.split(".indd")[0] + ".pdf");
doc.dataMergeProperties.dataMergePreferences.recordNumber = 3;
with (doc.dataMergeProperties.dataMergePreferences) {
recordSelection = RecordSelection.ONE_RECORD;
}
app.dataMergeOptions.removeBlankLines = true;
doc.dataMergeProperties.exportFile(myExport, "[High Quality Print]", );
Still takes some tweaking, but it's performing the merge - this is what I needed.

Teamcity Launch Test on build result

I have two build steps
in second step I am launching some UI Test that looks like:
DesiredCapabilities appCapabilities = new DesiredCapabilities();
appCapabilities.SetCapability("app", #"ViewBrowser/bin/Debug/ViewBrowser.exe");
appCapabilities.SetCapability("deviceName", "WPF");
var includeController = new WindowsDriver<WindowsElement>(new Uri(#"http://127.0.0.1:4723"), appCapabilities);
includeController.Manage().Window.Position = new System.Drawing.Point(20, 20);
includeController.CloseApp();
But in my code I need to specify #"ViewBrowser/bin/Debug/ViewBrowser.exe". In my case what i need to place there because Test cannot find .exe file. I am new in Teamcity. Can you advice something?
You can store ViewBrowser.exe in the resource folder or in the folder around your project.
and just take the file path. E.g.
string RunningPath = AppDomain.CurrentDomain.BaseDirectory
+ "path/to/folder/ViewBrowser.exe";
or take from resources
string FileName = string.Format("{0}Resources\\ViewBrowser.exe", Path.GetFullPath(Path.Combine(RunningPath, #"path/to/folder")));

uninstalling applications using SCCM SDK

I have been trying to uninstall applications on devices or users using SCCM. I have been successful in creating an application assignment that would install applications, but I haven't been able to get it to uninstall. The code I have been using is:
IResultObject assignment = this.manager.CreateInstance("SMS_ApplicationAssignment");
IResultObject application =
this.manager.GetInstance("SMS_Application.CI_ID=16777339");
assignment["ApplicationName"].StringValue = application["LocalizedDisplayName"].StringValue;
assignment["AssignedCI_UniqueID"].StringValue = application["CI_UniqueID"].StringValue;
assignment["AssignedCIs"].IntegerArrayValue = new[] { application["CI_ID"].IntegerValue};
assignment["AssignmentName"].StringValue = "Deepak's deployment";
assignment["CollectionName"].StringValue = "Deepak's Collection of Devices";
assignment["DisableMomAlerts"].BooleanValue = true;
assignment["NotifyUser"].BooleanValue = false;
assignment["OfferFlags"].IntegerValue = 0;
assignment["DesiredConfigType"].IntegerValue = 1;
assignment["OverrideServiceWindows"].BooleanValue = false;
assignment["RebootOutsideOfServiceWindows"].BooleanValue = false;
assignment["SuppressReboot"].IntegerValue = 0;
assignment["TargetCollectionID"].StringValue = "UKN0000F";
assignment["EnforcementDeadline"].DateTimeValue = DateTime.Now.AddDays(1);
assignment["StartTime"].DateTimeValue = DateTime.Now;
assignment["UseGMTTimes"].BooleanValue = false;
assignment["UserUIExperience"].BooleanValue = false;
assignment["WoLEnabled"].BooleanValue = false;
assignment["RequireApproval"].BooleanValue = true;
assignment["OfferTypeId"].IntegerValue = 2;
assignment.Put();
This code will put up the application as an install deployment in SCCM. How do I get it as an uninstall deployment?
There is an AppAction enumeration, which I suspect is used by the client and not on the server.
typedef enum AppAction
{
appDiscovery = 0,
appInstall = 1,
appUninstall = 2
} AppAction;
Any help would be appreciated!
The setting that needs to be changed is DesiredConfigType.
For your code add the following before put():
assignment["DesiredConfigType"].IntegerValue = 2;
A value of 1 represents install (required) and 2 will uninstall (not allowed).
https://msdn.microsoft.com/en-us/library/hh949014.aspx
The way I do it is first use uninstall.exe to determine the guid of the program, and then create a program for the package I wish to uninstall, and just call uninstall.exe /whatever as the command. This works for most apps that show up in Add/Remove, and if it doesn't show up there then it'll have to be a hack (or script) anyway to uninstall. I believe the reason you're falling short is because if there is no command to uninstall the deployment in sccm, then it has nothing to run.
After you create an uninstall program, you could just call that deployment from your code, and voila.
As long as the target program that you are trying to use was installed via an MSI (Microsoft Installer) then you can loop through the registry to find your product (Registry Location: "HKEY_LOCAL_MACHINE\Software\Microsoft\Windows\CurrentVersion\Uninstall") And just look at each DisplayName value.
In our environment, I accomplish this task by using Powershell, and we setup a program that specifically uninstalls whatever we are after.
Hope this helps...
Raged.

Find absolute java.exe path programmatically from java code

If I have a java jar or class file which is launched by the user (assuming java path is set in environment variables), so how can i from within the code, figure out absolute path of java.exe/javaw.exe from which this file is being launched.
Like on ubuntu we can run: % which java and it shows the path.
However on windows, if i check System.getenv() it may happen that there are multiple path's found e.g for old or new version. If through cmd line, I run java -version it does not show the path.
Can you tell me either through pure java or command line on windows how is it possible to find out the location of javaw.exe?
String javaHome = System.getProperty("java.home");
Can you tell me either through pure Java ... on windows how is it possible to find out the location of javaw.exe?
E.G.
import java.io.File;
class JavawLocation {
public static void main(String[] args) {
String javaHome = System.getProperty("java.home");
File f = new File(javaHome);
f = new File(f, "bin");
f = new File(f, "javaw.exe");
System.out.println(f + " exists: " + f.exists());
}
}
Output
C:\Program Files (x86)\Java\jdk1.6.0_29\jre\bin\javaw.exe exists: true
Press any key to continue . . .
And yes, I am confident that will work in a JRE.
On Windows, the java.library.path System Property begins with the path to the bin directory containing whichever java.exe was used to run your jar file.
This makes sense, because on Windows the first place any executable looks for DLL files is the directory containing the executable itself. So naturally, when the JVM runs, the first place it looks for DLLs is the directory containing java.exe.
You can acquire the path to java.exe as follows:
final String javaLibraryPath = System.getProperty("java.library.path");
final File javaExeFile = new File(
javaLibraryPath.substring(0, javaLibraryPath.indexOf(';')) + "\\java.exe"
);
final String javaExePath =
javaExeFile.exists() ? javaExeFile.getAbsolutePath() : "java";
This code is Windows-specific - I hard-coded the path separator (;) and the file separator (). I also put in a fallback to just "java" in case the library path trick somehow doesn't work.
I have tested this with Java 6 and 7 on Windows 7. I tried a 32-bit and 64-bit version of Java.
Here's a slightly more generalised solution that I came up with. Maybe useful:
private static String javaExe()
{
final String JAVA_HOME = System.getProperty("java.home");
final File BIN = new File(JAVA_HOME, "bin");
File exe = new File(BIN, "java");
if (!exe.exists())
{
// We might be on Windows, which needs an exe extension
exe = new File(BIN, "java.exe");
}
if (exe.exists())
{
return exe.getAbsolutePath();
}
try
{
// Just try invoking java from the system path; this of course
// assumes "java[.exe]" is /actually/ Java
final String NAKED_JAVA = "java";
new ProcessBuilder(NAKED_JAVA).start();
return NAKED_JAVA;
}
catch (IOException e)
{
return null;
}
}
an issue with using "System.getProperty("java.home");", is that it is not always the java exe that the jar is running on, if you want to get that, you can use "System.getProperty("sun.boot.library.path");", from there you can find "java", "java.exe", "javaw", or "javaw.exe"... However there is still an issue with this, java will run just fine if the executable has been renamed, and the actual java executable's structure changes from different JRE's/JDKS's, so there is not much way to find the java exe if it has been renamed. unless someone else has a method ofc, in which case, can you share? :)
(Also, I have seen some people suggest using the first index of System.getProperty("java.library.path");, note, this might not work if the user/launcher has manually set the library path, something which is not too uncommon)
Compilation of All above methods
static String getJavaPath(){
String tmp1 = System.getProperty("java.home") + "\\bin\\java.exe";
String tmp2 = System.getProperty("sun.boot.library.path") + "\\java.exe";
String tmp3 = System.getProperty("java.library.path")+ "\\java.exe";
if(new File(tmp1).exists()) {
return tmp1;
}else if(new File(tmp2).exists()){
return tmp2;
}else if(new File(tmp3).exists()) {
return tmp3;
}else{
String[] paths = System.getenv("PATH").split(";");
for(String path:paths){
if(new File(path + "\\java.exe").exists()){
return path + "\\java.exe";
}
}
}
return "";
}

How to get the installation directory?

The MSI stores the installation directory for the future uninstall tasks.
Using the INSTALLPROPERTY_INSTALLLOCATION property (that is "InstallLocation") works only the installer has set the ARPINSTALLLOCATION property during the installation. But this property is optional and almost nobody uses it.
How could I retrieve the installation directory?
Use a registry key to keep track of your install directory, that way you can reference it when upgrading and removing the product.
Using WIX I would create a Component that creates the key, right after the Directy tag of the install directory, declaration
I'd use MsiGetComponentPath() - you need the ProductId and a ComponentId, but you get the full path to the installed file - just pick one that goes to the location of your installation directory. If you want to get the value of a directory for any random MSI, I do not believe there is an API that lets you do that.
I would try to use Installer.OpenProduct(productcode). This opens a session, on which you can then ask for Property("TARGETDIR").
Try this:
var sPath = this.Context.Parameters["assemblypath"].ToString();
As stated elsewhere in the thread, I normally write a registry key in HKLM to be able to easily retrieve the installation directory for subsequent installs.
In cases when I am dealing with a setup that hasn't done this, I use the built-in Windows Installer feature AppSearch: http://msdn.microsoft.com/en-us/library/aa367578(v=vs.85).aspx to locate the directory of the previous install by specifying a file signature to look for.
A file signature can consist of the file name, file size and file version and other file properties. Each signature can be specified with a certain degree of flexibility so you can find different versions of the the same file for instance by specifying a version range to look for. Please check the SDK documentation: http://msdn.microsoft.com/en-us/library/aa371853(v=vs.85).aspx
In most cases I use the main application EXE and set a tight signature by looking for a narrow version range of the file with the correct version and date.
Recently I needed to automate Natural Docs install through Ketarin. I could assume it was installed into default path (%ProgramFiles(x86)%\Natural Docs), but I decided to take a safe approach. Sadly, even if the installer created a key on HKLM\Software\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall, none of it's value lead me to find the install dir.
The Stein answer suggests AppSearch MSI function, and it looks interesting, but sadly Natural Docs MSI installer doesn't provide a Signature table to his approach works.
So I decided to search through registry to find any reference to Natural Docs install dir, and I find one into HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Installer\UserData\S-1-5-18\Components key.
I developed a Reg Class in C# for Ketarin that allows recursion. So I look all values through HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Installer\UserData\S-1-5-18\Components and if the Main application executable (NaturalDocs.exe) is found into one of subkeys values, it's extracted (C:\Program Files (x86)\Natural Docs\NaturalDocs.exe becomes C:\Program Files (x86)\Natural Docs) and it's added to the system environment variable %PATH% (So I can call "NaturalDocs.exe" directly instead of using full path).
The Registry "class" (functions, actually) can be found on GitHub (RegClassCS).
System.Diagnostics.ProcessStartInfo startInfo = new System.Diagnostics.ProcessStartInfo("NaturalDocs.exe", "-h");
startInfo.UseShellExecute = false;
startInfo.CreateNoWindow = true;
var process = System.Diagnostics.Process.Start (startInfo);
process.WaitForExit();
if (process.ExitCode != 0)
{
string Components = #"SOFTWARE\Microsoft\Windows\CurrentVersion\Installer\UserData\S-1-5-18\Components";
bool breakFlag = false;
string hKeyName = "HKEY_LOCAL_MACHINE";
if (Environment.Is64BitOperatingSystem)
{
hKeyName = "HKEY_LOCAL_MACHINE64";
}
string[] subKeyNames = RegGetSubKeyNames(hKeyName, Components);
// Array.Reverse(subKeyNames);
for(int i = 0; i <= subKeyNames.Length - 1; i++)
{
string[] valueNames = RegGetValueNames(hKeyName, subKeyNames[i]);
foreach(string valueName in valueNames)
{
string valueKind = RegGetValueKind(hKeyName, subKeyNames[i], valueName);
switch(valueKind)
{
case "REG_SZ":
// case "REG_EXPAND_SZ":
// case "REG_BINARY":
string valueSZ = (RegGetValue(hKeyName, subKeyNames[i], valueName) as String);
if (valueSZ.IndexOf("NaturalDocs.exe") != -1)
{
startInfo = new System.Diagnostics.ProcessStartInfo("setx", "path \"%path%;" + System.IO.Path.GetDirectoryName(valueSZ) + "\" /M");
startInfo.Verb = "runas";
process = System.Diagnostics.Process.Start (startInfo);
process.WaitForExit();
if (process.ExitCode != 0)
{
Abort("SETX failed.");
}
breakFlag = true;
}
break;
/*
case "REG_MULTI_SZ":
string[] valueMultiSZ = (string[])RegGetValue("HKEY_CURRENT_USER", subKeyNames[i], valueKind);
for(int k = 0; k <= valueMultiSZ.Length - 1; k++)
{
Ketarin.Forms.LogDialog.Log("valueMultiSZ[" + k + "] = " + valueMultiSZ[k]);
}
break;
*/
default:
break;
}
if (breakFlag)
{
break;
}
}
if (breakFlag)
{
break;
}
}
}
Even if you don't use Ketarin, you can easily paste the function and build it through Visual Studio or CSC.
A more general approach can be taken using RegClassVBS that allow registry key recursion and doesn't depend on .NET Framework platform or build processes.
Please note that the process of enumerating the Components Key can be CPU intense. The example above has a Length parameter, that you can use to show some progress to the user (maybe something like "i from (subKeysName.Length - 1) keys remaining" - be creative). A similar approach can be taken in RegClassVBS.
Both classes (RegClassCS and RegClassVBS) have documentation and examples that can guide you, and you can use it in any software and contribute to the development of them making a commit on the git repo, and (of course) opening a issue on it's github pages if you find any problem that you couldn't resolve yourself so we can try to reproduce the issue to figure out what we can do about it. =)

Resources