From 494fccd9316da42c0e65ad8527902ced4f7733af Mon Sep 17 00:00:00 2001 From: Kenneth Skovhede Date: Tue, 14 Aug 2018 11:09:16 +0200 Subject: [PATCH 1/3] Added support for multiple exit codes, and added a unittest to verify that the exit codes are handled correctly --- .../Library/Interface/CustomExceptions.cs | 45 ++++- Duplicati/Library/Main/Controller.cs | 25 ++- .../Library/Modules/Builtin/RunScript.cs | 116 ++++++----- Duplicati/Library/Modules/Builtin/Strings.cs | 1 + .../Modules/Builtin/run-script-example.bat | 13 ++ .../Modules/Builtin/run-script-example.sh | 12 ++ Duplicati/UnitTest/Duplicati.UnitTest.csproj | 1 + Duplicati/UnitTest/RunScriptTests.cs | 187 ++++++++++++++++++ 8 files changed, 350 insertions(+), 50 deletions(-) create mode 100644 Duplicati/UnitTest/RunScriptTests.cs diff --git a/Duplicati/Library/Interface/CustomExceptions.cs b/Duplicati/Library/Interface/CustomExceptions.cs index 1d0cde056..2e0c90262 100644 --- a/Duplicati/Library/Interface/CustomExceptions.cs +++ b/Duplicati/Library/Interface/CustomExceptions.cs @@ -139,5 +139,48 @@ namespace Duplicati.Library.Interface public CancelException(string message, Exception innerException) : base(message, "Cancelled", innerException) { } -} + } + + /// + /// The reason why an operation is aborted + /// + public enum OperationAbortReason + { + /// + /// The operation is aborted, but this is considered a normal operation + /// + Normal, + /// + /// The operation is aborted and this should give a warning + /// + Warning, + /// + /// The operation is aborted and this is an error + /// + Error + } + + /// + /// A class that signals the operation should be aborted + /// + [Serializable] + public class OperationAbortException : UserInformationException + { + /// + /// The reason for the abort operation + /// + public readonly OperationAbortReason AbortReason; + + public OperationAbortException(OperationAbortReason reason, string message) + : base(message, "OperationAborted") + { + AbortReason = reason; + } + + public OperationAbortException(OperationAbortReason reason, string message, Exception innerException) + : base(message, "OperationAborted", innerException) + { + AbortReason = reason; + } + } } diff --git a/Duplicati/Library/Main/Controller.cs b/Duplicati/Library/Main/Controller.cs index a71a3c8d0..b744259a8 100644 --- a/Duplicati/Library/Main/Controller.cs +++ b/Duplicati/Library/Main/Controller.cs @@ -430,14 +430,29 @@ namespace Duplicati.Library.Main { result.EndTime = DateTime.UtcNow; - try { (result as BasicResults).OperationProgressUpdater.UpdatePhase(OperationPhase.Error); } - catch { } + if (ex is Library.Interface.OperationAbortException oae) + { + // Perform the module shutdown + OnOperationComplete(ex); - OnOperationComplete(ex); + // Log this as a normal operation, as the script rasing the exception, + // has already populated either warning or log messages as required + Logging.Log.WriteInformationMessage(LOGTAG, "AbortOperation", "Aborting operation by request, requested result: {0}", oae.AbortReason); - Logging.Log.WriteErrorMessage(LOGTAG, "FailedOperation", ex, Strings.Controller.FailedOperationMessage(m_options.MainAction, ex.Message)); + return result; + } + else + { + try { (result as BasicResults).OperationProgressUpdater.UpdatePhase(OperationPhase.Error); } + catch { } + + OnOperationComplete(ex); + + Logging.Log.WriteErrorMessage(LOGTAG, "FailedOperation", ex, Strings.Controller.FailedOperationMessage(m_options.MainAction, ex.Message)); + + throw; + } - throw; } finally { diff --git a/Duplicati/Library/Modules/Builtin/RunScript.cs b/Duplicati/Library/Modules/Builtin/RunScript.cs index c1897639a..7ab0ffaf7 100644 --- a/Duplicati/Library/Modules/Builtin/RunScript.cs +++ b/Duplicati/Library/Modules/Builtin/RunScript.cs @@ -24,6 +24,7 @@ using Duplicati.Library.Utility; using Duplicati.Library.Interface; using System.Linq; using Duplicati.Library.Modules.Builtin.ResultSerialization; +using System.Threading.Tasks; namespace Duplicati.Library.Modules.Builtin { @@ -220,7 +221,7 @@ namespace Duplicati.Library.Modules.Builtin psi.EnvironmentVariables["DUPLICATI__REMOTEURL"] = remoteurl; if (level != null) psi.EnvironmentVariables["DUPLICATI__PARSED_RESULT"] = level.Value.ToString(); - + if (localpath != null) psi.EnvironmentVariables["DUPLICATI__LOCALPATH"] = string.Join(System.IO.Path.PathSeparator.ToString(), localpath); @@ -230,9 +231,9 @@ namespace Duplicati.Library.Modules.Builtin if (!string.IsNullOrEmpty(datafile)) psi.EnvironmentVariables["DUPLICATI__RESULTFILE"] = datafile; - using(System.Diagnostics.Process p = System.Diagnostics.Process.Start(psi)) + using (System.Diagnostics.Process p = System.Diagnostics.Process.Start(psi)) { - ConsoleDataHandler cs = new ConsoleDataHandler(p); + var cs = new ConsoleDataHandler(p); if (timeout <= 0) p.WaitForExit(); @@ -249,14 +250,57 @@ namespace Duplicati.Library.Modules.Builtin if (p.HasExited) { + cs.WaitForCompletion(); + stderr = cs.StandardError; stdout = cs.StandardOutput; if (p.ExitCode != 0) - Logging.Log.WriteWarningMessage(LOGTAG, "InvalidExitCode", null, Strings.RunScript.InvalidExitCodeError(scriptpath, p.ExitCode)); + { + if (!requiredScript) + { + // We log a warning or an error depending on the exit code + switch (p.ExitCode) + { + case 0: + case 1: + // No messages here + break; + + case 2: + case 3: + Logging.Log.WriteWarningMessage(LOGTAG, "InvalidExitCode", null, Strings.RunScript.ExitCodeError(scriptpath, p.ExitCode, stderr)); + stderr = null; + break; + + case 4: + case 5: + default: + Logging.Log.WriteErrorMessage(LOGTAG, "InvalidExitCode", null, Strings.RunScript.ExitCodeError(scriptpath, p.ExitCode, stderr)); + stderr = null; + break; + } + + // If this is the start event, we abort the backup + if (eventname == "BEFORE") + { + switch (p.ExitCode) + { + case 1: + throw new OperationAbortException(OperationAbortReason.Normal, Strings.RunScript.InvalidExitCodeError(scriptpath, p.ExitCode)); + case 3: + throw new OperationAbortException(OperationAbortReason.Warning, Strings.RunScript.InvalidExitCodeError(scriptpath, p.ExitCode)); + case 5: + throw new OperationAbortException(OperationAbortReason.Error, Strings.RunScript.InvalidExitCodeError(scriptpath, p.ExitCode)); + } + } + } + else + Logging.Log.WriteWarningMessage(LOGTAG, "InvalidExitCode", null, Strings.RunScript.InvalidExitCodeError(scriptpath, p.ExitCode)); + } } else { - Logging.Log.WriteWarningMessage(LOGTAG, "ScriptTimeout", null, Strings.RunScript.ScriptTimeoutError(scriptpath)); + Logging.Log.WriteWarningMessage(LOGTAG, "ScriptTimeout", null, Strings.RunScript.ScriptTimeoutError(scriptpath)); } } @@ -266,7 +310,7 @@ namespace Duplicati.Library.Modules.Builtin //We only allow setting parameters on startup if (eventname == "BEFORE" && stdout != null) { - foreach(string rawline in stdout.Split(new string[] { Environment.NewLine }, StringSplitOptions.RemoveEmptyEntries)) + foreach (string rawline in stdout.Split(new string[] { Environment.NewLine }, StringSplitOptions.RemoveEmptyEntries)) { string line = rawline.Trim(); if (!line.StartsWith("--", StringComparison.Ordinal)) @@ -303,7 +347,7 @@ namespace Duplicati.Library.Modules.Builtin localpath = value.Split(System.IO.Path.PathSeparator); } else if ( - string.Equals(key, "eventname", StringComparison.OrdinalIgnoreCase) || + string.Equals(key, "eventname", StringComparison.OrdinalIgnoreCase) || string.Equals(key, "operationname", StringComparison.OrdinalIgnoreCase) || string.Equals(key, "main-action", StringComparison.OrdinalIgnoreCase) || key == "" @@ -313,10 +357,14 @@ namespace Duplicati.Library.Modules.Builtin } else options[key] = value; - } } } + catch (OperationAbortException) + { + // Do not log this, it is already logged + throw; + } catch (Exception ex) { Logging.Log.WriteWarningMessage(LOGTAG, "ScriptExecuteError", ex, Strings.RunScript.ScriptExecuteError(scriptpath, ex.Message)); @@ -325,49 +373,29 @@ namespace Duplicati.Library.Modules.Builtin } } + /// + /// Helper class to extract output from the program + /// private class ConsoleDataHandler { public ConsoleDataHandler(System.Diagnostics.Process p) { - p.OutputDataReceived += new System.Diagnostics.DataReceivedEventHandler(HandleOutputDataReceived); - p.ErrorDataReceived += new System.Diagnostics.DataReceivedEventHandler(HandleErrorDataReceived); - - p.BeginErrorReadLine(); - p.BeginOutputReadLine(); + m_task = Task.WhenAll( + Task.Run(async () => StandardOutput = await p.StandardOutput.ReadToEndAsync()), + Task.Run(async () => StandardError = await p.StandardError.ReadToEndAsync()) + ); } - private readonly StringBuilder m_standardOutput = new StringBuilder(); - private readonly StringBuilder m_standardError = new StringBuilder(); - private readonly object m_lock = new object(); - - private void HandleOutputDataReceived (object sender, System.Diagnostics.DataReceivedEventArgs e) + private readonly Task m_task; + public string StandardOutput { get; private set; } + public string StandardError { get; private set; } + public void WaitForCompletion() { - lock(m_lock) - m_standardOutput.AppendLine(e.Data); - } - - private void HandleErrorDataReceived (object sender, System.Diagnostics.DataReceivedEventArgs e) - { - lock(m_lock) - m_standardError.AppendLine(e.Data); - } - - public string StandardOutput - { - get - { - lock(m_lock) - return m_standardOutput.ToString().Trim(); - } - } - - public string StandardError - { - get - { - lock(m_lock) - return m_standardError.ToString().Trim(); - } + // NOTE: This is ugly, but there is a race where "HasExited" is set, + // but the stdout/stderr streams have not yet completed. + // If we wait a little here, we eventually get the data. + // If the streams have completed we do not wait. + m_task.Wait(TimeSpan.FromSeconds(5)); } } } diff --git a/Duplicati/Library/Modules/Builtin/Strings.cs b/Duplicati/Library/Modules/Builtin/Strings.cs index bc8852354..78946a4e0 100644 --- a/Duplicati/Library/Modules/Builtin/Strings.cs +++ b/Duplicati/Library/Modules/Builtin/Strings.cs @@ -54,6 +54,7 @@ namespace Duplicati.Library.Modules.Builtin.Strings { public static string FinishoptionLong { get { return LC.L(@"Executes a script after performing an operation. The script will receive the operation results written to stdout."); } } public static string FinishoptionShort { get { return LC.L(@"Run a script on exit"); } } public static string InvalidExitCodeError(string script, int exitcode) { return LC.L(@"The script ""{0}"" returned with exit code {1}", script, exitcode); } + public static string ExitCodeError(string script, int exitcode, string message) { return LC.L(@"The script ""{0}"" returned with exit code {1}{2}", script, exitcode, string.IsNullOrWhiteSpace(message) ? string.Empty : string.Format(": {0}", message)); } public static string RequiredoptionLong { get { return LC.L(@"Executes a script before performing an operation. The operation will block until the script has completed or timed out. If the script returns a non-zero error code or times out, the operation will be aborted."); } } public static string RequiredoptionShort { get { return LC.L(@"Run a required script on startup"); } } public static string ResultFormatShort { get { return LC.L(@"Selects the output format for results"); } } diff --git a/Duplicati/Library/Modules/Builtin/run-script-example.bat b/Duplicati/Library/Modules/Builtin/run-script-example.bat index 123c1bbe9..8ea3fd418 100644 --- a/Duplicati/Library/Modules/Builtin/run-script-example.bat +++ b/Duplicati/Library/Modules/Builtin/run-script-example.bat @@ -16,6 +16,15 @@ REM --run-script-before = REM Duplicati will run the script before the backup job and waits for its REM completion for 60 seconds (default timeout value). After a timeout a REM warning is logged and the backup is started. +REM The following exit codes are supported: +REM +REM - 0: OK, run operation +REM - 1: OK, don't run operation +REM - 2: Warning, run operation +REM - 3: Warning, don't run operation +REM - 4: Error, run operation +REM - 5: Error don't run operation +REM - other: Error don't run operation REM REM --run-script-before-required = REM Duplicati will run the script before the backup job and wait for its @@ -32,6 +41,10 @@ REM --run-script-after = REM Duplicati will run the script after the backup job and wait for its REM completion for 60 seconds (default timeout value). After a timeout a REM warning is logged. +REM The same exit codes as in --run-script-before are supported, but +REM the operation will always continue (i.e. 1 => 0, 3 => 2, 5 => 4) +REM as it has already completed so stopping it during stop is useless. + REM ############################################################################### diff --git a/Duplicati/Library/Modules/Builtin/run-script-example.sh b/Duplicati/Library/Modules/Builtin/run-script-example.sh index 150ac0d61..973dd96f4 100644 --- a/Duplicati/Library/Modules/Builtin/run-script-example.sh +++ b/Duplicati/Library/Modules/Builtin/run-script-example.sh @@ -16,6 +16,15 @@ # Duplicati will run the script before the backup job and waits for its # completion for 60 seconds (default timeout value). After a timeout a # warning is logged and the backup is started. +# The following exit codes are supported: +# +# - 0: OK, run operation +# - 1: OK, don't run operation +# - 2: Warning, run operation +# - 3: Warning, don't run operation +# - 4: Error, run operation +# - 5: Error don't run operation +# - other: Error don't run operation # # --run-script-before-required = # Duplicati will run the script before the backup job and wait for its @@ -32,6 +41,9 @@ # Duplicati will run the script after the backup job and wait for its # completion for 60 seconds (default timeout value). After a timeout a # warning is logged. +# The same exit codes as in --run-script-before are supported, but +# the operation will always continue (i.e. 1 => 0, 3 => 2, 5 => 4) +# as it has already completed so stopping it during stop is useless. ############################################################################### diff --git a/Duplicati/UnitTest/Duplicati.UnitTest.csproj b/Duplicati/UnitTest/Duplicati.UnitTest.csproj index 8ac832014..936e556f0 100644 --- a/Duplicati/UnitTest/Duplicati.UnitTest.csproj +++ b/Duplicati/UnitTest/Duplicati.UnitTest.csproj @@ -52,6 +52,7 @@ + diff --git a/Duplicati/UnitTest/RunScriptTests.cs b/Duplicati/UnitTest/RunScriptTests.cs new file mode 100644 index 000000000..fe5396340 --- /dev/null +++ b/Duplicati/UnitTest/RunScriptTests.cs @@ -0,0 +1,187 @@ +// Copyright (C) 2018, The Duplicati Team +// http://www.duplicati.com, info@duplicati.com +// +// 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using Duplicati.Library.Interface; +using NUnit.Framework; + +namespace Duplicati.UnitTest +{ + public class RunScriptTests : BasicSetupHelper + { + public override void PrepareSourceData() + { + base.PrepareSourceData(); + + Directory.CreateDirectory(DATAFOLDER); + Directory.CreateDirectory(TARGETFOLDER); + } + + [Test] + [Category("Border")] + public void RunScriptBefore() + { + PrepareSourceData(); + + var blocksize = 10 * 1024; + var options = TestOptions; + options["blocksize"] = blocksize.ToString() + "b"; + options["run-script-timeout"] = "5s"; + + BorderTests.WriteTestFilesToFolder(DATAFOLDER, blocksize, 0); + + using (var c = new Library.Main.Controller("file://" + TARGETFOLDER, options, null)) + { + var res = c.Backup(new string[] { DATAFOLDER }); + if (res.ParsedResult != ParsedResultType.Success) + throw new Exception("Unexpected result from base backup"); + + options["run-script-before"] = CreateScript(0); + res = c.Backup(new string[] { DATAFOLDER }); + if (res.ParsedResult != ParsedResultType.Success) + throw new Exception("Unexpected result from backup with return code 0"); + if (res.ExaminedFiles <= 0) + throw new Exception("Backup did not examine any files for code 0?"); + + options["run-script-before"] = CreateScript(1); + res = c.Backup(new string[] { DATAFOLDER }); + if (res.ParsedResult != ParsedResultType.Success) + throw new Exception("Unexpected result from backup with return code 1"); + if (res.ExaminedFiles > 0) + throw new Exception("Backup did examine files for code 1?"); + + options["run-script-before"] = CreateScript(2); + res = c.Backup(new string[] { DATAFOLDER }); + if (res.ParsedResult != ParsedResultType.Warning) + throw new Exception("Unexpected result from backup with return code 2"); + if (res.ExaminedFiles <= 0) + throw new Exception("Backup did not examine any files for code 2?"); + + options["run-script-before"] = CreateScript(3); + res = c.Backup(new string[] { DATAFOLDER }); + if (res.ParsedResult != ParsedResultType.Warning) + throw new Exception("Unexpected result from backup with return code 3"); + if (res.ExaminedFiles > 0) + throw new Exception("Backup did examine files for code 3?"); + + options["run-script-before"] = CreateScript(4); + res = c.Backup(new string[] { DATAFOLDER }); + if (res.ParsedResult != ParsedResultType.Error) + throw new Exception("Unexpected result from backup with return code 4"); + if (res.ExaminedFiles <= 0) + throw new Exception("Backup did not examine any files for code 4?"); + + options["run-script-before"] = CreateScript(5); + res = c.Backup(new string[] { DATAFOLDER }); + if (res.ParsedResult != ParsedResultType.Error) + throw new Exception("Unexpected result from backup with return code 5"); + if (res.ExaminedFiles > 0) + throw new Exception("Backup did examine files for code 5?"); + + + + options["run-script-before"] = CreateScript(2, "TEST WARNING MESSAGE"); + res = c.Backup(new string[] { DATAFOLDER }); + if (res.ParsedResult != ParsedResultType.Warning) + throw new Exception("Unexpected result from backup with return code 2"); + if (res.ExaminedFiles <= 0) + throw new Exception("Backup did examine files for code 2?"); + if (!res.Warnings.Any(x => x.IndexOf("TEST WARNING MESSAGE", StringComparison.Ordinal) >= 0)) + throw new Exception("Found no warning message in output for code 2"); + + options["run-script-before"] = CreateScript(3, "TEST WARNING MESSAGE"); + res = c.Backup(new string[] { DATAFOLDER }); + if (res.ParsedResult != ParsedResultType.Warning) + throw new Exception("Unexpected result from backup with return code 3"); + if (res.ExaminedFiles > 0) + throw new Exception("Backup did examine files for code 3?"); + if (!res.Warnings.Any(x => x.IndexOf("TEST WARNING MESSAGE", StringComparison.Ordinal) >= 0)) + throw new Exception("Found no warning message in output for code 3"); + + options["run-script-before"] = CreateScript(4, "TEST ERROR MESSAGE"); + res = c.Backup(new string[] { DATAFOLDER }); + if (res.ParsedResult != ParsedResultType.Error) + throw new Exception("Unexpected result from backup with return code 4"); + if (res.ExaminedFiles <= 0) + throw new Exception("Backup did examine files for code 4?"); + if (!res.Errors.Any(x => x.IndexOf("TEST ERROR MESSAGE", StringComparison.Ordinal) >= 0)) + throw new Exception("Found no error message in output for code 4"); + + options["run-script-before"] = CreateScript(5, "TEST ERROR MESSAGE"); + res = c.Backup(new string[] { DATAFOLDER }); + if (res.ParsedResult != ParsedResultType.Error) + throw new Exception("Unexpected result from backup with return code 5"); + if (res.ExaminedFiles > 0) + throw new Exception("Backup did examine files for code 5?"); + if (!res.Errors.Any(x => x.IndexOf("TEST ERROR MESSAGE", StringComparison.Ordinal) >= 0)) + throw new Exception("Found no error message in output for code 5"); + + options["run-script-before"] = CreateScript(0, sleeptime: 10); + res = c.Backup(new string[] { DATAFOLDER }); + if (res.ParsedResult != ParsedResultType.Warning) + throw new Exception("Unexpected result from backup with timeout script"); + if (res.ExaminedFiles <= 0) + throw new Exception("Backup did not examine any files after timeout?"); + } + } + + + private static string CreateScript(int exitcode, string stderr = null, string stdout = null, int sleeptime = 0) + { + var id = Guid.NewGuid().ToString("N").Substring(0, 6); + if (Library.Utility.Utility.IsClientWindows) + { + var commands = new List(); + if (!string.IsNullOrWhiteSpace(stdout)) + commands.Add($@"echo {stdout}"); + if (!string.IsNullOrWhiteSpace(stderr)) + commands.Add($@"echo {stderr} 1>&2"); + if (sleeptime > 0) + commands.Add($@"sleep {sleeptime}"); + + commands.Add($"exit {exitcode}"); + + var filename = Path.GetFullPath(Path.Combine(BASEFOLDER, $"run-script-{id}.bat")); + File.WriteAllLines(filename, commands); + + return filename; + } + else + { + var commands = new List(); + commands.Add("#!/bin/sh"); + + if (!string.IsNullOrWhiteSpace(stdout)) + commands.Add($@"echo {stdout}"); + if (!string.IsNullOrWhiteSpace(stderr)) + commands.Add($@"(>&2 echo {stderr})"); + if (sleeptime > 0) + commands.Add($@"sleep {sleeptime}"); + + commands.Add($"exit {exitcode}"); + var filename = Path.GetFullPath(Path.Combine(BASEFOLDER, $"run-script-{id}.sh")); + File.WriteAllLines(filename, commands); + + System.Diagnostics.Process.Start("chmod", $@"+x ""{filename}""").WaitForExit(); + + return filename; + } + } + } +} From 1626037fd732592027a17677d34a49c77301dc4d Mon Sep 17 00:00:00 2001 From: Kenneth Skovhede Date: Tue, 14 Aug 2018 11:30:17 +0200 Subject: [PATCH 2/3] Changed path where scripts are created to avoid littering the base folder with junk --- Duplicati/UnitTest/RunScriptTests.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Duplicati/UnitTest/RunScriptTests.cs b/Duplicati/UnitTest/RunScriptTests.cs index fe5396340..478ecee4d 100644 --- a/Duplicati/UnitTest/RunScriptTests.cs +++ b/Duplicati/UnitTest/RunScriptTests.cs @@ -142,7 +142,7 @@ namespace Duplicati.UnitTest } - private static string CreateScript(int exitcode, string stderr = null, string stdout = null, int sleeptime = 0) + private string CreateScript(int exitcode, string stderr = null, string stdout = null, int sleeptime = 0) { var id = Guid.NewGuid().ToString("N").Substring(0, 6); if (Library.Utility.Utility.IsClientWindows) @@ -157,7 +157,7 @@ namespace Duplicati.UnitTest commands.Add($"exit {exitcode}"); - var filename = Path.GetFullPath(Path.Combine(BASEFOLDER, $"run-script-{id}.bat")); + var filename = Path.GetFullPath(Path.Combine(DATAFOLDER, $"run-script-{id}.bat")); File.WriteAllLines(filename, commands); return filename; @@ -175,7 +175,7 @@ namespace Duplicati.UnitTest commands.Add($@"sleep {sleeptime}"); commands.Add($"exit {exitcode}"); - var filename = Path.GetFullPath(Path.Combine(BASEFOLDER, $"run-script-{id}.sh")); + var filename = Path.GetFullPath(Path.Combine(DATAFOLDER, $"run-script-{id}.sh")); File.WriteAllLines(filename, commands); System.Diagnostics.Process.Start("chmod", $@"+x ""{filename}""").WaitForExit(); From baee3e312e7a972937442e246f1d7467717fc602 Mon Sep 17 00:00:00 2001 From: Kenneth Skovhede Date: Wed, 15 Aug 2018 08:15:15 +0200 Subject: [PATCH 3/3] Added delay to the exit code tests to avoid overlapping timestamps --- Duplicati/UnitTest/RunScriptTests.cs | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/Duplicati/UnitTest/RunScriptTests.cs b/Duplicati/UnitTest/RunScriptTests.cs index 478ecee4d..28b39a91d 100644 --- a/Duplicati/UnitTest/RunScriptTests.cs +++ b/Duplicati/UnitTest/RunScriptTests.cs @@ -44,6 +44,9 @@ namespace Duplicati.UnitTest options["blocksize"] = blocksize.ToString() + "b"; options["run-script-timeout"] = "5s"; + // We need a small delay as we run very small backups back-to-back + var PAUSE_TIME = TimeSpan.FromSeconds(3); + BorderTests.WriteTestFilesToFolder(DATAFOLDER, blocksize, 0); using (var c = new Library.Main.Controller("file://" + TARGETFOLDER, options, null)) @@ -51,7 +54,8 @@ namespace Duplicati.UnitTest var res = c.Backup(new string[] { DATAFOLDER }); if (res.ParsedResult != ParsedResultType.Success) throw new Exception("Unexpected result from base backup"); - + + System.Threading.Thread.Sleep(PAUSE_TIME); options["run-script-before"] = CreateScript(0); res = c.Backup(new string[] { DATAFOLDER }); if (res.ParsedResult != ParsedResultType.Success) @@ -59,6 +63,7 @@ namespace Duplicati.UnitTest if (res.ExaminedFiles <= 0) throw new Exception("Backup did not examine any files for code 0?"); + System.Threading.Thread.Sleep(PAUSE_TIME); options["run-script-before"] = CreateScript(1); res = c.Backup(new string[] { DATAFOLDER }); if (res.ParsedResult != ParsedResultType.Success) @@ -66,6 +71,7 @@ namespace Duplicati.UnitTest if (res.ExaminedFiles > 0) throw new Exception("Backup did examine files for code 1?"); + System.Threading.Thread.Sleep(PAUSE_TIME); options["run-script-before"] = CreateScript(2); res = c.Backup(new string[] { DATAFOLDER }); if (res.ParsedResult != ParsedResultType.Warning) @@ -73,6 +79,7 @@ namespace Duplicati.UnitTest if (res.ExaminedFiles <= 0) throw new Exception("Backup did not examine any files for code 2?"); + System.Threading.Thread.Sleep(PAUSE_TIME); options["run-script-before"] = CreateScript(3); res = c.Backup(new string[] { DATAFOLDER }); if (res.ParsedResult != ParsedResultType.Warning) @@ -80,6 +87,7 @@ namespace Duplicati.UnitTest if (res.ExaminedFiles > 0) throw new Exception("Backup did examine files for code 3?"); + System.Threading.Thread.Sleep(PAUSE_TIME); options["run-script-before"] = CreateScript(4); res = c.Backup(new string[] { DATAFOLDER }); if (res.ParsedResult != ParsedResultType.Error) @@ -87,6 +95,7 @@ namespace Duplicati.UnitTest if (res.ExaminedFiles <= 0) throw new Exception("Backup did not examine any files for code 4?"); + System.Threading.Thread.Sleep(PAUSE_TIME); options["run-script-before"] = CreateScript(5); res = c.Backup(new string[] { DATAFOLDER }); if (res.ParsedResult != ParsedResultType.Error) @@ -95,7 +104,7 @@ namespace Duplicati.UnitTest throw new Exception("Backup did examine files for code 5?"); - + System.Threading.Thread.Sleep(PAUSE_TIME); options["run-script-before"] = CreateScript(2, "TEST WARNING MESSAGE"); res = c.Backup(new string[] { DATAFOLDER }); if (res.ParsedResult != ParsedResultType.Warning) @@ -105,6 +114,7 @@ namespace Duplicati.UnitTest if (!res.Warnings.Any(x => x.IndexOf("TEST WARNING MESSAGE", StringComparison.Ordinal) >= 0)) throw new Exception("Found no warning message in output for code 2"); + System.Threading.Thread.Sleep(PAUSE_TIME); options["run-script-before"] = CreateScript(3, "TEST WARNING MESSAGE"); res = c.Backup(new string[] { DATAFOLDER }); if (res.ParsedResult != ParsedResultType.Warning) @@ -114,6 +124,7 @@ namespace Duplicati.UnitTest if (!res.Warnings.Any(x => x.IndexOf("TEST WARNING MESSAGE", StringComparison.Ordinal) >= 0)) throw new Exception("Found no warning message in output for code 3"); + System.Threading.Thread.Sleep(PAUSE_TIME); options["run-script-before"] = CreateScript(4, "TEST ERROR MESSAGE"); res = c.Backup(new string[] { DATAFOLDER }); if (res.ParsedResult != ParsedResultType.Error) @@ -123,6 +134,7 @@ namespace Duplicati.UnitTest if (!res.Errors.Any(x => x.IndexOf("TEST ERROR MESSAGE", StringComparison.Ordinal) >= 0)) throw new Exception("Found no error message in output for code 4"); + System.Threading.Thread.Sleep(PAUSE_TIME); options["run-script-before"] = CreateScript(5, "TEST ERROR MESSAGE"); res = c.Backup(new string[] { DATAFOLDER }); if (res.ParsedResult != ParsedResultType.Error) @@ -132,6 +144,7 @@ namespace Duplicati.UnitTest if (!res.Errors.Any(x => x.IndexOf("TEST ERROR MESSAGE", StringComparison.Ordinal) >= 0)) throw new Exception("Found no error message in output for code 5"); + System.Threading.Thread.Sleep(PAUSE_TIME); options["run-script-before"] = CreateScript(0, sleeptime: 10); res = c.Backup(new string[] { DATAFOLDER }); if (res.ParsedResult != ParsedResultType.Warning)