using FluentTerminal.App.Services; using FluentTerminal.Models; using FluentTerminal.Models.Enums; using FluentTerminal.Models.Requests; using FluentTerminal.Models.Responses; using FluentTerminal.SystemTray.Services.ConPty; using FluentTerminal.SystemTray.Services.WinPty; using System; using System.Collections; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Text; using System.Text.RegularExpressions; using Windows.ApplicationModel; using FluentTerminal.Models.Messages; using GalaSoft.MvvmLight.Messaging; namespace FluentTerminal.SystemTray.Services { public struct TerminalSessionInfo { public DateTime StartTime { get; set; } public string ProfileName { get; set; } public ITerminalSession Session { get; set; } } public class TerminalsManager { private readonly Dictionary _terminals = new Dictionary(); public event EventHandler DisplayOutputRequested; public event EventHandler TerminalExited; private static readonly Regex EscapeSequencePattern = new Regex(@"((\x9B|\x1B\[)[0-?]*[ -\/]*[@-~])|((\x9D|\x1B\]).*\x07)", RegexOptions.Compiled); private readonly Dictionary _cachedLogPath = new Dictionary(); private ApplicationSettings _applicationSettings; public TerminalsManager(ISettingsService settingsService) { _applicationSettings = settingsService.GetApplicationSettings(); Messenger.Default.Register(this, OnApplicationSettingsChanged); } private void OnApplicationSettingsChanged(ApplicationSettingsChangedMessage message) { _applicationSettings = message.ApplicationSettings; } public void DisplayTerminalOutput(byte terminalId, byte[] output) { if (_applicationSettings.EnableLogging && Directory.Exists(_applicationSettings.LogDirectoryPath)) { var logOutput = output; if (_applicationSettings.PrintableOutputOnly) { var strOutput = Encoding.UTF8.GetString(logOutput); strOutput = EscapeSequencePattern.Replace(strOutput, ""); logOutput = Encoding.UTF8.GetBytes(strOutput); } try { using var logFileStream = System.IO.File.Open(GetLogFilePath(terminalId), System.IO.FileMode.Append); logFileStream.Write(logOutput, 0, logOutput.Length); } catch (Exception e) { Logger.Instance.Debug("DisplayTerminalOutput failed. Exception: {0}", e); } } DisplayOutputRequested?.Invoke(this, new TerminalOutput { TerminalId = terminalId, Data = output }); } private string GetLogFilePath(byte terminalId) { if (!_terminals.ContainsKey(terminalId)) return string.Empty; if (!_cachedLogPath.ContainsKey(terminalId)) { var sb = new StringBuilder(); sb.Append(_applicationSettings.LogDirectoryPath); sb.Append(Path.DirectorySeparatorChar); sb.Append(_terminals[terminalId].StartTime.ToString("yyyyMMddhhmmssfff")); sb.Append("_"); sb.Append(_terminals[terminalId].ProfileName); sb.Append(".log"); _cachedLogPath.Add(terminalId, sb.ToString()); } return _cachedLogPath[terminalId]; } public CreateTerminalResponse CreateTerminal(CreateTerminalRequest request) { if (_terminals.ContainsKey(request.Id)) { // App terminated without cleaning up, removing orphaned sessions foreach (var item in _terminals.Values) { item.Session.Dispose(); } _terminals.Clear(); } request.Profile.Location = Utilities.ResolveLocation(request.Profile.Location); ITerminalSession terminal = null; try { if (request.SessionType == SessionType.WinPty) { terminal = new WinPtySession(); } else { terminal = new ConPtySession(); } terminal.Start(request, this); } catch (Exception e) { return new CreateTerminalResponse { Error = e.ToString() }; } var name = string.IsNullOrEmpty(request.Profile.Name) ? terminal.ShellExecutableName : request.Profile.Name; terminal.ConnectionClosed += OnTerminalConnectionClosed; _terminals.Add(terminal.Id, new TerminalSessionInfo { ProfileName = name, StartTime = DateTime.Now, Session = terminal }); return new CreateTerminalResponse { Success = true, Name = name }; } public void Write(byte id, byte[] data) { if (_terminals.TryGetValue(id, out TerminalSessionInfo sessionInfo)) { try { sessionInfo.Session.Write(data); } catch (IOException e) { Logger.Instance.Error($"TerminalsManager.Write: sending user input to terminal with id '{id}' failed with exception: {e}"); } } } public void ResizeTerminal(byte id, TerminalSize size) { if (_terminals.TryGetValue(id, out TerminalSessionInfo sessionInfo)) { try { sessionInfo.Session.Resize(size); } catch (Exception e) { Logger.Instance.Error($"ResizeTerminal: resizing of terminal with id '{id}' failed with exception: {e}"); } } else { Debug.WriteLine($"ResizeTerminal: terminal with id '{id}' was not found"); } } public void CloseTerminal(byte id) { if (_terminals.TryGetValue(id, out TerminalSessionInfo sessionInfo)) { _terminals.Remove(sessionInfo.Session.Id); sessionInfo.Session.Close(); } } public PauseTerminalOutputResponse PauseTermimal(byte id, bool pause) { var response = new PauseTerminalOutputResponse() { Success = true }; if (_terminals.TryGetValue(id, out TerminalSessionInfo sessionInfo)) { sessionInfo.Session.Pause(pause); } return response; } public string GetDefaultEnvironmentVariableString(Dictionary additionalVariables) { var environmentVariables = Environment.GetEnvironmentVariables(); environmentVariables["TERM_PROGRAM"] = "FluentTerminal"; environmentVariables["TERM_PROGRAM_VERSION"] = $"{Package.Current.Id.Version.Major}.{Package.Current.Id.Version.Minor}.{Package.Current.Id.Version.Build}.{Package.Current.Id.Version.Revision}"; if (additionalVariables != null) { foreach (var kvp in additionalVariables) { environmentVariables[kvp.Key] = kvp.Value; } } var builder = new StringBuilder(); foreach (DictionaryEntry item in environmentVariables) { builder.Append(item.Key).Append("=").Append(item.Value).Append("\0"); } builder.Append('\0'); return builder.ToString(); } private void OnTerminalConnectionClosed(object sender, int exitcode) { if (sender is ITerminalSession terminal) { _terminals.Remove(terminal.Id); TerminalExited?.Invoke(this, new TerminalExitStatus(terminal.Id, exitcode)); terminal.Dispose(); } } } }