using System; using System.IO; using System.Xml; using System.Collections.Generic; using WebReports.Api.Scheduler; namespace Scheduler { public class SchedulerQueue { private static object QueueLockObject = new object(); private static object LogLockObject = new object(); private static object LockObject = new object(); private const string QueueDirectory = @"Path\To\Repository"; private const int FlushTime = 1; // hours; Flush is called from Exago web app, so we don't have the flush time to pass in (which is part of scheduler service config) private static string QueuelockFn = null; private static string LogFn = null; static SchedulerQueue() { QueuelockFn = String.Format(@"{0}\locked.txt", QueueDirectory); LogFn = String.Format(@"{0}\log.txt", QueueDirectory); } // begin interface methods // called when a specific scheduler service starts; service name is in format MachineName:Port public static void Start(string serviceName) { Log("Start: " + serviceName); // change 'stuck' running jobs to ready, which probably was due to service going down before job was complete foreach (string jobFn in Directory.GetFiles(QueueDirectory, "*.xml", SearchOption.TopDirectoryOnly)) { QueueApiJob job = GetJobFromFilename(jobFn); if (job != null && job.Status == JobStatus.Running && GetServiceName(jobFn) == serviceName) { job.Status = JobStatus.Ready; SaveJobToQueue(job, jobFn); Log("Incomplete job status changed to ready: " + job.ScheduleName); } } } // returns array of jobs for scheduler manager public static string[] GetJobList(string viewLevel, string companyId, string userId) { Log("GetJobList"); List jobXmlList = new List(); foreach (string jobFn in Directory.GetFiles(QueueDirectory, "*.xml", SearchOption.TopDirectoryOnly)) { QueueApiJob job = GetJobFromFilename(jobFn); if (job != null && job.IsUserViewable(viewLevel, companyId, userId)) jobXmlList.Add(job.JobListXml); // just job info and schedule for efficiency (doesn't include report or config) } return jobXmlList.ToArray(); } // returns next job to execute public static string GetNextExecuteJob(string serviceName) { Log("GetNextExecuteJob: " + serviceName); lock (LockObject) { try { WaitUntilQueueUnlocked(); // we need to flush occasionally to remove completed or deleted jobs; we can do this in a method that is hit like here, or start a thread that does it occasionally ProcessFlush(FlushTime); DateTime executeJobDate = DateTime.Now; string executeJobFn = null; QueueApiJob executeApiJob = null; foreach (string jobFn in Directory.GetFiles(QueueDirectory, "*.xml", SearchOption.TopDirectoryOnly)) { QueueApiJob job = GetJobFromFilename(jobFn); if (job != null && job.Status == JobStatus.Ready && job.NextExecuteDate <= executeJobDate) { executeJobDate = job.NextExecuteDate; executeJobFn = jobFn; executeApiJob = job; } } if (executeApiJob == null) return null; else { // change status so that we don't reuse this job executeApiJob.Status = JobStatus.Running; SaveJobToQueue(executeApiJob, executeJobFn); SaveServiceName(executeJobFn, serviceName); Log("Set job to execute status: " + executeApiJob.ScheduleName); return executeApiJob.Xml; } } finally { UnlockQueue(); } } } // called when a job is added or updated; check job.Status for result since job may be finished with an execution public static void SaveJob(string jobXml) { Log("SaveJob"); QueueApiJob job = GetJobFromXml(jobXml); string jobFn = GetJobFnFromId(job.JobId); if (job.Status == JobStatus.Removed) { File.Delete(jobFn); DeleteServiceName(jobFn); } else { SaveJobToQueue(job, jobFn); } } // returns entire XML package for a specific job id public static string GetJobData(string jobId) { Log("GetJobData"); string jobFn = GetJobFnFromId(jobId); QueueApiJob job = GetJobFromFilename(jobFn); return (job == null ? null : job.Xml); } // deletes any jobs that contains a specific report id; triggered by deleting a report in the Exago UI public static void DeleteReport(string reportId) { Log("DeleteReport: " + reportId); // this could be done in a sub-thread in order to prevent main thread locking lock (LockObject) { try { WaitUntilQueueUnlocked(); foreach (string jobFn in Directory.GetFiles(QueueDirectory, "*.xml", SearchOption.TopDirectoryOnly)) { QueueApiJob job = GetJobFromFilename(jobFn); if (job != null && job.ReportId == reportId) { job.SetDelete(); if (job.Status == JobStatus.Removed) File.Delete(jobFn); } } } finally { UnlockQueue(); } } } // renames any reports within jobs that contains a specific report id; triggered by renaming a report in the Exago UI public static void RenameReport(string reportId, string reportName) { Log("RenameReport: " + reportId + " " + reportName); // this could be done in a sub-thread in order to prevent main thread locking lock (LockObject) { try { WaitUntilQueueUnlocked(); foreach (string jobFn in Directory.GetFiles(QueueDirectory, "*.xml", SearchOption.TopDirectoryOnly)) { QueueApiJob job = GetJobFromFilename(jobFn); if (job != null && job.ReportId == reportId) { job.ReportName = reportName; SaveJobToQueue(job, jobFn); } } } finally { UnlockQueue(); } } } // updates report contents within any jobs that contain the specific report id; triggered by saving a report in the Exago UI public static void UpdateReport(string reportId, string reportXml) { Log("UpdateReport: " + reportId); // this could be done in a sub-thread in order to prevent main thread locking lock (LockObject) { try { WaitUntilQueueUnlocked(); foreach (string jobFn in Directory.GetFiles(QueueDirectory, "*.xml", SearchOption.TopDirectoryOnly)) { QueueApiJob job = GetJobFromFilename(jobFn); if (job != null && job.ReportId == reportId) { job.ReportXml = reportXml; SaveJobToQueue(job, jobFn); } } } finally { UnlockQueue(); } } } // deletes any jobs that are marked as deleted or completed; triggered by click of flush button from Exago scheduler manager public static void Flush(string viewLevel, string companyId, string userId) { Log("Flush"); lock (LockObject) { try { WaitUntilQueueUnlocked(); ProcessFlush(0, viewLevel, companyId, userId); } finally { UnlockQueue(); } } } // end interface methods // private support methods private static void SaveJobToQueue(QueueApiJob job, string filename) { Log("Saving job to queue: " + job.ScheduleName); XmlDocument xmlDoc = new XmlDocument(); xmlDoc.LoadXml(job.Xml); xmlDoc.Save(filename); if (job.Status != JobStatus.Running) DeleteServiceName(filename); } private static string GetJobFnFromId(string jobId) { return String.Format(@"{0}\{1}.xml", QueueDirectory, jobId); } private static QueueApiJob GetJobFromFilename(string jobFn) { // check in case job was flushed if (!File.Exists(jobFn)) return null; string jobXml = File.ReadAllText(jobFn); return QueueApi.GetJob(jobXml); } private static QueueApiJob GetJobFromXml(string jobXml) { return QueueApi.GetJob(jobXml); } private static XmlDocument GetJobDocFromXml(string jobXml) { XmlDocument xmlDoc = new XmlDocument(); xmlDoc.LoadXml(jobXml); return xmlDoc; } private static void ProcessFlush(int flushTime, string viewLevel = null, string companyId = null, string userId = null) { lock (LockObject) { foreach (string jobFn in Directory.GetFiles(QueueDirectory, "*.xml", SearchOption.TopDirectoryOnly)) { QueueApiJob job = GetJobFromFilename(jobFn); if (job != null) { job.SetFlush(flushTime, viewLevel, companyId, userId); if (job.Status == JobStatus.Removed) { File.Delete(jobFn); DeleteServiceName(jobFn); } } } } } // quick & dirty for multi-user queue operations; very imperfect due to timing of scheduler service access private static bool IsQueueLocked { get { lock (QueueLockObject) { return File.Exists(QueuelockFn); } } } private static void LockQueue() { lock (QueueLockObject) { File.WriteAllText(QueuelockFn, "1"); } } private static void UnlockQueue() { lock (QueueLockObject) { File.Delete(QueuelockFn); } } private static void WaitUntilQueueUnlocked() { while (IsQueueLocked) { System.Threading.Thread.Sleep(250); } // lock it again and return LockQueue(); } private static void SaveServiceName(string jobFn, string serviceName) { string serviceFn = jobFn + ".sn"; File.WriteAllText(jobFn + ".sn", serviceName); Log("Write service fn: " + serviceFn); } private static string GetServiceName(string jobFn) { string serviceFn = jobFn + ".sn"; return (File.Exists(serviceFn) ? File.ReadAllText(serviceFn) : String.Empty); } private static void DeleteServiceName(string jobFn) { string serviceFn = jobFn + ".sn"; if (File.Exists(serviceFn)) { File.Delete(serviceFn); Log("Remove service fn: " + serviceFn); } } private static void Log(string info) { lock (LogLockObject) { try { File.AppendAllText(LogFn, DateTime.Now.ToString("MM/dd/yyyy hh:mmtt ")); File.AppendAllText(LogFn, info); File.AppendAllText(LogFn, Environment.NewLine); } catch { /* do nothing */ } } } } }