// Copyright Keysight Technologies 2012-2019 // This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this // file, you can obtain one at http://mozilla.org/MPL/2.0/. using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; using System.Security; using System.Text; using System.Threading; using OpenTap.Translation; namespace OpenTap { /// /// Class for getting user input without using GUI. /// public class UserInput { /// Request user input from the GUI. Waits an amount of time specified by Timeout. If the timeout occurs a TimeoutException will be thrown. /// The object the user should fill out with data. /// How long to wait before timing out. /// set to True if a modal request is wanted. This means the user will have to answer before doing anything else. public static void Request(object dataObject, TimeSpan Timeout, bool modal = false) { inputInterface?.RequestUserInput(dataObject, Timeout, modal); } /// Request user input from the GUI. Waits indefinitely. /// The object the user should fill out with data. /// set to True if a modal request is wanted. This means the user will have to answer before doing anything else. public static void Request(object dataObject, bool modal = false) { inputInterface?.RequestUserInput(dataObject, TimeSpan.MaxValue, modal); } /// Currently selected interface. public static object Interface => inputInterface; static readonly SessionLocal _inputInterface = new SessionLocal(false); static IUserInputInterface inputInterface => _inputInterface.Value; static IUserInterface userInterface => inputInterface as IUserInterface; /// Sets the current user input interface. This should almost never be called from user code. /// public static void SetInterface(IUserInputInterface inputInterface) { _inputInterface.Value = inputInterface; } /// Call to notify the user interface that an object property has changed. /// /// public static void NotifyChanged(object obj, string property) { userInterface?.NotifyChanged(obj, property); } /// Gets the current user input interface. /// public static IUserInputInterface GetInterface() => inputInterface; } /// Defines a way for plugins to notify the user that a property has changed. public interface IUserInterface { /// /// This method is called to notify that a property has changed on an object. /// /// /// void NotifyChanged(object obj, string property); } /// Defines a way for plugins to request input from the user. public interface IUserInputInterface { /// The method called when the interface requests user input. /// The object the user should fill out with data. /// How long the user should have. /// True if a modal request is wanted void RequestUserInput(object dataObject, TimeSpan Timeout, bool modal); } /// The supported layout modes. [Flags] public enum LayoutMode { /// The default mode. Normal = 1, /// The user input fills the whole row. FullRow = 2, /// The user input floats to the bottom. FloatBottom = 4, /// Whether text wrapping should be enabled. WrapText = 8 } /// LayoutAttribute can be used to specify the wanted layout for user interfaces. public class LayoutAttribute : Attribute { /// Specifies the mode of layout. public LayoutMode Mode { get; } /// How much height should the input take. public int RowHeight { get; } /// Maximum row height for the input. public int MaxRowHeight { get; } = 1000; /// Minimum width. The unit is the width of an average character. Not an exact number of pixels. /// This can be used for displaying where the width is a variable, and it's useful with a first guess. public int MinWidth { get; set; } = -1; /// Creates a new instance of layout attribute. public LayoutAttribute(LayoutMode mode, int rowHeight = 1, int maxRowHeight = 1000) { Mode = mode; RowHeight = rowHeight; MaxRowHeight = maxRowHeight; } /// Creates a new instance of layout attribute. public LayoutAttribute() : this(0) { } } /// Specifies that a property finalizes input. public class SubmitAttribute : Attribute { } /// Standard implementation of UserInputInterface for Command Line interfaces public class CliUserInputInterface : IUserInputInterface { class Strings : IStringLocalizer { public static readonly Strings strings = new(); public static string PleaseEnterNumberOrName => strings.Translate("Please enter a number or name "); public static string PleaseEnter => strings.Translate("Please enter "); public static string Default => strings.Translate(" (default)"); } /// /// Thrown when userinput is requested when reading input from stdin and EOF is reached /// internal class EOFException : Exception { /// /// Initializes a new instance of the class with the specified message. /// /// public EOFException(string message) : base(message) { } } private bool IsPipeInput { get; } /// /// Initializes a new instance of the class in interactive mode. /// public CliUserInputInterface() { IsPipeInput = Console.IsInputRedirected; } readonly Mutex userInputMutex = new Mutex(); readonly object readerLock = new object(); private TapThread StartKeyboardReader() { return TapThread.Start(() => { try { var sb = new StringBuilder(); while (true) { var chr = Console.ReadKey(true); if (chr.KeyChar == 0) continue; if (chr.Key != ConsoleKey.Enter && chr.Key != ConsoleKey.Backspace) Console.Write(IsSecure ? '*' : chr.KeyChar); if (chr.Key == ConsoleKey.Enter) { lines.Add(sb.ToString()); sb.Clear(); Console.WriteLine(); } if (chr.Key == ConsoleKey.Backspace) { if (sb.Length > 0) { sb.Remove(sb.Length - 1, 1); // delete current char and move back. Console.Write("\b \b"); } } else { sb.Append(chr.KeyChar); } } } catch (Exception e) { log.Error(e); } }, "Console Reader"); } private bool EOFReached = false; private TapThread StartPipeReader() { return TapThread.Start(() => { const int EOF = -1; int b; var buf = new List(); var stdin = Console.OpenStandardInput(); do { b = stdin.ReadByte(); switch (b) { // If the line was terminaed, add the buffer to the list of lines, and clear the buffer. case '\n': case EOF: lines.Add(Encoding.UTF8.GetString(buf.ToArray())); buf.Clear(); break; // Otherwise, add the char we just read to the buffer default: buf.Add((byte)b); break; } } while (b != EOF); EOFReached = true; }, "Pipe Reader"); } private void MaybeStartReader() { if (readerThread == null) lock (readerLock) if (readerThread == null) { readerThread = IsPipeInput ? StartPipeReader() : StartKeyboardReader(); } } void IUserInputInterface.RequestUserInput(object dataObject, TimeSpan timeout, bool modal) { // If we are running in pipe mode, and EOF was reached, error or return immediately if (EOFReached && IsPipeInput) { throw new EOFException("End of input stream reached."); } MaybeStartReader(); DateTime timeoutAt; if (timeout == TimeSpan.MaxValue) timeoutAt = DateTime.MaxValue; else timeoutAt = DateTime.Now + timeout; if (timeout >= new TimeSpan(0, 0, 0, 0, int.MaxValue)) timeout = new TimeSpan(0, 0, 0, 0, -1); do { if (userInputMutex.WaitOne(timeout)) break; if (DateTime.Now >= timeoutAt) throw new TimeoutException("Request User Input timed out"); } while (true); try { var a = AnnotationCollection.Annotate(dataObject); AnnotationCollection[] members; { var members2 = a.Get()?.Members; if (members2 == null) return; members2 = members2.Concat(a.Get()?.Forwarded ?? Array.Empty()); // Order members members2 = members2.OrderBy(m => m.Get()?.Name ?? m.Get()?.Member.Name); members2 = members2.OrderBy(m => m.Get()?.Order ?? -10000); members2 = members2.OrderBy(m => m.Get()?.Member?.GetAttribute()?.Mode == LayoutMode.FloatBottom ? 1 : 0); members2 = members2.OrderBy(m => m.Get()?.Member?.HasAttribute() == true ? 1 : 0); members = members2.ToArray(); } var display = a.Get(); string title = null; if (display is DisplayAttribute attr && attr.IsDefaultAttribute() == false) title = display.Name; // flush and make sure that there is no new log messages coming in before starting to message the user. Log.Flush(); if (string.IsNullOrWhiteSpace(title)) // fallback magic title = TypeData.GetTypeData(dataObject)?.GetMember("Name")?.GetValue(dataObject) as string; if (string.IsNullOrWhiteSpace(title) == false) { Console.WriteLine(title); } bool isBrowsable(IMemberData m) { var browsable = m.GetAttribute(); // Browsable overrides everything if (browsable != null) return browsable.Browsable; if (m is IMemberData mem) { if (m.HasAttribute()) return true; if (!mem.Writable || !mem.Readable) return false; return true; } return false; } // indent readonly items to the same level int readonlyIndent = 0; foreach (var _message in members) { var layout = _message.Get()?.Member.GetAttribute(); bool showName = layout?.Mode.HasFlag(LayoutMode.FullRow) == true ? false : true; var isReadOnly = _message.Get()?.IsReadOnly ?? false; if (showName && isReadOnly) { var name = _message.Get()?.Name ?? ""; readonlyIndent = Math.Max(name.Length, readonlyIndent); } } foreach (var _message in members) { var mem = _message.Get()?.Member; if (mem != null) { if (!isBrowsable(mem)) continue; } bool secure = _message.Get()?.ReflectionInfo.DescendsTo(typeof(SecureString)) ?? false; var str = _message.Get(); if (str == null && !secure) continue; var name = _message.Get()?.Name ?? ""; start: var isVisible = _message.Get()?.IsVisible ?? true; if (!isVisible) continue; var layout = _message.Get()?.Member.GetAttribute(); bool showName = layout?.Mode.HasFlag(LayoutMode.FullRow) == true ? false : true; var isReadOnly = _message.Get()?.IsReadOnly ?? false; if (isReadOnly) { if (showName) { // Indent // Each text line should be indented separately var text = str.Value; var textLines = text.Replace("\r", "").Split(['\n'], StringSplitOptions.RemoveEmptyEntries); var padString = "\n" + new string(' ', readonlyIndent + ": ".Length); text = string.Join(padString, textLines); Console.WriteLine(name.PadRight(readonlyIndent) + ": " + text); } else { Console.WriteLine($"{str.Value}"); } continue; } var proxy = _message.Get(); List options = null; bool pleaseEnter = true; if (proxy != null) { pleaseEnter = false; options = new List(); int index = 0; var current_value = proxy.SelectedValue; foreach (var value in proxy.AvailableValues) { var v = value.Get(); if (v != null) { Console.Write("{1}: '{0}'", v.Value, index); if (value == current_value) { Console.WriteLine(Strings.Default); } else { Console.WriteLine(); } } options.Add(v?.Value); index++; } Console.Write(Strings.PleaseEnterNumberOrName); } if (pleaseEnter) { Console.Write(Strings.PleaseEnter); } if (secure && showName) { Console.Write($"{name}: "); } else if (showName) if (string.IsNullOrEmpty(str.Value)) Console.Write($"{name}: "); else Console.Write($"{name} ({str.Value}): "); else if (string.IsNullOrEmpty(str.Value) == false) Console.Write($"({str.Value}): "); else Console.WriteLine(":"); if (secure) { var read2 = (awaitReadLine(timeoutAt, true)).Trim(); _message.Get().Value = read2.ToSecureString(); continue; } var read = (awaitReadLine(timeoutAt, false)).Trim(); if (read == "") { // accept the default value. continue; } try { if (options != null && int.TryParse(read, out int result)) { if (result < options.Count) read = options[result]; else goto start; } str.Value = read; var err = a.Get(); IEnumerable errors = err?.Errors; _message.Write(); if (errors?.Any() == true) { Console.WriteLine("Unable to parse value {0}", read); goto start; } } catch (Exception) { Console.WriteLine("Unable to parse '{0}'", read); goto start; } } a.Write(); } finally { userInputMutex.ReleaseMutex(); } } private bool IsSecure; /// /// AwaitReadline reads the line asynchronously. /// This has to be done in a thread, otherwise we cannot abort the test plan in the meantime. /// /// /// string awaitReadLine(DateTime timeOut, bool secure) { try { IsSecure = secure; while (DateTime.Now <= timeOut) { if (lines.TryTake(out string line, 20, TapThread.Current.AbortToken)) { if (IsPipeInput && line == null) { EOFReached = true; // Write an empty line to avoid the exception message appearing as the answer to the prompt Console.WriteLine(); throw new EOFException("End of input stream reached."); } // Echo the output to stdout to indicate which option was picked from the pipe. // Otherwise the stdout of the dialogue becomes confusing if (IsPipeInput && secure == false && !string.IsNullOrWhiteSpace(line)) Console.WriteLine($"<<< '{line}'"); return line ?? ""; } } Console.WriteLine(); log.Info("Timed out while waiting for user input."); throw new TimeoutException("Request user input timed out"); } finally { IsSecure = false; } } TapThread readerThread = null; // Set a capacity on the blocking collection private BlockingCollection lines = new BlockingCollection(1); static readonly SessionLocal isLoaded = new SessionLocal(); /// Loads the CLI user input interface. Note, once it is loaded it cannot be unloaded. public static void Load() { if (!isLoaded.Value) { isLoaded.Value = true; UserInput.SetInterface(new CliUserInputInterface()); } } static readonly TraceSource log = Log.CreateSource("UserInput"); /// /// Acquires a lock on the user input requests, so that user inputs will have to /// wait for this object to be disposed in order to do the request. /// /// A disposable that must be disposed in the same thread as the caller. public static IDisposable AcquireUserInputLock() { if (isLoaded.Value && UserInput.Interface is CliUserInputInterface cli) { cli.userInputMutex.WaitOne(); return Utils.WithDisposable(cli.userInputMutex.ReleaseMutex); } // when CliUserInputInterface is not being used we don't have to do this. return Utils.WithDisposable(Utils.Noop); } } }