Files
duplicati/Duplicati/GUI/SingleInstance.cs
T
kenneth.skovhede@gmail.com 0e8c3162e1 Update issue #165
Status: Fixed
The signature files are now placed in "%DUPLICATI_HOME%/Signature Cache", which defaults to "appdata/duplicati/Signature Cache".

Update issue #128
Status: Fixed
Duplicati now uses an environment variable called %DUPLICATI_HOME% to determine where to place the data files.
As an alternative to setting the environment variable, Duplicati.exe can be invoked with the commandline option
--portable-mode, which will create a folder called "data" in the folder where Duplicati.exe is placed.
For maximum portability you can also set the temporary folder inside Duplicati's options to %DUPLICATI_HOME%\temp.

Update issue #70
This just got a whole lot easier after the introduction of the environment variable %DUPLICATI_HOME%.
The implementation can just invoke "Duplicati.exe --portable-mode" and everything should work.
The final package should have a customized default database with the temp folder set to %DUPLICATI_HOME%\temp.
The no auto-start setting can be applied by the "backup defaults.xml" file.

git-svn-id: https://duplicati.googlecode.com/svn/trunk@487 59da171f-624f-0410-aa54-27559c288bec
2010-08-31 20:58:11 +00:00

284 lines
11 KiB
C#

