diff --git a/NAVSCM Library/NavInterface/DevEnvInterface.cs b/NAVSCM Library/NavInterface/DevEnvInterface.cs index 1075ee6..07ed91c 100644 --- a/NAVSCM Library/NavInterface/DevEnvInterface.cs +++ b/NAVSCM Library/NavInterface/DevEnvInterface.cs @@ -4,6 +4,7 @@ using System.Linq; using System.Text; using System.Threading.Tasks; using System.IO; +using System.Diagnostics; using System.Diagnostics.Contracts; namespace NavScm.NavInterface @@ -22,6 +23,49 @@ namespace NavScm.NavInterface { 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. /// @@ -65,6 +109,131 @@ namespace NavScm.NavInterface } } + /// + /// 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. ///