Update: Plugin Updating

Updating plugins from the plugin catalogue, and automatic updating of
plugins after a newer version of Disco is installed.
This commit is contained in:
Gary Sharp
2013-02-14 19:00:01 +11:00
parent 734b02fa1d
commit 8f769809c2
53 changed files with 3795 additions and 2975 deletions
@@ -0,0 +1,217 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Disco.Data.Repository;
using Disco.Services.Plugins;
using Disco.Services.Tasks;
using Disco.Models.BI.Interop.Community;
using System.Net;
using System.Xml.Serialization;
using System.IO;
using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
using Quartz;
namespace Disco.Services.Plugins.CommunityInterop
{
public class PluginLibraryUpdateTask : ScheduledTask
{
public override string TaskName { get { return "Disco Community - Update Plugin Library"; } }
public static string CurrentDiscoVersion()
{
var AssemblyVersion = typeof(PluginLibraryUpdateTask).Assembly.GetName().Version;
return string.Format("{0}.{1}.{2:0000}.{3:0000}", AssemblyVersion.Major, AssemblyVersion.Minor, AssemblyVersion.Build, AssemblyVersion.Revision);
}
protected override void ExecuteTask()
{
ExecuteTaskInternal(this.Status);
Status.SetFinishedMessage("The Plugin Library Catalogue was updated.");
}
internal static void ExecuteTaskInternal(ScheduledTaskStatus Status)
{
PluginLibraryUpdateRequest updateRequestBody;
PluginLibraryUpdateResponse updateResult;
string catalogueFile;
PluginLibraryCompatibilityRequest compatRequestBody;
PluginLibraryCompatibilityResponse compatResult;
string compatibilityFile;
var DiscoBIVersion = CurrentDiscoVersion();
HttpWebRequest webRequest;
#region Update
Status.UpdateStatus(1, "Updating Plugin Library Catalogue", "Building Request");
using (DiscoDataContext dbContext = new DiscoDataContext())
{
catalogueFile = Plugins.CatalogueFile(dbContext);
updateRequestBody = new PluginLibraryUpdateRequest()
{
DeploymentId = dbContext.DiscoConfiguration.DeploymentId,
HostVersion = typeof(Plugins).Assembly.GetName().Version.ToString(4)
};
}
Status.UpdateStatus(10, "Sending Request");
webRequest = (HttpWebRequest)HttpWebRequest.Create(PluginLibraryUpdateUrl());
webRequest.KeepAlive = false;
webRequest.ContentType = "application/json";
webRequest.Method = WebRequestMethods.Http.Post;
webRequest.UserAgent = string.Format("Disco/{0} (PluginLibrary)", DiscoBIVersion);
using (var wrStream = webRequest.GetRequestStream())
{
XmlSerializer xml = new XmlSerializer(typeof(PluginLibraryUpdateRequest));
xml.Serialize(wrStream, updateRequestBody);
}
Status.UpdateStatus(20, "Waiting for Response");
using (HttpWebResponse webResponse = (HttpWebResponse)webRequest.GetResponse())
{
if (webResponse.StatusCode == HttpStatusCode.OK)
{
Status.UpdateStatus(45, "Reading Response");
using (var wResStream = webResponse.GetResponseStream())
{
XmlSerializer xml = new XmlSerializer(typeof(PluginLibraryUpdateResponse));
updateResult = (PluginLibraryUpdateResponse)xml.Deserialize(wResStream);
}
}
else
{
Status.SetTaskException(new WebException(string.Format("Server responded with: [{0}] {1}", webResponse.StatusCode, webResponse.StatusDescription)));
return;
}
}
if (!Directory.Exists(Path.GetDirectoryName(catalogueFile)))
Directory.CreateDirectory(Path.GetDirectoryName(catalogueFile));
using (FileStream fs = new FileStream(catalogueFile, FileMode.Create, FileAccess.Write, FileShare.None))
{
using (StreamWriter fsWriter = new StreamWriter(fs))
{
fsWriter.Write(JsonConvert.SerializeObject(updateResult));
fsWriter.Flush();
}
}
#endregion
#region Compatibility
Status.UpdateStatus(50, "Updating Plugin Library Compatibility", "Building Request");
using (DiscoDataContext dbContext = new DiscoDataContext())
{
compatibilityFile = Plugins.CompatibilityFile(dbContext);
compatRequestBody = new PluginLibraryCompatibilityRequest()
{
DeploymentId = dbContext.DiscoConfiguration.DeploymentId,
HostVersion = typeof(Plugins).Assembly.GetName().Version.ToString(4)
};
}
Status.UpdateStatus(60, "Sending Request");
webRequest = (HttpWebRequest)HttpWebRequest.Create(PluginLibraryCompatibilityUrl());
webRequest.KeepAlive = false;
webRequest.ContentType = "application/json";
webRequest.Method = WebRequestMethods.Http.Post;
webRequest.UserAgent = string.Format("Disco/{0} (PluginLibrary)", DiscoBIVersion);
using (var wrStream = webRequest.GetRequestStream())
{
XmlSerializer xml = new XmlSerializer(typeof(PluginLibraryCompatibilityRequest));
xml.Serialize(wrStream, compatRequestBody);
}
Status.UpdateStatus(70, "Waiting for Response");
using (HttpWebResponse webResponse = (HttpWebResponse)webRequest.GetResponse())
{
if (webResponse.StatusCode == HttpStatusCode.OK)
{
Status.UpdateStatus(95, "Reading Response");
using (var wResStream = webResponse.GetResponseStream())
{
XmlSerializer xml = new XmlSerializer(typeof(PluginLibraryCompatibilityResponse));
compatResult = (PluginLibraryCompatibilityResponse)xml.Deserialize(wResStream);
}
}
else
{
Status.SetTaskException(new WebException(string.Format("Server responded with: [{0}] {1}", webResponse.StatusCode, webResponse.StatusDescription)));
return;
}
}
if (!Directory.Exists(Path.GetDirectoryName(compatibilityFile)))
Directory.CreateDirectory(Path.GetDirectoryName(compatibilityFile));
using (FileStream fs = new FileStream(compatibilityFile, FileMode.Create, FileAccess.Write, FileShare.None))
{
using (StreamWriter fsWriter = new StreamWriter(fs))
{
fsWriter.Write(JsonConvert.SerializeObject(compatResult));
fsWriter.Flush();
}
}
#endregion
}
private static string PluginLibraryUpdateUrl()
{
return string.Concat(Disco.Data.Configuration.CommunityHelpers.CommunityUrl(), "DiscoPluginLibrary/V1");
}
private static string PluginLibraryCompatibilityUrl()
{
return string.Concat(Disco.Data.Configuration.CommunityHelpers.CommunityUrl(), "DiscoPluginLibrary/CompatibilityV1");
}
public static ScheduledTaskStatus ScheduleNow()
{
var taskStatus = ScheduledTasks.GetTaskStatuses(typeof(PluginLibraryUpdateTask)).Where(ts => ts.IsRunning).FirstOrDefault();
if (taskStatus != null)
return taskStatus;
else
{
var t = new PluginLibraryUpdateTask();
return t.ScheduleTask();
}
}
public static ScheduledTaskStatus RunningStatus
{
get
{
return ScheduledTasks.GetTaskStatuses(typeof(PluginLibraryUpdateTask)).Where(ts => ts.IsRunning).FirstOrDefault();
}
}
public override void InitalizeScheduledTask(DiscoDataContext dbContext)
{
// Random time between midday and midnight.
var rnd = new Random();
var rndHour = rnd.Next(12, 23);
var rndMinute = rnd.Next(0, 59);
TriggerBuilder triggerBuilder = TriggerBuilder.Create().
WithSchedule(CronScheduleBuilder.DailyAtHourAndMinute(rndHour, rndMinute));
this.ScheduleTask(triggerBuilder);
}
}
}
@@ -158,6 +158,10 @@ namespace Disco.Services.Plugins
{
if (ScheduledTasks.GetTaskStatuses(typeof(InstallPluginTask)).Where(s => s.IsRunning).Count() > 0)
throw new InvalidOperationException("A plugin is already being Installed");
if (ScheduledTasks.GetTaskStatuses(typeof(UpdatePluginTask)).Where(s => s.IsRunning).Count() > 0)
throw new InvalidOperationException("A plugin is being Updated");
if (ScheduledTasks.GetTaskStatuses(typeof(UninstallPluginTask)).Where(s => s.IsRunning).Count() > 0)
throw new InvalidOperationException("A plugin is being Uninstalled");
JobDataMap taskData = new JobDataMap() { { "PackageUrl", PackageUrl }, { "PackageFilePath", PackageFilePath }, { "DeletePackageAfterInstall", DeletePackageAfterInstall } };
+16
View File
@@ -348,6 +348,14 @@ namespace Disco.Services.Plugins
return handler;
}
[JsonIgnore]
public string ConfigurationUrl
{
get
{
return string.Format("/Config/Plugins/{0}", HttpUtility.UrlEncode(this.Id));
}
}
[JsonIgnore]
public bool HasWebHandler
{
get
@@ -371,6 +379,14 @@ namespace Disco.Services.Plugins
return handler;
}
[JsonIgnore]
public string WebHandlerUrl
{
get
{
return string.Format("/Plugin/{0}", HttpUtility.UrlEncode(this.Id));
}
}
public Tuple<string, string> WebResourcePath(string Resource)
{
+22
View File
@@ -10,6 +10,7 @@ using System.IO.Compression;
using Disco.Models.BI.Interop.Community;
using System.Web;
using Newtonsoft.Json;
using System.Threading;
namespace Disco.Services.Plugins
{
@@ -477,6 +478,27 @@ namespace Disco.Services.Plugins
return categoryDisplayNames;
}
#region Restart App
private static object _restartTimerLock = new object();
private static Timer _restartTimer;
internal static void RestartApp(int DelayMilliseconds)
{
lock (_restartTimerLock)
{
if (_restartTimer != null)
{
_restartTimer.Dispose();
}
_restartTimer = new Timer((state) =>
{
HttpRuntime.UnloadAppDomain();
//AppDomain.Unload(AppDomain.CurrentDomain);
}, null, DelayMilliseconds, Timeout.Infinite);
}
}
#endregion
#region Plugin Referenced Assemblies Resolving
public static Assembly CurrentDomain_AssemblyResolve(object sender, ResolveEventArgs args)
+5 -22
View File
@@ -45,13 +45,17 @@ namespace Disco.Services.Plugins
}
this.Status.Finished("Restarting Disco, please wait...", "/Config/Plugins");
RestartApp(1500);
Plugins.RestartApp(1500);
}
public static ScheduledTaskStatus UninstallPlugin(PluginManifest Manifest, bool UninstallData)
{
if (ScheduledTasks.GetTaskStatuses(typeof(InstallPluginTask)).Where(s => s.IsRunning).Count() > 0)
throw new InvalidOperationException("A plugin is already being Uninstalled");
if (ScheduledTasks.GetTaskStatuses(typeof(UpdatePluginTask)).Where(s => s.IsRunning).Count() > 0)
throw new InvalidOperationException("A plugin is being Updated");
if (ScheduledTasks.GetTaskStatuses(typeof(InstallPluginTask)).Where(s => s.IsRunning).Count() > 0)
throw new InvalidOperationException("A plugin is being Installed");
JobDataMap taskData = new JobDataMap() { { "PluginManifest", Manifest }, { "UninstallData", UninstallData } };
@@ -59,26 +63,5 @@ namespace Disco.Services.Plugins
return instance.ScheduleTask(taskData);
}
#region Restart App
private static object _restartTimerLock = new object();
private static Timer _restartTimer;
private void RestartApp(int DelayMilliseconds)
{
lock (_restartTimerLock)
{
if (_restartTimer != null)
{
_restartTimer.Dispose();
}
_restartTimer = new Timer((state) =>
{
HttpRuntime.UnloadAppDomain();
//AppDomain.Unload(AppDomain.CurrentDomain);
}, null, DelayMilliseconds, Timeout.Infinite);
}
}
#endregion
}
}
+252
View File
@@ -0,0 +1,252 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.IO.Compression;
using System.Linq;
using System.Net;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Web;
using Disco.Data.Repository;
using Disco.Models.BI.Interop.Community;
using Disco.Services.Tasks;
using Quartz;
namespace Disco.Services.Plugins
{
public class UpdatePluginTask : ScheduledTask
{
public override string TaskName { get { return "Updating Plugin/s"; } }
protected override void ExecuteTask()
{
string pluginId = (string)this.ExecutionContext.JobDetail.JobDataMap["PluginId"];
string packageFilePath = (string)this.ExecutionContext.JobDetail.JobDataMap["PackageFilePath"];
PluginLibraryUpdateResponse catalogue;
string pluginPackagesLocation;
if (!Plugins.PluginsLoaded)
throw new InvalidOperationException("Plugins have not been initialized");
List<Tuple<PluginManifest, string, PluginLibraryItem>> updatePlugins;
using (DiscoDataContext dbContext = new DiscoDataContext())
{
catalogue = Plugins.LoadCatalogue(dbContext);
pluginPackagesLocation = dbContext.DiscoConfiguration.PluginPackagesLocation;
}
if (!string.IsNullOrEmpty(pluginId))
{
if (string.IsNullOrEmpty(packageFilePath))
{
// Update Single from Catalogue
PluginManifest existingManifest = Plugins.GetPlugin(pluginId);
var catalogueItem = catalogue.Plugins.FirstOrDefault(p => p.Id == existingManifest.Id);
if (catalogueItem == null)
throw new InvalidOperationException("No updates are available for this Plugin");
if (Version.Parse(catalogueItem.LatestVersion) <= existingManifest.Version)
throw new InvalidOperationException("Only newer versions can be used to update a plugin");
updatePlugins = new List<Tuple<PluginManifest, string, PluginLibraryItem>>() {
new Tuple<PluginManifest,string,PluginLibraryItem>(existingManifest, null, catalogueItem)
};
}
else
{
// Update Single from Local
PluginManifest existingManifest = Plugins.GetPlugin(pluginId);
updatePlugins = new List<Tuple<PluginManifest, string, PluginLibraryItem>>() {
new Tuple<PluginManifest,string,PluginLibraryItem>(existingManifest, packageFilePath, null)
};
}
}
else
{
// Update All
updatePlugins = Plugins.GetPlugins().Join((IEnumerable<PluginLibraryItem>)catalogue.Plugins, manifest => manifest.Id, update => update.Id, (manifest, update) => new Tuple<PluginManifest, string, PluginLibraryItem>(manifest, null, update)).Where(i => Version.Parse(i.Item3.LatestVersion) > i.Item1.Version).ToList();
}
if (updatePlugins == null || updatePlugins.Count == 0)
{
this.Status.Finished("No plugins to update...", "/Config/Plugins");
return;
}
ExecuteTaskInternal(this.Status, pluginPackagesLocation, updatePlugins);
this.Status.Finished("Restarting Disco, please wait...", "/Config/Plugins");
Plugins.RestartApp(1500);
}
internal static void UpdateOffline(ScheduledTaskStatus Status)
{
string pluginsLocation;
string pluginPackagesLocation;
PluginLibraryUpdateResponse pluginCatalogue;
List<Tuple<PluginManifest, string, PluginLibraryItem>> UpdatePlugins = new List<Tuple<PluginManifest, string, PluginLibraryItem>>();
using (DiscoDataContext dbContext = new DiscoDataContext())
{
pluginCatalogue = Plugins.LoadCatalogue(dbContext);
pluginsLocation = dbContext.DiscoConfiguration.PluginsLocation;
pluginPackagesLocation = dbContext.DiscoConfiguration.PluginPackagesLocation;
}
DirectoryInfo pluginDirectoryRoot = new DirectoryInfo(pluginsLocation);
if (pluginDirectoryRoot.Exists)
{
foreach (DirectoryInfo pluginDirectory in pluginDirectoryRoot.EnumerateDirectories())
{
string pluginManifestFilename = Path.Combine(pluginDirectory.FullName, "manifest.json");
if (File.Exists(pluginManifestFilename))
{
PluginManifest pluginManifest = null;
try
{
pluginManifest = PluginManifest.FromPluginManifestFile(pluginManifestFilename);
}
catch (Exception) { }
if (pluginManifest != null)
{
// Check for Update
var catalogueItem = pluginCatalogue.Plugins.FirstOrDefault(i => i.Id == pluginManifest.Id && Version.Parse(i.LatestVersion) > pluginManifest.Version);
if (catalogueItem != null)
{ // Update Available
UpdatePlugins.Add(new Tuple<PluginManifest, string, PluginLibraryItem>(pluginManifest, null, catalogueItem));
}
}
}
}
}
if (UpdatePlugins.Count > 0)
{
ExecuteTaskInternal(Status, pluginPackagesLocation, UpdatePlugins);
}
}
internal static void ExecuteTaskInternal(ScheduledTaskStatus Status, string pluginPackagesLocation, List<Tuple<PluginManifest, string, PluginLibraryItem>> UpdatePlugins)
{
while (UpdatePlugins.Count > 0)
{
var updatePlugin = UpdatePlugins[0];
var existingManifest = updatePlugin.Item1;
var packageTempFilePath = updatePlugin.Item2;
var catalogueItem = updatePlugin.Item3;
UpdatePlugins.Remove(updatePlugin);
var pluginId = existingManifest != null ? existingManifest.Id : catalogueItem.Id;
var pluginName = existingManifest != null ? existingManifest.Name : catalogueItem.Name;
if (string.IsNullOrEmpty(packageTempFilePath))
{
// Download Update
Status.UpdateStatus(0, string.Format("Downloading Plugin Package: {0}", pluginName), "Connecting...");
packageTempFilePath = Path.Combine(pluginPackagesLocation, string.Format("{0}.discoPlugin", pluginId));
if (File.Exists(packageTempFilePath))
File.Delete(packageTempFilePath);
if (!Directory.Exists(Path.GetDirectoryName(packageTempFilePath)))
Directory.CreateDirectory(Path.GetDirectoryName(packageTempFilePath));
// Need to Download the Package
WebClient downloader = new WebClient();
DateTime progressExpires = DateTime.Now;
downloader.DownloadProgressChanged += (sender, e) =>
{
Console.WriteLine(e.ProgressPercentage);
if (progressExpires <= DateTime.Now)
{
Status.UpdateStatus(e.ProgressPercentage, string.Format("{0} of {1} KB downloaded", e.BytesReceived / 1024, e.TotalBytesToReceive / 1024));
// Throttle Updates for SignalR
progressExpires = DateTime.Now.AddMilliseconds(250);
}
};
downloader.DownloadFileTaskAsync(new Uri(catalogueItem.LatestDownloadUrl), packageTempFilePath).Wait();
}
Status.UpdateStatus(10, "Opening Plugin Package", Path.GetFileName(packageTempFilePath));
PluginManifest updateManifest;
using (var packageStream = File.OpenRead(packageTempFilePath))
{
using (ZipArchive packageArchive = new ZipArchive(packageStream, ZipArchiveMode.Read, false))
{
ZipArchiveEntry packageManifestEntry = packageArchive.GetEntry("manifest.json");
if (packageManifestEntry == null)
throw new InvalidDataException("The plugin package does not contain the 'manifest.json' entry");
using (Stream packageManifestStream = packageManifestEntry.Open())
{
updateManifest = PluginManifest.FromPluginManifestFile(packageManifestStream);
}
}
}
// Ensure not already installed
if (existingManifest != null)
if (updateManifest.Version <= existingManifest.Version)
throw new InvalidOperationException("Only newer versions can be used to update a plugin");
Status.UpdateStatus(20, string.Format("{0} [{1} v{2}] by {3}", updateManifest.Name, updateManifest.Id, updateManifest.Version.ToString(4), updateManifest.Author), "Initializing Update Environment");
using (DiscoDataContext dbContext = new DiscoDataContext())
{
// Check for Compatibility
var compatibilityData = Plugins.LoadCompatibilityData(dbContext);
var pluginCompatibility = compatibilityData.Plugins.FirstOrDefault(i => i.Id.Equals(updateManifest.Id, StringComparison.InvariantCultureIgnoreCase) && updateManifest.Version == Version.Parse(i.Version));
if (pluginCompatibility != null && !pluginCompatibility.Compatible)
throw new InvalidOperationException(string.Format("The plugin [{0} v{1}] is not compatible: {2}", updateManifest.Id, updateManifest.VersionFormatted, pluginCompatibility.Reason));
var updatePluginPath = Path.Combine(dbContext.DiscoConfiguration.PluginsLocation, string.Format("{0}.discoPlugin", updateManifest.Id));
File.Move(packageTempFilePath, updatePluginPath);
if (existingManifest != null && Plugins.PluginsLoaded)
{
PluginsLog.LogBeforeUpdate(existingManifest, updateManifest);
existingManifest.BeforePluginUpdate(dbContext, updateManifest, Status);
}
}
}
}
private static ScheduledTaskStatus UpdateHelper(string PluginId = null, string PackageFilePath = null)
{
if (ScheduledTasks.GetTaskStatuses(typeof(UpdatePluginTask)).Where(s => s.IsRunning).Count() > 0)
throw new InvalidOperationException("A plugin is already being Updated");
if (ScheduledTasks.GetTaskStatuses(typeof(UninstallPluginTask)).Where(s => s.IsRunning).Count() > 0)
throw new InvalidOperationException("A plugin is being Uninstalled");
if (ScheduledTasks.GetTaskStatuses(typeof(InstallPluginTask)).Where(s => s.IsRunning).Count() > 0)
throw new InvalidOperationException("A plugin is being Installed");
JobDataMap taskData = new JobDataMap() { { "PluginId", PluginId }, { "PackageFilePath", PackageFilePath } };
var instance = new UpdatePluginTask();
return instance.ScheduleTask(taskData);
}
public static ScheduledTaskStatus UpdateLocalPlugin(string PluginId, string PackageFilePath)
{
return UpdateHelper(PluginId, PackageFilePath);
}
public static ScheduledTaskStatus UpdatePlugin(string PluginId)
{
return UpdateHelper(PluginId);
}
public static ScheduledTaskStatus UpdateAllPlugins()
{
return UpdateHelper();
}
}
}
@@ -0,0 +1,77 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Disco.Services.Tasks;
namespace Disco.Services.Plugins
{
public class UpdatePluginsAfterDiscoUpdateTask : ScheduledTask
{
private static object _startLock = new object();
public override string TaskName { get { return "Updating Disco Plugins"; } }
protected override void ExecuteTask()
{
this.Status.UpdateStatus(0, "Updating plugins after Disco update", "Starting, please wait...");
// Wait for App to Load (6 Seconds)
for (int i = 0; i < 10; i++)
{
this.Status.UpdateStatus(10 * i);
System.Threading.Thread.Sleep(600);
}
// Update Catalogue
CommunityInterop.PluginLibraryUpdateTask.ExecuteTaskInternal(this.Status);
// Update all Plugins
UpdatePluginTask.UpdateOffline(this.Status);
// Restart
this.Status.Finished("Restarting Disco, please wait...", "/");
Plugins.RestartApp(1500);
}
public static ScheduledTaskStatus UpdateDiscoPlugins(bool ReturnExistingStatusIfRunning)
{
if (ScheduledTasks.GetTaskStatuses(typeof(UpdatePluginTask)).Where(s => s.IsRunning).Count() > 0)
throw new InvalidOperationException("A plugin is already being Updated");
if (ScheduledTasks.GetTaskStatuses(typeof(UninstallPluginTask)).Where(s => s.IsRunning).Count() > 0)
throw new InvalidOperationException("A plugin is being Uninstalled");
if (ScheduledTasks.GetTaskStatuses(typeof(InstallPluginTask)).Where(s => s.IsRunning).Count() > 0)
throw new InvalidOperationException("A plugin is being Installed");
var existingTask = ScheduledTasks.GetTaskStatuses(typeof(UpdatePluginsAfterDiscoUpdateTask)).Where(s => s.IsRunning).FirstOrDefault();
if (existingTask != null)
{
if (ReturnExistingStatusIfRunning)
return existingTask;
else
throw new InvalidOperationException("Plugins are already being updated");
}
lock (_startLock)
{
existingTask = ScheduledTasks.GetTaskStatuses(typeof(UpdatePluginsAfterDiscoUpdateTask)).Where(s => s.IsRunning).FirstOrDefault();
if (existingTask != null)
{
if (ReturnExistingStatusIfRunning)
return existingTask;
else
throw new InvalidOperationException("Plugins are already being updated");
}
var instance = new UpdatePluginsAfterDiscoUpdateTask();
var status = instance.ScheduleTask();
// Give the scheduler a chance to start the task
System.Threading.Thread.Sleep(150);
return status;
}
}
}
}