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