Ejemplo n.º 1
0
        public static PluginLibraryManifestV2 UpdateManifest(DiscoDataContext Database, IScheduledTaskStatus Status)
        {
            Status.UpdateStatus(10, "Sending Request");

            PluginLibraryManifestV2 result;

            var discoVersion = UpdateQuery.CurrentDiscoVersionFormatted();
            var url = UpdateUrl();

            using (var httpClient = new HttpClient())
            {
                using (var formData = new FormUrlEncodedContent(new KeyValuePair<string, string>[] {
                    new KeyValuePair<string, string>("DeploymentId", Database.DiscoConfiguration.DeploymentId),
                    new KeyValuePair<string, string>("DiscoVersion", discoVersion)
                }))
                {
                    var response = httpClient.PostAsync(url, formData).Result;

                    response.EnsureSuccessStatusCode();

                    Status.UpdateStatus(50, "Waiting for Response");

                    var resultJson = response.Content.ReadAsStringAsync().Result;

                    Status.UpdateStatus(90, "Processing Response");

                    result = JsonConvert.DeserializeObject<PluginLibraryManifestV2>(resultJson);
                }
            }

            var manifestJson = JsonConvert.SerializeObject(result, Formatting.Indented);

            var manifestFile = PluginLibrary.ManifestFilename(Database);

            if (!Directory.Exists(Path.GetDirectoryName(manifestFile)))
                Directory.CreateDirectory(Path.GetDirectoryName(manifestFile));

            File.WriteAllText(manifestFile, manifestJson);

            return result;
        }
Ejemplo n.º 2
0
        public static UpdateResponseV2 Check(DiscoDataContext Database, bool UseProxy, IScheduledTaskStatus Status)
        {
            Status.UpdateStatus(10, "Gathering statistics and building update request");

            var updateRequest = BuildRequest(Database);

            Status.UpdateStatus(40, "Sending statistics and update request");

            var discoVersion = CurrentDiscoVersionFormatted();

            HttpWebRequest request = (HttpWebRequest)HttpWebRequest.Create(UpdateUrl());

            // Fix for Proxy Servers which don't support KeepAlive
            request.KeepAlive = false;

            if (!UseProxy)
                request.Proxy = new WebProxy();

            request.ContentType = "application/json; charset=utf-8; encoding=gzip";
            request.Method = WebRequestMethods.Http.Post;
            request.UserAgent = string.Format("Disco/{0} (Update)", discoVersion);

            using (var requestStream = request.GetRequestStream())
            {
                using (var compressedStream = new GZipStream(requestStream, CompressionLevel.Optimal))
                {
                    using (var requestStreamWriter = new StreamWriter(compressedStream, Encoding.UTF8))
                    {
                        JsonSerializer serializer = new JsonSerializer();
                        serializer.Serialize(requestStreamWriter, updateRequest);

                        requestStreamWriter.Flush();
                    }
                }
            }

            Status.UpdateStatus(50, "Waiting for update response");
            using (HttpWebResponse response = (HttpWebResponse)request.GetResponse())
            {
                if (response.StatusCode == HttpStatusCode.OK)
                {
                    Status.UpdateStatus(90, "Reading update response");
                    string updateResultJson;
                    UpdateResponseV2 updateResult;

                    using (var responseStream = response.GetResponseStream())
                    {
                        using (var responseReader = new StreamReader(responseStream))
                        {
                            updateResultJson = responseReader.ReadToEnd();
                        }
                    }

                    updateResult = JsonConvert.DeserializeObject<UpdateResponseV2>(updateResultJson);

                    Database.DiscoConfiguration.UpdateLastCheckResponse = updateResult;
                    Database.SaveChanges();

                    Status.SetFinishedMessage(string.Format("The update server reported Version {0} is the latest.", updateResult.LatestVersion));

                    return updateResult;
                }
                else
                {
                    Status.SetTaskException(new WebException(string.Format("Server responded with: [{0}] {1}", response.StatusCode, response.StatusDescription)));
                    return null;
                }
            }
        }
