chr
2026-04-05 fe750b791d5b517cc4e9bc8e99a9a75139a0cfba
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
//            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
{
    /// <summary>
    /// Class for getting user input without using GUI.
    /// </summary>
    public class UserInput
    {
        /// <summary> Request user input from the GUI. Waits an amount of time specified by Timeout. If the timeout occurs a TimeoutException will be thrown.</summary>
        /// <param name="dataObject">The object the user should fill out with data.</param>
        /// <param name="Timeout">How long to wait before timing out. </param>
        /// <param name="modal">set to True if a modal request is wanted. This means the user will have to answer before doing anything else.</param>
        public static void Request(object dataObject, TimeSpan Timeout, bool modal = false)
        {
            inputInterface?.RequestUserInput(dataObject, Timeout, modal);
        }
 
        /// <summary> Request user input from the GUI. Waits indefinitely.</summary>
        /// <param name="dataObject">The object the user should fill out with data.</param>
        /// <param name="modal">set to True if a modal request is wanted. This means the user will have to answer before doing anything else.</param>
        public static void Request(object dataObject, bool modal = false)
        {
            inputInterface?.RequestUserInput(dataObject, TimeSpan.MaxValue, modal);
        }
 
        /// <summary> Currently selected interface. </summary>
        public static object Interface => inputInterface;
 
        static readonly SessionLocal<IUserInputInterface> _inputInterface = new SessionLocal<IUserInputInterface>(false);
        static IUserInputInterface inputInterface => _inputInterface.Value;
        static IUserInterface userInterface => inputInterface as IUserInterface;
        /// <summary> Sets the current user input interface. This should almost never be called from user code. </summary>
        /// <param name="inputInterface"></param>
        public static void SetInterface(IUserInputInterface inputInterface)
        {
            _inputInterface.Value = inputInterface;
        }
 
        /// <summary> Call to notify the user interface that an object property has changed. </summary>
        /// <param name="obj"></param>
        /// <param name="property"></param>
        public static void NotifyChanged(object obj, string property)
        {
            userInterface?.NotifyChanged(obj, property);
        }
 
        /// <summary> Gets the current user input interface. </summary>
        /// <returns></returns>
        public static IUserInputInterface GetInterface() => inputInterface;
    }
 
    /// <summary> Defines a way for plugins to notify the user that a property has changed. </summary>
    public interface IUserInterface
    {
        /// <summary>
        /// This method is called to notify that a property has changed on an object.
        /// </summary>
        /// <param name="obj"></param>
        /// <param name="property"></param>
        void NotifyChanged(object obj, string property);
    }
 
    /// <summary> Defines a way for plugins to request input from the user. </summary>
    public interface IUserInputInterface
    {
        /// <summary> The method called when the interface requests user input.</summary>
        /// <param name="dataObject">The object the user should fill out with data.</param>
        /// <param name="Timeout">How long the user should have.</param>
        /// <param name="modal"> True if a modal request is wanted</param>
        void RequestUserInput(object dataObject, TimeSpan Timeout, bool modal);
    }
 
    /// <summary> The supported layout modes. </summary>
    [Flags]
    public enum LayoutMode
    {
        /// <summary> The default mode.</summary>
        Normal = 1,
        /// <summary> The user input fills the whole row. </summary>
        FullRow = 2,
        /// <summary> The user input floats to the bottom.</summary>
        FloatBottom = 4,
        ///<summary> Whether text wrapping should be enabled. </summary>
        WrapText = 8
    }
 
    /// <summary> LayoutAttribute can be used to specify the wanted layout for user interfaces.</summary>
    public class LayoutAttribute : Attribute
    {
        /// <summary> Specifies the mode of layout.</summary>
        public LayoutMode Mode { get; }
 
        /// <summary> How much height should the input take.  </summary>
        public int RowHeight { get; }
 
        /// <summary> Maximum row height for the input. </summary>
        public int MaxRowHeight { get; } = 1000;
 
        /// <summary> 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.</summary>
        public int MinWidth { get; set; } = -1;
 
        /// <summary> Creates a new instance of layout attribute. </summary>
        public LayoutAttribute(LayoutMode mode, int rowHeight = 1, int maxRowHeight = 1000)
        {
            Mode = mode;
            RowHeight = rowHeight;
            MaxRowHeight = maxRowHeight;
        }
 
        /// <summary> Creates a new instance of layout attribute. </summary>
        public LayoutAttribute() : this(0)
        {
 
        }
    }
 
    /// <summary> Specifies that a property finalizes input.</summary>
    public class SubmitAttribute : Attribute { }
 
    /// <summary> Standard implementation of UserInputInterface for Command Line interfaces</summary>
    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)");
        }
 
        /// <summary>
        /// Thrown when userinput is requested when reading input from stdin and EOF is reached
        /// </summary>
        internal class EOFException : Exception
        {
            /// <summary>
            /// Initializes a new instance of the <see cref="EOFException"/> class with the specified message.
            /// </summary>
            /// <param name="message"></param>
            public EOFException(string message) : base(message)
            {
            }
        }
 
        private bool IsPipeInput { get; }
 
        /// <summary>
        /// Initializes a new instance of the <see cref="CliUserInputInterface"/> class in interactive mode. 
        /// </summary>
        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<byte>();
                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<IMembersAnnotation>()?.Members;
 
                    if (members2 == null) return;
                    members2 = members2.Concat(a.Get<IForwardedAnnotations>()?.Forwarded ?? Array.Empty<AnnotationCollection>());
 
                    // Order members
                    members2 = members2.OrderBy(m => m.Get<DisplayAttribute>()?.Name ?? m.Get<IMemberAnnotation>()?.Member.Name);
                    members2 = members2.OrderBy(m => m.Get<DisplayAttribute>()?.Order ?? -10000);
                    members2 = members2.OrderBy(m => m.Get<IMemberAnnotation>()?.Member?.GetAttribute<LayoutAttribute>()?.Mode == LayoutMode.FloatBottom ? 1 : 0);
                    members2 = members2.OrderBy(m => m.Get<IMemberAnnotation>()?.Member?.HasAttribute<SubmitAttribute>() == true ? 1 : 0);
                    members = members2.ToArray();
                }
 
                var display = a.Get<IDisplayAnnotation>();
 
                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<System.ComponentModel.BrowsableAttribute>();
 
                    // Browsable overrides everything
                    if (browsable != null) return browsable.Browsable;
 
                    if (m is IMemberData mem)
                    {
                        if (m.HasAttribute<OutputAttribute>())
                            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<IMemberAnnotation>()?.Member.GetAttribute<LayoutAttribute>();
                    bool showName = layout?.Mode.HasFlag(LayoutMode.FullRow) == true ? false : true;
                    var isReadOnly = _message.Get<IAccessAnnotation>()?.IsReadOnly ?? false;
                    if (showName && isReadOnly)
                    {
                        var name = _message.Get<DisplayAttribute>()?.Name ?? "";
                        readonlyIndent = Math.Max(name.Length, readonlyIndent);
                    }
                }
                
                foreach (var _message in members)
                {
                    var mem = _message.Get<IMemberAnnotation>()?.Member;
                    if (mem != null)
                    {
                        if (!isBrowsable(mem)) continue;
                    }
 
                    bool secure = _message.Get<IReflectionAnnotation>()?.ReflectionInfo.DescendsTo(typeof(SecureString)) ?? false;
                    var str = _message.Get<IStringValueAnnotation>();
                    if (str == null && !secure) continue;
                    var name = _message.Get<DisplayAttribute>()?.Name ?? "";
 
                start:
                    var isVisible = _message.Get<IAccessAnnotation>()?.IsVisible ?? true;
                    if (!isVisible) continue;
 
                    var layout = _message.Get<IMemberAnnotation>()?.Member.GetAttribute<LayoutAttribute>();
                    bool showName = layout?.Mode.HasFlag(LayoutMode.FullRow) == true ? false : true;
 
                    var isReadOnly = _message.Get<IAccessAnnotation>()?.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<IAvailableValuesAnnotationProxy>();
                    List<string> options = null;
                    bool pleaseEnter = true;
                    if (proxy != null)
                    {
                        pleaseEnter = false;
                        options = new List<string>();
 
                        int index = 0;
                        var current_value = proxy.SelectedValue;
                        foreach (var value in proxy.AvailableValues)
                        {
                            var v = value.Get<IStringValueAnnotation>();
                            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<IObjectValueAnnotation>().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<IErrorAnnotation>();
                        IEnumerable<string> 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;
 
        /// <summary>
        /// AwaitReadline reads the line asynchronously.  
        /// This has to be done in a thread, otherwise we cannot abort the test plan in the meantime. </summary>
        /// <param name="timeOut"></param>
        /// <param name="secure"></param>
        /// <returns></returns>
        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<string> lines = new BlockingCollection<string>(1);
 
        static readonly SessionLocal<bool> isLoaded = new SessionLocal<bool>();
 
        /// <summary> Loads the CLI user input interface. Note, once it is loaded it cannot be unloaded. </summary>
        public static void Load()
        {
            if (!isLoaded.Value)
            {
                isLoaded.Value = true;
                UserInput.SetInterface(new CliUserInputInterface());
            }
        }
 
        static readonly TraceSource log = Log.CreateSource("UserInput");
 
        /// <summary>
        /// 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.
        /// </summary>
        /// <returns>A disposable that must be disposed in the same thread as the caller.</returns>
        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);
        }
    }
}