API
 
Loading...
Searching...
No Matches
logdump.hpp
Go to the documentation of this file.
1/** \file logdump.hpp
2 * \brief A simple utility to dump MagAO-X binary logs to stdout.
3 *
4 * \ingroup logdump_files
5 */
6
7#ifndef logdump_hpp
8#define logdump_hpp
9
10#include <iostream>
11#include <cstring>
12#include <filesystem>
13
14#include <mx/mxError.hpp>
15#include <mx/ioutils/fileUtils.hpp>
16
17#include "../../libMagAOX/libMagAOX.hpp"
18using namespace MagAOX::logger;
19
20using namespace flatlogs;
21
22/** \defgroup logdump logdump: MagAO-X Log Reader
23 * \brief Read a MagAO-X binary log file.
24 *
25 * <a href="../handbook/utils/logdump.html">Utility Documentation</a>
26 *
27 * \ingroup utils
28 *
29 */
30
31/** \defgroup logdump_files logdump Files
32 * \ingroup logdump
33 */
34
35/// An application to dump MagAo-X binary logs to the terminal.
36/** \todo document this
37 * \todo add config for colors, both on/off and options to change.
38 *
39 * \ingroup logdump
40 */
41class logdump : public mx::app::application
42{
43 typedef mx::verbose::vvv verboseT;
44
45 protected:
46 std::string m_dir;
47 std::string m_ext;
48 std::string m_file;
49
50 bool m_time{ false };
51 bool m_jsonMode{ false };
52
53 unsigned long m_pauseTime{ 250 }; /// When following, pause time to check for new data. msec. Default is 250 msec.
55 20 }; /// When following, number of loops to wait before checking for a new file. Default is 4.
56
57 std::vector<std::string> m_prefixes;
58
59 size_t m_nfiles{ 0 }; /// Number of files to dump. Default is 0, unless following then the default is 1.
60
61 bool m_follow{ false };
62
64
65 std::vector<eventCodeT> m_codes;
66
67 void printLogBuff( const logPrioT &lvl, const eventCodeT &ec, const msgLenT &len, bufferPtrT &logBuff );
68 void printLogJson( const msgLenT &len, bufferPtrT &logBuff );
69
70 public:
71 virtual void setupConfig();
72
73 virtual void loadConfig();
74
75 virtual int execute();
76
77 virtual int gettimes( std::vector<std::string> &logs );
78};
79
81{
82 config.add( "pauseTime",
83 "p",
84 "pauseTime",
85 argType::Required,
86 "",
87 "pauseTime",
88 false,
89 "int",
90 "When following, time in milliseconds to pause before checking for new entries." );
91 config.add( "fileCheckInterval",
92 "",
93 "fileCheckInterval",
94 argType::Required,
95 "",
96 "fileCheckInterval",
97 false,
98 "int",
99 "When following, number of pause intervals between checks for new files." );
100
101 config.add( "dir",
102 "d",
103 "dir",
104 argType::Required,
105 "",
106 "dir",
107 false,
108 "string",
109 "Directory to search for logs. MagAO-X default is normally used." );
110 config.add( "ext",
111 "e",
112 "ext",
113 argType::Required,
114 "",
115 "ext",
116 false,
117 "string",
118 "The file extension of log files. MagAO-X default is normally used." );
119 config.add( "nfiles",
120 "n",
121 "nfiles",
122 argType::Required,
123 "",
124 "nfiles",
125 false,
126 "int",
127 "Number of log files to dump. If 0, then all matching files dumped. Default: 0, 1 if following." );
128 config.add( "follow",
129 "f",
130 "follow",
131 argType::True,
132 "",
133 "follow",
134 false,
135 "bool",
136 "Follow the log, printing new entries as they appear." );
137 config.add( "level",
138 "L",
139 "level",
140 argType::Required,
141 "",
142 "level",
143 false,
144 "int/string",
145 "Minimum log level to dump, either an integer or a string. -1/TELEMETRY [the default], 0/DEFAULT, "
146 "1/D1/DBG1/DEBUG2, 2/D2/DBG2/DEBUG1,3/INFO,4/WARNING,5/ERROR,6/CRITICAL,7/FATAL. Note that only the "
147 "mininum unique string is required." );
148 config.add( "code",
149 "C",
150 "code",
151 argType::Required,
152 "",
153 "code",
154 false,
155 "int",
156 "The event code, or vector of codes, to dump. If not specified, all codes are dumped. See "
157 "logCodes.hpp for a complete list of codes." );
158 config.add( "file",
159 "F",
160 "file",
161 argType::Required,
162 "",
163 "file",
164 false,
165 "string",
166 "A single file to process. If no / are found in name it will look in the specified directory (or "
167 "MagAO-X default)." );
168 config.add( "time",
169 "T",
170 "time",
171 argType::True,
172 "",
173 "time",
174 false,
175 "bool",
176 "time span mode: prints the ISO 8601 UTC timestamps of the first and last entry, the elapsed time in "
177 "seconds, and the number of records in the file as a space-delimited string" );
178 config.add( "json",
179 "J",
180 "json",
181 argType::True,
182 "",
183 "json",
184 false,
185 "bool",
186 "JSON mode: emits one JSON document per line for each record in the log" );
187}
188
190{
191 config( m_pauseTime, "pauseTime" );
192 config( m_fileCheckInterval, "fileCheckInterval" );
193
194 // Get default log dir
195 std::string tmpstr = mx::sys::getEnv( MAGAOX_env_path );
196 if( tmpstr == "" )
197 {
198 tmpstr = MAGAOX_path;
199 }
200 m_dir = tmpstr + "/" + MAGAOX_logRelPath;
201 ;
202
203 // Now check for config option for dir
204 config( m_dir, "dir" );
205
206 m_ext = ".";
208 config( m_ext, "ext" );
209 ///\todo need to check for lack of "." and error or fix
210
211 config( m_file, "file" );
212
213 if( m_file == "" && config.nonOptions.size() < 1 )
214 {
215 std::cerr << "logdump: need application name. Try logdump -h for help.\n";
216 }
217
218 if( m_file == "" && config.nonOptions.size() > 1 )
219 {
220 std::cerr << "logdump: only one application at a time supported. Try logdump -h for help.\n";
221 }
222
223 m_prefixes.resize( config.nonOptions.size() );
224 for( size_t i = 0; i < config.nonOptions.size(); ++i )
225 {
226 m_prefixes[i] = config.nonOptions[i];
227 }
228
229 if( config.isSet( "time" ) )
230 m_time = true;
231 if( config.isSet( "json" ) )
232 m_jsonMode = true;
233
234 config( m_follow, "follow" );
235
236 if( m_follow )
237 m_nfiles = 1; // default to 1 if follow is set.
238 config( m_nfiles, "nfiles" );
239
240 tmpstr = "";
241 config( tmpstr, "level" );
242 if( tmpstr != "" )
243 {
244 m_level = logLevelFromString( tmpstr );
245 }
246
247 config( m_codes, "code" );
248
249 std::cerr << m_codes.size() << "\n";
250}
251
253{
254
255 if( m_file == "" && m_prefixes.size() != 1 )
256 return -1; // error message will have been printed in loadConfig.
257
258 std::vector<std::string> subdirs, logs;
259
260 if( m_file != "" )
261 {
262 if( m_file.find( '/' ) == std::string::npos )
263 {
264
265 std::string devName, YYYY, MM, DD, hh, mm, ss, nn;
266 mx_error_check_rv(MagAOX::file::parseFilePath( devName, YYYY, MM, DD, hh, mm, ss, nn, m_file ),-1);
267
268 m_file = m_dir + '/' + devName + '/' + YYYY + '_' + MM + '_' + DD + '/' + m_file;
269 }
270
271 logs.push_back( m_file );
272 }
273 else
274 {
275 mx_error_check_rv(mx::ioutils::getFileNames(subdirs, m_dir + "/" + m_prefixes[0], "", "", "" ),-1);
276
277 if( m_follow )
278 {
279 mx_error_check_rv(mx::ioutils::getFileNames(logs, subdirs.back(), m_prefixes[0], "", m_ext ), -1);
280 }
281 else
282 {
283 for( size_t d = 0; d < subdirs.size(); ++d )
284 {
285 // get subdir vectors one at a time and append to build the whole list
286 std::vector<std::string> tlogs;
287 mx_error_check_rv(mx::ioutils::getFileNames(tlogs, subdirs[d], m_prefixes[0], "", m_ext ),-1);
288 logs.insert( logs.end(), tlogs.begin(), tlogs.end() );
289 }
290 }
291 }
292
293 ///\todo if follow is set, then should nfiles default to 1 unless explicitly set?
294 if( m_nfiles == 0 )
295 {
296 m_nfiles = logs.size();
297 }
298
299 if( m_nfiles > logs.size() )
300 m_nfiles = logs.size();
301
302 if( m_time )
303 {
304 return gettimes( logs );
305 }
306
307 bool firstRun = true; // for only showing latest entries on first run when following.
308
309 for( size_t i = logs.size() - m_nfiles; i < logs.size(); ++i )
310 {
311 std::string fname = logs[i];
312 FILE *fin;
313
314 bufferPtrT head( (char *) ::operator new(logHeader::maxHeadSize));
315
316 bufferPtrT logBuff;
317
318 fin = fopen( fname.c_str(), "rb" );
319
320 //--> get size here!!
321 off_t finSize = mx::ioutils::fileSize( fileno( fin ) );
322
323 std::cerr << fname << "\n";
324
325 off_t totNrd = 0;
326
327 size_t buffSz = 0;
328 while( !feof( fin ) ) //<--This should be an exit condition controlled by loop logic, not feof.
329 {
330 int nrd;
331
332 ///\todo check for errors on all reads . . .
333
334 // Read next header
335 nrd = fread( head.get(), sizeof( char ), logHeader::minHeadSize, fin );
336 if( nrd == 0 )
337 {
338 // If we're following and on the last log file, wait for more to show up.
339 if( m_follow == true && i == logs.size() - 1 )
340 {
341 int check = 0;
342 firstRun = false; // from now on we show all logs
343 while( nrd == 0 )
344 {
345 std::this_thread::sleep_for( std::chrono::duration<unsigned long, std::milli>( m_pauseTime ) );
346 clearerr( fin );
347 nrd = fread( head.get(), sizeof( char ), logHeader::minHeadSize, fin );
348 if( nrd > 0 )
349 break;
350
351 ++check;
352 if( check >= m_fileCheckInterval )
353 {
354 // Check if a new sub-directory exists now.
355 size_t old_subdirs_sz = subdirs.size();
356 mx_error_check_rv(mx::ioutils::getFileNames( subdirs, m_dir + "/" + m_prefixes[0], "", "", "" ),-1);
357 if( subdirs.size() > old_subdirs_sz )
358 {
359 // new subdirs(s) detected;
360 break;
361 }
362
363 // Check if a new file exists now.
364 size_t old_logs_sz = logs.size();
365
366 mx_error_check_rv(mx::ioutils::getFileNames( logs, subdirs.back(), m_prefixes[0], "", m_ext ),-1);
367
368 if( logs.size() > old_logs_sz )
369 {
370 // new file(s) detected;
371 break;
372 }
373 check = 0;
374 }
375 }
376 }
377 else
378 {
379 break;
380 }
381 }
382
383 // We got here without any data, probably means time to get a new file.
384 if( nrd == 0 )
385 break;
386
387 totNrd += nrd;
388
389 if( logHeader::msgLen0( head ) == logHeader::MAX_LEN0 - 1 )
390 {
391 // Intermediate size message, read two more bytes
392 nrd = fread( head.get() + logHeader::minHeadSize, sizeof( char ), sizeof( msgLen1T ), fin );
393 }
394 else if( logHeader::msgLen0( head ) == logHeader::MAX_LEN0 )
395 {
396 // Large size message: read 8 more bytes
397 nrd = fread( head.get() + logHeader::minHeadSize, sizeof( char ), sizeof( msgLen2T ), fin );
398 }
399
400 logPrioT lvl = logHeader::logLevel( head );
401 eventCodeT ec = logHeader::eventCode( head );
402 msgLenT len = logHeader::msgLen( head );
403
404 // Here: check if lvl, eventCode, etc, match what we want.
405 // If not, fseek and loop.
406 if( lvl > m_level )
407 {
408 fseek( fin, len, SEEK_CUR );
409 continue;
410 }
411
412 if( m_codes.size() > 0 )
413 {
414 bool found = false;
415 for( size_t c = 0; c < m_codes.size(); ++c )
416 {
417 if( m_codes[c] == ec )
418 {
419 found = true;
420 break;
421 }
422 }
423
424 if( !found )
425 {
426 fseek( fin, len, SEEK_CUR );
427 continue;
428 }
429 }
430
431 size_t hSz = logHeader::headerSize( head );
432
433 if( (size_t)hSz + (size_t)len > buffSz )
434 {
435 logBuff = bufferPtrT( (char *) ::operator new(hSz + len));
436 }
437 memcpy( logBuff.get(), head.get(), hSz );
438
439 ///\todo what do we do if nrd not equal to expected size?
440 nrd = fread( logBuff.get() + hSz, sizeof( char ), len, fin );
441 // If not following, exit loop without printing the incomplete log entry (go on to next file).cd
442 // If following, wait for it, but also be checking for new log file in case of crash
443
444 totNrd += nrd;
445
446 if( m_follow && firstRun && finSize > 512 && totNrd < finSize - 512 )
447 {
448 // firstRun = false;
449 continue;
450 }
451
452 if( !logVerify( ec, logBuff, len ) )
453 {
454 std::cerr << "Log " << fname << " failed verification on code=" << ec
455 << " at byte=" << totNrd - len - hSz << ". File possibly corrupt. Exiting." << std::endl;
456 return -1;
457 }
458
459 if( m_jsonMode )
460 {
461 printLogJson( len, logBuff );
462 }
463 else
464 {
465 printLogBuff( lvl, ec, len, logBuff );
466 }
467 }
468
469 fclose( fin );
470 }
471
472 return 0;
473}
474
475inline void logdump::printLogBuff( const logPrioT &lvl, const eventCodeT &ec, const msgLenT &len, bufferPtrT &logBuff )
476{
477 static_cast<void>( len ); // be unused
478
479 if( ec == eventCodes::GIT_STATE )
480 {
481 if( git_state::repoName( logHeader::messageBuffer( logBuff ) ) == "MagAOX" )
482 {
483 for( int i = 0; i < 80; ++i )
484 std::cout << '-';
485 std::cout << "\n\t\t\t\t SOFTWARE RESTART\n";
486 for( int i = 0; i < 80; ++i )
487 std::cout << '-';
488 std::cout << '\n';
489 }
490 }
491
492 if( lvl < logPrio::LOG_INFO )
493 {
494 if( lvl == logPrio::LOG_EMERGENCY )
495 {
496 std::cout << "\033[104m\033[91m\033[5m\033[1m";
497 }
498
499 if( lvl == logPrio::LOG_ALERT )
500 {
501 std::cout << "\033[101m\033[5m";
502 }
503
504 if( lvl == logPrio::LOG_CRITICAL )
505 {
506 std::cout << "\033[41m\033[1m";
507 }
508
509 if( lvl == logPrio::LOG_ERROR )
510 {
511 std::cout << "\033[91m\033[1m";
512 }
513
514 if( lvl == logPrio::LOG_WARNING )
515 {
516 std::cout << "\033[93m\033[1m";
517 }
518
519 if( lvl == logPrio::LOG_NOTICE )
520 {
521 std::cout << "\033[1m";
522 }
523 }
524
525 logStdFormat( std::cout, logBuff );
526
527 std::cout << "\033[0m";
528 std::cout << std::endl;
529}
530
531inline void logdump::printLogJson( const msgLenT &len, bufferPtrT &logBuff )
532{
533 static_cast<void>( len ); // be unused
534 logJsonFormat( std::cout, logBuff );
535 std::cout << std::endl;
536}
537
538int logdump::gettimes( std::vector<std::string> &logs )
539{
540 for( size_t i = logs.size() - m_nfiles; i < logs.size(); ++i )
541 {
542 std::string fname = logs[i];
543 FILE *fin;
544
545 bufferPtrT head( (char *) ::operator new(logHeader::maxHeadSize) );
546
547 fin = fopen( fname.c_str(), "rb" );
548
549 //--> get size here!!
550 // off_t finSize = mx::ioutils::fileSize( fileno(fin) );
551
552 off_t totNrd = 0;
553
554 // size_t buffSz = 0;
555
556 // Read firs header
557
558 int nrd;
559
560 ///\todo check for errors on all reads . . .
561
562 // Read next header
563 nrd = fread( head.get(), sizeof( char ), logHeader::minHeadSize, fin );
564 if( nrd == 0 )
565 {
566 std::cerr << "got no header\n";
567 return 0;
568 }
569
570 if( logHeader::msgLen0( head ) == logHeader::MAX_LEN0 - 1 )
571 {
572 // Intermediate size message, read two more bytes
573 nrd = fread( head.get() + logHeader::minHeadSize, sizeof( char ), sizeof( msgLen1T ), fin );
574 }
575 else if( logHeader::msgLen0( head ) == logHeader::MAX_LEN0 )
576 {
577 // Large size message: read 8 more bytes
578 nrd = fread( head.get() + logHeader::minHeadSize, sizeof( char ), sizeof( msgLen2T ), fin );
579 }
580
581 // logPrioT lvl = logHeader::logLevel(head);
582 // eventCodeT ec = logHeader::eventCode(head);
583 msgLenT len = logHeader::msgLen( head );
584 timespecX ts0 = logHeader::timespec( head );
585 // size_t hSz = logHeader::headerSize(head);
586
587 uint32_t nRecords = 1;
588 fseek( fin, len, SEEK_CUR );
589
590 timespecX ts;
591
592 while( !feof( fin ) ) //<--This should be an exit condition controlled by loop logic, not feof.
593 {
594 int nrd;
595
596 // Read next header
597 nrd = fread( head.get(), sizeof( char ), logHeader::minHeadSize, fin );
598 if( nrd == 0 )
599 {
600 break;
601 }
602 nRecords += 1;
603
604 // We got here without any data, probably means time to get a new file.
605 if( nrd == 0 )
606 break;
607
608 totNrd += nrd;
609
610 if( logHeader::msgLen0( head ) == logHeader::MAX_LEN0 - 1 )
611 {
612 // Intermediate size message, read two more bytes
613 nrd = fread( head.get() + logHeader::minHeadSize, sizeof( char ), sizeof( msgLen1T ), fin );
614 }
615 else if( logHeader::msgLen0( head ) == logHeader::MAX_LEN0 )
616 {
617 // Large size message: read 8 more bytes
618 nrd = fread( head.get() + logHeader::minHeadSize, sizeof( char ), sizeof( msgLen2T ), fin );
619 }
620
621 // lvl = logHeader::logLevel(head);
622 // ec = logHeader::eventCode(head);
623 len = logHeader::msgLen( head );
624 ts = logHeader::timespec( head );
625 // hSz = logHeader::headerSize(head);
626
627 fseek( fin, len, SEEK_CUR );
628 }
629
630 fclose( fin );
631
632 double t0 = ts0.time_s + ts0.time_ns / 1e9;
633 double t = ts.time_s + ts.time_ns / 1e9;
634
635 std::cout << fname << " " << ts0.ISO8601DateTimeStrX() << "Z " << ts.ISO8601DateTimeStrX() << "Z " << t - t0
636 << " " << nRecords << std::endl;
637 }
638
639 return 0;
640}
641
642#endif // logdump_hpp
static constexpr int minHeadSize
The minimum header size.
Definition logHeader.hpp:84
static constexpr size_t MAX_LEN0
The max value in the msgLen0 field.
Definition logHeader.hpp:76
static constexpr int maxHeadSize
The maximum header size.
Definition logHeader.hpp:90
An application to dump MagAo-X binary logs to the terminal.
Definition logdump.hpp:42
bool m_follow
Number of files to dump. Default is 0, unless following then the default is 1.
Definition logdump.hpp:61
size_t m_nfiles
Definition logdump.hpp:59
logPrioT m_level
Definition logdump.hpp:63
std::string m_dir
Definition logdump.hpp:46
std::string m_file
Definition logdump.hpp:48
virtual int gettimes(std::vector< std::string > &logs)
Definition logdump.hpp:538
std::vector< eventCodeT > m_codes
Definition logdump.hpp:65
void printLogJson(const msgLenT &len, bufferPtrT &logBuff)
Definition logdump.hpp:531
virtual int execute()
Definition logdump.hpp:252
int m_fileCheckInterval
When following, pause time to check for new data. msec. Default is 250 msec.
Definition logdump.hpp:54
bool m_time
Definition logdump.hpp:50
virtual void setupConfig()
Definition logdump.hpp:80
virtual void loadConfig()
Definition logdump.hpp:189
unsigned long m_pauseTime
Definition logdump.hpp:53
void printLogBuff(const logPrioT &lvl, const eventCodeT &ec, const msgLenT &len, bufferPtrT &logBuff)
Definition logdump.hpp:475
std::string m_ext
Definition logdump.hpp:47
std::vector< std::string > m_prefixes
When following, number of loops to wait before checking for a new file. Default is 4.
Definition logdump.hpp:57
mx::verbose::vvv verboseT
Definition logdump.hpp:43
bool m_jsonMode
Definition logdump.hpp:51
#define MAGAOX_default_logExt
The extension for MagAO-X binary log files.
Definition defaults.hpp:22
#define MAGAOX_logRelPath
The relative path to the log directory.
Definition paths.hpp:50
#define MAGAOX_path
The path to the MagAO-X system files.
Definition paths.hpp:22
#define MAGAOX_env_path
Environment variable setting the MagAO-X path.
uint16_t msgLen1T
The type used for intermediate message length.
Definition logDefs.hpp:54
uint16_t eventCodeT
The type of an event code (16-bit unsigned int).
Definition logDefs.hpp:40
uint64_t msgLen2T
The type used for long message length.
Definition logDefs.hpp:62
msgLen2T msgLenT
The type used to refer to the message length, regardless of length.
Definition logDefs.hpp:69
int8_t logPrioT
The type of the log priority code.
Definition logDefs.hpp:21
static int eventCode(bufferPtrT &logBuffer, const eventCodeT &ec)
Set the event code of a log entry.
static msgLen0T msgLen0(bufferPtrT &logBuffer)
Extract the short message length of a log entry message.
static void * messageBuffer(bufferPtrT &logBuffer)
Get the message buffer address.
std::shared_ptr< char > bufferPtrT
The log entry buffer smart pointer.
Definition logHeader.hpp:58
static size_t headerSize(bufferPtrT &logBuffer)
Get the size of the header, including the variable size length field, for an existing logBuffer.
static int msgLen(bufferPtrT &logBuffer, const msgLenT &msgLen)
Set the message length of a log entry message.
static int logLevel(bufferPtrT &logBuffer, const logPrioT &lvl)
Set the level of a log entry in a logBuffer header.
static int timespec(bufferPtrT &logBuffer, const timespecX &ts)
Set the timespec of a log entry.
mx::error_t parseFilePath(std::string &devName, std::string &YYYY, std::string &MM, std::string &DD, std::string &hh, std::string &mm, std::string &ss, std::string &nn, const std::string &fname)
Parse a standard XWCTk timestamp filepath.
static constexpr flatlogs::eventCodeT GIT_STATE
Definition logCodes.hpp:10
iosT & logJsonFormat(iosT &ios, flatlogs::bufferPtrT &buffer)
bool logVerify(flatlogs::eventCodeT ec, flatlogs::bufferPtrT &buffer, flatlogs::msgLenT len)
Definition logVerify.hpp:9
iosT & logStdFormat(iosT &ios, flatlogs::bufferPtrT &buffer)
static constexpr logPrioT LOG_NOTICE
A normal but significant condition.
static constexpr logPrioT LOG_INFO
Informational. The info log level is the lowest level recorded during normal operations.
static constexpr logPrioT LOG_CRITICAL
The process can not continue and will shut down (fatal)
static constexpr logPrioT LOG_WARNING
A condition has occurred which may become an error, but the process continues.
static constexpr logPrioT LOG_ERROR
An error has occured which the software will attempt to correct.
static constexpr logPrioT LOG_DEFAULT
Used to denote "use the default level for this log type".
static constexpr logPrioT LOG_EMERGENCY
Normal operations of the entire system should be shut down immediately.
static constexpr logPrioT LOG_ALERT
This should only be used if some action is required by operators to keep the system safe.
logPrioT logLevelFromString(const std::string &str)
Get the log priority from a string, which might have the number or the name.
static std::string repoName(void *msgBuffer)
Access the repo name field.
Definition git_state.hpp:85
A fixed-width timespec structure.
Definition timespecX.hpp:35
nanosecT time_ns
Nanoseconds.
Definition timespecX.hpp:37
secT time_s
Time since the Unix epoch.
Definition timespecX.hpp:36
std::string ISO8601DateTimeStrX()
Get a date-time string in ISO 8601 format for timespecX.