// 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);
}
}
}