From 566d727297424accaf8f1e679756c63ef40e564f Mon Sep 17 00:00:00 2001 From: Torben Nehmer Date: Fri, 25 Nov 2016 22:32:34 +0100 Subject: [PATCH] Tested DevEnv Interface, fixed up several Code Contract errors / misuses. Added Export as an DevEnv interface function, exporting a single object to txt. Builds a filter string and writes the whole stuff out. Works like a breeze. Added Charset conversion for finsql.exe result values (command output and error log), which uses something similar (but not quite) to CP850. For any error message, this should be enough, however, be aware that the format of the TXT files generated by ExportObject is in the same strange encoding. Details can be found here: http://forum.mibuso.com/discussion/37078/encoding-of-exported-navision-objects-txt-files It should be considered if it makes sense to normalize the objects as best as possible into the Unicode plane and convert them back on exit - though this will probably generate a whole different set problems when automating Build jobs. For now, leave it as-is, better safe than sorry. --- .../NAV Source Control Test Host.csproj | 2 +- NAV Source Control Test Host/Program.cs | 23 +++++--- .../NavInterface/DevEnvInterface.cs | 52 +++++++++++++------ NAVSCM Library/NavInterface/NavObject.cs | 23 +++++++- 4 files changed, 76 insertions(+), 24 deletions(-) diff --git a/NAV Source Control Test Host/NAV Source Control Test Host.csproj b/NAV Source Control Test Host/NAV Source Control Test Host.csproj index 226b651..8ce0894 100644 --- a/NAV Source Control Test Host/NAV Source Control Test Host.csproj +++ b/NAV Source Control Test Host/NAV Source Control Test Host.csproj @@ -41,7 +41,7 @@ 4 True False - False + True False False True diff --git a/NAV Source Control Test Host/Program.cs b/NAV Source Control Test Host/Program.cs index 5a895db..8fc71a6 100644 --- a/NAV Source Control Test Host/Program.cs +++ b/NAV Source Control Test Host/Program.cs @@ -29,20 +29,20 @@ namespace NavScm.TestHost log.InfoFormat("{0} total entries in NavSqlObjects", NavSqlObjects.Count()); var query = from sql in NavSqlObjects - where sql.Modified == 1 + where sql.Modified == 1 && sql.Type > 0 select sql; int count = query.Count(); - log.InfoFormat("{0} modified objects detected, reading them...", count); + log.InfoFormat("{0} modified objects detected, reading them into the cache...", count); var foundObjects = new Dictionary(count); - int i = 1; + // int i = 1; foreach (NavObject o in query) { - log.DebugFormat("Row {6}/{7}: Type {0}, ID {1}, Name {2}, Modified {3} {4}, Version {5}", - o.Type, o.ID, o.Name, o.Date.ToShortDateString(), o.Time.ToShortTimeString(), o.Version_List, i++, count); + //log.DebugFormat("Row {6}/{7}: Type {0}, ID {1}, Name {2}, Modified {3} {4}, Version {5}", + // o.Type, o.ID, o.Name, o.Date.ToShortDateString(), o.Time.ToShortTimeString(), o.Version_List, i++, count); foundObjects.Add(o.CacheKey, o); } @@ -59,12 +59,21 @@ namespace NavScm.TestHost XmlDictionaryReader xmlReader = XmlDictionaryReader.CreateTextReader(reader, new XmlDictionaryReaderQuotas()); var loadedObjects = (Dictionary)serializer.ReadObject(xmlReader, true); - log.Debug("Dumping sample object 1.82004"); + // 5.99997 => CU TN_Test - NavObject o2 = loadedObjects["1.82004"]; + log.Debug("Dumping sample object descriptor for 5.99997 "); + + NavObject o2 = loadedObjects["5.99997"]; log.DebugFormat("Type {0}, ID {1}, Name {2}, Modified {3} {4}, Version {5}", o2.Type, o2.ID, o2.Name, o2.Date.ToShortDateString(), o2.Time.ToShortTimeString(), o2.Version_List); + DevEnvInterface devenv = new DevEnvInterface("C:\\Program Files (x86)\\Microsoft Dynamics NAV\\tbrt-nav-erp-02\\RoleTailored Client\\finsql.exe", + "tbrt-sql-erp-01", "TERRABIT 2015 DEV"); + + devenv.Export(loadedObjects["5.80"], $"{Directory.GetCurrentDirectory()}\\CU80.txt"); + devenv.Export(loadedObjects["5.99997"], $"{Directory.GetCurrentDirectory()}\\CU99997.txt"); + devenv.Export(loadedObjects["1.13"], $"{Directory.GetCurrentDirectory()}\\TAB13.txt"); + log.Info("Shutting down..."); Console.ReadLine(); diff --git a/NAVSCM Library/NavInterface/DevEnvInterface.cs b/NAVSCM Library/NavInterface/DevEnvInterface.cs index 07ed91c..64555a5 100644 --- a/NAVSCM Library/NavInterface/DevEnvInterface.cs +++ b/NAVSCM Library/NavInterface/DevEnvInterface.cs @@ -19,7 +19,7 @@ namespace NavScm.NavInterface /// 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 + public class DevEnvInterface { private static readonly log4net.ILog log = log4net.LogManager.GetLogger(typeof(DevEnvInterface)); @@ -31,23 +31,23 @@ namespace NavScm.NavInterface /// /// The exit code of finsql.exe. So far this is always 0, tests pending. /// - public int ExitCode; + public readonly int ExitCode; /// /// True, if the command executed successfully. /// - public bool Success; + public readonly bool Success; /// /// The output produced by the command, as written to navcommandresult.txt by finsql.exe. /// - public string CommandOutput; + 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 string ErrorMessage; + public readonly string ErrorMessage; /// /// Constructs the whole object. @@ -117,9 +117,6 @@ namespace NavScm.NavInterface /// 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. @@ -146,6 +143,8 @@ namespace NavScm.NavInterface continue; } + Contract.Assert(!Directory.EnumerateFileSystemEntries(tempPath).Any()); + return tempPath; } @@ -173,7 +172,6 @@ namespace NavScm.NavInterface Contract.Requires(command != ""); string tempPath = GetNewTempDirectory(); - Contract.Ensures(!Directory.Exists(tempPath)); string errorLog = $"{tempPath}\\error.log"; string commandOutput = $"{tempPath}\\navcommandresult.txt"; @@ -184,7 +182,6 @@ namespace NavScm.NavInterface // Execute finsql.exe and wait for its exit... Process process = new Process(); - Contract.Ensures(process.HasExited); process.StartInfo.FileName = DevEnvPath; process.StartInfo.Arguments = fullArguments; @@ -207,18 +204,22 @@ namespace NavScm.NavInterface log.Debug($"ExecuteCommand: Execution took: {sw.Elapsed}"); } - // Parse and store finsql.exe result information + // 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), File.ReadAllText(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), ""); + result = new CommandResult(process.ExitCode, true, File.ReadAllText(commandOutput, finsqlEncoding), ""); log.Info("ExecuteCommand: Command executed successfully"); log.DebugFormat("ExecuteCommand: Command result: {0}", result.CommandOutput); } @@ -231,19 +232,40 @@ namespace NavScm.NavInterface } 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 system ensures, that the file - /// ends with .txt, as finsql.exe deduces the export format from the destiation files extension (crap). + /// 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}"); + } } } diff --git a/NAVSCM Library/NavInterface/NavObject.cs b/NAVSCM Library/NavInterface/NavObject.cs index 9c8c341..3018b47 100644 --- a/NAVSCM Library/NavInterface/NavObject.cs +++ b/NAVSCM Library/NavInterface/NavObject.cs @@ -52,7 +52,7 @@ namespace NavScm.NavInterface partial void OnLoaded() { Contract.Requires(Company_Name.Length == 0); - Contract.Requires(Type >= 0 && Type <= 9 && Type != 2 && Type != 4 ); + Contract.Requires(Type >= 1 && Type <= 9 && Type != 2 && Type != 4 ); /* if (Company_Name.Length > 0) throw new InvalidOperationException($"The object {CacheKey} holds a variant with the company name {Company_Name}, which is unsupported"); @@ -93,6 +93,27 @@ namespace NavScm.NavInterface get { return Date.Add(Time.TimeOfDay); } } + /// + /// Returns a filter string suitable to filter the NAV Object table during finsql operation. + /// + /// Filter-String usable f.x. in ExportObjects. + [Pure] + public string GetFilter() + { + switch(NavType) + { + case NavObjectType.Codeunit: return $"Type=Codeunit;ID={ID}"; + case NavObjectType.MenuSuite: return $"Type=MenuSuite;ID={ID}"; + case NavObjectType.Page: return $"Type=Page;ID={ID}"; + case NavObjectType.Query: return $"Type=Query;ID={ID}"; + case NavObjectType.Report: return $"Type=Report;ID={ID}"; + case NavObjectType.Table: return $"Type=Table;ID={ID}"; + case NavObjectType.XmlPort: return $"Type=XmlPort;ID={ID}"; + } + + throw new InvalidOperationException($"The Type {Type} is unknown, cannot convert to filter."); + } + /// /// Constructs a hash key based on Type and ID. ///