using System; using System.Collections.Generic; using System.Diagnostics; using System.Globalization; using System.IO; using System.Threading; using Microsoft.Win32; using Microsoft.Win32.SafeHandles; namespace WinSCP { internal class ExeSessionProcess : IDisposable { public event OutputDataReceivedEventHandler OutputDataReceived; public bool HasExited { get { return _process.HasExited; } } public int ExitCode { get { return _process.ExitCode; } } public ExeSessionProcess(Session session) { _session = session; _logger = session.Logger; _incompleteLine = string.Empty; using (_logger.CreateCallstack()) { string executablePath = GetExecutablePath(); _logger.WriteLine("EXE executable path resolved to {0}", executablePath); string assemblyFilePath = _logger.GetAssemblyFilePath(); FileVersionInfo assemblyVersion = null; if (assemblyFilePath != null) { assemblyVersion = FileVersionInfo.GetVersionInfo(assemblyFilePath); } CheckVersion(executablePath, assemblyVersion); string configSwitch; if (_session.DefaultConfiguration) { configSwitch = "/ini=nul "; } else { if (!string.IsNullOrEmpty(_session.IniFilePath)) { configSwitch = string.Format(CultureInfo.InvariantCulture, "/ini=\"{0}\" ", _session.IniFilePath); } else { configSwitch = ""; } } string logSwitch = null; if (!string.IsNullOrEmpty(_session.SessionLogPath)) { logSwitch = string.Format(CultureInfo.InvariantCulture, "/log=\"{0}\" ", LogPathEscape(_session.SessionLogPath)); } string xmlLogSwitch = string.Format(CultureInfo.InvariantCulture, "/xmllog=\"{0}\" ", LogPathEscape(_session.XmlLogPath)); string assemblyVersionStr = (assemblyVersion == null) ? "unk" : string.Format(CultureInfo.InvariantCulture, "{0}{1}{2} ", assemblyVersion.ProductMajorPart, assemblyVersion.ProductMinorPart, assemblyVersion.ProductBuildPart); string assemblyVersionSwitch = string.Format(CultureInfo.InvariantCulture, "/dotnet={0} ", assemblyVersionStr); string arguments = xmlLogSwitch + "/xmlgroups /nointeractiveinput " + assemblyVersionSwitch + configSwitch + logSwitch + _session.AdditionalExecutableArguments; _process = new Process(); _process.StartInfo.FileName = executablePath; _process.StartInfo.WorkingDirectory = Path.GetDirectoryName(executablePath); _process.StartInfo.Arguments = arguments; _process.StartInfo.UseShellExecute = false; _process.Exited += ProcessExited; if (_logger.Logging) { _process.OutputDataReceived += ProcessOutputDataReceived; _process.ErrorDataReceived += ProcessErrorDataReceived; } } } private static string LogPathEscape(string path) { return Session.ArgumentEscape(path).Replace("!", "!!"); } public void Abort() { using (_logger.CreateCallstack()) { lock (_lock) { if ((_process != null) && !_process.HasExited) { _process.Kill(); } } } } public void Start() { using (_logger.CreateCallstack()) { InitializeConsole(); InitializeChild(); } } private void InitializeChild() { using (_logger.CreateCallstack()) { _process.StartInfo.Arguments += string.Format(CultureInfo.InvariantCulture, " /console /consoleinstance={0}", _instanceName); _logger.WriteLine("Starting \"{0}\" {1}", _process.StartInfo.FileName, _process.StartInfo.Arguments); _process.Start(); _logger.WriteLine("Started process {0}", _process.Id); _job.AddProcess(_process.Handle); _thread = new Thread(ProcessEvents); _thread.IsBackground = true; _thread.Start(); } } private void ProcessExited(object sender, EventArgs e) { _logger.WriteLine("Process {0} exited with exit code {1}", _process.Id, _process.ExitCode); } private void ProcessOutputDataReceived(object sender, DataReceivedEventArgs e) { _logger.WriteLine("Process output: {0}", e.Data); } private void ProcessErrorDataReceived(object sender, DataReceivedEventArgs e) { _logger.WriteLine("Process error output: {0}", e.Data); } private bool AbortedOrExited() { if (_abort) { _logger.WriteLine("Aborted"); return true; } else if (_process.HasExited) { _logger.WriteLine("Exited"); return true; } else { return false; } } private void ProcessEvents() { using (_logger.CreateCallstack()) { while (!AbortedOrExited()) { if (_requestEvent.WaitOne(100, false)) { ProcessEvent(); } } } } private void ProcessEvent() { using (_logger.CreateCallstack()) { using (ConsoleCommStruct commStruct = AcquireCommStruct()) { switch (commStruct.Event) { case ConsoleEvent.Print: ProcessPrintEvent(commStruct.PrintEvent); break; case ConsoleEvent.Input: ProcessInputEvent(commStruct.InputEvent); break; case ConsoleEvent.Choice: ProcessChoiceEvent(commStruct.ChoiceEvent); break; case ConsoleEvent.Title: ProcessTitleEvent(commStruct.TitleEvent); break; case ConsoleEvent.Init: ProcessInitEvent(commStruct.InitEvent); break; case ConsoleEvent.Progress: ProcessProgressEvent(commStruct.ProgressEvent); break; default: throw new NotImplementedException(); } } _responseEvent.Set(); } } private void ProcessChoiceEvent(ConsoleChoiceEventStruct e) { using (_logger.CreateCallstack()) { if (e.Timeouting) { Thread.Sleep((int)e.Timer); e.Result = e.Timeouted; } else { e.Result = e.Break; } } } private void ProcessTitleEvent(ConsoleTitleEventStruct e) { using (_logger.CreateCallstack()) { _logger.WriteLine("Not-supported title event [{0}]", e.Title); } } private void ProcessInputEvent(ConsoleInputEventStruct e) { using (_logger.CreateCallstack()) { while (!AbortedOrExited()) { lock (_input) { if (_input.Count > 0) { e.Str = _input[0]; e.Result = true; _input.RemoveAt(0); Print(false, e.Str + "\n"); return; } } _inputEvent.WaitOne(100, false); } } } private void Print(bool fromBeginning, string message) { if (fromBeginning && ((message.Length == 0) || (message[0] != '\n'))) { _lastFromBeginning = message; _logger.WriteLine("Buffered from-beginning message [{0}]", _lastFromBeginning); } else { if (!string.IsNullOrEmpty(_lastFromBeginning)) { AddToOutput(_lastFromBeginning); _lastFromBeginning = null; } if (fromBeginning && (message.Length > 0) && (message[0] == '\n')) { AddToOutput("\n"); _lastFromBeginning = message.Substring(1); _logger.WriteLine("Buffered from-beginning message [{0}]", _lastFromBeginning); } else { AddToOutput(message); } } } private void AddToOutput(string message) { string[] lines = (_incompleteLine + message).Split(new[] { '\n' }); _incompleteLine = lines[lines.Length - 1]; for (int i = 0; i < lines.Length - 1; ++i) { if (OutputDataReceived != null) { OutputDataReceived(this, new OutputDataReceivedEventArgs(lines[i])); } } } private void ProcessPrintEvent(ConsolePrintEventStruct e) { Print(e.FromBeginning, e.Message); } private void ProcessInitEvent(ConsoleInitEventStruct e) { using (_logger.CreateCallstack()) { e.InputType = 3; // pipe e.OutputType = 3; // pipe e.WantsProgress = _session.WantsProgress; } } private void ProcessProgressEvent(ConsoleProgressEventStruct e) { using (_logger.CreateCallstack()) { FileTransferProgressEventArgs args = new FileTransferProgressEventArgs(); switch (e.Operation) { case ConsoleProgressEventStruct.ProgressOperation.Copy: args.Operation = ProgressOperation.Transfer; break; default: throw new ArgumentOutOfRangeException("Unknown progress operation", (Exception)null); } switch (e.Side) { case ConsoleProgressEventStruct.ProgressSide.Local: args.Side = ProgressSide.Local; break; case ConsoleProgressEventStruct.ProgressSide.Remote: args.Side = ProgressSide.Remote; break; default: throw new ArgumentOutOfRangeException("Unknown progress side", (Exception)null); } args.FileName = e.FileName; args.Directory = e.Directory; args.OverallProgress = ((double)e.OverallProgress) / 100; args.FileProgress = ((double)e.FileProgress) / 100; args.CPS = (int) e.CPS; _session.ProcessProgress(args); } } private void InitializeConsole() { using (_logger.CreateCallstack()) { int attempts = 0; Random random = new Random(); int process = Process.GetCurrentProcess().Id; bool uniqEvent; do { if (attempts > MaxAttempts) { throw new SessionLocalException(_session, "Cannot find unique name for event object."); } int instanceNumber = random.Next(1000); _instanceName = string.Format(CultureInfo.InvariantCulture, "_{0}_{1}", process, instanceNumber); _requestEvent = new EventWaitHandle(false, EventResetMode.AutoReset, ConsoleEventRequest + _instanceName, out uniqEvent); if (!uniqEvent) { _requestEvent.Close(); _requestEvent = null; } else { _responseEvent = CreateEvent(ConsoleEventResponse + _instanceName); _cancelEvent = CreateEvent(ConsoleEventCancel + _instanceName); string fileMappingName = ConsoleMapping + _instanceName; _fileMapping = UnsafeNativeMethods.CreateFileMapping( new SafeFileHandle(new IntPtr(-1), true), IntPtr.Zero, 0x04 /*readwrite*/, 0, ConsoleCommStruct.Size, fileMappingName); if (_fileMapping.IsInvalid) { throw new SessionLocalException(_session, string.Format(CultureInfo.InvariantCulture, "Cannot create file mapping {0}", fileMappingName)); } } ++attempts; } while (!uniqEvent); using (ConsoleCommStruct commStruct = AcquireCommStruct()) { commStruct.InitHeader(); } } } private ConsoleCommStruct AcquireCommStruct() { return new ConsoleCommStruct(_session, _fileMapping); } private EventWaitHandle CreateEvent(string name) { bool createdNew; EventWaitHandle ev = new EventWaitHandle(false, EventResetMode.AutoReset, name, out createdNew); if (!createdNew) { throw new SessionLocalException(_session, string.Format(CultureInfo.InvariantCulture, "Event {0} already exists", name)); } return ev; } public void ExecuteCommand(string command) { using (_logger.CreateCallstack()) { lock (_input) { _input.Add(command); _inputEvent.Set(); } } } public void Close() { using (_logger.CreateCallstack()) { _logger.WriteLine("Waiting for process to exit"); if (!_process.WaitForExit(1000)) { _logger.WriteLine("Killing process"); _process.Kill(); } } } public void Dispose() { using (_logger.CreateCallstack()) { lock (_lock) { _abort = true; if (_thread != null) { _thread.Join(); _thread = null; } if (_process != null) { _process.Dispose(); _process = null; } if (_requestEvent != null) { _requestEvent.Close(); } if (_responseEvent != null) { _responseEvent.Close(); } if (_cancelEvent != null) { _cancelEvent.Close(); } if (_fileMapping != null) { _fileMapping.Dispose(); _fileMapping = null; } if (_inputEvent != null) { _inputEvent.Close(); _inputEvent = null; } if (_job != null) { _job.Dispose(); _job = null; } } } } private string GetExecutablePath() { using (_logger.CreateCallstack()) { string executablePath; if (!string.IsNullOrEmpty(_session.ExecutablePath)) { executablePath = _session.ExecutablePath; if (!File.Exists(executablePath)) { throw new SessionLocalException(_session, string.Format(CultureInfo.CurrentCulture, "{0} does not exists.", executablePath)); } } else { if (!TryFindExecutableInPath(GetAssemblyPath(), out executablePath) && !TryFindExecutableInPath(GetInstallationPath(Registry.CurrentUser), out executablePath) && !TryFindExecutableInPath(GetInstallationPath(Registry.LocalMachine), out executablePath) && !TryFindExecutableInPath(GetDefaultInstallationPath(), out executablePath)) { throw new SessionLocalException(_session, string.Format(CultureInfo.CurrentCulture, "The {0} executable was not found at location of the assembly ({1}), nor in an installation path. You may use Session.ExecutablePath property to explicitly set path to {0}.", ExeExecutableFileName, GetAssemblyPath())); } } return executablePath; } } private static string GetDefaultInstallationPath() { return Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles), "WinSCP"); } private static string GetInstallationPath(RegistryKey rootKey) { RegistryKey key = rootKey.OpenSubKey(@"Software\Microsoft\Windows\CurrentVersion\Uninstall\winscp3_is1"); return (key != null) ? (string)key.GetValue("Inno Setup: App Path") : null; } private bool TryFindExecutableInPath(string path, out string result) { if (string.IsNullOrEmpty(path)) { result = null; } else { string executablePath = Path.Combine(path, ExeExecutableFileName); if (File.Exists(executablePath)) { result = executablePath; _logger.WriteLine("Executable found in {0}", executablePath); } else { result = null; _logger.WriteLine("Executable not found in {0}", executablePath); } } return (result != null); } private string GetAssemblyPath() { string codeBasePath = _logger.GetAssemblyFilePath(); string path = null; if (!string.IsNullOrEmpty(codeBasePath)) { path = Path.GetDirectoryName(codeBasePath); Debug.Assert(path != null); } return path; } private void CheckVersion(string exePath, FileVersionInfo assemblyVersion) { using (_logger.CreateCallstack()) { FileVersionInfo version = FileVersionInfo.GetVersionInfo(exePath); _logger.WriteLine("Version of {0} is {1}, product {2} version is {3}", exePath, version.FileVersion, version.ProductName, version.ProductVersion); if (_session.DisableVersionCheck) { _logger.WriteLine("Version check disabled (not recommended)"); } else if (assemblyVersion == null) { _logger.WriteLine("Assembly version not known, cannot check version"); } else if (assemblyVersion.ProductVersion != version.ProductVersion) { throw new SessionLocalException( _session, string.Format(CultureInfo.CurrentCulture, "The version of {0} ({1}) does not match version of this assembly {2} ({3}). You can disable this check using Session.DisableVersionCheck (not recommended).", exePath, version.ProductVersion, _logger.GetAssemblyFilePath(), assemblyVersion.ProductVersion)); } } } public void WriteStatus() { string executablePath = GetExecutablePath(); _logger.WriteLine("{0} - exists [{1}]", executablePath, File.Exists(executablePath)); } private const int MaxAttempts = 10; private const string ConsoleMapping = "WinSCPConsoleMapping"; private const string ConsoleEventRequest = "WinSCPConsoleEventRequest"; private const string ConsoleEventResponse = "WinSCPConsoleEventResponse"; private const string ConsoleEventCancel = "WinSCPConsoleEventCancel"; private const string ExeExecutableFileName = "winscp.exe"; private Process _process; private readonly object _lock = new object(); private readonly Logger _logger; private readonly Session _session; private EventWaitHandle _requestEvent; private EventWaitHandle _responseEvent; private EventWaitHandle _cancelEvent; private SafeFileHandle _fileMapping; private string _instanceName; private Thread _thread; private bool _abort; private string _lastFromBeginning; private string _incompleteLine; private readonly List _input = new List(); private AutoResetEvent _inputEvent = new AutoResetEvent(false); private Job _job = new Job(); } }