Team Fortress 2 Source Code as on 22/4/2020
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

419 lines
9.5 KiB

  1. //========= Copyright Valve Corporation, All rights reserved. ============//
  2. //
  3. // Purpose:
  4. //
  5. // $NoKeywords: $
  6. //
  7. //=============================================================================//
  8. // CTextConsoleUnix.cpp: Unix implementation of the TextConsole class.
  9. //
  10. //////////////////////////////////////////////////////////////////////
  11. #ifndef _WIN32
  12. #include <sys/ioctl.h>
  13. #include "TextConsoleUnix.h"
  14. #include "tier0/icommandline.h"
  15. #include "tier1/utllinkedlist.h"
  16. #include "filesystem.h"
  17. #include "../thirdparty/libedit-3.1/src/histedit.h"
  18. #include "tier0/vprof.h"
  19. #define CONSOLE_LOG_FILE "console.log"
  20. static pthread_mutex_t g_lock;
  21. static pthread_t g_threadid = (pthread_t)-1;
  22. CUtlLinkedList< CUtlString > g_Commands;
  23. static volatile int g_ProcessingCommands = false;
  24. #if defined( LINUX )
  25. // Dynamically load the libtinfo stuff. On servers without a tty attached,
  26. // we get to skip all of these dependencies on servers without ttys.
  27. #define TINFO_SYM(rc, fn, params, args, ret) \
  28. typedef rc (*DYNTINFOFN_##fn) params; \
  29. static DYNTINFOFN_##fn g_TINFO_##fn; \
  30. extern "C" rc fn params \
  31. { \
  32. ret g_TINFO_##fn args; \
  33. }
  34. TINFO_SYM(char *, tgoto, (char *string, int x, int y), (string, x, y), return);
  35. TINFO_SYM(int, tputs, (const char *str, int affcnt, int (*putc)(int)), (str, affcnt, putc), return);
  36. TINFO_SYM(int, tgetflag, (char *id), (id), return);
  37. TINFO_SYM(int, tgetnum, (char *id), (id), return);
  38. TINFO_SYM(int, tgetent, (char *bufp, const char *name), (bufp, name), return);
  39. TINFO_SYM(char *, tgetstr, (char *id, char **area), (id, area), return);
  40. #endif // LINUX
  41. //----------------------------------------------------------------------------------------------------------------------
  42. // init_tinfo_functions
  43. //----------------------------------------------------------------------------------------------------------------------
  44. static bool init_tinfo_functions()
  45. {
  46. #if !defined( LINUX )
  47. return true;
  48. #else
  49. static void *s_ncurses_handle = NULL;
  50. if ( !s_ncurses_handle )
  51. {
  52. // Long time ago, ncurses was two libraries. So if libtinfo fails, try libncurses.
  53. static const char *names[] = { "libtinfo.so.5", "libncurses.so.5" };
  54. for ( int i = 0; !s_ncurses_handle && ( i < ARRAYSIZE( names ) ); i++ )
  55. {
  56. bool bFailed = true;
  57. s_ncurses_handle = dlopen( names[i], RTLD_NOW );
  58. if ( s_ncurses_handle )
  59. {
  60. bFailed = false;
  61. #define LOADTINFOFUNC(_handle, _func, _failed) \
  62. do { \
  63. g_TINFO_##_func = ( DYNTINFOFN_##_func )dlsym(_handle, #_func); \
  64. if ( !g_TINFO_##_func) \
  65. _failed = true; \
  66. } while (0)
  67. LOADTINFOFUNC( s_ncurses_handle, tgoto, bFailed );
  68. LOADTINFOFUNC( s_ncurses_handle, tputs, bFailed );
  69. LOADTINFOFUNC( s_ncurses_handle, tgetflag, bFailed );
  70. LOADTINFOFUNC( s_ncurses_handle, tgetnum, bFailed );
  71. LOADTINFOFUNC( s_ncurses_handle, tgetent, bFailed );
  72. LOADTINFOFUNC( s_ncurses_handle, tgetstr, bFailed );
  73. #undef LOADTINFOFUNC
  74. }
  75. if ( bFailed )
  76. s_ncurses_handle = NULL;
  77. }
  78. if ( !s_ncurses_handle )
  79. {
  80. fprintf( stderr, "\nWARNING: Failed to load 32-bit libtinfo.so.5 or libncurses.so.5.\n"
  81. " Please install (lib32tinfo5 / ncurses-libs.i686 / equivalent) to enable readline.\n\n");
  82. }
  83. }
  84. return !!s_ncurses_handle;
  85. #endif // LINUX
  86. }
  87. static unsigned char editline_complete( EditLine *el, int ch __attribute__((__unused__)) )
  88. {
  89. static const char *s_cmds[] =
  90. {
  91. "cvarlist ",
  92. "find ",
  93. "help ",
  94. "maps ",
  95. "nextlevel",
  96. "quit",
  97. "status",
  98. "sv_cheats ",
  99. "tf_bot_quota ",
  100. "toggle ",
  101. "sv_dump_edicts",
  102. #ifdef STAGING_ONLY
  103. "tf_bot_use_items ",
  104. #endif
  105. };
  106. const LineInfo *lf = el_line(el);
  107. const char *cmd = lf->buffer;
  108. size_t len = lf->cursor - cmd;
  109. if ( len > 0 )
  110. {
  111. for (int i = 0; i < ARRAYSIZE(s_cmds); i++)
  112. {
  113. if ( len > strlen( s_cmds[i] ) )
  114. continue;
  115. if ( !Q_strncmp( cmd, s_cmds[i], len ) )
  116. {
  117. if ( el_insertstr( el, s_cmds[i] + len ) == -1 )
  118. return CC_ERROR;
  119. else
  120. return CC_REFRESH;
  121. }
  122. }
  123. }
  124. return CC_ERROR;
  125. }
  126. static const char *editline_prompt( EditLine *e )
  127. {
  128. // Something like: "\1\033[7m\1Srcds$\1\033[0m\1 "
  129. static const char *szPrompt = getenv( "SRCDS_PROMPT" );
  130. return szPrompt ? szPrompt : "";
  131. }
  132. static void editline_cleanup_handler( void *arg )
  133. {
  134. if ( arg )
  135. {
  136. EditLine *el = (EditLine *)arg;
  137. el_end( el );
  138. }
  139. }
  140. static bool add_command( const char *cmd, int cmd_len )
  141. {
  142. if ( cmd )
  143. {
  144. tmZone( TELEMETRY_LEVEL0, TMZF_NONE, "%s", __FUNCTION__ );
  145. // Trim trailing whitespace.
  146. while ( ( cmd_len > 0 ) && isspace( cmd[ cmd_len - 1 ] ) )
  147. cmd_len--;
  148. if ( cmd_len > 0 )
  149. {
  150. pthread_mutex_lock( &g_lock );
  151. if ( g_Commands.Count() < 32 )
  152. {
  153. CUtlString szCommand( cmd, cmd_len );
  154. g_Commands.AddToTail( szCommand );
  155. g_ProcessingCommands = true;
  156. }
  157. pthread_mutex_unlock( &g_lock );
  158. // Wait a bit until we've processed the command we added.
  159. for ( int i = 0; i < 6; i++ )
  160. {
  161. while ( g_ProcessingCommands )
  162. usleep( 500 );
  163. }
  164. return true;
  165. }
  166. }
  167. return false;
  168. }
  169. static void *editline_threadproc( void *arg )
  170. {
  171. HistEvent ev;
  172. EditLine *el;
  173. History *myhistory;
  174. FILE *tty = (FILE *)arg;
  175. ThreadSetDebugName( "libedit" );
  176. // Set up state
  177. el = el_init( "srcds_linux", stdin, tty, stderr );
  178. el_set( el, EL_PROMPT, &editline_prompt );
  179. el_set( el, EL_EDITOR, "emacs" ); // or "vi"
  180. // Hitting Ctrl+R will reset prompt.
  181. el_set( el, EL_BIND, "^R", "ed-redisplay", NULL );
  182. /* Add a user-defined function */
  183. el_set( el, EL_ADDFN, "ed-complete", "Complete argument", editline_complete );
  184. /* Bind tab to it */
  185. el_set( el, EL_BIND, "^I", "ed-complete", NULL );
  186. // Init history.
  187. myhistory = history_init();
  188. if (myhistory == 0)
  189. {
  190. fprintf( stderr, "history could not be initialized\n" );
  191. g_threadid = (pthread_t)-1;
  192. return (void *)-1;
  193. }
  194. // History size.
  195. history( myhistory, &ev, H_SETSIZE, 800 );
  196. // History callback.
  197. el_set( el, EL_HIST, history, myhistory );
  198. // Source user's defaults.
  199. el_source( el, NULL );
  200. pthread_cleanup_push( editline_cleanup_handler, el );
  201. while ( g_threadid != (pthread_t)-1 )
  202. {
  203. // count is the number of characters read.
  204. // line is a const char* of our command line with the tailing \n
  205. int count;
  206. const char *line = el_gets( el, &count );
  207. if ( add_command( line, count ) )
  208. {
  209. // Add command to history.
  210. history( myhistory, &ev, H_ENTER, line );
  211. }
  212. }
  213. pthread_cleanup_pop( 0 );
  214. // Clean up...
  215. history_end( myhistory );
  216. el_end( el );
  217. return NULL;
  218. }
  219. static void *fgets_threadproc( void *arg )
  220. {
  221. pthread_cleanup_push( editline_cleanup_handler, NULL );
  222. while ( g_threadid != (pthread_t)-1 )
  223. {
  224. char cmd[ 512 ];
  225. if ( fgets( cmd, sizeof( cmd ), stdin ) )
  226. {
  227. cmd[ sizeof(cmd) - 1 ] = 0;
  228. add_command( cmd, strlen( cmd ) );
  229. }
  230. }
  231. pthread_cleanup_pop( 0 );
  232. return NULL;
  233. }
  234. bool CTextConsoleUnix::Init()
  235. {
  236. if( g_threadid != (pthread_t)-1 )
  237. {
  238. Assert( !"CTextConsoleUnix can only handle a single thread!" );
  239. return false;
  240. }
  241. pthread_mutex_init( &g_lock, NULL );
  242. // This code is for echo-ing key presses to the connected tty
  243. // (which is != STDOUT)
  244. if ( isatty( STDIN_FILENO ) )
  245. {
  246. const char *termid_str = ctermid( NULL );
  247. m_tty = fopen( termid_str, "w+" );
  248. if ( !m_tty )
  249. {
  250. fprintf( stderr, "WARNING: Unable to open tty(%s) for output.\n", termid_str );
  251. m_tty = stdout;
  252. }
  253. void *(*terminal_threadproc) (void *) = editline_threadproc;
  254. if ( !init_tinfo_functions() )
  255. terminal_threadproc = fgets_threadproc;
  256. if ( pthread_create( &g_threadid, NULL, terminal_threadproc, (void *)m_tty ) != 0 )
  257. {
  258. g_threadid = (pthread_t)-1;
  259. fprintf( stderr, "WARNING: pthread_create failed: %s.\n", strerror(errno) );
  260. }
  261. }
  262. else
  263. {
  264. m_tty = fopen( "/dev/null", "w+" );
  265. if ( !m_tty )
  266. m_tty = stdout;
  267. }
  268. m_bConDebug = CommandLine()->FindParm( "-condebug" ) != 0;
  269. if ( m_bConDebug && CommandLine()->FindParm( "-conclearlog" ) )
  270. g_pFullFileSystem->RemoveFile( CONSOLE_LOG_FILE, "GAME" );
  271. return CTextConsole::Init();
  272. }
  273. void CTextConsoleUnix::ShutDown()
  274. {
  275. if ( g_threadid != (pthread_t)-1 )
  276. {
  277. void *status = NULL;
  278. pthread_t tid = g_threadid;
  279. g_threadid = (pthread_t)-1;
  280. pthread_cancel( tid );
  281. pthread_join( tid, &status );
  282. }
  283. pthread_mutex_destroy( &g_lock );
  284. }
  285. void CTextConsoleUnix::Print( char * pszMsg )
  286. {
  287. int nChars = strlen( pszMsg );
  288. if ( nChars > 0 )
  289. {
  290. if ( m_bConDebug )
  291. {
  292. FileHandle_t fh = g_pFullFileSystem->Open( CONSOLE_LOG_FILE, "a" );
  293. if ( fh != FILESYSTEM_INVALID_HANDLE )
  294. {
  295. g_pFullFileSystem->Write( pszMsg, nChars, fh );
  296. g_pFullFileSystem->Close( fh );
  297. }
  298. }
  299. fwrite( pszMsg, 1, nChars, m_tty );
  300. }
  301. }
  302. void CTextConsoleUnix::SetTitle( char *pszTitle )
  303. {
  304. }
  305. void CTextConsoleUnix::SetStatusLine( char *pszStatus )
  306. {
  307. }
  308. void CTextConsoleUnix::UpdateStatus()
  309. {
  310. }
  311. char *CTextConsoleUnix::GetLine( int index, char *buf, int buflen )
  312. {
  313. if ( g_threadid != (pthread_t)-1 )
  314. {
  315. if ( g_Commands.Count() > 0 )
  316. {
  317. pthread_mutex_lock( &g_lock );
  318. const CUtlString& psCommand = g_Commands[ g_Commands.Head() ];
  319. V_strncpy( buf, psCommand.Get(), buflen );
  320. g_Commands.Remove( g_Commands.Head() );
  321. pthread_mutex_unlock( &g_lock );
  322. return buf;
  323. }
  324. else if ( index == 0 )
  325. {
  326. // We're being asked for the first command. Must be a new frame.
  327. // Reset the processed commands global.
  328. g_ProcessingCommands = false;
  329. }
  330. }
  331. return NULL;
  332. }
  333. int CTextConsoleUnix::GetWidth()
  334. {
  335. int nWidth = 0;
  336. struct winsize ws;
  337. if ( ioctl( STDOUT_FILENO, TIOCGWINSZ, &ws ) == 0 )
  338. nWidth = (int)ws.ws_col;
  339. if ( nWidth <= 1 )
  340. nWidth = 80;
  341. return nWidth;
  342. }
  343. #endif // !_WIN32