Files
duplicati/Duplicati/Library/Modules/Builtin/RunScript.cs
T

346 lines
15 KiB
C#

// Copyright (C) 2015, 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.Diagnostics;
using System.IO;
using System.Text;
using System.Collections.Generic;
using Duplicati.Library.Utility;
namespace Duplicati.Library.Modules.Builtin
{
public class RunScript : Duplicati.Library.Interface.IGenericCallbackModule
{
private const string STARTUP_OPTION = "run-script-before";
private const string FINISH_OPTION = "run-script-after";
private const string REQUIRED_OPTION = "run-script-before-required";
private const string TIMEOUT_OPTION = "run-script-timeout";
private const string DEFAULT_TIMEOUT = "60s";
private string m_requiredScript = null;
private string m_startScript = null;
private string m_finishScript = null;
private int m_timeout = 0;
private string m_operationName;
private string m_remoteurl;
private string[] m_localpath;
private IDictionary<string, string> m_options;
#region IGenericModule implementation
public void Configure(IDictionary<string, string> commandlineOptions)
{
commandlineOptions.TryGetValue(STARTUP_OPTION, out m_startScript);
commandlineOptions.TryGetValue(REQUIRED_OPTION, out m_requiredScript);
commandlineOptions.TryGetValue(FINISH_OPTION, out m_finishScript);
string t;
if (!commandlineOptions.TryGetValue(TIMEOUT_OPTION, out t))
t = DEFAULT_TIMEOUT;
m_timeout = (int)Utility.Timeparser.ParseTimeSpan(t).TotalMilliseconds;
m_options = commandlineOptions;
}
public string Key { get { return "runscript"; } }
public string DisplayName { get { return Strings.RunScript.DisplayName; } }
public string Description { get { return Strings.RunScript.Description; } }
public bool LoadAsDefault { get { return true; } }
public IList<Duplicati.Library.Interface.ICommandLineArgument> SupportedCommands
{
get
{
return new List<Duplicati.Library.Interface.ICommandLineArgument>(new Duplicati.Library.Interface.ICommandLineArgument[] {
new Duplicati.Library.Interface.CommandLineArgument(STARTUP_OPTION, Duplicati.Library.Interface.CommandLineArgument.ArgumentType.Path, Strings.RunScript.StartupoptionShort, Strings.RunScript.StartupoptionLong),
new Duplicati.Library.Interface.CommandLineArgument(FINISH_OPTION, Duplicati.Library.Interface.CommandLineArgument.ArgumentType.Path, Strings.RunScript.FinishoptionShort, Strings.RunScript.FinishoptionLong),
new Duplicati.Library.Interface.CommandLineArgument(REQUIRED_OPTION, Duplicati.Library.Interface.CommandLineArgument.ArgumentType.Path, Strings.RunScript.RequiredoptionShort, Strings.RunScript.RequiredoptionLong),
new Duplicati.Library.Interface.CommandLineArgument(TIMEOUT_OPTION, Duplicati.Library.Interface.CommandLineArgument.ArgumentType.Timespan, Strings.RunScript.TimeoutoptionShort, Strings.RunScript.TimeoutoptionLong, DEFAULT_TIMEOUT),
});
}
}
#endregion
#region IGenericCallbackModule implementation
public void OnStart(string operationname, ref string remoteurl, ref string[] localpath)
{
m_operationName = operationname;
m_remoteurl = remoteurl;
m_localpath = localpath;
if (!string.IsNullOrEmpty(m_requiredScript))
Execute(m_requiredScript, "BEFORE", m_operationName, ref m_remoteurl, ref m_localpath, m_timeout, true, m_options, null);
if (!string.IsNullOrEmpty(m_startScript))
Execute(m_startScript, "BEFORE", m_operationName, ref m_remoteurl, ref m_localpath, m_timeout, false, m_options, null);
}
public void OnFinish (object result)
{
if (string.IsNullOrEmpty(m_finishScript))
return;
using (TempFile tmpfile = new TempFile())
{
SerializeResult(tmpfile, result);
Execute(m_finishScript, "AFTER", m_operationName, ref m_remoteurl, ref m_localpath, m_timeout, false, m_options, tmpfile);
}
}
#endregion
public static void SerializeResult(string file, object result)
{
using(StreamWriter sw = new StreamWriter(file))
{
if (result == null)
{
sw.WriteLine("null?");
}
else if (result is System.Collections.IEnumerable)
{
System.Collections.IEnumerable ie = (System.Collections.IEnumerable)result;
System.Collections.IEnumerator ien = ie.GetEnumerator();
ien.Reset();
while (ien.MoveNext())
{
object c = ien.Current;
if (c == null)
continue;
if (c.GetType().IsGenericType && !c.GetType().IsGenericTypeDefinition && c.GetType().GetGenericTypeDefinition() == typeof(KeyValuePair<,>))
{
object key = c.GetType().GetProperty("Key").GetValue(c, null);
object value = c.GetType().GetProperty("Value").GetValue(c, null);
sw.WriteLine("{0}: {1}", key, value);
}
else
sw.WriteLine(c.ToString());
}
}
else if (result.GetType().IsArray)
{
Array a = (Array)result;
for(int i = a.GetLowerBound(0); i <= a.GetUpperBound(0); i++)
{
object c = a.GetValue(i);
if (c == null)
continue;
if (c.GetType().IsGenericType && !c.GetType().IsGenericTypeDefinition && c.GetType().GetGenericTypeDefinition() == typeof(KeyValuePair<,>))
{
object key = c.GetType().GetProperty("Key").GetValue(c, null);
object value = c.GetType().GetProperty("Value").GetValue(c, null);
sw.WriteLine("{0}: {1}", key, value);
}
else
sw.WriteLine(c.ToString());
}
}
else if (result is Exception)
{
//No localization, must be parseable by script
Exception e = (Exception)result;
sw.WriteLine("Failed: {0}", e.Message);
sw.WriteLine("Details: {0}", e.ToString());
}
else
{
Utility.Utility.PrintSerializeObject(result, sw);
}
}
}
private static void Execute(string scriptpath, string eventname, string operationname, ref string remoteurl, ref string[] localpath, int timeout, bool requiredScript, IDictionary<string, string> options, string datafile)
{
try
{
System.Diagnostics.ProcessStartInfo psi = new System.Diagnostics.ProcessStartInfo(scriptpath);
psi.WindowStyle = System.Diagnostics.ProcessWindowStyle.Hidden;
psi.CreateNoWindow = true;
psi.UseShellExecute = false;
psi.RedirectStandardOutput = true;
psi.RedirectStandardError = true;
foreach(KeyValuePair<string, string> kv in options)
psi.EnvironmentVariables["DUPLICATI__" + kv.Key.Replace('-', '_')] = kv.Value;
if (!options.ContainsKey("backup-name"))
psi.EnvironmentVariables["DUPLICATI__backup_name"] = System.IO.Path.GetFileNameWithoutExtension(System.Reflection.Assembly.GetEntryAssembly().Location);
psi.EnvironmentVariables["DUPLICATI__EVENTNAME"] = eventname;
psi.EnvironmentVariables["DUPLICATI__OPERATIONNAME"] = operationname;
psi.EnvironmentVariables["DUPLICATI__REMOTEURL"] = remoteurl;
if (localpath != null)
psi.EnvironmentVariables["DUPLICATI__LOCALPATH"] = string.Join(System.IO.Path.PathSeparator.ToString(), localpath);
string stderr = null;
string stdout = null;
if (!string.IsNullOrEmpty(datafile))
psi.EnvironmentVariables["DUPLICATI__RESULTFILE"] = datafile;
using(System.Diagnostics.Process p = System.Diagnostics.Process.Start(psi))
{
ConsoleDataHandler cs = new ConsoleDataHandler(p);
if (timeout <= 0)
p.WaitForExit();
else
p.WaitForExit(timeout);
if (requiredScript)
{
if (!p.HasExited)
throw new Exception(string.Format(Strings.RunScript.ScriptTimeoutError, scriptpath));
else if (p.ExitCode != 0)
throw new Exception(string.Format(Strings.RunScript.InvalidExitCodeError, scriptpath, p.ExitCode));
}
if (p.HasExited)
{
stderr = cs.StandardError;
stdout = cs.StandardOutput;
if (p.ExitCode != 0)
Logging.Log.WriteMessage(string.Format(Strings.RunScript.InvalidExitCodeError, scriptpath, p.ExitCode), Duplicati.Library.Logging.LogMessageType.Warning);
}
else
{
Logging.Log.WriteMessage(string.Format(Strings.RunScript.ScriptTimeoutError, scriptpath), Duplicati.Library.Logging.LogMessageType.Warning);
}
}
if (!string.IsNullOrEmpty(stderr))
Logging.Log.WriteMessage(string.Format(Strings.RunScript.StdErrorReport, scriptpath, stderr), Duplicati.Library.Logging.LogMessageType.Warning);
//We only allow setting parameters on startup
if (eventname == "BEFORE" && stdout != null)
{
foreach(string rawline in stdout.Split(new string[] { Environment.NewLine }, StringSplitOptions.RemoveEmptyEntries))
{
string line = rawline.Trim();
if (!line.StartsWith("--"))
continue; //Ingore anything that does not start with --
line = line.Substring(2);
int lix = line.IndexOf('=');
if (lix == 0) //Skip --= as that makes no sense
continue;
string key;
string value;
if (lix < 0)
{
key = line.Trim();
value = "";
}
else
{
key = line.Substring(0, lix).Trim();
value = line.Substring(lix + 1).Trim();
if (value.Length >= 2 && value.StartsWith("\"") && value.EndsWith("\""))
value = value.Substring(1, value.Length - 2);
}
if (string.Equals(key, "remoteurl", StringComparison.InvariantCultureIgnoreCase))
{
remoteurl = value;
}
else if (string.Equals(key, "localpath", StringComparison.InvariantCultureIgnoreCase))
{
localpath = value.Split(System.IO.Path.PathSeparator);
}
else if (
string.Equals(key, "eventname", StringComparison.InvariantCultureIgnoreCase) ||
string.Equals(key, "operationname", StringComparison.InvariantCultureIgnoreCase) ||
string.Equals(key, "main-action", StringComparison.InvariantCultureIgnoreCase) ||
key == ""
)
{
//Ignore
}
else
options[key] = value;
}
}
}
catch (Exception ex)
{
Logging.Log.WriteMessage(string.Format(Strings.RunScript.ScriptExecuteError, scriptpath, ex.Message), Duplicati.Library.Logging.LogMessageType.Warning, ex);
if (requiredScript)
throw;
}
}
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();
}
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)
{
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();
}
}
}
}
}