Ejemplo n.º 3
0
        public static void DeleteJobQueue(DiscoDataContext Database, int JobQueueId, IScheduledTaskStatus Status)
        {
            JobQueue queue = Database.JobQueues.Find(JobQueueId);

            // Validate: Current Jobs?
            var currentJobs = Database.JobQueueJobs.Any(jqj => jqj.JobQueueId == queue.Id && !jqj.RemovedDate.HasValue);
            if (currentJobs)
                throw new InvalidOperationException("The Job Queue cannot be deleted because it contains jobs");

            // Delete History
            Status.UpdateStatus(0, string.Format("Removing '{0}' [{1}] Job Queue", queue.Name, queue.Id), "Starting");
            var jobQueueJobs = Database.JobQueueJobs.Include("Job").Where(jsj => jsj.JobQueueId == queue.Id).ToList();
            if (jobQueueJobs.Count > 0)
            {
                double progressInterval = 90 / jobQueueJobs.Count;
                for (int jqjIndex = 0; jqjIndex < jobQueueJobs.Count; jqjIndex++)
                {
                    var jqj = jobQueueJobs[jqjIndex];

                    Status.UpdateStatus(jqjIndex * progressInterval, string.Format("Merging history into job #{0} logs", jqj.JobId));

                    // Write Logs
                    Database.JobLogs.Add(new JobLog()
                    {
                        JobId = jqj.JobId,
                        TechUserId = jqj.AddedUserId,
                        Timestamp = jqj.AddedDate,
                        Comments = string.Format("# Added to Queue\r\n**{0}**\r\nPriority: **{1}**\r\n{2}", Environment.NewLine, queue.Name, jqj.Priority.ToString(), string.IsNullOrWhiteSpace(jqj.AddedComment) ? "<no comment>" : jqj.AddedComment)
                    });
                    Database.JobLogs.Add(new JobLog()
                    {
                        JobId = jqj.JobId,
                        TechUserId = jqj.RemovedUserId,
                        Timestamp = jqj.RemovedDate.Value,
                        Comments = string.Format("# Removed from Queue\r\n**{0}**\r\n{1}", queue.Name, string.IsNullOrWhiteSpace(jqj.RemovedComment) ? "<no comment>" : jqj.RemovedComment)
                    });

                    // Delete JQJ
                    Database.JobQueueJobs.Remove(jqj);

                    // Save Changes
                    Database.SaveChanges();
                }
            }

            // Delete Queue
            Status.UpdateStatus(90, "Deleting Queue");
            Database.JobQueues.Remove(queue);
            Database.SaveChanges();

            // Remove from Cache
            _cache.RemoveQueue(JobQueueId);

            Status.Finished(string.Format("Successfully Deleted Job Queue: '{0}' [{1}]", queue.Name, queue.Id));
        }
Ejemplo n.º 4
0
        public static int ApplyRecords(this DeviceImportContext Context, DiscoDataContext Database, IScheduledTaskStatus Status)
        {
            if (Context.Records == null)
                throw new InvalidOperationException("Import Records have not been parsed");

            if (Context.Records.Count == 0)
                throw new InvalidOperationException("There are no records to import");

            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)))
            {
                Status.UpdateStatus(((double)record.Item2 / Context.Records.Count) * 100, string.Format("Applying: {0}", record.Item1.DeviceSerialNumber));

                if (record.Item1.Apply(Database))
                    affectedRecords++;
            }

            return affectedRecords;
        }
