using System; using System.Collections.Generic; using System.Linq; using System.Text; using Duplicati.Library.Localization.Short; namespace Duplicati.Server { public class Program { /// /// The path to the directory that contains the main executable /// public static readonly string StartupPath = Duplicati.Library.AutoUpdater.UpdaterManager.InstalledBaseDir; /// /// The name of the environment variable that holds the path to the data folder used by Duplicati /// public static readonly string DATAFOLDER_ENV_NAME = Duplicati.Library.AutoUpdater.AutoUpdateSettings.AppName.ToUpper() + "_HOME"; /// /// The environment variable that holdes the database key used to encrypt the SQLite database /// public static readonly string DB_KEY_ENV_NAME = Duplicati.Library.AutoUpdater.AutoUpdateSettings.AppName.ToUpper() + "_DB_KEY"; /// /// Gets the folder where Duplicati data is stored /// public static string DATAFOLDER { get { return Library.Utility.Utility.AppendDirSeparator(Environment.ExpandEnvironmentVariables("%" + DATAFOLDER_ENV_NAME + "%").TrimStart('"').TrimEnd('"')); } } /// /// The single instance /// public static SingleInstance Instance = null; /// /// A flag indicating if database encryption is in use /// public static bool UseDatabaseEncryption; /// /// This is the only access to the database /// public static Database.Connection DataConnection; /// /// This is the lock to be used before manipulating the shared resources /// public static object MainLock = new object(); /// /// This is the scheduling thread /// public static Scheduler Scheduler; /// /// This is the working thread /// public static Duplicati.Library.Utility.WorkerThread WorkThread; /// /// The thread running the ping-pong handler /// public static System.Threading.Thread PingPongThread; /// /// The path to the file that contains the current database /// public static string DatabasePath; /// /// The controller interface for pause/resume and throttle options /// public static LiveControls LiveControl; /// /// The application exit event /// public static System.Threading.ManualResetEvent ApplicationExitEvent; /// /// The webserver instance /// public static WebServer.Server WebServer; /// /// The update poll thread. /// public static UpdatePollThread UpdatePoller; /// /// An event that is set once the server is ready to respond to requests /// public static System.Threading.ManualResetEvent ServerStartedEvent = new System.Threading.ManualResetEvent(false); /// /// The status event signaler, used to controll long polling of status updates /// public static EventPollNotify StatusEventNotifyer = new EventPollNotify(); /// /// A delegate method for creating a copy of the current progress state /// public static Func GenerateProgressState; /// /// An event ID that increases whenever the database is updated /// public static long LastDataUpdateID = 0; /// /// The log redirect handler /// public static LogWriteHandler LogHandler = new LogWriteHandler(); public static int ServerPort { get { return WebServer.Port; } } public static bool IsFirstRun { get { return DataConnection.ApplicationSettings.IsFirstRun; } set { DataConnection.ApplicationSettings.IsFirstRun = value; } } public static bool ServerPortChanged { get { return DataConnection.ApplicationSettings.ServerPortChanged; } set { DataConnection.ApplicationSettings.ServerPortChanged = value; } } /// /// The main entry point for the application. /// [STAThread] public static int Main(string[] args) { return Duplicati.Library.AutoUpdater.UpdaterManager.RunFromMostRecent(typeof(Program).GetMethod("RealMain"), args, Duplicati.Library.AutoUpdater.AutoUpdateStrategy.Never); } public static void RealMain(string[] args) { //If we are on Windows, append the bundled "win-tools" programs to the search path //We add it last, to allow the user to override with other versions if (!Library.Utility.Utility.IsClientLinux) { Environment.SetEnvironmentVariable("PATH", Environment.GetEnvironmentVariable("PATH") + System.IO.Path.PathSeparator.ToString() + System.IO.Path.Combine( System.IO.Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly().Location), "win-tools") ); } //If this executable is invoked directly, write to console, otherwise throw exceptions bool writeConsole = System.Reflection.Assembly.GetEntryAssembly() == System.Reflection.Assembly.GetExecutingAssembly(); //If we are on windows we encrypt the database by default //We do not encrypt on Linux as most distros use a SQLite library without encryption support, //Linux users can use an encrypted home folder, or install a SQLite library with encryption support if (!Library.Utility.Utility.IsClientLinux && string.IsNullOrEmpty(Environment.GetEnvironmentVariable(DB_KEY_ENV_NAME))) { //Note that the password here is a default password and public knowledge // //The purpose of this is to prevent casual read of the database, as well // as protect from harddisk string scans, not to protect from determined // attacks. // //If you desire better security, start Duplicati once with the commandline option // --unencrypted-database to decrypt the database. //Then set the environment variable DUPLICATI_DB_KEY to the desired key, // and run Duplicati again without the --unencrypted-database option // to re-encrypt it with the new key // //If you change the key, please note that you need to supply the same // key when restoring the setup, as the setup being backed up will // be encrypted as well. Environment.SetEnvironmentVariable(DB_KEY_ENV_NAME, Library.AutoUpdater.AutoUpdateSettings.AppName + "_Key_42"); } //Find commandline options here for handling special startup cases Dictionary commandlineOptions = Duplicati.Library.Utility.CommandLineParser.ExtractOptions(new List(args)); foreach(string s in args) if ( s.Equals("help", StringComparison.InvariantCultureIgnoreCase) || s.Equals("/help", StringComparison.InvariantCultureIgnoreCase) || s.Equals("usage", StringComparison.InvariantCultureIgnoreCase) || s.Equals("/usage", StringComparison.InvariantCultureIgnoreCase)) commandlineOptions["help"] = ""; //If the commandline issues --help, just stop here if (commandlineOptions.ContainsKey("help")) { if (writeConsole) { Console.WriteLine(Strings.Program.HelpDisplayDialog); foreach(Library.Interface.ICommandLineArgument arg in SupportedCommands) Console.WriteLine(Strings.Program.HelpDisplayFormat, arg.Name, arg.LongDescription); return; } else { throw new Exception("Server invoked with --help"); } } #if DEBUG //Log various information in the logfile if (!commandlineOptions.ContainsKey("log-file")) { commandlineOptions["log-file"] = System.IO.Path.Combine(StartupPath, "Duplicati.debug.log"); commandlineOptions["log-level"] = Duplicati.Library.Logging.LogMessageType.Profiling.ToString(); } #endif //Set the %DUPLICATI_HOME% env variable, if it is not already set if (string.IsNullOrEmpty(Environment.GetEnvironmentVariable(DATAFOLDER_ENV_NAME))) { #if DEBUG //debug mode uses a lock file located in the app folder Environment.SetEnvironmentVariable(DATAFOLDER_ENV_NAME, StartupPath); #else bool portableMode = commandlineOptions.ContainsKey("portable-mode") ? Library.Utility.Utility.ParseBool(commandlineOptions["portable-mode"], true) : false; if (portableMode) { //Portable mode uses a data folder in the application home dir Environment.SetEnvironmentVariable(DATAFOLDER_ENV_NAME, System.IO.Path.Combine(StartupPath, "data")); } else { //Normal release mode uses the systems "Application Data" folder Environment.SetEnvironmentVariable(DATAFOLDER_ENV_NAME, System.IO.Path.Combine(System.Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), Library.AutoUpdater.AutoUpdateSettings.AppName)); } #endif } try { try { //This will also create Program.DATAFOLDER if it does not exist Instance = new SingleInstance(Duplicati.Library.AutoUpdater.AutoUpdateSettings.AppName, Program.DATAFOLDER); } catch (Exception ex) { if (writeConsole) { Console.WriteLine(Strings.Program.StartupFailure, ex.ToString()); return; } else { throw new Exception(Strings.Program.StartupFailure, ex); } } if (!Instance.IsFirstInstance) { if (writeConsole) { Console.WriteLine(Strings.Program.AnotherInstanceDetected); return; } else { throw new SingleInstance.MultipleInstanceException(Strings.Program.AnotherInstanceDetected); } } // Setup the log redirect Duplicati.Library.Logging.Log.CurrentLog = Program.LogHandler; if (commandlineOptions.ContainsKey("log-file")) { if (System.IO.File.Exists(commandlineOptions["log-file"])) System.IO.File.Delete(commandlineOptions["log-file"]); var loglevel = Duplicati.Library.Logging.LogMessageType.Error; if (commandlineOptions.ContainsKey("log-level")) Enum.TryParse(commandlineOptions["log-level"], true, out loglevel); Program.LogHandler.SetServerFile(commandlineOptions["log-file"], loglevel); } Version sqliteVersion = new Version((string)Duplicati.Library.SQLiteHelper.SQLiteLoader.SQLiteConnectionType.GetProperty("SQLiteVersion").GetValue(null, null)); if (sqliteVersion < new Version(3, 6, 3)) { if (writeConsole) { //The official Mono SQLite provider is also broken with less than 3.6.3 Console.WriteLine(Strings.Program.WrongSQLiteVersion, sqliteVersion, "3.6.3"); return; } else { throw new Exception(string.Format(Strings.Program.WrongSQLiteVersion, sqliteVersion, "3.6.3")); } } //Create the connection instance System.Data.IDbConnection con = (System.Data.IDbConnection)Activator.CreateInstance(Duplicati.Library.SQLiteHelper.SQLiteLoader.SQLiteConnectionType); try { DatabasePath = System.IO.Path.Combine(Program.DATAFOLDER, "Duplicati-server.sqlite"); if (!System.IO.Directory.Exists(System.IO.Path.GetDirectoryName(DatabasePath))) System.IO.Directory.CreateDirectory(System.IO.Path.GetDirectoryName(DatabasePath)); #if DEBUG //Default is to not use encryption for debugging Program.UseDatabaseEncryption = commandlineOptions.ContainsKey("unencrypted-database") ? !Library.Utility.Utility.ParseBool(commandlineOptions["unencrypted-database"], true) : false; #else Program.UseDatabaseEncryption = commandlineOptions.ContainsKey("unencrypted-database") ? !Library.Utility.Utility.ParseBool(commandlineOptions["unencrypted-database"], true) : true; #endif con.ConnectionString = "Data Source=" + DatabasePath; //Attempt to open the database, handling any encryption present OpenDatabase(con); Duplicati.Library.SQLiteHelper.DatabaseUpgrader.UpgradeDatabase(con, DatabasePath, typeof(Duplicati.Server.Database.Connection)); } catch (Exception ex) { //Unwrap the reflection exceptions if (ex is System.Reflection.TargetInvocationException && ex.InnerException != null) ex = ex.InnerException; if (writeConsole) { Console.WriteLine(Strings.Program.DatabaseOpenError, ex.Message); return; } else { throw new Exception(string.Format(Strings.Program.DatabaseOpenError, ex.Message), ex); } } DataConnection = new Duplicati.Server.Database.Connection(con); if (commandlineOptions.ContainsKey("webservice-password")) Program.DataConnection.ApplicationSettings.SetWebserverPassword(commandlineOptions["webservice-password"]); ApplicationExitEvent = new System.Threading.ManualResetEvent(false); Duplicati.Library.AutoUpdater.UpdaterManager.OnError += (Exception obj) => { Program.DataConnection.LogError(null, "Error in updater", obj); }; UpdatePoller = new UpdatePollThread(); LiveControl = new LiveControls(DataConnection.ApplicationSettings); LiveControl.StateChanged += new EventHandler(LiveControl_StateChanged); LiveControl.ThreadPriorityChanged += new EventHandler(LiveControl_ThreadPriorityChanged); LiveControl.ThrottleSpeedChanged += new EventHandler(LiveControl_ThrottleSpeedChanged); Program.WorkThread = new Duplicati.Library.Utility.WorkerThread((x) => { Runner.Run(x, true); }, LiveControl.State == LiveControls.LiveControlState.Paused); Program.Scheduler = new Scheduler(WorkThread); Program.WorkThread.StartingWork += new EventHandler(SignalNewEvent); Program.WorkThread.CompletedWork += new EventHandler(SignalNewEvent); Program.WorkThread.WorkQueueChanged += new EventHandler(SignalNewEvent); Program.Scheduler.NewSchedule += new EventHandler(SignalNewEvent); Program.WebServer = new WebServer.Server(commandlineOptions); if (Program.WebServer.Port != DataConnection.ApplicationSettings.LastWebserverPort) ServerPortChanged = true; DataConnection.ApplicationSettings.LastWebserverPort = Program.WebServer.Port; if (Library.Utility.Utility.ParseBoolOption(commandlineOptions, "ping-pong-keepalive")) { Program.PingPongThread = new System.Threading.Thread(PingPongMethod); Program.PingPongThread.IsBackground = true; Program.PingPongThread.Start(); } ServerStartedEvent.Set(); ApplicationExitEvent.WaitOne(); } catch (SingleInstance.MultipleInstanceException mex) { System.Diagnostics.Trace.WriteLine(string.Format(Strings.Program.SeriousError, mex.ToString())); if (writeConsole) Console.WriteLine(Strings.Program.SeriousError, mex.ToString()); else throw mex; } catch (Exception ex) { System.Diagnostics.Trace.WriteLine(string.Format(Strings.Program.SeriousError, ex.ToString())); if (writeConsole) Console.WriteLine(Strings.Program.SeriousError, ex.ToString()); else throw new Exception(string.Format(Strings.Program.SeriousError, ex.ToString()), ex); } finally { StatusEventNotifyer.SignalNewEvent(); if (UpdatePoller != null) UpdatePoller.Terminate(); if (Scheduler != null) Scheduler.Terminate(true); if (WorkThread != null) WorkThread.Terminate(true); if (Instance != null) Instance.Dispose(); if (PingPongThread != null) try { PingPongThread.Abort(); } catch { } if (LogHandler != null) LogHandler.Dispose(); } } private static void SignalNewEvent(object sender, EventArgs e) { StatusEventNotifyer.SignalNewEvent(); } /// /// Handles a change in the LiveControl and updates the Runner /// /// /// private static void LiveControl_ThreadPriorityChanged(object sender, EventArgs e) { StatusEventNotifyer.SignalNewEvent(); } /// /// Handles a change in the LiveControl and updates the Runner /// /// /// private static void LiveControl_ThrottleSpeedChanged(object sender, EventArgs e) { StatusEventNotifyer.SignalNewEvent(); } /// /// This event handler updates the trayicon menu with the current state of the runner. /// static void LiveControl_StateChanged(object sender, EventArgs e) { switch (LiveControl.State) { case LiveControls.LiveControlState.Paused: { WorkThread.Pause(); var t = WorkThread.CurrentTask; if (t != null) t.Pause(); break; } case LiveControls.LiveControlState.Running: { WorkThread.Resume(); var t = WorkThread.CurrentTask; if (t != null) t.Resume(); break; } } StatusEventNotifyer.SignalNewEvent(); } /// /// Returns a localized name for a task type /// /// /// public static string LocalizeTaskType(Duplicati.Server.Serialization.DuplicatiOperation type) { switch (type) { case Duplicati.Server.Serialization.DuplicatiOperation.Backup: return Strings.TaskType.FullBackup; case Duplicati.Server.Serialization.DuplicatiOperation.List: return Strings.TaskType.IncrementalBackup; case Duplicati.Server.Serialization.DuplicatiOperation.Remove: return Strings.TaskType.ListActualFiles; case Duplicati.Server.Serialization.DuplicatiOperation.Verify: return Strings.TaskType.ListBackupEntries; case Duplicati.Server.Serialization.DuplicatiOperation.Restore: return Strings.TaskType.ListBackups; default: return type.ToString(); } } /// /// Helper method with logic to handle opening a database in possibly encrypted format /// /// The SQLite connection object internal static void OpenDatabase(System.Data.IDbConnection con) { bool noEncryption = !Program.UseDatabaseEncryption; string password = Environment.GetEnvironmentVariable(DB_KEY_ENV_NAME); System.Reflection.MethodInfo setPwdMethod = con.GetType().GetMethod("SetPassword", new Type[] { typeof(string) }); string attemptedPassword; if (noEncryption || string.IsNullOrEmpty(password)) attemptedPassword = null; //No encryption specified, attempt to open without else attemptedPassword = password; //Encryption specified, attempt to open with if (setPwdMethod != null) setPwdMethod.Invoke(con, new object[] { attemptedPassword }); try { //Attempt to open in preferred state con.Open(); // Do a dummy query to make sure we have a working db using (var cmd = con.CreateCommand()) { cmd.CommandText = "SELECT COUNT(*) FROM SQLITE_MASTER"; cmd.ExecuteScalar(); } } catch { try { //We can't try anything else without a password if (string.IsNullOrEmpty(password)) throw; //Open failed, now try the reverse if (attemptedPassword == null) attemptedPassword = password; else attemptedPassword = null; con.Close(); setPwdMethod.Invoke(con, new object[] { attemptedPassword }); con.Open(); // Do a dummy query to make sure we have a working db using (var cmd = con.CreateCommand()) { cmd.CommandText = "SELECT COUNT(*) FROM SQLITE_MASTER"; cmd.ExecuteScalar(); } } catch { try { con.Close(); } catch { } } //If the db is not open now, it won't open if (con.State != System.Data.ConnectionState.Open) throw; //Report original error //The open method succeeded with the non-default method, now change the password System.Reflection.MethodInfo changePwdMethod = con.GetType().GetMethod("ChangePassword", new Type[] { typeof(string) }); changePwdMethod.Invoke(con, new object[] { noEncryption ? null : password }); } } private static void PingPongMethod() { var rd = new System.IO.StreamReader(Console.OpenStandardInput()); var wr = new System.IO.StreamWriter(Console.OpenStandardOutput()); while (rd.ReadLine() != null) { wr.WriteLine("pong"); wr.Flush(); } } /// /// Gets a list of all supported commandline options /// public static Library.Interface.ICommandLineArgument[] SupportedCommands { get { return new Duplicati.Library.Interface.ICommandLineArgument[] { new Duplicati.Library.Interface.CommandLineArgument("help", Duplicati.Library.Interface.CommandLineArgument.ArgumentType.Boolean, Strings.Program.HelpCommandDescription, Strings.Program.HelpCommandDescription), new Duplicati.Library.Interface.CommandLineArgument("unencrypted-database", Duplicati.Library.Interface.CommandLineArgument.ArgumentType.Boolean, Strings.Program.UnencrypteddatabaseCommandDescription, Strings.Program.UnencrypteddatabaseCommandDescription), new Duplicati.Library.Interface.CommandLineArgument("portable-mode", Duplicati.Library.Interface.CommandLineArgument.ArgumentType.Boolean, Strings.Program.PortablemodeCommandDescription, Strings.Program.PortablemodeCommandDescription), new Duplicati.Library.Interface.CommandLineArgument("log-file", Duplicati.Library.Interface.CommandLineArgument.ArgumentType.Path, Strings.Program.LogfileCommandDescription, Strings.Program.LogfileCommandDescription), new Duplicati.Library.Interface.CommandLineArgument("log-level", Duplicati.Library.Interface.CommandLineArgument.ArgumentType.Enumeration, Strings.Program.LoglevelCommandDescription, Strings.Program.LoglevelCommandDescription, "Warning", null, Enum.GetNames(typeof(Duplicati.Library.Logging.LogMessageType))), new Duplicati.Library.Interface.CommandLineArgument(Duplicati.Server.WebServer.Server.OPTION_WEBROOT, Duplicati.Library.Interface.CommandLineArgument.ArgumentType.Path, Strings.Program.WebserverWebrootDescription, Strings.Program.WebserverWebrootDescription, Duplicati.Server.WebServer.Server.DEFAULT_OPTION_WEBROOT), new Duplicati.Library.Interface.CommandLineArgument(Duplicati.Server.WebServer.Server.OPTION_PORT, Duplicati.Library.Interface.CommandLineArgument.ArgumentType.String, Strings.Program.WebserverPortDescription, Strings.Program.WebserverPortDescription, Duplicati.Server.WebServer.Server.DEFAULT_OPTION_PORT.ToString()), new Duplicati.Library.Interface.CommandLineArgument(Duplicati.Server.WebServer.Server.OPTION_INTERFACE, Duplicati.Library.Interface.CommandLineArgument.ArgumentType.String, Strings.Program.WebserverInterfaceDescription, Strings.Program.WebserverInterfaceDescription, Duplicati.Server.WebServer.Server.DEFAULT_OPTION_INTERFACE), new Duplicati.Library.Interface.CommandLineArgument("webservice-password", Duplicati.Library.Interface.CommandLineArgument.ArgumentType.Password, Strings.Program.WebserverPasswordDescription, Strings.Program.WebserverPasswordDescription), new Duplicati.Library.Interface.CommandLineArgument("ping-pong-keepalive", Duplicati.Library.Interface.CommandLineArgument.ArgumentType.Boolean, LC.L("Enables the ping-pong responder"), LC.L("When running as a server, the service daemon must verify that the process is responding. If this option is enabled, the server reads stdin and writes a reply to each line read")), }; } } } }