using FluentFTP; using Microsoft.WindowsAzure.Storage.Blob; using System; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.IO.Compression; using System.Linq; using System.Text; using System.Threading; using System.Threading.Tasks; using Tango.Core; using Tango.Core.DB; using Tango.Core.ExtensionMethods; using Tango.Core.Helpers; using Tango.Core.IO; using Tango.Integration.Operation; using Tango.PMR.Synchronization; using Tango.PPC.Common.Application; using Tango.PPC.Common.Connection; using Tango.PPC.Common.Web; using Tango.Settings; using Tango.SharedUI.Helpers; using Tango.SQLExaminer; using Tango.Transport.Web; namespace Tango.PPC.Common.MachineUpdate { public class MachineUpdateManager : ExtendedObject, IMachineUpdateManager { private IPPCApplicationManager _app_manager; private IMachineProvider _machineProvider; private PPCWebClient _client; #region Events /// /// Occurs when there is a text log message available. /// public event EventHandler ProgressLog; /// /// Occurs when the update has made some progress. /// public event EventHandler Progress; #endregion #region Properties private MachineUpdateProgress _status; public MachineUpdateProgress Status { get { return _status; } private set { _status = value; RaisePropertyChangedAuto(); } } #endregion #region Constructors /// /// Initializes a new instance of the class. /// /// The application manager. public MachineUpdateManager(PPCWebClient ppcWebClient, IPPCApplicationManager applicationManager, IMachineProvider machineProvider) { _client = ppcWebClient; _machineProvider = machineProvider; _app_manager = applicationManager; } #endregion #region Private Methods private Task Login(String serialNumber) { return _client.Login(new LoginRequest() { Mode = LoginMode.Machine, SerialNumber = serialNumber, }); } #endregion #region Public Methods /// /// Performs a machine setup using the specified serial number and machine service address. /// /// The serial number. /// The machine service address. /// public async Task Update(string serialNumber) { TaskCompletionSource result = new TaskCompletionSource(); try { var machineServiceAddress = SettingsManager.Default.GetOrCreate().GetMachineServiceAddress(); LogManager.Log($"Starting machine update for serial number {serialNumber}..."); //Connecting to machine... LogManager.Log("Verifying machine connection and state..."); UpdateProgress("Verifying machine state", "Initializing..."); await Task.Delay(1000); IMachineOperator op = _machineProvider.MachineOperator; if (_machineProvider.Machine.SetupFirmware) { LogManager.Log("Machine is configured to update firmware..."); if (op.State != Transport.TransportComponentState.Connected) { throw LogManager.Log(new InvalidOperationException("Could not perform an update while the machine is not connected.")); } if (op.IsPrinting) { throw LogManager.Log(new InvalidOperationException($"Could not perform an update while the machine is in {op.Status} status.")); } } //Connect to machine service and get matching packages for this machine. UpdateProgress("Downloading software package", "Connecting to machine service..."); LogManager.Log($"Connecting to machine service on {machineServiceAddress}..."); await Login(serialNumber); DownloadUpdateRequest request = new DownloadUpdateRequest(); request.SerialNumber = serialNumber; DownloadUpdateResponse update_response = null; update_response = await _client.MachineUpdate(request); LogManager.Log($"Machine update response received: {Environment.NewLine}{update_response.ToJsonString()}"); //Create temporary folders for packages. var _newPackageTempFolder = TemporaryManager.CreateFolder(); _newPackageTempFolder.Persist = true; LogManager.Log($"Temporary package folder created: {_newPackageTempFolder}."); //Download software package. var tempFile = TemporaryManager.CreateFile(".zip"); LogManager.Log($"Temporary package zip file created: {tempFile}."); LogManager.Log("Downloading software package..."); long fileSize = 0; UpdateProgress("Downloading software package", "Downloading...", false); using (FileStreamWrapper fs = new FileStreamWrapper(tempFile.Path, FileMode.Create, (current) => { UpdateProgress("Downloading software package", "Downloading...", false, current, fileSize); })) { LogManager.Log($"Connecting to storage blob with address {update_response.BlobAddress}"); CloudBlockBlob blob = new CloudBlockBlob(new Uri(update_response.BlobAddress)); LogManager.Log("Fetching blob attributes..."); blob.FetchAttributes(); fileSize = blob.Properties.Length; LogManager.Log("Download size: " + fileSize + " bytes."); LogManager.Log("Starting blob download..."); blob.DownloadToStream(fs); } UpdateProgress("Downloading software package", "Extracting package..."); LogManager.Log("Extracting downloaded zip file..."); //Extract software package. ZipFile.ExtractToDirectory(tempFile, _newPackageTempFolder); LogManager.Log("Copying latest updater utility to application path..."); //Copy new updater utility to app path. File.Copy(Path.Combine(_newPackageTempFolder, "Tango.PPC.Updater.exe"), Path.Combine(PathHelper.GetStartupPath(), "Tango.PPC.Updater.exe"), true); //Synchronize database UpdateProgress("Updating Database", "Initializing..."); var localDataSource = SettingsManager.Default.GetOrCreate().DataSource; LogManager.Log($"Synchronizing database '{update_response.DataSource.ToString()}' => '{localDataSource.ToString()}'..."); UpdateProgress("Updating Database", "Connecting to local database..."); LogManager.Log("Initializing database manager..."); DbManager db = DbManager.FromDataSource(localDataSource); LogManager.Log("Checking Tango database exists on the local machine..."); if (!db.Exists(localDataSource.Catalog)) { throw new InvalidProgramException("Database tango does not exists."); } LogManager.Log("Disposing database manager."); db.Dispose(); LogManager.Log($"Initializing {nameof(ExaminerSequenceConfigurationRunner)}..."); UpdateProgress("Updating Database", "Initializing update sequence..."); ExaminerSequenceConfigurationRunner runner = new ExaminerSequenceConfigurationRunner( Path.Combine(_newPackageTempFolder, "Update Scripts", "config.xml"), Path.Combine(_newPackageTempFolder, "Update Scripts"), update_response.DataSource, localDataSource, serialNumber); runner.Log += (x, msg) => { LogManager.Log(msg); ProgressLog?.Invoke(this, msg); }; runner.ScriptExecuting += (x, item) => { LogManager.Log($"Executing script {item.ToString()}..."); UpdateProgress("Updating Database", item.Name + "..."); }; LogManager.Log("Starting synchronization process..."); try { await runner.Run(); LogManager.Log("Synchronization completed successfully!"); UpdateProgress("Updating Database", "Database synchronization completed successfully."); } catch (Exception ex) { throw LogManager.Log(ex, "Setup manager error while trying to synchronize database."); } //Updating firmware if (_machineProvider.Machine.SetupFirmware) { UpdateProgress("Updating Firmware", "Connecting to firmware device..."); LogManager.Log(""); LogManager.Log("-------------------------------------------------------------------------"); LogManager.Log("Updating Firmware..."); UpdateProgress("Updating Firmware", "Loading firmware package..."); var tfpPath = Path.Combine(_newPackageTempFolder, "firmware_package.tfp"); var stream = new FileStream(tfpPath, FileMode.Open); var handler = await op.UpgradeFirmware(stream); handler.Failed += (_, ex) => { stream.Dispose(); result.SetException(ex); }; handler.Completed += (_, __) => { UpdateProgress("Updating Firmware", "Firmware update completed successfully."); stream.Dispose(); result.SetResult(new MachineUpdateResult() { UpdatePackagePath = _newPackageTempFolder, }); }; handler.Canceled += (_, __) => { stream.Dispose(); result.SetException(new Exception("The operation has been canceled.")); }; handler.Progress += (_, e) => { UpdateProgress("Updating Firmware", e.Message, false, e.Current, e.Total); }; } else { result.SetResult(new MachineUpdateResult() { UpdatePackagePath = _newPackageTempFolder, }); } } catch (Exception ex) { LogManager.Log(ex, "An error occurred in machine update."); result.SetException(ex); } return await result.Task; } /// /// Checks if any update are available for the specified machine serial number. /// /// The serial number. /// The machine service address. /// public Task CheckForUpdate(string serialNumber) { return Task.Factory.StartNew(() => { var machineServiceAddress = SettingsManager.Default.GetOrCreate().GetMachineServiceAddress(); LogManager.Log($"Connecting to machine service on {machineServiceAddress}..."); Login(serialNumber).GetAwaiter().GetResult(); LogManager.Log($"Checking if updates available..."); CheckForUpdateRequest request = new CheckForUpdateRequest(); request.SerialNumber = serialNumber; request.Version = _app_manager.Version.ToString(); CheckForUpdateResponse update_response = null; update_response = _client.CheckForUpdates(request).Result; LogManager.Log($"Check for update response received: {Environment.NewLine}{update_response.ToJsonString()}"); return update_response; }); } /// /// Updates all the "overwrite-able" database tables. /// /// The serial number. /// The machine service address. /// public Task UpdateDB(DbCompareResult dbCompareResult, String serialNumber) { return Task.Factory.StartNew(() => { LogManager.Log("Starting database update..."); UpdateProgress("Updating Database", "Initializing..."); LogManager.Log("Looking for update scripts configuration on application path..."); String config_file = Path.Combine(PathHelper.GetStartupPath(), "Update Scripts", "config.xml"); if (!File.Exists(config_file)) { throw LogManager.Log(new FileNotFoundException($"Could not locate '{config_file}' file on application folder.")); } UpdateDBResponse update_response = dbCompareResult.UpdateDBResponse; var localDataSource = SettingsManager.Default.GetOrCreate().DataSource; LogManager.Log($"Updating database '{update_response.DataSource.ToString()}' => '{localDataSource.ToString()}'..."); UpdateProgress("Updating Database", "Initializing update sequence..."); ExaminerSequenceConfiguration config_sequence = ExaminerSequenceConfiguration.FromFile(config_file); foreach (var item in config_sequence.Items.Where(x => x.Type == ExaminerSequenceItemType.Data || update_response.PerformSchemaUpdate).OrderBy(x => x.Index)) { LogManager.Log($"Executing update script '{item.FileName}...'"); ExaminerConfigurationBuilder builder = new ExaminerConfigurationBuilder(Path.Combine(Path.GetDirectoryName(config_file), item.FileName)); builder.SetSource(update_response.DataSource); builder.SetTarget(localDataSource); if (item.RequiresSerialNumber) { builder.SetMachineSerialNumber(serialNumber); } builder.Synchronize(); var config = builder.Build(); ExaminerProcess process = new ExaminerProcess(config, item.Type == ExaminerSequenceItemType.Data ? ExaminerProcessType.Data : ExaminerProcessType.Schema); process.Progress += (x, msg) => { LogManager.Log(msg); }; try { UpdateProgress("Updating Database", item.Name + "..."); var result = process.Execute().Result; if (result.ExitCode != ExaminerProcessExitCode.Success) { throw LogManager.Log(new InvalidDataException($"{item.FileName} script has terminated with exit code '{result.ExitCode}'.")); } LogManager.Log("Script executed successfully."); } catch (Exception ex) { throw LogManager.Log(ex, "Setup manager error while trying to update the database."); } } UpdateProgress("Updating Database", "Database synchronization completed successfully."); LogManager.Log("Update completed successfully."); }); } /// /// Checks whether it is necessary to updates all the "overwrite-able" database tables. /// /// The serial number. /// The machine service address. /// public Task UpdateDBCheck(string serialNumber) { return Task.Factory.StartNew(() => { var machineServiceAddress = SettingsManager.Default.GetOrCreate().GetMachineServiceAddress(); LogManager.Log($"Checking if database update is required for serial number {serialNumber}..."); LogManager.Log("Looking for update scripts configuration on application path..."); String config_file = Path.Combine(PathHelper.GetStartupPath(), "Update Scripts", "config.xml"); if (!File.Exists(config_file)) { throw LogManager.Log(new FileNotFoundException($"Could not locate '{config_file}' file on application folder.")); } LogManager.Log($"Connecting to machine service on {machineServiceAddress}..."); Login(serialNumber).Wait(); UpdateDBRequest request = new UpdateDBRequest(); request.SerialNumber = serialNumber; UpdateDBResponse update_response = null; update_response = _client.UpdateDB(request).Result; LogManager.Log($"Update DB response received: {Environment.NewLine}{update_response.ToJsonString()}"); var localDataSource = SettingsManager.Default.GetOrCreate().DataSource; LogManager.Log($"Comparing database '{update_response.DataSource.ToString()}' => '{localDataSource.ToString()}'..."); var report_file = TemporaryManager.CreateFile(".xml"); ExaminerSequenceConfiguration config_sequence = ExaminerSequenceConfiguration.FromFile(config_file); bool has_differences = false; foreach (var item in config_sequence.Items.Where(x => x.Type == ExaminerSequenceItemType.Data).OrderBy(x => x.Index)) { LogManager.Log($"Executing update script '{item.FileName}...'"); ExaminerConfigurationBuilder builder = new ExaminerConfigurationBuilder(Path.Combine(Path.GetDirectoryName(config_file), item.FileName)); builder.SetSource(update_response.DataSource); builder.SetTarget(localDataSource); builder.SetReportFile(report_file); if (item.RequiresSerialNumber) { builder.SetMachineSerialNumber(serialNumber); } var config = builder.Build(); ExaminerProcess process = new ExaminerProcess(config, ExaminerProcessType.Data); process.Progress += (x, msg) => { LogManager.Log(msg); }; LogManager.Log("Starting comparison process..."); LogManager.Log("Generating report on " + report_file); try { var result = process.Execute().Result; if (result.ExitCode != ExaminerProcessExitCode.Success) { throw LogManager.Log(new InvalidDataException(String.Format("Update script has terminated with exit code '{0}'.", result.ExitCode))); } LogManager.Log("Comparison completed successfully!"); LogManager.Log("Loading report file..."); ExaminerDataReport report = ExaminerDataReport.FromFile(report_file); report_file.Delete(); LogManager.Log("Comparison summary: \n" + report.Totals.ToJsonString()); if (report.HasDifferences) { has_differences = true; break; } } catch (Exception ex) { throw LogManager.Log(ex, "Update manager error while trying to compare the database."); } } LogManager.Log("Comparison completed successfully."); return new DbCompareResult() { RequiresUpdate = has_differences, UpdateDBResponse = update_response, }; }); } /// /// Performs a machine update using the specified software update package path. /// /// Name of the file. /// public Task UpdateFromTUP(string fileName) { return Task.Factory.StartNew(() => { LogManager.Log($"Starting machine update from update package '{fileName}'..."); //Create temporary folders for packages. var _newPackageTempFolder = TemporaryManager.CreateFolder(); _newPackageTempFolder.Persist = true; LogManager.Log("Extracting downloaded zip file..."); //Extract software package. ZipFile.ExtractToDirectory(fileName, _newPackageTempFolder); LogManager.Log("Copying latest updater utility to application path..."); //Copy new updater utility to app path. File.Copy(Path.Combine(_newPackageTempFolder, "Tango.PPC.Updater.exe"), Path.Combine(PathHelper.GetStartupPath(), "Tango.PPC.Updater.exe"), true); LogManager.Log("Update operation completed!"); return new MachineUpdateResult() { UpdatePackagePath = _newPackageTempFolder, }; }); } /// /// Gets the update package file information. /// /// The file path. /// public Task GetUpdatePackageFileInfo(string filePath) { return Task.Factory.StartNew(() => { UpdatePackageFile file = new UpdatePackageFile(); var tempFolder = TemporaryManager.CreateFolder(); using (Ionic.Zip.ZipFile zip = new Ionic.Zip.ZipFile(filePath)) { var appEntry = zip.Entries.SingleOrDefault(x => x.FileName == "Tango.PPC.UI.exe"); appEntry.Extract(tempFolder); } FileVersionInfo info = FileVersionInfo.GetVersionInfo(Path.Combine(tempFolder, "Tango.PPC.UI.exe")); file.Version = Version.Parse(info.ProductVersion); tempFolder.Delete(); return file; }); } #endregion #region Protected Methods protected virtual void UpdateProgress(String name, String message = "", bool isIntermediate = true, double progress = 0, double total = 0) { InvokeUINow(() => { Status = new MachineUpdateProgress() { Name = name, Message = message, IsIntermediate = isIntermediate, Progress = progress, Total = total, }; Progress?.Invoke(this, Status); }); UIHelper.DoEvents(); } #endregion } }