Ejemplo n.º 5
0
        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");

            if (Context.Header.Count == 0)
                throw new InvalidOperationException("No Headers were found");

            if (!Context.Header.Any(h => h.Item2 == DeviceImportFieldTypes.DeviceSerialNumber))
                throw new ArgumentException("At least one column must be the Device Serial Number", "Header");

            if (Context.RawData == null || Context.RawData.Count == 0)
                throw new ArgumentException("No data was found in the import file", "RawData");

            IDeviceImportCache cache;
            if (Context.RawData.Count > 20)
                cache = new DeviceImportInMemoryCache(Database);
            else
                cache = new DeviceImportDatabaseCache(Database);

            Context.HeaderDeviceSerialNumberIndex = Context.Header.IndexOf(Context.Header.First(h => h.Item2 == DeviceImportFieldTypes.DeviceSerialNumber));
            Context.ParsedHeaders = Context.Header
                .Select((h, i) => Tuple.Create(h.Item1, h.Item2, i))
                .Where(h => h.Item2 != DeviceImportFieldTypes.IgnoreColumn)
                .Select(h => new Tuple<string, DeviceImportFieldTypes, Func<string[], string>, Type>(h.Item1, h.Item2, (f) => f[h.Item3], DeviceImport.FieldHandlers.Value[h.Item2]))
                .ToList();

            Status.UpdateStatus(0, "Parsing Import Records", "Starting...");

            Context.Records = Context.RawData.Select((d, recordIndex) =>
            {
                string deviceSerialNumber = Fields.DeviceSerialNumberImportField.ParseRawDeviceSerialNumber(d[Context.HeaderDeviceSerialNumberIndex]);

                Status.UpdateStatus(((double)recordIndex / Context.RawData.Count) * 100, string.Format("Parsing: {0}", deviceSerialNumber));

                Device existingDevice = null;
                if (Fields.DeviceSerialNumberImportField.IsDeviceSerialNumberValid(deviceSerialNumber))
                    existingDevice = cache.Devices.FirstOrDefault(device => device.SerialNumber == deviceSerialNumber);

                var values = Context.ParsedHeaders
                    .ToDictionary(k => k.Item2, k => k.Item3(d));

                var fields = Context.ParsedHeaders.Select(h =>
                {
                    var f = (DeviceImportFieldBase)Activator.CreateInstance(h.Item4);
                    f.Parse(Database, cache, Context, recordIndex, deviceSerialNumber, existingDevice, values, h.Item3(d));
                    return f;
                }).ToList();

                EntityState recordAction;
                if (fields.Any(f => !f.FieldAction.HasValue))
                    recordAction = EntityState.Detached;
                else if (existingDevice == null)
                    recordAction = EntityState.Added;
                else if (fields.Any(f => f.FieldAction == EntityState.Modified))
                    recordAction = EntityState.Modified;
                else
                    recordAction = EntityState.Unchanged;

                return new DeviceImportRecord(deviceSerialNumber, fields, recordAction);
            }).Cast<IDeviceImportRecord>().ToList();
        }
        public static void UpdateLastNetworkLogonDates(DiscoDataContext Database, IScheduledTaskStatus status)
        {
            var context = ActiveDirectory.Context;
            const string ldapFilter = "(objectCategory=Computer)";
            string[] ldapProperties = new string[] { "sAMAccountName", "lastLogon" };

            status.UpdateStatus(2, "Initializing", "Determining Domains and Available Domain Controllers");

            // Determine Domain Scopes to Query
            var domainQueries = context.Domains
                .Select(d => Tuple.Create(d, d.SearchContainers ?? new List<string>() { d.DistinguishedName }))
                .Where(d => d.Item2.Count > 0);

            // Determine Domain Controllers to Query
            IEnumerable<Tuple<ADDomain, ADDomainController, List<string>>> serverQueries;
            if (context.SearchAllForestServers)
                serverQueries = domainQueries.SelectMany(q => q.Item1.GetAllReachableDomainControllers(), (q, dc) => Tuple.Create(q.Item1, dc, q.Item2));
            else
                serverQueries = domainQueries.SelectMany(q => q.Item1.GetReachableSiteDomainControllers(), (q, dc) => Tuple.Create(q.Item1, dc, q.Item2));

            var scopedQueries = serverQueries.SelectMany(q => q.Item3, (q, scope) => Tuple.Create(q.Item1, q.Item2, scope)).ToList();

            var queries = Enumerable.Range(0, scopedQueries.Count).Select(i =>
            {
                var q = scopedQueries[i];
                return Tuple.Create(i, q.Item1, q.Item2, q.Item3);
            });

            var queryResults = queries.SelectMany(q =>
            {
                var queryIndex = q.Item1;
                var domain = q.Item2;
                var domainController = q.Item3;
                var searchRoot = q.Item4;

                // Update Status
                double progress = 5 + (queryIndex * (90 / scopedQueries.Count));
                status.UpdateStatus(progress, string.Format("Querying Domain [{0}] using controller [{1}]", domain.NetBiosName, domainController.Name), string.Format("Searching: {0}", searchRoot));

                // Perform Query
                var directoryResults = domainController.SearchInternal(searchRoot, ldapFilter, ldapProperties, null);

                return directoryResults.Select(result =>
                {
                    var samAccountName = result.Value<string>("sAMAccountName");

                    long lastLogonValue = default(long);
                    long lastLogonTimestampValue = default(long);

                    lastLogonValue = result.Value<long>("lastLogon");
                    lastLogonTimestampValue = result.Value<long>("lastLogonTimestamp");

                    long highedValue = Math.Max(lastLogonValue, lastLogonTimestampValue);

                    if (highedValue > 0)
                    {
                        var computerName = string.Format(@"{0}\{1}", domain.NetBiosName, samAccountName.TrimEnd('$'));
                        var lastLogon = new DateTime((DateTime.FromFileTime(highedValue).Ticks / 10000000L) * 10000000L);
                        return Tuple.Create(computerName, lastLogon);
                    }
                    else
                        return null;
                }).Where(i => i != null).ToList();

            }).GroupBy(r => r.Item1, StringComparer.OrdinalIgnoreCase).ToDictionary(g => g.Key.ToUpper(), g => g.Max(i => i.Item2));

            status.UpdateStatus(90, "Processing Results", "Processing last network logon dates and looking for updates");

            foreach (Device device in Database.Devices.Where(device => device.DeviceDomainId != null))
            {
                DateTime lastLogonDate;
                if (queryResults.TryGetValue(device.DeviceDomainId.ToUpper(), out lastLogonDate))
                {
                    if (!device.LastNetworkLogonDate.HasValue)
                        device.LastNetworkLogonDate = lastLogonDate;
                    else
                    {
                        // Change accuracy to the second
                        lastLogonDate = new DateTime((lastLogonDate.Ticks / 10000000L) * 10000000L);

                        if (device.LastNetworkLogonDate.Value < lastLogonDate)
                            device.LastNetworkLogonDate = lastLogonDate;
                    }
                }
            }
        }
