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. /// 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 int ExitCode; /// /// True, if the command executed successfully. /// public bool Success; /// /// The output produced by the command, as written to navcommandresult.txt by finsql.exe. /// public string CommandOutput; /// /// The errormessage if any, as written to the log file passed to finsql.exe. Empty, if /// the call was successful. /// public 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() { Contract.Ensures(Directory.Exists(Contract.Result())); Contract.Ensures(! Directory.EnumerateFileSystemEntries(Contract.Result()).Any()); // 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; } 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(); Contract.Ensures(!Directory.Exists(tempPath)); 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(); Contract.Ensures(process.HasExited); 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 CommandResult result; if (File.Exists(errorLog)) { result = new CommandResult(process.ExitCode, false, File.ReadAllText(commandOutput), File.ReadAllText(errorLog)); 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), ""); 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); return result; } /// /// Exports a given NAV object to disk. /// /// The NAV object as taken from the SQL database or from the cache (doesn't matter). /// The name of the destination file. The system ensures, that the file /// ends with .txt, as finsql.exe deduces the export format from the destiation files extension (crap). public void Export(NavObject obj, string destinationFileName) { Contract.Requires(obj != null); Contract.Requires(destinationFileName != ""); } } }