kopano-ol-extension/src/AcaciaZPushPlugin/AcaciaZPushPlugin/ZPush/ZPushSync.cs

521 lines
19 KiB
C#

/// Copyright 2018 Kopano b.v.
///
/// This program is free software: you can redistribute it and/or modify
/// it under the terms of the GNU Affero General Public License, version 3,
/// as published by the Free Software Foundation.
///
/// This program 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 Affero General Public License for more details.
///
/// You should have received a copy of the GNU Affero General Public License
/// along with this program.If not, see<http://www.gnu.org/licenses/>.
///
/// Consult LICENSE file for details
using Acacia.Features;
using Acacia.Stubs;
using Acacia.Utils;
using Acacia.ZPush.Connect;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
namespace Acacia.ZPush
{
/// <summary>
/// Helper for periodically synchronising state with ZPush servers
/// </summary>
public class ZPushSync : DisposableWrapper
{
#region SyncTask
public interface SyncTask
{
}
private class Schedule
{
public delegate void TickHandler(Schedule schedule);
public readonly SyncTaskImpl Task;
public readonly ZPushAccount Account;
private readonly TickHandler _tick;
private TimeSpan _period;
/// <summary>
/// We use a Threading.Timer here, as the schedule may be modified from any thread
/// </summary>
private Timer _timer;
public TimeSpan Period
{
get { return _period; }
}
public Schedule(SyncTaskImpl task, ZPushAccount account, TickHandler scheduleTick, TimeSpan period)
{
this.Task = task;
this.Account = account;
this._tick = scheduleTick;
this._period = period;
}
private void _timer_Tick(object state)
{
_tick(this);
}
public void Cancel()
{
if (_timer != null)
{
_timer.Dispose();
_timer = null;
}
}
public void SetPeriod(TimeSpan value, bool executeNow)
{
_period = value;
// Start, this will destroy the old and start the new
Start(executeNow);
}
public void Start(bool executeNow)
{
// Cancel any existing timer
Cancel();
_timer = new Timer(_timer_Tick, null, executeNow ? TimeSpan.Zero : _period, _period);
}
}
/// <summary>
/// Represents a SyncTask. This is not specific for an account. When tasks are executed, GetInstance is
/// used to create a task instance for each account, which will be executed by the system task manager.
/// </summary>
private class SyncTaskImpl : SyncTask
{
private readonly Feature _owner;
private readonly string _name;
private readonly SyncAction _action;
private readonly SyncActionConnection _actionConnection;
/// <summary>
/// Optional schedule for all accounts.
/// </summary>
private Schedule _scheduleAll;
/// <summary>
/// Optional schedules per account.
/// </summary>
private readonly Dictionary<ZPushAccount, Schedule> _schedulesAccount = new Dictionary<ZPushAccount, Schedule>();
public SyncTaskImpl(Feature owner, string name, SyncAction action, SyncActionConnection actionConnection)
{
this._owner = owner;
this._name = name;
this._action = action;
this._actionConnection = actionConnection;
}
public override string ToString()
{
string s = _owner.Name;
if (_name != null)
s += ":" + _name;
return s;
}
/// <summary>
/// Returns the task for execution.
/// </summary>
/// <param name="schedule">The schedule that triggered the task execution, or null if this is a sync.</param>
/// <param name="account">The account for which the tasks are requested.</param>
/// <returns>The task instance, or null if there is no task to execute in this schedule.</returns>
public AcaciaTask GetInstance(Schedule schedule, ZPushAccount account)
{
if (!IsTaskScheduleApplicable(schedule, account))
return null;
if (_actionConnection != null)
{
return new AcaciaTask(null, _owner, _name, () =>
{
// TODO: reuse connections
using (ZPushConnection con = account.Connect())
{
_actionConnection(con);
}
});
}
else
{
return new AcaciaTask(null, _owner, _name, () => _action(account));
}
}
/// <summary>
/// Sets a custom schedule for the task. This method only records the schedule, execution is done by ZPushSync.
/// </summary>
/// <param name="account">The specific account to set the schedule for, or null to set it for all accounts</param>
/// <param name="schedule">The schedule. Specify null to clear the schedule and resume the original.</param>
public void SetTaskSchedule(ZPushAccount account, Schedule schedule)
{
if (account == null)
_scheduleAll = schedule;
else
{
if (schedule == null)
_schedulesAccount.Remove(account);
else
_schedulesAccount[account] = schedule;
}
}
/// <summary>
/// Returns any custom task scheddule registered for the account, or all accounts.
/// </summary>
/// <param name="account">The account, or null to retrieve the schedule for all accounts.</param>
/// <returns>The schedule, or null if no schedule is registered</returns>
public Schedule GetTaskSchedule(ZPushAccount account)
{
if (account == null)
{
return _scheduleAll;
}
else
{
Schedule schedule;
_schedulesAccount.TryGetValue(account, out schedule);
return schedule;
}
}
private bool IsTaskScheduleApplicable(Schedule schedule, ZPushAccount account)
{
// The fastest schedule is applicable, so determine that
Schedule perAccount = GetTaskSchedule(account);
Schedule all = _scheduleAll;
// If there is no custom schedule, only applicable if the task schedule is the standard sync
if (perAccount == null && all == null)
return schedule == null;
// Otherwise the standard sync is not applicable
else if (schedule == null)
return false;
// Determine the quickest
Schedule quickest = perAccount;
if (quickest == null)
quickest = all;
else if (all != null && all.Period.TotalMilliseconds < quickest.Period.TotalMilliseconds)
quickest = all;
// Applicable if that is the effective schedule
return quickest == schedule;
}
}
public void AddEndTaskOnce(AcaciaTask after)
{
_endTasks.Enqueue(after);
}
#endregion
#region Setup
private readonly ISyncObject _syncObject;
private readonly System.Windows.Forms.Timer _timer;
private ZPushWatcher _watcher;
private bool _started;
private readonly List<SyncTaskImpl> _tasks = new List<SyncTaskImpl>();
public readonly bool Enabled;
public readonly TimeSpan Period;
public readonly TimeSpan PeriodThrottle;
public DateTime LastSyncTime;
public ZPushSync(ZPushWatcher watcher, IAddIn addIn)
{
// Get the settings
Enabled = GlobalOptions.INSTANCE.ZPushSync;
Period = GlobalOptions.INSTANCE.ZPushSync_Period;
if (Period.Ticks == 0)
Period = Constants.ZPUSH_SYNC_DEFAULT_PERIOD;
PeriodThrottle = GlobalOptions.INSTANCE.ZPushSync_PeriodThrottle;
if (PeriodThrottle.Ticks == 0)
PeriodThrottle = Constants.ZPUSH_SYNC_DEFAULT_PERIOD_THROTTLE;
// Set up a timer and events if enabled
if (Enabled)
{
_watcher = watcher;
_timer = new System.Windows.Forms.Timer();
_timer.Interval = (int)Period.TotalMilliseconds;
_timer.Tick += _timer_Tick;
_timer.Start();
// Need to keep a reference to keep receiving events
_syncObject = addIn.GetSyncObject();
_syncObject.SyncStart += SyncObject_SyncStart;
_syncObject.SyncEnd += SyncObject_SyncEnd;
watcher.AccountDiscovered += Watcher_AccountDiscovered;
}
}
protected override void DoRelease()
{
_syncObject.Dispose();
}
#endregion
#region Public interface
/// <summary>
/// Starts the synchronisation engine. After it is started, new tasks can no longer be added.
/// </summary>
public void Start()
{
_started = true;
}
/// <summary>
/// Delegate for an account-specific task.
/// </summary>
/// <param name="account">The account</param>
public delegate void SyncAction(ZPushAccount account);
/// <summary>
/// Delegate for an account-specific task that will connect to the ZPush server.
/// The account can be obtained from the connection.
/// </summary>
/// <param name="connection">The connection to the ZPush server. This must not be dispose by
/// the task, as it may be reused for different tasks</param>
public delegate void SyncActionConnection(ZPushConnection connection);
/// <summary>
/// Adds a synchronisation task.
/// </summary>
/// <param name="owner">The feature owning the task.</param>
/// <param name="name">The task's name, for logging.</param>
/// <param name="action">The action to execute.</param>
public SyncTask AddTask(Feature owner, string name, SyncAction action)
{
if (_started)
throw new Exception("Already started, cannot add task");
SyncTaskImpl task = new SyncTaskImpl(owner, name, action, null);
_tasks.Add(task);
return task;
}
/// <summary>
/// Adds a synchronisation task that will use a connection to ZPUsh.
/// </summary>
/// <param name="owner">The feature owning the task.</param>
/// <param name="name">The task's name, for logging.</param>
/// <param name="action">The action to execute.</param>
public SyncTask AddTask(Feature owner, string name, SyncActionConnection action)
{
if (_started)
throw new Exception("Already started, cannot add task");
SyncTaskImpl task = new SyncTaskImpl(owner, name, null, action);
_tasks.Add(task);
return task;
}
/// <summary>
/// Sets a custom schedule for the specified task.
/// </summary>
/// <param name="task">The task</param>
/// <param name="account">The specific account to set the schedule for, or null to set it for all accounts.
/// If a per-account schedule is set, and an all-account schedule, the quickest of both applies.</param>
/// <param name="period">The schedule. Specify null to clear the schedule and resume the original.</param>
/// <returns>The old schedule, or null if there was none.</returns>
public TimeSpan? SetTaskSchedule(SyncTask task, ZPushAccount account, TimeSpan? period, bool executeNow = false)
{
SyncTaskImpl impl = (SyncTaskImpl)task;
Logger.Instance.Trace(this, "Setting task schedule for {0}{1} to {2}",
impl.ToString(),
account == null ? "" : (" for account " + account),
period == null ? "default" : period.Value.ToString());
// Check if there's an existing schedule to modify
Schedule schedule = impl.GetTaskSchedule(account);
TimeSpan? old = schedule?.Period;
if (period == null)
{
// Clearing the schedule
if (schedule != null)
{
// Clear the existing schedule
schedule.Cancel();
impl.SetTaskSchedule(account, null);
}
// If there was no schedule, we're already done
}
else
{
// Setting the schedule
if (schedule == null)
{
// Create a new schedule
schedule = new Schedule(impl, account, ScheduleTick, period.Value);
impl.SetTaskSchedule(account, schedule);
schedule.Start(executeNow);
}
else
{
// Update the existing schedule
schedule.SetPeriod(period.Value, executeNow);
}
}
return old;
}
private void ScheduleTick(Schedule schedule)
{
// Don't contact the network if Outlook is offline
if (ThisAddIn.Instance.IsOffline)
return;
if (schedule.Account == null)
{
// Execute tasks for all accounts
foreach (ZPushAccount account in _watcher.Accounts.GetAccounts())
{
ExecuteTask(schedule.Task.GetInstance(schedule, account), false);
}
}
else
{
// Execute tasks for the specific account
ExecuteTask(schedule.Task.GetInstance(schedule, schedule.Account), false);
}
}
#endregion
#region Task execution
/// <summary>
/// Executes the tasks for all known ZPush accounts. Only executed if enough time has passed since the last check.
/// </summary>
private void PossiblyExecuteTasks()
{
// Don't contact the network if Outlook is offline
if (ThisAddIn.Instance.IsOffline)
return;
// Check for time
DateTime now = DateTime.Now;
if (LastSyncTime != null && now.Subtract(LastSyncTime) < PeriodThrottle)
{
// Back off
return;
}
LastSyncTime = now;
// Execute tasks for all accounts
foreach (ZPushAccount account in _watcher.Accounts.GetAccounts())
ExecuteTasks(account, false);
}
public void ExecuteTasks(ZPushAccount[] accounts, bool synchronous = false)
{
foreach (ZPushAccount account in accounts)
ExecuteTasks(account, synchronous);
}
/// <summary>
/// Executes the tasks for the specified ZPush account. The tasks are pushed into the system
/// task queue.
/// </summary>
private void ExecuteTasks(ZPushAccount account, bool synchronous = false)
{
// Don't contact the network if Outlook is offline
if (ThisAddIn.Instance.IsOffline)
return;
// Execute the tasks for the account
foreach (SyncTaskImpl task in _tasks)
{
ExecuteTask(task.GetInstance(null, account), synchronous);
}
}
/// <summary>
/// Helper to execute a task. Silently ignores null tasks.
/// </summary>
private void ExecuteTask(AcaciaTask task, bool synchronous)
{
if (task == null)
return;
Tasks.Task(task, synchronous);
}
/// <summary>
/// Timer callback, executes any tasks.
/// </summary>
private void _timer_Tick(object sender, EventArgs e)
{
PossiblyExecuteTasks();
}
/// <summary>
/// Invoked when a new ZPush account is discovered, runs tasks for that account.
/// This is also invoked at start-up, so triggers initial sync.
/// </summary>
private void Watcher_AccountDiscovered(ZPushAccount account)
{
ExecuteTasks(account);
}
/// <summary>
/// Invoked when an explicit send and receive is performed, invokes any tasks.
/// </summary>
private void SyncObject_SyncStart()
{
// TODO: this checks _started, others don't. Also, this is probably invoked on
// start-up, as is AccountDiscoverd. Does that mean tasks are invoked twice
// on start-up?
if (_started)
{
// Explicit sync, run tasks
PossiblyExecuteTasks();
}
}
private readonly ConcurrentQueue<AcaciaTask> _endTasks = new ConcurrentQueue<AcaciaTask>();
/// <summary>
/// Invoked after an explicit send and receive is performed, invokes any tasks.
/// </summary>
private void SyncObject_SyncEnd()
{
AcaciaTask task;
while (_endTasks.TryDequeue(out task))
{
Tasks.Task(task, false);
}
}
#endregion
}
}