Ejemplo n.º 7
0
        public static void DeleteUserFlag(DiscoDataContext Database, int UserFlagId, IScheduledTaskStatus Status)
        {
            UserFlag flag = Database.UserFlags.Find(UserFlagId);

            // Dispose of AD Managed Groups
            Interop.ActiveDirectory.ActiveDirectory.Context.ManagedGroups.Remove(UserFlagUserDevicesManagedGroup.GetKey(flag));
            Interop.ActiveDirectory.ActiveDirectory.Context.ManagedGroups.Remove(UserFlagUsersManagedGroup.GetKey(flag));

            // Delete Assignments
            Status.UpdateStatus(0, string.Format("Removing '{0}' [{1}] User Flag", flag.Name, flag.Id), "Starting");
            List<UserFlagAssignment> flagAssignments = Database.UserFlagAssignments.Where(fa => fa.UserFlagId == flag.Id).ToList();
            if (flagAssignments.Count > 0)
            {
                Status.UpdateStatus(20, "Removing flag from users");
                flagAssignments.ForEach(flagAssignment => Database.UserFlagAssignments.Remove(flagAssignment));
                Database.SaveChanges();
            }

            // Delete Flag
            Status.UpdateStatus(90, "Deleting User Flag");
            Database.UserFlags.Remove(flag);
            Database.SaveChanges();

            // Remove from Cache
            _cache.Remove(UserFlagId);

            Status.Finished(string.Format("Successfully Deleted User Flag: '{0}' [{1}]", flag.Name, flag.Id));
        }
