Update: SignalR 2.0.3 Migration; Noticeboards
Migrate all SignalR 1.x Persistent Connections to SignalR 2.x Hubs. Abstracts ScheduledTaskStatus with core interface and adds a Mock for optional status reporting. Noticeboards rewritten (with new theme) to be more resilient and accurate.
This commit is contained in:
@@ -0,0 +1,86 @@
|
||||
using Disco.Data.Repository.Monitor;
|
||||
using Disco.Services.Authorization;
|
||||
using Disco.Services.Web.Signalling;
|
||||
using Microsoft.AspNet.SignalR;
|
||||
using Microsoft.AspNet.SignalR.Hubs;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using System.Reactive.Linq;
|
||||
using Disco.Models.Repository;
|
||||
using Disco.Data.Repository;
|
||||
|
||||
namespace Disco.Services.Devices
|
||||
{
|
||||
[HubName("deviceUpdates"), DiscoHubAuthorizeAll(Claims.Device.Show, Claims.Device.ShowAttachments)]
|
||||
public class DeviceUpdatesHub : Hub
|
||||
{
|
||||
private const string UserPrefix = "Device_";
|
||||
public static IHubContext HubContext { get; private set; }
|
||||
|
||||
private static IDisposable RepositoryBeforeSubscription;
|
||||
private static IDisposable RepositoryAfterSubscription;
|
||||
|
||||
static DeviceUpdatesHub()
|
||||
{
|
||||
HubContext = GlobalHost.ConnectionManager.GetHubContext<DeviceUpdatesHub>();
|
||||
|
||||
// Subscribe to Repository Monitor for Changes
|
||||
RepositoryBeforeSubscription = RepositoryMonitor.StreamBeforeCommit
|
||||
.Where(e => e.EntityType == typeof(DeviceAttachment) && e.EventType == RepositoryMonitorEventType.Deleted)
|
||||
.Subscribe(RepositoryEventBefore);
|
||||
RepositoryAfterSubscription = RepositoryMonitor.StreamAfterCommit
|
||||
.Where(e => e.EntityType == typeof(DeviceAttachment) && e.EventType == RepositoryMonitorEventType.Added)
|
||||
.Subscribe(RepositoryAfterEvent);
|
||||
}
|
||||
|
||||
private static string GroupName(string DeviceSerialNumber)
|
||||
{
|
||||
return UserPrefix + DeviceSerialNumber;
|
||||
}
|
||||
|
||||
public override Task OnConnected()
|
||||
{
|
||||
var deviceSerialNumber = Context.QueryString["DeviceSerialNumber"];
|
||||
|
||||
if (string.IsNullOrWhiteSpace(deviceSerialNumber))
|
||||
throw new ArgumentNullException("DeviceSerialNumber");
|
||||
|
||||
Groups.Add(Context.ConnectionId, GroupName(deviceSerialNumber));
|
||||
|
||||
return base.OnConnected();
|
||||
}
|
||||
|
||||
private static void RepositoryEventBefore(RepositoryMonitorEvent e)
|
||||
{
|
||||
if (e.EventType == RepositoryMonitorEventType.Deleted)
|
||||
{
|
||||
if (e.EntityType == typeof(DeviceAttachment))
|
||||
{
|
||||
var repositoryAttachment = (DeviceAttachment)e.Entity;
|
||||
string attachmentDeviceSerialNumber;
|
||||
|
||||
using (DiscoDataContext Database = new DiscoDataContext())
|
||||
attachmentDeviceSerialNumber = Database.DeviceAttachments.Where(a => a.Id == repositoryAttachment.Id).Select(a => a.DeviceSerialNumber).First();
|
||||
|
||||
HubContext.Clients.Group(GroupName(attachmentDeviceSerialNumber)).removeAttachment(repositoryAttachment.Id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void RepositoryAfterEvent(RepositoryMonitorEvent e)
|
||||
{
|
||||
if (e.EventType == RepositoryMonitorEventType.Added)
|
||||
{
|
||||
if (e.EntityType == typeof(DeviceAttachment))
|
||||
{
|
||||
var a = (DeviceAttachment)e.Entity;
|
||||
|
||||
HubContext.Clients.Group(GroupName(a.DeviceSerialNumber)).addAttachment(a.Id);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -16,7 +16,7 @@ namespace Disco.Services.Devices.Exporting
|
||||
public static class DeviceExport
|
||||
{
|
||||
|
||||
public static DeviceExportResult GenerateExport(DiscoDataContext Database, IQueryable<Device> Devices, DeviceExportOptions Options, IScheduledTaskBasicStatus TaskStatus)
|
||||
public static DeviceExportResult GenerateExport(DiscoDataContext Database, IQueryable<Device> Devices, DeviceExportOptions Options, IScheduledTaskStatus TaskStatus)
|
||||
{
|
||||
TaskStatus.UpdateStatus(15, "Building metadata and database query");
|
||||
var metadata = Options.BuildMetadata();
|
||||
@@ -32,15 +32,17 @@ namespace Disco.Services.Devices.Exporting
|
||||
Options.AssignedUserEmailAddress)
|
||||
{
|
||||
TaskStatus.UpdateStatus(20, "Updating Assigned User details");
|
||||
var users = Devices.Where(d => d.AssignedUserId != null).Select(d => d.AssignedUserId).Distinct().ToList();
|
||||
|
||||
Devices.Where(d => d.AssignedUserId != null).Select(d => d.AssignedUserId).Distinct().ToList().ForEach(userId =>
|
||||
users.Select((userId, index) =>
|
||||
{
|
||||
TaskStatus.UpdateStatus(20 + (((double)20 / users.Count) * index), string.Format("Updating Assigned User details: {0}", userId));
|
||||
try
|
||||
{
|
||||
UserService.GetUser(userId, Database);
|
||||
return UserService.GetUser(userId, Database);
|
||||
}
|
||||
catch (Exception) { } // Ignore Errors
|
||||
});
|
||||
catch (Exception) { return null; } // Ignore Errors
|
||||
}).ToList();
|
||||
}
|
||||
|
||||
// Update Last Network Logon Date
|
||||
@@ -49,8 +51,16 @@ namespace Disco.Services.Devices.Exporting
|
||||
TaskStatus.UpdateStatus(40, "Updating device last network logon dates");
|
||||
try
|
||||
{
|
||||
Interop.ActiveDirectory.ADTaskUpdateNetworkLogonDates.UpdateLastNetworkLogonDates(Database, ScheduledTaskMockStatus.Create());
|
||||
TaskStatus.IgnoreCurrentProcessChanges = true;
|
||||
TaskStatus.ProgressMultiplier = 20 / 100;
|
||||
TaskStatus.ProgressOffset = 40;
|
||||
|
||||
Interop.ActiveDirectory.ADTaskUpdateNetworkLogonDates.UpdateLastNetworkLogonDates(Database, TaskStatus);
|
||||
Database.SaveChanges();
|
||||
|
||||
TaskStatus.IgnoreCurrentProcessChanges = false;
|
||||
TaskStatus.ProgressMultiplier = 1;
|
||||
TaskStatus.ProgressOffset = 0;
|
||||
}
|
||||
catch (Exception) { } // Ignore Errors
|
||||
}
|
||||
@@ -103,7 +113,7 @@ namespace Disco.Services.Devices.Exporting
|
||||
return GenerateExport(Database, Devices, Options, ScheduledTaskMockStatus.Create());
|
||||
}
|
||||
|
||||
public static DeviceExportResult GenerateExport(DiscoDataContext Database, DeviceExportOptions Options, IScheduledTaskBasicStatus TaskStatus)
|
||||
public static DeviceExportResult GenerateExport(DiscoDataContext Database, DeviceExportOptions Options, IScheduledTaskStatus TaskStatus)
|
||||
{
|
||||
switch (Options.ExportType)
|
||||
{
|
||||
|
||||
@@ -102,7 +102,7 @@ namespace Disco.Services.Devices.Importing
|
||||
Context.Header = Context.Header.Zip(HeaderTypes, (h, ht) => Tuple.Create(h.Item1, ht)).ToList();
|
||||
}
|
||||
|
||||
public static void ParseRecords(this DeviceImportContext Context, DiscoDataContext Database, IScheduledTaskBasicStatus Status)
|
||||
public static void ParseRecords(this DeviceImportContext Context, DiscoDataContext Database, IScheduledTaskStatus Status)
|
||||
{
|
||||
if (Context.Header == null)
|
||||
throw new InvalidOperationException("The Import Context has not been initialized");
|
||||
@@ -129,18 +129,13 @@ namespace Disco.Services.Devices.Importing
|
||||
.Select(h => new Tuple<string, DeviceImportFieldTypes, Func<string[], string>, Type>(h.Item1, h.Item2, (f) => f[h.Item3], DeviceImport.FieldHandlers.Value[h.Item2]))
|
||||
.ToList();
|
||||
|
||||
DateTime nextProgress = DateTime.Now;
|
||||
Status.UpdateStatus(0, "Parsing Import Records", "Starting...");
|
||||
|
||||
Context.Records = Context.RawData.Select((d, recordIndex) =>
|
||||
{
|
||||
string deviceSerialNumber = Fields.DeviceSerialNumberImportField.ParseRawDeviceSerialNumber(d[Context.HeaderDeviceSerialNumberIndex]);
|
||||
|
||||
if (nextProgress <= DateTime.Now)
|
||||
{
|
||||
Status.UpdateStatus(((double)recordIndex / Context.RawData.Count) * 100, string.Format("Parsing: {0}", deviceSerialNumber));
|
||||
nextProgress = DateTime.Now.AddSeconds(.5);
|
||||
}
|
||||
Status.UpdateStatus(((double)recordIndex / Context.RawData.Count) * 100, string.Format("Parsing: {0}", deviceSerialNumber));
|
||||
|
||||
Device existingDevice = null;
|
||||
if (Fields.DeviceSerialNumberImportField.IsDeviceSerialNumberValid(deviceSerialNumber))
|
||||
@@ -170,7 +165,7 @@ namespace Disco.Services.Devices.Importing
|
||||
}).Cast<IDeviceImportRecord>().ToList();
|
||||
}
|
||||
|
||||
public static int ApplyRecords(this DeviceImportContext Context, DiscoDataContext Database, IScheduledTaskBasicStatus Status)
|
||||
public static int ApplyRecords(this DeviceImportContext Context, DiscoDataContext Database, IScheduledTaskStatus Status)
|
||||
{
|
||||
if (Context.Records == null)
|
||||
throw new InvalidOperationException("Import Records have not been parsed");
|
||||
@@ -178,18 +173,13 @@ namespace Disco.Services.Devices.Importing
|
||||
if (Context.Records.Count == 0)
|
||||
throw new InvalidOperationException("There are no records to import");
|
||||
|
||||
DateTime nextProgress = DateTime.Now;
|
||||
Status.UpdateStatus(0, "Applying Import Records to Database", "Starting...");
|
||||
|
||||
int affectedRecords = 0;
|
||||
|
||||
foreach (var record in Context.Records.Cast<DeviceImportRecord>().Select((r, i) => Tuple.Create(r, i)))
|
||||
{
|
||||
if (nextProgress <= DateTime.Now)
|
||||
{
|
||||
Status.UpdateStatus(((double)record.Item2 / Context.Records.Count) * 100, string.Format("Applying: {0}", record.Item1.DeviceSerialNumber));
|
||||
nextProgress = DateTime.Now.AddSeconds(.5);
|
||||
}
|
||||
Status.UpdateStatus(((double)record.Item2 / Context.Records.Count) * 100, string.Format("Applying: {0}", record.Item1.DeviceSerialNumber));
|
||||
|
||||
if (record.Item1.Apply(Database))
|
||||
affectedRecords++;
|
||||
|
||||
@@ -42,21 +42,21 @@
|
||||
<Reference Include="LumenWorks.Framework.IO">
|
||||
<HintPath>..\packages\LumenWorks.Framework.IO.3.8.0\lib\net20\LumenWorks.Framework.IO.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="Microsoft.AspNet.SignalR.Core, Version=1.1.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL">
|
||||
<SpecificVersion>False</SpecificVersion>
|
||||
<HintPath>..\packages\Microsoft.AspNet.SignalR.Core.1.1.2\lib\net40\Microsoft.AspNet.SignalR.Core.dll</HintPath>
|
||||
<Reference Include="Microsoft.AspNet.SignalR.Core">
|
||||
<HintPath>..\packages\Microsoft.AspNet.SignalR.Core.2.0.3\lib\net45\Microsoft.AspNet.SignalR.Core.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="Microsoft.AspNet.SignalR.Owin, Version=1.1.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL">
|
||||
<SpecificVersion>False</SpecificVersion>
|
||||
<HintPath>..\packages\Microsoft.AspNet.SignalR.Owin.1.1.2\lib\net45\Microsoft.AspNet.SignalR.Owin.dll</HintPath>
|
||||
<Reference Include="Microsoft.Owin">
|
||||
<HintPath>..\packages\Microsoft.Owin.2.0.1\lib\net45\Microsoft.Owin.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="Microsoft.Owin.Security">
|
||||
<HintPath>..\packages\Microsoft.Owin.Security.2.0.1\lib\net45\Microsoft.Owin.Security.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="Microsoft.Web.Infrastructure, Version=1.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL">
|
||||
<Private>True</Private>
|
||||
<HintPath>..\packages\Microsoft.Web.Infrastructure.1.0.0.0\lib\net40\Microsoft.Web.Infrastructure.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="Newtonsoft.Json, Version=4.5.0.0, Culture=neutral, PublicKeyToken=30ad4fe6b2a6aeed, processorArchitecture=MSIL">
|
||||
<SpecificVersion>False</SpecificVersion>
|
||||
<HintPath>..\packages\Newtonsoft.Json.5.0.8\lib\net45\Newtonsoft.Json.dll</HintPath>
|
||||
<Reference Include="Newtonsoft.Json">
|
||||
<HintPath>..\packages\Newtonsoft.Json.5.0.1\lib\net45\Newtonsoft.Json.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="Owin">
|
||||
<HintPath>..\packages\Owin.1.0\lib\net40\Owin.dll</HintPath>
|
||||
@@ -201,7 +201,9 @@
|
||||
<Compile Include="Devices\Importing\Fields\DeviceSerialNumberImportField.cs" />
|
||||
<Compile Include="Devices\Importing\Fields\ModelIdImportField.cs" />
|
||||
<Compile Include="Devices\Importing\IDeviceImportCache.cs" />
|
||||
<Compile Include="Devices\DeviceUpdatesHub.cs" />
|
||||
<Compile Include="Extensions\DateTimeExtensions.cs" />
|
||||
<Compile Include="Extensions\RxExtensions.cs" />
|
||||
<Compile Include="Extensions\StringExtensions.cs" />
|
||||
<Compile Include="Interop\ActiveDirectory\ActiveDirectory.cs" />
|
||||
<Compile Include="Interop\ActiveDirectory\ActiveDirectoryContext.cs" />
|
||||
@@ -227,6 +229,11 @@
|
||||
<Compile Include="Jobs\JobQueues\JobQueueService.cs" />
|
||||
<Compile Include="Jobs\JobQueues\JobQueueToken.cs" />
|
||||
<Compile Include="Jobs\JobLists\ManagedJobList.cs" />
|
||||
<Compile Include="Jobs\JobUpdatesHub.cs" />
|
||||
<Compile Include="Jobs\Noticeboards\HeldDeviceItem.cs" />
|
||||
<Compile Include="Jobs\Noticeboards\NoticeboardUpdatesHub.cs" />
|
||||
<Compile Include="Jobs\Noticeboards\HeldDevices.cs" />
|
||||
<Compile Include="Jobs\Noticeboards\HeldDevicesForUsers.cs" />
|
||||
<Compile Include="Logging\LogBase.cs" />
|
||||
<Compile Include="Logging\LogContext.cs" />
|
||||
<Compile Include="Logging\LogReInitalizeJob.cs" />
|
||||
@@ -236,9 +243,8 @@
|
||||
<Compile Include="Logging\Models\LogModule.cs" />
|
||||
<Compile Include="Logging\ReadLogContext.cs" />
|
||||
<Compile Include="Logging\SystemLog.cs" />
|
||||
<Compile Include="Logging\Targets\LogLiveContext.cs" />
|
||||
<Compile Include="Logging\Targets\LogPersistContext.cs" />
|
||||
<Compile Include="Logging\Targets\LogPersistContextInitializer.cs" />
|
||||
<Compile Include="Logging\Persistance\LogPersistContext.cs" />
|
||||
<Compile Include="Logging\Persistance\LogPersistContextInitializer.cs" />
|
||||
<Compile Include="Logging\Utilities.cs" />
|
||||
<Compile Include="Plugins\CommunityInterop\PluginLibraryUpdateTask.cs" />
|
||||
<Compile Include="Plugins\Features\UIExtension\Results\LiteralResult.cs" />
|
||||
@@ -278,9 +284,10 @@
|
||||
<Compile Include="Plugins\WebPageHelper.cs" />
|
||||
<Compile Include="Properties\AssemblyInfo.cs" />
|
||||
<Compile Include="Searching\Search.cs" />
|
||||
<Compile Include="Tasks\IScheduledTaskBasicStatus.cs" />
|
||||
<Compile Include="Tasks\IScheduledTaskStatus.cs" />
|
||||
<Compile Include="Tasks\ScheduledTask.cs" />
|
||||
<Compile Include="Tasks\ScheduledTaskMockStatus.cs" />
|
||||
<Compile Include="Tasks\ScheduledTaskNotificationsHub.cs" />
|
||||
<Compile Include="Tasks\ScheduledTasks.cs" />
|
||||
<Compile Include="Tasks\ScheduledTasksLog.cs" />
|
||||
<Compile Include="Tasks\ScheduledTaskStatus.cs" />
|
||||
@@ -290,17 +297,24 @@
|
||||
<Compile Include="Users\CacheCleanTask.cs" />
|
||||
<Compile Include="Users\UserExtensions.cs" />
|
||||
<Compile Include="Users\UserService.cs" />
|
||||
<Compile Include="Users\UserUpdatesHub.cs" />
|
||||
<Compile Include="Web\AuthorizedController.cs" />
|
||||
<Compile Include="Web\AuthorizedDatabaseController.cs" />
|
||||
<Compile Include="Web\BaseController.cs" />
|
||||
<Compile Include="Web\Bundles\Bundle.cs" />
|
||||
<Compile Include="Web\Bundles\FileBundle.cs" />
|
||||
<Compile Include="Web\Bundles\IBundle.cs" />
|
||||
<Compile Include="Web\Bundles\BundleExtensions.cs" />
|
||||
<Compile Include="Web\Bundles\BundleHandler.cs" />
|
||||
<Compile Include="Web\Bundles\BundleModule.cs" />
|
||||
<Compile Include="Web\Bundles\BundleTable.cs" />
|
||||
<Compile Include="Web\Bundles\UrlBundle.cs" />
|
||||
<Compile Include="Web\DatabaseController.cs" />
|
||||
<Compile Include="Web\HandleErrorAttribute.cs" />
|
||||
<Compile Include="Web\HelperExtensions.cs" />
|
||||
<Compile Include="Web\Signalling\DiscoHubAuthorizeAllAttribute.cs" />
|
||||
<Compile Include="Web\Signalling\DiscoHubAuthorizeAnyAttribute.cs" />
|
||||
<Compile Include="Web\Signalling\DiscoHubAuthorizeAttribute.cs" />
|
||||
<Compile Include="Logging\LogNotificationsHub.cs" />
|
||||
<Compile Include="Web\WebViewPage.cs" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
@@ -328,7 +342,7 @@
|
||||
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
|
||||
<ProjectExtensions>
|
||||
<VisualStudio>
|
||||
<UserProperties BuildVersion_UseGlobalSettings="False" BuildVersion_DetectChanges="False" BuildVersion_BuildAction="Both" BuildVersion_StartDate="2011/7/1" BuildVersion_BuildVersioningStyle="None.DeltaBaseYear.MonthAndDayStamp.TimeStamp" BuildVersion_UpdateAssemblyVersion="True" BuildVersion_UpdateFileVersion="True" />
|
||||
<UserProperties BuildVersion_UpdateFileVersion="True" BuildVersion_UpdateAssemblyVersion="True" BuildVersion_BuildVersioningStyle="None.DeltaBaseYear.MonthAndDayStamp.TimeStamp" BuildVersion_StartDate="2011/7/1" BuildVersion_BuildAction="Both" BuildVersion_DetectChanges="False" BuildVersion_UseGlobalSettings="False" />
|
||||
</VisualStudio>
|
||||
</ProjectExtensions>
|
||||
<PropertyGroup>
|
||||
|
||||
@@ -0,0 +1,182 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Reactive.Concurrency;
|
||||
using System.Reactive.Disposables;
|
||||
using System.Reactive.Linq;
|
||||
|
||||
namespace Disco
|
||||
{
|
||||
public static class RxExtensions
|
||||
{
|
||||
|
||||
#region RxExtensions
|
||||
|
||||
public static IObservable<IEnumerable<T>> DelayBuffer<T>(this IObservable<T> source, TimeSpan delay)
|
||||
{
|
||||
return Observable.Create<IEnumerable<T>>(o =>
|
||||
{
|
||||
var gate = new object();
|
||||
var buffer = new List<T>();
|
||||
var trigger = (IDisposable)null;
|
||||
var subscription = (IDisposable)null;
|
||||
var scheduler = Scheduler.Default;
|
||||
|
||||
Action dump = () =>
|
||||
{
|
||||
var bts = buffer.ToArray();
|
||||
buffer = new List<T>();
|
||||
if (o != null)
|
||||
o.OnNext(bts);
|
||||
if (trigger != null)
|
||||
{
|
||||
trigger.Dispose();
|
||||
trigger = null;
|
||||
}
|
||||
};
|
||||
|
||||
Action dispose = () =>
|
||||
{
|
||||
if (subscription != null)
|
||||
subscription.Dispose();
|
||||
if (trigger != null)
|
||||
{
|
||||
trigger.Dispose();
|
||||
trigger = null;
|
||||
}
|
||||
};
|
||||
|
||||
Action<Action<IObserver<IEnumerable<T>>>> onErrorOrCompleted =
|
||||
onAction =>
|
||||
{
|
||||
lock (gate)
|
||||
{
|
||||
dispose();
|
||||
dump();
|
||||
if (o != null)
|
||||
onAction(o);
|
||||
}
|
||||
};
|
||||
|
||||
Action<Exception> onError = ex =>
|
||||
onErrorOrCompleted(x => x.OnError(ex));
|
||||
|
||||
Action onCompleted = () => onErrorOrCompleted(x => x.OnCompleted());
|
||||
|
||||
Action<T> onNext = t =>
|
||||
{
|
||||
lock (gate)
|
||||
{
|
||||
buffer.Add(t);
|
||||
|
||||
if (trigger == null)
|
||||
{
|
||||
trigger = scheduler.Schedule(delay, () =>
|
||||
{
|
||||
lock (gate)
|
||||
{
|
||||
dump();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
subscription =
|
||||
source
|
||||
.ObserveOn(scheduler)
|
||||
.Subscribe(onNext, onError, onCompleted);
|
||||
|
||||
return () =>
|
||||
{
|
||||
lock (gate)
|
||||
{
|
||||
o = null;
|
||||
dispose();
|
||||
}
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
public static IObservable<IEnumerable<T>> BufferWithInactivity<T>(this IObservable<T> source, TimeSpan inactivity)
|
||||
{
|
||||
return Observable.Create<IEnumerable<T>>(o =>
|
||||
{
|
||||
var gate = new object();
|
||||
var buffer = new List<T>();
|
||||
var mutable = new SerialDisposable();
|
||||
var subscription = (IDisposable)null;
|
||||
var scheduler = Scheduler.Default;
|
||||
|
||||
Action dump = () =>
|
||||
{
|
||||
var bts = buffer.ToArray();
|
||||
buffer = new List<T>();
|
||||
if (o != null)
|
||||
{
|
||||
o.OnNext(bts);
|
||||
}
|
||||
};
|
||||
|
||||
Action dispose = () =>
|
||||
{
|
||||
if (subscription != null)
|
||||
{
|
||||
subscription.Dispose();
|
||||
}
|
||||
mutable.Dispose();
|
||||
};
|
||||
|
||||
Action<Action<IObserver<IEnumerable<T>>>> onErrorOrCompleted =
|
||||
onAction =>
|
||||
{
|
||||
lock (gate)
|
||||
{
|
||||
dispose();
|
||||
dump();
|
||||
if (o != null)
|
||||
{
|
||||
onAction(o);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
Action<Exception> onError = ex =>
|
||||
onErrorOrCompleted(x => x.OnError(ex));
|
||||
|
||||
Action onCompleted = () => onErrorOrCompleted(x => x.OnCompleted());
|
||||
|
||||
Action<T> onNext = t =>
|
||||
{
|
||||
lock (gate)
|
||||
{
|
||||
buffer.Add(t);
|
||||
mutable.Disposable = scheduler.Schedule(inactivity, () =>
|
||||
{
|
||||
lock (gate)
|
||||
{
|
||||
dump();
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
subscription =
|
||||
source
|
||||
.ObserveOn(scheduler)
|
||||
.Subscribe(onNext, onError, onCompleted);
|
||||
|
||||
return () =>
|
||||
{
|
||||
lock (gate)
|
||||
{
|
||||
o = null;
|
||||
dispose();
|
||||
}
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
}
|
||||
}
|
||||
@@ -114,7 +114,7 @@ namespace Disco.Services.Interop.ActiveDirectory
|
||||
return false;
|
||||
}
|
||||
|
||||
public static void UpdateLastNetworkLogonDates(DiscoDataContext Database, IScheduledTaskBasicStatus status)
|
||||
public static void UpdateLastNetworkLogonDates(DiscoDataContext Database, IScheduledTaskStatus status)
|
||||
{
|
||||
var context = ActiveDirectory.Context;
|
||||
const string ldapFilter = "(objectCategory=Computer)";
|
||||
|
||||
@@ -72,7 +72,7 @@ namespace Disco.Services.Jobs.JobQueues
|
||||
|
||||
return _cache.UpdateQueue(JobQueue);
|
||||
}
|
||||
public static void DeleteJobQueue(DiscoDataContext Database, int JobQueueId, ScheduledTaskStatus Status)
|
||||
public static void DeleteJobQueue(DiscoDataContext Database, int JobQueueId, IScheduledTaskStatus Status)
|
||||
{
|
||||
JobQueue queue = Database.JobQueues.Find(JobQueueId);
|
||||
|
||||
|
||||
@@ -0,0 +1,124 @@
|
||||
using Disco.Data.Repository;
|
||||
using Disco.Data.Repository.Monitor;
|
||||
using Disco.Models.Repository;
|
||||
using Disco.Services.Authorization;
|
||||
using Disco.Services.Web.Signalling;
|
||||
using Microsoft.AspNet.SignalR;
|
||||
using Microsoft.AspNet.SignalR.Hubs;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Reactive.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Disco.Services.Users;
|
||||
|
||||
namespace Disco.Services.Jobs
|
||||
{
|
||||
[HubName("jobUpdates"), DiscoHubAuthorize(Claims.Job.Show)]
|
||||
public class JobUpdatesHub : Hub
|
||||
{
|
||||
private const string JobLogsPrefix = "JobLogs_";
|
||||
private const string JobAttachmentsPrefix = "JobAttachments_";
|
||||
public static IHubContext HubContext { get; private set; }
|
||||
|
||||
private static IDisposable RepositoryBeforeSubscription;
|
||||
private static IDisposable RepositoryAfterSubscription;
|
||||
|
||||
static JobUpdatesHub()
|
||||
{
|
||||
HubContext = GlobalHost.ConnectionManager.GetHubContext<JobUpdatesHub>();
|
||||
|
||||
// Subscribe to Repository Monitor for Changes
|
||||
RepositoryBeforeSubscription = RepositoryMonitor.StreamBeforeCommit
|
||||
.Where(e => (
|
||||
e.EntityType == typeof(JobLog) && e.EventType == RepositoryMonitorEventType.Deleted ||
|
||||
e.EntityType == typeof(JobAttachment) && e.EventType == RepositoryMonitorEventType.Deleted
|
||||
))
|
||||
.Subscribe(RepositoryEventBefore);
|
||||
RepositoryAfterSubscription = RepositoryMonitor.StreamAfterCommit
|
||||
.Where(e => (
|
||||
e.EntityType == typeof(JobLog) && e.EventType == RepositoryMonitorEventType.Added ||
|
||||
e.EntityType == typeof(JobAttachment) && e.EventType == RepositoryMonitorEventType.Added
|
||||
))
|
||||
.Subscribe(RepositoryAfterEvent);
|
||||
}
|
||||
|
||||
private static string LogsGroupName(int JobId)
|
||||
{
|
||||
return JobLogsPrefix + JobId.ToString();
|
||||
}
|
||||
private static string AttachmentsGroupName(int JobId)
|
||||
{
|
||||
return JobAttachmentsPrefix + JobId.ToString();
|
||||
}
|
||||
|
||||
public override Task OnConnected()
|
||||
{
|
||||
int jobId;
|
||||
string jobIdParam;
|
||||
|
||||
jobIdParam = Context.QueryString["JobId"];
|
||||
|
||||
if (string.IsNullOrWhiteSpace(jobIdParam))
|
||||
throw new ArgumentNullException("JobId");
|
||||
if (!int.TryParse(jobIdParam, out jobId))
|
||||
throw new ArgumentException("An integer was expected", "JobId");
|
||||
|
||||
var userAuth = UserService.GetAuthorization(Context.User.Identity.Name);
|
||||
|
||||
if (userAuth.Has(Claims.Job.ShowLogs))
|
||||
Groups.Add(Context.ConnectionId, LogsGroupName(jobId));
|
||||
if (userAuth.Has(Claims.Job.ShowAttachments))
|
||||
Groups.Add(Context.ConnectionId, AttachmentsGroupName(jobId));
|
||||
|
||||
return base.OnConnected();
|
||||
}
|
||||
|
||||
private static void RepositoryEventBefore(RepositoryMonitorEvent e)
|
||||
{
|
||||
if (e.EventType == RepositoryMonitorEventType.Deleted)
|
||||
{
|
||||
if (e.EntityType == typeof(JobLog))
|
||||
{
|
||||
var repositoryLog = (JobLog)e.Entity;
|
||||
int logJobId;
|
||||
|
||||
using (DiscoDataContext Database = new DiscoDataContext())
|
||||
logJobId = Database.JobLogs.Where(l => l.Id == repositoryLog.Id).Select(a => a.JobId).First();
|
||||
|
||||
HubContext.Clients.Group(LogsGroupName(logJobId)).removeLog(repositoryLog.Id);
|
||||
}
|
||||
if (e.EntityType == typeof(JobAttachment))
|
||||
{
|
||||
var repositoryAttachment = (JobAttachment)e.Entity;
|
||||
int attachmentJobId;
|
||||
|
||||
using (DiscoDataContext Database = new DiscoDataContext())
|
||||
attachmentJobId = Database.JobAttachments.Where(a => a.Id == repositoryAttachment.Id).Select(a => a.JobId).First();
|
||||
|
||||
HubContext.Clients.Group(AttachmentsGroupName(attachmentJobId)).removeAttachment(repositoryAttachment.Id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void RepositoryAfterEvent(RepositoryMonitorEvent e)
|
||||
{
|
||||
if (e.EventType == RepositoryMonitorEventType.Added)
|
||||
{
|
||||
if (e.EntityType == typeof(JobLog))
|
||||
{
|
||||
var a = (JobLog)e.Entity;
|
||||
|
||||
HubContext.Clients.Group(LogsGroupName(a.JobId)).addLog(a.Id);
|
||||
}
|
||||
if (e.EntityType == typeof(JobAttachment))
|
||||
{
|
||||
var a = (JobAttachment)e.Entity;
|
||||
|
||||
HubContext.Clients.Group(AttachmentsGroupName(a.JobId)).addAttachment(a.Id);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
using Disco.Data.Configuration.Modules;
|
||||
using Disco.Models.Repository;
|
||||
using Disco.Models.Services.Jobs.Noticeboards;
|
||||
using Disco.Services.Interop.ActiveDirectory;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
|
||||
namespace Disco.Services.Jobs.Noticeboards
|
||||
{
|
||||
public class HeldDeviceItem : IHeldDeviceItem
|
||||
{
|
||||
public int JobId { get; set; }
|
||||
|
||||
public string DeviceSerialNumber { get; set; }
|
||||
public string DeviceComputerNameFriendly
|
||||
{
|
||||
get
|
||||
{
|
||||
return DeviceComputerName == null ? null : UserExtensions.FriendlyUserId(DeviceComputerName);
|
||||
}
|
||||
}
|
||||
public string DeviceComputerName { get; set; }
|
||||
|
||||
public string DeviceLocation { get; set; }
|
||||
public string DeviceDescription
|
||||
{
|
||||
get
|
||||
{
|
||||
StringBuilder sb = new StringBuilder(this.DeviceComputerNameFriendly);
|
||||
|
||||
if (UserId != null)
|
||||
sb.Append(" - ").Append(this.UserDisplayName).Append(" (").Append(this.UserIdFriendly).Append(")");
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(this.DeviceLocation))
|
||||
sb.Append(" - ").Append(this.DeviceLocation);
|
||||
else if (UserId == null)
|
||||
sb.Append(" - ").Append(this.DeviceSerialNumber);
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
}
|
||||
|
||||
public int DeviceProfileId { get; set; }
|
||||
public int? DeviceAddressId { get; set; }
|
||||
public string DeviceAddressShortName
|
||||
{
|
||||
get
|
||||
{
|
||||
if (DeviceAddressId.HasValue)
|
||||
{
|
||||
var config = new OrganisationAddressesConfiguration(null);
|
||||
var address = config.GetAddress(DeviceAddressId.Value);
|
||||
if (address != null)
|
||||
return address.ShortName;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public string UserId { get; set; }
|
||||
public string UserIdFriendly
|
||||
{
|
||||
get
|
||||
{
|
||||
return UserId == null ? null : UserExtensions.FriendlyUserId(UserId);
|
||||
}
|
||||
}
|
||||
public string UserDisplayName { get; set; }
|
||||
|
||||
public bool WaitingForUserAction { get; set; }
|
||||
public DateTime? WaitingForUserActionSince { get; set; }
|
||||
public long? WaitingForUserActionSinceUnixEpoc
|
||||
{
|
||||
get
|
||||
{
|
||||
return WaitingForUserActionSince.ToUnixEpoc();
|
||||
}
|
||||
}
|
||||
|
||||
public bool ReadyForReturn { get; set; }
|
||||
public DateTime? EstimatedReturnTime { get; set; }
|
||||
public long? EstimatedReturnTimeUnixEpoc
|
||||
{
|
||||
get
|
||||
{
|
||||
return EstimatedReturnTime.ToUnixEpoc();
|
||||
}
|
||||
}
|
||||
|
||||
public DateTime? ReadyForReturnSince { get; set; }
|
||||
public long? ReadyForReturnSinceUnixEpoc
|
||||
{
|
||||
get
|
||||
{
|
||||
return ReadyForReturnSince.ToUnixEpoc();
|
||||
}
|
||||
}
|
||||
|
||||
public bool IsAlert
|
||||
{
|
||||
get
|
||||
{
|
||||
if (this.ReadyForReturn && (this.ReadyForReturnSince.Value < DateTime.Now.AddDays(-3)))
|
||||
return true;
|
||||
|
||||
if (this.WaitingForUserAction && (this.WaitingForUserActionSince.Value < DateTime.Now.AddDays(-6)))
|
||||
return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
internal static IEnumerable<HeldDeviceItem> FromJobs(IQueryable<Job> jobs)
|
||||
{
|
||||
return jobs.Select(j =>
|
||||
new HeldDeviceItem
|
||||
{
|
||||
JobId = j.Id,
|
||||
DeviceSerialNumber = j.DeviceSerialNumber,
|
||||
DeviceComputerName = j.Device.DeviceDomainId,
|
||||
DeviceLocation = j.Device.Location,
|
||||
DeviceProfileId = j.Device.DeviceProfileId,
|
||||
DeviceAddressId = j.Device.DeviceProfile.DefaultOrganisationAddress,
|
||||
UserId = j.Device.AssignedUserId,
|
||||
UserDisplayName = j.Device.AssignedUser.DisplayName,
|
||||
WaitingForUserAction = j.WaitingForUserAction.HasValue || ((j.JobMetaNonWarranty.AccountingChargeRequiredDate.HasValue || j.JobMetaNonWarranty.AccountingChargeAddedDate.HasValue) && !j.JobMetaNonWarranty.AccountingChargePaidDate.HasValue),
|
||||
WaitingForUserActionSince = j.WaitingForUserAction.HasValue ? j.WaitingForUserAction : (j.JobMetaNonWarranty.AccountingChargeRequiredDate.HasValue ? j.JobMetaNonWarranty.AccountingChargeRequiredDate : j.JobMetaNonWarranty.AccountingChargeAddedDate),
|
||||
ReadyForReturn = j.DeviceReadyForReturn.HasValue && !j.DeviceReturnedDate.HasValue,
|
||||
EstimatedReturnTime = j.ExpectedClosedDate,
|
||||
ReadyForReturnSince = j.DeviceReadyForReturn
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,239 @@
|
||||
using Disco.Data.Repository;
|
||||
using Disco.Data.Repository.Monitor;
|
||||
using Disco.Models.Repository;
|
||||
using Disco.Models.Services.Jobs.Noticeboards;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Reactive.Linq;
|
||||
|
||||
namespace Disco.Services.Jobs.Noticeboards
|
||||
{
|
||||
public static class HeldDevices
|
||||
{
|
||||
public const string Name = "HeldDevices";
|
||||
|
||||
private readonly static List<string> MonitorJobProperties = new List<string>() {
|
||||
"DeviceSerialNumber",
|
||||
"UserId",
|
||||
"ExpectedClosedDate",
|
||||
"ClosedDate",
|
||||
"WaitingForUserAction",
|
||||
"DeviceHeld",
|
||||
"DeviceReadyForReturn",
|
||||
"DeviceReturnedDate"
|
||||
};
|
||||
private readonly static List<string> MonitorJobMetaNonWarrantyProperties = new List<string>(){
|
||||
"AccountingChargeRequiredDate",
|
||||
"AccountingChargeAddedDate",
|
||||
"AccountingChargePaidDate"
|
||||
};
|
||||
private readonly static List<string> MonitorDeviceProperties = new List<string>(){
|
||||
"Location",
|
||||
"DeviceProfileId",
|
||||
"DeviceDomainId",
|
||||
"AssignedUserId",
|
||||
};
|
||||
private readonly static List<string> MonitorDeviceProfileProperties = new List<string>(){
|
||||
"DefaultOrganisationAddress"
|
||||
};
|
||||
private readonly static List<string> MonitorUserProperties = new List<string>(){
|
||||
"DisplayName"
|
||||
};
|
||||
|
||||
static HeldDevices()
|
||||
{
|
||||
// Subscribe to Repository Notifications
|
||||
RepositoryMonitor.StreamAfterCommit.Where(e =>
|
||||
(e.EntityType == typeof(Job) &&
|
||||
(e.EventType == RepositoryMonitorEventType.Added ||
|
||||
e.EventType == RepositoryMonitorEventType.Deleted ||
|
||||
(e.EventType == RepositoryMonitorEventType.Modified && e.ModifiedProperties.Any(p => MonitorJobProperties.Contains(p))))
|
||||
) ||
|
||||
(e.EntityType == typeof(JobMetaNonWarranty) &&
|
||||
(e.EventType == RepositoryMonitorEventType.Added ||
|
||||
e.EventType == RepositoryMonitorEventType.Deleted ||
|
||||
(e.EventType == RepositoryMonitorEventType.Modified && e.ModifiedProperties.Any(p => MonitorJobMetaNonWarrantyProperties.Contains(p))))
|
||||
) ||
|
||||
(e.EntityType == typeof(Device) &&
|
||||
(e.EventType == RepositoryMonitorEventType.Modified && e.ModifiedProperties.Any(p => MonitorDeviceProperties.Contains(p)))
|
||||
) ||
|
||||
(e.EntityType == typeof(DeviceProfile) &&
|
||||
(e.EventType == RepositoryMonitorEventType.Modified && e.ModifiedProperties.Any(p => MonitorDeviceProfileProperties.Contains(p)))
|
||||
) ||
|
||||
(e.EntityType == typeof(User) &&
|
||||
(e.EventType == RepositoryMonitorEventType.Modified && e.ModifiedProperties.Any(p => MonitorUserProperties.Contains(p)))
|
||||
)
|
||||
)
|
||||
.DelayBuffer(TimeSpan.FromMilliseconds(500))
|
||||
.Subscribe(RepositoryEvent);
|
||||
}
|
||||
|
||||
private static void RepositoryEvent(IEnumerable<RepositoryMonitorEvent> e)
|
||||
{
|
||||
List<string> deviceSerialNumbers = new List<string>();
|
||||
List<string> userIds = new List<string>();
|
||||
|
||||
using (DiscoDataContext Database = new DiscoDataContext())
|
||||
{
|
||||
foreach (var i in e)
|
||||
{
|
||||
if (i.EntityType == typeof(Job))
|
||||
{
|
||||
if (i.EventType == RepositoryMonitorEventType.Modified &&
|
||||
i.ModifiedProperties.Contains("DeviceSerialNumber"))
|
||||
{
|
||||
var p = i.GetPreviousPropertyValue<string>("DeviceSerialNumber");
|
||||
if (p != null)
|
||||
deviceSerialNumbers.Add(p);
|
||||
}
|
||||
|
||||
var j = (Job)i.Entity;
|
||||
if (j.DeviceSerialNumber != null)
|
||||
deviceSerialNumbers.Add(j.DeviceSerialNumber);
|
||||
}
|
||||
else if (i.EntityType == typeof(JobMetaNonWarranty))
|
||||
{
|
||||
var jmnw = (JobMetaNonWarranty)i.Entity;
|
||||
|
||||
if (jmnw.Job != null)
|
||||
{
|
||||
if (jmnw.Job.DeviceSerialNumber != null)
|
||||
deviceSerialNumbers.Add(jmnw.Job.DeviceSerialNumber);
|
||||
}
|
||||
else
|
||||
{
|
||||
var sn = Database.Jobs.Where(j => j.Id == jmnw.JobId).Select(j => j.DeviceSerialNumber).FirstOrDefault();
|
||||
if (sn != null)
|
||||
deviceSerialNumbers.Add(sn);
|
||||
}
|
||||
}
|
||||
else if (i.EntityType == typeof(Device))
|
||||
{
|
||||
var d = (Device)i.Entity;
|
||||
deviceSerialNumbers.Add(d.SerialNumber);
|
||||
|
||||
if (i.EventType == RepositoryMonitorEventType.Modified &&
|
||||
i.ModifiedProperties.Contains("AssignedUserId"))
|
||||
{
|
||||
var p = i.GetPreviousPropertyValue<string>("AssignedUserId");
|
||||
if (p != null)
|
||||
userIds.Add(p);
|
||||
}
|
||||
}
|
||||
else if (i.EntityType == typeof(DeviceProfile))
|
||||
{
|
||||
var dp = (DeviceProfile)i.Entity;
|
||||
|
||||
deviceSerialNumbers.AddRange(
|
||||
Database.Jobs
|
||||
.Where(j => !j.ClosedDate.HasValue && j.Device.DeviceProfileId == dp.Id)
|
||||
.Select(j => j.DeviceSerialNumber)
|
||||
);
|
||||
}
|
||||
else if (i.EntityType == typeof(User))
|
||||
{
|
||||
var u = (User)i.Entity;
|
||||
|
||||
deviceSerialNumbers.AddRange(
|
||||
Database.Jobs
|
||||
.Where(j => !j.ClosedDate.HasValue && j.Device.AssignedUserId == u.UserId)
|
||||
.Select(j => j.DeviceSerialNumber)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
deviceSerialNumbers = deviceSerialNumbers.Distinct().ToList();
|
||||
|
||||
// Determine Held Devices for Users
|
||||
userIds.AddRange(
|
||||
Database.Devices
|
||||
.Where(d => d.AssignedUserId != null && deviceSerialNumbers.Contains(d.SerialNumber))
|
||||
.Select(d => d.AssignedUserId)
|
||||
);
|
||||
userIds = userIds.Distinct().ToList();
|
||||
|
||||
|
||||
// Notify Held Devices
|
||||
HeldDevices.BroadcastUpdates(Database, deviceSerialNumbers);
|
||||
|
||||
// Notify Held Devices for Users
|
||||
HeldDevicesForUsers.BroadcastUpdates(Database, userIds);
|
||||
}
|
||||
}
|
||||
|
||||
internal static void BroadcastUpdates(DiscoDataContext Database, List<string> DeviceSerialNumbers)
|
||||
{
|
||||
var jobs = Database.Jobs.Where(j => DeviceSerialNumbers.Contains(j.DeviceSerialNumber));
|
||||
|
||||
var items = GetHeldDevices(jobs).ToDictionary(i => i.DeviceSerialNumber, StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
for (int skipAmount = 0; skipAmount < DeviceSerialNumbers.Count; skipAmount = skipAmount + 30)
|
||||
{
|
||||
var updates = DeviceSerialNumbers
|
||||
.Skip(skipAmount).Take(30)
|
||||
.ToDictionary(dsn => dsn,
|
||||
dsn => {
|
||||
IHeldDeviceItem item;
|
||||
items.TryGetValue(dsn, out item);
|
||||
return item;
|
||||
});
|
||||
|
||||
NoticeboardUpdatesHub.HubContext.Clients
|
||||
.Group(HeldDevices.Name)
|
||||
.updateHeldDevice(updates);
|
||||
}
|
||||
}
|
||||
|
||||
private static IEnumerable<IHeldDeviceItem> GetHeldDevices(IQueryable<Job> query)
|
||||
{
|
||||
var jobs = query
|
||||
.Where(j =>
|
||||
!j.ClosedDate.HasValue &&
|
||||
j.DeviceSerialNumber != null &&
|
||||
((j.DeviceHeld.HasValue && !j.DeviceReturnedDate.HasValue) || j.WaitingForUserAction.HasValue)
|
||||
)
|
||||
.SelectHeldDeviceItems()
|
||||
.GroupBy(j => j.DeviceSerialNumber);
|
||||
|
||||
foreach (var job in jobs.ToList())
|
||||
{
|
||||
if (job.Any(j => j.WaitingForUserAction))
|
||||
{
|
||||
var item = job.Where(j => j.WaitingForUserAction).OrderBy(j => j.WaitingForUserActionSince).First();
|
||||
|
||||
yield return item;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (job.All(j => j.ReadyForReturn))
|
||||
{
|
||||
var item = job.OrderByDescending(j => j.ReadyForReturnSince).First();
|
||||
|
||||
yield return item;
|
||||
}
|
||||
else
|
||||
{
|
||||
var item = job.Where(j => !j.ReadyForReturn).OrderByDescending(j => j.EstimatedReturnTime).First();
|
||||
|
||||
yield return item;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static IEnumerable<IHeldDeviceItem> GetHeldDevices(DiscoDataContext Database)
|
||||
{
|
||||
return GetHeldDevices(Database.Jobs);
|
||||
}
|
||||
public static IHeldDeviceItem GetHeldDevice(DiscoDataContext Database, string DeviceSerialNumber)
|
||||
{
|
||||
return GetHeldDevices(Database.Jobs.Where(j => j.DeviceSerialNumber == DeviceSerialNumber)).FirstOrDefault();
|
||||
}
|
||||
|
||||
internal static IEnumerable<HeldDeviceItem> SelectHeldDeviceItems(this IQueryable<Job> jobs)
|
||||
{
|
||||
return HeldDeviceItem.FromJobs(jobs);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
using Disco.Data.Repository;
|
||||
using Disco.Models.Repository;
|
||||
using Disco.Models.Services.Jobs.Noticeboards;
|
||||
using Disco.Services.Interop.ActiveDirectory;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace Disco.Services.Jobs.Noticeboards
|
||||
{
|
||||
public class HeldDevicesForUsers
|
||||
{
|
||||
public const string Name = "HeldDevicesForUsers";
|
||||
|
||||
// NOTE: Calculation for updates is performed by HeldDevices to avoid duplication
|
||||
|
||||
internal static void BroadcastUpdates(DiscoDataContext Database, List<string> UserIds)
|
||||
{
|
||||
var jobs = Database.Devices.Where(d => UserIds.Contains(d.AssignedUserId)).SelectMany(d => d.Jobs);
|
||||
|
||||
var items = GetHeldDevicesForUsers(jobs).ToDictionary(i => i.UserId, StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
for (int skipAmount = 0; skipAmount < UserIds.Count; skipAmount = skipAmount + 30)
|
||||
{
|
||||
var updates = UserIds
|
||||
.Skip(skipAmount).Take(30)
|
||||
.ToDictionary(userId => userId,
|
||||
userId =>
|
||||
{
|
||||
IHeldDeviceItem item;
|
||||
items.TryGetValue(userId, out item);
|
||||
return item;
|
||||
});
|
||||
|
||||
NoticeboardUpdatesHub.HubContext.Clients
|
||||
.Group(HeldDevicesForUsers.Name)
|
||||
.updateHeldDeviceForUser(updates);
|
||||
}
|
||||
}
|
||||
|
||||
private static IEnumerable<IHeldDeviceItem> GetHeldDevicesForUsers(IQueryable<Job> query)
|
||||
{
|
||||
var jobs = query
|
||||
.Where(j =>
|
||||
!j.ClosedDate.HasValue &&
|
||||
j.DeviceSerialNumber != null &&
|
||||
j.Device.AssignedUserId != null &&
|
||||
((j.DeviceHeld.HasValue && !j.DeviceReturnedDate.HasValue) || j.WaitingForUserAction.HasValue)
|
||||
)
|
||||
.SelectHeldDeviceItems()
|
||||
.GroupBy(j => j.UserId);
|
||||
|
||||
foreach (var job in jobs.ToList())
|
||||
{
|
||||
if (job.Any(j => j.WaitingForUserAction))
|
||||
{
|
||||
var item = job.Where(j => j.WaitingForUserAction).OrderBy(j => j.WaitingForUserActionSince).First();
|
||||
|
||||
yield return item;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (job.All(j => j.ReadyForReturn))
|
||||
{
|
||||
var item = job.OrderByDescending(j => j.ReadyForReturnSince).First();
|
||||
|
||||
yield return item;
|
||||
}
|
||||
else
|
||||
{
|
||||
var item = job.Where(j => !j.ReadyForReturn).OrderByDescending(j => j.EstimatedReturnTime).First();
|
||||
|
||||
yield return item;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static IEnumerable<IHeldDeviceItem> GetHeldDevicesForUsers(DiscoDataContext Database)
|
||||
{
|
||||
return GetHeldDevicesForUsers(Database.Jobs);
|
||||
}
|
||||
public static IHeldDeviceItem GetHeldDeviceForUsers(DiscoDataContext Database, string UserId)
|
||||
{
|
||||
var split = UserExtensions.SplitUserId(UserId);
|
||||
if (split.Item1 == null)
|
||||
UserId = string.Format(@"{0}\{1}", ActiveDirectory.Context.PrimaryDomain.NetBiosName, UserId);
|
||||
|
||||
return GetHeldDevicesForUsers(Database.Devices.Where(d => d.AssignedUserId == UserId).SelectMany(d => d.Jobs)).FirstOrDefault();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
using Microsoft.AspNet.SignalR;
|
||||
using Microsoft.AspNet.SignalR.Hubs;
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Disco.Services.Jobs.Noticeboards
|
||||
{
|
||||
[HubName("noticeboardUpdates")] // Public
|
||||
public class NoticeboardUpdatesHub : Hub
|
||||
{
|
||||
public static IHubContext HubContext { get; private set; }
|
||||
|
||||
static NoticeboardUpdatesHub()
|
||||
{
|
||||
HubContext = GlobalHost.ConnectionManager.GetHubContext<NoticeboardUpdatesHub>();
|
||||
}
|
||||
|
||||
public override Task OnConnected()
|
||||
{
|
||||
var noticeboardId = Context.QueryString["Noticeboard"];
|
||||
|
||||
if (string.IsNullOrWhiteSpace(noticeboardId))
|
||||
throw new ArgumentNullException("Noticeboard");
|
||||
|
||||
switch (noticeboardId)
|
||||
{
|
||||
case HeldDevicesForUsers.Name:
|
||||
Groups.Add(Context.ConnectionId, HeldDevicesForUsers.Name);
|
||||
break;
|
||||
case HeldDevices.Name:
|
||||
Groups.Add(Context.ConnectionId, HeldDevices.Name);
|
||||
break;
|
||||
default:
|
||||
throw new ArgumentException("Invalid Noticeboard Specified", "Noticeboard");
|
||||
}
|
||||
|
||||
return base.OnConnected();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -70,7 +70,7 @@ namespace Disco.Services.Logging
|
||||
}
|
||||
}
|
||||
|
||||
private static void InitalizeDatabase(Targets.LogPersistContext LogDatabase)
|
||||
private static void InitalizeDatabase(Persistance.LogPersistContext LogDatabase)
|
||||
{
|
||||
// Add Modules
|
||||
var existingModules = LogDatabase.Modules.Include("EventTypes").ToDictionary(m => m.Id);
|
||||
@@ -183,7 +183,7 @@ namespace Disco.Services.Logging
|
||||
if (!File.Exists(logPath))
|
||||
{
|
||||
// Create Database
|
||||
using (var context = new Targets.LogPersistContext(connectionString))
|
||||
using (var context = new Persistance.LogPersistContext(connectionString))
|
||||
{
|
||||
context.Database.CreateIfNotExists();
|
||||
}
|
||||
@@ -191,7 +191,7 @@ namespace Disco.Services.Logging
|
||||
|
||||
// Add Modules/Event Types
|
||||
InitalizeModules();
|
||||
using (var context = new Targets.LogPersistContext(connectionString))
|
||||
using (var context = new Persistance.LogPersistContext(connectionString))
|
||||
{
|
||||
InitalizeDatabase(context);
|
||||
}
|
||||
@@ -211,7 +211,7 @@ namespace Disco.Services.Logging
|
||||
sqlCeCSB.DataSource = yesterdaysLogPath;
|
||||
var connectionString = sqlCeCSB.ToString();
|
||||
int logCount;
|
||||
using (var context = new Targets.LogPersistContext(connectionString))
|
||||
using (var context = new Persistance.LogPersistContext(connectionString))
|
||||
{
|
||||
logCount = context.Events.Where(e => !(e.ModuleId == 0 && e.EventTypeId == 100)).Count();
|
||||
if (logCount == 0)
|
||||
@@ -267,7 +267,7 @@ namespace Disco.Services.Logging
|
||||
var eventTimestamp = DateTime.Now;
|
||||
if (eventType.UseLive)
|
||||
{
|
||||
Targets.LogLiveContext.Broadcast(logModule, eventType, eventTimestamp, Args);
|
||||
LogNotificationsHub.BroadcastLog(logModule, eventType, eventTimestamp, Args);
|
||||
}
|
||||
if (eventType.UsePersist)
|
||||
{
|
||||
@@ -276,7 +276,7 @@ namespace Disco.Services.Logging
|
||||
{
|
||||
args = JsonConvert.SerializeObject(Args);
|
||||
}
|
||||
using (var context = new Targets.LogPersistContext(PersistantStoreConnectionString))
|
||||
using (var context = new Persistance.LogPersistContext(PersistantStoreConnectionString))
|
||||
{
|
||||
var e = new Models.LogEvent()
|
||||
{
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
using Disco.Services.Authorization;
|
||||
using Disco.Services.Logging.Models;
|
||||
using Disco.Services.Web.Signalling;
|
||||
using Microsoft.AspNet.SignalR;
|
||||
using Microsoft.AspNet.SignalR.Hubs;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Disco.Services.Logging
|
||||
{
|
||||
[HubName("logNotifications"), DiscoHubAuthorize(Claims.Config.Logging.Show)]
|
||||
public class LogNotificationsHub : Hub
|
||||
{
|
||||
private const string NotificationsModulePrefix = "Logging_";
|
||||
public const string AllLoggingNotification = NotificationsModulePrefix + "_ALL";
|
||||
|
||||
public override Task OnConnected()
|
||||
{
|
||||
var logModules = Context.QueryString["LogModules"];
|
||||
if (!string.IsNullOrWhiteSpace(logModules) && logModules.Length > 0)
|
||||
{
|
||||
foreach (var modules in ValidLogModuleGroupNames(logModules))
|
||||
Groups.Add(Context.ConnectionId, modules);
|
||||
}
|
||||
|
||||
return base.OnConnected();
|
||||
}
|
||||
|
||||
internal static void BroadcastLog(LogBase logModule, LogEventType eventType, DateTime Timestamp, params object[] Arguments)
|
||||
{
|
||||
var message = LogLiveEvent.Create(logModule, eventType, Timestamp, Arguments);
|
||||
|
||||
var connectionManager = GlobalHost.ConnectionManager;
|
||||
var context = connectionManager.GetHubContext<LogNotificationsHub>();
|
||||
var targets = new List<string> { AllLoggingNotification, NotificationsModulePrefix + logModule.ModuleName };
|
||||
context.Clients.Groups(targets).receiveLog(message);
|
||||
}
|
||||
|
||||
private IEnumerable<string> ValidLogModuleGroupNames(string ModuleNames)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(ModuleNames))
|
||||
yield break;
|
||||
|
||||
var names = ModuleNames.Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries);
|
||||
|
||||
foreach (var name in names)
|
||||
{
|
||||
// Special Case: __ALL
|
||||
if (name.Equals(AllLoggingNotification, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
yield return AllLoggingNotification;
|
||||
}
|
||||
else
|
||||
{
|
||||
var module = LogContext.LogModules.Values.FirstOrDefault(m => m.ModuleName.Equals(name, StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
if (module == null)
|
||||
throw new ArgumentException(string.Format("Invalid Module Name specified: {0}", name), "ModuleNames");
|
||||
|
||||
yield return NotificationsModulePrefix + module.ModuleName;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -5,7 +5,7 @@ using System.Text;
|
||||
using System.Data.Entity;
|
||||
using System.Data.Entity.Infrastructure;
|
||||
|
||||
namespace Disco.Services.Logging.Targets
|
||||
namespace Disco.Services.Logging.Persistance
|
||||
{
|
||||
public class LogPersistContext : DbContext
|
||||
{
|
||||
+1
-1
@@ -5,7 +5,7 @@ using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Disco.Services.Logging.Targets
|
||||
namespace Disco.Services.Logging.Persistance
|
||||
{
|
||||
public class LogPersistContextInitializer : IDatabaseInitializer<LogPersistContext>
|
||||
{
|
||||
@@ -1,13 +1,12 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using Disco.Services.Logging.Targets;
|
||||
using Disco.Data.Repository;
|
||||
using System.IO;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Data.SqlServerCe;
|
||||
using Disco.Data.Repository;
|
||||
using Disco.Services.Logging.Models;
|
||||
using Disco.Services.Logging.Persistance;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Data.SqlServerCe;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace Disco.Services.Logging
|
||||
{
|
||||
@@ -45,7 +44,7 @@ namespace Disco.Services.Logging
|
||||
|
||||
var logModules = LogContext.LogModules;
|
||||
|
||||
using (var context = new Targets.LogPersistContext(sqlCeCSB.ToString()))
|
||||
using (var context = new LogPersistContext(sqlCeCSB.ToString()))
|
||||
{
|
||||
var query = this.BuildQuery(context, logFile.Item2, results.Count);
|
||||
IEnumerable<LogEvent> queryResults = query; // Run the Query
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
using Disco.Services.Logging.Models;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Disco.Services.Logging.Targets
|
||||
{
|
||||
public static class LogLiveContext
|
||||
{
|
||||
public delegate void LogBroadcastEvent(LogBase logModule, LogEventType eventType, DateTime Timestamp, params object[] Arguments);
|
||||
public static event LogBroadcastEvent LogBroadcast;
|
||||
|
||||
internal static void Broadcast(LogBase logModule, LogEventType eventType, DateTime Timestamp, params object[] Arguments)
|
||||
{
|
||||
if (LogBroadcast != null)
|
||||
LogBroadcast.Invoke(logModule, eventType, Timestamp, Arguments);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -21,11 +21,11 @@ namespace Disco.Services.Plugins.Features.UIExtension.Results
|
||||
this._resource = Resource;
|
||||
this._resourceUrl = Source.PluginManifest.WebResourceUrl(Resource);
|
||||
|
||||
var deferredBundles = HttpContext.Current.Items[Bundle.UIExtensionCssKey] as List<string>;
|
||||
var deferredBundles = HttpContext.Current.Items[BundleTable.UIExtensionCssKey] as List<string>;
|
||||
if (deferredBundles == null)
|
||||
{
|
||||
deferredBundles = new List<string>();
|
||||
HttpContext.Current.Items[Bundle.UIExtensionCssKey] = deferredBundles;
|
||||
HttpContext.Current.Items[BundleTable.UIExtensionCssKey] = deferredBundles;
|
||||
}
|
||||
if (!deferredBundles.Contains(this._resourceUrl))
|
||||
deferredBundles.Add(this._resourceUrl);
|
||||
|
||||
@@ -23,11 +23,11 @@ namespace Disco.Services.Plugins.Features.UIExtension.Results
|
||||
|
||||
if (this._placeInPageHead)
|
||||
{
|
||||
var deferredBundles = HttpContext.Current.Items[Bundle.UIExtensionScriptsKey] as List<string>;
|
||||
var deferredBundles = HttpContext.Current.Items[BundleTable.UIExtensionScriptsKey] as List<string>;
|
||||
if (deferredBundles == null)
|
||||
{
|
||||
deferredBundles = new List<string>();
|
||||
HttpContext.Current.Items[Bundle.UIExtensionScriptsKey] = deferredBundles;
|
||||
HttpContext.Current.Items[BundleTable.UIExtensionScriptsKey] = deferredBundles;
|
||||
}
|
||||
if (!deferredBundles.Contains(this._resourceUrl))
|
||||
deferredBundles.Add(this._resourceUrl);
|
||||
|
||||
@@ -97,12 +97,12 @@ namespace Disco.Services.Plugins
|
||||
public static void IncludeStyleSheetResource(this HttpContextBase Context, string Resource, PluginManifest manifest)
|
||||
{
|
||||
var resourceUrl = manifest.WebResourceUrl(Resource);
|
||||
|
||||
var deferredBundles = Context.Items[Bundle.UIExtensionCssKey] as List<string>;
|
||||
|
||||
var deferredBundles = Context.Items[BundleTable.UIExtensionCssKey] as List<string>;
|
||||
if (deferredBundles == null)
|
||||
{
|
||||
deferredBundles = new List<string>();
|
||||
HttpContext.Current.Items[Bundle.UIExtensionCssKey] = deferredBundles;
|
||||
HttpContext.Current.Items[BundleTable.UIExtensionCssKey] = deferredBundles;
|
||||
}
|
||||
if (!deferredBundles.Contains(resourceUrl))
|
||||
deferredBundles.Add(resourceUrl);
|
||||
@@ -111,11 +111,11 @@ namespace Disco.Services.Plugins
|
||||
{
|
||||
var resourcePath = manifest.WebResourceUrl(Resource);
|
||||
|
||||
var deferredBundles = Context.Items[Bundle.UIExtensionScriptsKey] as List<string>;
|
||||
var deferredBundles = Context.Items[BundleTable.UIExtensionScriptsKey] as List<string>;
|
||||
if (deferredBundles == null)
|
||||
{
|
||||
deferredBundles = new List<string>();
|
||||
HttpContext.Current.Items[Bundle.UIExtensionScriptsKey] = deferredBundles;
|
||||
HttpContext.Current.Items[BundleTable.UIExtensionScriptsKey] = deferredBundles;
|
||||
}
|
||||
if (!deferredBundles.Contains(resourcePath))
|
||||
deferredBundles.Add(resourcePath);
|
||||
@@ -223,7 +223,9 @@ namespace Disco.Services.Plugins
|
||||
var routeValues = new RouteValueDictionary(new { PluginId = manifest.Id, PluginAction = PluginAction });
|
||||
string pluginActionUrl = UrlHelper.GenerateUrl("Plugin", null, null, routeValues, RouteTable.Routes, ViewPage.ViewContext.RequestContext, false);
|
||||
|
||||
#pragma warning disable 618
|
||||
return ViewPage.FormHelper(pluginActionUrl, method, htmlAttributes);
|
||||
#pragma warning restore 618
|
||||
}
|
||||
[Obsolete("Inherit ViewPages from 'Disco.Services.Plugins.WebViewPage' instead.")]
|
||||
public static MvcForm DiscoPluginActionBeginForm<T>(this WebViewPage<T> ViewPage, string PluginAction, FormMethod method)
|
||||
@@ -302,11 +304,11 @@ namespace Disco.Services.Plugins
|
||||
|
||||
HtmlString pluginResourceUrlHtml = new HtmlString(pluginResourceUrl);
|
||||
|
||||
var deferredBundles = RequestContext.HttpContext.Items[Bundle.UIExtensionCssKey] as List<HtmlString>;
|
||||
var deferredBundles = RequestContext.HttpContext.Items[BundleTable.UIExtensionCssKey] as List<HtmlString>;
|
||||
if (deferredBundles == null)
|
||||
{
|
||||
deferredBundles = new List<HtmlString>();
|
||||
HttpContext.Current.Items[Bundle.UIExtensionCssKey] = deferredBundles;
|
||||
HttpContext.Current.Items[BundleTable.UIExtensionCssKey] = deferredBundles;
|
||||
}
|
||||
if (!deferredBundles.Contains(pluginResourceUrlHtml))
|
||||
deferredBundles.Add(pluginResourceUrlHtml);
|
||||
|
||||
+19
-1
@@ -6,12 +6,22 @@ using System.Threading.Tasks;
|
||||
|
||||
namespace Disco.Services.Tasks
|
||||
{
|
||||
public interface IScheduledTaskBasicStatus
|
||||
public interface IScheduledTaskStatus
|
||||
{
|
||||
byte Progress { get; }
|
||||
string CurrentProcess { get; }
|
||||
string CurrentDescription { get; }
|
||||
|
||||
bool IgnoreCurrentProcessChanges { get; set; }
|
||||
bool IgnoreCurrentDescription { get; set; }
|
||||
double ProgressMultiplier { get; set; }
|
||||
byte ProgressOffset { get; set; }
|
||||
|
||||
string FinishedMessage { get; }
|
||||
string FinishedUrl { get; }
|
||||
|
||||
Exception TaskException { get; }
|
||||
|
||||
void UpdateStatus(byte Progress);
|
||||
void UpdateStatus(double Progress);
|
||||
void UpdateStatus(string CurrentDescription);
|
||||
@@ -19,5 +29,13 @@ namespace Disco.Services.Tasks
|
||||
void UpdateStatus(double Progress, string CurrentDescription);
|
||||
void UpdateStatus(byte Progress, string CurrentProcess, string CurrentDescription);
|
||||
void UpdateStatus(double Progress, string CurrentProcess, string CurrentDescription);
|
||||
|
||||
void SetFinishedUrl(string FinishedUrl);
|
||||
void SetFinishedMessage(string FinishedMessage);
|
||||
void Finished();
|
||||
void Finished(string FinishedMessage);
|
||||
void Finished(string FinishedMessage, string FinishedUrl);
|
||||
|
||||
void SetTaskException(Exception TaskException);
|
||||
}
|
||||
}
|
||||
@@ -16,8 +16,8 @@ namespace Disco.Services.Tasks
|
||||
|
||||
public virtual bool CancelInitiallySupported { get { return true; } }
|
||||
public virtual bool SingleInstanceTask { get { return true; } }
|
||||
public virtual bool IsSilent { get { return false; } }
|
||||
public virtual bool LogExceptionsOnly { get { return false; } }
|
||||
|
||||
public abstract string TaskName { get; }
|
||||
protected abstract void ExecuteTask();
|
||||
|
||||
|
||||
@@ -6,19 +6,30 @@ using System.Threading.Tasks;
|
||||
|
||||
namespace Disco.Services.Tasks
|
||||
{
|
||||
public class ScheduledTaskMockStatus : IScheduledTaskBasicStatus
|
||||
public class ScheduledTaskMockStatus : IScheduledTaskStatus
|
||||
{
|
||||
private byte progress;
|
||||
private string currentProcess;
|
||||
private string currentDescription;
|
||||
public byte Progress { get; set; }
|
||||
public string CurrentProcess { get; set; }
|
||||
public string CurrentDescription { get; set; }
|
||||
|
||||
public byte Progress { get { return this.progress; } }
|
||||
public string CurrentProcess { get { return this.currentProcess; } }
|
||||
public string CurrentDescription { get { return this.currentDescription; } }
|
||||
public bool IgnoreCurrentProcessChanges { get; set; }
|
||||
public bool IgnoreCurrentDescription { get; set; }
|
||||
public double ProgressMultiplier { get; set; }
|
||||
public byte ProgressOffset { get; set; }
|
||||
|
||||
public string FinishedMessage { get; set; }
|
||||
public string FinishedUrl { get; set; }
|
||||
|
||||
public Exception TaskException { get; set; }
|
||||
|
||||
private byte CalculateProgressValue(byte Progress)
|
||||
{
|
||||
return (byte)((Progress * this.ProgressMultiplier) + this.ProgressOffset);
|
||||
}
|
||||
|
||||
public void UpdateStatus(byte Progress)
|
||||
{
|
||||
this.progress = Progress;
|
||||
this.Progress = CalculateProgressValue(Progress);
|
||||
}
|
||||
public void UpdateStatus(double Progress)
|
||||
{
|
||||
@@ -26,12 +37,14 @@ namespace Disco.Services.Tasks
|
||||
}
|
||||
public void UpdateStatus(string CurrentDescription)
|
||||
{
|
||||
this.currentDescription = CurrentDescription;
|
||||
if (!IgnoreCurrentDescription)
|
||||
this.CurrentDescription = CurrentDescription;
|
||||
}
|
||||
public void UpdateStatus(byte Progress, string CurrentDescription)
|
||||
{
|
||||
this.progress = Progress;
|
||||
this.currentDescription = CurrentDescription;
|
||||
this.Progress = CalculateProgressValue(Progress);
|
||||
if (!IgnoreCurrentDescription)
|
||||
this.CurrentDescription = CurrentDescription;
|
||||
}
|
||||
public void UpdateStatus(double Progress, string CurrentDescription)
|
||||
{
|
||||
@@ -39,15 +52,46 @@ namespace Disco.Services.Tasks
|
||||
}
|
||||
public void UpdateStatus(byte Progress, string CurrentProcess, string CurrentDescription)
|
||||
{
|
||||
this.progress = Progress;
|
||||
this.currentProcess = CurrentProcess;
|
||||
this.currentDescription = CurrentDescription;
|
||||
this.Progress = CalculateProgressValue(Progress);
|
||||
if (!IgnoreCurrentProcessChanges)
|
||||
this.CurrentProcess = CurrentProcess;
|
||||
if (!IgnoreCurrentDescription)
|
||||
this.CurrentDescription = CurrentDescription;
|
||||
}
|
||||
public void UpdateStatus(double Progress, string CurrentProcess, string CurrentDescription)
|
||||
{
|
||||
UpdateStatus((byte)Progress, CurrentProcess, CurrentDescription);
|
||||
}
|
||||
|
||||
public void SetFinishedUrl(string FinishedUrl)
|
||||
{
|
||||
this.FinishedUrl = FinishedUrl;
|
||||
}
|
||||
public void SetFinishedMessage(string FinishedMessage)
|
||||
{
|
||||
this.FinishedMessage = FinishedMessage;
|
||||
}
|
||||
public void Finished()
|
||||
{
|
||||
Finished(this.FinishedMessage, this.FinishedUrl);
|
||||
}
|
||||
public void Finished(string FinishedMessage)
|
||||
{
|
||||
Finished(FinishedMessage, this.FinishedUrl);
|
||||
}
|
||||
public void Finished(string FinishedMessage, string FinishedUrl)
|
||||
{
|
||||
this.FinishedMessage = FinishedMessage;
|
||||
this.FinishedUrl = FinishedUrl;
|
||||
}
|
||||
|
||||
public void SetTaskException(Exception TaskException)
|
||||
{
|
||||
this.TaskException = TaskException;
|
||||
}
|
||||
|
||||
|
||||
|
||||
public static ScheduledTaskMockStatus Create()
|
||||
{
|
||||
return new ScheduledTaskMockStatus();
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
using Disco.Services.Web.Signalling;
|
||||
using Microsoft.AspNet.SignalR;
|
||||
using Microsoft.AspNet.SignalR.Hubs;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Reactive.Linq;
|
||||
using System.Linq;
|
||||
|
||||
namespace Disco.Services.Tasks
|
||||
{
|
||||
using System.Reactive.Subjects;
|
||||
using ChangedItem = KeyValuePair<string, object>;
|
||||
|
||||
[HubName("scheduledTaskNotifications"), DiscoHubAuthorize()]
|
||||
public class ScheduledTaskNotificationsHub : Hub
|
||||
{
|
||||
private const string NotificationsPrefix = "Logging_";
|
||||
private static Subject<Tuple<string, IEnumerable<ChangedItem>>> taskUpdatesStream = new Subject<Tuple<string, IEnumerable<ChangedItem>>>();
|
||||
private static IDisposable taskUpdatesStreamSubscription;
|
||||
|
||||
internal static void Initialize()
|
||||
{
|
||||
if (taskUpdatesStreamSubscription == null)
|
||||
{
|
||||
lock (taskUpdatesStream)
|
||||
{
|
||||
if (taskUpdatesStreamSubscription == null)
|
||||
{
|
||||
taskUpdatesStreamSubscription = taskUpdatesStream
|
||||
.DelayBuffer(TimeSpan.FromMilliseconds(200))
|
||||
.Subscribe(BroadcastBufferedEvents);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal static void PublishEvent(string TaskSessionId, IEnumerable<ChangedItem> ChangedItems)
|
||||
{
|
||||
taskUpdatesStream.OnNext(Tuple.Create(TaskSessionId, ChangedItems));
|
||||
}
|
||||
|
||||
public override System.Threading.Tasks.Task OnConnected()
|
||||
{
|
||||
var taskSessionId = Context.QueryString["TaskSessionId"];
|
||||
|
||||
if (string.IsNullOrEmpty(taskSessionId))
|
||||
throw new ArgumentNullException("TaskSessionId");
|
||||
|
||||
var status = ScheduledTasks.GetTaskStatus(taskSessionId);
|
||||
|
||||
if (status == null)
|
||||
throw new ArgumentException("Invalid ScheduledTask SessionId", "TaskSessionId");
|
||||
|
||||
// Send Status:
|
||||
var currentStatus = ScheduledTaskStatusLive.FromScheduledTaskStatus(status, null);
|
||||
Clients.Caller.initializeTaskStatus(currentStatus);
|
||||
|
||||
// Add to Group
|
||||
var groupName = NotificationsPrefix + taskSessionId;
|
||||
Groups.Add(Context.ConnectionId, groupName);
|
||||
|
||||
return base.OnConnected();
|
||||
}
|
||||
|
||||
private static void BroadcastBufferedEvents(IEnumerable<Tuple<string, IEnumerable<ChangedItem>>> Events)
|
||||
{
|
||||
var connectionManager = GlobalHost.ConnectionManager;
|
||||
var context = connectionManager.GetHubContext<ScheduledTaskNotificationsHub>();
|
||||
|
||||
var taskStatusEvents = Events.GroupBy(e => e.Item1).Select(taskEventsGroup =>
|
||||
{
|
||||
Dictionary<string, object> changes = new Dictionary<string, object>();
|
||||
|
||||
foreach (var changeEvents in taskEventsGroup.Select(taskEvents => taskEvents.Item2))
|
||||
foreach (var changeEvent in changeEvents)
|
||||
changes[changeEvent.Key] = changeEvent.Value;
|
||||
|
||||
return Tuple.Create(taskEventsGroup.Key, changes);
|
||||
});
|
||||
|
||||
foreach (var taskStatusEvent in taskStatusEvents)
|
||||
{
|
||||
var groupName = NotificationsPrefix + taskStatusEvent.Item1;
|
||||
context.Clients.Group(groupName).updateTaskStatus(taskStatusEvent.Item2);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -9,7 +9,9 @@ using System.Threading.Tasks;
|
||||
|
||||
namespace Disco.Services.Tasks
|
||||
{
|
||||
public class ScheduledTaskStatus : IScheduledTaskBasicStatus
|
||||
using ChangedItem = KeyValuePair<string, object>;
|
||||
|
||||
public class ScheduledTaskStatus : IScheduledTaskStatus
|
||||
{
|
||||
#region Backing Fields
|
||||
|
||||
@@ -52,6 +54,11 @@ namespace Disco.Services.Tasks
|
||||
public string CurrentProcess { get { return this._currentProcess; } }
|
||||
public string CurrentDescription { get { return this._currentDescription; } }
|
||||
|
||||
public bool IgnoreCurrentProcessChanges { get; set; }
|
||||
public bool IgnoreCurrentDescription { get; set; }
|
||||
public double ProgressMultiplier { get; set; }
|
||||
public byte ProgressOffset { get; set; }
|
||||
|
||||
public Exception TaskException { get { return this._taskException; } }
|
||||
public bool CancelSupported { get { return this._cancelSupported; } }
|
||||
public bool IsCanceling { get { return this._isCanceling; } }
|
||||
@@ -73,21 +80,13 @@ namespace Disco.Services.Tasks
|
||||
}
|
||||
}
|
||||
|
||||
public Task CompletionTask
|
||||
{
|
||||
get
|
||||
{
|
||||
return _tcs.Task;
|
||||
}
|
||||
}
|
||||
public Task CompletionTask { get { return _tcs.Task; } }
|
||||
|
||||
#endregion
|
||||
|
||||
#region Events
|
||||
public delegate void UpdatedBroadcastEvent(ScheduledTaskStatusLive SessionStatus);
|
||||
public delegate void UpdatedEvent(ScheduledTaskStatus sender, string[] ChangedProperties);
|
||||
public delegate void UpdatedEvent(ScheduledTaskStatus sender, KeyValuePair<string, object>[] ChangedProperties);
|
||||
public delegate void CancelingEvent(ScheduledTaskStatus sender);
|
||||
public static event UpdatedBroadcastEvent UpdatedBroadcast;
|
||||
public event UpdatedEvent Updated;
|
||||
public event CancelingEvent Canceling;
|
||||
#endregion
|
||||
@@ -112,10 +111,15 @@ namespace Disco.Services.Tasks
|
||||
}
|
||||
|
||||
#region Progress Actions
|
||||
private byte CalculateProgressValue(byte Progress)
|
||||
{
|
||||
return (byte)((Progress * this.ProgressMultiplier) + this.ProgressOffset);
|
||||
}
|
||||
|
||||
public void UpdateStatus(byte Progress)
|
||||
{
|
||||
this._progress = Progress;
|
||||
UpdateTriggered(new string[] { "Progress" });
|
||||
this._progress = CalculateProgressValue(Progress);
|
||||
UpdateTriggered("Progress", this._progress);
|
||||
}
|
||||
public void UpdateStatus(double Progress)
|
||||
{
|
||||
@@ -123,14 +127,27 @@ namespace Disco.Services.Tasks
|
||||
}
|
||||
public void UpdateStatus(string CurrentDescription)
|
||||
{
|
||||
this._currentDescription = CurrentDescription;
|
||||
UpdateTriggered(new string[] { "CurrentDescription" });
|
||||
if (!IgnoreCurrentDescription)
|
||||
{
|
||||
this._currentDescription = CurrentDescription;
|
||||
UpdateTriggered("CurrentDescription", this._currentDescription);
|
||||
}
|
||||
}
|
||||
public void UpdateStatus(byte Progress, string CurrentDescription)
|
||||
{
|
||||
this._progress = Progress;
|
||||
this._currentDescription = CurrentDescription;
|
||||
UpdateTriggered(new string[] { "Progress", "CurrentDescription" });
|
||||
this._progress = CalculateProgressValue(Progress);
|
||||
|
||||
var changedProperties = new List<ChangedItem>() {
|
||||
new ChangedItem("Progress", Progress)
|
||||
};
|
||||
|
||||
if (!IgnoreCurrentDescription)
|
||||
{
|
||||
this._currentDescription = CurrentDescription;
|
||||
changedProperties.Add(new ChangedItem("CurrentDescription", CurrentDescription));
|
||||
}
|
||||
|
||||
UpdateTriggered(changedProperties.ToArray());
|
||||
}
|
||||
public void UpdateStatus(double Progress, string CurrentDescription)
|
||||
{
|
||||
@@ -138,10 +155,24 @@ namespace Disco.Services.Tasks
|
||||
}
|
||||
public void UpdateStatus(byte Progress, string CurrentProcess, string CurrentDescription)
|
||||
{
|
||||
this._progress = Progress;
|
||||
this._currentProcess = CurrentProcess;
|
||||
this._currentDescription = CurrentDescription;
|
||||
UpdateTriggered(new string[] { "Progress", "CurrentProcess", "CurrentDescription" });
|
||||
this._progress = CalculateProgressValue(Progress);
|
||||
|
||||
var changedProperties = new List<ChangedItem>() {
|
||||
new ChangedItem("Progress", Progress)
|
||||
};
|
||||
|
||||
if (!IgnoreCurrentProcessChanges)
|
||||
{
|
||||
this._currentProcess = CurrentProcess;
|
||||
changedProperties.Add(new ChangedItem("CurrentProcess", CurrentProcess));
|
||||
}
|
||||
if (!IgnoreCurrentDescription)
|
||||
{
|
||||
this._currentDescription = CurrentDescription;
|
||||
changedProperties.Add(new ChangedItem("CurrentDescription", CurrentDescription));
|
||||
}
|
||||
|
||||
UpdateTriggered(changedProperties.ToArray());
|
||||
}
|
||||
public void UpdateStatus(double Progress, string CurrentProcess, string CurrentDescription)
|
||||
{
|
||||
@@ -157,7 +188,7 @@ namespace Disco.Services.Tasks
|
||||
if (_cancelSupported)
|
||||
{ // Cancelling
|
||||
this._isCanceling = true;
|
||||
UpdateTriggered(new string[] { "IsCancelling" });
|
||||
UpdateTriggered("IsCancelling", true);
|
||||
if (this.Canceling != null)
|
||||
Canceling(this);
|
||||
return true;
|
||||
@@ -177,7 +208,7 @@ namespace Disco.Services.Tasks
|
||||
if (this._cancelSupported != CancelSupported)
|
||||
{
|
||||
this._cancelSupported = CancelSupported;
|
||||
UpdateTriggered(new string[] { "CancelSupported" });
|
||||
UpdateTriggered("CancelSupported", CancelSupported);
|
||||
}
|
||||
}
|
||||
public void SetTaskException(Exception TaskException)
|
||||
@@ -185,7 +216,7 @@ namespace Disco.Services.Tasks
|
||||
if (this._taskException != TaskException)
|
||||
{
|
||||
this._taskException = TaskException;
|
||||
UpdateTriggered(new string[] { "TaskException" });
|
||||
UpdateTriggered("TaskExceptionMessage", (this._taskException == null ? null : this._taskException.Message));
|
||||
}
|
||||
}
|
||||
public void SetIsSilent(bool IsSilent)
|
||||
@@ -198,7 +229,7 @@ namespace Disco.Services.Tasks
|
||||
if (this._finishedUrl != FinishedUrl)
|
||||
{
|
||||
this._finishedUrl = FinishedUrl;
|
||||
UpdateTriggered(new string[] { "FinishedUrl" });
|
||||
UpdateTriggered("FinishedUrl", FinishedUrl);
|
||||
}
|
||||
}
|
||||
public void SetFinishedMessage(string FinishedMessage)
|
||||
@@ -206,7 +237,7 @@ namespace Disco.Services.Tasks
|
||||
if (this._finishedMessage != FinishedMessage)
|
||||
{
|
||||
this._finishedMessage = FinishedMessage;
|
||||
UpdateTriggered(new string[] { "FinishedMessage" });
|
||||
UpdateTriggered("FinishedMessage", FinishedMessage);
|
||||
}
|
||||
}
|
||||
public void SetNextScheduledTimestamp(DateTime? NextScheduledTimestamp)
|
||||
@@ -214,59 +245,62 @@ namespace Disco.Services.Tasks
|
||||
if (this._nextScheduledTimestamp != NextScheduledTimestamp)
|
||||
{
|
||||
this._nextScheduledTimestamp = NextScheduledTimestamp;
|
||||
UpdateTriggered(new string[] { "NextScheduledTimestamp" });
|
||||
UpdateTriggered("NextScheduledTimestamp", NextScheduledTimestamp);
|
||||
}
|
||||
}
|
||||
public void Started()
|
||||
{
|
||||
List<string> changedProperties = new List<string>() { "IsRunning", "StartedTimestamp" };
|
||||
var changedProperties = new List<ChangedItem>();
|
||||
|
||||
// Change StartedTimestamp
|
||||
this._startedTimestamp = DateTime.Now;
|
||||
changedProperties.Add(new ChangedItem("StartedTimestamp", this.StartedTimestamp));
|
||||
|
||||
if (this._finishedTimestamp != null)
|
||||
{
|
||||
this._finishedTimestamp = null;
|
||||
changedProperties.Add(new ChangedItem("FinishedTimestamp", this._finishedTimestamp));
|
||||
}
|
||||
|
||||
if (this._nextScheduledTimestamp != null)
|
||||
{
|
||||
this._nextScheduledTimestamp = null;
|
||||
changedProperties.Add("NextScheduledTimestamp");
|
||||
}
|
||||
if (this._finishedTimestamp != null)
|
||||
{
|
||||
this._finishedTimestamp = null;
|
||||
changedProperties.Add("FinishedTimestamp");
|
||||
changedProperties.Add(new ChangedItem("NextScheduledTimestamp", this._nextScheduledTimestamp));
|
||||
}
|
||||
|
||||
changedProperties.Add(new ChangedItem("IsRunning", this.IsRunning));
|
||||
|
||||
if (this._progress != 0)
|
||||
{
|
||||
this._progress = 0;
|
||||
changedProperties.Add("Progress");
|
||||
changedProperties.Add(new ChangedItem("Progress", this._progress));
|
||||
}
|
||||
if (this._currentProcess != "Starting")
|
||||
{
|
||||
this._currentProcess = "Starting";
|
||||
changedProperties.Add("CurrentProcess");
|
||||
changedProperties.Add(new ChangedItem("CurrentProcess", this._currentProcess));
|
||||
}
|
||||
if (this._currentDescription != "Initializing Task for Execution")
|
||||
{
|
||||
this._currentDescription = "Initializing Task for Execution";
|
||||
changedProperties.Add("CurrentDescription");
|
||||
changedProperties.Add(new ChangedItem("CurrentDescription", this._currentDescription));
|
||||
}
|
||||
if (this._taskException != null)
|
||||
{
|
||||
this._taskException = null;
|
||||
changedProperties.Add("TaskException");
|
||||
changedProperties.Add(new ChangedItem("TaskExceptionMessage", (this._taskException == null ? null : this._taskException.Message)));
|
||||
}
|
||||
if (this._cancelSupported != this._cancelInitiallySupported)
|
||||
{
|
||||
this._cancelSupported = this._cancelInitiallySupported;
|
||||
changedProperties.Add("CancelSupported");
|
||||
}
|
||||
{
|
||||
this._isCanceling = false;
|
||||
changedProperties.Add("IsCanceling");
|
||||
changedProperties.Add(new ChangedItem("CancelSupported", this._cancelSupported));
|
||||
}
|
||||
if (this._isCanceling)
|
||||
{
|
||||
this._isCanceling = false;
|
||||
changedProperties.Add("IsCanceling");
|
||||
changedProperties.Add(new ChangedItem("IsCanceling", this._isCanceling));
|
||||
}
|
||||
|
||||
UpdateTriggered(changedProperties.ToArray());
|
||||
}
|
||||
public void Finished()
|
||||
@@ -279,26 +313,29 @@ namespace Disco.Services.Tasks
|
||||
}
|
||||
public void Finished(string FinishedMessage, string FinishedUrl)
|
||||
{
|
||||
List<string> changedProperties = new List<string>() { "IsRunning", "FinishedTimestamp" };
|
||||
var changedProperties = new List<ChangedItem>();
|
||||
|
||||
this._finishedTimestamp = DateTime.Now;
|
||||
changedProperties.Add(new ChangedItem("FinishedTimestamp", this._finishedTimestamp));
|
||||
changedProperties.Add(new ChangedItem("IsRunning", this.IsRunning));
|
||||
|
||||
if (FinishedMessage != this._finishedMessage)
|
||||
{
|
||||
this._finishedMessage = FinishedMessage;
|
||||
changedProperties.Add("FinishedMessage");
|
||||
changedProperties.Add(new ChangedItem("FinishedMessage", this._finishedMessage));
|
||||
}
|
||||
if (FinishedUrl != this._finishedUrl)
|
||||
{
|
||||
this._finishedUrl = FinishedUrl;
|
||||
changedProperties.Add("FinishedUrl");
|
||||
changedProperties.Add(new ChangedItem("FinishedUrl", this._finishedUrl));
|
||||
}
|
||||
|
||||
if (this._isCanceling)
|
||||
{
|
||||
this._isCanceling = false;
|
||||
changedProperties.Add("IsCanceling");
|
||||
changedProperties.Add(new ChangedItem("IsCanceling", this._isCanceling));
|
||||
}
|
||||
|
||||
UpdateTriggered(changedProperties.ToArray());
|
||||
}
|
||||
internal void Finally()
|
||||
@@ -311,53 +348,58 @@ namespace Disco.Services.Tasks
|
||||
this._tcs.Task.Dispose();
|
||||
this._tcs = new TaskCompletionSource<ScheduledTaskStatus>();
|
||||
|
||||
List<string> changedProperties = new List<string>();
|
||||
var changedProperties = new List<ChangedItem>();
|
||||
|
||||
if (this._nextScheduledTimestamp != NextScheduledTimestamp)
|
||||
{
|
||||
this._nextScheduledTimestamp = NextScheduledTimestamp;
|
||||
changedProperties.Add("NextScheduledTimestamp");
|
||||
changedProperties.Add(new ChangedItem("NextScheduledTimestamp", this._nextScheduledTimestamp));
|
||||
}
|
||||
|
||||
if (this._startedTimestamp != null)
|
||||
{
|
||||
this._startedTimestamp = null;
|
||||
changedProperties.Add("StartedTimestamp");
|
||||
changedProperties.Add(new ChangedItem("StartedTimestamp", this._startedTimestamp));
|
||||
}
|
||||
if (this._finishedTimestamp != null)
|
||||
{
|
||||
this._finishedTimestamp = null;
|
||||
changedProperties.Add("FinishedTimestamp");
|
||||
changedProperties.Add(new ChangedItem("FinishedTimestamp", this._finishedTimestamp));
|
||||
}
|
||||
if (this._finishedMessage != null)
|
||||
{
|
||||
this._finishedMessage = null;
|
||||
changedProperties.Add("FinishedMessage");
|
||||
changedProperties.Add(new ChangedItem("FinishedMessage", this._finishedMessage));
|
||||
}
|
||||
if (this._finishedUrl != null)
|
||||
{
|
||||
this._finishedUrl = null;
|
||||
changedProperties.Add("FinishedUrl");
|
||||
changedProperties.Add(new ChangedItem("FinishedUrl", this._finishedUrl));
|
||||
}
|
||||
if (this._progress != 0)
|
||||
{
|
||||
this._progress = 0;
|
||||
changedProperties.Add("Progress");
|
||||
changedProperties.Add(new ChangedItem("Progress", this._progress));
|
||||
}
|
||||
this.ProgressMultiplier = 0;
|
||||
this.ProgressOffset = 0;
|
||||
this.IgnoreCurrentDescription = false;
|
||||
this.IgnoreCurrentProcessChanges = false;
|
||||
|
||||
if (this._currentProcess != "Scheduled")
|
||||
{
|
||||
this._currentProcess = "Scheduled";
|
||||
changedProperties.Add("CurrentProcess");
|
||||
changedProperties.Add(new ChangedItem("CurrentProcess", this._currentProcess));
|
||||
}
|
||||
if (this._currentDescription != "Scheduled Task for Execution")
|
||||
{
|
||||
this._currentDescription = "Scheduled Task for Execution";
|
||||
changedProperties.Add("CurrentDescription");
|
||||
changedProperties.Add(new ChangedItem("CurrentDescription", this._currentDescription));
|
||||
}
|
||||
if (this._isCanceling)
|
||||
{
|
||||
this._isCanceling = false;
|
||||
changedProperties.Add("IsCanceling");
|
||||
changedProperties.Add(new ChangedItem("IsCanceling", this._isCanceling));
|
||||
}
|
||||
UpdateTriggered(changedProperties.ToArray());
|
||||
}
|
||||
@@ -373,15 +415,20 @@ namespace Disco.Services.Tasks
|
||||
}
|
||||
#endregion
|
||||
|
||||
private void UpdateTriggered(string[] ChangedProperties)
|
||||
private void UpdateTriggered(string ChangedProperty, object NewValue)
|
||||
{
|
||||
UpdateTriggered(new ChangedItem[] { new ChangedItem(ChangedProperty, NewValue) });
|
||||
}
|
||||
|
||||
private void UpdateTriggered(params ChangedItem[] ChangedProperties)
|
||||
{
|
||||
this._statusVersion++;
|
||||
|
||||
if (Updated != null)
|
||||
Updated(this, ChangedProperties);
|
||||
|
||||
if (!_isSilent && UpdatedBroadcast != null)
|
||||
UpdatedBroadcast.Invoke(ScheduledTaskStatusLive.FromScheduledTaskStatus(this, ChangedProperties));
|
||||
if (!_isSilent)
|
||||
ScheduledTaskNotificationsHub.PublishEvent(this.SessionId, ChangedProperties);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,13 +28,16 @@ namespace Disco.Services.Tasks
|
||||
// Scheduled Cleanup
|
||||
ScheduledTaskCleanup.Schedule(_TaskScheduler);
|
||||
|
||||
ScheduledTaskNotificationsHub.Initialize();
|
||||
|
||||
if (InitiallySchedule)
|
||||
{
|
||||
// Discover DiscoScheduledTask
|
||||
var appDomain = AppDomain.CurrentDomain;
|
||||
var scheduledTasksHostAssemblyName = typeof(ScheduledTask).Assembly.GetName().Name;
|
||||
|
||||
var scheduledTaskTypes = (from a in appDomain.GetAssemblies()
|
||||
where !a.GlobalAssemblyCache && !a.IsDynamic
|
||||
where !a.GlobalAssemblyCache && !a.IsDynamic && a.GetReferencedAssemblies().Any(ra => ra.Name == scheduledTasksHostAssemblyName)
|
||||
from type in a.GetTypes()
|
||||
where typeof(ScheduledTask).IsAssignableFrom(type) && !type.IsAbstract
|
||||
select type);
|
||||
|
||||
@@ -1,10 +1,6 @@
|
||||
using Disco.Models.Repository;
|
||||
using Disco.Services.Interop.ActiveDirectory;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Disco.Services
|
||||
{
|
||||
@@ -15,6 +11,11 @@ namespace Disco.Services
|
||||
return u.Domain.Equals(ActiveDirectory.Context.PrimaryDomain.NetBiosName, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
public static string ToStringFriendly(this User u)
|
||||
{
|
||||
return string.Format("{0} ({1})", u.DisplayName, u.FriendlyId());
|
||||
}
|
||||
|
||||
public static string FriendlyId(this User u)
|
||||
{
|
||||
return FriendlyUserId(u.UserId);
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
using Disco.Data.Repository.Monitor;
|
||||
using Disco.Services.Authorization;
|
||||
using Disco.Services.Web.Signalling;
|
||||
using Microsoft.AspNet.SignalR;
|
||||
using Microsoft.AspNet.SignalR.Hubs;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using System.Reactive.Linq;
|
||||
using Disco.Models.Repository;
|
||||
using Disco.Data.Repository;
|
||||
|
||||
namespace Disco.Services.Users
|
||||
{
|
||||
[HubName("userUpdates"), DiscoHubAuthorizeAll(Claims.User.Show, Claims.User.ShowAttachments)]
|
||||
public class UserUpdatesHub : Hub
|
||||
{
|
||||
private const string UserPrefix = "User_";
|
||||
public static IHubContext HubContext { get; private set; }
|
||||
|
||||
private static IDisposable RepositoryBeforeSubscription;
|
||||
private static IDisposable RepositoryAfterSubscription;
|
||||
|
||||
static UserUpdatesHub()
|
||||
{
|
||||
HubContext = GlobalHost.ConnectionManager.GetHubContext<UserUpdatesHub>();
|
||||
|
||||
// Subscribe to Repository Monitor for Changes
|
||||
RepositoryBeforeSubscription = RepositoryMonitor.StreamBeforeCommit
|
||||
.Where(e => e.EntityType == typeof(UserAttachment) && e.EventType == RepositoryMonitorEventType.Deleted)
|
||||
.Subscribe(RepositoryEventBefore);
|
||||
RepositoryAfterSubscription = RepositoryMonitor.StreamAfterCommit
|
||||
.Where(e => e.EntityType == typeof(UserAttachment) && e.EventType == RepositoryMonitorEventType.Added)
|
||||
.Subscribe(RepositoryAfterEvent);
|
||||
}
|
||||
|
||||
private static string GroupName(string UserId)
|
||||
{
|
||||
return UserPrefix + UserId;
|
||||
}
|
||||
|
||||
public override Task OnConnected()
|
||||
{
|
||||
var userId = Context.QueryString["UserId"];
|
||||
|
||||
if (string.IsNullOrWhiteSpace(userId))
|
||||
throw new ArgumentNullException("UserId");
|
||||
|
||||
Groups.Add(Context.ConnectionId, GroupName(userId));
|
||||
|
||||
return base.OnConnected();
|
||||
}
|
||||
|
||||
private static void RepositoryEventBefore(RepositoryMonitorEvent e)
|
||||
{
|
||||
if (e.EventType == RepositoryMonitorEventType.Deleted)
|
||||
{
|
||||
if (e.EntityType == typeof(UserAttachment))
|
||||
{
|
||||
var repositoryAttachment = (UserAttachment)e.Entity;
|
||||
string attachmentUserId;
|
||||
|
||||
using (DiscoDataContext Database = new DiscoDataContext())
|
||||
attachmentUserId = Database.UserAttachments.Where(a => a.Id == repositoryAttachment.Id).Select(a => a.UserId).First();
|
||||
|
||||
HubContext.Clients.Group(GroupName(attachmentUserId)).removeAttachment(repositoryAttachment.Id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void RepositoryAfterEvent(RepositoryMonitorEvent e)
|
||||
{
|
||||
if (e.EventType == RepositoryMonitorEventType.Added)
|
||||
{
|
||||
if (e.EntityType == typeof(UserAttachment))
|
||||
{
|
||||
var a = (UserAttachment)e.Entity;
|
||||
|
||||
HubContext.Clients.Group(GroupName(a.UserId)).addAttachment(a.Id);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -16,21 +16,21 @@ namespace Disco.Services.Web
|
||||
// Ensure 'App-Relative' Url:
|
||||
BundleUrl = BundleUrl.StartsWith("~/") ? BundleUrl : (BundleUrl.StartsWith("/") ? string.Concat("~", BundleUrl) : string.Concat("~/", BundleUrl));
|
||||
|
||||
var deferredBundles = htmlHelper.ViewContext.HttpContext.Items[Bundle.DeferredKey] as List<string>;
|
||||
var deferredBundles = htmlHelper.ViewContext.HttpContext.Items[BundleTable.DeferredKey] as List<string>;
|
||||
if (deferredBundles == null)
|
||||
{
|
||||
deferredBundles = new List<string>();
|
||||
htmlHelper.ViewContext.HttpContext.Items[Bundle.DeferredKey] = deferredBundles;
|
||||
htmlHelper.ViewContext.HttpContext.Items[BundleTable.DeferredKey] = deferredBundles;
|
||||
}
|
||||
if (!deferredBundles.Contains(BundleUrl))
|
||||
deferredBundles.Add(BundleUrl);
|
||||
}
|
||||
public static HtmlString BundleRenderDeferred(this HtmlHelper htmlHelper)
|
||||
{
|
||||
var deferredBundles = htmlHelper.ViewContext.HttpContext.Items[Bundle.DeferredKey] as List<string>;
|
||||
var deferredBundles = htmlHelper.ViewContext.HttpContext.Items[BundleTable.DeferredKey] as List<string>;
|
||||
|
||||
var uiExtensionScripts = htmlHelper.ViewContext.HttpContext.Items[Bundle.UIExtensionScriptsKey] as List<string>;
|
||||
var uiExtensionCss = htmlHelper.ViewContext.HttpContext.Items[Bundle.UIExtensionCssKey] as List<string>;
|
||||
var uiExtensionScripts = htmlHelper.ViewContext.HttpContext.Items[BundleTable.UIExtensionScriptsKey] as List<string>;
|
||||
var uiExtensionCss = htmlHelper.ViewContext.HttpContext.Items[BundleTable.UIExtensionCssKey] as List<string>;
|
||||
|
||||
if (deferredBundles != null || uiExtensionScripts != null || uiExtensionCss != null)
|
||||
{
|
||||
|
||||
@@ -9,10 +9,10 @@ namespace Disco.Services.Web.Bundles
|
||||
{
|
||||
internal sealed class BundleHandler : IHttpHandler
|
||||
{
|
||||
public Bundle RequestBundle { get; private set; }
|
||||
public IBundle RequestBundle { get; private set; }
|
||||
public string BundleVirtualPath { get; private set; }
|
||||
|
||||
public BundleHandler(Bundle requestBundle, string bundleVirtualPath)
|
||||
public BundleHandler(IBundle requestBundle, string bundleVirtualPath)
|
||||
{
|
||||
this.RequestBundle = requestBundle;
|
||||
this.BundleVirtualPath = bundleVirtualPath;
|
||||
|
||||
@@ -9,14 +9,18 @@ namespace Disco.Services.Web.Bundles
|
||||
{
|
||||
public static class BundleTable
|
||||
{
|
||||
private static Dictionary<string, Bundle> _bundles;
|
||||
public const string DeferredKey = "Bundles.Deferred";
|
||||
public const string UIExtensionScriptsKey = "Bundles.UIExtensionScripts";
|
||||
public const string UIExtensionCssKey = "Bundles.UIExtensionCss";
|
||||
|
||||
private static Dictionary<string, IBundle> _bundles;
|
||||
|
||||
static BundleTable()
|
||||
{
|
||||
_bundles = new Dictionary<string, Bundle>();
|
||||
_bundles = new Dictionary<string, IBundle>();
|
||||
}
|
||||
|
||||
public static void Add(Bundle Bundle)
|
||||
public static void Add(IBundle Bundle)
|
||||
{
|
||||
_bundles[Bundle.Url] = Bundle;
|
||||
}
|
||||
@@ -29,7 +33,7 @@ namespace Disco.Services.Web.Bundles
|
||||
}
|
||||
}
|
||||
|
||||
internal static Bundle GetBundleFor(string Url)
|
||||
internal static IBundle GetBundleFor(string Url)
|
||||
{
|
||||
if (_bundles.ContainsKey(Url))
|
||||
{
|
||||
|
||||
@@ -9,16 +9,13 @@ using System.Web;
|
||||
|
||||
namespace Disco.Services.Web.Bundles
|
||||
{
|
||||
public class Bundle
|
||||
public class FileBundle : IBundle
|
||||
{
|
||||
public const string DeferredKey = "Bundles.Deferred";
|
||||
public const string UIExtensionScriptsKey = "Bundles.UIExtensionScripts";
|
||||
public const string UIExtensionCssKey = "Bundles.UIExtensionCss";
|
||||
|
||||
private DateTime? _FileLastModified { get; set; }
|
||||
private string _FileHash { get; set; }
|
||||
private string _VersionUrl { get; set; }
|
||||
|
||||
public bool RemapRequest { get { return true; } }
|
||||
public string Url { get; private set; }
|
||||
public string File { get; private set; }
|
||||
public string FileHash
|
||||
@@ -44,7 +41,7 @@ namespace Disco.Services.Web.Bundles
|
||||
}
|
||||
}
|
||||
|
||||
public Bundle(string Url, string File)
|
||||
public FileBundle(string Url, string File)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(Url))
|
||||
throw new ArgumentNullException("Url");
|
||||
@@ -117,7 +114,7 @@ namespace Disco.Services.Web.Bundles
|
||||
this._FileHash = string.Empty;
|
||||
}
|
||||
|
||||
internal void ProcessRequest(HttpContext context)
|
||||
public void ProcessRequest(HttpContext context)
|
||||
{
|
||||
// Write Content Type
|
||||
context.Response.ContentType = this.ContentType;
|
||||
@@ -0,0 +1,23 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using System.Web;
|
||||
|
||||
namespace Disco.Services.Web.Bundles
|
||||
{
|
||||
public interface IBundle
|
||||
{
|
||||
bool RemapRequest { get; }
|
||||
|
||||
string Url { get; }
|
||||
string VersionUrl { get; }
|
||||
|
||||
string ContentType { get; }
|
||||
|
||||
void ProcessRequest(HttpContext context);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Disco.Services.Web.Bundles
|
||||
{
|
||||
public class UrlBundle : IBundle
|
||||
{
|
||||
public bool RemapRequest { get { return false; } }
|
||||
|
||||
public string Url { get; private set; }
|
||||
public string VersionUrl { get; private set; }
|
||||
|
||||
public string ContentType { get; private set; }
|
||||
|
||||
public void ProcessRequest(System.Web.HttpContext context)
|
||||
{
|
||||
// Not needed for Url Bundle
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public UrlBundle(string Url, string ContentType)
|
||||
{
|
||||
this.Url = Url;
|
||||
this.VersionUrl = Url;
|
||||
|
||||
this.ContentType = ContentType;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
using Disco.Services.Users;
|
||||
using Microsoft.AspNet.SignalR;
|
||||
using System;
|
||||
using System.Security.Principal;
|
||||
|
||||
namespace Disco.Services.Web.Signalling
|
||||
{
|
||||
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, Inherited = true, AllowMultiple = true)]
|
||||
public class DiscoHubAuthorizeAllAttribute : AuthorizeAttribute
|
||||
{
|
||||
string[] authorizedClaims;
|
||||
|
||||
public DiscoHubAuthorizeAllAttribute(params string[] AuthorisedClaims)
|
||||
{
|
||||
if (AuthorisedClaims == null || AuthorisedClaims.Length == 0)
|
||||
throw new ArgumentNullException("AuthorisedClaims");
|
||||
|
||||
this.authorizedClaims = AuthorisedClaims;
|
||||
}
|
||||
|
||||
protected override bool UserAuthorized(IPrincipal user)
|
||||
{
|
||||
if (user == null || !user.Identity.IsAuthenticated)
|
||||
return false;
|
||||
|
||||
var username = user.Identity.Name;
|
||||
var userToken = UserService.GetAuthorization(username);
|
||||
|
||||
if (userToken == null)
|
||||
return false; // No User
|
||||
|
||||
return userToken.HasAll(authorizedClaims);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
using Disco.Services.Users;
|
||||
using Microsoft.AspNet.SignalR;
|
||||
using System;
|
||||
using System.Security.Principal;
|
||||
|
||||
namespace Disco.Services.Web.Signalling
|
||||
{
|
||||
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, Inherited = true, AllowMultiple = true)]
|
||||
public class DiscoHubAuthorizeAnyAttribute : AuthorizeAttribute
|
||||
{
|
||||
string[] authorizedClaims;
|
||||
|
||||
public DiscoHubAuthorizeAnyAttribute(params string[] AuthorisedClaims)
|
||||
{
|
||||
if (AuthorisedClaims == null || AuthorisedClaims.Length == 0)
|
||||
throw new ArgumentNullException("AuthorisedClaims");
|
||||
|
||||
this.authorizedClaims = AuthorisedClaims;
|
||||
}
|
||||
|
||||
protected override bool UserAuthorized(IPrincipal user)
|
||||
{
|
||||
if (user == null || !user.Identity.IsAuthenticated)
|
||||
return false;
|
||||
|
||||
var username = user.Identity.Name;
|
||||
var userToken = UserService.GetAuthorization(username);
|
||||
|
||||
if (userToken == null)
|
||||
return false; // No User
|
||||
|
||||
return userToken.HasAny(authorizedClaims);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
using Disco.Services.Users;
|
||||
using Microsoft.AspNet.SignalR;
|
||||
using System;
|
||||
using System.Security.Principal;
|
||||
|
||||
namespace Disco.Services.Web.Signalling
|
||||
{
|
||||
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, Inherited = true, AllowMultiple = true)]
|
||||
public class DiscoHubAuthorizeAttribute : AuthorizeAttribute
|
||||
{
|
||||
string authorizedClaim;
|
||||
|
||||
public DiscoHubAuthorizeAttribute() { }
|
||||
|
||||
public DiscoHubAuthorizeAttribute(string AuthorisedClaim)
|
||||
{
|
||||
this.authorizedClaim = AuthorisedClaim;
|
||||
}
|
||||
|
||||
protected override bool UserAuthorized(IPrincipal user)
|
||||
{
|
||||
if (user == null || !user.Identity.IsAuthenticated)
|
||||
return false;
|
||||
|
||||
var username = user.Identity.Name;
|
||||
var userToken = UserService.GetAuthorization(username);
|
||||
|
||||
if (userToken == null)
|
||||
return false; // No User
|
||||
|
||||
if (authorizedClaim == null)
|
||||
return userToken.RoleTokens.Count > 0; // Just Authenticate - no Authorization (but require at least 1 role)
|
||||
else
|
||||
return userToken.Has(authorizedClaim);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,12 +4,13 @@
|
||||
<package id="LumenWorks.Framework.IO" version="3.8.0" targetFramework="net45" />
|
||||
<package id="Microsoft.AspNet.Mvc" version="4.0.30506.0" targetFramework="net45" />
|
||||
<package id="Microsoft.AspNet.Razor" version="2.0.30506.0" targetFramework="net45" />
|
||||
<package id="Microsoft.AspNet.SignalR.Core" version="1.1.2" targetFramework="net45" />
|
||||
<package id="Microsoft.AspNet.SignalR.Owin" version="1.1.2" targetFramework="net45" />
|
||||
<package id="Microsoft.AspNet.SignalR.Core" version="2.0.3" targetFramework="net45" />
|
||||
<package id="Microsoft.AspNet.WebPages" version="2.0.30506.0" targetFramework="net45" />
|
||||
<package id="Microsoft.Owin" version="2.0.1" targetFramework="net45" />
|
||||
<package id="Microsoft.Owin.Security" version="2.0.1" targetFramework="net45" />
|
||||
<package id="Microsoft.SqlServer.Compact" version="4.0.8876.1" targetFramework="net45" />
|
||||
<package id="Microsoft.Web.Infrastructure" version="1.0.0.0" targetFramework="net40" />
|
||||
<package id="Newtonsoft.Json" version="5.0.8" targetFramework="net45" />
|
||||
<package id="Newtonsoft.Json" version="5.0.1" targetFramework="net45" />
|
||||
<package id="Owin" version="1.0" targetFramework="net45" />
|
||||
<package id="RazorGenerator.Mvc" version="2.1.2" targetFramework="net45" />
|
||||
<package id="Rx-Core" version="2.1.30214.0" targetFramework="net45" />
|
||||
|
||||
Reference in New Issue
Block a user