using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.IO;
using System.Diagnostics;
using System.Diagnostics.Contracts;
namespace NavScm.NavInterface
{
///
/// Interfaces to the NAV IDE command line interface to export / import objects.
///
///
/// The interface is based on the powershell snippets delivered with NAV.
/// Note, that the devenv does not give any return values. Instead, errors can only
/// be detected by the existance of the log file, which is created only upon errors.
/// Currently, the interface expects to be able to access the database using
/// NTLM Single Sign on. SQL user/pass authentication is not supported.
///
public class DevEnvInterface
{
private static readonly log4net.ILog log = log4net.LogManager.GetLogger(typeof(DevEnvInterface));
///
/// Helper structure to capture the full execution result of finsql.exe.
///
public struct CommandResult
{
///
/// The exit code of finsql.exe. So far this is always 0, tests pending.
///
public readonly int ExitCode;
///
/// True, if the command executed successfully.
///
public readonly bool Success;
///
/// The output produced by the command, as written to navcommandresult.txt by finsql.exe.
///
public readonly string CommandOutput;
///
/// The errormessage if any, as written to the log file passed to finsql.exe. Empty, if
/// the call was successful.
///
public readonly string ErrorMessage;
///
/// Constructs the whole object.
///
/// The exit code of finsql.exe.
/// Flag indicating success.
/// The output produced by the command, as written to navcommandresult.txt by finsql.exe.
/// The errormessage if any, as written to the log file passed to finsql.exe. Empty, if
/// the call was successful.
public CommandResult(int exitCode, bool success, string commandOutput, string errorMessage)
{
this.ExitCode = exitCode;
this.Success = success;
this.CommandOutput = commandOutput;
this.ErrorMessage = errorMessage;
}
}
///
/// Path to the dev env executable.
///
protected string DevEnvPath { get; private set; }
///
/// Hostname of the database server.
///
protected string DatabaseServer { get; private set; }
///
/// Name of the database itself.
///
protected string DatabaseName { get; private set; }
///
/// Create an interface class to a given NAV developer environment.
///
/// Full path and name to finsql.exe (or however you call it).
public DevEnvInterface(string devEnvPath, string databaseServer, string databaseName)
{
Contract.Requires(devEnvPath != "");
Contract.Requires(databaseServer != "");
Contract.Requires(databaseName != "");
Contract.Ensures(File.Exists(DevEnvPath));
Contract.Ensures(databaseName == DatabaseName);
Contract.Ensures(databaseServer == DatabaseServer);
Contract.Ensures(devEnvPath == DevEnvPath);
DevEnvPath = devEnvPath;
if (!File.Exists(DevEnvPath))
throw new InvalidOperationException($"The file {DevEnvPath} was not found.");
DatabaseServer = databaseServer;
DatabaseName = databaseName;
if (log.IsDebugEnabled)
{
log.Debug($"Constructed and attached to DevEnv {DevEnvPath}");
log.Debug($"Using database [{DatabaseName}] on server {DatabaseServer}");
}
}
///
/// Creates a new temporary directory for finsql.exe to store its stuff into it. Creates an
/// unique temporary directory using Path.GetTempPath and Path.GetRandomFileName, which should
/// work easily out of the box. Retries 3 times in case of unexpected errors and aborts afterwards.
///
/// The full path to the created directory, which is empty.
protected string GetNewTempDirectory()
{
// Try a few times. Uniqueness should not be a problem, as GetRandomFileName is cryptographically strong,
// but beware of other errors e.g. during director creation.
// Note, that in theory an race condition is possible here in case GetRandomFileName doesn't behave.
for (int i = 0; i < 3; i++)
{
string tempPath = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName());
log.DebugFormat("GetNewTempDirectoy: Checking against tempPath {0}", tempPath);
if (Directory.Exists(tempPath))
{
log.ErrorFormat("GetNewTempDirectory: The directory {0} did already exist, this is highly unusual, trying again nevertheless...", tempPath);
continue;
}
DirectoryInfo dirInfo;
try
{
dirInfo = Directory.CreateDirectory(tempPath);
}
catch (Exception ex)
{
log.ErrorFormat("GetNewTempDirectory: The directory {0} did not exist but could be created: {1}", tempPath, ex.Message);
log.Debug("GetNewTempDirectory: Exception Details:", ex);
log.Error("GetNewTempDirectory: Trying again nevertheless...");
continue;
}
Contract.Assert(!Directory.EnumerateFileSystemEntries(tempPath).Any());
return tempPath;
}
log.Fatal("GetNewTempDirectory: Could not generate a new temporary directory after three attempts, this is fatal.");
throw new InvalidOperationException("Could not create a new temp directory, this is fatal.");
}
///
/// Executes the given command with finsql.exe. Database access parameters and log file
/// parameters are added automatically and must not be included in the given command.
/// finsql.exe is run synchronously, so this call blocks until whatever you requested
/// from it is done.
///
///
/// Uses GetNewTempDirectory to create a directory to work in. Be aware, that all
/// files stored in it will be deleted unconditionally after execution completes.
/// The directory itself is deleted as well.
/// Appends LogFile, ServerName and Database arguments to the given command.
///
/// The command to execute, excluding any database login information
/// and log file specification. Do not add a trailing comma as well.
/// Full command execution result, see the CommandResult structure for details.
protected CommandResult ExecuteCommand(string command)
{
Contract.Requires(command != "");
string tempPath = GetNewTempDirectory();
string errorLog = $"{tempPath}\\error.log";
string commandOutput = $"{tempPath}\\navcommandresult.txt";
string fullArguments = $"{command},LogFile=\"{errorLog}\",ServerName=\"{DatabaseServer}\",Database=\"{DatabaseName}\"";
log.DebugFormat("ExecuteCommand: Working in {0}", tempPath);
log.InfoFormat("ExecuteCommand: Executing: {0} {1}", DevEnvPath, fullArguments);
// Execute finsql.exe and wait for its exit...
Process process = new Process();
process.StartInfo.FileName = DevEnvPath;
process.StartInfo.Arguments = fullArguments;
process.StartInfo.WindowStyle = ProcessWindowStyle.Hidden;
process.StartInfo.WorkingDirectory = tempPath;
process.StartInfo.CreateNoWindow = true;
Stopwatch sw = new Stopwatch();
if (log.IsDebugEnabled)
{
sw.Start();
}
process.Start();
process.WaitForExit();
if (log.IsDebugEnabled)
{
sw.Stop();
log.Debug($"ExecuteCommand: Execution took: {sw.Elapsed}");
}
// Parse and store finsql.exe result information, be aware, that we are not getting
// Unicode back from finsql.exe, this is CP850 instead. Interesting, all this in the
// current millenia, as CP850 was introduced 1987 with DOS 3.3... Not to mention that
// the current windows ANSI Encoding is ignored as well. (Checked up to NAV 2015).
var finsqlEncoding = Encoding.GetEncoding(850);
CommandResult result;
if (File.Exists(errorLog))
{
result = new CommandResult(process.ExitCode, false, File.ReadAllText(commandOutput, finsqlEncoding), File.ReadAllText(errorLog, finsqlEncoding));
log.ErrorFormat("ExecuteCommand: Command failed with: {0}", result.ErrorMessage);
log.DebugFormat("ExecuteCommand: Command result: {0}", result.CommandOutput);
log.DebugFormat("ExecuteCommand: finsql.exe finished with exit code {0}", process.ExitCode);
}
else
{
result = new CommandResult(process.ExitCode, true, File.ReadAllText(commandOutput, finsqlEncoding), "");
log.Info("ExecuteCommand: Command executed successfully");
log.DebugFormat("ExecuteCommand: Command result: {0}", result.CommandOutput);
}
// Do some cleanup and be done with it.
foreach (var entry in Directory.EnumerateFileSystemEntries(tempPath))
{
log.DebugFormat("ExecuteCommand: Deleting File {0}", entry);
File.Delete(entry);
}
Directory.Delete(tempPath);
Contract.Assert(!Directory.Exists(tempPath));
Contract.Assume(process.HasExited);
return result;
}
///
/// Exports a given NAV object to disk.
///
///
/// The file name must end with .txt, as finsql.exe deduces the export format from the destiation files
/// extension (crap). We have no other option here as to play by these rules.
/// Be aware, that NAV uses some strange mix of CP850 and CP1252 to encode the text files,
/// this is mean stuff here. The call does not try to convert this into something more sensible
/// at this point, especially since the IDE won't be able to handle this properly if you have to
/// work with the files manually. Checked with NAV 2015, YMMV.
/// Check http://forum.mibuso.com/discussion/37078/encoding-of-exported-navision-objects-txt-files
/// for further details about this.
/// The NAV object as taken from the SQL database or from the cache (doesn't matter).
/// The name of the destination file. The file name must end with .txt.
public void Export(NavObject obj, string destinationFileName)
{
Contract.Requires(obj != null);
Contract.Requires(destinationFileName != "");
Contract.Requires(Path.GetExtension(destinationFileName) == ".txt");
// TODO: Skip Unlicensed objects?
string command = $"Command=ExportObjects,File=\"{destinationFileName}\",Filter=\"{obj.GetFilter()}\"";
log.DebugFormat("Export: Build command string: {0}", command);
var result = ExecuteCommand(command);
if (! result.Success)
{
throw new ArgumentException($"Cannot export object {obj.NavType} ID {obj.ID}: {result.ErrorMessage}");
}
}
}
}