/*---------------------------------------------------------------------------* Copyright (C) Nintendo. All rights reserved. These coded instructions, statements, and computer programs contain proprietary information of Nintendo of America Inc. and/or Nintendo Company Ltd., and are protected by Federal copyright law. They may not be disclosed to third parties or copied or duplicated in any form, in whole or in part, without the prior written consent of Nintendo. *---------------------------------------------------------------------------*/ using System; using System.Collections.Generic; using System.Diagnostics; using System.Globalization; using System.IO; using System.Net; using System.Net.NetworkInformation; using System.Net.Sockets; using System.Runtime.InteropServices; using System.Text; using System.Threading; using CafeX; namespace Nintendo.SDSG { public class KeyboardInputProcessorBase { protected static readonly string DK_CONSOLE_EXPIRE_MESSAGE = "cattoucan: Demand enabling input because {0} seconds have elapsed."; protected enum FileType { Unknown, Disk, Char, Pipe }; protected enum StdHandle { Stdin = -10, Stdout = -11, Stderr = -12 }; protected const uint KEY_EVENT = 0x0001; // Event contains key event record protected const uint MOUSE_EVENT = 0x0002; // Event contains mouse event record protected const uint WINDOW_BUFFER_SIZE_EVENT = 0x0004; // Event contains window change event record protected const uint MENU_EVENT = 0x0008; // Event contains menu event record protected const uint FOCUS_EVENT = 0x0010; // event contains focus change protected const uint ENABLE_ECHO_INPUT = 0x0004; protected const uint ENABLE_LINE_INPUT = 0x0002; protected const uint ENABLE_PROCESSED_INPUT = 0x0001; protected const int STD_INPUT_HANDLE = -10; protected const int KEY_POOLING_WAIT_MSECONDS = 20; protected static int THREAD_JOIN_TIMEOUT_MSECONDS = 1000; protected static int DK_INPUT_BUFFER_MAX_SIZE = 1024 * 4; // max 1024 Unicode chars protected static double DK_READY_TIMEOUT = 8.0; protected Stream input_stream; protected Thread input_thread; protected EventWaitHandle wait_handle = new EventWaitHandle(false, EventResetMode.AutoReset); protected volatile bool bAborted = false; protected volatile bool enableDKInput = false; protected StringBuilder DKinputBuffer = new StringBuilder(); protected NetworkStream stream; protected byte[] dk_input_buffer = new byte[DK_INPUT_BUFFER_MAX_SIZE]; protected int dk_input_count = 0; protected DateTime startTime = DateTime.Now; protected Encoding targetEncoding = null; protected Encoding utf8Encoding = new UTF8Encoding(); protected Encoding unicodeEncoding = new System.Text.UnicodeEncoding(); protected byte[] UP_ARROW_BYTES = new byte[] { 0x1B, 0x5B, 0x41 }; protected byte[] DOWN_ARROW_BYTES = new byte[] { 0x1B, 0x5B, 0x42 }; protected byte[] RIGHT_ARROW_BYTES = new byte[] { 0x1B, 0x5B, 0x43 }; protected byte[] LEFT_ARROW_BYTES = new byte[] { 0x1B, 0x5B, 0x44 }; protected byte[] BACKSPACE_BYTES = new byte[] { 0x7F }; protected byte[] ESCAPE_BYTES = new byte[] { 0x1B }; protected byte[] END_BYTES = new byte[] { 0x1B, 0x5B, 0x34, 0x7E }; protected byte[] HOME_BYTES = new byte[] { 0x1B, 0x5B, 0x31, 0x7E }; protected byte[] DELETE_BYTES = new byte[] { 0x1B, 0x5B, 0x33, 0x7E }; protected byte[] INSERT_BYTES = new byte[] { 0x1B, 0x5B, 0x32, 0x7E }; protected byte[] PG_UP_BYTES = new byte[] { 0x1B, 0x5B, 0x35, 0x7E }; protected byte[] PG_DOWN_BYTES = new byte[] { 0x1B, 0x5B, 0x36, 0x7E }; // Function keys - - request from SDSG. protected byte[] F1_BYTES = new byte[] { 0x1B, 0x5B, 0x5B, 0x41 }; protected byte[] F2_BYTES = new byte[] { 0x1B, 0x5B, 0x5B, 0x42 }; protected byte[] F3_BYTES = new byte[] { 0x1B, 0x5B, 0x5B, 0x43 }; protected byte[] F4_BYTES = new byte[] { 0x1B, 0x5B, 0x5B, 0x44 }; protected byte[] F5_BYTES = new byte[] { 0x1B, 0x5B, 0x5B, 0x45 }; protected byte[] F6_BYTES = new byte[] { 0x1B, 0x5B, 0x31, 0x37, 0x7E }; protected byte[] F7_BYTES = new byte[] { 0x1B, 0x5B, 0x31, 0x38, 0x7E }; protected byte[] F8_BYTES = new byte[] { 0x1B, 0x5B, 0x31, 0x39, 0x7E }; protected byte[] F9_BYTES = new byte[] { 0x1B, 0x5B, 0x32, 0x30, 0x7E }; protected byte[] F10_BYTES = new byte[] { 0x1B, 0x5B, 0x32, 0x31, 0x7E }; protected byte[] F11_BYTES = new byte[] { 0x1B, 0x5B, 0x32, 0x33, 0x7E }; protected byte[] F12_BYTES = new byte[] { 0x1B, 0x5B, 0x32, 0x34, 0x7E }; #region P-Invoke [StructLayout(LayoutKind.Explicit)] public struct INPUT_RECORD { [FieldOffset(0)] public ushort EventType; [FieldOffset(4)] public KEY_EVENT_RECORD KeyEvent; [FieldOffset(4)] public MOUSE_EVENT_RECORD MouseEvent; [FieldOffset(4)] public WINDOW_BUFFER_SIZE_RECORD WindowBufferSizeEvent; [FieldOffset(4)] public MENU_EVENT_RECORD MenuEvent; [FieldOffset(4)] public FOCUS_EVENT_RECORD FocusEvent; }; [StructLayout(LayoutKind.Explicit, CharSet = CharSet.Unicode)] public struct KEY_EVENT_RECORD { [FieldOffset(0), MarshalAs(UnmanagedType.Bool)] public bool bKeyDown; [FieldOffset(4), MarshalAs(UnmanagedType.U2)] public ushort wRepeatCount; [FieldOffset(6), MarshalAs(UnmanagedType.U2)] //public VirtualKeys wVirtualKeyCode; public ushort wVirtualKeyCode; [FieldOffset(8), MarshalAs(UnmanagedType.U2)] public ushort wVirtualScanCode; [FieldOffset(10)] public char UnicodeChar; [FieldOffset(12), MarshalAs(UnmanagedType.U4)] //public ControlKeyState dwControlKeyState; public uint dwControlKeyState; } [StructLayout(LayoutKind.Sequential)] public struct COORD { public short X; public short Y; } public struct SMALL_RECT { public short Left; public short Top; public short Right; public short Bottom; } [StructLayout(LayoutKind.Sequential)] public struct MOUSE_EVENT_RECORD { public COORD dwMousePosition; public uint dwButtonState; public uint dwControlKeyState; public uint dwEventFlags; } public struct WINDOW_BUFFER_SIZE_RECORD { public COORD dwSize; public WINDOW_BUFFER_SIZE_RECORD(short x, short y) { dwSize = new COORD(); dwSize.X = x; dwSize.Y = y; } } [StructLayout(LayoutKind.Sequential)] public struct MENU_EVENT_RECORD { public uint dwCommandId; } [StructLayout(LayoutKind.Sequential)] public struct FOCUS_EVENT_RECORD { public uint bSetFocus; } //CHAR_INFO struct, which was a union in the old days // so we want to use LayoutKind.Explicit to mimic it as closely // as we can [StructLayout(LayoutKind.Explicit)] public struct CHAR_INFO { [FieldOffset(0)] public char UnicodeChar; [FieldOffset(0)] public char AsciiChar; [FieldOffset(2)] //2 bytes seems to work properly public UInt16 Attributes; } [StructLayout(LayoutKind.Sequential)] public struct CONSOLE_CURSOR_INFO { public uint Size; public bool Visible; } [StructLayout(LayoutKind.Sequential)] public struct CONSOLE_HISTORY_INFO { public ushort cbSize; public ushort HistoryBufferSize; public ushort NumberOfHistoryBuffers; public uint dwFlags; } [StructLayout(LayoutKind.Sequential)] public struct CONSOLE_SELECTION_INFO { public uint Flags; public COORD SelectionAnchor; public SMALL_RECT Selection; public const uint CONSOLE_MOUSE_DOWN = 0x0008; // Mouse is down public const uint CONSOLE_MOUSE_SELECTION = 0x0004; //Selecting with the mouse public const uint CONSOLE_NO_SELECTION = 0x0000; //No selection public const uint CONSOLE_SELECTION_IN_PROGRESS = 0x0001; //Selection has begun public const uint CONSOLE_SELECTION_NOT_EMPTY = 0x0002; //Selection rectangle is not empty } public enum CtrlTypes : uint { CTRL_C_EVENT = 0, CTRL_BREAK_EVENT, CTRL_CLOSE_EVENT, CTRL_LOGOFF_EVENT = 5, CTRL_SHUTDOWN_EVENT } public enum VirtualKeys : ushort { LeftButton = 0x01, RightButton = 0x02, Cancel = 0x03, MiddleButton = 0x04, ExtraButton1 = 0x05, ExtraButton2 = 0x06, Back = 0x08, Tab = 0x09, Clear = 0x0C, Return = 0x0D, Shift = 0x10, Control = 0x11, Menu = 0x12, Pause = 0x13, Kana = 0x15, Hangeul = 0x15, Hangul = 0x15, Junja = 0x17, Final = 0x18, Hanja = 0x19, Kanji = 0x19, Escape = 0x1B, Convert = 0x1C, NonConvert = 0x1D, Accept = 0x1E, ModeChange = 0x1F, Space = 0x20, Prior = 0x21, Next = 0x22, End = 0x23, Home = 0x24, Left = 0x25, Up = 0x26, Right = 0x27, Down = 0x28, Select = 0x29, Print = 0x2A, Execute = 0x2B, Snapshot = 0x2C, Insert = 0x2D, Delete = 0x2E, Help = 0x2F, N0 = 0x30, N1 = 0x31, N2 = 0x32, N3 = 0x33, N4 = 0x34, N5 = 0x35, N6 = 0x36, N7 = 0x37, N8 = 0x38, N9 = 0x39, A = 0x41, B = 0x42, C = 0x43, D = 0x44, E = 0x45, F = 0x46, G = 0x47, H = 0x48, I = 0x49, J = 0x4A, K = 0x4B, L = 0x4C, M = 0x4D, N = 0x4E, O = 0x4F, P = 0x50, Q = 0x51, R = 0x52, S = 0x53, T = 0x54, U = 0x55, V = 0x56, W = 0x57, X = 0x58, Y = 0x59, Z = 0x5A, LeftWindows = 0x5B, RightWindows = 0x5C, Application = 0x5D, Sleep = 0x5F, Numpad0 = 0x60, Numpad1 = 0x61, Numpad2 = 0x62, Numpad3 = 0x63, Numpad4 = 0x64, Numpad5 = 0x65, Numpad6 = 0x66, Numpad7 = 0x67, Numpad8 = 0x68, Numpad9 = 0x69, Multiply = 0x6A, Add = 0x6B, Separator = 0x6C, Subtract = 0x6D, Decimal = 0x6E, Divide = 0x6F, F1 = 0x70, F2 = 0x71, F3 = 0x72, F4 = 0x73, F5 = 0x74, F6 = 0x75, F7 = 0x76, F8 = 0x77, F9 = 0x78, F10 = 0x79, F11 = 0x7A, F12 = 0x7B, F13 = 0x7C, F14 = 0x7D, F15 = 0x7E, F16 = 0x7F, F17 = 0x80, F18 = 0x81, F19 = 0x82, F20 = 0x83, F21 = 0x84, F22 = 0x85, F23 = 0x86, F24 = 0x87, NumLock = 0x90, ScrollLock = 0x91, NEC_Equal = 0x92, Fujitsu_Jisho = 0x92, Fujitsu_Masshou = 0x93, Fujitsu_Touroku = 0x94, Fujitsu_Loya = 0x95, Fujitsu_Roya = 0x96, LeftShift = 0xA0, RightShift = 0xA1, LeftControl = 0xA2, RightControl = 0xA3, LeftMenu = 0xA4, RightMenu = 0xA5, BrowserBack = 0xA6, BrowserForward = 0xA7, BrowserRefresh = 0xA8, BrowserStop = 0xA9, BrowserSearch = 0xAA, BrowserFavorites = 0xAB, BrowserHome = 0xAC, VolumeMute = 0xAD, VolumeDown = 0xAE, VolumeUp = 0xAF, MediaNextTrack = 0xB0, MediaPrevTrack = 0xB1, MediaStop = 0xB2, MediaPlayPause = 0xB3, LaunchMail = 0xB4, LaunchMediaSelect = 0xB5, LaunchApplication1 = 0xB6, LaunchApplication2 = 0xB7, OEM1 = 0xBA, OEMPlus = 0xBB, OEMComma = 0xBC, OEMMinus = 0xBD, OEMPeriod = 0xBE, OEM2 = 0xBF, OEM3 = 0xC0, OEM4 = 0xDB, OEM5 = 0xDC, OEM6 = 0xDD, OEM7 = 0xDE, OEM8 = 0xDF, OEMAX = 0xE1, OEM102 = 0xE2, ICOHelp = 0xE3, ICO00 = 0xE4, ProcessKey = 0xE5, ICOClear = 0xE6, Packet = 0xE7, OEMReset = 0xE9, OEMJump = 0xEA, OEMPA1 = 0xEB, OEMPA2 = 0xEC, OEMPA3 = 0xED, OEMWSCtrl = 0xEE, OEMCUSel = 0xEF, OEMATTN = 0xF0, OEMFinish = 0xF1, OEMCopy = 0xF2, OEMAuto = 0xF3, OEMENLW = 0xF4, OEMBackTab = 0xF5, ATTN = 0xF6, CRSel = 0xF7, EXSel = 0xF8, EREOF = 0xF9, Play = 0xFA, Zoom = 0xFB, Noname = 0xFC, PA1 = 0xFD, OEMClear = 0xFE } [DllImport("kernel32.dll")] protected static extern FileType GetFileType(IntPtr hdl); [DllImport("kernel32.dll")] protected static extern IntPtr GetStdHandle(StdHandle std); [DllImport("Kernel32.dll")] protected static extern bool SetConsoleMode(IntPtr hConsoleHandle, uint dwMode); [DllImport("Kernel32.dll")] protected static extern bool GetConsoleMode(IntPtr hConsoleHandle, out uint lpMode); [DllImport("Kernel32.dll")] protected static extern uint GetLastError(); [DllImport("kernel32.dll")] protected static extern bool GetNumberOfConsoleInputEvents(IntPtr hConsoleInput, out uint lpcNumberOfEvents); [DllImport("kernel32.dll", EntryPoint = "ReadConsoleInputW", CharSet = CharSet.Unicode)] protected static extern bool ReadConsoleInput(IntPtr hConsoleInput, [Out] INPUT_RECORD[] lpBuffer, uint nLength, out uint lpNumberOfEventsRead); #endregion public KeyboardInputProcessorBase(Encoding targetEncoding) { if (targetEncoding == null) { throw new ArgumentNullException("targetEncoding"); } this.targetEncoding = targetEncoding; } public void Abort() { this.bAborted = true; wait_handle.Set(); // Wait for the input_thread to finish try { if (input_thread != null) { bool success = input_thread.Join(THREAD_JOIN_TIMEOUT_MSECONDS); if (!success) { this.input_thread.Abort(); } } } catch (ThreadStateException) { // Thread had already ended. } catch (ThreadInterruptedException) { // Main application thread (the one calling Abort) is being killed. } catch (ThreadAbortException) { // Exception when calling abort. } } public void EnableDKInput() { if (this.enableDKInput == true) { // Nothing to do. This is the case when the DK Console message came after the internal timeout to // enable dk console expired, which set the variable already. return; } lock (this.dk_input_buffer.SyncRoot) { if (this.enableDKInput == false) { if (this.dk_input_count > 0) { // Flush buffer FlushDKBuffer(); } // Disabling buffering this.enableDKInput = true; } } } protected void BufferOrDiscardDKInput(ref byte[] buffer, int bytesToBuffer) { lock (this.dk_input_buffer.SyncRoot) { if (this.enableDKInput == false) { // Check if the time has expired if (this.startTime.AddSeconds(DK_READY_TIMEOUT).CompareTo(DateTime.Now) < 0) { // Flush buffer Console.WriteLine(DK_CONSOLE_EXPIRE_MESSAGE, DK_READY_TIMEOUT); this.FlushDKBuffer(); this.sendBuffer(ref buffer, 0, bytesToBuffer); this.enableDKInput = true; } else { // Only keep buffering if there is space if (this.dk_input_count + bytesToBuffer >= DK_INPUT_BUFFER_MAX_SIZE) { Console.WriteLine("cattoucan : input '{0}' discarded!", buffer); } else { // there is space, keep buffering buffer.CopyTo(this.dk_input_buffer, this.dk_input_count); this.dk_input_count += bytesToBuffer; } } } } } protected void FlushDKBuffer() { // Check if there is something to flush. if (this.dk_input_count > 0) { try { this.sendBuffer(ref this.dk_input_buffer, 0, this.dk_input_count); } catch (IOException) { // Network must be shutting down } // Clean up the buffer Array.Clear(this.dk_input_buffer, 0, this.dk_input_count); this.dk_input_count = 0; } } protected void sendBuffer(ref byte[] byteBuffer, int start, int size) { //Replace '\13' for '\10' for (int counter = start; counter < (start + size); ++counter) { if (byteBuffer[counter] == 13) { byteBuffer[counter] = 10; } } stream.Write(byteBuffer, start, size); } } public class InputThread : KeyboardInputProcessorBase { TcpClient tcp_client; int bytes_read; public InputThread(TcpClient tcp) : base(Encoding.Default) { tcp_client = tcp; stream = tcp.GetStream(); input_stream = null; input_thread = null; } public InputThread(TcpClient tcp, Stream input, Encoding targetEncoding) : base(targetEncoding) { tcp_client = tcp; input_stream = input; stream = tcp.GetStream(); input_thread = new Thread(input_thread_proc); wait_handle = new EventWaitHandle(false, EventResetMode.AutoReset); input_thread.Start(); } private void read_callback(IAsyncResult ar) { try { bytes_read = input_stream.EndRead(ar); } catch (IOException) { Thread.Sleep(100); bytes_read = 0; } wait_handle.Set(); } private void input_thread_proc() { byte[] buffer = new byte[256]; this.startTime = DateTime.Now; while (!this.bAborted) { try { input_stream.BeginRead(buffer, 0, buffer.Length, read_callback, null); wait_handle.WaitOne(); if ((this.bytes_read != 0) && (!this.bAborted)) { //IMPORTANT Do not encode to this stream. Just send the bytes directly. if (this.enableDKInput == true) { this.sendBuffer(ref buffer, 0, this.bytes_read); } else // not enabled yet, should keep buffering { lock (this.dk_input_buffer.SyncRoot) { if (this.enableDKInput == false) { // Check if the time has expired if (startTime.AddSeconds(DK_READY_TIMEOUT).CompareTo(DateTime.Now) < 0) { // Flush buffer Console.WriteLine(DK_CONSOLE_EXPIRE_MESSAGE, DK_READY_TIMEOUT); this.FlushDKBuffer(); this.sendBuffer(ref buffer, 0, this.bytes_read); this.enableDKInput = true; } else { // Only keep buffering if there is space this.BufferOrDiscardDKInput(ref buffer, this.bytes_read); } } // enableDKInput == false (check inside lock) } // lock } // enableDKInput == false Array.Clear(buffer, 0, this.bytes_read); }// has bytes and not aborted } catch (ThreadAbortException) { return; } catch (IOException) { return; } } // while not aborted } } public class ConsoleInputNative : KeyboardInputProcessorBase { public ConsoleInputNative(TcpClient tcp, Encoding targetEncoding) : base(targetEncoding) { stream = tcp.GetStream(); input_thread = new Thread(input_thread_proc); input_thread.Start(); } private void input_thread_proc() { IntPtr INVALID_HANDLE_VALUE = new IntPtr(-1); IntPtr handle = GetStdHandle(StdHandle.Stdin); uint mode; if (handle == IntPtr.Zero || handle == INVALID_HANDLE_VALUE) { Console.WriteLine("cafex : unable to initialize keyboard input!"); return; } if (!GetConsoleMode(handle, out mode)) { Console.WriteLine("cafex : unable to initialize keyboard input!"); return; } mode &= ~(ENABLE_ECHO_INPUT | ENABLE_LINE_INPUT); if (!SetConsoleMode(handle, mode)) { Console.WriteLine("cafex : unable to initialize keyboard input!"); return; } uint numEvents = 0; uint numEventsRead = 0; this.startTime = DateTime.Now; while (!bAborted) { GetNumberOfConsoleInputEvents(handle, out numEvents); if (numEvents != 0) { INPUT_RECORD[] eventBuffer = new INPUT_RECORD[128]; ; ReadConsoleInput(handle, eventBuffer, 128, out numEventsRead); for (int eventCounter = 0; eventCounter < numEventsRead; ++eventCounter) { INPUT_RECORD eventReceived = eventBuffer[eventCounter]; { if (eventReceived.EventType == KEY_EVENT && eventReceived.KeyEvent.bKeyDown) { byte[] byteBuffer = null; bool handled = false; // Check for arrows first switch (eventReceived.KeyEvent.wVirtualKeyCode) { case (ushort)VirtualKeys.Escape: byteBuffer = ESCAPE_BYTES; handled = true; break; case (ushort)VirtualKeys.Up: byteBuffer = UP_ARROW_BYTES; handled = true; break; case (ushort)VirtualKeys.Down: byteBuffer = DOWN_ARROW_BYTES; handled = true; break; case (ushort)VirtualKeys.Left: byteBuffer = LEFT_ARROW_BYTES; handled = true; break; case (ushort)VirtualKeys.Right: byteBuffer = RIGHT_ARROW_BYTES; handled = true; break; case (ushort)VirtualKeys.Home: byteBuffer = HOME_BYTES; handled = true; break; case (ushort)VirtualKeys.End: byteBuffer = END_BYTES; handled = true; break; case (ushort)VirtualKeys.Insert: byteBuffer = INSERT_BYTES; handled = true; break; case (ushort)VirtualKeys.Delete: byteBuffer = DELETE_BYTES; handled = true; break; case (ushort)VirtualKeys.Prior: byteBuffer = PG_UP_BYTES; handled = true; break; case (ushort)VirtualKeys.Next: byteBuffer = PG_DOWN_BYTES; handled = true; break; case (ushort)VirtualKeys.F1: byteBuffer = F1_BYTES; handled = true; break; case (ushort)VirtualKeys.F2: byteBuffer = F2_BYTES; handled = true; break; case (ushort)VirtualKeys.F3: byteBuffer = F3_BYTES; handled = true; break; case (ushort)VirtualKeys.F4: byteBuffer = F4_BYTES; handled = true; break; case (ushort)VirtualKeys.F5: byteBuffer = F5_BYTES; handled = true; break; case (ushort)VirtualKeys.F6: byteBuffer = F6_BYTES; handled = true; break; case (ushort)VirtualKeys.F7: byteBuffer = F7_BYTES; handled = true; break; case (ushort)VirtualKeys.F8: byteBuffer = F8_BYTES; handled = true; break; case (ushort)VirtualKeys.F9: byteBuffer = F9_BYTES; handled = true; break; case (ushort)VirtualKeys.F10: byteBuffer = F10_BYTES; handled = true; break; case (ushort)VirtualKeys.F11: byteBuffer = F11_BYTES; handled = true; break; case (ushort)VirtualKeys.F12: byteBuffer = F12_BYTES; handled = true; break; } // Check for Unicode chars then if (!handled && eventReceived.KeyEvent.UnicodeChar != 0) { // prepare the data. byteBuffer = Encoding.Convert(this.unicodeEncoding, this.utf8Encoding, this.unicodeEncoding.GetBytes(new char[] { eventReceived.KeyEvent.UnicodeChar })); } if (byteBuffer != null && byteBuffer.Length > 0) { if (this.enableDKInput) { // Can send right now this.sendBuffer(ref byteBuffer, 0, byteBuffer.Length); } else { // Need to buffer this.BufferOrDiscardDKInput(ref byteBuffer, byteBuffer.Length); } } } } } } Thread.Sleep(KEY_POOLING_WAIT_MSECONDS); } } } public class CatToucan { private const double DEBUG_PORT_CONNECTION_TIMEOUT = 30.0; // 30s private Stream input_stream = null; private bool exclusive_match = false; private TcpClient tcp = null; private Semaphore wait_handle = new Semaphore(0, 2); private int bytes_read = 0; private IPEndPoint m_portcombo_to_listen_to = new IPEndPoint(IPAddress.Loopback, 6001); private StringBuilder m_string_builder = new StringBuilder(); private KeyboardInputProcessorBase input_thread = null; private Encoding utf8Encoding = new UTF8Encoding(); private List> matchNotify = new List>(); private List>> matchCallback = new List>>(); private ParserState currentState = ParserState.NormalParsing; public static class Terms { public static string IOP_START = "USB Trace: Activating root hubs"; public static string PPC_START = "PPC Init"; public static string PPC_SHUTDOWN_COMPLETED = "***PPC SHUTDOWN COMPLETED***"; public static string VPAD_INIT_END = "VPAD Init end"; public static string PPC_FAST_CYCLE = "***PPC FAST CYCLE***"; public static string DEVICE_REMOTE_COMMANDS = "Device remote commands:"; public static string KI_PIREPARE_TITLE_ASYNC = "DIAG:Issued KiPrepareTitleAsync"; public static string SYSTEM_BUSY = "System busy, try again"; public static string DK_READY_MESSAGE = "[+-* DK is ready for console input *-+]"; public static string MENU_SOURCE = "source -p -q /vol/content"; public static string COS_LAUNCH = "cos launch 0x"; } public class ExitReason { public enum Reason { StoppedByUser, StoppedDueToDroppedConnection, StoppedByApplicationExit, StoppedByWaitingForDebugger, StoppedByAlternateTermination, NoExit } public Reason reason; public Int32 AppExitCode; public ExitReason(Reason r ,Int32 app_exit_code) { reason = r; AppExitCode = app_exit_code; } } /// /// Uses the default portcombo of localhost:6001 /// public CatToucan(int port) : this(new IPEndPoint(IPAddress.Loopback, port)) { } /// /// Uses the specified portcombo. /// public CatToucan(IPEndPoint character_stream_server_location) { if (character_stream_server_location == null) { m_portcombo_to_listen_to = new IPEndPoint(IPAddress.Loopback, 6001); } else { m_portcombo_to_listen_to = character_stream_server_location; } } public bool ExclusiveMatch { get { return exclusive_match; } set { exclusive_match = value; } } public int Port { get { return m_portcombo_to_listen_to.Port; } } ~CatToucan() { wait_handle.Close(); wait_handle = null; } /// /// Contains the application return value, if any. /// public int? ReturnCodeThatWeSnooped { get; private set; } /// /// Adds an event that will be set if 'match' string is found in the output stream. /// /// The string and event key-value pair to add to the list public void AddNotification(KeyValuePair kvp) { matchNotify.Add(kvp); } /// /// Adds a callback that will be executed if 'match' string is found in the output stream. /// /// The string and callback key-value pair to add to the list public void AddNotification(KeyValuePair> kvp) { int already = matchCallback.IndexOf(kvp); if(already >= 0) { #if DEBUG CafeX.Log.WriteLine("Overriding existing handler for '{0}'!", kvp.Key); #endif matchCallback.RemoveAt(already); } matchCallback.Add(kvp); } /// /// Clear all of the string match events /// public void ClearNotifications() { matchNotify.Clear(); matchCallback.Clear(); } private void read_callback(IAsyncResult ar) { try { try { int read = input_stream.EndRead(ar); lock (wait_handle) { if (bytes_read >= 0) { bytes_read = read; } } } catch (IOException) { lock (wait_handle) { if (bytes_read > 0) { bytes_read = 0; } } if (bytes_read < 0) { return; } Thread.Sleep(100); } wait_handle.Release(); } catch (ObjectDisposedException) { } } public void StopRelay() { lock (wait_handle) { bytes_read = -1; } wait_handle.Release(); } /// /// Monitors the portcombo and relays the data to the specified output stream. /// public ExitReason RelaySerialCharacters(TextWriter output_stream, bool timeout) { // Uncomment this line to prevent a timeout. // timeout = false; tcp = new TcpClient(); // connection loop { DateTime now = DateTime.Now; DateTime give_up = now.AddSeconds(DEBUG_PORT_CONNECTION_TIMEOUT); while (tcp.Connected == false) { try { tcp.Connect(m_portcombo_to_listen_to); } catch { if (DateTime.Now > give_up) { return new ExitReason(ExitReason.Reason.StoppedDueToDroppedConnection,0); } } } } if (Program.no_console) { // Don't use the console stdin because we told not to input_thread = (KeyboardInputProcessorBase)(new InputThread(tcp)); } else if (CafeX.Program.IsInputRedirected) { // Can't use the ReadKeys stream because the console output is being redirected input_thread = (KeyboardInputProcessorBase)(new InputThread(tcp, Console.OpenStandardInput(), Console.InputEncoding)); } else { // Can use the ReadKeys enabled stream input_thread = (KeyboardInputProcessorBase)(new ConsoleInputNative(tcp, Console.InputEncoding)); } const int k_max_bytes_to_read = 1024; const int k_unit_bytes_to_read = 128; //OSReport() has a buffer of 128 bytes byte[] holding_pool = new byte[k_max_bytes_to_read]; int read_index = 0; this.input_stream = tcp.GetStream(); ExitReason reason_for_remotely_initiated_stop = new ExitReason(ExitReason.Reason.NoExit,0); int read; while ((read = bytes_read) >= 0 && tcp.Connected) { try { // read only up to 128 bytes at a time, since that's OSReport's buffer size // and we want to not split up multibyte characters due to that input_stream.BeginRead(holding_pool, read_index, Math.Min(k_unit_bytes_to_read, k_max_bytes_to_read - read_index), read_callback, null); } catch (IOException) { Log.WriteLine("Input stream could not be opened for reading."); break; } wait_handle.WaitOne(); read = bytes_read; if (read == 0) { Thread.Sleep(100); continue; } if (read < 0) { break; } //Basically what the following does is find the first single byte character in the pool, //Then call consume_raw_bytes on all of the bytes up until that single character. //Remaining bytes in the buffer are then shifted to the beginning of the buffer and the process //starts all over again // find the first character in the holding pool starting from the back that doesn't // represent the start of a multibyte character sequence (ASCII chars, in other words) int end_index = -1; for (int i = read_index + read - 1; i >= read_index; --i) { if (holding_pool[i] < 0x80) { end_index = i; break; } } // are all of the 128 bytes we're examining multibyte chars and we got to the end of the holding pool? // Set end_index to the end of the pool if (end_index == -1 && read_index + read >= k_max_bytes_to_read) { end_index = k_max_bytes_to_read - 1; } // there was a singlebyte character in there somewhere, which potentially means that // OSReport could split a string >128 bytes up, resulting in the last multibyte char getting split // In that case, process all of the characters up to and including the first single byte character, ensuring that // we don't end up with split chars since we'll be using a new set of 128 bytes after this if (end_index != -1) { reason_for_remotely_initiated_stop = consume_raw_bytes(holding_pool, end_index + 1, output_stream, exclusive_match); if (reason_for_remotely_initiated_stop.reason != ExitReason.Reason.NoExit) { #if DEBUG CafeX.Log.WriteLine("############# CLOSING TCP CONNECTION WITH CONSOLE ON PORT " + m_portcombo_to_listen_to.Port.ToString() + "#############"); #endif read_index = 0; break; } //Essentially left-shift the buffer's contents the amount we processed Buffer.BlockCopy(holding_pool, end_index + 1, holding_pool, 0, read_index + read - end_index - 1); read_index = read_index + read - end_index - 1; } else { //The whole buffer contains only multibyte characters read_index += read; } } //push everything else out if there was an error if (read_index > 0) { consume_raw_bytes(holding_pool, read_index, output_stream, exclusive_match); } input_thread.Abort(); input_stream.Close(); tcp.Close(); if (read < 0) { reason_for_remotely_initiated_stop.reason = ExitReason.Reason.StoppedByAlternateTermination; reason_for_remotely_initiated_stop.AppExitCode = 0; } else if (reason_for_remotely_initiated_stop.reason == ExitReason.Reason.NoExit) { reason_for_remotely_initiated_stop.reason = ExitReason.Reason.StoppedDueToDroppedConnection; reason_for_remotely_initiated_stop.AppExitCode = 0; } return reason_for_remotely_initiated_stop; } private void EnableDKConsoleInput() { if (input_thread != null) { input_thread.EnableDKInput(); } } private string[] cafeEventList = new string[] { Terms.IOP_START, //IOP Start Terms.PPC_START, //PPC Start Terms.PPC_SHUTDOWN_COMPLETED, //PPC Shutdown Terms.VPAD_INIT_END, //String to find the PID Terms.PPC_FAST_CYCLE, //Used only for Fast Relaunch }; private static DateTime evtProfile; private static int cafeProfile = -1; private const int thousand = 1000; private const int million = 1000000; private string TrimSpecial(string input, char seperator) { if (!string.IsNullOrEmpty(input)) { StringBuilder sb = new StringBuilder(); foreach (char c in input) { if (c != seperator) { sb.Append(c); } } return sb.ToString(); } return null; } private void UpdateEventTimes(string newTime) { int temp = Convert.ToInt32(newTime); int diff = temp - CatToucan.cafeProfile; CatToucan.cafeProfile = temp; int sec = 0; int msec = (diff % million) / thousand; if (diff > million) { sec = (int)(diff / million); } TimeSpan ts = new TimeSpan(0, 0, 0, sec, msec); CatToucan.evtProfile = CatToucan.evtProfile.Add(ts); } private bool HandleCafeEvents(string cafeEvent) { bool isValid = false; if (Program.CAFE_PROFILE.value != null && Program.CAFE_PROFILE.value == "1") { int i = -1; foreach (string str in cafeEventList) { i++; if (cafeEvent.Contains(str)) { isValid = true; break; } } if (isValid == true) { switch (i) { case 0: //IOP Start case 4: //Fast Relaunch { //Log the current PC Time CatToucan.evtProfile = DateTime.Now; //Log the current PC Time if (i == 0) { //Log the current CAFE time string temp = cafeEvent.Substring(0, 12); temp = this.TrimSpecial(temp, ':'); CatToucan.cafeProfile = Convert.ToInt32(temp) * thousand; CafeXEventtLog.Instance.WriteToEventLog(522, CatToucan.evtProfile, EventStatus.BEGIN, EventProcess.IOP, "IOP STARTED: " + CatToucan.cafeProfile); } else { CatToucan.cafeProfile = 0; CafeXEventtLog.Instance.WriteToEventLog(582, CatToucan.evtProfile, EventStatus.BEGIN, EventProcess.IOP, "***PPC FAST CYCLE***: " + CatToucan.cafeProfile); } } break; case 1: //PPC Start { //If the value is more than 0, then it means system started as cold boot //Update the timings if (CatToucan.cafeProfile > 0) { string temp = cafeEvent.Substring(cafeEvent.LastIndexOf("/") + 1); temp = temp.Substring(0, temp.LastIndexOf(")")); UpdateEventTimes(temp); CafeXEventtLog.Instance.WriteToEventLog(532, CatToucan.evtProfile, EventStatus.BEGIN, EventProcess.PPC, "PPC STARTED: " + CatToucan.cafeProfile); } else { CafeXEventtLog.Instance.WriteToEventLog(532, DateTime.Now, EventStatus.BEGIN, EventProcess.PPC, "PPC STARTED"); } } break; case 2: //PPC Shutdown CafeXEventtLog.Instance.WriteToEventLog(512, DateTime.Now, EventStatus.END, EventProcess.PPC, "***PPC SHUTDOWN COMPLETED***"); break; case 3: //App Start/Launch { string pidStr = cafeEvent.Substring(cafeEvent.LastIndexOf('=') + 1).Trim(); int currentPID = Convert.ToInt32(pidStr); if (currentPID == 2 || currentPID == 15) { string temp = cafeEvent.Substring(cafeEvent.LastIndexOf("/") + 1); temp = temp.Substring(0, temp.LastIndexOf(")")); UpdateEventTimes(temp); CafeXEventtLog.Instance.WriteToEventLog(542, DateTime.Now, EventStatus.BEGIN, EventProcess.TITLE, "Title Launched: " + CatToucan.cafeProfile); } } break; } } } return isValid; } //states to dictate how consume_raw_bytes will parse data private enum ParserState : uint { NormalParsing, EscapeSequenceParsing, CommandSequenceParsing, PerformanceTimerParsing_Boot1 }; private ExitReason consume_raw_bytes(byte[] holding_pool, int holding_bytes, TextWriter output_stream, bool exclusive_match) { byte[] outBytes = null; char[] outChars = null; ExitReason reason = new ExitReason(ExitReason.Reason.NoExit, 0); if (!String.IsNullOrEmpty(Program.CAFE_OUTPUT_ENCODING.value)) { outBytes = Encoding.Convert(Program.cafeOutEncoding, output_stream.Encoding, holding_pool, 0, holding_bytes); } else //user didn't specify a language, so we'll just assume UTF-8 { char[] utf8Chars = this.utf8Encoding.GetChars(holding_pool, 0, holding_bytes); //1)this.utf8Encoding.GetChars(holding_pool); then 2)output_stream.Encoding.GetChars(holding_pool); byte[] utf8Bytes = this.utf8Encoding.GetBytes(utf8Chars); outBytes = Encoding.Convert(this.utf8Encoding, output_stream.Encoding, utf8Bytes); } outChars = output_stream.Encoding.GetChars(outBytes); foreach (char next_char in outChars) { bool recognizedComSeq = true; if (currentState == ParserState.EscapeSequenceParsing) { if (next_char == '[') // user is issuing some other command { currentState = ParserState.CommandSequenceParsing; continue; } else if (next_char == 'c') // user wants to clear the terminal { Console.Clear(); currentState = ParserState.NormalParsing; continue; } } else if (currentState == ParserState.CommandSequenceParsing) { switch (next_char) { case 'H': { Console.SetCursorPosition(0, 0); currentState = ParserState.NormalParsing; continue; } default: // everything else should just pass through. { currentState = ParserState.NormalParsing; recognizedComSeq = false; break; } } } UnicodeCategory x = char.GetUnicodeCategory(next_char); // ignore most of the characters in this class. if (x == UnicodeCategory.Control) { //If the parser has hit a newline character, print what we've consumed so far. //Then check to see if we need to do something further based on the output if (next_char == '\n' || next_char == '\r') { string thing = m_string_builder.ToString(); output_stream.WriteLine(); #if DEBUG CafeX.Log.WriteLineRaw("Line: " + thing); #endif // Clears string buffer used for storage before parsing m_string_builder = new StringBuilder(); #if TIMER //We want to time up until we stop seeing BOOT1 messages. if (thing.StartsWith("BOOT1:")) { currentState = ParserState.PerformanceTimerParsing_Boot1; CafeX.Program.CafeXSetupTime.Report(); } if (currentState == ParserState.PerformanceTimerParsing_Boot1 && !thing.StartsWith("BOOT1:")) { CafeX.Program.CafeXSetupTime.Report(); currentState = ParserState.NormalParsing; } if (thing.StartsWith(Terms.VPAD_INIT_END)) { if (thing.Contains("PID=15") || (CafeX.Program.command == "on" && !CafeX.Program.IsBooting && thing.Contains("PID=2"))) { CafeX.Program.CafeXSetupTime.Stop(); } else { CafeX.Program.CafeXSetupTime.Report(); } } #endif if (cattoucan.sync != SyncStage.None) { if (cattoucan.sync == SyncStage.TitleSoft) { if (thing.Contains(Terms.KI_PIREPARE_TITLE_ASYNC)) { cattoucan.SignalTitleEvent(SyncStage.None); } else { cattoucan.SignalTitleEvent(SyncStage.TitleSoft); } } else if (cattoucan.sync == SyncStage.TitleFast) { if (thing.Contains(Terms.COS_LAUNCH)) { cattoucan.SignalTitleEvent(SyncStage.None); } else if (thing.Contains(Terms.SYSTEM_BUSY)) { cattoucan.SignalTitleEvent(SyncStage.Busy); } else { cattoucan.SignalTitleEvent(SyncStage.TitleFast); } } else if (cattoucan.sync == SyncStage.Boot) { if (thing.Contains(Terms.DEVICE_REMOTE_COMMANDS)) { cattoucan.SignalBootEvent(); } } else if (cattoucan.sync == SyncStage.Menu) { if (thing.Contains(Terms.MENU_SOURCE)) { cattoucan.SignalMenuEvent(); } } } else { // check the line for the magic strings: // [+-*APPLICATION EXITED %d*-+] // [+-*APPLICATION EXITED*-+] // [+-*WAITING FOR GDB*-+] // [+-*WAITING FOR DEBUGGER*-+] if (!exclusive_match && thing.Contains("[+-*APPLICATION EXITED")) { Int32 app_exit_code = 0; app_exit_code = Int32.Parse(System.Text.RegularExpressions.Regex.Match(thing, @"\-*?\d+").Value); reason = new ExitReason(ExitReason.Reason.StoppedByApplicationExit, app_exit_code); #if DEBUG CafeX.Log.WriteLine("CatToucan found 'APPLICATION EXITED' message and will exit with this reason."); #endif output_stream.WriteLine(); return reason; } else if (thing.Contains("[+-*WAITING FOR DEBUGGER*-+]")) { reason = new ExitReason(ExitReason.Reason.StoppedByWaitingForDebugger, 42); #if DEBUG CafeX.Log.WriteLine("CatToucan found 'WAITING FOR DEBUGGER' message and will exit with this reason."); #endif output_stream.WriteLine(); return reason; } else if (thing.Contains("[+-*WAITING FOR GDB*-+]")) { reason = new ExitReason(ExitReason.Reason.StoppedByWaitingForDebugger, 0); #if DEBUG CafeX.Log.WriteLine("CatToucan found 'WAITING FOR DEBUGGER' message and will exit with this reason."); #endif output_stream.WriteLine(); return reason; } } if (thing.Contains(Terms.DK_READY_MESSAGE)) { // enable keyboard input this.EnableDKConsoleInput(); } else if (!HandleCafeEvents(thing) && !string.IsNullOrEmpty(CafeX.Program.CATTOUCAN_TERM.value) && thing.Contains(CafeX.Program.CATTOUCAN_TERM.value)) { output_stream.WriteLine("\nPattern Matched '" + CafeX.Program.CATTOUCAN_TERM.value); return new ExitReason(ExitReason.Reason.StoppedByAlternateTermination, 0); } else { // Check the notification strings foreach (KeyValuePair kvp in matchNotify) { // See if we have any matches if (thing.IndexOf(kvp.Key, 0, thing.Length, StringComparison.CurrentCultureIgnoreCase) >= 0) { // Set the manual event #if DEBUG Console.WriteLine(" Found key '{0}'...setting handle {1}", kvp.Key, kvp.Value.Handle); #endif kvp.Value.Set(); } } // Check the callback strings foreach (KeyValuePair> kvp in matchCallback) { // See if we have any matches if (thing.IndexOf(kvp.Key, 0, thing.Length, StringComparison.CurrentCultureIgnoreCase) >= 0) { // Run the callback #if DEBUG Console.WriteLine(" Found key '{0}'...calling {1}({2})", kvp.Key, kvp.Value.Method.Name, thing); #endif if (!kvp.Value.Invoke(thing)) { output_stream.WriteLine("Callback {0}({1}) requested termination.", kvp.Value.Method.Name, thing); return new ExitReason(ExitReason.Reason.StoppedByAlternateTermination, 0); } } } } } else if (next_char == '\t') { m_string_builder.Append(next_char); output_stream.Write(next_char); } else if (next_char == '\b') { m_string_builder.Append(next_char); output_stream.Write(next_char); Console.Write(" \b"); } else if (next_char == '\u001b' ) { //if we read the escape character, we'll start checking the next few bytes //to determine what special command we have to do currentState = ParserState.EscapeSequenceParsing; } } else if (!recognizedComSeq) { // because we ate the ESC and [ earlier, add those back to the stream char[] sequence = { '\u001b', '[', next_char }; output_stream.Write(sequence); m_string_builder.Append(sequence); } else { output_stream.Write(next_char); m_string_builder.Append(next_char); } } // return reason; } #region Helper APIs - Static Public public static IPEndPoint ParseStrings(string ipaddr, string port_num, out string error) { IPAddress actual_ip_addr; int actual_port; bool could_parse_addr = IPAddress.TryParse(ipaddr, out actual_ip_addr); bool could_parse_port = int.TryParse(port_num, out actual_port); if (!could_parse_addr) { error = "Couldn't parse IP address."; return (null); } if (!could_parse_port) { error = "Couldn't parse port number."; return (null); } error = string.Empty; IPEndPoint rc = new IPEndPoint(actual_ip_addr, actual_port); return (rc); } #endregion } }