Ejemplo n.º 8
0
        public static IEnumerable<UserFlagAssignment> BulkAssignOverrideUsers(DiscoDataContext Database, UserFlag UserFlag, User Technician, string Comments, List<User> Users, IScheduledTaskStatus Status)
        {
            double progressInterval;
            const int databaseChunkSize = 100;
            string comments = string.IsNullOrWhiteSpace(Comments) ? null : Comments.Trim();

            Status.UpdateStatus(0, "Calculating assignment changes");

            var currentAssignments = Database.UserFlagAssignments.Include("User").Where(a => a.UserFlagId == UserFlag.Id && !a.RemovedDate.HasValue).ToList();
            var removeAssignments = currentAssignments.Where(ca => !Users.Any(u => u.UserId.Equals(ca.UserId, StringComparison.OrdinalIgnoreCase))).ToList();
            var addUsers = Users.Where(u => !currentAssignments.Any(ca => ca.UserId.Equals(u.UserId, StringComparison.OrdinalIgnoreCase))).ToList();

            if (removeAssignments.Count > 0 || addUsers.Count > 0)
            {
                progressInterval = (double)100 / (removeAssignments.Count + addUsers.Count);
                var removedDateTime = DateTime.Now;

                // Remove Assignments
                removeAssignments.Chunk(databaseChunkSize).SelectMany((chunk, chunkIndex) =>
                {
                    var chunkIndexOffset = (chunkIndex * databaseChunkSize) + removeAssignments.Count;

                    var chunkResults = chunk.Select((flagAssignment, index) =>
                    {
                        Status.UpdateStatus((chunkIndexOffset + index) * progressInterval, string.Format("Removing Flag: {0}", flagAssignment.User.ToString()));

                        flagAssignment.OnRemoveUnsafe(Database, Technician);
                        
                        return flagAssignment;
                    }).ToList();

                    // Save Chunk Items to Database
                    Database.SaveChanges();

                    return chunkResults;
                }).ToList();

                // Add Assignments
                var addedUserAssignments = addUsers.Chunk(databaseChunkSize).SelectMany((chunk, chunkIndex) =>
                {
                    var chunkIndexOffset = (chunkIndex * databaseChunkSize) + removeAssignments.Count;

                    var chunkResults = chunk.Select((user, index) =>
                    {
                        Status.UpdateStatus((chunkIndexOffset + index) * progressInterval, string.Format("Assigning Flag: {0}", user.ToString()));

                        return user.OnAddUserFlag(Database, UserFlag, Technician, comments);
                    }).ToList();

                    // Save Chunk Items to Database
                    Database.SaveChanges();

                    return chunkResults;
                }).ToList();

                Status.SetFinishedMessage(string.Format("{0} Users/s Added; {1} User/s Removed; {2} User/s Skipped", addUsers.Count, removeAssignments.Count, (Users.Count - addUsers.Count)));

                return addedUserAssignments;
            }
            else
            {
                Status.SetFinishedMessage("No changes found");
                return Enumerable.Empty<UserFlagAssignment>();
            }
        }
Ejemplo n.º 9
0
        public static IEnumerable<UserFlagAssignment> BulkAssignAddUsers(DiscoDataContext Database, UserFlag UserFlag, User Technician, string Comments, List<User> Users, IScheduledTaskStatus Status)
        {
            if (Users.Count > 0)
            {
                double progressInterval;
                const int databaseChunkSize = 100;
                string comments = string.IsNullOrWhiteSpace(Comments) ? null : Comments.Trim();

                var addUsers = Users.Where(u => !u.UserFlagAssignments.Any(a => a.UserFlagId == UserFlag.Id && !a.RemovedDate.HasValue)).ToList();

                progressInterval = (double)100 / addUsers.Count;

                var addedUserAssignments = addUsers.Chunk(databaseChunkSize).SelectMany((chunk, chunkIndex) =>
                {
                    var chunkIndexOffset = databaseChunkSize * chunkIndex;

                    var chunkResults = chunk.Select((user, index) =>
                    {
                        Status.UpdateStatus((chunkIndexOffset + index) * progressInterval, string.Format("Assigning Flag: {0}", user.ToString()));

                        return user.OnAddUserFlag(Database, UserFlag, Technician, comments);
                    }).ToList();

                    // Save Chunk Items to Database
                    Database.SaveChanges();

                    return chunkResults;
                }).Where(fa => fa != null).ToList();

                Status.SetFinishedMessage(string.Format("{0} Users/s Added; {1} User/s Skipped", addUsers.Count, (Users.Count - addUsers.Count)));

                return addedUserAssignments;
            }
            else
            {
                Status.SetFinishedMessage("No changes found");
                return Enumerable.Empty<UserFlagAssignment>();
            }
        }
