#region Disclaimer / License
// Copyright (C) 2011, Kenneth Skovhede
// http://www.hexad.dk, opensource@hexad.dk
//
// This library is free software; you can redistribute it and/or
// modify it under the terms of the GNU Lesser General Public
// License as published by the Free Software Foundation; either
// version 2.1 of the License, or (at your option) any later version.
//
// This library 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
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
//
#endregion
using System;
using System.Collections.Generic;
using System.Text;
using System.Threading;
using Duplicati.Datamodel;
using System.Data.LightDatamodel;
using Duplicati.Library.Utility;
namespace Duplicati.GUI
{
///
/// This class handles scheduled runs of backups
///
public class Scheduler
{
///
/// The thread that runs the scheduler
///
private Thread m_thread;
///
/// The connection to the database
///
private IDataFetcherCached m_connection;
///
/// A termination flag
///
private volatile bool m_terminate;
///
/// The worker thread that is invoked to do work
///
private WorkerThread m_worker;
///
/// The wait event
///
private AutoResetEvent m_event;
///
/// The data syncronization lock
///
private object m_lock = new object();
///
/// The program lock for the database connection
///
private object m_datalock;
///
/// An event that is raised when the schedule changes
///
public event EventHandler NewSchedule;
///
/// The currently scheduled items
///
private Schedule[] m_schedule;
///
/// Constructs a new scheduler
///
/// The database connection
/// The worker thread
/// The database lock object
public Scheduler(IDataFetcherCached connection, WorkerThread worker, object datalock)
{
m_datalock = datalock;
m_connection = connection;
m_thread = new Thread(new ThreadStart(Runner));
m_worker = worker;
m_schedule = new Schedule[0];
m_terminate = false;
m_event = new AutoResetEvent(false);
m_thread.IsBackground = true;
m_thread.Start();
}
///
/// Forces the scheduler to re-evaluate the order.
/// Call this method if something changes
///
public void Reschedule()
{
m_event.Set();
}
///
/// A copy of the current schedule list
///
public List Schedule
{
get
{
lock (m_lock)
return new List(m_schedule);
}
}
///
/// Terminates the thread. Any items still in queue will be removed
///
/// True if the call should block until the thread has exited, false otherwise
public void Terminate(bool wait)
{
m_terminate = true;
m_event.Set();
if (wait)
m_thread.Join();
}
///
/// Returns the next valid date, given the start and the interval
///
/// The starting time
/// The repetition interval
/// The days the backup is allowed to run
/// The next valid date, or throws an exception if no such date can be found
public static DateTime GetNextValidTime(DateTime start, string repetition, DayOfWeek[] allowedDays)
{
DateTime res = start;
int i = 50000;
while (!IsDateAllowed(res, allowedDays) && i-- > 0)
res = Timeparser.ParseTimeInterval(repetition, res);
if (!IsDateAllowed(res, allowedDays))
{
StringBuilder sb = new StringBuilder();
if (allowedDays != null)
foreach (DayOfWeek w in allowedDays)
{
if (sb.Length != 0)
sb.Append(", ");
sb.Append(w.ToString());
}
throw new Exception(string.Format(Strings.Scheduler.InvalidTimeSetupError, start, repetition, sb.ToString()));
}
return res;
}
///
/// The actual scheduling procedure
///
private void Runner()
{
while (!m_terminate)
{
List reps = new List();
List tmp;
lock (m_datalock)
tmp = new List(m_connection.GetObjects());
//Determine schedule list
foreach (Schedule sc in tmp)
{
if (!string.IsNullOrEmpty(sc.Repeat))
{
DateTime start = sc.NextScheduledTime;
try { start = GetNextValidTime(sc.NextScheduledTime, sc.Repeat, sc.AllowedWeekdays); }
catch { } //TODO: Report this somehow
//If time is exceeded, run it now
if (start <= DateTime.Now)
{
//See if it is already queued
List tmplst = m_worker.CurrentTasks;
IDuplicityTask tastTemp = m_worker.CurrentTask;
if (tastTemp != null)
tmplst.Add(tastTemp);
bool found = false;
foreach (IDuplicityTask t in tmplst)
if (t != null && t is IncrementalBackupTask && ((IncrementalBackupTask)t).Schedule.ID == sc.ID)
{
found = true;
break;
}
//If it is not already in queue, put it there
if (!found)
m_worker.AddTask(new IncrementalBackupTask(sc));
//Caluclate next time, by adding the interval to the start until we have
// passed the current date and time
//TODO: Make this more efficient
int i = 50000;
while (start <= DateTime.Now && i-- > 0)
try { start = GetNextValidTime(Timeparser.ParseTimeInterval(sc.Repeat, start), sc.Repeat, sc.AllowedWeekdays); }
catch
{
//TODO: Report this somehow
continue;
}
if (start < DateTime.Now)
continue;
}
//Add to schedule list at the new time
reps.Add(sc);
sc.NextScheduledTime = start;
}
}
System.Data.LightDatamodel.QueryModel.OperationOrParameter op = System.Data.LightDatamodel.QueryModel.Parser.ParseQuery("ORDER BY When ASC");
//Sort them, lock as we assign the m_schedule variable
lock(m_lock)
m_schedule = op.EvaluateList(reps).ToArray();
//Raise event if needed
if (NewSchedule != null)
NewSchedule(this, null);
int waittime = 0;
//Figure out a sensible amount of time to sleep the thread
if (m_schedule.Length > 0)
{
//When is the next run scheduled?
TimeSpan nextrun = m_schedule[0].NextScheduledTime - DateTime.Now;
if (nextrun.TotalMilliseconds < 0)
continue;
//Don't sleep for more than 5 minutes
waittime = (int)Math.Min(nextrun.TotalMilliseconds, 60 * 1000 * 5);
}
else
{
//No tasks, check back later
waittime = 60 * 1000;
}
//Waiting on the event, enables a wakeup call from termination
// never use waittime = 0
m_event.WaitOne(Math.Max(100, waittime), false);
}
}
///
/// Returns true if the time is at an allowed weekday, false otherwise
///
/// The time to evaluate
/// The allowed days
/// True if the backup is allowed to run, false otherwise
private static bool IsDateAllowed(DateTime time, DayOfWeek[] allowedDays)
{
if (allowedDays == null || allowedDays.Length == 0)
return true;
else
return Array.IndexOf(allowedDays, time.DayOfWeek) >= 0;
}
}
}