Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1,287 changes: 1,017 additions & 270 deletions code/dbgprint.cpp

Large diffs are not rendered by default.

37 changes: 28 additions & 9 deletions code/dbgprint.h
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,24 @@

#pragma once

#include <filesystem>

#if defined(_WIN32)

#include <sal.h>

// Initialization function with support for UTF-16 command-line arguments,
// for use with wmain().
void Debug_Init(int argc, wchar_t const * argv[]);

// WinMain() doesn't provide command-line arguments in the typical format,
// but they can be retrieved with a function call. This is a convenience wrapper
// for Debug_Init() used in WinMain().
void Debug_Init(void);

// Dedicated reporting function for GetLastError() on Windows.
char const * Last_Error_Text(unsigned long error);

#else

// The annotation and the calling convention are the compiler's own. Off Windows
Expand All @@ -26,14 +40,19 @@

#endif

void Debug_Init(void);
void Debug_Init_Console(void);
void Debug_Console_Hold(void);
char const * Debug_Log_File_Name(void);
char const * Debug_Directory(void);
bool Delete_Files_Older_Than(char const * directory, char const * pattern, unsigned days);
#if defined(__GNUC__)
// printf() formatting attribute supported by clang and gcc
#define DEBUG_PRINTF_ATTR __attribute__((format(printf, 1, 2)))
#else
#define DEBUG_PRINTF_ATTR
#endif

void __cdecl DebugString(_Printf_format_string_ char const * string, ...);
void __cdecl DebugStringNoPrefix(_Printf_format_string_ char const * string, ...);
void Debug_Init(int argc, char const * argv[]);
bool Debug_Enable_Console(void);
void Debug_Console_Hold(void);
std::filesystem::path Debug_Log_File_Path(void);
std::filesystem::path Debug_Directory(void);
bool Delete_Files_Older_Than(const std::filesystem::path & directory, const std::string_view & pattern, unsigned int days);

char const * Last_Error_Text(unsigned long error);
void __cdecl DebugString(_Printf_format_string_ char const * string, ...) DEBUG_PRINTF_ATTR;
void __cdecl DebugStringNoPrefix(_Printf_format_string_ char const * string, ...) DEBUG_PRINTF_ATTR;
1 change: 0 additions & 1 deletion code/globals.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,6 @@ bool Debug_Check_Map = false; // true = validate the map each frame
bool Debug_Playtest = false;
bool Debug_Trap_Check_Heap = false; // true = check the Heap
bool Debug_Print_Events = false; // true = print event & packet processing
bool Debug_Console = false;
bool Debug_MultipleInstances = false; // true = let this copy run beside another one


Expand Down
1 change: 0 additions & 1 deletion code/globals.h
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,6 @@ extern bool Debug_Check_Map;
extern bool Debug_Playtest;
extern bool Debug_Trap_Check_Heap;
extern bool Debug_Print_Events;
extern bool Debug_Console;
extern bool Debug_MultipleInstances;

