Files
duplicati/Duplicati/GUI/MainForm.cs
T
kenneth@hexad.dk 2e3e0abdff A bit of a hack, but now the main message loop is aborted if the application hangs after closing.
This should not cause any problems because the timer is started at the very last moment, and there should be no work to do after invocation but simply discard the hidden window.
The abort is picked up so the program cleanup-on-exit code is executed as normal.

git-svn-id: https://duplicati.googlecode.com/svn/trunk@999 59da171f-624f-0410-aa54-27559c288bec
2011-11-17 19:56:41 +00:00

759 lines
29 KiB
C#

#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.ComponentModel;
using System.Data;
using System.Drawing;
using System.Text;
using System.Windows.Forms;
namespace Duplicati.GUI
{
/// <summary>
/// This class handles the Duplicati TrayIcon
/// </summary>
public partial class MainForm : Form
{
private static readonly int BALLOON_SHOW_TIME = (int)TimeSpan.FromSeconds(60).TotalMilliseconds;
private ServiceStatus StatusDialog;
private WizardHandler WizardDialog;
private Datamodel.ApplicationSettings m_settings;
private delegate void EmptyDelegate();
private string[] m_initialArguments;
private bool m_hasAttemptedBackupTermination = false;
private DuplicatiRunner.RunnerResult m_currentIconState;
private Icon m_currentIcon;
private string m_currentTooltip;
private TrayIconProxy m_trayIcon;
/// <summary>
/// A recursion protection helper flag
/// </summary>
private bool m_inQuit = false;
/// <summary>
/// Internal helper class that can filter all access to the trayicon if the program runs without a trayicon
/// </summary>
private class TrayIconProxy
{
private readonly NotifyIcon m_owner = null;
private Icon m_icon;
private bool m_visible;
private string m_text;
public TrayIconProxy(NotifyIcon owner)
{
if (!Program.TraylessMode)
m_owner = owner;
}
public Icon Icon
{
get { return m_icon; }
set
{
m_icon = value;
if (m_owner != null)
m_owner.Icon = value;
}
}
public bool Visible
{
get { return m_visible; }
set
{
m_visible = value;
if (m_owner != null)
m_owner.Visible = value;
}
}
public string Text
{
get { return m_text; }
set
{
m_text = value;
if (m_owner != null)
m_owner.Text = value;
}
}
public void ShowBalloonTip(int timeout, string tipTitle, string tipText, ToolTipIcon tipIcon)
{
if (m_owner != null)
m_owner.ShowBalloonTip(timeout, tipTitle, tipText, tipIcon);
}
}
public string[] InitialArguments
{
get { return m_initialArguments; }
set { m_initialArguments = value; }
}
public MainForm()
{
InitializeComponent();
m_trayIcon = new TrayIconProxy(TrayIcon);
m_currentIcon = Properties.Resources.TrayNormal;
m_currentTooltip = Strings.MainForm.TrayStatusReady;
Program.LiveControl.StateChanged += new EventHandler(LiveControl_StateChanged);
Program.WorkThread.StartingWork += new EventHandler(WorkThread_StartingWork);
Program.WorkThread.CompletedWork += new EventHandler(WorkThread_CompletedWork);
Program.SingleInstance.SecondInstanceDetected += new SingleInstance.SecondInstanceDelegate(SingleInstance_SecondInstanceDetected);
m_settings = new Duplicati.Datamodel.ApplicationSettings(Program.DataConnection);
Program.DataConnection.AfterDataChange += new System.Data.LightDatamodel.DataChangeEventHandler(DataConnection_AfterDataChange);
Program.Runner.ProgressEvent += new DuplicatiRunner.ProgressEventDelegate(Runner_DuplicatiProgress);
Program.Runner.ResultEvent += new DuplicatiRunner.ResultEventDelegate(Runner_ResultEvent);
#if DEBUG
this.Text += " (DEBUG)";
#endif
}
public void SetCurrentIcon(DuplicatiRunner.RunnerResult icon, string message)
{
if (icon == DuplicatiRunner.RunnerResult.Error)
{
m_currentIcon = Properties.Resources.TrayNormalError;
m_currentIconState = icon;
m_currentTooltip = message;
}
else if ((icon == DuplicatiRunner.RunnerResult.Warning || icon == DuplicatiRunner.RunnerResult.Partial) && m_currentIconState != DuplicatiRunner.RunnerResult.Error)
{
m_currentIcon = Properties.Resources.TrayNormalWarning;
m_currentIconState = icon;
m_currentTooltip = message;
}
UpdateTrayIcon();
}
public void ResetCurrentIcon()
{
m_currentIcon = Properties.Resources.TrayNormal;
m_currentTooltip = Strings.MainForm.TrayStatusReady;
m_currentIconState = DuplicatiRunner.RunnerResult.OK;
UpdateTrayIcon();
}
private void UpdateTrayIcon()
{
//TODO: Hard to maintain consistent state because the number of cases here can change
if (Program.LiveControl.State == LiveControls.LiveControlState.Running && !Program.WorkThread.Active)
{
m_trayIcon.Icon = m_currentIcon;
SetTrayIconText(m_currentTooltip);
}
}
void Runner_ResultEvent(DuplicatiRunner.RunnerResult result, string parsedMessage, string message)
{
if (this.InvokeRequired)
{
this.BeginInvoke(new DuplicatiRunner.ResultEventDelegate(Runner_ResultEvent), result, parsedMessage, message);
return;
}
string name = "";
//Dirty read of the instance variable
try { name = Program.WorkThread.CurrentTask.Schedule.Name; }
catch { }
if (result == DuplicatiRunner.RunnerResult.Error)
SetCurrentIcon(result, String.Format(Strings.MainForm.BalloonTip_Error, name, parsedMessage));
else if (result == DuplicatiRunner.RunnerResult.Partial || result == DuplicatiRunner.RunnerResult.Warning)
SetCurrentIcon(result, String.Format(Strings.MainForm.BalloonTip_Warning, name));
if (result == DuplicatiRunner.RunnerResult.OK || m_settings.BallonNotificationLevel == Duplicati.Datamodel.ApplicationSettings.NotificationLevel.Off)
return;
if (result == DuplicatiRunner.RunnerResult.Error)
m_trayIcon.ShowBalloonTip(BALLOON_SHOW_TIME, Application.ProductName, String.Format(Strings.MainForm.BalloonTip_Error, name, parsedMessage), ToolTipIcon.Error);
else if (result == DuplicatiRunner.RunnerResult.Warning || result == DuplicatiRunner.RunnerResult.Partial)
m_trayIcon.ShowBalloonTip(BALLOON_SHOW_TIME, Application.ProductName, String.Format(Strings.MainForm.BalloonTip_Warning, name), ToolTipIcon.Warning);
}
void DataConnection_AfterDataChange(object sender, string propertyname, object oldvalue, object newvalue)
{
if (sender as Datamodel.ApplicationSetting == null)
return;
if (this.InvokeRequired)
{
this.BeginInvoke(new System.Data.LightDatamodel.DataChangeEventHandler(DataConnection_AfterDataChange), sender, propertyname, oldvalue, newvalue);
return;
}
m_settings = new Duplicati.Datamodel.ApplicationSettings(Program.DataConnection);
}
void Runner_DuplicatiProgress(Duplicati.Library.Main.DuplicatiOperation operation, DuplicatiRunner.RunnerState state, string message, string submessage, int progress, int subprogress)
{
if (this.InvokeRequired)
{
this.BeginInvoke(new DuplicatiRunner.ProgressEventDelegate(Runner_DuplicatiProgress), operation, state, message, submessage, progress, subprogress);
return;
}
Datamodel.ApplicationSettings.NotificationLevel level = m_settings.BallonNotificationLevel;
string name = "";
//Dirty read of the instance variable
try { name = Program.WorkThread.CurrentTask.Schedule.Name; }
catch {}
if (state == DuplicatiRunner.RunnerState.Started && (level == Duplicati.Datamodel.ApplicationSettings.NotificationLevel.StartAndStop || level == Duplicati.Datamodel.ApplicationSettings.NotificationLevel.Start || level == Duplicati.Datamodel.ApplicationSettings.NotificationLevel.Continous))
{
//Show start balloon
m_trayIcon.ShowBalloonTip(BALLOON_SHOW_TIME, Application.ProductName, String.Format(Strings.MainForm.BalloonTip_Started, name), ToolTipIcon.Info);
}
else if (state == DuplicatiRunner.RunnerState.Stopped && (level == Duplicati.Datamodel.ApplicationSettings.NotificationLevel.StartAndStop || level == Duplicati.Datamodel.ApplicationSettings.NotificationLevel.Continous))
{
//Show stop balloon
m_trayIcon.ShowBalloonTip(BALLOON_SHOW_TIME, Application.ProductName, String.Format(Strings.MainForm.BalloonTip_Stopped, name), ToolTipIcon.Info);
}
else if (state == DuplicatiRunner.RunnerState.Running && level == Duplicati.Datamodel.ApplicationSettings.NotificationLevel.Continous)
{
//Show update balloon
m_trayIcon.ShowBalloonTip(BALLOON_SHOW_TIME, Application.ProductName, String.Format(Strings.MainForm.BalloonTip_Running, message), ToolTipIcon.Info);
}
}
void LiveControl_StateChanged(object sender, EventArgs e)
{
if (InvokeRequired)
{
Invoke(new EventHandler(LiveControl_StateChanged), sender, e);
return;
}
switch (Program.LiveControl.State)
{
case LiveControls.LiveControlState.Paused:
pauseToolStripMenuItem.Text = Strings.Common.MenuResume;
pauseToolStripMenuItem.Checked = true;
m_trayIcon.Icon = Program.WorkThread.Active ? Properties.Resources.TrayWorkingPause : Properties.Resources.TrayNormalPause;
SetTrayIconText(Strings.MainForm.TrayStatusPause);
break;
case LiveControls.LiveControlState.Running:
//Restore the icon and tooltip
if (Program.WorkThread.Active)
WorkThread_StartingWork(Program.WorkThread, null);
else
WorkThread_CompletedWork(Program.WorkThread, null);
pauseToolStripMenuItem.Checked = false;
pauseToolStripMenuItem.Text = Strings.Common.MenuPause;
break;
}
}
private void MainForm_Load(object sender, EventArgs e)
{
m_trayIcon.Icon = m_currentIcon;
SetTrayIconText(m_currentTooltip);
LiveControl_StateChanged(Program.LiveControl, null);
m_trayIcon.Visible = true;
long count = 0;
lock (Program.MainLock)
count = Program.DataConnection.GetObjects<Datamodel.Schedule>().Length;
if (count == 0)
ShowWizard();
else if (InitialArguments != null)
HandleCommandlineArguments(InitialArguments);
if (Library.Utility.Utility.IsMono)
MonoSupport.BeginInvoke (this, new EmptyDelegate(HideWindow));
else
BeginInvoke(new EmptyDelegate(HideWindow));
if (Program.TraylessMode)
ShowStatus();
}
private void HideWindow()
{
this.Visible = false;
}
private void TrayIcon_MouseDoubleClick(object sender, MouseEventArgs e)
{
TrayIcon_MouseClick(sender, e);
}
private static void AbortOwnThread(object thread)
{
System.Threading.Thread.Sleep(5000);
if (Program.IsRunningMainLoop)
{
Program.IsRunningMainLoop = false;
((System.Threading.Thread)thread).Abort();
}
}
private void TrayIcon_MouseClick(object sender, MouseEventArgs e)
{
if (e.Button == MouseButtons.Left)
{
if (Program.DataConnection.GetObjects<Datamodel.Schedule>().Length == 0)
ShowWizard();
else
ToggleStatus();
}
}
private void DelayDurationMenu_Click(object sender, EventArgs e)
{
Program.LiveControl.Pause((string)((ToolStripItem)sender).Tag);
}
private void pauseToolStripMenuItem_Click(object sender, EventArgs e)
{
if (Program.LiveControl.State == LiveControls.LiveControlState.Running)
Program.LiveControl.Pause();
else
Program.LiveControl.Resume();
}
private void stopToolStripMenuItem_Click(object sender, EventArgs e)
{
Program.Runner.Stop(CloseReason.UserClosing);
}
private void SetTrayIconText(string text)
{
//Strange 64 character limit on linux: http://code.google.com/p/duplicati/issues/detail?id=298
try { m_trayIcon.Text = text; }
catch
{
if (text.Length >= 64)
m_trayIcon.Text = text.Substring(0, 60) + "...";
}
}
private void WorkThread_StartingWork(object sender, EventArgs e)
{
if (InvokeRequired)
{
Invoke(new EventHandler(WorkThread_StartingWork), sender, e);
return;
}
m_trayIcon.Icon = Properties.Resources.TrayWorking;
string tmp = string.Format(Strings.MainForm.TrayStatusRunning, Program.WorkThread.CurrentTask == null ? "" : Program.WorkThread.CurrentTask.Schedule.Name);
SetTrayIconText(tmp);
stopToolStripMenuItem.Enabled = true;
}
private void WorkThread_CompletedWork(object sender, EventArgs e)
{
if (InvokeRequired)
{
Invoke(new EventHandler(WorkThread_CompletedWork), sender, e);
return;
}
if (Program.LiveControl.State != LiveControls.LiveControlState.Paused)
{
m_trayIcon.Icon = m_currentIcon;
SetTrayIconText(m_currentTooltip);
}
stopToolStripMenuItem.Enabled = false;
}
private void quitToolStripMenuItem_Click(object sender, EventArgs e)
{
Quit();
}
public bool IsInQuit
{
get { return m_inQuit; }
}
public bool Quit()
{
//Under Mono we somehow keep invoking this function recursively, eventually causing a stack-overflow exception
if (m_inQuit)
return false;
try
{
m_inQuit = true;
if (Program.WorkThread.Active && MessageBox.Show(Strings.MainForm.ExitWhileBackupIsRunningQuestion, Application.ProductName, MessageBoxButtons.YesNoCancel, MessageBoxIcon.Question) != DialogResult.Yes)
return false;
Program.LiveControl.Pause();
Program.Runner.Stop(CloseReason.ApplicationExitCall);
m_trayIcon.Visible = false;
if (StatusDialog != null && StatusDialog.Visible)
StatusDialog.Close();
if (WizardDialog != null && WizardDialog.Visible)
WizardDialog.Close();
EnsureBackupIsTerminated(CloseReason.ApplicationExitCall);
Application.Exit();
return true;
}
finally
{
m_inQuit = false;
}
}
private void optionsToolStripMenuItem_Click(object sender, EventArgs e)
{
ShowSettings();
}
private void statusToolStripMenuItem_Click(object sender, EventArgs e)
{
ShowStatus();
}
private void wizardToolStripMenuItem_Click(object sender, EventArgs e)
{
ShowWizard();
}
private void throttleOptionsToolStripMenuItem_Click(object sender, EventArgs e)
{
new ThrottleControl().ShowDialog(this);
}
private void ChangeStatusVisibility(bool toggle)
{
if (InvokeRequired)
{
Invoke(new EmptyDelegate(ShowStatus));
return;
}
if (StatusDialog == null || !StatusDialog.Visible)
StatusDialog = new ServiceStatus();
if (toggle && StatusDialog.Visible)
StatusDialog.Close();
else
{
StatusDialog.Show();
StatusDialog.Activate();
}
}
public void ToggleStatus()
{
ChangeStatusVisibility(true);
}
public void ShowStatus()
{
ChangeStatusVisibility(false);
}
public void ShowWizard()
{
if (InvokeRequired)
{
Invoke(new EmptyDelegate(ShowWizard));
return;
}
if (WizardDialog == null || !WizardDialog.Visible)
WizardDialog = new WizardHandler();
WizardDialog.Show();
}
public void ShowSettings()
{
if (InvokeRequired)
{
Invoke(new EmptyDelegate(ShowSettings));
return;
}
lock (Program.MainLock)
{
ApplicationSetup dlg = new ApplicationSetup();
dlg.ShowDialog(this);
}
}
private void MainForm_FormClosing(object sender, FormClosingEventArgs e)
{
m_trayIcon.Visible = false;
Program.LiveControl.StateChanged -= new EventHandler(LiveControl_StateChanged);
Program.WorkThread.StartingWork -= new EventHandler(WorkThread_StartingWork);
Program.WorkThread.CompletedWork -= new EventHandler(WorkThread_CompletedWork);
Program.SingleInstance.SecondInstanceDetected -= new SingleInstance.SecondInstanceDelegate(SingleInstance_SecondInstanceDetected);
EnsureBackupIsTerminated(e.CloseReason == CloseReason.UserClosing ? CloseReason.ApplicationExitCall : e.CloseReason);
//The form occasionally hangs on Mono, so we force kill it after 5 secs if it did not quit itself
if (Library.Utility.Utility.IsMono)
new System.Threading.Thread(AbortOwnThread).Start (System.Threading.Thread.CurrentThread);
}
private void SingleInstance_SecondInstanceDetected(string[] commandlineargs)
{
if (this.InvokeRequired)
{
this.Invoke(new SingleInstance.SecondInstanceDelegate(SingleInstance_SecondInstanceDetected), new object[] { commandlineargs });
return;
}
if (HandleCommandlineArguments(commandlineargs))
return;
//TODO: This actually blocks the app thread, and thus may pile up remote invocations
if (Program.TraylessMode)
try { StatusDialog.Focus(); }
catch { }
else
ShowWizard();
}
private bool HandleCommandlineArguments(string[] _args)
{
bool anyUiShown = false;
List<string> args = new List<string>(_args);
Dictionary<string, string> options = CommandLine.CommandLineParser.ExtractOptions(args);
//Backwards compatible options
if (args.Count == 2 && args[0].ToLower().Trim() == "run-backup")
options["run-backup"] = args[1].Trim();
//Backwards compatible options
if (args.Count == 1 && args[0] == "show-status")
options["show-status"] = "";
//Backwards compatible options
if (args.Count == 2 && args[0].ToLower().Trim() == "run-backup-group")
options["run-backup-group"] = args[1].Trim();
//If pause is requested, pause before parsing --run-backup
if (options.ContainsKey("pause"))
{
string period = options["pause"];
if (string.IsNullOrEmpty(period))
Program.LiveControl.Pause();
else
{
try
{
Program.LiveControl.Pause(period);
}
catch (Exception ex)
{
Program.LiveControl.Pause();
MessageBox.Show(this, string.Format(Strings.MainForm.PauseOperationFailed, ex), Application.ProductName, MessageBoxButtons.OK, MessageBoxIcon.Error);
}
}
anyUiShown = true;
}
if (options.ContainsKey("run-backup"))
{
string backupname = options["run-backup"];
if (string.IsNullOrEmpty(backupname))
{
if (WizardDialog == null || !WizardDialog.Visible)
{
WizardDialog = new WizardHandler(new System.Windows.Forms.Wizard.IWizardControl[] { new Wizard_pages.SelectBackup(Duplicati.GUI.Wizard_pages.WizardSettingsWrapper.MainAction.RunNow) });
WizardDialog.Show();
anyUiShown = true;
}
}
else
{
anyUiShown = true;
Datamodel.Schedule[] schedules = Program.DataConnection.GetObjects<Datamodel.Schedule>("Name LIKE ?", backupname.Trim());
if (schedules == null || schedules.Length == 0)
{
MessageBox.Show(string.Format(Strings.MainForm.NamedBackupNotFound, backupname), Application.ProductName, MessageBoxButtons.OK, MessageBoxIcon.Warning);
}
else if (schedules.Length > 1)
{
MessageBox.Show(string.Format(Strings.MainForm.MultipleNamedBackupsFound, backupname, schedules.Length), Application.ProductName, MessageBoxButtons.OK, MessageBoxIcon.Warning);
}
else
{
if (Library.Utility.Utility.ParseBoolOption(options, "full"))
Program.WorkThread.AddTask(new FullBackupTask(schedules[0]));
else
Program.WorkThread.AddTask(new IncrementalBackupTask(schedules[0]));
}
}
}
if (options.ContainsKey("run-backup-group"))
{
string groupname = options["run-backup-group"];
if (string.IsNullOrEmpty(groupname))
{
if (WizardDialog == null || !WizardDialog.Visible)
{
WizardDialog = new WizardHandler(new System.Windows.Forms.Wizard.IWizardControl[] { new Wizard_pages.SelectBackup(Duplicati.GUI.Wizard_pages.WizardSettingsWrapper.MainAction.RunNow) });
WizardDialog.Show();
anyUiShown = true;
}
}
else
{
Datamodel.Schedule[] schedules = Program.DataConnection.GetObjects<Datamodel.Schedule>("Path LIKE ?", groupname.Trim());
if (schedules == null || schedules.Length == 0)
{
MessageBox.Show(string.Format(Strings.MainForm.NamedBackupGroupNotFound, groupname), Application.ProductName, MessageBoxButtons.OK, MessageBoxIcon.Warning);
}
else
{
bool full = Library.Utility.Utility.ParseBoolOption(options, "full");
foreach(Datamodel.Schedule s in schedules)
if (full)
Program.WorkThread.AddTask(new FullBackupTask(s));
else
Program.WorkThread.AddTask(new IncrementalBackupTask(s));
}
anyUiShown = true;
}
}
if (Library.Utility.Utility.ParseBoolOption(options, "show-status"))
{
ShowStatus();
anyUiShown = true;
}
if (Library.Utility.Utility.ParseBoolOption(options, "show-wizard"))
{
ShowWizard();
anyUiShown = true;
}
//Resume if requested
if (Library.Utility.Utility.ParseBoolOption(options, "resume"))
{
Program.LiveControl.Resume();
anyUiShown = true;
}
return anyUiShown;
}
private void EnsureBackupIsTerminated(CloseReason reason)
{
//Ensure that this function can only be called once,
// as this prevents multiple termination questions presented to the user
if (m_hasAttemptedBackupTermination)
return;
m_hasAttemptedBackupTermination = true;
if (Program.Runner != null && Program.WorkThread != null && Program.WorkThread.Active)
{
//Make sure no new items can enter the queue
if (Program.Scheduler != null)
Program.Scheduler.Terminate(true);
//We want no new items to enter the queue
Program.WorkThread.Terminate(false);
Program.Runner.Pause();
if (!Program.Runner.IsStopRequested)
Program.Runner.Stop(reason);
//Wait 15 seconds to see if the stop works
for (int i = 0; i < 15; i++)
{
Program.WorkThread.Join(1000);
Application.DoEvents();
if (!Program.WorkThread.Active)
break;
}
while (Program.WorkThread.Active)
{
//Ask the user if we should abort
if (MessageBox.Show(Strings.MainForm.TerminateForExitQuestion, Application.ProductName, MessageBoxButtons.YesNo, MessageBoxIcon.Question) == DialogResult.No)
{
//Abort the thread
Program.Runner.Terminate(reason);
//Give last 5 second chance to write the remaining data
int i = 5;
while (i > 0 && !Program.WorkThread.Join(1000))
{
//The Join call blocks the main thread, so let pending events through
Application.DoEvents();
i--;
}
break;
}
//Wait 18 * 10 seconds = 3 minutes before asking again
for (int i = 0; i < 18; i++)
{
Program.WorkThread.Join(1000 * 10);
Application.DoEvents();
if (!Program.WorkThread.Active)
break;
}
}
}
}
private void TrayIcon_BalloonTipClicked(object sender, EventArgs e)
{
ShowStatus();
}
}
}