Added framework to call finsql synchronously. Captures all output and handles management of a temporary working directory which is created on the fly in the current GetTempPath location. Any result is read into memory and passed back to the caller.

Added some timing and debugging output to trace calls to finsql.exe.
TODO: Add an optional callback to optionally allow the caller to handle any additional files created in the working directory.
This commit is contained in:
Torben Nehmer 2016-11-23 22:26:36 +01:00
parent 6c0d57303a
commit 98a044e2de

View File

@ -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));
/// <summary>
/// Helper structure to capture the full execution result of finsql.exe.
/// </summary>
public struct CommandResult
{
/// <summary>
/// The exit code of finsql.exe. So far this is always 0, tests pending.
/// </summary>
public int ExitCode;
/// <summary>
/// True, if the command executed successfully.
/// </summary>
public bool Success;
/// <summary>
/// The output produced by the command, as written to navcommandresult.txt by finsql.exe.
/// </summary>
public string CommandOutput;
/// <summary>
/// The errormessage if any, as written to the log file passed to finsql.exe. Empty, if
/// the call was successful.
/// </summary>
public string ErrorMessage;
/// <summary>
/// Constructs the whole object.
/// </summary>
/// <param name="exitCode">The exit code of finsql.exe.</param>
/// <param name="success">Flag indicating success.</param>
/// <param name="output">The output produced by the command, as written to navcommandresult.txt by finsql.exe.</param>
/// <param name="errorMessage">The errormessage if any, as written to the log file passed to finsql.exe. Empty, if
/// the call was successful.</param>
public CommandResult(int exitCode, bool success, string commandOutput, string errorMessage)
{
this.ExitCode = exitCode;
this.Success = success;
this.CommandOutput = commandOutput;
this.ErrorMessage = errorMessage;
}
}
/// <summary>
/// Path to the dev env executable.
/// </summary>
@ -65,6 +109,131 @@ namespace NavScm.NavInterface
}
}
/// <summary>
/// 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.
/// </summary>
/// <returns>The full path to the created directory, which is empty.</returns>
protected string GetNewTempDirectory()
{
Contract.Ensures(Directory.Exists(Contract.Result<string>()));
Contract.Ensures(! Directory.EnumerateFileSystemEntries(Contract.Result<string>()).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.");
}
/// <summary>
/// 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.
/// </summary>
/// <remarks>
/// <para>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.</para>
/// <para>Appends LogFile, ServerName and Database arguments to the given command.</para>
/// </remarks>
/// <param name="command">The command to execute, excluding any database login information
/// and log file specification. Do not add a trailing comma as well.</param>
/// <returns>Full command execution result, see the CommandResult structure for details.</returns>
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;
}
/// <summary>
/// Exports a given NAV object to disk.
/// </summary>