Free cookie consent management tool by TermsFeed Policy Generator

Ignore:
Timestamp:
12/15/18 12:07:16 (6 years ago)
Author:
gkronber
Message:

#2925 merged changes r15972:16382 from trunk to branch

Location:
branches/2925_AutoDiffForDynamicalModels
Files:
3 edited

Legend:

Unmodified
Added
Removed
  • branches/2925_AutoDiffForDynamicalModels

  • branches/2925_AutoDiffForDynamicalModels/HeuristicLab.Clients.Hive

  • branches/2925_AutoDiffForDynamicalModels/HeuristicLab.Clients.Hive/3.3/HiveAdminClient.cs

    r15583 r16386  
    2424using HeuristicLab.Common;
    2525using HeuristicLab.Core;
     26using System.Collections.Generic;
     27using System.Linq;
     28using HeuristicLab.Clients.Access;
    2629
    2730namespace HeuristicLab.Clients.Hive {
     
    3639    }
    3740
     41    #region Properties
    3842    private IItemList<Resource> resources;
    3943    public IItemList<Resource> Resources {
     
    5761    }
    5862
     63    private IItemList<Project> projects;
     64    public IItemList<Project> Projects {
     65      get { return projects; }
     66    }
     67
     68    private IItemList<AssignedProjectResource> projectResourceAssignments;
     69    public IItemList<AssignedProjectResource> ProjectResourceAssignments {
     70      get { return projectResourceAssignments; }
     71    }
     72
     73    private Dictionary<Guid, HiveItemCollection<RefreshableJob>> jobs;
     74    public Dictionary<Guid, HiveItemCollection<RefreshableJob>> Jobs {
     75      get { return jobs; }
     76      set {
     77        if (value != jobs)
     78          jobs = value;
     79        }
     80    }
     81
     82    private Dictionary<Guid, List<LightweightTask>> tasks;
     83    public Dictionary<Guid, List<LightweightTask>> Tasks {
     84      get { return tasks; }
     85    }
     86
     87    private Dictionary<Guid, HashSet<Guid>> projectAncestors;
     88    public Dictionary<Guid, HashSet<Guid>> ProjectAncestors {
     89      get { return projectAncestors; }
     90    }
     91
     92    private Dictionary<Guid, HashSet<Guid>> projectDescendants;
     93    public Dictionary<Guid, HashSet<Guid>> ProjectDescendants {
     94      get { return projectDescendants; }
     95    }
     96
     97    private Dictionary<Guid, HashSet<Guid>> resourceAncestors;
     98    public Dictionary<Guid, HashSet<Guid>> ResourceAncestors {
     99      get { return resourceAncestors; }
     100    }
     101
     102    private Dictionary<Guid, HashSet<Guid>> resourceDescendants;
     103    public Dictionary<Guid, HashSet<Guid>> ResourceDescendants {
     104      get { return resourceDescendants; }
     105    }
     106
     107    private Dictionary<Guid, string> projectNames;
     108    public Dictionary<Guid, string> ProjectNames {
     109      get { return projectNames; }
     110    }
     111
     112    private HashSet<Project> disabledParentProjects;
     113    public HashSet<Project> DisabledParentProjects {
     114      get { return disabledParentProjects; }
     115    }
     116
     117    private Dictionary<Guid, string> resourceNames;
     118    public Dictionary<Guid, string> ResourceNames {
     119      get { return resourceNames; }
     120    }
     121
     122    private HashSet<Resource> disabledParentResources;
     123    public HashSet<Resource> DisabledParentResources {
     124      get { return disabledParentResources; }
     125    }
     126    #endregion
     127
    59128    #region Events
    60129    public event EventHandler Refreshing;
     
    78147      try {
    79148        resources = new ItemList<Resource>();
     149        projects = new ItemList<Project>();
     150        projectResourceAssignments = new ItemList<AssignedProjectResource>();
     151        jobs = new Dictionary<Guid, HiveItemCollection<RefreshableJob>>();
     152        tasks = new Dictionary<Guid, List<LightweightTask>>();
     153        projectNames = new Dictionary<Guid, string>();
     154        resourceNames = new Dictionary<Guid, string>();
     155
     156        projectAncestors = new Dictionary<Guid, HashSet<Guid>>();
     157        projectDescendants = new Dictionary<Guid, HashSet<Guid>>();
     158        resourceAncestors = new Dictionary<Guid, HashSet<Guid>>();
     159        resourceDescendants = new Dictionary<Guid, HashSet<Guid>>();
    80160
    81161        HiveServiceLocator.Instance.CallHiveService(service => {
    82           service.GetSlaveGroups().ForEach(g => resources.Add(g));
    83           service.GetSlaves().ForEach(s => resources.Add(s));
     162          service.GetSlaveGroupsForAdministration().ForEach(g => resources.Add(g));
     163          service.GetSlavesForAdministration().ForEach(s => resources.Add(s));
     164          service.GetProjectsForAdministration().ForEach(p => projects.Add(p));
     165          var projectIds = projects.Select(p => p.Id).ToList();
     166          if (projectIds.Any()) {
     167            service.GetAssignedResourcesForProjectsAdministration(projectIds)
     168              .ForEach(a => projectResourceAssignments.Add(a));
     169            projectIds.ForEach(p => jobs.Add(p, new HiveItemCollection<RefreshableJob>()));
     170            var unsortedJobs = service.GetJobsByProjectIds(projectIds)
     171              .OrderBy(x => x.DateCreated).ToList();
     172
     173            unsortedJobs.Where(j => j.State == JobState.DeletionPending).ToList().ForEach(j => jobs[j.ProjectId].Add(new RefreshableJob(j)));
     174            unsortedJobs.Where(j => j.State == JobState.StatisticsPending).ToList().ForEach(j => jobs[j.ProjectId].Add(new RefreshableJob(j)));
     175            unsortedJobs.Where(j => j.State == JobState.Online).ToList().ForEach(j => jobs[j.ProjectId].Add(new RefreshableJob(j)));
     176
     177            foreach (var job in jobs.SelectMany(x => x.Value))
     178              LoadLightweightJob(job);
     179
     180            projectNames = service.GetProjectNames();
     181            resourceNames = service.GetResourceNames();
     182          }
    84183        });
     184
     185        UpdateResourceGenealogy();
     186        UpdateProjectGenealogy();
     187        RefreshDisabledParentProjects();
     188        RefreshDisabledParentResources();
    85189      }
    86190      catch {
     
    91195      }
    92196    }
     197
     198    //public void UpdateResourceGenealogy(IItemList<Resource> resources) {
     199    //  resourceAncestors.Clear();
     200    //  resourceDescendants.Clear();
     201
     202    //  foreach (var r in resources) {
     203    //    resourceAncestors.Add(r.Id, new HashSet<Resource>());
     204    //    resourceDescendants.Add(r.Id, new HashSet<Resource>());
     205    //  }
     206
     207    //  foreach (var r in resources) {
     208    //    var parentResourceId = r.ParentResourceId;
     209    //    while (parentResourceId != null) {
     210    //      var parent = resources.SingleOrDefault(x => x.Id == parentResourceId);
     211    //      if (parent != null) {
     212    //        resourceAncestors[r.Id].Add(parent);
     213    //        resourceDescendants[parent.Id].Add(r);
     214    //        parentResourceId = parent.ParentResourceId;
     215    //      } else {
     216    //        parentResourceId = null;
     217    //      }
     218    //    }
     219    //  }
     220    //}
     221
     222    //public void UpdateProjectGenealogy(IItemList<Project> projects) {
     223    //  projectAncestors.Clear();
     224    //  projectDescendants.Clear();
     225
     226    //  foreach (var p in projects) {
     227    //    projectAncestors.Add(p.Id, new HashSet<Project>());
     228    //    projectDescendants.Add(p.Id, new HashSet<Project>());
     229    //  }
     230
     231    //  foreach (var p in projects) {
     232    //    var parentProjectId = p.ParentProjectId;
     233    //    while (parentProjectId != null) {
     234    //      var parent = projects.SingleOrDefault(x => x.Id == parentProjectId);
     235    //      if (parent != null) {
     236    //        projectAncestors[p.Id].Add(parent);
     237    //        projectDescendants[parent.Id].Add(p);
     238    //        parentProjectId = parent.ParentProjectId;
     239    //      } else {
     240    //        parentProjectId = null;
     241    //      }
     242    //    }
     243    //  }
     244    //}
     245
     246    private void UpdateResourceGenealogy() {
     247      resourceAncestors.Clear();
     248      resourceDescendants.Clear();
     249
     250      // fetch resource ancestor set
     251      HiveServiceLocator.Instance.CallHiveService(service => {
     252        var ra = service.GetResourceGenealogy();
     253        ra.Keys.ToList().ForEach(k => resourceAncestors.Add(k, new HashSet<Guid>()));
     254        resourceAncestors.Keys.ToList().ForEach(k => resourceAncestors[k].UnionWith(ra[k]));
     255      });
     256
     257      // build resource descendant set
     258      resourceAncestors.Keys.ToList().ForEach(k => resourceDescendants.Add(k, new HashSet<Guid>()));
     259      foreach (var ra in resourceAncestors) {
     260        foreach (var ancestor in ra.Value) {
     261          resourceDescendants[ancestor].Add(ra.Key);
     262        }
     263      }
     264    }
     265
     266    private void UpdateProjectGenealogy() {
     267      projectAncestors.Clear();
     268      projectDescendants.Clear();
     269
     270      // fetch project ancestor list
     271      HiveServiceLocator.Instance.CallHiveService(service => {
     272        var pa = service.GetProjectGenealogy();
     273        pa.Keys.ToList().ForEach(k => projectAncestors.Add(k, new HashSet<Guid>()));
     274        projectAncestors.Keys.ToList().ForEach(k => projectAncestors[k].UnionWith(pa[k]));
     275      });
     276
     277      // build project descendant list
     278      projectAncestors.Keys.ToList().ForEach(k => projectDescendants.Add(k, new HashSet<Guid>()));
     279      foreach (var pa in projectAncestors) {
     280        foreach (var ancestor in pa.Value) {
     281          projectDescendants[ancestor].Add(pa.Key);
     282        }
     283      }
     284    }
     285
     286    private void RefreshDisabledParentProjects() {
     287      disabledParentProjects = new HashSet<Project>();
     288
     289      foreach (var pid in projects
     290        .Where(x => x.ParentProjectId.HasValue)
     291        .SelectMany(x => projectAncestors[x.Id]).Distinct()
     292        .Where(x => !projects.Select(y => y.Id).Contains(x))) {
     293        var p = new Project();
     294        p.Id = pid;
     295        p.ParentProjectId = projectAncestors[pid].FirstOrDefault();
     296        p.Name = projectNames[pid];
     297        disabledParentProjects.Add(p);
     298      }
     299    }
     300
     301    private void RefreshDisabledParentResources() {
     302      disabledParentResources = new HashSet<Resource>();
     303
     304      foreach (var rid in resources
     305        .Where(x => x.ParentResourceId.HasValue)
     306        .SelectMany(x => resourceAncestors[x.Id]).Distinct()
     307        .Where(x => !resources.Select(y => y.Id).Contains(x))) {
     308        var r = new SlaveGroup();
     309        r.Id = rid;
     310        r.ParentResourceId = resourceAncestors[rid].FirstOrDefault();
     311        r.Name = resourceNames[rid];
     312        disabledParentResources.Add(r);
     313      }
     314    }
     315
     316    public void RefreshJobs() {
     317      var projectIds = new List<Guid>();
     318      jobs = new Dictionary<Guid, HiveItemCollection<RefreshableJob>>();
     319      tasks = new Dictionary<Guid, List<LightweightTask>>();
     320
     321      HiveServiceLocator.Instance.CallHiveService(service => {
     322        service.GetProjectsForAdministration().ForEach(p => projectIds.Add(p.Id));
     323        if(projectIds.Any()) {
     324          projectIds.ForEach(p => jobs.Add(p, new HiveItemCollection<RefreshableJob>()));
     325          var unsortedJobs = service.GetJobsByProjectIds(projectIds)
     326            .OrderBy(x => x.DateCreated).ToList();
     327         
     328          unsortedJobs.Where(j => j.State == JobState.DeletionPending).ToList().ForEach(j => jobs[j.ProjectId].Add(new RefreshableJob(j)));
     329          unsortedJobs.Where(j => j.State == JobState.StatisticsPending).ToList().ForEach(j => jobs[j.ProjectId].Add(new RefreshableJob(j)));
     330          unsortedJobs.Where(j => j.State == JobState.Online).ToList().ForEach(j => jobs[j.ProjectId].Add(new RefreshableJob(j)));
     331
     332          foreach(var job in jobs.SelectMany(x => x.Value))
     333            LoadLightweightJob(job);
     334        }
     335      });
     336    }
     337
     338    public void LoadLightweightJob(RefreshableJob refreshableJob) {
     339      var job = refreshableJob.Job;
     340      var lightweightTasks = HiveServiceLocator.Instance.CallHiveService(s => s.GetLightweightJobTasksWithoutStateLog(job.Id));
     341
     342      if (tasks.ContainsKey(job.Id)) {
     343        tasks[job.Id].Clear();
     344        tasks[job.Id].AddRange(lightweightTasks);
     345      } else {
     346        tasks.Add(job.Id, new List<LightweightTask>(lightweightTasks));       
     347      }
     348
     349      if (lightweightTasks != null && lightweightTasks.Count > 0 && lightweightTasks.All(x => x.Id != Guid.Empty)) {
     350        if (lightweightTasks.All(x =>
     351          x.State == TaskState.Finished
     352          || x.State == TaskState.Aborted
     353          || x.State == TaskState.Failed)) {
     354          refreshableJob.ExecutionState = ExecutionState.Stopped;
     355          refreshableJob.RefreshAutomatically = false;
     356        } else if (
     357          lightweightTasks
     358            .Where(x => x.ParentTaskId != null)
     359            .All(x =>
     360              x.State != TaskState.Waiting
     361              || x.State != TaskState.Transferring
     362              || x.State != TaskState.Calculating)
     363          && lightweightTasks
     364             .Where(x => x.ParentTaskId != null)
     365             .Any(x => x.State == TaskState.Paused)) {
     366          refreshableJob.ExecutionState = ExecutionState.Paused;
     367          refreshableJob.RefreshAutomatically = false;
     368        } else if (lightweightTasks.Any(x => x.State == TaskState.Calculating
     369                                  || x.State == TaskState.Transferring
     370                                  || x.State == TaskState.Waiting)) {
     371          refreshableJob.ExecutionState = ExecutionState.Started;
     372        }
     373
     374        refreshableJob.ExecutionTime = TimeSpan.FromMilliseconds(lightweightTasks.Sum(x => x.ExecutionTime.TotalMilliseconds));
     375      }
     376    }
     377
     378    public void SortJobs() {
     379      for(int i = 0; i < jobs.Count; i++) {
     380        var projectId = jobs.Keys.ElementAt(i);
     381        var unsortedJobs = jobs.Values.ElementAt(i);
     382
     383        var sortedJobs = new HiveItemCollection<RefreshableJob>();
     384        sortedJobs.AddRange(unsortedJobs.Where(j => j.Job.State == JobState.DeletionPending));
     385        sortedJobs.AddRange(unsortedJobs.Where(j => j.Job.State == JobState.StatisticsPending));
     386        sortedJobs.AddRange(unsortedJobs.Where(j => j.Job.State == JobState.Online));
     387
     388        jobs[projectId] = sortedJobs;
     389      }
     390    }
     391
    93392    #endregion
    94393
     
    127426          item.Id = HiveServiceLocator.Instance.CallHiveService((s) => s.AddDowntime((Downtime)item));
    128427        }
     428        if (item is Project) {
     429          item.Id = HiveServiceLocator.Instance.CallHiveService(s => s.AddProject((Project)item));
     430        }
    129431      } else {
    130432        if (item is SlaveGroup) {
     
    136438        if (item is Downtime) {
    137439          HiveServiceLocator.Instance.CallHiveService((s) => s.UpdateDowntime((Downtime)item));
     440        }
     441        if (item is Project) {
     442          HiveServiceLocator.Instance.CallHiveService((s) => s.UpdateProject((Project)item));
    138443        }
    139444      }
     
    149454      } else if (item is Downtime) {
    150455        HiveServiceLocator.Instance.CallHiveService((s) => s.DeleteDowntime(item.Id));
    151       }
     456      } else if (item is Project) {
     457        HiveServiceLocator.Instance.CallHiveService((s) => s.DeleteProject(item.Id));
     458      }
     459    }
     460
     461    public static void RemoveJobs(List<Guid> jobIds) {
     462      HiveServiceLocator.Instance.CallHiveService((s) => s.UpdateJobStates(jobIds, JobState.StatisticsPending));
     463    }
     464    #endregion
     465
     466    #region Job Handling
     467
     468    public static void ResumeJob(RefreshableJob refreshableJob) {
     469      HiveServiceLocator.Instance.CallHiveService(service => {
     470        var tasks = service.GetLightweightJobTasksWithoutStateLog(refreshableJob.Id);
     471        foreach (var task in tasks) {
     472          if (task.State == TaskState.Paused) {
     473            service.RestartTask(task.Id);
     474          }
     475        }
     476      });
     477      refreshableJob.ExecutionState = ExecutionState.Started;
     478    }
     479
     480    public static void PauseJob(RefreshableJob refreshableJob) {
     481      HiveServiceLocator.Instance.CallHiveService(service => {
     482        var tasks = service.GetLightweightJobTasksWithoutStateLog(refreshableJob.Id);
     483        foreach (var task in tasks) {
     484          if (task.State != TaskState.Finished && task.State != TaskState.Aborted && task.State != TaskState.Failed)
     485            service.PauseTask(task.Id);
     486        }
     487      });
     488      refreshableJob.ExecutionState = ExecutionState.Paused;
     489    }
     490
     491    public static void StopJob(RefreshableJob refreshableJob) {
     492      HiveServiceLocator.Instance.CallHiveService(service => {
     493        var tasks = service.GetLightweightJobTasksWithoutStateLog(refreshableJob.Id);
     494        foreach (var task in tasks) {
     495          if (task.State != TaskState.Finished && task.State != TaskState.Aborted && task.State != TaskState.Failed)
     496            service.StopTask(task.Id);
     497        }
     498      });
     499      refreshableJob.ExecutionState = ExecutionState.Stopped;
     500    }
     501
     502    public static void RemoveJob(RefreshableJob refreshableJob) {
     503      HiveServiceLocator.Instance.CallHiveService((service) => {
     504        service.UpdateJobState(refreshableJob.Id, JobState.StatisticsPending);
     505      });
    152506    }
    153507    #endregion
     
    159513      }
    160514    }
     515
     516    #region Helper
     517    public IEnumerable<Project> GetAvailableProjectAncestors(Guid id) {
     518      if (projectAncestors.ContainsKey(id)) return projects.Where(x => projectAncestors[id].Contains(x.Id));
     519      else return Enumerable.Empty<Project>();
     520    }
     521
     522    public IEnumerable<Project> GetAvailableProjectDescendants(Guid id) {
     523      if(projectDescendants.ContainsKey(id)) return projects.Where(x => projectDescendants[id].Contains(x.Id));
     524      else return Enumerable.Empty<Project>();
     525    }
     526
     527    public IEnumerable<Resource> GetAvailableResourceAncestors(Guid id) {
     528      if (resourceAncestors.ContainsKey(id)) return resources.Where(x => resourceAncestors[id].Contains(x.Id));
     529      else return Enumerable.Empty<Resource>();
     530    }
     531
     532    public IEnumerable<Resource> GetAvailableResourceDescendants(Guid id) {
     533      if (resourceDescendants.ContainsKey(id)) return resources.Where(x => resourceDescendants[id].Contains(x.Id));
     534      else return Enumerable.Empty<Resource>();
     535    }
     536
     537    public IEnumerable<Resource> GetDisabledResourceAncestors(IEnumerable<Resource> availableResources) {
     538      var missingParentIds = availableResources
     539        .Where(x => x.ParentResourceId.HasValue)
     540        .SelectMany(x => resourceAncestors[x.Id]).Distinct()
     541        .Where(x => !availableResources.Select(y => y.Id).Contains(x));
     542
     543      return resources.OfType<SlaveGroup>().Union(disabledParentResources).Where(x => missingParentIds.Contains(x.Id));
     544    }
     545
     546    public bool CheckAccessToAdminAreaGranted() {
     547      if(projects != null) {
     548        return projects.Count > 0;
     549      } else {
     550        bool accessGranted = false;
     551        HiveServiceLocator.Instance.CallHiveService(s => {
     552          accessGranted = s.CheckAccessToAdminAreaGranted();
     553        });
     554        return accessGranted;
     555      }
     556    }
     557
     558    public bool CheckOwnershipOfResource(Resource res, Guid userId) {
     559      if (res == null || userId == Guid.Empty) return false;
     560
     561      if (res.OwnerUserId == userId) {
     562        return true;
     563      } else if(resourceAncestors.ContainsKey(res.Id)) {
     564        return GetAvailableResourceAncestors(res.Id).Where(x => x.OwnerUserId == userId).Any();
     565      }
     566
     567      return false;
     568    }
     569
     570    public bool CheckOwnershipOfProject(Project pro, Guid userId) {
     571      if (pro == null || userId == Guid.Empty) return false;
     572
     573      if (pro.OwnerUserId == userId) {
     574        return true;
     575      } else if (projectAncestors.ContainsKey(pro.Id)) {
     576        return GetAvailableProjectAncestors(pro.Id).Where(x => x.OwnerUserId == userId).Any();
     577      }
     578
     579      return false;
     580    }
     581
     582    public bool CheckOwnershipOfParentProject(Project pro, Guid userId) {
     583      if (pro == null || userId == Guid.Empty) return false;
     584
     585      if(projectAncestors.ContainsKey(pro.Id)) {
     586        return GetAvailableProjectAncestors(pro.Id).Any(x => x.OwnerUserId == userId);
     587      }
     588
     589      if (pro.ParentProjectId != null && pro.ParentProjectId != Guid.Empty) {
     590        var parent = projects.FirstOrDefault(x => x.Id == pro.ParentProjectId.Value);
     591        if (parent != null)
     592          return parent.OwnerUserId == userId || GetAvailableProjectAncestors(parent.Id).Any(x => x.OwnerUserId == userId);
     593      }
     594
     595      return false;
     596    }
     597
     598    public bool CheckParentChange(Project child, Project parent) {
     599      bool changePossible = true;
     600
     601      // change is not possible...
     602      // ... if the moved project is null
     603      // ... or the new parent is not stored yet
     604      // ... or there is not parental change
     605      if (child == null
     606          || (parent != null && parent.Id == Guid.Empty)
     607          || (parent != null && parent.Id == child.ParentProjectId)) {
     608        changePossible = false;
     609      } else if (parent == null && !IsAdmin()) {
     610        // ... if parent is null, but user is no admin (only admins are allowed to create root projects)
     611        changePossible = false;
     612      } else if (parent != null && (!IsAdmin() && parent.OwnerUserId != UserInformation.Instance.User.Id && !CheckOwnershipOfParentProject(parent, UserInformation.Instance.User.Id))) {
     613        // ... if the user is no admin nor owner of the new parent or grand..grandparents
     614        changePossible = false;
     615      } else if(parent != null && projectDescendants.ContainsKey(child.Id)) {
     616        // ... if the new parent is among the moved project's descendants
     617        changePossible = !GetAvailableProjectDescendants(child.Id).Where(x => x.Id == parent.Id).Any();
     618      }
     619
     620      return changePossible;
     621    }
     622
     623    public bool CheckParentChange(Resource child, Resource parent) {
     624      bool changePossible = true;
     625
     626      // change is not possisble...
     627      // ... if the child resource is null
     628      // ... or the child resource equals the parent
     629      // ... or the new parent is not stored yet
     630      // ... or the new parent is a slave
     631      // ... or there is not parental change
     632      if (child == null
     633        || child == parent
     634        || (parent != null && parent.Id == Guid.Empty)
     635        || (parent != null && parent is Slave)
     636        || (parent != null && parent.Id == child.ParentResourceId)) {
     637        changePossible = false;
     638      } else if (parent != null && resourceDescendants.ContainsKey(child.Id)) {
     639        // ... or if the new parent is among the moved resource's descendants
     640        changePossible = !GetAvailableResourceDescendants(child.Id).Where(x => x.Id == parent.Id).Any();
     641      }
     642
     643      return changePossible;
     644    }
     645
     646    public IEnumerable<Resource> GetAssignedResourcesForJob(Guid jobId) {
     647      var assignedJobResource =  HiveServiceLocator.Instance.CallHiveService(service => service.GetAssignedResourcesForJob(jobId));
     648      return Resources.Where(x => assignedJobResource.Select(y => y.ResourceId).Contains(x.Id));
     649    }
     650
     651    private bool IsAdmin() {
     652      return HiveRoles.CheckAdminUserPermissions();
     653    }
     654    #endregion
    161655  }
    162656}
Note: See TracChangeset for help on using the changeset viewer.