Files
duplicati/Duplicati/UnitTest/RunScriptTests.cs
T

558 lines
27 KiB
C#
Raw Normal View History

// Copyright (C) 2026, The Duplicati Team
2025-05-21 15:47:00 +02:00
// 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
2024-02-28 15:45:30 +01:00
// DEALINGS IN THE SOFTWARE.
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Duplicati.Library.Interface;
using Duplicati.Library.Main;
using NUnit.Framework;
2026-05-13 15:04:13 +02:00
using System.Threading.Tasks;
2025-06-13 11:48:24 +02:00
using Assert = NUnit.Framework.Legacy.ClassicAssert;
namespace Duplicati.UnitTest
{
public class RunScriptTests : BasicSetupHelper
{
2021-01-09 11:14:31 -08:00
[Test]
[Category("Border")]
[TestCase(0)]
[TestCase(1)]
[TestCase(2)]
[TestCase(3)]
[TestCase(4)]
[TestCase(5)]
[TestCase(6)]
2026-05-13 15:04:13 +02:00
public async Task RunScriptAfterAsync(int exitCode)
2021-01-09 11:14:31 -08:00
{
const string expectedMessage = "Hello";
2026-05-13 15:04:13 +02:00
var expectedFile = Path.Combine(this.RESTOREFOLDER, "hello.txt");
var customCommands = new List<string>
2021-01-09 11:14:31 -08:00
{
2021-01-09 12:26:20 -08:00
$"echo {expectedMessage}>\"{expectedFile}\""
2021-01-09 11:14:31 -08:00
};
2026-05-13 15:04:13 +02:00
var options = new Dictionary<string, string>(this.TestOptions);
2021-01-09 11:14:31 -08:00
options["run-script-after"] = CreateScript(exitCode, null, null, 0, customCommands);
2026-05-13 15:04:13 +02:00
using (var c = new Controller("file://" + this.TARGETFOLDER, options, null))
2021-01-09 11:14:31 -08:00
{
2026-05-13 15:04:13 +02:00
var backupResults = await c.BackupAsync(new[] { this.DATAFOLDER });
2021-01-09 11:14:31 -08:00
switch (exitCode)
{
case 0:
case 1:
Assert.AreEqual(0, backupResults.Errors.Count());
Assert.AreEqual(0, backupResults.Warnings.Count());
break;
case 2:
case 3:
Assert.AreEqual(0, backupResults.Errors.Count());
Assert.AreEqual(1, backupResults.Warnings.Count());
break;
default:
Assert.AreEqual(1, backupResults.Errors.Count());
Assert.AreEqual(0, backupResults.Warnings.Count());
break;
}
// run script after does not set interrupted flag
Assert.IsFalse(backupResults.Interrupted);
2021-01-09 11:14:31 -08:00
string[] targetEntries = Directory.EnumerateFileSystemEntries(this.RESTOREFOLDER).ToArray();
Assert.AreEqual(1, targetEntries.Length);
Assert.AreEqual(expectedFile, targetEntries[0]);
string[] lines = File.ReadAllLines(expectedFile);
Assert.AreEqual(1, lines.Length);
Assert.AreEqual(expectedMessage, lines[0]);
}
}
[Test]
[Category("Border")]
2026-05-13 15:04:13 +02:00
public async Task RunScriptBeforeAsync()
{
var blocksize = 10 * 1024;
2026-05-13 15:04:13 +02:00
var options = new Dictionary<string, string>(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))
{
2026-05-13 15:04:13 +02:00
var res = await c.BackupAsync(new string[] { DATAFOLDER });
Assert.AreEqual(0, res.Errors.Count());
Assert.AreEqual(0, res.Warnings.Count());
if (res.ParsedResult != ParsedResultType.Success)
throw new Exception("Unexpected result from base backup");
2026-05-14 22:45:40 +02:00
}
2025-05-21 15:47:00 +02:00
2026-05-14 22:45:40 +02:00
options["run-script-before"] = CreateScript(0);
using (var c = new Library.Main.Controller("file://" + TARGETFOLDER, options, null))
{
var res = await c.BackupAsync(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?");
2026-05-14 22:45:40 +02:00
}
2026-05-14 22:45:40 +02:00
options["run-script-before"] = CreateScript(1);
using (var c = new Library.Main.Controller("file://" + TARGETFOLDER, options, null))
{
var res = await c.BackupAsync(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?");
2026-05-14 22:45:40 +02:00
}
2026-05-14 22:45:40 +02:00
options["run-script-before"] = CreateScript(2);
using (var c = new Library.Main.Controller("file://" + TARGETFOLDER, options, null))
{
var res = await c.BackupAsync(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?");
2026-05-14 22:45:40 +02:00
}
2026-05-14 22:45:40 +02:00
options["run-script-before"] = CreateScript(3);
using (var c = new Library.Main.Controller("file://" + TARGETFOLDER, options, null))
{
var res = await c.BackupAsync(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?");
2026-05-14 22:45:40 +02:00
}
2026-05-14 22:45:40 +02:00
options["run-script-before"] = CreateScript(4);
using (var c = new Library.Main.Controller("file://" + TARGETFOLDER, options, null))
{
var res = await c.BackupAsync(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?");
2026-05-14 22:45:40 +02:00
}
2026-05-14 22:45:40 +02:00
foreach (int exitCode in new[] { 5, 6, 10, 99 })
{
options["run-script-before"] = CreateScript(exitCode);
using (var c = new Library.Main.Controller("file://" + TARGETFOLDER, options, null))
2021-01-09 21:03:47 -08:00
{
2026-05-14 22:45:40 +02:00
var res = await c.BackupAsync(new string[] { DATAFOLDER });
2021-01-09 21:03:47 -08:00
if (res.ParsedResult != ParsedResultType.Error)
throw new Exception($"Unexpected result from backup with return code {exitCode}");
if (res.ExaminedFiles > 0)
throw new Exception($"Backup did examine files for code {exitCode}?");
}
2026-05-14 22:45:40 +02:00
}
2026-05-14 22:45:40 +02:00
options["run-script-before"] = CreateScript(2, "TEST WARNING MESSAGE");
using (var c = new Library.Main.Controller("file://" + TARGETFOLDER, options, null))
{
var res = await c.BackupAsync(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");
2026-05-14 22:45:40 +02:00
}
2026-05-14 22:45:40 +02:00
options["run-script-before"] = CreateScript(3, "TEST WARNING MESSAGE");
using (var c = new Library.Main.Controller("file://" + TARGETFOLDER, options, null))
{
var res = await c.BackupAsync(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");
2026-05-14 22:45:40 +02:00
}
2026-05-14 22:45:40 +02:00
options["run-script-before"] = CreateScript(4, "TEST ERROR MESSAGE");
using (var c = new Library.Main.Controller("file://" + TARGETFOLDER, options, null))
{
var res = await c.BackupAsync(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");
2026-05-14 22:45:40 +02:00
}
2026-05-14 22:45:40 +02:00
options["run-script-before"] = CreateScript(5, "TEST ERROR MESSAGE");
using (var c = new Library.Main.Controller("file://" + TARGETFOLDER, options, null))
{
var res = await c.BackupAsync(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");
2026-05-14 22:45:40 +02:00
}
2026-05-14 22:45:40 +02:00
options["run-script-before"] = CreateScript(0, sleeptime: 10);
using (var c = new Library.Main.Controller("file://" + TARGETFOLDER, options, null))
{
var res = await c.BackupAsync(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?");
}
}
2025-05-21 15:47:00 +02:00
private static void AssertWithMessage(IEnumerable<string> messages, int count)
{
if (messages.Count() != count)
{
string message = string.Join(Environment.NewLine, messages);
Assert.Fail($"Expected {count} messages, but got {messages.Count()}:\n{message}");
}
}
[Test]
[Category("Border")]
[TestCase(0)]
[TestCase(1)]
[TestCase(2)]
[TestCase(3)]
[TestCase(4)]
[TestCase(5)]
[TestCase(6)]
2026-05-13 15:04:13 +02:00
public async Task RunScriptParsedResultAsync(int exitCode)
{
2026-05-13 15:04:13 +02:00
var parsedResultFile = Path.Combine(this.RESTOREFOLDER, "result.txt");
var customCommands = new List<string>();
2024-05-30 02:14:05 +02:00
if (OperatingSystem.IsWindows())
{
customCommands.Add($"echo %DUPLICATI__PARSED_RESULT%>\"{parsedResultFile}\"");
}
else
{
customCommands.Add($"echo $DUPLICATI__PARSED_RESULT>\"{parsedResultFile}\"");
}
2026-05-13 15:04:13 +02:00
var options = new Dictionary<string, string>(this.TestOptions);
options["run-script-before"] = this.CreateScript(exitCode);
options["run-script-after"] = this.CreateScript(0, null, null, 0, customCommands);
2025-05-21 15:47:00 +02:00
using (var c = new Controller("file://" + this.TARGETFOLDER, options, null))
{
2026-05-13 15:04:13 +02:00
var backupResults = await c.BackupAsync(new[] { this.DATAFOLDER });
bool expectBackup;
string expectedParsedResult;
switch (exitCode)
{
2025-05-30 10:07:58 +02:00
case 0: // OK, run operation
if (OperatingSystem.IsWindows())
{
// Windows reports warnings due to CI test mounted folders
AssertWithMessage(backupResults.Errors, 0);
}
else
{
TestUtils.AssertResults(backupResults);
}
expectBackup = true;
expectedParsedResult = ParsedResultType.Success.ToString();
break;
case 1: // OK, don't run operation
2025-05-21 15:47:00 +02:00
TestUtils.AssertResults(backupResults);
Assert.IsTrue(backupResults.Interrupted);
expectBackup = false;
expectedParsedResult = ParsedResultType.Success.ToString();
break;
case 2: // Warning, run operation
2025-05-21 15:47:00 +02:00
AssertWithMessage(backupResults.Errors, 0);
AssertWithMessage(backupResults.Warnings, 1);
Assert.IsFalse(backupResults.Interrupted);
expectBackup = true;
expectedParsedResult = ParsedResultType.Warning.ToString();
break;
case 3: // Warning, don't run operation
2025-05-21 15:47:00 +02:00
AssertWithMessage(backupResults.Errors, 0);
AssertWithMessage(backupResults.Warnings, 1);
Assert.IsTrue(backupResults.Interrupted);
expectBackup = false;
expectedParsedResult = ParsedResultType.Warning.ToString();
break;
case 4: // Error, run operation
2025-05-21 15:47:00 +02:00
AssertWithMessage(backupResults.Errors, 1);
AssertWithMessage(backupResults.Warnings, 0);
Assert.IsFalse(backupResults.Interrupted);
expectBackup = true;
expectedParsedResult = ParsedResultType.Error.ToString();
break;
default: // Error don't run operation
2025-05-21 15:47:00 +02:00
AssertWithMessage(backupResults.Errors, 1);
AssertWithMessage(backupResults.Warnings, 0);
Assert.IsTrue(backupResults.Interrupted);
expectBackup = false;
expectedParsedResult = ParsedResultType.Error.ToString();
break;
}
2026-05-13 15:04:13 +02:00
var targetEntries = Directory.EnumerateFileSystemEntries(this.TARGETFOLDER);
if (expectBackup)
{
// We expect a dblock, dlist, and dindex file.
Assert.AreEqual(3, targetEntries.Count());
}
else
{
Assert.AreEqual(0, targetEntries.Count());
}
2026-05-13 15:04:13 +02:00
var lines = File.ReadAllLines(parsedResultFile);
Assert.AreEqual(1, lines.Length);
Assert.AreEqual(expectedParsedResult, lines[0]);
}
}
[Test]
[Category("Border")]
2026-05-13 15:04:13 +02:00
public async Task CustomRemoteURLAsync()
{
2026-05-13 15:04:13 +02:00
var customTargetFolder = Path.Combine(this.TARGETFOLDER, "destination");
Directory.CreateDirectory(customTargetFolder);
2026-05-13 15:04:13 +02:00
var customCommands = new List<string>
{
$"echo --remoteurl = \"{customTargetFolder}\""
};
2026-05-13 15:04:13 +02:00
var options = new Dictionary<string, string>(this.TestOptions);
options["run-script-before"] = CreateScript(0, null, null, 0, customCommands);
2026-05-13 15:04:13 +02:00
using (var c = new Library.Main.Controller("file://" + this.TARGETFOLDER, options, null))
{
2026-05-13 15:04:13 +02:00
var backupResults = await c.BackupAsync(new[] { this.DATAFOLDER });
Assert.AreEqual(0, backupResults.Errors.Count());
Assert.AreEqual(0, backupResults.Warnings.Count());
}
string[] targetEntries = Directory.EnumerateFileSystemEntries(this.TARGETFOLDER).ToArray();
Assert.AreEqual(1, targetEntries.Length);
Assert.AreEqual(customTargetFolder, targetEntries[0]);
// We expect a dblock, dlist, and dindex file.
IEnumerable<string> customTargetEntries = Directory.EnumerateFileSystemEntries(customTargetFolder);
Assert.AreEqual(3, customTargetEntries.Count());
}
[Test]
[Category("Border")]
public async Task CustomLocalPathAsync()
{
var customSourceFolder = Path.Combine(this.DATAFOLDER, "source2");
Directory.CreateDirectory(customSourceFolder);
var expectedFile = Path.Combine(customSourceFolder, "file2.txt");
File.WriteAllText(expectedFile, "some content");
var customCommands = new List<string>
{
$"echo --localpath=\"{customSourceFolder}\""
};
var options = new Dictionary<string, string>(this.TestOptions);
options["run-script-before"] = CreateScript(0, null, null, 0, customCommands);
using (var c = new Library.Main.Controller("file://" + this.TARGETFOLDER, options, null))
{
var backupResults = await c.BackupAsync(new[] { this.DATAFOLDER });
Assert.AreEqual(0, backupResults.Errors.Count());
Assert.AreEqual(0, backupResults.Warnings.Count());
Assert.AreEqual(1, backupResults.ExaminedFiles);
}
using (var c = new Library.Main.Controller("file://" + this.TARGETFOLDER, this.TestOptions, null))
{
// Restore to a specific folder so we don't mess up original,
// or just restore exactly since it's a test environment.
var restoreOptions = new Dictionary<string, string>(this.TestOptions);
restoreOptions["restore-path"] = this.RESTOREFOLDER;
var c2 = new Library.Main.Controller("file://" + this.TARGETFOLDER, restoreOptions, null);
var restoreResults = await c2.RestoreAsync(new[] { expectedFile });
Assert.AreEqual(0, restoreResults.Errors.Count());
Assert.AreEqual(0, restoreResults.Warnings.Count());
Assert.AreEqual(1, restoreResults.RestoredFiles);
}
}
2026-06-09 16:29:02 +02:00
[Test]
[Category("Border")]
public async Task CustomFilterAsync()
{
var expectedFilter = ".*b.*";
2026-06-09 16:29:02 +02:00
var parsedResultFileAfter = Path.Combine(this.RESTOREFOLDER, "result_after.txt");
var parsedResultFilePostBackup = Path.Combine(this.RESTOREFOLDER, "result_post_backup.txt");
var customCommandsBefore = new List<string>
{
2026-06-12 19:27:15 +02:00
$"echo --filter=\"+{expectedFilter}\""
2026-06-09 16:29:02 +02:00
};
var customCommandsAfter = new List<string>();
if (OperatingSystem.IsWindows())
{
customCommandsAfter.Add($"echo %DUPLICATI__filter%>\"{parsedResultFileAfter}\"");
}
else
{
customCommandsAfter.Add($"echo \"$DUPLICATI__filter\">\"{parsedResultFileAfter}\"");
}
var customCommandsPostBackup = new List<string>();
if (OperatingSystem.IsWindows())
{
customCommandsPostBackup.Add($"echo %DUPLICATI__filter%>\"{parsedResultFilePostBackup}\"");
}
else
{
customCommandsPostBackup.Add($"echo \"$DUPLICATI__filter\">\"{parsedResultFilePostBackup}\"");
}
var options = new Dictionary<string, string>(this.TestOptions);
options["run-script-before"] = CreateScript(0, null, null, 0, customCommandsBefore);
options["run-script-after"] = CreateScript(0, null, null, 0, customCommandsAfter);
options["run-script-post-backup"] = CreateScript(0, null, null, 0, customCommandsPostBackup);
using (var c = new Library.Main.Controller("file://" + this.TARGETFOLDER, options, null))
{
var backupResults = await c.BackupAsync(new[] { this.DATAFOLDER });
Assert.AreEqual(0, backupResults.Errors.Count());
Assert.AreEqual(0, backupResults.Warnings.Count());
}
var linesAfter = File.ReadAllLines(parsedResultFileAfter);
Assert.AreEqual(1, linesAfter.Length);
2026-06-12 19:40:32 +02:00
Assert.IsTrue(linesAfter[0].Contains(expectedFilter, StringComparison.OrdinalIgnoreCase),
$"Expected the after-script filter output to contain '{expectedFilter}', but got: '{linesAfter[0]}'");
2026-06-09 16:29:02 +02:00
var linesPostBackup = File.ReadAllLines(parsedResultFilePostBackup);
Assert.AreEqual(1, linesPostBackup.Length);
2026-06-12 19:40:32 +02:00
Assert.IsTrue(linesPostBackup[0].Contains(expectedFilter, StringComparison.OrdinalIgnoreCase),
$"Expected the post-backup-script filter output to contain '{expectedFilter}', but got: '{linesPostBackup[0]}'");
2026-06-09 16:29:02 +02:00
}
[Test]
[Category("Border")]
public async Task CommandLineFilterSeenByScriptsAsync()
{
var expectedFilter = "+*b*";
2026-06-09 16:29:02 +02:00
var parsedResultFileAfter = Path.Combine(this.RESTOREFOLDER, "result_after_cmdline.txt");
var parsedResultFilePostBackup = Path.Combine(this.RESTOREFOLDER, "result_post_backup_cmdline.txt");
var customCommandsAfter = new List<string>();
if (OperatingSystem.IsWindows())
{
customCommandsAfter.Add($"echo %DUPLICATI__filter%>\"{parsedResultFileAfter}\"");
}
else
{
customCommandsAfter.Add($"echo \"$DUPLICATI__filter\">\"{parsedResultFileAfter}\"");
}
var customCommandsPostBackup = new List<string>();
if (OperatingSystem.IsWindows())
{
customCommandsPostBackup.Add($"echo %DUPLICATI__filter%>\"{parsedResultFilePostBackup}\"");
}
else
{
customCommandsPostBackup.Add($"echo \"$DUPLICATI__filter\">\"{parsedResultFilePostBackup}\"");
}
var options = new Dictionary<string, string>(this.TestOptions);
options["run-script-after"] = CreateScript(0, null, null, 0, customCommandsAfter);
options["run-script-post-backup"] = CreateScript(0, null, null, 0, customCommandsPostBackup);
using (var c = new Library.Main.Controller("file://" + this.TARGETFOLDER, options, null))
{
var filter = new Library.Utility.FilterExpression("*b*", true);
var backupResults = await c.BackupAsync(new[] { this.DATAFOLDER }, filter);
Assert.AreEqual(0, backupResults.Errors.Count());
Assert.AreEqual(0, backupResults.Warnings.Count());
}
var linesAfter = File.ReadAllLines(parsedResultFileAfter);
Assert.AreEqual(1, linesAfter.Length);
Assert.IsTrue(linesAfter[0].Contains(expectedFilter, StringComparison.OrdinalIgnoreCase));
2026-06-09 16:29:02 +02:00
var linesPostBackup = File.ReadAllLines(parsedResultFilePostBackup);
Assert.AreEqual(1, linesPostBackup.Length);
Assert.IsTrue(linesPostBackup[0].Contains(expectedFilter, StringComparison.OrdinalIgnoreCase));
2026-06-09 16:29:02 +02:00
}
private string CreateScript(int exitcode, string stderr = null, string stdout = null, int sleeptime = 0, List<string> customCommands = null)
{
var id = Guid.NewGuid().ToString("N").Substring(0, 6);
2024-05-30 02:14:05 +02:00
if (OperatingSystem.IsWindows())
{
var commands = customCommands ?? new List<string>();
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(DATAFOLDER, $"run-script-{id}.bat"));
File.WriteAllLines(filename, commands);
return filename;
}
else
{
2026-06-09 16:29:02 +02:00
var commands = new List<string>() { "#!/bin/sh" };
if (customCommands != null)
commands.AddRange(customCommands);
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(DATAFOLDER, $"run-script-{id}.sh"));
File.WriteAllLines(filename, commands);
System.Diagnostics.Process.Start("chmod", $@"+x ""{filename}""").WaitForExit();
return filename;
}
}
}
}