#region License Information
/* HeuristicLab
* Copyright (C) 2002-2011 Heuristic and Evolutionary Algorithms Laboratory (HEAL)
*
* This file is part of HeuristicLab.
*
* HeuristicLab is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* HeuristicLab is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with HeuristicLab. If not, see .
*/
#endregion
using System;
using System.Collections.Generic;
using System.Linq;
using System.ServiceModel;
using HeuristicLab.Services.Hive.Common;
using HeuristicLab.Services.Hive.Common.DataTransfer;
using HeuristicLab.Services.Hive.Common.ServiceContracts;
namespace HeuristicLab.Services.Hive {
///
/// Implementation of the Hive service (interface ).
/// We need 'IgnoreExtensionDataObject' Attribute for the slave to work.
///
[ServiceBehavior(InstanceContextMode = InstanceContextMode.PerCall, IgnoreExtensionDataObject = true)]
public class HiveService : IHiveService {
private DataAccess.IHiveDao dao {
get { return ServiceLocator.Instance.HiveDao; }
}
private IAuthenticationManager authen {
get { return ServiceLocator.Instance.AuthenticationManager; }
}
private IAuthorizationManager author {
get { return ServiceLocator.Instance.AuthorizationManager; }
}
private ITransactionManager trans {
get { return ServiceLocator.Instance.TransactionManager; }
}
private ILifecycleManager lifecycleManager {
get { return ServiceLocator.Instance.LifecycleManager; }
}
private IUserManager userManager {
get { return ServiceLocator.Instance.UserManager; }
}
private HeartbeatManager heartbeatManager {
get { return ServiceLocator.Instance.HeartbeatManager; }
}
#region Job Methods
public Guid AddJob(Job job, JobData jobData, IEnumerable resourceIds) {
authen.AuthenticateForAnyRole(HiveRoles.Administrator, HiveRoles.Client);
return trans.UseTransaction(() => {
job.Id = dao.AddJob(job);
jobData.JobId = job.Id;
jobData.LastUpdate = DateTime.Now;
foreach (Guid slaveGroupId in resourceIds) {
dao.AssignJobToResource(job.Id, slaveGroupId);
}
dao.AddJobData(jobData);
dao.UpdateJobState(job.Id, JobState.Waiting, null, userManager.CurrentUserId, null);
return jobData.JobId;
}, false, true);
}
public Guid AddChildJob(Guid parentJobId, Job job, JobData jobData) {
authen.AuthenticateForAnyRole(HiveRoles.Administrator, HiveRoles.Client);
return trans.UseTransaction(() => {
job.ParentJobId = parentJobId;
return AddJob(job, jobData, dao.GetAssignedResources(parentJobId).Select(x => x.Id));
}, false, true);
}
public Job GetJob(Guid jobId) {
authen.AuthenticateForAnyRole(HiveRoles.Administrator, HiveRoles.Client, HiveRoles.Slave);
author.AuthorizeForJob(jobId, Permission.Read);
return dao.GetJob(jobId);
}
public IEnumerable GetJobs() {
authen.AuthenticateForAnyRole(HiveRoles.Administrator, HiveRoles.Client);
var jobs = dao.GetJobs(x => true);
foreach (var job in jobs)
author.AuthorizeForJob(job.Id, Permission.Read);
return jobs;
}
public IEnumerable GetLightweightJobs(IEnumerable jobIds) {
authen.AuthenticateForAnyRole(HiveRoles.Administrator, HiveRoles.Client);
var jobs = dao.GetJobs(x => jobIds.Contains(x.JobId)).Select(x => new LightweightJob(x)).ToArray();
foreach (var job in jobs)
author.AuthorizeForJob(job.Id, Permission.Read);
return jobs;
}
public IEnumerable GetLightweightChildJobs(Guid? parentJobId, bool recursive, bool includeParent) {
authen.AuthenticateForAnyRole(HiveRoles.Administrator, HiveRoles.Client);
var jobs = GetChildJobs(parentJobId, recursive, includeParent).Select(x => new LightweightJob(x)).ToArray();
foreach (var job in jobs)
author.AuthorizeForJob(job.Id, Permission.Read);
return jobs;
}
public IEnumerable GetLightweightExperimentJobs(Guid experimentId) {
authen.AuthenticateForAnyRole(HiveRoles.Administrator, HiveRoles.Client);
author.AuthorizeForExperiment(experimentId, Permission.Read);
return dao.GetJobs(x => x.HiveExperimentId == experimentId).Select(x => new LightweightJob(x)).ToArray();
}
public JobData GetJobData(Guid jobId) {
authen.AuthenticateForAnyRole(HiveRoles.Administrator, HiveRoles.Client, HiveRoles.Slave);
author.AuthorizeForJob(jobId, Permission.Read);
return dao.GetJobData(jobId);
}
public void UpdateJob(Job job) {
authen.AuthenticateForAnyRole(HiveRoles.Administrator, HiveRoles.Client, HiveRoles.Slave);
author.AuthorizeForJob(job.Id, Permission.Full);
trans.UseTransaction(() => {
dao.UpdateJob(job);
});
}
public void UpdateJobData(Job job, JobData jobData) {
authen.AuthenticateForAnyRole(HiveRoles.Administrator, HiveRoles.Client, HiveRoles.Slave);
author.AuthorizeForJob(job.Id, Permission.Full);
author.AuthorizeForJob(jobData.JobId, Permission.Full);
//trans.UseTransaction(() => { // cneumuel: try without transaction
jobData.LastUpdate = DateTime.Now;
dao.UpdateJob(job);
dao.UpdateJobData(jobData);
//}, false, true);
}
public void DeleteJob(Guid jobId) {
authen.AuthenticateForAnyRole(HiveRoles.Administrator, HiveRoles.Client, HiveRoles.Slave);
author.AuthorizeForJob(jobId, Permission.Full);
trans.UseTransaction(() => {
dao.DeleteJob(jobId);
});
}
public void DeleteChildJobs(Guid parentJobId) {
authen.AuthenticateForAnyRole(HiveRoles.Administrator, HiveRoles.Client, HiveRoles.Slave);
author.AuthorizeForJob(parentJobId, Permission.Full);
trans.UseTransaction(() => {
var jobs = GetChildJobs(parentJobId, true, false);
foreach (var job in jobs) {
dao.DeleteJob(job.Id);
dao.DeleteJobData(job.Id);
};
});
}
public Job UpdateJobState(Guid jobId, JobState jobState, Guid? slaveId, Guid? userId, string exception) {
authen.AuthenticateForAnyRole(HiveRoles.Administrator, HiveRoles.Client, HiveRoles.Slave);
author.AuthorizeForJob(jobId, Permission.Full);
return trans.UseTransaction(() => {
Job job = dao.UpdateJobState(jobId, jobState, slaveId, userId, exception);
if (job.Command.HasValue && job.Command.Value == Command.Pause && job.State == JobState.Paused) {
job.Command = null;
} else if (job.Command.HasValue && job.Command.Value == Command.Abort && job.State == JobState.Aborted) {
job.Command = null;
} else if (job.Command.HasValue && job.Command.Value == Command.Stop && job.State == JobState.Aborted) {
job.Command = null;
} else if (jobState == JobState.Paused && !job.Command.HasValue) {
// slave paused and uploaded the job (no user-command) -> set waiting.
job = dao.UpdateJobState(jobId, JobState.Waiting, slaveId, userId, exception);
}
dao.UpdateJob(job);
return job;
});
}
public IEnumerable GetJobsByResourceId(Guid resourceId) {
authen.AuthenticateForAnyRole(HiveRoles.Administrator);
var jobs = trans.UseTransaction(() => dao.GetJobsByResourceId(resourceId));
foreach(var job in jobs)
author.AuthorizeForJob(job.Id, Permission.Read);
return jobs;
}
#endregion
#region Job Control Methods
public void StopJob(Guid jobId) {
authen.AuthenticateForAnyRole(HiveRoles.Administrator, HiveRoles.Client, HiveRoles.Slave);
author.AuthorizeForJob(jobId, Permission.Full);
trans.UseTransaction(() => {
var job = dao.GetJob(jobId);
if (job.State == JobState.Calculating || job.State == JobState.Transferring) {
job.Command = Command.Stop;
dao.UpdateJob(job);
} else {
if (job.State != JobState.Aborted && job.State != JobState.Finished && job.State != JobState.Failed) {
job = UpdateJobState(jobId, JobState.Aborted, null, null, string.Empty);
}
}
});
}
public void PauseJob(Guid jobId) {
authen.AuthenticateForAnyRole(HiveRoles.Administrator, HiveRoles.Client, HiveRoles.Slave);
author.AuthorizeForJob(jobId, Permission.Full);
trans.UseTransaction(() => {
var job = dao.GetJob(jobId);
if (job.State == JobState.Calculating || job.State == JobState.Transferring) {
job.Command = Command.Pause;
dao.UpdateJob(job);
} else {
job = UpdateJobState(jobId, JobState.Paused, null, null, string.Empty);
}
});
}
public void RestartJob(Guid jobId) {
authen.AuthenticateForAnyRole(HiveRoles.Administrator, HiveRoles.Client, HiveRoles.Slave);
author.AuthorizeForJob(jobId, Permission.Full);
trans.UseTransaction(() => {
Job job = dao.UpdateJobState(jobId, JobState.Waiting, null, userManager.CurrentUserId, string.Empty);
job.Command = null;
dao.UpdateJob(job);
});
}
#endregion
#region HiveExperiment Methods
public HiveExperiment GetHiveExperiment(Guid id) {
authen.AuthenticateForAnyRole(HiveRoles.Administrator, HiveRoles.Client);
author.AuthorizeForExperiment(id, Permission.Read);
var hiveExperiment = dao.GetHiveExperiments(x =>
x.HiveExperimentId == id
&& (x.OwnerUserId == userManager.CurrentUserId || x.HiveExperimentPermissions.Count(hep => hep.Permission != Permission.NotAllowed && hep.GrantedUserId == userManager.CurrentUserId) > 0)
).FirstOrDefault();
if (hiveExperiment != null) {
hiveExperiment.Permission = dao.GetPermissionForExperiment(hiveExperiment.Id, userManager.CurrentUserId);
hiveExperiment.OwnerUsername = userManager.GetUserById(hiveExperiment.OwnerUserId).UserName;
}
return hiveExperiment;
}
public IEnumerable GetHiveExperiments() {
authen.AuthenticateForAnyRole(HiveRoles.Administrator, HiveRoles.Client);
var hiveExperiments = dao.GetHiveExperiments(x => x.OwnerUserId == userManager.CurrentUserId || x.HiveExperimentPermissions.Count(hep => hep.Permission != Permission.NotAllowed && hep.GrantedUserId == userManager.CurrentUserId) > 0);
foreach (var he in hiveExperiments) {
author.AuthorizeForExperiment(he.Id, Permission.Read);
he.Permission = dao.GetPermissionForExperiment(he.Id, userManager.CurrentUserId);
he.OwnerUsername = userManager.GetUserById(he.OwnerUserId).UserName;
}
return hiveExperiments;
}
public IEnumerable GetAllHiveExperiments() {
authen.AuthenticateForAnyRole(HiveRoles.Administrator);
var hiveExperiments = dao.GetHiveExperiments(x => true);
foreach (var he in hiveExperiments) { // no authorization here, since this method is admin-only! (admin is allowed to read all jobs)
he.Permission = dao.GetPermissionForExperiment(he.Id, userManager.CurrentUserId);
he.OwnerUsername = userManager.GetUserById(he.OwnerUserId).UserName;
}
return hiveExperiments;
}
public Guid AddHiveExperiment(HiveExperiment hiveExperimentDto) {
authen.AuthenticateForAnyRole(HiveRoles.Administrator, HiveRoles.Client);
return trans.UseTransaction(() => {
hiveExperimentDto.OwnerUserId = userManager.CurrentUserId;
hiveExperimentDto.DateCreated = DateTime.Now;
return dao.AddHiveExperiment(hiveExperimentDto);
});
}
public void UpdateHiveExperiment(HiveExperiment hiveExperimentDto) {
authen.AuthenticateForAnyRole(HiveRoles.Administrator, HiveRoles.Client);
author.AuthorizeForExperiment(hiveExperimentDto.Id, Permission.Full);
trans.UseTransaction(() => {
dao.UpdateHiveExperiment(hiveExperimentDto);
});
}
public void DeleteHiveExperiment(Guid hiveExperimentId) {
authen.AuthenticateForAnyRole(HiveRoles.Administrator, HiveRoles.Client);
author.AuthorizeForExperiment(hiveExperimentId, Permission.Full);
trans.UseTransaction(() => {
dao.DeleteHiveExperiment(hiveExperimentId); // child jobs will be deleted by db-trigger
});
}
#endregion
#region HiveExperimentPermission Methods
public void GrantPermission(Guid hiveExperimentId, Guid grantedUserId, Permission permission) {
authen.AuthenticateForAnyRole(HiveRoles.Administrator, HiveRoles.Client);
trans.UseTransaction(() => {
HiveExperiment he = dao.GetHiveExperiment(hiveExperimentId);
if (he == null) throw new FaultException(new FaultReason("Could not find hiveExperiment with id " + hiveExperimentId));
Permission perm = dao.GetPermissionForExperiment(he.Id, userManager.CurrentUserId);
if (perm != Permission.Full) throw new FaultException(new FaultReason("Not allowed to grant permissions for this experiment"));
dao.SetHiveExperimentPermission(hiveExperimentId, userManager.CurrentUserId, grantedUserId, permission);
});
}
public void RevokePermission(Guid hiveExperimentId, Guid grantedUserId) {
authen.AuthenticateForAnyRole(HiveRoles.Administrator, HiveRoles.Client);
trans.UseTransaction(() => {
HiveExperiment he = dao.GetHiveExperiment(hiveExperimentId);
if (he == null) throw new FaultException(new FaultReason("Could not find hiveExperiment with id " + hiveExperimentId));
Permission perm = dao.GetPermissionForExperiment(he.Id, userManager.CurrentUserId);
if (perm != Permission.Full) throw new FaultException(new FaultReason("Not allowed to grant permissions for this experiment"));
dao.SetHiveExperimentPermission(hiveExperimentId, userManager.CurrentUserId, grantedUserId, Permission.NotAllowed);
});
}
public IEnumerable GetHiveExperimentPermissions(Guid hiveExperimentId) {
authen.AuthenticateForAnyRole(HiveRoles.Administrator, HiveRoles.Client);
return trans.UseTransaction(() => {
Permission currentUserPermission = dao.GetPermissionForExperiment(hiveExperimentId, userManager.CurrentUserId);
if (currentUserPermission != Permission.Full) throw new FaultException(new FaultReason("Not allowed to list permissions for this experiment"));
return dao.GetHiveExperimentPermissions(x => x.HiveExperimentId == hiveExperimentId);
});
}
public bool IsAllowedPrivileged() {
authen.AuthenticateForAnyRole(HiveRoles.Administrator, HiveRoles.Client);
return authen.IsInRole(HiveRoles.IsAllowedPrivileged);
}
#endregion
#region Login Methods
public void Hello(Slave slaveInfo) {
authen.AuthenticateForAnyRole(HiveRoles.Slave);
trans.UseTransaction(() => {
var slave = dao.GetSlave(slaveInfo.Id);
if (slave == null) {
dao.AddSlave(slaveInfo);
} else {
var dbSlave = dao.GetSlave(slaveInfo.Id);
dbSlave.Name = slaveInfo.Name;
dbSlave.Description = slaveInfo.Description;
dbSlave.Cores = slaveInfo.Cores;
dbSlave.CpuArchitecture = slaveInfo.CpuArchitecture;
dbSlave.CpuSpeed = slaveInfo.CpuSpeed;
dbSlave.FreeCores = slaveInfo.FreeCores;
dbSlave.FreeMemory = slaveInfo.FreeMemory;
dbSlave.Memory = slaveInfo.Memory;
dbSlave.OperatingSystem = slaveInfo.OperatingSystem;
dbSlave.LastHeartbeat = DateTime.Now;
dbSlave.SlaveState = SlaveState.Idle;
// don't update those properties: dbSlave.IsAllowedToCalculate, dbSlave.ParentResourceId
dao.UpdateSlave(dbSlave);
}
});
}
public void GoodBye(Guid slaveId) {
authen.AuthenticateForAnyRole(HiveRoles.Slave);
trans.UseTransaction(() => {
var slave = dao.GetSlave(slaveId);
if (slave != null) {
slave.SlaveState = SlaveState.Offline;
dao.UpdateSlave(slave);
}
});
}
#endregion
#region Heartbeat Methods
public List Heartbeat(Heartbeat heartbeat) {
authen.AuthenticateForAnyRole(HiveRoles.Slave);
TriggerLifecycle(false);
return trans.UseTransaction(() => heartbeatManager.ProcessHeartbeat(heartbeat));
}
#endregion
#region Plugin Methods
public Guid AddPlugin(Plugin plugin, List pluginDatas) {
authen.AuthenticateForAnyRole(HiveRoles.Administrator, HiveRoles.Client);
return trans.UseTransaction(() => {
plugin.UserId = userManager.CurrentUserId;
plugin.DateCreated = DateTime.Now;
var existing = dao.GetPlugins(x => x.Hash != null).Where(x => x.Hash.SequenceEqual(plugin.Hash));
if (existing.Count() > 0) {
// a plugin already exists.
throw new FaultException(new PluginAlreadyExistsFault(existing.Single().Id));
}
Guid pluginId = dao.AddPlugin(plugin);
foreach (PluginData pluginData in pluginDatas) {
pluginData.PluginId = pluginId;
dao.AddPluginData(pluginData);
}
return pluginId;
});
}
public Plugin GetPlugin(Guid pluginId) {
authen.AuthenticateForAnyRole(HiveRoles.Administrator, HiveRoles.Client, HiveRoles.Slave);
return dao.GetPlugin(pluginId);
}
public Plugin GetPluginByHash(byte[] hash) {
authen.AuthenticateForAnyRole(HiveRoles.Administrator, HiveRoles.Client, HiveRoles.Slave);
return dao.GetPlugins(x => x.Hash == hash).FirstOrDefault();
}
// note: this is a possible security problem, since a client is able to download all plugins, which may contain proprietary code (which can be disassembled)
// change so that only with GetPluginByHash it is possible to download plugins
public IEnumerable GetPlugins() {
authen.AuthenticateForAnyRole(HiveRoles.Administrator, HiveRoles.Client, HiveRoles.Slave);
return dao.GetPlugins(x => x.Hash != null);
}
public IEnumerable GetPluginDatas(List pluginIds) {
authen.AuthenticateForAnyRole(HiveRoles.Administrator, HiveRoles.Client, HiveRoles.Slave);
var pluginDatas = new List();
return trans.UseTransaction(() => {
foreach (Guid guid in pluginIds) {
pluginDatas.AddRange(dao.GetPluginDatas(x => x.PluginId == guid).ToList());
}
return pluginDatas;
});
}
public void DeletePlugin(Guid pluginId) {
authen.AuthenticateForAnyRole(HiveRoles.Administrator, HiveRoles.Client, HiveRoles.Slave);
dao.DeletePlugin(pluginId);
}
#endregion
#region Slave Methods
public Guid AddSlave(Slave slave) {
authen.AuthenticateForAnyRole(HiveRoles.Administrator);
return trans.UseTransaction(() => dao.AddSlave(slave));
}
public Guid AddSlaveGroup(SlaveGroup slaveGroup) {
authen.AuthenticateForAnyRole(HiveRoles.Administrator);
return trans.UseTransaction(() => dao.AddSlaveGroup(slaveGroup));
}
public Slave GetSlave(Guid slaveId) {
authen.AuthenticateForAnyRole(HiveRoles.Administrator);
return dao.GetSlave(slaveId);
}
public SlaveGroup GetSlaveGroup(Guid slaveGroupId) {
authen.AuthenticateForAnyRole(HiveRoles.Administrator);
return dao.GetSlaveGroup(slaveGroupId);
}
public IEnumerable GetSlaves() {
authen.AuthenticateForAnyRole(HiveRoles.Administrator);
return dao.GetSlaves(x => true);
}
public IEnumerable GetSlaveGroups() {
authen.AuthenticateForAnyRole(HiveRoles.Administrator);
return dao.GetSlaveGroups(x => true);
}
public void UpdateSlave(Slave slave) {
authen.AuthenticateForAnyRole(HiveRoles.Administrator);
trans.UseTransaction(() => {
dao.UpdateSlave(slave);
});
}
public void UpdateSlaveGroup(SlaveGroup slaveGroup) {
authen.AuthenticateForAnyRole(HiveRoles.Administrator);
trans.UseTransaction(() => {
dao.UpdateSlaveGroup(slaveGroup);
});
}
public void DeleteSlave(Guid slaveId) {
authen.AuthenticateForAnyRole(HiveRoles.Administrator);
trans.UseTransaction(() => {
dao.DeleteSlave(slaveId);
});
}
public void DeleteSlaveGroup(Guid slaveGroupId) {
authen.AuthenticateForAnyRole(HiveRoles.Administrator);
trans.UseTransaction(() => {
dao.DeleteSlaveGroup(slaveGroupId);
});
}
public void AddResourceToGroup(Guid slaveGroupId, Guid resourceId) {
authen.AuthenticateForAnyRole(HiveRoles.Administrator);
trans.UseTransaction(() => {
var resource = dao.GetResource(resourceId);
resource.ParentResourceId = slaveGroupId;
dao.UpdateResource(resource);
});
}
public void RemoveResourceFromGroup(Guid slaveGroupId, Guid resourceId) {
authen.AuthenticateForAnyRole(HiveRoles.Administrator);
trans.UseTransaction(() => {
var resource = dao.GetResource(resourceId);
resource.ParentResourceId = null;
dao.UpdateResource(resource);
});
}
public Guid GetResourceId(string resourceName) {
authen.AuthenticateForAnyRole(HiveRoles.Administrator, HiveRoles.Client);
return trans.UseTransaction(() => {
var resource = dao.GetResources(x => x.Name == resourceName).FirstOrDefault();
if (resource != null) {
return resource.Id;
} else {
return Guid.Empty;
}
});
}
public void TriggerLifecycle(bool force) {
authen.AuthenticateForAnyRole(HiveRoles.Administrator, HiveRoles.Slave);
// use a serializable transaction here to ensure not two threads execute this simultaniously (mutex-lock would not work since IIS may use multiple AppDomains)
trans.UseTransaction(() => {
DateTime lastCleanup = dao.GetLastCleanup();
if (force || DateTime.Now - lastCleanup > TimeSpan.FromSeconds(59)) {
dao.SetLastCleanup(DateTime.Now);
lifecycleManager.Cleanup();
}
}, true);
}
#endregion
#region Downtime Methods
public Guid AddDowntime(Downtime downtime) {
authen.AuthenticateForAnyRole(HiveRoles.Administrator);
return trans.UseTransaction(() => dao.AddDowntime(downtime));
}
public void DeleteDowntime(Guid downtimeId) {
authen.AuthenticateForAnyRole(HiveRoles.Administrator);
trans.UseTransaction(() => {
dao.DeleteDowntime(downtimeId);
});
}
public void UpdateDowntime(Downtime downtime) {
authen.AuthenticateForAnyRole(HiveRoles.Administrator);
trans.UseTransaction(() => {
dao.UpdateDowntime(downtime);
});
}
public IEnumerable GetDowntimesForResource(Guid resourceId) {
authen.AuthenticateForAnyRole(HiveRoles.Administrator);
return trans.UseTransaction(() => dao.GetDowntimes(x => x.ResourceId == resourceId));
}
#endregion
#region User Methods
public string GetUsernameByUserId(Guid userId) {
authen.AuthenticateForAnyRole(HiveRoles.Administrator, HiveRoles.Client);
var user = ServiceLocator.Instance.UserManager.GetUserById(userId);
if (user != null)
return user.UserName;
else
return null;
}
public Guid GetUserIdByUsername(string username) {
authen.AuthenticateForAnyRole(HiveRoles.Administrator, HiveRoles.Client);
var user = ServiceLocator.Instance.UserManager.GetUserByName(username);
return user != null ? (Guid)user.ProviderUserKey : Guid.Empty;
}
#endregion
#region Helper Methods
private IEnumerable GetChildJobs(Guid? parentJobId, bool recursive, bool includeParent) {
var jobs = new List(dao.GetJobs(x => parentJobId == null ? !x.ParentJobId.HasValue : x.ParentJobId.Value == parentJobId));
if (recursive) {
var childs = new List();
foreach (var job in jobs) {
childs.AddRange(GetChildJobs(job.Id, recursive, false));
}
jobs.AddRange(childs);
}
if (includeParent) jobs.Add(GetJob(parentJobId.Value));
return jobs;
}
#endregion
}
}