diff --git a/Action/DataUpdatePlayerAction.cs b/Action/DataUpdatePlayerAction.cs new file mode 100644 index 00000000..b971ea08 --- /dev/null +++ b/Action/DataUpdatePlayerAction.cs @@ -0,0 +1,20 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace XiboClient.Action +{ + class DataUpdatePlayerAction : PlayerActionInterface + { + public const string Name = "dataUpdate"; + + public int widgetId; + + public string GetActionName() + { + return Name; + } + } +} diff --git a/Action/XmrSubscriber.cs b/Action/XmrSubscriber.cs index 65231db6..9815e276 100644 --- a/Action/XmrSubscriber.cs +++ b/Action/XmrSubscriber.cs @@ -1,5 +1,5 @@ /** - * Copyright (C) 2022 Xibo Signage Ltd + * Copyright (C) 2023 Xibo Signage Ltd * * Xibo - Digital Signage - http://www.xibo.org.uk * @@ -232,6 +232,11 @@ private void processMessage(NetMQMessage message, AsymmetricCipherKeyPair rsaKey new Thread(new ThreadStart(command.Run)).Start(); break; + case "dataUpdate": + DataUpdatePlayerAction dataUpdate = JsonConvert.DeserializeObject(opened); + OnAction?.Invoke(dataUpdate); + break; + case "collectNow": case RevertToSchedulePlayerAction.Name: OnAction?.Invoke(action); diff --git a/Control/FaultController.cs b/Control/FaultController.cs index 99f1fbd5..bca172d3 100644 --- a/Control/FaultController.cs +++ b/Control/FaultController.cs @@ -52,11 +52,14 @@ public async Task Fault() } string[] splitKey = data.key.Split(new char[] { '_' }, StringSplitOptions.RemoveEmptyEntries); - string widgetId = splitKey[1]; + + // widgetId must be parseable as an integer + // only old widgets have string IDs and they would not support calling this method. + int widgetId = int.Parse(splitKey[1]); CacheManager.Instance.AddUnsafeWidget( (UnsafeFaultCodes)data.code, - widgetId, + widgetId + "", data.reason, data.ttl ); diff --git a/Control/InfoController.cs b/Control/InfoController.cs index be5fbd89..cfa828e3 100644 --- a/Control/InfoController.cs +++ b/Control/InfoController.cs @@ -72,6 +72,7 @@ public void GetInfo() jObject.Add("scheduleManagerStatus", ClientInfo.Instance.ScheduleManagerStatus); jObject.Add("unsafeList", ClientInfo.Instance.UnsafeList); jObject.Add("requiredFileList", ClientInfo.Instance.RequiredFilesList); + jObject.Add("dataList", ClientInfo.Instance.DataFilesList); #endif writer.Write(jObject.ToString()); diff --git a/InfoScreen.xaml.cs b/InfoScreen.xaml.cs index e138fe71..e95186e7 100644 --- a/InfoScreen.xaml.cs +++ b/InfoScreen.xaml.cs @@ -85,8 +85,10 @@ private void Update() textBoxSchedule.Text = ClientInfo.Instance.ScheduleManagerStatus; textBoxRequiredFiles.Text = ClientInfo.Instance.UnsafeList - + Environment.NewLine - + ClientInfo.Instance.RequiredFilesList; + + Environment.NewLine + + ClientInfo.Instance.RequiredFilesList + + Environment.NewLine + + ClientInfo.Instance.DataFilesList; // Log grid logDataGridView.Items.Clear(); diff --git a/Log/ClientInfo.cs b/Log/ClientInfo.cs index d2f9cca9..4c89a127 100644 --- a/Log/ClientInfo.cs +++ b/Log/ClientInfo.cs @@ -55,6 +55,11 @@ private static readonly Lazy /// public string RequiredFilesList; + /// + /// Set the data files list + /// + public string DataFilesList; + /// /// Set the schedule manager status /// @@ -160,6 +165,15 @@ public void UpdateRequiredFiles(string requiredFilesString) RequiredFilesList = requiredFilesString; } + /// + /// Update the data files text box + /// + /// + public void UpdateDataFiles(string dataFilesString) + { + DataFilesList = dataFilesString; + } + /// /// Update the unsafe files list /// diff --git a/Logic/ApplicationSettings.cs b/Logic/ApplicationSettings.cs index bcbc32f3..ab7c018a 100644 --- a/Logic/ApplicationSettings.cs +++ b/Logic/ApplicationSettings.cs @@ -52,9 +52,9 @@ private static readonly Lazy /// private List ExcludedProperties; - public string ClientVersion { get; } = "4 R400.6"; + public string ClientVersion { get; } = "4 R401.0"; public string Version { get; } = "7"; - public int ClientCodeVersion { get; } = 400; + public int ClientCodeVersion { get; } = 401; private ApplicationSettings() { diff --git a/Logic/RequiredFiles.cs b/Logic/RequiredFiles.cs index 56d57859..28f409c6 100644 --- a/Logic/RequiredFiles.cs +++ b/Logic/RequiredFiles.cs @@ -69,7 +69,7 @@ public int FilesMissing int count = 0; foreach (RequiredFile rf in RequiredFileList) { - if (!rf.Complete) + if (!rf.Complete && !rf.IsWidgetData) { count++; } @@ -270,15 +270,7 @@ private void SetRequiredFiles() { // Add and skip onward rf.Id = int.Parse(attributes["id"].Value); - rf.SaveAs = rf.Id + ".json"; rf.UpdateInterval = attributes["updateInterval"] != null ? int.Parse(attributes["updateInterval"].Value) : 120; - - // Does this data file already exist? and if so, is it sufficiently up to date. - if (File.Exists(ApplicationSettings.Default.LibraryPath + @"\" + rf.SaveAs)) - { - rf.Complete = File.GetLastWriteTime(ApplicationSettings.Default.LibraryPath + @"\" + rf.SaveAs) > DateTime.Now.AddMinutes(-1 * rf.UpdateInterval); - } - RequiredFileList.Add(rf); continue; } @@ -444,7 +436,12 @@ public void ReportInventory() foreach (RequiredFile rf in RequiredFileList) { - if (rf.FileType == "dependency") + if (rf.FileType == "widget") + { + // We don't report media inventory for data + continue; + } + else if (rf.FileType == "dependency") { xml += string.Format("", rf.FileType, @@ -518,5 +515,13 @@ public class RequiredFile // Data public int UpdateInterval; + + public bool IsWidgetData + { + get + { + return FileType.Equals("widget", StringComparison.OrdinalIgnoreCase); + } + } } } diff --git a/Logic/Schedule.cs b/Logic/Schedule.cs index 599da63d..4d081fa2 100644 --- a/Logic/Schedule.cs +++ b/Logic/Schedule.cs @@ -96,6 +96,11 @@ public class Schedule private FaultsAgent _faultsAgent; Thread _faultsAgentThread; + // Data Agent + private DataAgent _dataAgent; + + Thread _dataAgentThread; + // XMR Subscriber private XmrSubscriber _xmrSubscriber; Thread _xmrSubscriberThread; @@ -136,8 +141,15 @@ public Schedule(string scheduleLocation) _scheduleManagerThread = new Thread(new ThreadStart(_scheduleManager.Run)); _scheduleManagerThread.Name = "ScheduleManagerThread"; + // Data Agent + _dataAgent = new DataAgent(); + _dataAgentThread = new Thread(new ThreadStart(_dataAgent.Run)) + { + Name = "DataAgent" + }; + // Create a RequiredFilesAgent - _scheduleAndRfAgent = new ScheduleAndFilesAgent(); + _scheduleAndRfAgent = new ScheduleAndFilesAgent(_dataAgent); _scheduleAndRfAgent.CurrentScheduleManager = _scheduleManager; _scheduleAndRfAgent.ScheduleLocation = scheduleLocation; _scheduleAndRfAgent.HardwareKey = _hardwareKey.Key; @@ -202,6 +214,9 @@ public void InitializeComponents() // Start the RequiredFilesAgent thread _scheduleAndRfAgentThread.Start(); + // Start the data agent thread + _dataAgentThread.Start(); + // Start the ScheduleManager thread _scheduleManagerThread.Start(); @@ -350,6 +365,12 @@ void _xmrSubscriber_OnAction(Action.PlayerActionInterface action) wakeUpXmds(); break; + case "dataUpdate": + // Wakeup the data agent and mark the widget to be force updated. + _dataAgent.ForceUpdateWidget(((DataUpdatePlayerAction)action).widgetId); + _dataAgent.WakeUp(); + break; + case LayoutChangePlayerAction.Name: // Add to a collection of Layout Change events if (((LayoutChangePlayerAction)action).changeMode == "replace") diff --git a/Properties/AssemblyInfo.cs b/Properties/AssemblyInfo.cs index d4dc5f94..457a3205 100644 --- a/Properties/AssemblyInfo.cs +++ b/Properties/AssemblyInfo.cs @@ -49,6 +49,6 @@ // You can specify all the values or you can default the Build and Revision Numbers // by using the '*' as shown below: // [assembly: AssemblyVersion("1.0.*")] -[assembly: AssemblyVersion("4.400.6.0")] -[assembly: AssemblyFileVersion("4.400.6.0")] +[assembly: AssemblyVersion("4.401.0.0")] +[assembly: AssemblyFileVersion("4.401.0.0")] [assembly: Guid("3bd467a4-4ef9-466a-b156-a79c13a863f7")] diff --git a/XiboClient.csproj b/XiboClient.csproj index 7d14b9f7..dd266c3e 100644 --- a/XiboClient.csproj +++ b/XiboClient.csproj @@ -104,6 +104,7 @@ Designer + @@ -191,6 +192,7 @@ OptionsForm.xaml + @@ -198,6 +200,7 @@ + Designer MSBuild:Compile diff --git a/XmdsAgents/DataAgent.cs b/XmdsAgents/DataAgent.cs new file mode 100644 index 00000000..5e7a6033 --- /dev/null +++ b/XmdsAgents/DataAgent.cs @@ -0,0 +1,237 @@ +/** + * Copyright (C) 2023 Xibo Signage Ltd + * + * Xibo - Digital Signage - http://www.xibo.org.uk + * + * This file is part of Xibo. + * + * Xibo is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * any later version. + * + * Xibo is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Xibo. If not, see . + */ +using Newtonsoft.Json.Linq; +using Newtonsoft.Json; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Threading; +using Swan; +using System.IO; +using XiboClient.Log; + +namespace XiboClient.XmdsAgents +{ + internal class DataAgent + { + private static readonly object _locker = new object(); + private bool _forceStop = false; + private ManualResetEvent _manualReset = new ManualResetEvent(false); + + private Dictionary _widgets; + + /// + /// New HTTP file required + /// + /// + public delegate void OnNewHttpRequiredFileDelegate(int mediaId, double fileSize, string md5, string saveAs, string path); + public event OnNewHttpRequiredFileDelegate OnNewHttpRequiredFile; + + /// + /// Construct a new data agent + /// + public DataAgent() + { + _widgets = new Dictionary(); + } + + /// + /// Wake Up + /// + public void WakeUp() + { + _manualReset.Set(); + } + + /// + /// Stops the thread + /// + public void Stop() + { + _forceStop = true; + _manualReset.Set(); + } + + /// + /// Clear our list of data items + /// + public void Clear() + { + lock (_locker) + { + _widgets.Clear(); + } + } + + /// + /// Add a widget to be kept updated + /// + /// + /// + public void AddWidget(int widgetId, int updateInterval) + { + lock (_locker) + { + if (!_widgets.ContainsKey(widgetId)) + { + WidgetData widgetData = new WidgetData(); + widgetData.WidgetId = widgetId; + widgetData.UpdateInterval = updateInterval; + _widgets.Add(widgetId, widgetData); + } + } + } + + /// + /// Force a widget to be updated forthwith + /// + /// + public void ForceUpdateWidget(int widgetId) + { + lock (_locker) + { + if (_widgets.ContainsKey(widgetId)) + { + _widgets[widgetId].ForceUpdate = true; + } + } + } + + /// + /// Run Thread + /// + public void Run() + { + LogMessage.Info("DataAgent", "Run", "Thread Started"); + + int retryAfterSeconds; + + while (!_forceStop) + { + // If we are restarting, reset + _manualReset.Reset(); + + // Reset backOff + retryAfterSeconds = 0; + + lock (_locker) + { + string dataFilesList = ""; + try + { + foreach (WidgetData widget in _widgets.Values) + { + if (widget.ForceUpdate || !widget.IsUpToDate) + { + // Download using XMDS GetResource + using (xmds.xmds xmds = new xmds.xmds()) + { + xmds.Credentials = null; + xmds.UseDefaultCredentials = true; + + xmds.Url = ApplicationSettings.Default.XiboClient_xmds_xmds + "&method=getData"; + string result = xmds.GetData(ApplicationSettings.Default.ServerKey, ApplicationSettings.Default.HardwareKey, widget.WidgetId); + + // Write the result to disk + using (FileStream fileStream = File.Open(widget.Path, FileMode.Create, FileAccess.Write, FileShare.Read)) + { + using (StreamWriter sw = new StreamWriter(fileStream)) + { + sw.Write(result); + sw.Close(); + } + } + + // Clear the force update flag if set. + widget.UpdatedDt = DateTime.Now; + widget.ForceUpdate = false; + + // Load the result into a JSON response. + try + { + JObject json = JsonConvert.DeserializeObject(result); + if (json != null && json.ContainsKey("files")) + { + foreach (JObject file in json.GetValueOrDefault("files").Cast()) + { + // Make a new fileagent somehow, to download this file. + OnNewHttpRequiredFile?.Invoke( + int.Parse(file.GetValue("id").ToString()), + double.Parse(file.GetValue("size").ToString()), + file.GetValue("md5").ToString(), + file.GetValue("saveAs").ToString(), + file.GetValue("path").ToString() + ); + } + } + } + catch (Exception ex) + { + LogMessage.Error("DataAgent", "Run", "Unable to parse JSON result. e = " + ex.Message); + } + } + } + + dataFilesList += widget.WidgetId + ", " + widget.UpdatedDt + Environment.NewLine; + } + } + catch (WebException webEx) when (webEx.Response is HttpWebResponse httpWebResponse && (int)httpWebResponse.StatusCode == 429) + { + // Get the header for how long we ought to wait + retryAfterSeconds = webEx.Response.Headers["Retry-After"] != null ? int.Parse(webEx.Response.Headers["Retry-After"]) : 120; + + // Log it. + LogMessage.Info("DataAgent", "Run", "429 received, waiting for " + retryAfterSeconds + " seconds."); + } + catch (WebException webEx) + { + // Increment the quantity of XMDS failures and bail out + ApplicationSettings.Default.IncrementXmdsErrorCount(); + + // Log this message, but dont abort the thread + LogMessage.Info("DataAgent", "Run", "WebException: " + webEx.Message); + } + catch (Exception ex) + { + // Log this message, but dont abort the thread + LogMessage.Error("DataAgent", "Run", "Exception: " + ex.Message); + } + + ClientInfo.Instance.DataFilesList = dataFilesList; + } + + if (retryAfterSeconds > 0) + { + // Sleep this thread until we've fulfilled our try after + _manualReset.WaitOne(retryAfterSeconds * 1000); + } + else + { + // Sleep this thread until for 60 seconds + _manualReset.WaitOne(60 * 1000); + } + } + + LogMessage.Info("DataAgent", "Run", "Thread Stopped"); + } + } +} diff --git a/XmdsAgents/FileAgent.cs b/XmdsAgents/FileAgent.cs index bf6cca73..34ee4c95 100644 --- a/XmdsAgents/FileAgent.cs +++ b/XmdsAgents/FileAgent.cs @@ -18,17 +18,12 @@ * You should have received a copy of the GNU Affero General Public License * along with Xibo. If not, see . */ -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; -using Swan; using System; using System.Diagnostics; using System.IO; -using System.Linq; using System.Net; using System.Text; using System.Threading; -using System.Windows.Documents; namespace XiboClient.XmdsAgents { @@ -48,13 +43,6 @@ class FileAgent public delegate void OnPartCompleteDelegate(int fileId); public event OnPartCompleteDelegate OnPartComplete; - /// - /// OnPartComplete delegate - /// - /// - public delegate void OnNewHttpRequiredFileDelegate(int mediaId, double fileSize, string md5, string saveAs, string path); - public event OnNewHttpRequiredFileDelegate OnNewHttpRequiredFile; - /// /// Client Hardware key /// @@ -115,26 +103,17 @@ public void Run() { Trace.WriteLine(new LogMessage("FileAgent - Run", "Thread alive and Lock Obtained"), LogType.Audit.ToString()); - if (_requiredFile.FileType == "resource" || _requiredFile.FileType == "widget") + if (_requiredFile.FileType == "resource") { // Download using XMDS GetResource using (xmds.xmds xmds = new xmds.xmds()) { xmds.Credentials = null; xmds.UseDefaultCredentials = true; + xmds.Url = ApplicationSettings.Default.XiboClient_xmds_xmds + "&method=getResource"; + + string result = xmds.GetResource(ApplicationSettings.Default.ServerKey, ApplicationSettings.Default.HardwareKey, _requiredFile.LayoutId, _requiredFile.RegionId, _requiredFile.MediaId); - string result; - if (_requiredFile.FileType == "widget") - { - xmds.Url = ApplicationSettings.Default.XiboClient_xmds_xmds + "&method=getData"; - result = xmds.GetData(ApplicationSettings.Default.ServerKey, ApplicationSettings.Default.HardwareKey, _requiredFile.Id); - } - else - { - xmds.Url = ApplicationSettings.Default.XiboClient_xmds_xmds + "&method=getResource"; - result = xmds.GetResource(ApplicationSettings.Default.ServerKey, ApplicationSettings.Default.HardwareKey, _requiredFile.LayoutId, _requiredFile.RegionId, _requiredFile.MediaId); - } - // Write the result to disk using (FileStream fileStream = File.Open(ApplicationSettings.Default.LibraryPath + @"\" + _requiredFile.SaveAs, FileMode.Create, FileAccess.Write, FileShare.Read)) { @@ -148,28 +127,6 @@ public void Run() // File completed _requiredFile.Downloading = false; _requiredFile.Complete = true; - - // Do we have any new required file nodes to parse out from this return. - if (_requiredFile.FileType == "widget") - { - // Load the result into a JSON response. - try - { - JObject json = JsonConvert.DeserializeObject(result); - if (json != null && json.ContainsKey("files")) - { - foreach (JObject file in json.GetValueOrDefault("files").Cast()) - { - // Make a new fileagent somehow, to download this file. - OnNewHttpRequiredFile?.Invoke(int.Parse(file.GetValue("id").ToString()), double.Parse(file.GetValue("size").ToString()), file.GetValue("md5").ToString(), file.GetValue("saveAs").ToString(), file.GetValue("path").ToString()); - } - } - } - catch (Exception ex) - { - LogMessage.Error("FileAgent", "Run", "Unable to parse JSON result. e = " + ex.Message); - } - } } } else if (_requiredFile.Http) diff --git a/XmdsAgents/ScheduleAndFilesAgent.cs b/XmdsAgents/ScheduleAndFilesAgent.cs index df8008a9..85ff2acd 100644 --- a/XmdsAgents/ScheduleAndFilesAgent.cs +++ b/XmdsAgents/ScheduleAndFilesAgent.cs @@ -101,13 +101,19 @@ public string HardwareKey /// private string _lastCheckSchedule; + /// + /// The data agent + /// + private DataAgent _dataAgent; + /// /// Required Files Agent /// - public ScheduleAndFilesAgent() + public ScheduleAndFilesAgent(DataAgent dataAgent) { int limit = (ApplicationSettings.Default.MaxConcurrentDownloads <= 0) ? 1 : ApplicationSettings.Default.MaxConcurrentDownloads; - + + _dataAgent = dataAgent; _fileDownloadLimit = new Semaphore(limit, limit); _requiredFiles = new RequiredFiles(); } @@ -154,6 +160,9 @@ public void Run() { Trace.WriteLine(new LogMessage("RequiredFilesAgent - Run", "Thread Started"), LogType.Info.ToString()); + // Bind to the data agent incase it has any new files for us. + _dataAgent.OnNewHttpRequiredFile += OnNewRequiredFile; + int retryAfterSeconds = 0; while (!_forceStop) @@ -214,6 +223,9 @@ public void Run() // Clear any layout codes CacheManager.Instance.ClearLayoutCodes(); + // Clear the data agent once we're sure we have a successful response + _dataAgent.Clear(); + // Create a required files object and set it to contain the RF returned this tick _requiredFiles = new RequiredFiles(); _requiredFiles.RequiredFilesXml = xml; @@ -244,6 +256,14 @@ public void Run() continue; } + // Is this widget data? + if (fileToDownload.IsWidgetData) + { + // Register this with the widget data processor. + _dataAgent.AddWidget(fileToDownload.Id, fileToDownload.UpdateInterval); + continue; + } + // Can we fit the file on the drive? if (freeSpace != -1) { @@ -265,7 +285,6 @@ public void Run() }; fileAgent.OnComplete += new FileAgent.OnCompleteDelegate(fileAgent_OnComplete); fileAgent.OnPartComplete += new FileAgent.OnPartCompleteDelegate(fileAgent_OnPartComplete); - fileAgent.OnNewHttpRequiredFile += new FileAgent.OnNewHttpRequiredFileDelegate(OnNewRequiredFile); // Create the thread and add it to the list of threads to start Thread thread = new Thread(new ThreadStart(fileAgent.Run)) @@ -364,13 +383,14 @@ private string RequiredFilesString() foreach (RequiredFile requiredFile in _requiredFiles.RequiredFileList) { string percentComplete; - if (requiredFile.Complete) + if (requiredFile.FileType == "widget") { - percentComplete = "100"; - } - else if (requiredFile.FileType == "widget") + // Skip data widgets + continue; + } + else if (requiredFile.Complete) { - percentComplete = "0"; + percentComplete = "100"; } else { diff --git a/XmdsAgents/WidgetData.cs b/XmdsAgents/WidgetData.cs new file mode 100644 index 00000000..779ed76f --- /dev/null +++ b/XmdsAgents/WidgetData.cs @@ -0,0 +1,58 @@ +/** + * Copyright (C) 2023 Xibo Signage Ltd + * + * Xibo - Digital Signage - http://www.xibo.org.uk + * + * This file is part of Xibo. + * + * Xibo is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * any later version. + * + * Xibo is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Xibo. If not, see . + */ +using System; +using System.IO; + +namespace XiboClient.XmdsAgents +{ + class WidgetData + { + public int WidgetId; + public int UpdateInterval; + public bool ForceUpdate = false; + public DateTime UpdatedDt; + + public string Path + { + get + { + return ApplicationSettings.Default.LibraryPath + @"\" + WidgetId + ".json"; + } + } + + public bool IsUpToDate + { + get + { + // Does this data file already exist? and if so, is it sufficiently up to date. + if (File.Exists(Path)) + { + UpdatedDt = File.GetLastWriteTime(Path); + return UpdatedDt > DateTime.Now.AddMinutes(-1 * UpdateInterval); + } + else + { + return false; + } + } + } + } +}