// Copyright (C) 2011, Kenneth Skovhede // http://www.hexad.dk, opensource@hexad.dk // // 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 m_options; #region IGenericModule implementation public void Configure(IDictionary 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 SupportedCommands { get { return new List(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 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 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(); } } } } }