#region Disclaimer / License
// Copyright (C) 2010, 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;
namespace Duplicati.GUI
{
/// <summary>
/// The purpose of this class is to ensure that there is ever only one instance of the application running.
/// This class is based on file-locking, making it much more cross-platform portable than other versions,
/// that depend on memory communication, such as pipes or shared memory.
/// </summary>
public class SingleInstance : IDisposable
{
/// <summary>
/// The folder where control files are placed
/// </summary>
private const string CONTROL_DIR = "control_dir";
/// <summary>
/// The file that is locked by the first process
/// </summary>
private const string CONTROL_FILE = "lock";
/// <summary>
/// The prefix on files that communicate with the first instance
/// </summary>
private const string COMM_FILE_PREFIX = "other_invocation_";
/// <summary>
/// The delegate that is used to inform the first instance of the second invocation
/// </summary>
/// <param name="commandlineargs">The commandlinearguments for the second invocation</param>
public delegate void SecondInstanceDelegate(string[] commandlineargs);
/// <summary>
/// When the user tries to launch the application the second time, this event is raised
/// </summary>
public event SecondInstanceDelegate SecondInstanceDetected;
/// <summary>
/// The file that is locked to prevent other access
/// </summary>
private System.IO.FileStream m_file;
/// <summary>
/// The folder where control files are placed
/// </summary>
private string m_controldir;
/// <summary>
/// The watcher that allows interprocess communication
/// </summary>
private System.IO.FileSystemWatcher m_filewatcher;
/// <summary>
/// Gets a value indicating if this is the first instance of the application
/// </summary>
public bool IsFirstInstance { get { return m_file != null; } }
/// <summary>
/// Constructs a new SingleInstance object
/// </summary>
/// <param name="appname">The application name</param>
public SingleInstance(string appname)
: this(appname, System.IO.Path.Combine(System.Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), appname))
{ }
/// <summary>
/// Constructs a new SingleInstance object
/// </summary>
/// <param name="appname">The application name</param>
/// <param name="basefolder">The folder in which the control file structure is placed</param>
public SingleInstance(string appname, string basefolder)
{
if (!System.IO.Directory.Exists(basefolder))
System.IO.Directory.CreateDirectory(basefolder);
m_controldir = System.IO.Path.Combine(basefolder, CONTROL_DIR);
if (!System.IO.Directory.Exists(m_controldir))
System.IO.Directory.CreateDirectory(m_controldir);
string lockfile = System.IO.Path.Combine(m_controldir, CONTROL_FILE);
m_file = null;
try
{
m_file = new System.IO.FileStream(lockfile, System.IO.FileMode.Create, System.IO.FileAccess.Write, System.IO.FileShare.None);
}
catch
{
}
//If we have write access
if (m_file != null)
{
m_filewatcher = new System.IO.FileSystemWatcher(m_controldir);
m_filewatcher.Created += new System.IO.FileSystemEventHandler(m_filewatcher_Created);
m_filewatcher.EnableRaisingEvents = true;
System.IO.StreamWriter sw = new System.IO.StreamWriter(m_file);
sw.WriteLine(System.Diagnostics.Process.GetCurrentProcess().Id);
sw.Flush();
//Do not dispose the SW, as that will close the file and release the lock
DateTime startup = System.IO.File.GetLastWriteTime(lockfile);
//Clean up any files that were created before the app launched
foreach(string s in System.IO.Directory.GetFiles(m_controldir))
if (s != lockfile && System.IO.File.GetCreationTime(s) < startup)
try { System.IO.File.Delete(s); }
catch { }
}
else
{
//Wait for the initial process to signal that the filewatcher is activated
int retrycount = 5;
while (retrycount > 0 && new System.IO.FileInfo(lockfile).Length == 0)
{
System.Threading.Thread.Sleep(500);
retrycount--;
}
//HACK: the unix file lock does not allow us to read the file length when the file is locked
if (new System.IO.FileInfo(lockfile).Length == 0)
if (!Library.Core.Utility.IsClientLinux)
throw new Exception("The file was locked, but had no data");
//Notify the other process that we have started
string filename = System.IO.Path.Combine(m_controldir, COMM_FILE_PREFIX + Guid.NewGuid().ToString());
//Write out the commandline arguments
string[] cmdargs = System.Environment.GetCommandLineArgs();
using (System.IO.FileStream fs = new System.IO.FileStream(filename, System.IO.FileMode.CreateNew, System.IO.FileAccess.Write, System.IO.FileShare.None))
using (System.IO.StreamWriter sw = new System.IO.StreamWriter(fs))
for (int i = 1; i < cmdargs.Length; i++) //Skip the first, as that is the filename
sw.WriteLine(cmdargs[i]);
//Wait for the other process to delete the file, indicating that it is processed
retrycount = 5;
while (retrycount > 0 && System.IO.File.Exists(filename))
{
System.Threading.Thread.Sleep(500);
retrycount--;
}
//This may happen if the other process is closing as we write the command
if (System.IO.File.Exists(filename))
{
//Try to clean up, so the other process does not spuriously show this
try { System.IO.File.Delete(filename); }
catch { }
throw new Exception("The lock file was locked, but the locking process did not respond to the start command");
}
}
}
/// <summary>
/// The event that is raised when a new file is created in the control dir
/// </summary>
/// <param name="sender">The event sender</param>
/// <param name="e">The file event arguments</param>
private void m_filewatcher_Created(object sender, System.IO.FileSystemEventArgs e)
{
//Retry 5 times if the other process is slow on releasing the file lock
int retrycount = 5;
//Indicator and holder of arguments passed
string[] commandline = null;
do
{
try
{
//If the other process deleted the file, just quit
if (!System.IO.File.Exists(e.FullPath))
return;
List<string> args = new List<string>();
using (System.IO.FileStream fs = new System.IO.FileStream(e.FullPath, System.IO.FileMode.Open, System.IO.FileAccess.Read, System.IO.FileShare.None))
using (System.IO.StreamReader sr = new System.IO.StreamReader(fs))
while(!sr.EndOfStream)
args.Add(sr.ReadLine());
commandline = args.ToArray();
//Remove the file to notify the other process that we have correctly processed the call
System.IO.File.Delete(e.FullPath);
}
catch
{
}
//If file-reading failed, wait a little before retry
if (commandline == null)
{
System.Threading.Thread.Sleep(500);
retrycount--;
}
} while (retrycount > 0 && commandline == null);
//If this happens, we detected the file, but was unable to read it's contents
if (commandline == null)
{
//There is nothing we can do :(
}
else
{
//If we read the data but did not delete the file, the other end still hangs
//and waits for us to clean up, so try again.
retrycount = 5;
while (retrycount > 0 && System.IO.File.Exists(e.FullPath))
{
try
{
System.IO.File.Delete(e.FullPath);
}
catch
{
//Wait before the retry
System.Threading.Thread.Sleep(500);
}
retrycount--;
}
//If this happens, the other process will give an error message
if (System.IO.File.Exists(e.FullPath))
{
//There is nothing we can do :(
}
//Finally inform this instance about the call
if (SecondInstanceDetected != null)
SecondInstanceDetected(commandline);
}
}
#region IDisposable Members
public void Dispose()
{
if (m_filewatcher != null)
{
m_filewatcher.EnableRaisingEvents = false;
m_filewatcher.Created -= new System.IO.FileSystemEventHandler(m_filewatcher_Created);
m_filewatcher.Dispose();
m_filewatcher = null;
}
if (m_file != null)
{
m_file.Dispose();
try { System.IO.File.Delete(m_file.Name); }
catch { }
m_file = null;
}
}
#endregion
}
}