1 //
2 // Copyright (c) 2010-2024 Antmicro
3 //
4 // This file is licensed under the MIT License.
5 // Full license text is available in 'licenses/MIT.txt'.
6 //
7 using System;
8 using System.Collections.Generic;
9 using System.Globalization;
10 using System.IO;
11 using System.Linq;
12 using System.Text;
13 using Antmicro.Migrant;
14 using Antmicro.Renode.Core;
15 using Antmicro.Renode.Exceptions;
16 using Antmicro.Renode.Peripherals;
17 using Antmicro.Renode.Peripherals.UART;
18 using Antmicro.Renode.Peripherals.USB;
19 using Antmicro.Renode.Utilities;
20 using TermSharp.Vt100;
21 
22 namespace Antmicro.Renode.Integrations
23 {
24     public static class AsciinemaRecorderExtensions
25     {
RecordToAsciinema(this IUART uart, string filePath, bool useVirtualTimeStamps = false, int width = 80, int height = 24)26         public static void RecordToAsciinema(this IUART uart, string filePath, bool useVirtualTimeStamps = false, int width = 80, int height = 24)
27         {
28             var emulation = EmulationManager.Instance.CurrentEmulation;
29             IMachine machine = null;
30             // This is a temporary solution and should be addressed later
31             var cdcAcmUart = uart as CDCToUARTConverter;
32             if(cdcAcmUart != null)
33             {
34                 // In USB, the CDC ACM UART isn't enabled before enumeration.
35                 // We use Children here, assuming that after enumeration the
36                 // UART will be available.
37                 var devices = cdcAcmUart.Children;
38                 foreach(var device in devices)
39                 {
40                     if(emulation.TryGetMachineForPeripheral(device.Peripheral, out machine))
41                     {
42                         break;
43                     }
44                 }
45 
46                 if(machine == null)
47                 {
48                     throw new RecoverableException("Could not find machine for the given CDC ACM UART");
49                 }
50             }
51             else if(!emulation.TryGetMachineForPeripheral(uart, out machine))
52             {
53                 throw new RecoverableException("Could not find machine for the given UART");
54             }
55             var name = machine.GetAnyNameOrTypeName(uart);
56             var machineName = emulation[machine];
57             var recorder = new AsciinemaRecorder(filePath, machine, name, useVirtualTimeStamps, width, height);
58             emulation.ExternalsManager.AddExternal(recorder, $"{machineName}-{name}-recorder");
59             emulation.Connector.Connect(uart, recorder);
60         }
61     }
62 
63     [Transient]
64     public class AsciinemaRecorder: IConnectable<IUART>, IDisposable, IExternal
65     {
AsciinemaRecorder(string filePath, IMachine machine, string name, bool useVirtualTimeStamps, int width, int height)66         public AsciinemaRecorder(string filePath, IMachine machine, string name, bool useVirtualTimeStamps, int width, int height)
67         {
68             this.filePath = filePath;
69             this.name = name;
70             this.machine = machine;
71             this.decoder = new ByteUtf8Decoder(HandleCharReceived);
72             this.height = height;
73             this.width = width;
74             this.useVirtualTimeStamps = useVirtualTimeStamps;
75         }
76 
AttachTo(IUART uart)77         public void AttachTo(IUART uart)
78         {
79             this.uart = uart;
80             this.uart.CharReceived += decoder.Feed;
81             writer = new StreamWriter(filePath);
82             writer.WriteLine(String.Format(Header, width, height, name));
83         }
84 
DetachFrom(IUART uart)85         public void DetachFrom(IUART uart)
86         {
87             if(this.uart != uart)
88             {
89                 throw new ArgumentException($"Trying to detach unattached UART from {this.GetType().Name}");
90             }
91             this.uart.CharReceived -= decoder.Feed;
92             Dispose();
93         }
94 
Dispose()95         public void Dispose()
96         {
97             writer?.Close();
98             writer = null;
99         }
100 
HandleCharReceived(string data)101         private void HandleCharReceived(string data)
102         {
103             data = EncodeNonPrintableCharacters(data);
104             double time;
105             if(useVirtualTimeStamps)
106             {
107                 time = machine.LocalTimeSource.ElapsedVirtualTime.TotalSeconds;
108             }
109             else
110             {
111                 time = machine.LocalTimeSource.ElapsedHostTime.TotalSeconds;
112             }
113             writer.WriteLine(String.Format(EntryTemplate, time, data));
114         }
115 
EncodeNonPrintableCharacters(string value)116         static string EncodeNonPrintableCharacters(string value)
117         {
118             foreach(var mapping in mappings)
119             {
120                 value = value.Replace(mapping.Item1, mapping.Item2);
121             }
122             var builder = new StringBuilder();
123             var nonPrintable = new UnicodeCategory[]
124             {
125                 UnicodeCategory.Control,
126                 UnicodeCategory.OtherNotAssigned,
127                 UnicodeCategory.Surrogate
128             };
129             foreach(var c in value)
130             {
131                 if(nonPrintable.Contains(char.GetUnicodeCategory(c)) || char.IsControl(c))
132                 {
133                     var encodedValue = "\\u" + ((int)c).ToString("x4");
134                     builder.Append(encodedValue);
135                 }
136                 else
137                 {
138                     builder.Append(c);
139                 }
140             }
141             return builder.ToString();
142         }
143 
144         private static List<Tuple<string, string>> mappings = new List<Tuple<string, string>>
145         {
146             {"\\",     "\\\\"},
147             {"\u0007", "\\a"},
148             {"\u0008", "\\b"},
149             {"\u0009", "\\t"},
150             {"\u000a", "\\n"},
151             {"\u000b", "\\v"},
152             {"\u000c", "\\f"},
153             {"\u000d", "\\r"},
154             {"\"",     "\\\""}
155         };
156 
157         private StreamWriter writer;
158         private IUART uart;
159 
160         private readonly ByteUtf8Decoder decoder;
161         private readonly string name;
162         private readonly IMachine machine;
163         private readonly bool useVirtualTimeStamps;
164         private readonly string filePath;
165         private readonly int width;
166         private readonly int height;
167         private const string EntryTemplate = "[{0}, \"o\", \"{1}\"]";
168         private const string Header = "{{\"version\": 2, \"width\": {0}, \"height\": {1}, \"name\": \"{2}\", \"env\": {{\"TERM\": \"xterm-256color\"}}}}";
169     }
170 }
171