duplicati/Duplicati/Server/SingleInstance.cs
Kenneth Skovhede 9464caf622
Feature/update license 2025 (#5851)
* Fixed some minor whitespace issues

* Updated all copyright to 2025
2025-01-07 09:40:39 +01:00

344 lines
14 KiB
C#

// Copyright (C) 2025, The Duplicati Team
// https://duplicati.com, hello@duplicati.com
//
// Permission is hereby granted, free of charge, to any person obtaining a
// copy of this software and associated documentation files (the "Software"),
// to deal in the Software without restriction, including without limitation
// the rights to use, copy, modify, merge, publish, distribute, sublicense,
// and/or sell copies of the Software, and to permit persons to whom the
// Software is furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
// DEALINGS IN THE SOFTWARE.
using System;
using System.Collections.Generic;
using Duplicati.Library.Common;
using Duplicati.Library.Common.IO;
namespace Duplicati.Server
{
/// <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>
/// An exception that can be thrown to indicate a second instance was running
/// </summary>
[Serializable]
public class MultipleInstanceException : Exception
{
/// <summary>
/// Constructs the new exception
/// </summary>
public MultipleInstanceException()
: base()
{
}
/// <summary>
/// Constructs the new exception
/// </summary>
/// <param name="message">The message</param>
public MultipleInstanceException(string message)
: base(message)
{
}
/// <summary>
/// Constructs the new exception
/// </summary>
/// <param name="message">The message</param>
/// <param name="innerException">The inner exception</param>
public MultipleInstanceException(string message, Exception innerException)
: base(message, innerException)
{
}
}
/// <summary>
/// The folder where control files are placed
/// </summary>
/// <remarks>
/// This directory is referenced in the common filters in FilterGroups.cs.
/// If it is ever changed, the filter should be updated as well.
/// </remarks>
private const string CONTROL_DIR = "control_dir_v2";
/// <summary>
/// The file that is locked by the first process
/// </summary>
private const string CONTROL_FILE = "lock_v2";
/// <summary>
/// The prefix on files that communicate with the first instance
/// </summary>
private const string COMM_FILE_PREFIX = "other_invocation_v2_";
/// <summary>
/// The delegate that is used to inform the first instance of the second invocation
/// </summary>
/// <param name="commandlineargs">The command-line arguments 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 IDisposable m_file;
/// <summary>
/// The folder where control files are placed
/// </summary>
private readonly string m_controldir;
/// <summary>
/// The full path to the locking file
/// </summary>
private readonly string m_lockfilename;
/// <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="basefolder">The folder in which the control file structure is placed</param>
///
public SingleInstance(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);
m_lockfilename = System.IO.Path.Combine(m_controldir, CONTROL_FILE);
m_file = null;
System.IO.Stream temp_fs = null;
try
{
if (OperatingSystem.IsMacOS() || OperatingSystem.IsLinux())
temp_fs = PosixFile.OpenExclusive(m_lockfilename, System.IO.FileAccess.Write);
else
temp_fs = System.IO.File.Open(m_lockfilename, System.IO.FileMode.Create, System.IO.FileAccess.Write, System.IO.FileShare.None);
if (temp_fs != null)
{
System.IO.StreamWriter sw = new System.IO.StreamWriter(temp_fs);
sw.WriteLine(System.Diagnostics.Process.GetCurrentProcess().Id);
sw.Flush();
//Do not dispose sw as that would dispose the stream
m_file = temp_fs;
}
}
catch
{
if (temp_fs != null)
try { temp_fs.Dispose(); }
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;
DateTime startup = System.IO.File.GetLastWriteTime(m_lockfilename);
//Clean up any files that were created before the app launched
foreach (string s in SystemIO.IO_OS.GetFiles(m_controldir))
if (s != m_lockfilename && 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(m_lockfilename).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(m_lockfilename).Length == 0)
if (!(OperatingSystem.IsMacOS() || OperatingSystem.IsLinux()))
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.StreamWriter sw = new System.IO.StreamWriter((OperatingSystem.IsMacOS() || OperatingSystem.IsLinux()) ? PosixFile.OpenExclusive(filename, System.IO.FileAccess.Write)
: new System.IO.FileStream(filename, System.IO.FileMode.CreateNew, System.IO.FileAccess.Write, System.IO.FileShare.None)))
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;
//HACK: Linux has some locking issues
//The problem is that there is no atomic open-and-lock operation, so the other process
// needs a little time to create+lock the file. This is not really a fix, but an
// ugly workaround. This functionality is only used to allow a new instance to signal
// the running instance, so errors here would only affect that functionality
if ((OperatingSystem.IsMacOS() || OperatingSystem.IsLinux()))
System.Threading.Thread.Sleep(1000);
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.StreamReader sr = new System.IO.StreamReader((OperatingSystem.IsMacOS() || OperatingSystem.IsLinux()) ? PosixFile.OpenExclusive(e.FullPath, System.IO.FileAccess.ReadWrite)
: new System.IO.FileStream(e.FullPath, System.IO.FileMode.Open, System.IO.FileAccess.Read, System.IO.FileShare.None)))
{
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_lockfilename); }
catch { }
m_file = null;
}
}
#endregion
}
}