Skip to content
Merged
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
5 changes: 5 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,11 @@ directly by users.
Do not include internal leaf headers directly from user code unless the header
explicitly documents itself as standalone.

Do not use parent-directory paths such as `../` in `#include` directives.
Aggregate-owned headers should receive their shared dependencies from the
owning umbrella header. Standalone public leaf headers should use forward
project include paths instead of walking upward through directories.

When adding a new internal header, prefer one of two clear modes:

1. **Standalone header**: include all direct dependencies explicitly;
Expand Down
10 changes: 10 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,16 @@ if(CONSOLIX_BUILD_TESTS)
consolix_add_test(test_app_component_manager_shutdown_order "tests/test_app_component_manager_shutdown_order.cpp")
endif()

if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/tests/test_console_application_runner.cpp")
consolix_add_test(test_console_application_runner "tests/test_console_application_runner.cpp")
set_tests_properties(test_console_application_runner PROPERTIES TIMEOUT 15)
endif()

if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/tests/test_loop_throttle_component.cpp")
consolix_add_test(test_loop_throttle_component "tests/test_loop_throttle_component.cpp")
set_tests_properties(test_loop_throttle_component PROPERTIES TIMEOUT 15)
endif()

if(UNIX AND NOT WIN32 AND EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/tests/test_posix_signal_shutdown.cpp")
consolix_add_test(test_posix_signal_shutdown "tests/test_posix_signal_shutdown.cpp")
target_compile_definitions(test_posix_signal_shutdown PRIVATE CONSOLIX_WAIT_ON_ERROR=0)
Expand Down
88 changes: 88 additions & 0 deletions README-RU.md
Original file line number Diff line number Diff line change
Expand Up @@ -156,9 +156,97 @@ int main(int argc, char* argv[]) {
}
```

Если `main()` должен вернуть результат lifecycle, а не завершаться через
`std::exit` из `ConsoleApplication::run()`, используйте runner с exit code:

```cpp
int main() {
consolix::add<MyComponent>();
return consolix::run_for_exit_code();
}
```

Компонент может запросить конкретный код через `consolix::stop(code)`;
`consolix::request_stop(code)` остается доступен как более явная форма.
Если приложение уже владеет своим `AppComponentManager`, можно создать
`consolix::ConsoleApplicationRunner runner(manager);` и вернуть
`runner.run_for_exit_code()`.

Экземпляр `ConsoleApplicationRunner` одноразовый. Для нового lifecycle создайте
новый manager и новый runner. Пока `run_for_exit_code()` активен, глобальные
`consolix::stop()` и `consolix::request_stop()` направляются в активный runner;
иначе они работают с singleton facade `ConsoleApplication`.

`ConsoleApplication::run()` остается совместимым process-owning facade:
внутри он использует тот же runner и завершает процесс с полученным кодом.
На Windows Ctrl+C/Ctrl+Break запрашивают cooperative shutdown, а close/logoff/
shutdown events запрашивают shutdown на runner thread и ждут ограниченное окно
для cleanup.

### Порядок shutdown и финальные hooks

`AppComponentManager` завершает компоненты в обратном порядке регистрации. После
всех component shutdown callbacks runner очищает `ServiceLocator`, а затем
закрывает LogIt. Это значит, что services и logging еще доступны, пока
выполняются component `shutdown()` callbacks.

Если приложению нужен финальный cleanup hook после остановки обычных
компонентов, но до `ServiceLocator::clear_all()`, зарегистрируйте этот hook
раньше компонентов, которые им пользуются:

```cpp
consolix::add<consolix::LoopComponent>(
[]() { return true; },
[]() {},
[](int exit_code) {
// Финальный cleanup, пока services и LogIt еще живы.
});

consolix::add<RealComponent>();
```

Так как shutdown идет в LIFO-порядке, `RealComponent` завершится первым, а hook
выполнится после него. Такой component-level паттерн лучше runner-level
shutdown hooks: cleanup остается в той же lifecycle-модели, что и остальное
приложение.

### Main loop and CPU usage

Consolix выполняет компоненты в polling loop и по умолчанию не делает sleep
между итерациями. Если все компоненты сразу возвращаются из `process()`, процесс
может тратить слишком много CPU. Для latency-sensitive приложений это
намеренно: framework не прячет implicit delay внутри runner.

Для обычных сервисов и утилит добавьте throttle component ближе к концу списка
компонентов:

```cpp
auto throttle = consolix::add<consolix::LoopThrottleComponent>(
std::chrono::milliseconds(1));

// Другой поток может завершить ожидание раньше, когда добавил новую работу.
throttle->wake();
```

`LoopThrottleComponent` ждет не дольше настроенного delay на каждом проходе
loop. `wake()` прерывает текущее ожидание, а wake request, сделанный до начала
ожидания, будет обработан на следующем проходе. Когда приложение запущено через
`ConsoleApplicationRunner`, `consolix::stop()` и `consolix::request_stop()`
тоже будят throttle wait через общий `LoopWakeService`, поэтому shutdown не
ждет длинный throttle delay. Если вы запускаете `AppComponentManager` напрямую
без runner-а, держите delay коротким или вызывайте `wake()` из того же control
path, который запрашивает stop.

На POSIX signal handlers остаются async-signal-safe: они только сохраняют флаг
ожидающего сигнала. Они не будят C++ condition variables напрямую, поэтому
долгий блокирующий `process()` может отложить обработку SIGINT/SIGTERM до
возврата управления в runner loop.

## Documentation

- developer guidelines: `docs/header-implementation-guidelines.md`
- agent playbook: `guides/header-implementation-guidelines.md`
- lifecycle example: `examples/example_shutdown_and_resources.cpp`
- exit-code runner example: `examples/example_exit_code_runner.cpp`
- loop throttle example: `examples/example_loop_throttle_component.cpp`
- API docs: https://newyaroslav.github.io/Consolix/
86 changes: 86 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,90 @@ int main(int argc, char* argv[]) {
}
```

When `main()` must return the lifecycle result instead of letting
`ConsoleApplication::run()` call `std::exit`, use the exit-code runner:

```cpp
int main() {
consolix::add<MyComponent>();
return consolix::run_for_exit_code();
}
```

Components can request a specific code with `consolix::stop(code)`;
`consolix::request_stop(code)` is available as the explicit form.
For applications that already own an `AppComponentManager`, use
`consolix::ConsoleApplicationRunner runner(manager);` and return
`runner.run_for_exit_code()`.

A `ConsoleApplicationRunner` instance is single-use. Create a new manager and a
new runner for another lifecycle. While `run_for_exit_code()` is active, global
`consolix::stop()` and `consolix::request_stop()` target the active runner;
otherwise they target the singleton `ConsoleApplication` facade.

`ConsoleApplication::run()` remains the process-owning compatibility facade:
internally it uses the same runner and exits with the returned code. On Windows,
Ctrl+C/Ctrl+Break request cooperative shutdown; close/logoff/shutdown events
request shutdown on the runner thread and wait for a bounded cleanup window.

### Shutdown Ordering and Final Hooks

`AppComponentManager` shuts down components in reverse registration order. After
all component shutdown callbacks finish, the runner clears `ServiceLocator` and
then shuts down LogIt. This means services and logging are still available while
component `shutdown()` callbacks run.

If an application needs a final cleanup hook after ordinary components stop, but
before `ServiceLocator::clear_all()`, register that hook before the components
that use it:

```cpp
consolix::add<consolix::LoopComponent>(
[]() { return true; },
[]() {},
[](int exit_code) {
// Final cleanup while services and LogIt are still alive.
});

consolix::add<RealComponent>();
```

Because shutdown is LIFO, `RealComponent` shuts down first and the hook runs
after it. Prefer this component-level pattern over runner-level shutdown hooks
so cleanup stays in the same lifecycle model as the rest of the application.

### Main Loop and CPU Usage

Consolix runs components in a polling loop and does not sleep between
iterations by default. If all components return immediately, the process may
use excessive CPU. For latency-sensitive applications this is intentional:
the framework does not hide an implicit delay inside the runner.

For ordinary services and tools, add a throttle component near the end of the
component list:

```cpp
auto throttle = consolix::add<consolix::LoopThrottleComponent>(
std::chrono::milliseconds(1));

// Another thread can end the wait early when it posts new work.
throttle->wake();
```

`LoopThrottleComponent` waits at most for the configured delay on every loop
pass. `wake()` interrupts the current wait, and wake requests made before the
wait begins are consumed by the next pass. When the application is run through
`ConsoleApplicationRunner`, `consolix::stop()` and `consolix::request_stop()`
also wake throttle waits through the shared `LoopWakeService`, so shutdown does
not have to wait for a long throttle delay. If you drive `AppComponentManager`
directly without the runner, keep the delay short or call `wake()` from the
same control path that requests stop.

On POSIX platforms, signal handlers remain async-signal-safe: they only store a
pending signal flag. They do not wake C++ condition variables directly, so a
long blocking `process()` call can delay SIGINT/SIGTERM handling until control
returns to the runner loop.

## Diagnostic Streams

Consolix provides two multi-target log macros that route messages through
Expand Down Expand Up @@ -198,6 +282,8 @@ Additional repository guidance:
- developer guidelines: `docs/header-implementation-guidelines.md`
- agent playbook: `guides/header-implementation-guidelines.md`
- lifecycle example: `examples/example_shutdown_and_resources.cpp`
- exit-code runner example: `examples/example_exit_code_runner.cpp`
- loop throttle example: `examples/example_loop_throttle_component.cpp`
- diagnostic streams: `examples/example_stderr_diagnostics.cpp`

API documentation: https://newyaroslav.github.io/Consolix/
6 changes: 6 additions & 0 deletions docs/header-implementation-guidelines.md
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,12 @@ That approach is appropriate when:

Do not centralize dependencies merely to save a few include lines if the subsystem is designed around standalone leaf headers.

Avoid parent-directory include paths such as `../` in `#include` directives.
For aggregate-first subsystems, place cross-module prerequisites in the owning
aggregate header and include leaf headers only after that context is prepared.
For standalone public leaf headers, use forward project include paths instead of
walking upward through directories.

### Implementation-only helpers

Build-mode helpers, implementation macros, and similar infrastructure should stay at the implementation boundary.
Expand Down
79 changes: 78 additions & 1 deletion docs/mainpage.dox
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,8 @@ It also targets the recurring runtime concerns of console tools: startup and shu

- **Core**:
- `ServiceLocator` for global resource management
- `AppComponentManager` and `ConsoleApplication` for application lifecycle
- `AppComponentManager`, `ConsoleApplication`, and `ConsoleApplicationRunner` for application lifecycle
- `LoopWakeService` for waking polling-loop wait components
- service and application helpers for common setup flows

- **Components**:
Expand All @@ -46,6 +47,7 @@ It also targets the recurring runtime concerns of console tools: startup and shu
- `ConfigComponent`: JSON-based configuration with comment support
- `LogoComponent`: displays a custom ASCII logo in the console
- `LoopComponent`: customizable application loops
- `LoopThrottleComponent`: optional wakeable wait step for polling loops

- **Utilities**:
- `path_utils.hpp`: path and executable-location helpers
Expand Down Expand Up @@ -97,6 +99,81 @@ int main(int argc, char* argv[]) {
}
```

Use `consolix::run_for_exit_code()` when `main()` should return the lifecycle
result instead of using the legacy `ConsoleApplication::run()` path that exits
the process. Components can request a specific result with
`consolix::stop(exit_code)`; `consolix::request_stop(exit_code)` is available
as the explicit form. Applications that own their own `AppComponentManager`
can run it through `consolix::ConsoleApplicationRunner`.

`ConsoleApplicationRunner` instances are single-use. Create a new manager and a
new runner for another lifecycle. While `run_for_exit_code()` is active, global
`consolix::stop()` and `consolix::request_stop()` target the active runner;
otherwise they target the singleton `ConsoleApplication` facade.

`ConsoleApplication::run()` is a compatibility facade over the same runner: it
exits the process with the returned code. On Windows, Ctrl+C/Ctrl+Break request
cooperative shutdown, while close/logoff/shutdown console events request cleanup
on the runner thread and wait for a bounded timeout.

### Shutdown Ordering and Final Hooks

`AppComponentManager` shuts down components in reverse registration order. After
all component shutdown callbacks finish, the runner clears `ServiceLocator` and
then shuts down LogIt. Services and logging are therefore still available while
component `shutdown()` callbacks run.

If an application needs a final cleanup hook after ordinary components stop, but
before `ServiceLocator::clear_all()`, register that hook before the components
that use it:

```cpp
consolix::add<consolix::LoopComponent>(
[]() { return true; },
[]() {},
[](int exit_code) {
// Final cleanup while services and LogIt are still alive.
});

consolix::add<RealComponent>();
```

Because shutdown is LIFO, `RealComponent` shuts down first and the hook runs
after it. Prefer this component-level pattern over runner-level shutdown hooks
so cleanup stays in the same lifecycle model as the rest of the application.

### Main Loop and CPU Usage

Consolix runs components in a polling loop and does not sleep between iterations
by default. If every component returns immediately, the process may use
excessive CPU. This keeps the runner explicit and avoids adding hidden latency
for low-latency applications.

For ordinary services and tools, add `consolix::LoopThrottleComponent` near the
end of the component list:

```cpp
auto throttle = consolix::add<consolix::LoopThrottleComponent>(
std::chrono::milliseconds(1));

// Producer threads can end the wait early when new work arrives.
throttle->wake();
```

The component waits at most for the configured delay on each pass. `wake()`
interrupts an active wait and preserves wake requests made just before the wait
begins. When the application is run through `ConsoleApplicationRunner`,
`consolix::stop()` and `consolix::request_stop()` also wake throttle waits
through the shared `LoopWakeService`, so shutdown does not have to wait for a
long throttle delay. If you drive `AppComponentManager` directly without the
runner, keep the delay short or call `wake()` from the same control path that
requests stop.

On POSIX platforms, signal handlers remain async-signal-safe: they only store a
pending signal flag. They do not wake C++ condition variables directly, so a
long blocking `process()` call can delay SIGINT/SIGTERM handling until control
returns to the runner loop.

## Documentation Structure

### Main Sections
Expand Down
26 changes: 26 additions & 0 deletions examples/example_exit_code_runner.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/// \example example_exit_code_runner.cpp
/// \brief Demonstrates returning an exit code from the Consolix lifecycle runner.

#include <consolix/core.hpp>

class ExitCodeRunnerDemo final : public consolix::BaseLoopComponent {
public:
bool on_once() override {
CONSOLIX_STREAM() << "Exit-code runner demo initialized.";
return true;
}

void on_loop() override {
CONSOLIX_STREAM() << "Requesting stop with exit code 7.";
consolix::stop(7);
}

void on_shutdown(int exit_code) override {
CONSOLIX_STREAM() << "Shutdown completed with code: " << exit_code;
}
};

int main() {
consolix::add<ExitCodeRunnerDemo>();
return consolix::run_for_exit_code();
}
Loading
Loading