mirror of
https://github.com/libretro/RetroArch.git
synced 2026-11-07 08:36:02 -10:00
RARCH_LOG_V copies each formatted line into a ring of the last 256 lines once it is armed, which the MCP server's start or the first GET_LOG does; unarmed, a log line costs one relaxed load more. Any thread may log: a writer claims a line number, writes the slot as atomic words between a busy mark and its stamp, and the reader keeps a line only when the stamp matches before and after the copy, so a reader never waits and never sees a torn line. GET_LOG answers the lines since the one asked for, then NEXT; lines overwritten before they were read are counted as SKIPPED. Desktop builds with the MCP server only; samples/runloop/log_ring writes from four threads while reading, under ASan and TSan.
829 lines
25 KiB
C
829 lines
25 KiB
C
/* RetroArch - A frontend for libretro.
|
|
* Copyright (C) 2011-2017 - Daniel De Matteis
|
|
* Copyright (C) 2016-2019 - Brad Parker
|
|
*
|
|
* RetroArch is free software: you can redistribute it and/or modify it under the terms
|
|
* of the GNU General Public License as published by the Free Software Found-
|
|
* ation, either version 3 of the License, or (at your option) any later version.
|
|
*
|
|
* RetroArch is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
|
|
* without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
|
|
* PURPOSE. See the GNU General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public License along with RetroArch.
|
|
* If not, see <http://www.gnu.org/licenses/>.
|
|
*/
|
|
|
|
#ifdef _XBOX1
|
|
#include <xtl.h>
|
|
#endif
|
|
|
|
#if defined(__PSL1GHT__) || defined(__PS3__)
|
|
#include <defines/ps3_defines.h>
|
|
#endif
|
|
|
|
#ifdef __MACH__
|
|
#include <TargetConditionals.h>
|
|
#include <Availability.h>
|
|
#if TARGET_IPHONE_SIMULATOR
|
|
#include <stdio.h>
|
|
#else
|
|
/* The same test as the os_log call site below: a 10.0 floor logs through
|
|
* os_log, anything lower (or an SDK without os/log.h) through asl. */
|
|
#if (defined(__IPHONE_10_0) && (__IPHONE_OS_VERSION_MIN_REQUIRED >= __IPHONE_10_0)) \
|
|
|| (defined(__TV_OS_VERSION_MIN_REQUIRED) && defined(__TVOS_10_0) \
|
|
&& (__TV_OS_VERSION_MIN_REQUIRED >= __TVOS_10_0))
|
|
#include <os/log.h>
|
|
#else
|
|
#include <asl.h>
|
|
#endif
|
|
#endif
|
|
#endif
|
|
|
|
#include <stdio.h>
|
|
#include <stdarg.h>
|
|
#include <time.h>
|
|
|
|
#ifdef _MSC_VER
|
|
#include <compat/msvc.h>
|
|
#endif
|
|
#include <compat/strl.h>
|
|
|
|
#ifdef ANDROID
|
|
#include <android/log.h>
|
|
#endif
|
|
|
|
#if defined(_WIN32)
|
|
|
|
#if defined(_XBOX)
|
|
#include <Xtl.h>
|
|
#else
|
|
#ifndef WIN32_LEAN_AND_MEAN
|
|
#define WIN32_LEAN_AND_MEAN
|
|
#endif
|
|
#include <windows.h>
|
|
#endif
|
|
|
|
#endif
|
|
|
|
#include <file/file_path.h>
|
|
#include <streams/file_stream.h>
|
|
#include <compat/fopen_utf8.h>
|
|
#include <time/rtime.h>
|
|
#include <retro_miscellaneous.h>
|
|
|
|
#ifdef HAVE_CONFIG_H
|
|
#include "config.h"
|
|
#endif
|
|
|
|
#ifdef RARCH_INTERNAL
|
|
#include "frontend/frontend_driver.h"
|
|
#endif
|
|
|
|
#include "verbosity.h"
|
|
#include "file_path_special.h"
|
|
|
|
#ifdef RARCH_INTERNAL
|
|
/* Defines HAVE_COMPANION_WIMP when a desktop companion (Qt, native
|
|
* Win32, native Cocoa) is in the build; its log view mirrors the log. */
|
|
#include "ui/ui_companion_driver.h"
|
|
#endif
|
|
|
|
/* The companion copy of a log line is formatted from a copy of the
|
|
* argument list so the platform sink below still sees an untouched one.
|
|
* C89 has no va_copy; gcc/clang spell the same thing __va_copy. */
|
|
#ifndef va_copy
|
|
# ifdef __va_copy
|
|
# define va_copy(dst, src) __va_copy(dst, src)
|
|
# else
|
|
# define va_copy(dst, src) ((dst) = (src))
|
|
# endif
|
|
#endif
|
|
|
|
#ifdef RARCH_INTERNAL
|
|
#include "config.def.h"
|
|
#else
|
|
#define DEFAULT_FRONTEND_LOG_LEVEL 1
|
|
#endif
|
|
|
|
#if defined(IS_SALAMANDER)
|
|
#define FILE_PATH_PROGRAM_NAME "RetroArch Salamander"
|
|
#else
|
|
#define FILE_PATH_PROGRAM_NAME "RetroArch"
|
|
#endif
|
|
|
|
typedef struct verbosity_state
|
|
{
|
|
#ifdef HAVE_LIBNX
|
|
Mutex mtx;
|
|
#endif
|
|
/* If this is non-NULL, RARCH_LOG and friends
|
|
* will write to this file. */
|
|
FILE *fp; /* pointer-sized: keep near top */
|
|
|
|
/* Booleans grouped together to avoid padding holes */
|
|
bool verbosity;
|
|
bool initialized;
|
|
bool override_active;
|
|
|
|
/* Large array last: avoids padding before it */
|
|
char override_path[PATH_MAX_LENGTH];
|
|
} verbosity_state_t;
|
|
|
|
/* TODO/FIXME - static public global variables */
|
|
static verbosity_state_t main_verbosity_st;
|
|
static unsigned verbosity_log_level =
|
|
DEFAULT_FRONTEND_LOG_LEVEL;
|
|
|
|
#ifdef HAVE_LIBNX
|
|
#ifdef NXLINK
|
|
/* TODO/FIXME - global referenced in platform_switch.c - not
|
|
* thread-safe */
|
|
bool nxlink_connected = false;
|
|
#endif /* NXLINK */
|
|
|
|
#endif /* HAVE_LIBNX */
|
|
|
|
void verbosity_set_log_level(unsigned level)
|
|
{
|
|
verbosity_log_level = level;
|
|
}
|
|
|
|
unsigned verbosity_get_log_level(void)
|
|
{
|
|
return verbosity_log_level;
|
|
}
|
|
|
|
void verbosity_enable(void)
|
|
{
|
|
main_verbosity_st.verbosity = true;
|
|
#ifdef RARCH_INTERNAL
|
|
if (!main_verbosity_st.initialized)
|
|
frontend_driver_attach_console();
|
|
#endif
|
|
}
|
|
|
|
void verbosity_disable(void)
|
|
{
|
|
main_verbosity_st.verbosity = false;
|
|
#ifdef RARCH_INTERNAL
|
|
if (!main_verbosity_st.initialized)
|
|
frontend_driver_detach_console();
|
|
#endif
|
|
}
|
|
|
|
bool verbosity_is_enabled(void)
|
|
{
|
|
return main_verbosity_st.verbosity;
|
|
}
|
|
|
|
bool is_logging_to_file(void)
|
|
{
|
|
return main_verbosity_st.initialized;
|
|
}
|
|
|
|
/* The one consumer is the menu's LOG_VERBOSITY setting, which
|
|
* binds this as its boolean target. The framework writes through
|
|
* it, and the setting's change handler immediately re-drives
|
|
* verbosity_enable()/verbosity_disable() from the written value, so
|
|
* the console attach/detach side effect always follows. Removing
|
|
* the getter would mean shadowing the flag in menu code and syncing
|
|
* it by hand; a bound pointer with a reconciling handler is the
|
|
* smaller contract. Nothing else may write through this. */
|
|
bool *verbosity_get_ptr(void)
|
|
{
|
|
return &main_verbosity_st.verbosity;
|
|
}
|
|
|
|
void retro_main_log_file_init(const char *path, bool append)
|
|
{
|
|
FILE *tmp;
|
|
#ifdef HAVE_LIBNX
|
|
static bool mtx_inited = false;
|
|
#endif
|
|
|
|
if (main_verbosity_st.initialized)
|
|
return;
|
|
|
|
#ifdef HAVE_LIBNX
|
|
/* Once for the process: a file->console->file cycle re-enters
|
|
* here, and re-initializing a mutex another thread may be
|
|
* holding for a write is exactly the race the mutex exists to
|
|
* prevent. Deinit never destroys it for the same reason. */
|
|
if (!mtx_inited)
|
|
{
|
|
mutexInit(&main_verbosity_st.mtx);
|
|
mtx_inited = true;
|
|
}
|
|
#endif
|
|
|
|
/* Default to stderr; only override when a valid path is given */
|
|
main_verbosity_st.fp = stderr;
|
|
|
|
if (!path)
|
|
return;
|
|
|
|
tmp = (FILE*)fopen_utf8(path, append ? "ab" : "wb");
|
|
|
|
if (!tmp)
|
|
{
|
|
RARCH_ERR("Failed to open system event log file: \"%s\".\n", path);
|
|
return;
|
|
}
|
|
|
|
main_verbosity_st.fp = tmp;
|
|
main_verbosity_st.initialized = true;
|
|
|
|
/* No setvbuf here, deliberately.
|
|
*
|
|
* There used to be a 16 KiB buffer installed at this point, with a
|
|
* note wondering which platforms it helped. The answer is none, and
|
|
* the reason is a few lines further down: every path that writes a
|
|
* log line calls fflush immediately afterwards, so the buffer never
|
|
* holds more than the line just written and the write reaches the
|
|
* operating system either way. Measured at twenty thousand lines,
|
|
* a 16 KiB buffer and no buffer at all are the same speed, and both
|
|
* cost exactly one write per line.
|
|
*
|
|
* The flushing is the part worth keeping. A log is read after a
|
|
* crash, so a line that is still sitting in a buffer when the
|
|
* process dies is a line that was not worth writing. Given that,
|
|
* the buffer only added an allocation and a copy per line. */
|
|
}
|
|
|
|
void retro_main_log_file_deinit(void)
|
|
{
|
|
if (main_verbosity_st.fp && main_verbosity_st.initialized)
|
|
{
|
|
fclose(main_verbosity_st.fp);
|
|
main_verbosity_st.fp = NULL;
|
|
}
|
|
|
|
main_verbosity_st.initialized = false;
|
|
}
|
|
|
|
#if !defined(HAVE_LOGGER)
|
|
/* Compiled for every consumer past the Xbox/WinRT head branch,
|
|
* which formats into its own bounded buffer: the Android file arm,
|
|
* the Apple arms (one format feeding printf/os_log/asl and the
|
|
* file), the desktop companion mirror, and the generic branch. */
|
|
#if defined(ANDROID) \
|
|
|| (!defined(_XBOX1) && !defined(__WINRT__))
|
|
/* Format tag and message into one buffer and write it with a single
|
|
* stdio call: the per-call lock then keeps concurrent lines whole.
|
|
* Lines wider than the stack buffer take an exact-sized heap detour
|
|
* rather than truncating; if that allocation fails, the truncated
|
|
* stack line ships - a shortened message over a dropped one. */
|
|
/* Format "tag body" once into the caller's stack line, taking an
|
|
* exact-sized heap detour for wider lines rather than truncating
|
|
* (allocation failure ships the truncated stack line - a shortened
|
|
* message over a dropped one). Returns the buffer to hand to every
|
|
* sink; *heap is owned by the caller and freed after the sinks are
|
|
* done. Consumes one formatting pass of ap (plus one of a private
|
|
* copy for the measuring pass). */
|
|
static const char *rarch_log_line_format(char *line, size_t line_size,
|
|
char **heap, const char *tag_v, const char *fmt, va_list ap)
|
|
{
|
|
int t_len = snprintf(line, line_size, "%s ", tag_v);
|
|
int b_len;
|
|
va_list ap_cp;
|
|
|
|
*heap = NULL;
|
|
|
|
if (t_len < 0 || t_len >= (int)line_size)
|
|
t_len = 0;
|
|
|
|
va_copy(ap_cp, ap);
|
|
b_len = vsnprintf(line + t_len, line_size - (size_t)t_len,
|
|
fmt, ap_cp);
|
|
va_end(ap_cp);
|
|
|
|
if (b_len >= (int)(line_size - (size_t)t_len))
|
|
{
|
|
size_t need = (size_t)t_len + (size_t)b_len + 1;
|
|
if ((*heap = (char*)malloc(need)))
|
|
{
|
|
memcpy(*heap, line, (size_t)t_len);
|
|
vsnprintf(*heap + t_len, need - (size_t)t_len, fmt, ap);
|
|
return *heap;
|
|
}
|
|
}
|
|
else if (b_len < 0)
|
|
line[t_len] = '\0';
|
|
|
|
return line;
|
|
}
|
|
|
|
/* One write, one flush, under the LibNX mutex where it exists.
|
|
*
|
|
* The flush is unconditional, for every tag. Buffering informational
|
|
* lines was tried and taken back out: a log is read after a crash, and
|
|
* the lines that explain it are usually the INFO/DEBUG ones written
|
|
* just before it - exactly the tail a buffer loses - and anything
|
|
* following the log live (tail -f, a monitoring script) sees nothing
|
|
* until the buffer happens to fill. */
|
|
static void rarch_log_line_emit(FILE *fp, const char *out)
|
|
{
|
|
#if defined(HAVE_LIBNX)
|
|
/* Around exactly one write and its flush; libnx newlib's stdio
|
|
* locking history is why this exists at all. */
|
|
mutexLock(&main_verbosity_st.mtx);
|
|
#endif
|
|
fputs(out, fp);
|
|
fflush(fp);
|
|
#if defined(HAVE_LIBNX)
|
|
mutexUnlock(&main_verbosity_st.mtx);
|
|
#endif
|
|
}
|
|
|
|
#if defined(ANDROID)
|
|
/* The Android file arm is the one caller that still wants
|
|
* format-and-emit as a unit; the generic and Apple region formats
|
|
* once up front and hands the buffer to each sink itself. */
|
|
static void rarch_log_line_write(FILE *fp, const char *tag_v,
|
|
const char *fmt, va_list ap)
|
|
{
|
|
char line[1024];
|
|
char *heap = NULL;
|
|
const char *out = rarch_log_line_format(line, sizeof(line), &heap,
|
|
tag_v, fmt, ap);
|
|
|
|
rarch_log_line_emit(fp, out);
|
|
free(heap);
|
|
}
|
|
#endif
|
|
#endif
|
|
|
|
#ifdef HAVE_MCP
|
|
#include <string.h>
|
|
#include <retro_atomic.h>
|
|
|
|
/* The last lines logged, for GET_LOG, filled by whatever thread logs
|
|
* and read on the main thread without a lock: a writer claims a line
|
|
* number, marks the slot busy, writes the text as atomic words and
|
|
* stamps the slot with its number; a reader takes a slot's text only
|
|
* when the stamp is the line it wants before and after the copy. */
|
|
#define LOG_RING_SLOTS 256
|
|
#define LOG_RING_WORDS (256 / sizeof(size_t))
|
|
|
|
struct log_ring_slot
|
|
{
|
|
retro_atomic_size_t stamp; /* line number + 1, 0 while written */
|
|
retro_atomic_size_t text[LOG_RING_WORDS];
|
|
};
|
|
|
|
static struct
|
|
{
|
|
struct log_ring_slot slots[LOG_RING_SLOTS];
|
|
retro_atomic_size_t head;
|
|
retro_atomic_int_t armed;
|
|
} log_ring;
|
|
|
|
void verbosity_ring_arm(void)
|
|
{
|
|
retro_atomic_store_release_int(&log_ring.armed, 1);
|
|
}
|
|
|
|
static void verbosity_ring_put(const char *line)
|
|
{
|
|
size_t i;
|
|
size_t seq;
|
|
size_t words[LOG_RING_WORDS];
|
|
struct log_ring_slot *slot;
|
|
|
|
if (!retro_atomic_load_relaxed_int(&log_ring.armed))
|
|
return;
|
|
memset(words, 0, sizeof(words));
|
|
strlcpy((char*)words, line, sizeof(words));
|
|
seq = retro_atomic_fetch_add_size(&log_ring.head, 1);
|
|
slot = &log_ring.slots[seq % LOG_RING_SLOTS];
|
|
retro_atomic_store_relaxed_size(&slot->stamp, 0);
|
|
retro_atomic_thread_fence_release();
|
|
for (i = 0; i < LOG_RING_WORDS; i++)
|
|
retro_atomic_store_relaxed_size(&slot->text[i], words[i]);
|
|
retro_atomic_store_release_size(&slot->stamp, seq + 1);
|
|
}
|
|
|
|
size_t verbosity_ring_read(size_t since, char *s, size_t len,
|
|
size_t *first, size_t *next)
|
|
{
|
|
size_t _len = 0;
|
|
size_t head = retro_atomic_load_acquire_size(&log_ring.head);
|
|
size_t seq = since;
|
|
|
|
if (head > LOG_RING_SLOTS && seq < head - LOG_RING_SLOTS)
|
|
seq = head - LOG_RING_SLOTS;
|
|
if (seq > head)
|
|
seq = head;
|
|
*first = seq;
|
|
for (; seq < head; seq++)
|
|
{
|
|
size_t i, n;
|
|
size_t words[LOG_RING_WORDS];
|
|
struct log_ring_slot *slot = &log_ring.slots[seq % LOG_RING_SLOTS];
|
|
size_t stamp = retro_atomic_load_acquire_size(&slot->stamp);
|
|
/* still being written, or already a later line: skipped */
|
|
if (stamp != seq + 1)
|
|
continue;
|
|
for (i = 0; i < LOG_RING_WORDS; i++)
|
|
words[i] = retro_atomic_load_relaxed_size(&slot->text[i]);
|
|
retro_atomic_thread_fence_acquire();
|
|
if (retro_atomic_load_relaxed_size(&slot->stamp) != stamp)
|
|
continue;
|
|
((char*)words)[sizeof(words) - 1] = '\0';
|
|
n = strlen((const char*)words);
|
|
if (_len + n + 2 > len)
|
|
break;
|
|
memcpy(s + _len, words, n);
|
|
_len += n;
|
|
if (!n || s[_len - 1] != '\n')
|
|
s[_len++] = '\n';
|
|
}
|
|
if (len)
|
|
s[_len < len ? _len : len - 1] = '\0';
|
|
*next = seq;
|
|
return _len;
|
|
}
|
|
#else
|
|
#define verbosity_ring_put(line) ((void)0)
|
|
#endif
|
|
|
|
void RARCH_LOG_V(const char *tag, const char *fmt, va_list ap)
|
|
{
|
|
#if defined(_XBOX1) || defined(__WINRT__)
|
|
/* wvsprintf takes no size and writes past any small buffer on a
|
|
* long expansion; vsnprintf is bounded on both targets (the msvc
|
|
* compat header maps it for the Xbox toolchain), and 1024 matches
|
|
* the line budget the generic branch uses. Truncation stays the
|
|
* policy here - no heap on this target. */
|
|
char buffer[1024];
|
|
int _len;
|
|
const char *tag_v = tag ? tag : FILE_PATH_LOG_INFO;
|
|
buffer[0] = '\0';
|
|
_len = snprintf(buffer, sizeof(buffer),
|
|
"%s: %s ", FILE_PATH_PROGRAM_NAME, tag_v);
|
|
|
|
if (_len > 0 && _len < (int)sizeof(buffer))
|
|
vsnprintf(buffer + _len, sizeof(buffer) - (size_t)_len, fmt, ap);
|
|
#ifdef _DEBUG
|
|
OutputDebugStringA(buffer);
|
|
#endif
|
|
if (main_verbosity_st.initialized && main_verbosity_st.fp)
|
|
{
|
|
fputs(buffer, main_verbosity_st.fp);
|
|
fflush(main_verbosity_st.fp);
|
|
}
|
|
|
|
#elif defined(ANDROID)
|
|
{
|
|
FILE *fp = main_verbosity_st.fp;
|
|
if (main_verbosity_st.initialized && fp)
|
|
{
|
|
/* Logging to file: one formatted write, tag included -
|
|
* the file carries the same line format as every other
|
|
* platform (logcat gets the tag as a priority instead). */
|
|
const char *tag_v = tag ? tag : FILE_PATH_LOG_INFO;
|
|
rarch_log_line_write(fp, tag_v, fmt, ap);
|
|
}
|
|
else
|
|
{
|
|
int prio = ANDROID_LOG_INFO;
|
|
if (tag)
|
|
{
|
|
/* strcmp, not memcmp with the pattern's sizeof: a sized
|
|
* compare reads past a shorter tag's terminator. */
|
|
if (strcmp(tag, FILE_PATH_LOG_WARN) == 0)
|
|
prio = ANDROID_LOG_WARN;
|
|
else if (strcmp(tag, FILE_PATH_LOG_ERROR) == 0)
|
|
prio = ANDROID_LOG_ERROR;
|
|
}
|
|
__android_log_vprint(prio, FILE_PATH_PROGRAM_NAME, fmt, ap);
|
|
}
|
|
}
|
|
|
|
#else
|
|
{
|
|
FILE *fp = main_verbosity_st.fp;
|
|
const char *tag_v = tag ? tag : FILE_PATH_LOG_INFO;
|
|
/* One format for every consumer in this region - the desktop
|
|
* companion's log view, the Apple sinks, the file - instead of
|
|
* a formatting pass (and on Apple a heap vasprintf, and in the
|
|
* companion a second 1024-byte copy whose overflow went
|
|
* unhandled) per sink. */
|
|
char line[1024];
|
|
char *heap = NULL;
|
|
const char *out = rarch_log_line_format(line, sizeof(line),
|
|
&heap, tag_v, fmt, ap);
|
|
|
|
verbosity_ring_put(out);
|
|
|
|
#ifdef HAVE_COMPANION_WIMP
|
|
/* Mirror the line into the desktop companion's log view, when
|
|
* one is open. The companion now shows the tag too, and wide
|
|
* lines arrive whole through the shared heap detour. */
|
|
if (ui_companion_driver_log_active())
|
|
ui_companion_driver_log_msg(out);
|
|
#endif
|
|
|
|
#if TARGET_OS_MAC
|
|
{
|
|
#if TARGET_OS_MAC && !TARGET_OS_IPHONE
|
|
/* Emit to the terminal unconditionally so Terminal.app and
|
|
* Xcode's console always see output. The file write is gated
|
|
* on `initialized` (= a real log file was opened) because fp
|
|
* defaults to stderr when no file is configured; without the
|
|
* guard, every line would print twice in the no-file case. */
|
|
printf("%s", out);
|
|
if (main_verbosity_st.initialized && fp)
|
|
rarch_log_line_emit(fp, out);
|
|
|
|
#else
|
|
{
|
|
#if TARGET_OS_SIMULATOR
|
|
fprintf(stderr, "%s", out);
|
|
#elif defined(__IPHONE_10_0) && (__IPHONE_OS_VERSION_MIN_REQUIRED >= __IPHONE_10_0)
|
|
os_log(OS_LOG_DEFAULT, "%s", out);
|
|
#elif defined(__TV_OS_VERSION_MIN_REQUIRED) && defined(__TVOS_10_0) \
|
|
&& (__TV_OS_VERSION_MIN_REQUIRED >= __TVOS_10_0)
|
|
os_log(OS_LOG_DEFAULT, "%s", out);
|
|
#else
|
|
{
|
|
static aslclient asl_client = NULL;
|
|
static int asl_initialized = 0;
|
|
aslmsg msg;
|
|
|
|
if (!asl_initialized)
|
|
{
|
|
asl_client = asl_open(FILE_PATH_PROGRAM_NAME,
|
|
"com.apple.console",
|
|
ASL_OPT_STDERR | ASL_OPT_NO_DELAY);
|
|
asl_initialized = 1;
|
|
}
|
|
msg = asl_new(ASL_TYPE_MSG);
|
|
asl_set(msg, ASL_KEY_READ_UID, "-1");
|
|
asl_log(asl_client, msg, ASL_LEVEL_NOTICE, "%s", out);
|
|
asl_free(msg);
|
|
}
|
|
#endif
|
|
|
|
if (main_verbosity_st.initialized && fp)
|
|
rarch_log_line_emit(fp, out);
|
|
}
|
|
#endif
|
|
}
|
|
|
|
#else
|
|
if (fp)
|
|
/* stdio's per-call lock keeps the single write whole under
|
|
* concurrent writers; the format above already took the
|
|
* heap detour for wide lines. */
|
|
rarch_log_line_emit(fp, out);
|
|
#endif
|
|
|
|
free(heap);
|
|
}
|
|
#endif
|
|
}
|
|
|
|
void RARCH_LOG_BUFFER(uint8_t *data, size_t len)
|
|
{
|
|
size_t i;
|
|
size_t offset = 0;
|
|
uint8_t buf[16];
|
|
|
|
if (!data && len)
|
|
return;
|
|
|
|
RARCH_LOG("== %d-byte buffer ==================\n", (int)len);
|
|
|
|
/* Zero-init once; tail padding written below */
|
|
for (i = 0; i < 16; i++)
|
|
buf[i] = 0;
|
|
|
|
for (i = 0; i < len; i++)
|
|
{
|
|
buf[offset++] = data[i];
|
|
if (offset == 16)
|
|
{
|
|
offset = 0;
|
|
RARCH_LOG("%02x%02x%02x%02x%02x%02x%02x%02x %02x%02x%02x%02x%02x%02x%02x%02x\n",
|
|
buf[0], buf[1], buf[2], buf[3], buf[4], buf[5], buf[6], buf[7],
|
|
buf[8], buf[9], buf[10], buf[11], buf[12], buf[13], buf[14], buf[15]);
|
|
}
|
|
}
|
|
|
|
/* Flush partial last row (offset > 0 means leftover bytes) */
|
|
if (offset > 0)
|
|
{
|
|
/* Pad remainder with 0xff to match original sentinel */
|
|
for (i = offset; i < 16; i++)
|
|
buf[i] = 0xff;
|
|
RARCH_LOG("%02x%02x%02x%02x%02x%02x%02x%02x %02x%02x%02x%02x%02x%02x%02x%02x\n",
|
|
buf[0], buf[1], buf[2], buf[3], buf[4], buf[5], buf[6], buf[7],
|
|
buf[8], buf[9], buf[10], buf[11], buf[12], buf[13], buf[14], buf[15]);
|
|
}
|
|
RARCH_LOG("==================================\n");
|
|
}
|
|
|
|
void RARCH_DBG(const char *fmt, ...)
|
|
{
|
|
va_list ap;
|
|
#ifndef _DEBUG
|
|
if (!main_verbosity_st.verbosity || verbosity_log_level > 0)
|
|
return;
|
|
#endif
|
|
va_start(ap, fmt);
|
|
RARCH_LOG_V(FILE_PATH_LOG_DBG, fmt, ap);
|
|
va_end(ap);
|
|
}
|
|
|
|
void RARCH_LOG(const char *fmt, ...)
|
|
{
|
|
va_list ap;
|
|
#ifndef _DEBUG
|
|
if (!main_verbosity_st.verbosity || verbosity_log_level > 1)
|
|
return;
|
|
#endif
|
|
va_start(ap, fmt);
|
|
RARCH_LOG_V(FILE_PATH_LOG_INFO, fmt, ap);
|
|
va_end(ap);
|
|
}
|
|
|
|
void RARCH_LOG_OUTPUT(const char *msg, ...)
|
|
{
|
|
va_list ap;
|
|
va_start(ap, msg);
|
|
RARCH_LOG_OUTPUT_V(FILE_PATH_LOG_INFO, msg, ap);
|
|
va_end(ap);
|
|
}
|
|
|
|
void RARCH_WARN(const char *fmt, ...)
|
|
{
|
|
va_list ap;
|
|
#ifndef _DEBUG
|
|
if (!main_verbosity_st.verbosity || verbosity_log_level > 2)
|
|
return;
|
|
#endif
|
|
va_start(ap, fmt);
|
|
RARCH_WARN_V(FILE_PATH_LOG_WARN, fmt, ap);
|
|
va_end(ap);
|
|
}
|
|
|
|
void RARCH_ERR(const char *fmt, ...)
|
|
{
|
|
va_list ap;
|
|
#ifndef _DEBUG
|
|
if (!main_verbosity_st.verbosity)
|
|
return;
|
|
#endif
|
|
va_start(ap, fmt);
|
|
RARCH_ERR_V(FILE_PATH_LOG_ERROR, fmt, ap);
|
|
va_end(ap);
|
|
}
|
|
#endif
|
|
|
|
size_t rarch_log_file_set_override(const char *path)
|
|
{
|
|
main_verbosity_st.override_active = true;
|
|
return strlcpy(main_verbosity_st.override_path, path,
|
|
sizeof(main_verbosity_st.override_path));
|
|
}
|
|
|
|
void rarch_log_file_init(
|
|
bool log_to_file,
|
|
bool log_to_file_timestamp,
|
|
const char *log_dir
|
|
)
|
|
{
|
|
char log_directory[DIR_MAX_LENGTH] = {0};
|
|
char log_file_path[PATH_MAX_LENGTH];
|
|
static bool log_file_created = false;
|
|
static char timestamped_log_file_name[64] = {0};
|
|
bool logging_to_file = main_verbosity_st.initialized;
|
|
|
|
/* The override branch below leaves log_directory untouched
|
|
* when the override path has no directory component
|
|
* (e.g. --log-file retroarch.log); an uninitialised buffer
|
|
* here was passed to path_mkdir(), creating garbage-named
|
|
* directories in the current working directory. */
|
|
log_directory[0] = '\0';
|
|
log_file_path[0] = '\0';
|
|
|
|
/* If this is the first run, generate a timestamped log
|
|
* file name (do this even when not outputting timestamped
|
|
* log files, since user may decide to switch at any moment...) */
|
|
if (!timestamped_log_file_name[0])
|
|
{
|
|
struct tm tm_;
|
|
time_t cur_time = time(NULL);
|
|
|
|
rtime_localtime(&cur_time, &tm_);
|
|
#ifdef DJGPP
|
|
strftime(timestamped_log_file_name,
|
|
sizeof(timestamped_log_file_name),
|
|
"RA%d%H%M.log", &tm_);
|
|
#else
|
|
strftime(timestamped_log_file_name,
|
|
sizeof(timestamped_log_file_name),
|
|
"retroarch__%Y_%m_%d__%H_%M_%S.log", &tm_);
|
|
#endif
|
|
}
|
|
|
|
/* If nothing has changed, do nothing */
|
|
if ( (!log_to_file && !logging_to_file)
|
|
|| (log_to_file && logging_to_file))
|
|
return;
|
|
|
|
/* If we are currently logging to file and wish to stop,
|
|
* deinitialise existing logger... */
|
|
if (!log_to_file && logging_to_file)
|
|
{
|
|
retro_main_log_file_deinit();
|
|
/* ...and revert to console */
|
|
retro_main_log_file_init(NULL, false);
|
|
return;
|
|
}
|
|
|
|
/* If we reach this point, then we are not currently
|
|
* logging to file, and wish to do so */
|
|
|
|
/* > Check whether we are already logging to console */
|
|
/* De-initialise existing logger */
|
|
if (main_verbosity_st.fp)
|
|
retro_main_log_file_deinit();
|
|
|
|
/* Get directory/file paths */
|
|
if (main_verbosity_st.override_active)
|
|
{
|
|
/* Get log directory */
|
|
const char *override_path = main_verbosity_st.override_path;
|
|
const char *last_slash = find_last_slash(override_path);
|
|
|
|
if (last_slash)
|
|
{
|
|
char tmp_buf[PATH_MAX_LENGTH] = {0};
|
|
size_t __len = last_slash + 1 - override_path;
|
|
|
|
if ((__len > 1) && (__len < PATH_MAX_LENGTH))
|
|
strlcpy(tmp_buf, override_path, __len * sizeof(char));
|
|
strlcpy(log_directory, tmp_buf, sizeof(log_directory));
|
|
}
|
|
|
|
/* Get log file path */
|
|
strlcpy(log_file_path, override_path, sizeof(log_file_path));
|
|
}
|
|
else if (log_dir && *log_dir)
|
|
{
|
|
/* Get log directory */
|
|
strlcpy(log_directory, log_dir, sizeof(log_directory));
|
|
|
|
/* Get log file path */
|
|
fill_pathname_join_special(log_file_path,
|
|
log_dir,
|
|
log_to_file_timestamp
|
|
? timestamped_log_file_name
|
|
: FILE_PATH_DEFAULT_EVENT_LOG,
|
|
sizeof(log_file_path));
|
|
}
|
|
else
|
|
log_file_path[0] = '\0';
|
|
|
|
/* Attempt to initialise log file */
|
|
if (log_file_path[0])
|
|
{
|
|
/* Create log directory, if required */
|
|
if ( log_directory[0]
|
|
&& !path_is_directory(log_directory)
|
|
&& !path_mkdir(log_directory))
|
|
{
|
|
/* Re-enable console logging and output error message */
|
|
retro_main_log_file_init(NULL, false);
|
|
RARCH_ERR("Failed to create system event log directory: %s\n", log_directory);
|
|
return;
|
|
}
|
|
|
|
/* When RetroArch is launched, log file is overwritten.
|
|
* On subsequent calls within the same session, it is appended to. */
|
|
retro_main_log_file_init(log_file_path, log_file_created);
|
|
if (main_verbosity_st.initialized)
|
|
log_file_created = true;
|
|
return;
|
|
}
|
|
|
|
/* If we reach this point, then something went wrong...
|
|
* Just fall back to console logging */
|
|
retro_main_log_file_init(NULL, false);
|
|
RARCH_ERR("Failed to initialise system event file logging...\n");
|
|
}
|
|
|
|
void rarch_log_file_deinit(void)
|
|
{
|
|
/* De-initialise existing logger, if currently logging to file */
|
|
if (main_verbosity_st.initialized)
|
|
retro_main_log_file_deinit();
|
|
|
|
/* If logging is currently disabled... */
|
|
if (!main_verbosity_st.fp) /* ...initialise logging to console */
|
|
retro_main_log_file_init(NULL, false);
|
|
}
|