extern ParticleSystemClass *GasSystem;
Expand Down
7 changes: 3 additions & 4 deletions code/init.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1461,7 +1461,7 @@ bool Parse_Command_Line(int argc, char * argv[])
*/
if (stricmp("/?", string) == 0 || stricmp("-?", string) == 0 || stricmp("-h", string) == 0 || stricmp("/h", string) == 0) {
#ifdef _DEBUG
Debug_Init_Console();
Debug_Enable_Console();
#endif

/*
Expand Down Expand Up @@ -1677,8 +1677,7 @@ bool Parse_Command_Line(int argc, char * argv[])
* Enable debug output to a console.
*/
case 'C':
Debug_Console = true;
Debug_Init_Console();
/* Argument already processed by dbgprint module. */
break;

/*
Expand All @@ -1690,7 +1689,7 @@ bool Parse_Command_Line(int argc, char * argv[])

default:
#ifdef _DEBUG
Debug_Init_Console();
Debug_Enable_Console();
#endif
puts(Fetch_String(TXT_INVALID));
return(false);
Expand Down
4 changes: 2 additions & 2 deletions code/startup.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -458,7 +458,7 @@ int CALLBACK WinMain ( HINSTANCE instance , HINSTANCE , char * , int )

// Handed over now because the exception path may not ask the logger for anything: the
// thread that crashed may be the one holding the logger's lock.
Exception_Register_Log_File(Debug_Log_File_Name());
Exception_Register_Log_File((const char *)Debug_Log_File_Path().u8string().c_str());

// LZO asks for this before any codec call. It validates rather than initializes, and a
// vendored static build cannot fail it, but thirdparty/lzo is upgraded in place and a
Expand Down Expand Up @@ -507,7 +507,7 @@ int CALLBACK WinMain ( HINSTANCE instance , HINSTANCE , char * , int )
#ifdef _DEBUG
// Debug builds show the console without -XC, except to a headless run, which shows nothing.
if (!Headless::Is_Active()) {
Debug_Init_Console();
Debug_Enable_Console();
}
#endif

Expand Down
8 changes: 4 additions & 4 deletions code/syncreport.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -194,13 +194,13 @@
unsigned int GameCRC = 0;

char filename[512];
char const * debug_dir = Debug_Directory();
if (debug_dir != NULL && debug_dir[0] != '\0') {
std::filesystem::path debug_dir = Debug_Directory();
if (!debug_dir.empty()) {
SYSTEMTIME now;
GetLocalTime(&now);
Delete_Files_Older_Than(debug_dir, "SYNC_*.LOG", SYNC_REPORT_MAX_AGE_DAYS);
Delete_Files_Older_Than(debug_dir, "SYNC_.*\\.LOG", SYNC_REPORT_MAX_AGE_DAYS);
snprintf(filename, sizeof(filename), "%s\\SYNC_H%d_%02u-%02u-%04u_%02u-%02u-%02u_F%d.LOG",
debug_dir, PlayerPtr->HeapID,
(const char *)debug_dir.u8string().c_str(), PlayerPtr->HeapID,
now.wDay, now.wMonth, now.wYear, now.wHour, now.wMinute, now.wSecond, Frame);
} else {
snprintf(filename, sizeof(filename), "SYNC%01d.TXT", PlayerPtr->HeapID);
Expand Down Expand Up @@ -341,7 +341,7 @@

fprintf(fp,"COORD:%6d,%6d,%5d Facing:%3d Mission:%2d Type:%-14s Tgt:%-16s Speed:%4d NavCom:%s\n",
coord.X,coord.Y,coord.Z,(int)infp->PrimaryFacing.Current().As_Dir256(),infp->Get_Mission(),
type_text, tarcom_text, int(infp->Speed * (MPH_LIGHT_SPEED + 1.0)), navcom_text);

Check warning on line 344 in code/syncreport.cpp

View workflow job for this annotation

GitHub Actions / Build / Win32 Release

operator '+': deprecated between enumerations and floating-point types [D:\a\OpenTS\OpenTS\build\code\OpenTS.vcxproj]

Check warning on line 344 in code/syncreport.cpp

View workflow job for this annotation

GitHub Actions / Build / x64 Debug

operator '+': deprecated between enumerations and floating-point types [D:\a\OpenTS\OpenTS\build\code\OpenTS.vcxproj]

Check warning on line 344 in code/syncreport.cpp

View workflow job for this annotation

GitHub Actions / Build / Win32 Debug

operator '+': deprecated between enumerations and floating-point types [D:\a\OpenTS\OpenTS\build\code\OpenTS.vcxproj]

Check warning on line 344 in code/syncreport.cpp

View workflow job for this annotation

GitHub Actions / Build / x64 Release

operator '+': deprecated between enumerations and floating-point types [D:\a\OpenTS\OpenTS\build\code\OpenTS.vcxproj]
}
}
Mono_Printf("%s Infantry:%x\n",housep->Class->Name(),GameCRC);
Expand Down
17 changes: 17 additions & 0 deletions manual/changes/debug-log-user-dir.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
---
title: Store logs into the user directory when specified
category: feature
release: 0.3.0
targets:
- type: command
id: launch:console-debug
effect: changed
credit: [DaliborFox]
---

Previously, logs were always stored in the `Debug` folder beside the game executable.
Since it's quite likely for the game not to have write access to this folder, it now
stores the logs in a `Debug` folder in the user directory when the
[`-USERDIR`](/using/command-line/#user-directory) command-line option is provided.

See [Debug logs and console](/using/debug-logging/) for specific details.
4 changes: 2 additions & 2 deletions manual/content/using/debug-logging.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,15 +15,15 @@ related:

## Where the log is written

Every run, in Release and Debug builds alike, writes a new log to a `Debug` folder in the directory that holds the executable. The file is named for the local time the process started:
Every run, in Release and Debug builds alike, writes a new log to a `Debug` folder in the user directory. The file is named for the local time the process started:

```
Debug/DEBUG_17-08-2026_06-00-35.LOG
```

Because each run gets its own file, the log from a run that crashed is still there after the next launch. When two processes start within the same second, the second adds its process id to the name.

The log stays beside the executable when [`-USERDIR`](/using/command-line/#user-directory) names a user directory, so the game needs write access to the executable's directory to keep a log. If the folder or the file cannot be created, the game still runs, and its output still reaches the console and an attached debugger.
When the user directory is not specified, i.e. no [`-USERDIR`](/using/command-line/#user-directory) command-line option is provided, the log is placed into a platform-dependent default directory. For Windows, this is the directory that holds the executable. On Linux, it is `$XDG_STATE_HOME/OpenTS/` (or `$HOME/.local/state/OpenTS/` in case `$XDG_STATE_HOME` is not specified), and on Mac, it is `$HOME/Library/Logs/OpenTS/`. In case the game doesn't have write access to this directory (especially on Windows), the [`-USERDIR`](/using/command-line/#user-directory) command-line option should be used.

At startup, the game deletes logs last written more than 14 days ago. A single log stops growing at 64 MB, and its last line then says that the size limit was reached.

Expand Down
9 changes: 9 additions & 0 deletions tests/deploymentconfig/deploymentconfigcontract.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
#include <cstring>
#include <string>

#include "dbgprint.h"
#include "_deploymentconfig.h"
#include "deploymentconfig.h"
#include "rawfile.h"
Expand Down Expand Up @@ -123,6 +124,7 @@ TEST_CASE_FIXTURE(RootFixture, "with no file the INI, MIX and Maps folders are s

TEST_CASE_FIXTURE(RootFixture, "a file beside the game is read, and the folders it names replace the default")
{
Debug_Init(0, (const char **)NULL);
DeploymentConfigClass config;

Write_File(Root + "\\OPENTS.INI", "[Paths]\nSearchPaths=Data,More\n");
Expand All @@ -146,6 +148,7 @@ TEST_CASE_FIXTURE(RootFixture, "a file beside the game is read, and the folders

TEST_CASE_FIXTURE(RootFixture, "the file is looked for beside the game, then in INI, then in MIX")
{
Debug_Init(0, (const char **)NULL);
DeploymentConfigClass config;

Write_File(Root + "\\MIX\\OPENTS.INI", "[Paths]\nSearchPaths=FromMix\n");
Expand All @@ -168,6 +171,7 @@ TEST_CASE_FIXTURE(RootFixture, "the file is looked for beside the game, then in

TEST_CASE_FIXTURE(RootFixture, "the file is read from the directory named, and what it names is taken from there")
{
Debug_Init(0, (const char **)NULL);
DeploymentConfigClass config;
std::string const data = Root + "\\Data\\";

Expand All @@ -184,6 +188,7 @@ TEST_CASE_FIXTURE(RootFixture, "the file is read from the directory named, and w

TEST_CASE_FIXTURE(RootFixture, "a read starts over, so with the file gone every setting returns to its default")
{
Debug_Init(0, (const char **)NULL);
DeploymentConfigClass config;

Write_File(Root + "\\OPENTS.INI", "[Paths]\nSearchPaths=Data\n");
Expand All @@ -197,6 +202,7 @@ TEST_CASE_FIXTURE(RootFixture, "a read starts over, so with the file gone every

TEST_CASE_FIXTURE(RootFixture, "a save carries the scenario file only when the file asks for it")
{
Debug_Init(0, (const char **)NULL);
DeploymentConfigClass config;

CHECK_MESSAGE(!config.CarryScenarioFile, "with no file a save carries no scenario file");
Expand All @@ -223,6 +229,7 @@ TEST_CASE_FIXTURE(RootFixture, "a save carries the scenario file only when the f

TEST_CASE_FIXTURE(RootFixture, "with no file the rules, multiplayer rules and settings come from their shipped files")
{
Debug_Init(0, (const char **)NULL);
DeploymentConfigClass config;

CHECK_MESSAGE(config.RulesFile == "RULES.INI", "with no file the rules come from RULES.INI");
Expand Down Expand Up @@ -261,6 +268,7 @@ TEST_CASE_FIXTURE(RootFixture, "with no file the rules, multiplayer rules and se

TEST_CASE_FIXTURE(RootFixture, "the expansion rules have their shipped names, and a name the file writes is taken")
{
Debug_Init(0, (const char **)NULL);
DeploymentConfigClass config;

CHECK_MESSAGE(config.RulesExpansionFile == "FIRESTRM.INI", "with no file the expansion rules are FIRESTRM.INI");
Expand Down Expand Up @@ -289,6 +297,7 @@ TEST_CASE_FIXTURE(RootFixture, "the expansion rules have their shipped names, an

TEST_CASE_FIXTURE(RootFixture, "the scheme and starting palettes have their shipped names, and names the file writes are taken")
{
Debug_Init(0, (const char **)NULL);
DeploymentConfigClass config;

CHECK_MESSAGE(config.SchemePaletteFile == "UNITSNO.PAL", "with no file the scheme palette is UNITSNO.PAL");
Expand Down
2 changes: 2 additions & 0 deletions tests/gamedirs/gamedirscontract.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
#include <vector>

#include "cdfile.h"
#include "dbgprint.h"
#include "deploymentconfig.h"
#include "gamedirs.h"
#include "rawfile.h"
Expand Down Expand Up @@ -108,6 +109,7 @@ void Remove_Directory(std::string const & path)
*/
void Reset(void)
{
Debug_Init(0, (const char **)NULL);
CDFileClass::Clear_Search_Drives();
Set_Data_Directory("");
Set_User_Directory("");
Expand Down
8 changes: 6 additions & 2 deletions tests/ini/inicontract.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,11 @@ std::string Save_Bytes(INIClass const & ini)
// diagnostics a load produced by taking the count before and after it.
int Log_Lines(char const * needle)
{
FILE * handle = std::fopen(Debug_Log_File_Name(), "rb");
#ifdef _WIN32
FILE * handle = _wfopen(Debug_Log_File_Path().c_str(), L"rb");
#else
FILE * handle = std::fopen(Debug_Log_File_Path().c_str(), "rb");
#endif
if (handle == NULL) {
return(0);
}
Expand Down Expand Up @@ -240,7 +244,7 @@ struct Fixture
{
Fixture(void)
{
Debug_Init();
Debug_Init(0, (const char **)NULL);
}
};

Expand Down
31 changes: 18 additions & 13 deletions tests/logstress/logstress.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -83,9 +83,9 @@ TEST_CASE("the logger keeps up with many threads and writes every record whole,

// The timings below describe the sinks a released game actually runs with, so the console
// stays shut until they are done.
Debug_Init();
Debug_Init(0, (const char **)NULL);

std::string const log = Debug_Log_File_Name();
std::filesystem::path const log = Debug_Log_File_Path();
REQUIRE_MESSAGE(!log.empty(), "the log file opened");

// A record assembled from several calls must come out as one stamped line.
Expand Down Expand Up @@ -175,7 +175,11 @@ TEST_CASE("the logger keeps up with many threads and writes every record whole,
CHECK_MESSAGE(p999_us <= 50000.0, "contended p99.9 call latency within 50 ms");

// Every line must have survived intact, and every message must be present.
#ifdef _WIN32
FILE * handle = _wfopen(log.c_str(), L"rb");
#else
FILE * handle = std::fopen(log.c_str(), "rb");
#endif
REQUIRE_MESSAGE(handle != NULL, "the log is readable while the game holds it open");

int intact = 0;
Expand Down Expand Up @@ -244,23 +248,23 @@ TEST_CASE("the logger keeps up with many threads and writes every record whole,


// Pruning must leave a file that is younger than the threshold alone.
TEST_CASE("pruning keeps a recent log and refuses an absurd age or a null directory")
TEST_CASE("pruning keeps a recent log and refuses a null directory")
{
std::string const directory = Log_Directory();
CreateDirectoryA(directory.c_str(), NULL);
std::filesystem::path const directory = Log_Directory();
CreateDirectoryW(directory.c_str(), NULL);

std::string const keep = directory + "\\DEBUG_keepme.LOG";
FILE * young = std::fopen(keep.c_str(), "wb");
std::filesystem::path const keep = directory / u8"DEBUG_keepme.LOG";
FILE * young = _wfopen(keep.c_str(), L"wb");
if (young != NULL) {
std::fputs("young\n", young);
std::fclose(young);
}
Delete_Files_Older_Than(directory.c_str(), "DEBUG_*.LOG", 14);
CHECK(GetFileAttributesA(keep.c_str()) != INVALID_FILE_ATTRIBUTES);
DeleteFileA(keep.c_str());
Delete_Files_Older_Than(directory, "DEBUG_.*\\.LOG", 14);
CHECK(GetFileAttributesW(keep.c_str()) != INVALID_FILE_ATTRIBUTES);
DeleteFileW(keep.c_str());

CHECK_MESSAGE(!Delete_Files_Older_Than(directory.c_str(), "DEBUG_*.LOG", 91), "an absurd age is refused");
CHECK_MESSAGE(!Delete_Files_Older_Than(NULL, "DEBUG_*.LOG", 14), "a null directory is refused");
CHECK_MESSAGE(Delete_Files_Older_Than(directory, "DEBUG_.*\\.LOG", 9001), "an absurd age should not be refused anymore");
CHECK_MESSAGE(!Delete_Files_Older_Than("", "DEBUG_.*\\.LOG", 14), "a null directory is refused");
}


Expand All @@ -269,7 +273,8 @@ TEST_CASE("the console sink allocates a console with its close button disabled")
{
// A console subsystem program already owns a console, which would turn AllocConsole away.
FreeConsole();
Debug_Init_Console();
Debug_Init(0, (const char **)NULL);
Debug_Enable_Console();
DebugString("the console sink is reachable\n");

HWND const console = GetConsoleWindow();
Expand Down
3 changes: 3 additions & 0 deletions tests/soundini/soundini.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@

#include "audio/audiodefs.hh"
#include "audio/audioevent.h"
#include "dbgprint.h"
#include "ini.h"
#include "voc.h"
#include "xstraw.h"
Expand Down Expand Up @@ -316,6 +317,7 @@ TEST_CASE_FIXTURE(ViniferaFixture, "SHROUDED is the hidden type, with an integer

TEST_CASE("a priority is a name or clamped points, and anything else keeps the fallback")
{
Debug_Init(0, (const char **)NULL);
CHECK(Sound_Parse_Priority("LOWEST", 7) == 0);
CHECK(Sound_Parse_Priority("low", 7) == 10);
CHECK(Sound_Parse_Priority("Normal", 7) == 50);
Expand Down Expand Up @@ -343,6 +345,7 @@ TEST_CASE("a volume is a fraction or a percentage, clamped, and anything else ke

TEST_CASE("type flags combine, unknown ones are ignored, and an empty type keeps the fallback")
{
Debug_Init(0, (const char **)NULL);
CHECK_MESSAGE(Sound_Parse_Type("NORMAL", 99) == 0u, "NORMAL is no flags");
CHECK(Sound_Parse_Type("SCREEN SHROUD", 99) == (SOUND_TYPE_SCREEN | SOUND_TYPE_SHROUD));
CHECK(Sound_Parse_Type("SCREEN WIBBLE", 99) == SOUND_TYPE_SCREEN);
Expand Down
3 changes: 3 additions & 0 deletions tests/themeini/themeini.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
// Reads music track sections shaped like the shipped THEME.INI and with the
// keys OpenTS adds, and checks what each key becomes. Needs no game data.

#include "dbgprint.h"
#include "ini.h"
#include "side.h"
#include "theme.h"
Expand Down Expand Up @@ -161,6 +162,7 @@ TEST_CASE_FIXTURE(NewKeysFixture, "a Volume= above 1 is a percentage, and one th

TEST_CASE_FIXTURE(SidesFixture, "Side= names one side or a list, skipping a side the rules do not declare")
{
Debug_Init(0, (const char **)NULL);
ThemeControl one = Track(ini, "ONE");
CHECK(one.Allows_Side(SIDE_GDI));
CHECK_FALSE(one.Allows_Side(SIDE_NOD));
Expand All @@ -179,6 +181,7 @@ TEST_CASE_FIXTURE(SidesFixture, "Side= names one side or a list, skipping a side

TEST_CASE_FIXTURE(SidesFixture, "a list naming no declared side keeps the sides before it")
{
Debug_Init(0, (const char **)NULL);
ThemeControl bad;
std::strcpy(bad.Name, "BAD");
bad.Owners.push_back(SIDE_NOD);
Expand Down
Loading
Loading