Ejemplo n.º 10
0
        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();

            if (metadata.Count == 0)
                throw new ArgumentException("At least one export field must be specified", "Options");

            // Update Users
            if (Options.AssignedUserDisplayName ||
                Options.AssignedUserSurname ||
                Options.AssignedUserGivenName ||
                Options.AssignedUserPhoneNumber ||
                Options.AssignedUserEmailAddress)
            {
                TaskStatus.UpdateStatus(20, "Updating Assigned User details");
                var users = Devices.Where(d => d.AssignedUserId != null).Select(d => d.AssignedUserId).Distinct().ToList();

                users.Select((userId, index) =>
                {
                    TaskStatus.UpdateStatus(20 + (((double)20 / users.Count) * index), string.Format("Updating Assigned User details: {0}", userId));
                    try
                    {
                        return UserService.GetUser(userId, Database);
                    }
                    catch (Exception) { return null; } // Ignore Errors
                }).ToList();
            }

            // Update Last Network Logon Date
            if (Options.DeviceLastNetworkLogon)
            {
                TaskStatus.UpdateStatus(40, "Updating device last network logon dates");
                try
                {
                    TaskStatus.IgnoreCurrentProcessChanges = true;
                    TaskStatus.ProgressMultiplier = 20 / 100;
                    TaskStatus.ProgressOffset = 40;

                    Interop.ActiveDirectory.ADNetworkLogonDatesUpdateTask.UpdateLastNetworkLogonDates(Database, TaskStatus);
                    Database.SaveChanges();

                    TaskStatus.IgnoreCurrentProcessChanges = false;
                    TaskStatus.ProgressMultiplier = 1;
                    TaskStatus.ProgressOffset = 0;
                }
                catch (Exception) { } // Ignore Errors
            }

            TaskStatus.UpdateStatus(60, "Extracting records from the database");

            var records = BuildRecords(Devices).ToList();

            var stream = new MemoryStream();

            TaskStatus.UpdateStatus(80, string.Format("Formatting {0} records for export", records.Count));

            using (StreamWriter writer = new StreamWriter(stream, Encoding.Default, 0x400, true))
            {
                // Header
                writer.Write('"');
                writer.Write(string.Join("\",\"", metadata.Select(m => m.Item2)));
                writer.Write('"');

                // Records
                foreach (var record in records)
                {
                    writer.WriteLine();
                    writer.Write(string.Join(",", metadata.Select(m =>
                    {
                        var value = m.Item3(record);
                        var isString = m.Item4;

                        if (value == null)
                            return null;
                        else if (!isString)
                            return value;
                        else if (Options.ExcelCsvFormat)
                            return string.Concat("=\"", value, "\"");
                        else
                            return string.Concat("\"", value, "\"");
                    })));
                }
            }

            stream.Position = 0;
            return new DeviceExportResult()
            {
                CsvResult = stream,
                RecordCount = records.Count
            };
        }
Ejemplo n.º 11
0
 public static DeviceExportResult GenerateExport(DiscoDataContext Database, DeviceExportOptions Options, IScheduledTaskStatus TaskStatus)
 {
     switch (Options.ExportType)
     {
         case DeviceExportTypes.All:
             return GenerateExport(Database, Database.Devices, Options, TaskStatus);
         case DeviceExportTypes.Batch:
             if (Options.ExportTypeTargetId.HasValue && Options.ExportTypeTargetId.Value > 0)
                 return GenerateExport(Database, Database.Devices.Where(d => d.DeviceBatchId == Options.ExportTypeTargetId), Options, TaskStatus);
             else
                 return GenerateExport(Database, Database.Devices.Where(d => d.DeviceBatchId == null), Options, TaskStatus);
         case DeviceExportTypes.Model:
             return GenerateExport(Database, Database.Devices.Where(d => d.DeviceModelId == Options.ExportTypeTargetId), Options, TaskStatus);
         case DeviceExportTypes.Profile:
             return GenerateExport(Database, Database.Devices.Where(d => d.DeviceProfileId == Options.ExportTypeTargetId), Options, TaskStatus);
         default:
             throw new ArgumentException(string.Format("Unknown Device Export Type", Options.ExportType.ToString()), "Options");
     }
 }