diff --git a/.ci/scripts/init_py.sh b/.ci/scripts/init_py.sh index 07eabab..7894fbc 100755 --- a/.ci/scripts/init_py.sh +++ b/.ci/scripts/init_py.sh @@ -7,7 +7,7 @@ init_py() { python3 -m venv "$VENV" source $VENV/bin/activate pip3 install cmake==${CMAKE_VERSION} - pip3 install --no-cache-dir -r ${PROJECT}/python/requirements.txt + pip3 install --no-cache-dir asn1tools } if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then diff --git a/.clang-format b/.clang-format index 84653cd..7d0b343 100644 --- a/.clang-format +++ b/.clang-format @@ -4,30 +4,88 @@ # SPDX-License-Identifier: Apache-2.0 # +Language: Cpp BasedOnStyle: Google - +AccessModifierOffset: -1 AlignAfterOpenBracket: Align +AlignConsecutiveAssignments: false +AlignConsecutiveDeclarations: false AlignOperands: AlignAfterOperator +AlignTrailingComments: true AllowShortFunctionsOnASingleLine: Empty BinPackArguments: false BinPackParameters: false +BraceWrapping: + AfterCaseLabel: false + AfterClass: false + AfterControlStatement: false + AfterEnum: false + AfterFunction: false + AfterNamespace: false + AfterStruct: false + AfterUnion: false + AfterExternBlock: false + BeforeCatch: false + BeforeElse: false + BeforeLambdaBody: false + BeforeWhile: false + SplitEmptyFunction: true + SplitEmptyRecord: true + SplitEmptyNamespace: true BreakBeforeBinaryOperators: NonAssignment +BreakBeforeBraces: Custom +BreakConstructorInitializers: BeforeColon +BreakConstructorInitializersBeforeComma: false +BreakStringLiterals: true +ColumnLimit: 80 +ConstructorInitializerAllOnOneLineOrOnePerLine: true +ContinuationIndentWidth: 4 DerivePointerAlignment: false FixNamespaceComments: true -IncludeBlocks: Preserve +IncludeBlocks: Regroup +IncludeCategories: + - Regex: '^["<]MAIN_INCLUDE_FILE[">]$' + Priority: 1 + - Regex: '^(()|())$' + Priority: 2 + - Regex: '^<[^/]*>$' + Priority: 3 + - Regex: '^<.*>$' + Priority: 4 + - Regex: '^".*"$' + Priority: 5 + - Regex: '.*' + Priority: 6 +IndentWidth: 2 InsertBraces: true +InsertNewlineAtEOF: true InsertTrailingCommas: Wrapped +MacroBlockBegin: '' +MacroBlockEnd: '' +MaxEmptyLinesToKeep: 2 NamespaceIndentation: All PointerAlignment: Right QualifierAlignment: Custom -QualifierOrder: ['inline', 'static', 'constexpr', 'const', 'volatile', 'type', 'restrict'] +QualifierOrder: + - inline + - static + - constexpr + - const + - volatile + - type + - restrict ReferenceAlignment: Right ReflowComments: true -ShortNamespaceLines: 0 +ShortNamespaceLines: 2 SortIncludes: CaseSensitive - -# use "// clang-format off" + "// clang-format on" -BreakStringLiterals: true - -# comment if not supported -InsertNewlineAtEOF: true +SpaceAfterCStyleCast: false +SpaceAfterTemplateKeyword: true +SpaceBeforeRangeBasedForLoopColon: true +SpaceInEmptyParentheses: false +SpacesBeforeTrailingComments: 2 +SpacesInAngles: false +SpacesInConditionalStatement: false +SpacesInCStyleCastParentheses: false +SpacesInParentheses: false +TabWidth: 2 +... diff --git a/.githooks/README.md b/.githooks/README.md new file mode 100755 index 0000000..fdee8ef --- /dev/null +++ b/.githooks/README.md @@ -0,0 +1,27 @@ +[//]: # ( +Copyright Quadrivium LLC +All Rights Reserved +SPDX-License-Identifier: Apache-2.0 +) + +# Git Hooks + +This folder _might_ contain some git-hooks to execute various checkup in Kagome. + +To activate presented (_and all future_) hooks just execute **once** shell-script [activate_hooks.sh](./activate_hooks.sh) from Kagome project root path. + +## pre-commit + +This hook check existing `clang-format` and `cmake-format` and their versions. +If they have recommended version, specific checkup is enabled. + +Each changed C++ file (`.hpp` and `.cpp` extensions) gets processed by `clang-format`. + +Each changed CMake file (`CMakeLists.txt` and `.cmake` extension) gets processed by `cmake-format` + +Commit will be blocked while there are any differences between original files and `clang-format/cmake-format` output files. + +## etc. + +_Other hooks might be provided by maintainers in the future._ + diff --git a/.githooks/activate_hooks.sh b/.githooks/activate_hooks.sh new file mode 100755 index 0000000..8a815e6 --- /dev/null +++ b/.githooks/activate_hooks.sh @@ -0,0 +1,22 @@ +#!/bin/sh +# +# Copyright Quadrivium LLC +# All Rights Reserved +# SPDX-License-Identifier: Apache-2.0 +# + +GIT=$(which git) + +if [ -z "${GIT}" ]; then + echo "Command ``git'' command not found" + exit 1 +fi + +cd "$(dirname "$0")/.." || (echo "Run script from file" | exit 1) + +chmod +x .githooks/* + +${GIT} config --local core.hooksPath .githooks || (echo "Hooks activation has failed" | exit 1) + +echo "Hooks activated successfully" +exit 0 diff --git a/.githooks/pre-commit_ b/.githooks/pre-commit_ new file mode 100755 index 0000000..5e70227 --- /dev/null +++ b/.githooks/pre-commit_ @@ -0,0 +1,90 @@ +#!/bin/sh +# +# Copyright Quadrivium LLC +# All Rights Reserved +# SPDX-License-Identifier: Apache-2.0 +# + +if git rev-parse --verify HEAD >/dev/null 2>&1; then + BASE=HEAD +else + # Initial commit: diff BASE an empty tree object + BASE=$(git hash-object -t tree /dev/null) +fi + +# check clang-format binary +CLANG_FORMAT_ENABLED=1 +CLANG_FORMAT=$(which clang-format-16 2>/dev/null) +if [ -z "${CLANG_FORMAT}" ]; then + CLANG_FORMAT=$(which clang-format) + if [ -z "${CLANG_FORMAT}" ]; then + echo "Command clang-format is not found" >&2 + echo "Please, install clang-format version 16 to enable checkup C++-files formatting over git pre-commit hook" >&2 + CLANG_FORMAT_ENABLED=0 + fi +fi + +# check clang-format version +if [ $CLANG_FORMAT_ENABLED ]; then + CLANG_FORMAT_VERSION=$($CLANG_FORMAT --version | sed -r "s/.*version ([[:digit:]]+).*/\1/") + + if [ "$CLANG_FORMAT_VERSION" != "16" ]; then + echo "clang-format version 16 is recommended" >&2 + fi +fi + +FILES=$(git diff --staged --diff-filter=ACMR --name-only) + +# check c++ files' format with clang-format +CXX_RES=0 +if [ $CLANG_FORMAT_ENABLED ]; then +# for FILE in $(git diff-index --name-only "${BASE}" --diff-filter=ACMR | grep -e "\\.[ch]pp$"); do + for FILE in $(echo "$FILES" | grep -e "\\.[ch]pp$"); do + O_HASH=$(shasum <"${FILE}") + F_HASH=$(${CLANG_FORMAT} --style=file "$FILE" | shasum) + if [ "${O_HASH}" != "${F_HASH}" ]; then + echo "File looks nonformatted: $FILE" + CXX_RES=1 + fi + done + + if [ $CXX_RES = 1 ]; then + CLANG_FORMAT_VERSION_FULL=$($CLANG_FORMAT --version | sed -r "s/.*version ([[:digit:]\.]+).*/\1/") + echo "Used clang-format version $CLANG_FORMAT_VERSION_FULL" >&2 + fi +fi + +## check cmake-format binary +#CMAKE_FORMAT_ENABLED=1 +#CMAKE_FORMAT=$(which cmake-format) +#if [ -z "${CMAKE_FORMAT}" ]; then +# echo "Command cmake-format is not found" >&2 +# echo "Please, install cmake-format version 15 to enable checkup cmake-files formatting over git pre-commit hook" >&2 +# CMAKE_FORMAT_ENABLED=0 +#fi +# +## check cmake-files' format with cmake-format +#CMAKE_RES=0 +#if [ $CMAKE_FORMAT_ENABLED ]; then +# for FILE in $(echo "$FILES" | grep -e "\(\(CMakeLists\\.txt\)\|\(\\.cmake\)\)$"); do +# O_HASH=$(shasum <"${FILE}") +# F_HASH=$(${CMAKE_FORMAT} "$FILE" | shasum) +# if [ "${O_HASH}" != "${F_HASH}" ]; then +# echo "File looks nonformatted: $FILE" +# CMAKE_RES=1 +# fi +# done +# +# if [ $CMAKE_RES = 1 ]; then +# CMAKE_FORMAT_VERSION_FULL=$($CMAKE_FORMAT --version) +# echo "Used cmake-format version $CMAKE_FORMAT_VERSION_FULL" >&2 +# fi +#fi + +# result of checks +if [ "$CXX_RES" = "1" ] || [ "$CMAKE_RES" = "1" ]; then + echo "Formatter required" >&2 + exit 1 +fi + +exit 0 diff --git a/CMakeLists.txt b/CMakeLists.txt index 3f50388..a84f22b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -7,42 +7,53 @@ cmake_minimum_required(VERSION 3.25) option(TESTING "Build and run test suite" ON) -if(TESTING) - list(APPEND VCPKG_MANIFEST_FEATURES test) -endif() +if (TESTING) + list(APPEND VCPKG_MANIFEST_FEATURES test) +endif () -project(cpp-jam - VERSION 0.0.1 - LANGUAGES C CXX -) set(CMAKE_CXX_STANDARD 20) set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_CXX_EXTENSIONS OFF) +set(CMAKE_POSITION_INDEPENDENT_CODE ON) + +project(cpp-jam + VERSION 0.0.1 + LANGUAGES CXX +) + +message(STATUS "CMAKE_BUILD_TYPE: ${CMAKE_BUILD_TYPE}") +message(STATUS "Boost_DIR: ${Boost_DIR}") find_package(Python3 REQUIRED) find_package(PkgConfig REQUIRED) pkg_check_modules(libb2 REQUIRED IMPORTED_TARGET GLOBAL libb2) -find_package(Boost CONFIG REQUIRED) +find_package(Boost CONFIG REQUIRED COMPONENTS algorithm outcome program_options) find_package(fmt CONFIG REQUIRED) +find_package(yaml-cpp CONFIG REQUIRED) find_package(jam_crust CONFIG REQUIRED) find_package(scale CONFIG REQUIRED) +find_package(soralog CONFIG REQUIRED) find_package(schnorrkel_crust CONFIG REQUIRED) +find_package(Boost.DI CONFIG REQUIRED) +find_package(qtils CONFIG REQUIRED) +find_package(prometheus-cpp CONFIG REQUIRED) add_library(headers INTERFACE) target_include_directories(headers INTERFACE - $ + $ $ ) add_subdirectory(src) -if(TESTING) - enable_testing() +if (TESTING) + enable_testing() - find_package(GTest CONFIG REQUIRED) - set(GTEST_DEPS GTest::gtest GTest::gtest_main GTest::gmock GTest::gmock_main) + find_package(GTest CONFIG REQUIRED) + set(GTEST_DEPS GTest::gtest_main) - add_subdirectory(test-vectors) + add_subdirectory(test-vectors) + add_subdirectory(tests) endif () diff --git a/clang-format.py b/clang-format.py new file mode 100644 index 0000000..ff1108e --- /dev/null +++ b/clang-format.py @@ -0,0 +1,187 @@ +import os +import sys +import subprocess +import re + +# Paths relative to the project root +PROJECT_DIR = os.path.abspath(os.path.dirname(__file__)) +SRC_DIR = os.path.join(PROJECT_DIR, "core") +TEST_DIR = os.path.join(PROJECT_DIR, "test") +MOCK_DIR = os.path.join(TEST_DIR, "mock") +SRC_REFLECTION = "core" # Set to "" if no reflection component, or to SRC_DIR if it matches the source directory name +CLANG_FORMAT = "/usr/local/bin/clang-format" # Path to the clang-format executable + + +def log(message): + print(f"[DEBUG] {message}") + + +def find_clang_format(): + log(f"Searching for .clang-format in project directory: {PROJECT_DIR}") + clang_format_path = os.path.join(PROJECT_DIR, '.clang-format') + if os.path.isfile(clang_format_path): + log(f"Found .clang-format at: {clang_format_path}") + return clang_format_path + log("No .clang-format file found.") + return None + + +def find_main_file(file_path): + file_name = os.path.basename(file_path) + file_dir = os.path.dirname(file_path) + name, ext = os.path.splitext(file_name) + + log(f"Finding main file for: {file_path}") + + def search_candidates(file_dir, base_name, suffix): + candidates = [] + + candidates.append(os.path.join(file_dir, base_name + suffix + ".hpp")) + candidates.append(os.path.join(file_dir, base_name + ".hpp")) + + if os.path.basename(file_dir) == "impl": + parent_dir = os.path.dirname(file_dir) + candidates.append(os.path.join(parent_dir, base_name + ".hpp")) + + log(f"search: dir={file_dir} base={base_name} suffix={suffix}") + return candidates + + def find_first_existing(candidates): + # Return the first existing file from the candidate list + log(f"Evaluating candidates: {candidates}") + for candidate in candidates: + if candidate and os.path.isfile(candidate): + log(f"Selected candidate: {candidate}") + return candidate + log("No candidates found.") + return None + + seen = set() + candidates = [] + if ext == ".cpp" or ext == ".hpp": + if name.endswith("_mock") and MOCK_DIR in file_dir: + base_name = name[:-5] # Remove `_mock` + candidates.extend(search_candidates(file_dir, base_name, "_mock")) + src_dir = file_dir.replace(MOCK_DIR + "/" + SRC_REFLECTION, SRC_DIR) + log(f"MOCK_DIR={MOCK_DIR} SRC_REFLECTION={SRC_REFLECTION} +={MOCK_DIR + "/" + SRC_REFLECTION} SRC_DIR={SRC_DIR} src={src_dir}") + candidates.extend(search_candidates(src_dir, base_name, "")) + candidates = [x for x in candidates if not (x in seen or seen.add(x)) and x != file_path] + return find_first_existing(candidates) + + if name.endswith("_test") and TEST_DIR in file_dir: + base_name = name[:-5] # Remove `_test` + candidates.extend(search_candidates(file_dir, base_name, "_test")) + src_dir = file_dir.replace(TEST_DIR + "/" + SRC_REFLECTION, SRC_DIR) + candidates.extend(search_candidates(src_dir, "impl/" + base_name, "_impl")) + src_dir = file_dir.replace(TEST_DIR + "/" + SRC_REFLECTION, SRC_DIR) + candidates.extend(search_candidates(src_dir, "impl/" + base_name, "")) + src_dir = file_dir.replace(TEST_DIR + "/" + SRC_REFLECTION, SRC_DIR) + candidates.extend(search_candidates(src_dir, base_name, "_impl")) + src_dir = file_dir.replace(TEST_DIR + "/" + SRC_REFLECTION, SRC_DIR) + candidates.extend(search_candidates(src_dir, base_name, "")) + candidates = [x for x in candidates if not (x in seen or seen.add(x)) and x != file_path] + return find_first_existing(candidates) + + if name.endswith("_impl") and SRC_DIR in file_dir: + base_name = name[:-5] # Remove `_impl` + candidates.extend(search_candidates(file_dir, base_name, "_impl")) + candidates = [x for x in candidates if not (tuple(x) in seen or seen.add(tuple(x))) and x != file_path] + return find_first_existing(candidates) + + base_name = name + candidates.extend(search_candidates(file_dir, base_name, "")) + candidates = [x for x in candidates if not (x in seen or seen.add(x)) and x != file_path] + return find_first_existing(candidates) + + log("No main file found.") + return None + + +def make_relative_to_base(base_path, target_path): + return os.path.relpath(target_path, base_path) + + +def modify_clang_format(base_clang_format, main_file, output_clang_format): + if TEST_DIR in main_file: + relative_main_file = make_relative_to_base(TEST_DIR, main_file) + elif MOCK_DIR in main_file: + relative_main_file = make_relative_to_base(MOCK_DIR, main_file) + else: + relative_main_file = make_relative_to_base(SRC_DIR, main_file) + + log(f"Using relative path for IncludeIsMainRegex: {relative_main_file}") + + with open(base_clang_format, 'r') as file: + config_lines = file.readlines() + + # include_regex = f"IncludeIsMainRegex: '^{re.escape(relative_main_file)}$'\n" + # updated = False + + for i, line in enumerate(config_lines): + line = line.replace("MAIN_INCLUDE_FILE", f"{relative_main_file}" + # f"{re.escape(relative_main_file)}" + , 1) + config_lines[i] = line + + # if line.startswith(" - Regex: 'MAIN_INCLUDE_FILE'"): + # config_lines[i] = f" - Regex: '^{re.escape(relative_main_file)}$'\n" + # # if line.startswith("IncludeIsMainRegex:"): + # # config_lines[i] = include_regex + # log(f"Line in config: [{config_lines[i]}]") + # # updated = True + # break + + # if not updated: + # config_lines.append(include_regex) + + with open(output_clang_format, 'w') as file: + file.writelines(config_lines) + log(f"Modified .clang-format written to: {output_clang_format}") + + +def format_file(file_path, clang_format_path): + log(f"Formatting file: {file_path} using .clang-format: {clang_format_path}") + subprocess.run([CLANG_FORMAT, "-i", f"--style=file:{clang_format_path}", file_path], + env={"CLANG_FORMAT_STYLE": clang_format_path}, check=True) + + +def main(): + if len(sys.argv) < 2: + print("Usage: python script.py ...") + sys.exit(1) + + arguments = sys.argv + + for i, arg in enumerate(arguments): + if i == 0: continue + file_path = os.path.abspath(arg) + log(f"Processing file: {file_path}") + + if not os.path.isfile(file_path): + print("Error: The specified file does not exist.") + sys.exit(1) + + base_clang_format = find_clang_format() + if not base_clang_format: + print("Error: No .clang-format file found in the project directory.") + sys.exit(1) + + main_file = find_main_file(file_path) + + if main_file: + log(f"Main file found: {main_file}") + temp_clang_format = base_clang_format + ".tmp" + modify_clang_format(base_clang_format, main_file, temp_clang_format) + try: + format_file(file_path, temp_clang_format) + finally: + if os.path.isfile(temp_clang_format): + # os.remove(temp_clang_format) + log(f"Temporary .clang-format file removed: {temp_clang_format}") + else: + log("No main file found. Using base .clang-format.") + format_file(file_path, base_clang_format) + + +if __name__ == "__main__": + main() diff --git a/docker/ci-docker b/docker/ci-docker index 32cb534..3ba1a4a 100755 --- a/docker/ci-docker +++ b/docker/ci-docker @@ -58,7 +58,7 @@ fi # DOCKER: FROM base AS ci # install project requirements -pip3 install -r $PROJECT/python/requirements.txt +pip3 install asn1tools # configure rm -rf $BUILD diff --git a/example/config.yaml b/example/config.yaml new file mode 100644 index 0000000..1992f97 --- /dev/null +++ b/example/config.yaml @@ -0,0 +1,29 @@ +general: + name: NameFromConfig + +metrics: + enabled: true + host: 127.0.0.1 + port: 9615 + +logging: + sinks: + - name: console + type: console + stream: stdout + thread: name + color: true + latency: 0 + groups: + - name: main + sink: console + level: info + is_fallback: true + children: + - name: jam + children: + - name: injector + - name: application + - name: rpc + - name: metrics + - name: threads \ No newline at end of file diff --git a/python/README.md b/python/README.md deleted file mode 100644 index 425663e..0000000 --- a/python/README.md +++ /dev/null @@ -1,9 +0,0 @@ -create venv\ -`uv venv`\ -`source .venv/bin/activate` - -install dependencies using [uv](https://github.com/astral-sh/uv)\ -`uv pip install -r requirements.txt` - -format python code using [ruff](https://github.com/astral-sh/ruff)\ -`ruff format asn1.py` diff --git a/python/requirements.txt b/python/requirements.txt deleted file mode 100644 index 1dc2202..0000000 --- a/python/requirements.txt +++ /dev/null @@ -1 +0,0 @@ -asn1tools diff --git a/python/asn1.py b/scripts/asn1.py similarity index 80% rename from python/asn1.py rename to scripts/asn1.py index 1459057..b92967c 100755 --- a/python/asn1.py +++ b/scripts/asn1.py @@ -1,11 +1,51 @@ #!/usr/bin/env python3 +""" +ASN.1 to C++ Code Generator + +This script generates C++ header files from ASN.1 definitions. +It processes ASN.1 type definitions, constants, and structures, then translates them into +C++ data structures with serialization and deserialization capabilities. + +Usage: + python asn1.py [ ...] + +Arguments: + The directory where the generated files will be saved. + One or more targets to generate. Available targets: + - "constants" : Generates configuration constants. + - "types" : Generates common ASN.1 types. + - "history" : Generates history module types. + - "safrole" : Generates safrole module types. + - "disputes" : Generates dispute module types. + - "authorizations" : Generates authorization module types. + +Requirements: + - Python 3 + - asn1tools (install with `pip install asn1tools`) + +Example: + python asn1.py ./output constants types + +This command will generate the required header files for constants and common types +and place them in the `./output` directory. + +Notes: + - The script enforces that must be inside the project directory. + - It parses ASN.1 files located in predefined subdirectories. + - The generated C++ files include type declarations, enums, and diff functions. + +""" + import asn1tools import os.path import sys -DIR = os.path.dirname(__file__) -TEST_VECTORS_DIR = os.path.join(DIR, "../test-vectors") +DIR = os.path.abspath(os.path.dirname(__file__)) +PROJECT_DIR = os.path.abspath(os.path.join(DIR, "..")) +TEST_VECTORS_DIR = os.path.abspath(os.path.join(PROJECT_DIR, "test-vectors")) +OUTPUT_DIR = os.curdir; + def flatten(aaa: list[list]): return [a for aa in aaa for a in aa] @@ -50,7 +90,6 @@ def __init__(self, name: str): self.name = name self.args: list[str] = [] self.decl: list[str] = [] - self.scale: list[str] = [] self.diff: list[str] = [] def c_tdecl(self): @@ -113,31 +152,6 @@ def asn_args(ARGS: list[str], types: dict, deps2: dict[str, set[str]]): return {k: [a for a in ARGS if a in aa] for k, aa in args.items()} -def c_scale(ty: Type, encode: list[str], decode: list[str]): - return [ - *ty.c_tdecl(), - "inline scale::ScaleEncoderStream &operator<<(scale::ScaleEncoderStream &s, const %s &v) {" - % ty.c_tname(), - *indent(encode), - " return s;", - "}", - *ty.c_tdecl(), - "inline scale::ScaleDecoderStream &operator>>(scale::ScaleDecoderStream &s, %s &v) {" - % ty.c_tname(), - *indent(decode), - " return s;", - "}", - ] - - -def c_scale_struct(ty: Type, members: list[str]): - return c_scale( - ty, - ["s << v.%s;" % x for x in members], - ["s >> v.%s;" % x for x in members], - ) - - def c_diff(NS: str, ty: Type, lines: list[str]): return [ *ty.c_tdecl(), @@ -194,13 +208,7 @@ def asn_member(t): else: t = dict(type="SEQUENCE OF", element=dict(type="U8")) if t["type"] == "NULL": - if "tag" in t: - if "number" in t["tag"]: - r = "Tagged" % t["tag"]["number"] - else: - r = "Tagged" % c_dash(t["tag"]["str"]) - else: - r = "Empty" + r = "qtils::Empty" elif t["type"] == "SEQUENCE OF": r = asn_sequence_of(t) elif t["type"] in types: @@ -209,6 +217,11 @@ def asn_member(t): raise TypeError(t) if t.get("optional", False): return "std::optional<%s>" % r + if "tag" in t: + if "number" in t["tag"]: + r = "qtils::Tagged<%s, struct _%d>" % (r, t["tag"]["number"]) + else: + r = "qtils::Tagged<%s, struct %s>" % (r, c_dash(t["tag"]["str"])) return r asn_types: dict = asn1tools.parse_files([path])[key]["types"] @@ -232,7 +245,7 @@ def asn_member(t): ) continue ty.decl = [ - "using %s = Tagged, struct %s_Tag>;" % tname @@ -248,7 +261,8 @@ def asn_member(t): ] continue if t["type"] == "ENUMERATED": - base_type = c_fittest_int_type(min((x for _, x in t["values"]), default=0), max((x for _, x in t["values"]), default=0)) + base_type = c_fittest_int_type(min((x for _, x in t["values"]), default=0), + max((x for _, x in t["values"]), default=0)) if base_type is None: raise TypeError(t) ty.decl = [ "enum class %s : %s {" % (tname, base_type), @@ -280,16 +294,12 @@ def asn_member(t): ty.decl = c_struct( tname, [(c_dash(x["name"]), asn_member(x)) for x in t["members"]] ) - ty.scale += c_scale_struct(ty, [c_dash(x["name"]) for x in t["members"]]) - ty.diff = c_diff( - cpp_namespace, ty, ["DIFF_M(%s);" % c_dash(x["name"]) for x in t["members"]] - ) ty.diff = c_diff( cpp_namespace, ty, ["DIFF_M(%s);" % c_dash(x["name"]) for x in t["members"]] ) continue if t["type"] == "NULL": - ty.decl = c_using(tname, "Empty"); + ty.decl = c_using(tname, "qtils::Empty"); continue ty.decl = c_using(tname, asn_member(t)) @@ -365,8 +375,8 @@ def __init__(self, cpp_namespace: str, path: str): "", "#pragma once", "", - "#include ", - "#include " % set_name, + "#include ", + "#include " % set_name, "", "namespace %s::config {" % cpp_namespace, "", @@ -379,7 +389,7 @@ def __init__(self, cpp_namespace: str, path: str): self.configs[set_name] = content def write(self): - prefix = os.path.join(TEST_VECTORS_DIR) + prefix = OUTPUT_DIR write(prefix + "/config.hpp", self.struct) for (set_name, content) in self.constants.items(): write(prefix + "/constants-%s.hpp" % set_name, content) @@ -403,48 +413,29 @@ def __init__(self, cpp_namespace: str, path: str): "#include ", "", "#include ", + "#include ", + "#include ", "", + "#include ", "#include ", - "#include ", - "#include ", - "#include ", "", *self.g_types, ] - self.g_scale = flatten([ty.scale for ty in self.types]) - self.g_scale = [ - "namespace %s {" % cpp_namespace, - *indent(self.g_scale), "}" - ] - self.g_scale = [ - "// Auto-generated file", - "", - "#pragma once", - "", - "#include ", - "", - "#include ", - '#include ', - "", - *self.g_scale, - *self.enum_trait, - ] self.g_diff = flatten([ty.diff for ty in self.types]) self.g_diff = [ "// Auto-generated file", "", "#pragma once", "", + '#include ', "#include ", - '#include ', "", *self.g_diff, ] def write(self): - prefix = os.path.join(TEST_VECTORS_DIR) + prefix = os.path.join(OUTPUT_DIR) write(prefix + "/common-types.hpp", self.g_types) - write(prefix + "/common-scale.hpp", self.g_scale) write(prefix + "/common-diff.hpp", self.g_diff) @@ -459,13 +450,14 @@ def __init__(self, cpp_namespace: str, name: str, path: str, module: str): for module_name, importing_types in asn_imports.items(): match module_name: case 'JamTypes': - includes += ["#include "] + includes += ["#include "] usings += ["using ::%s::%s;" % (cpp_namespace, t) for t in importing_types] self.types, self.enum_trait = parse_types("%s::%s" % (cpp_namespace, name), [], self.asn_file, module, flatten(asn_imports.values())) self.g_types = flatten([[*ty.c_tdecl(), *ty.decl] for ty in self.types]) - self.g_types = ["namespace %s::%s {" % (cpp_namespace, name), "", *indent(usings), "", *indent(self.g_types), "", "}"] + self.g_types = ["namespace %s::%s {" % (cpp_namespace, name), "", *indent(usings), "", *indent(self.g_types), + "", "}"] self.g_types = [ "// Auto-generated file", "", @@ -478,44 +470,28 @@ def __init__(self, cpp_namespace: str, name: str, path: str, module: str): "", "#include ", "", - "#include ", - "#include ", *includes, + "#include ", "", *self.g_types, ] - self.g_scale = flatten([ty.scale for ty in self.types]) - self.g_scale = ["namespace %s::%s {" % (cpp_namespace, name), "", *indent(self.g_scale), "", "}"] - self.g_scale = [ - "// Auto-generated file", - "", - "#pragma once", - "", - "#include ", - "", - '#include ', - '#include ' % (name, name), - "", - *self.g_scale, - *self.enum_trait, - ] self.g_diff = flatten([ty.diff for ty in self.types]) self.g_diff = [ "// Auto-generated file", "", "#pragma once", "", + '#include ', + "", + '#include ' % name, "#include ", - '#include ', - '#include ' % (name, name), "", *self.g_diff, ] def write(self, name: str): - prefix = os.path.join(TEST_VECTORS_DIR, name, name) + prefix = os.path.join(OUTPUT_DIR, name) write(prefix + "-types.hpp", self.g_types) - write(prefix + "-scale.hpp", self.g_scale) write(prefix + "-diff.hpp", self.g_diff) @@ -575,13 +551,29 @@ def authorizations(): g.write("authorizations") +generators = { + "constants": constants, + "types": types, + "history": history, + "safrole": safrole, + "disputes": disputes, + "authorizations": authorizations, +} + if __name__ == "__main__": - for arg in sys.argv[1:]: - dict( - constants=constants, - types=types, - history=history, - safrole=safrole, - disputes=disputes, - authorizations=authorizations, - )[arg]() + if len(sys.argv) < 3: + print(f"python {sys.argv[0]} [ ...]") + + OUTPUT_DIR = os.path.abspath(sys.argv[1]) + if not OUTPUT_DIR.startswith(PROJECT_DIR): + print(f" must be nested into {PROJECT_DIR}>") + sys.exit(1) + + if not os.path.exists(OUTPUT_DIR): + os.makedirs(OUTPUT_DIR) + + for name in sys.argv[2:]: + if name not in generators: + print(f"Generator '{name}' does not exist") + sys.exit(1) + generators[name]() diff --git a/scripts/get_version.sh b/scripts/get_version.sh new file mode 100755 index 0000000..b7d7e35 --- /dev/null +++ b/scripts/get_version.sh @@ -0,0 +1,96 @@ +#!/bin/sh -eu +# +# Copyright Quadrivium LLC +# All Rights Reserved +# SPDX-License-Identifier: Apache-2.0 +# +# +# Version String Generator for Git Repositories +# +# This script generates a version string based on the current Git state. +# It retrieves the latest tag, calculates the number of commits since the last tag, +# includes the current branch (if different from master), and appends the commit hash. +# If the working directory has uncommitted changes, it marks the version as "dirty". +# +# Usage: +# ./get_version.sh [--sanitized] +# +# Options: +# --sanitized Replaces non-alphanumeric characters in the version string +# with hyphens for compatibility with package managers. +# +# Output: +# Prints a version string in the format: +# ---- +# If the repository is dirty, "-dirty" is appended. +# +# Example: +# v1.2.3-5-feature-branch-2-a1b2c3d-dirty +# +# Requirements: +# - Git +# - sed (for --sanitized mode) +# +# Notes: +# - If the repository has no tags, an initial tag "_init" is created. +# - If the repository is not a Git repo, outputs "Unknown(no git)". +# - If Git is not installed, it exits with an error. +# + +sanitize_version() { + echo "$1" | sed -E 's/[^a-zA-Z0-9.+~:-]/-/g' +} + +realpath() { + case "$1" in + /*) echo "$1" ;; + *) echo "$(pwd)/$1" ;; + esac +} + +cd "$(dirname "$(realpath "$0")")" + +SANITIZED=false +[ "$#" -gt 0 ] && [ "$1" = "--sanitized" ] && SANITIZED=true + +if [ -x "$(command -v git)" ] && [ -d "$(git rev-parse --git-dir 2>/dev/null)" ]; then + HEAD=$(git rev-parse --short HEAD) + + # Determine the main branch (fallback to default names if necessary) + MAIN_BRANCH=$(git symbolic-ref refs/remotes/origin/HEAD 2>/dev/null | sed 's@^refs/remotes/origin/@@') + if [ -z "$MAIN_BRANCH" ]; then + MAIN_BRANCH=$(git branch --format='%(refname:short)' | grep -E '^(main|master)$' | head -n1) + fi + if [ -z "$MAIN_BRANCH" ]; then + MAIN_BRANCH="master" # Fallback to "master" if no main branch detected + fi + + COMMON=$(git merge-base HEAD "$MAIN_BRANCH" 2>/dev/null || echo "$HEAD") + + if ! git tag | grep -q .; then + ROOT_COMMIT=$(git rev-list --max-parents=0 HEAD || echo "") + [ -n "$ROOT_COMMIT" ] && ! git tag | grep -q "_init" && git tag _init "$ROOT_COMMIT" + fi + + DESCR=$(git describe --tags --long "$COMMON" 2>/dev/null || echo "") + [ -z "$DESCR" ] && DESCR="$HEAD-0-g$HEAD" + + TAG_IN_MASTER=$(echo "$DESCR" | sed -E "s/v?(.*)-([0-9]+)-g[a-f0-9]+/\1/") + TAG_TO_FORK_DISTANCE=$(echo "$DESCR" | sed -E "s/v?(.*)-([0-9]+)-g[a-f0-9]+/\2/") + + BRANCH=$(git branch --show-current 2>/dev/null || echo "$HEAD") + FORK_TO_HEAD_DISTANCE=$(git rev-list --count "$COMMON..HEAD" 2>/dev/null || echo "0") + + RESULT=$TAG_IN_MASTER + [ "$TAG_TO_FORK_DISTANCE" != "0" ] && RESULT="$RESULT-$TAG_TO_FORK_DISTANCE" + [ "$BRANCH" != "$MAIN_BRANCH" ] && RESULT="$RESULT-$BRANCH-$FORK_TO_HEAD_DISTANCE-$HEAD" + + git diff --quiet || DIRTY="-dirty" + RESULT="$RESULT${DIRTY:-}" +else + RESULT="Unknown(no git)" +fi + +[ "$SANITIZED" = true ] && RESULT=$(sanitize_version "$RESULT") + +printf "%s" "$RESULT" diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 185da66..08f0356 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -4,4 +4,23 @@ # SPDX-License-Identifier: Apache-2.0 # -add_subdirectory(jam) +include_directories(${CMAKE_CURRENT_SOURCE_DIR}) + +# Executables (should contain `main()` function) +add_subdirectory(executable) + +# Application's thinks +add_subdirectory(app) + +# Dependency injection +add_subdirectory(injector) + +# Logging subsystem +add_subdirectory(log) + +# Metrics subsystem +add_subdirectory(metrics) + +# Clocks and time subsystem +add_subdirectory(clock) + diff --git a/src/TODO_qtils/bytes_std_hash.hpp b/src/TODO_qtils/bytes_std_hash.hpp deleted file mode 100644 index 5737feb..0000000 --- a/src/TODO_qtils/bytes_std_hash.hpp +++ /dev/null @@ -1,18 +0,0 @@ -/** - * Copyright Quadrivium LLC - * All Rights Reserved - * SPDX-License-Identifier: Apache-2.0 - */ - -#pragma once - -#include -#include - -namespace qtils { - struct BytesStdHash { - std::size_t operator()(qtils::BytesIn bytes) const { - return std::hash{}(qtils::byte2str(bytes)); - } - }; -} // namespace qtils diff --git a/src/TODO_qtils/cxx23/ranges/contains.hpp b/src/TODO_qtils/cxx23/ranges/contains.hpp deleted file mode 100644 index 05ffba7..0000000 --- a/src/TODO_qtils/cxx23/ranges/contains.hpp +++ /dev/null @@ -1,20 +0,0 @@ -/** - * Copyright Quadrivium LLC - * All Rights Reserved - * SPDX-License-Identifier: Apache-2.0 - */ - -#pragma once -#include - -namespace qtils::cxx23::ranges { - auto contains(auto &&r, const auto &v) { - auto end = std::ranges::end(r); - return std::find(std::ranges::begin(r), end, v) != end; - } - - auto contains_if(auto &&r, const auto &f) { - auto end = std::ranges::end(r); - return std::find_if(std::ranges::begin(r), end, f) != end; - } -} // namespace qtils::cxx23::ranges diff --git a/src/app/CMakeLists.txt b/src/app/CMakeLists.txt new file mode 100644 index 0000000..61fd884 --- /dev/null +++ b/src/app/CMakeLists.txt @@ -0,0 +1,51 @@ +# +# Copyright Quadrivium LLC +# All Rights Reserved +# SPDX-License-Identifier: Apache-2.0 +# + +set(BUILD_VERSION_CPP "${CMAKE_BINARY_DIR}/generated/app/build_version.cpp") +set(GET_VERSION_SCRIPT "${CMAKE_SOURCE_DIR}/scripts/get_version.sh") +add_custom_command( + OUTPUT ${BUILD_VERSION_CPP} + COMMAND echo "// Auto-generated file\\n" > ${BUILD_VERSION_CPP} + COMMAND echo "#include \\n" >> ${BUILD_VERSION_CPP} + COMMAND echo "namespace jam {" >> ${BUILD_VERSION_CPP} + COMMAND echo " const std::string &buildVersion() {" >> ${BUILD_VERSION_CPP} + COMMAND printf " static const std::string buildVersion(\"" >> ${BUILD_VERSION_CPP} + COMMAND ${GET_VERSION_SCRIPT} >> ${BUILD_VERSION_CPP} + COMMAND echo "\");" >> ${BUILD_VERSION_CPP} + COMMAND echo " return buildVersion;" >> ${BUILD_VERSION_CPP} + COMMAND echo " }" >> ${BUILD_VERSION_CPP} + COMMAND echo "}" >> ${BUILD_VERSION_CPP} + COMMENT "Generate build_version.cpp" + DEPENDS ${GET_VERSION_SCRIPT} + VERBATIM +) +add_library(build_version + ${CMAKE_BINARY_DIR}/generated/app/build_version.cpp +) + +add_library(app_configuration SHARED configuration.cpp) +target_link_libraries(app_configuration + Boost::boost) + +add_library(app_configurator SHARED configurator.cpp) +target_link_libraries(app_configurator + app_configuration + yaml-cpp::yaml-cpp + Boost::program_options + build_version +) + +add_library(app_state_manager SHARED impl/state_manager_impl.cpp) +target_link_libraries(app_state_manager + logger +) + +add_library(application SHARED impl/application_impl.cpp) +target_link_libraries(application + qtils::qtils + app_configuration + metrics +) diff --git a/src/app/application.hpp b/src/app/application.hpp new file mode 100644 index 0000000..907bf61 --- /dev/null +++ b/src/app/application.hpp @@ -0,0 +1,22 @@ +/** + * Copyright Quadrivium LLC + * All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +#pragma once + +#include + +namespace jam::app { + + /// @class Application - JAM-application interface + class Application : private Singleton { + public: + virtual ~Application() = default; + + /// Runs node + virtual void run() = 0; + }; + +} // namespace jam::app diff --git a/src/app/build_version.hpp b/src/app/build_version.hpp new file mode 100644 index 0000000..15908cf --- /dev/null +++ b/src/app/build_version.hpp @@ -0,0 +1,19 @@ +/** + * Copyright Quadrivium LLC + * All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +#pragma once + +#include + +namespace jam { + /** + * @returns String indicating current build version. Might to contain: tag, + * number of commits from tag to fork, commit branch and number of commits + * from fork to current commit. + * @note Definition is generating by cmake + */ + const std::string &buildVersion(); +} // namespace jam diff --git a/src/app/configuration.cpp b/src/app/configuration.cpp new file mode 100644 index 0000000..d2f1aa1 --- /dev/null +++ b/src/app/configuration.cpp @@ -0,0 +1,26 @@ +/** + * Copyright Soramitsu Co., Ltd. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +#include "app/configuration.hpp" + +namespace jam::app { + + Configuration::Configuration() + : version_("undefined"), name_("unnamed"), metrics_endpoint_() {} + + std::string Configuration::nodeVersion() const { + return version_; + } + + std::string Configuration::nodeName() const { + return name_; + } + + std::optional Configuration::metricsEndpoint() + const { + return metrics_endpoint_; + } + +} // namespace jam::app diff --git a/src/app/configuration.hpp b/src/app/configuration.hpp new file mode 100644 index 0000000..380dff5 --- /dev/null +++ b/src/app/configuration.hpp @@ -0,0 +1,37 @@ +/** + * Copyright Soramitsu Co., Ltd. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +#pragma once + +#include + +#include +#include + +namespace jam::app { + class Configuration final : Singleton { + public: + using Endpoint = boost::asio::ip::tcp::endpoint; + + Configuration(); + + // /// Generate yaml-file with actual config + // virtual void generateConfigFile() const = 0; + + [[nodiscard]] std::string nodeVersion() const; + [[nodiscard]] std::string nodeName() const; + [[nodiscard]] std::optional metricsEndpoint() const; + + private: + friend class Configurator; // for external configure + + std::string version_; + std::string name_; + + Endpoint metrics_endpoint_; + std::optional metrics_enabled_; + }; + +} // namespace jam::app diff --git a/src/app/configurator.cpp b/src/app/configurator.cpp new file mode 100644 index 0000000..3963bdc --- /dev/null +++ b/src/app/configurator.cpp @@ -0,0 +1,368 @@ +/** + * Copyright Soramitsu Co., Ltd. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +#include "app/configuration.hpp" + +#include +#include + +#include +#include +#include +#include + +#include "app/build_version.hpp" +#include "app/configurator.hpp" + +#include + +using Endpoint = boost::asio::ip::tcp::endpoint; + +OUTCOME_CPP_DEFINE_CATEGORY(jam::app, Configurator::Error, e) { + using E = jam::app::Configurator::Error; + switch (e) { + case E::CliArgsParseFailed: + return "CLI Arguments parse failed"; + case E::ConfigFileParseFailed: + return "Config file parse failed"; + } + BOOST_UNREACHABLE_RETURN("Unknown log::Error"); +} + +namespace { + template + void find_argument(boost::program_options::variables_map &vm, + const char *name, + Func &&f) { + assert(nullptr != name); + if (auto it = vm.find(name); it != vm.end()) { + if (it->second.defaulted()) { + return; + } + std::forward(f)(it->second.as()); + } + } + + template + std::optional find_argument(boost::program_options::variables_map &vm, + const std::string &name) { + if (auto it = vm.find(name); it != vm.end()) { + if (!it->second.defaulted()) { + return it->second.as(); + } + } + return std::nullopt; + } + + bool find_argument(boost::program_options::variables_map &vm, + const std::string &name) { + if (auto it = vm.find(name); it != vm.end()) { + if (!it->second.defaulted()) { + return true; + } + } + return false; + } +} // namespace + +namespace jam::app { + + Configurator::Configurator(int argc, const char **argv, const char **env) + : argc_(argc), argv_(argv), env_(env) { + config_ = std::make_shared(); + + config_->version_ = buildVersion(); + config_->name_ = "noname"; + config_->metrics_endpoint_ = {boost::asio::ip::address_v4::any(), 9615}; + config_->metrics_enabled_ = std::nullopt; + + namespace po = boost::program_options; + + // clang-format off + + po::options_description general_options("General options", 120, 100); + general_options.add_options() + ("help,h", "show this help message") + ("version,v", "show version information") + ("name,n", po::value(), "set name of node") + ("config,c", po::value(), "optional, filepath to load configuration from. Overrides default config values") + ("log,l", po::value>(), + "Sets a custom logging filter.\n" + "Syntax is `=`, e.g. -llibp2p=off.\n" + "Log levels (most to least verbose) are trace, debug, verbose, info, warn, error, critical, off.\n" + "By default, all targets log `info`.\n" + "The global log level can be set with -l.") + ; + + po::options_description metrics_options("Metric options"); + metrics_options.add_options() + ("prometheus-disable", "set to disable OpenMetrics") + ("prometheus-host", po::value(), "address for OpenMetrics over HTTP") + ("prometheus-port", po::value(), "port for OpenMetrics over HTTP") + ; + + // clang-format on + + cli_options_ + .add(general_options) // + .add(metrics_options); + } + + outcome::result Configurator::step1() { + namespace po = boost::program_options; + namespace fs = std::filesystem; + + po::options_description options; + options.add_options()("help,h", "show help")("version,v", "show version")( + "config,c", po::value(), "config-file path"); + + po::variables_map vm; + + // first-run parse to read only general options and to lookup for "help", + // "config" and "version". all the rest options are ignored + try { + po::parsed_options parsed = po::command_line_parser(argc_, argv_) + .options(options) + .allow_unregistered() + .run(); + po::store(parsed, vm); + po::notify(vm); + } catch (const std::exception &e) { + std::cerr << "Error: " << e.what() << '\n' + << "Try run with option '--help' for more information\n"; + return Error::CliArgsParseFailed; + } + + if (vm.contains("help")) { + std::cout << "JAM-node version " << buildVersion() << '\n'; + std::cout << cli_options_ << '\n'; + return true; + } + + if (vm.contains("version")) { + std::cout << "JAM-node version " << buildVersion() << '\n'; + return true; + } + + if (vm.contains("config")) { + auto path = vm["config"].as(); + try { + config_file_ = YAML::LoadFile(path); + } catch (const std::exception &exception) { + std::cerr << "Error: Can't parse file " + << std::filesystem::weakly_canonical(path) << ": " + << exception.what() << "\n" + << "Option --config must be path to correct yaml-file\n" + << "Try run with option '--help' for more information\n"; + return Error::ConfigFileParseFailed; + } + } + + return false; + } + + outcome::result Configurator::step2() { + namespace po = boost::program_options; + namespace fs = std::filesystem; + + try { + // second-run parse to gather all known options + // with reporting about any unrecognized input + po::parsed_options parsed = + po::command_line_parser(argc_, argv_).options(cli_options_).run(); + po::store(parsed, cli_values_map_); + po::notify(cli_values_map_); + } catch (const std::exception &e) { + std::cerr << "Error: " << e.what() << '\n' + << "Try run with option '--help' for more information\n"; + return Error::CliArgsParseFailed; + } + + return false; + } + + outcome::result Configurator::getLoggingConfig() { + auto logging = (*config_file_)["logging"]; + if (logging.IsDefined()) { + return logging; + } + return YAML::Node{}; // TODO return default logging config + } + + outcome::result> Configurator::calculateConfig( + std::shared_ptr logger) { + OUTCOME_TRY(initGeneralConfig()); + OUTCOME_TRY(initOpenMetricsConfig()); + + return config_; + } + + outcome::result Configurator::initGeneralConfig() { + if (config_file_.has_value()) { + auto section = (*config_file_)["general"]; + if (section.IsDefined()) { + if (section.IsMap()) { + auto name = section["name"]; + if (name.IsDefined()) { + if (name.IsScalar()) { + auto value = name.as(); + config_->name_ = value; + } else { + file_errors_ << "E: Value 'general.name' must be scalar\n"; + file_has_error_ = true; + } + } + } else { + file_errors_ << "E: Section 'general' defined, but is not scalar\n"; + file_has_error_ = true; + } + } + } + + bool fail; + + fail = false; + find_argument( + cli_values_map_, "name", [&](const std::string &value) { + config_->name_ = value; + }); + if (fail) { + return Error::CliArgsParseFailed; + } + + return outcome::success(); + } + + outcome::result Configurator::initOpenMetricsConfig() { + if (config_file_.has_value()) { + auto section = (*config_file_)["metrics"]; + if (section.IsDefined()) { + if (section.IsMap()) { + auto enabled = section["enabled"]; + if (enabled.IsDefined()) { + if (enabled.IsScalar()) { + auto value = enabled.as(); + if (value == "true") { + config_->metrics_enabled_ = true; + } else if (value == "false") { + config_->metrics_enabled_ = false; + } else { + file_errors_ + << "E: Value 'network.metrics.enabled' has wrong value. " + "Expected 'true' or 'false'\n"; + file_has_error_ = true; + } + } else { + file_errors_ << "E: Value 'metrics.enabled' must be scalar\n"; + file_has_error_ = true; + } + } + + auto host = section["host"]; + if (host.IsDefined()) { + if (host.IsScalar()) { + auto value = host.as(); + boost::beast::error_code ec; + auto address = boost::asio::ip::address::from_string(value, ec); + if (!ec) { + config_->metrics_endpoint_ = { + address, config_->metrics_endpoint_.port()}; + if (not config_->metrics_enabled_.has_value()) { + config_->metrics_enabled_ = true; + } + } else { + file_errors_ << "E: Value 'network.metrics.host' defined, " + "but has invalid value\n"; + } + } else { + file_errors_ << "E: Value 'network.metrics.host' defined, " + "but is not scalar\n"; + file_has_error_ = true; + } + } + + auto port = section["port"]; + if (port.IsDefined()) { + if (port.IsScalar()) { + auto value = port.as(); + if (value > 0 and value <= 65535) { + config_->metrics_endpoint_ = { + config_->metrics_endpoint_.address(), + static_cast(value)}; + if (not config_->metrics_enabled_.has_value()) { + config_->metrics_enabled_ = true; + } + } else { + file_errors_ << "E: Value 'network.metrics.port' defined, " + "but has invalid value\n"; + file_has_error_ = true; + } + } else { + file_errors_ << "E: Value 'network.metrics.port' defined, " + "but is not scalar\n"; + file_has_error_ = true; + } + } + + } else { + file_errors_ << "E: Section 'metrics' defined, but is not map\n"; + file_has_error_ = true; + } + } + } + + bool fail; + + fail = false; + find_argument( + cli_values_map_, "prometheus-host", [&](const std::string &value) { + boost::beast::error_code ec; + auto address = boost::asio::ip::address::from_string(value, ec); + if (!ec) { + config_->metrics_endpoint_ = {address, + config_->metrics_endpoint_.port()}; + if (not config_->metrics_enabled_.has_value()) { + config_->metrics_enabled_ = true; + } + } else { + std::cerr << "Option --prometheus-host has invalid value\n" + << "Try run with option '--help' for more information\n"; + fail = true; + } + }); + if (fail) { + return Error::CliArgsParseFailed; + } + + fail = false; + find_argument( + cli_values_map_, "prometheus-port", [&](const uint16_t &value) { + if (value > 0 and value <= 65535) { + config_->metrics_endpoint_ = {config_->metrics_endpoint_.address(), + static_cast(value)}; + if (not config_->metrics_enabled_.has_value()) { + config_->metrics_enabled_ = true; + } + } else { + std::cerr << "Option --prometheus-port has invalid value\n" + << "Try run with option '--help' for more information\n"; + fail = true; + } + }); + if (fail) { + return Error::CliArgsParseFailed; + } + + if (find_argument(cli_values_map_, "prometheus-disabled")) { + config_->metrics_enabled_ = false; + }; + if (not config_->metrics_enabled_.has_value()) { + config_->metrics_enabled_ = false; + } + + return outcome::success(); + } + +} // namespace jam::app diff --git a/src/app/configurator.hpp b/src/app/configurator.hpp new file mode 100644 index 0000000..8fd1a8e --- /dev/null +++ b/src/app/configurator.hpp @@ -0,0 +1,75 @@ +/** + * Copyright Soramitsu Co., Ltd. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +#pragma once + +#include +#include +#include +#include + +#include "injector/dont_inject.hpp" + +namespace soralog { + class Logger; +} // namespace soralog + +namespace jam::app { + class Configuration; +} // namespace jam::app + +namespace jam::app { + + class Configurator final { + public: + enum class Error : uint8_t { + CliArgsParseFailed, + ConfigFileParseFailed, + }; + + DONT_INJECT(Configurator); + + Configurator() = delete; + Configurator(Configurator &&) noexcept = delete; + Configurator(const Configurator &) = delete; + ~Configurator() = default; + Configurator &operator=(Configurator &&) noexcept = delete; + Configurator &operator=(const Configurator &) = delete; + + Configurator(int argc, const char **argv, const char **env); + + // Parse CLI args for help, version and config + outcome::result step1(); + + // Parse remaining CLI args + outcome::result step2(); + + outcome::result getLoggingConfig(); + + outcome::result> calculateConfig( + std::shared_ptr logger); + + private: + outcome::result initGeneralConfig(); + outcome::result initOpenMetricsConfig(); + + int argc_; + const char **argv_; + const char **env_; + + std::shared_ptr config_; + + std::optional config_file_; + bool file_has_warn_ = false; + bool file_has_error_ = false; + std::ostringstream file_errors_; + + boost::program_options::options_description cli_options_; + boost::program_options::variables_map cli_values_map_; + }; + +} // namespace jam::app + +OUTCOME_HPP_DECLARE_ERROR(jam::app, Configurator::Error); diff --git a/src/app/impl/application_impl.cpp b/src/app/impl/application_impl.cpp new file mode 100644 index 0000000..2f29cb1 --- /dev/null +++ b/src/app/impl/application_impl.cpp @@ -0,0 +1,73 @@ +/** + * Copyright Quadrivium LLC + * All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +#include "app/impl/application_impl.hpp" + +#include +#include + +#include "app/configuration.hpp" +#include "app/impl/watchdog.hpp" +#include "app/state_manager.hpp" +#include "clock/clock.hpp" +#include "log/logger.hpp" +#include "metrics/histogram_timer.hpp" +#include "metrics/metrics.hpp" + +namespace jam::app { + + ApplicationImpl::ApplicationImpl( + std::shared_ptr logsys, + std::shared_ptr config, + std::shared_ptr state_manager, + std::shared_ptr watchdog, + std::shared_ptr metrics_exposer, + std::shared_ptr system_clock) + : logger_(logsys->getLogger("Application", "application")), + app_config_(std::move(config)), + state_manager_(std::move(state_manager)), + watchdog_(std::move(watchdog)), + metrics_exposer_(std::move(metrics_exposer)), + system_clock_(std::move(system_clock)), + metrics_registry_(metrics::createRegistry()) { + // Metric for exposing name and version of node + constexpr auto buildInfoMetricName = "jam_build_info"; + metrics_registry_->registerGaugeFamily( + buildInfoMetricName, + "A metric with a constant '1' value labeled by name, version"); + auto metric_build_info = metrics_registry_->registerGaugeMetric( + buildInfoMetricName, + {{"name", app_config_->nodeName()}, + {"version", app_config_->nodeVersion()}}); + metric_build_info->set(1); + } + + void ApplicationImpl::run() { + logger_->info("Start as node version '{}' named as '{}' with PID {}", + app_config_->nodeVersion(), + app_config_->nodeName(), + getpid()); + + std::thread watchdog_thread([this] { + soralog::util::setThreadName("watchdog"); + watchdog_->checkLoop(kWatchdogDefaultTimeout); + }); + + state_manager_->atShutdown([this] { watchdog_->stop(); }); + + // Metric storing start time + metrics::GaugeHelper("jam_process_start_time_seconds", + "UNIX timestamp of the moment the process started") + ->set(system_clock_->nowSec()); + + state_manager_->run(); + + watchdog_->stop(); + + watchdog_thread.join(); + } + +} // namespace jam::app diff --git a/src/app/impl/application_impl.hpp b/src/app/impl/application_impl.hpp new file mode 100644 index 0000000..43a4d38 --- /dev/null +++ b/src/app/impl/application_impl.hpp @@ -0,0 +1,67 @@ +/** + * Copyright Quadrivium LLC + * All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +#pragma once + +#include "app/application.hpp" + +#include + +#include + +namespace jam { + class Watchdog; +} // namespace jam + +namespace jam::app { + class Configuration; + class StateManager; +} // namespace jam::app + +namespace jam::clock { + class SystemClock; +} // namespace jam::clock + +namespace soralog { + class Logger; +} // namespace soralog + +namespace jam::log { + class LoggingSystem; +} // namespace jam::log + +namespace jam::metrics { + class Registry; + class Gauge; + class Exposer; +} // namespace jam::metrics + +namespace jam::app { + + class ApplicationImpl final : public Application { + public: + ApplicationImpl(std::shared_ptr logsys, + std::shared_ptr config, + std::shared_ptr state_manager, + std::shared_ptr watchdog, + std::shared_ptr metrics_exposer, + std::shared_ptr system_clock); + + void run() override; + + private: + std::shared_ptr logger_; + std::shared_ptr app_config_; + std::shared_ptr state_manager_; + std::shared_ptr watchdog_; + std::shared_ptr metrics_exposer_; + std::shared_ptr system_clock_; + + // Metrics + std::unique_ptr metrics_registry_; + }; + +} // namespace jam::app diff --git a/src/app/impl/state_manager_impl.cpp b/src/app/impl/state_manager_impl.cpp new file mode 100644 index 0000000..f5e2b73 --- /dev/null +++ b/src/app/impl/state_manager_impl.cpp @@ -0,0 +1,284 @@ +/** + * Copyright Quadrivium LLC + * All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +#include "app/impl/state_manager_impl.hpp" + +#include +#include + +#include "log/logger.hpp" + +namespace jam::app { + std::weak_ptr StateManagerImpl::wp_to_myself; + + std::atomic_bool StateManagerImpl::shutting_down_signals_enabled{false}; + + void StateManagerImpl::shuttingDownSignalsEnable() { + // NOLINTNEXTLINE(cppcoreguidelines-pro-type-member-init,hicpp-member-init) + struct sigaction act; + memset(&act, 0, sizeof(act)); + act.sa_handler = shuttingDownSignalsHandler; + sigemptyset(&act.sa_mask); + sigaddset(&act.sa_mask, SIGINT); + sigaddset(&act.sa_mask, SIGTERM); + sigaddset(&act.sa_mask, SIGQUIT); + sigprocmask(SIG_BLOCK, &act.sa_mask, nullptr); + sigaction(SIGINT, &act, nullptr); + sigaction(SIGTERM, &act, nullptr); + sigaction(SIGQUIT, &act, nullptr); + shutting_down_signals_enabled.store(true); + sigprocmask(SIG_UNBLOCK, &act.sa_mask, nullptr); + } + + void StateManagerImpl::shuttingDownSignalsDisable() { + auto expected = true; + if (not shutting_down_signals_enabled.compare_exchange_strong(expected, + false)) { + return; + } + // NOLINTNEXTLINE(cppcoreguidelines-pro-type-member-init,hicpp-member-init) + struct sigaction act; + memset(&act, 0, sizeof(act)); + act.sa_handler = SIG_DFL; + sigaction(SIGINT, &act, nullptr); + sigaction(SIGTERM, &act, nullptr); + sigaction(SIGQUIT, &act, nullptr); + } + + void StateManagerImpl::shuttingDownSignalsHandler(int signal) { + shuttingDownSignalsDisable(); + if (auto self = wp_to_myself.lock()) { + SL_TRACE(self->logger_, "Shutdown signal {} received", signal); + self->shutdown(); + } + } + + std::atomic_bool StateManagerImpl::log_rotate_signals_enabled{false}; + + void StateManagerImpl::logRotateSignalsEnable() { + // NOLINTNEXTLINE(cppcoreguidelines-pro-type-member-init,hicpp-member-init) + struct sigaction act; + memset(&act, 0, sizeof(act)); + act.sa_handler = logRotateSignalsHandler; + sigemptyset(&act.sa_mask); + sigaddset(&act.sa_mask, SIGHUP); + sigprocmask(SIG_BLOCK, &act.sa_mask, nullptr); + sigaction(SIGHUP, &act, nullptr); + log_rotate_signals_enabled.store(true); + sigprocmask(SIG_UNBLOCK, &act.sa_mask, nullptr); + } + + void StateManagerImpl::logRotateSignalsDisable() { + auto expected = true; + if (not log_rotate_signals_enabled.compare_exchange_strong(expected, + false)) { + return; + } + // NOLINTNEXTLINE(cppcoreguidelines-pro-type-member-init,hicpp-member-init) + struct sigaction act; + memset(&act, 0, sizeof(act)); + act.sa_handler = SIG_DFL; + sigaction(SIGHUP, &act, nullptr); + } + + void StateManagerImpl::logRotateSignalsHandler(int signal) { + if (auto self = wp_to_myself.lock()) { + SL_TRACE(self->logger_, "Log rotate signal {} received", signal); + self->logging_system_->doLogRotate(); + } + } + + StateManagerImpl::StateManagerImpl( + std::shared_ptr logging_system) + : logger_(logging_system->getLogger("StateManager", "application")), + logging_system_(std::move(logging_system)) { + shuttingDownSignalsEnable(); + logRotateSignalsEnable(); + SL_TRACE(logger_, "Signal handlers set up"); + } + + StateManagerImpl::~StateManagerImpl() { + shuttingDownSignalsDisable(); + logRotateSignalsDisable(); + wp_to_myself.reset(); + } + + void StateManagerImpl::reset() { + std::lock_guard lg(mutex_); + + std::queue empty_prepare; + std::swap(prepare_, empty_prepare); + + std::queue empty_launch; + std::swap(launch_, empty_launch); + + std::queue empty_shutdown; + std::swap(shutdown_, empty_shutdown); + + state_ = State::Init; + } + + void StateManagerImpl::atPrepare(OnPrepare &&cb) { + std::lock_guard lg(mutex_); + if (state_ > State::Prepare) { + throw AppStateException("adding callback for stage 'prepare'"); + } + prepare_.emplace(std::move(cb)); + } + + void StateManagerImpl::atLaunch(OnLaunch &&cb) { + std::lock_guard lg(mutex_); + if (state_ > State::Starting) { + throw AppStateException("adding callback for stage 'launch'"); + } + launch_.emplace(std::move(cb)); + } + + void StateManagerImpl::atShutdown(OnShutdown &&cb) { + std::lock_guard lg(mutex_); + if (state_ > State::ShuttingDown) { + throw AppStateException("adding callback for stage 'shutdown'"); + } + shutdown_.emplace(std::move(cb)); + } + + void StateManagerImpl::doPrepare() { + std::lock_guard lg(mutex_); + + auto state = State::Init; + if (not state_.compare_exchange_strong(state, State::Prepare)) { + if (state != State::ShuttingDown) { + throw AppStateException("running stage 'preparing'"); + } + } + + if (not prepare_.empty()) { + SL_TRACE(logger_, "Running stage 'preparing'…"); + } + + while (!prepare_.empty()) { + auto &cb = prepare_.front(); + if (state_ == State::Prepare) { + auto success = cb(); + if (not success) { + SL_ERROR(logger_, "Stage 'preparing' is failed"); + state = State::Prepare; + state_.compare_exchange_strong(state, State::ShuttingDown); + } + } + prepare_.pop(); + } + + state = State::Prepare; + state_.compare_exchange_strong(state, State::ReadyToStart); + } + + void StateManagerImpl::doLaunch() { + std::lock_guard lg(mutex_); + + auto state = State::ReadyToStart; + if (not state_.compare_exchange_strong(state, State::Starting)) { + if (state != State::ShuttingDown) { + throw AppStateException("running stage 'launch'"); + } + } + + if (not launch_.empty()) { + SL_TRACE(logger_, "Running stage 'launch'…"); + } + + while (!launch_.empty()) { + auto &cb = launch_.front(); + if (state_.load() == State::Starting) { + auto success = cb(); + if (not success) { + SL_ERROR(logger_, "Stage 'launch' is failed"); + state = State::Starting; + state_.compare_exchange_strong(state, State::ShuttingDown); + } + } + launch_.pop(); + } + + state = State::Starting; + state_.compare_exchange_strong(state, State::Works); + } + + void StateManagerImpl::doShutdown() { + std::lock_guard lg(mutex_); + + auto state = State::Works; + if (not state_.compare_exchange_strong(state, State::ShuttingDown)) { + if (state != State::ShuttingDown) { + throw AppStateException("running stage 'shutting down'"); + } + } + + std::queue empty_prepare; + std::swap(prepare_, empty_prepare); + + std::queue empty_launch; + std::swap(launch_, empty_launch); + + while (!shutdown_.empty()) { + auto &cb = shutdown_.front(); + cb(); + shutdown_.pop(); + } + + state = State::ShuttingDown; + state_.compare_exchange_strong(state, State::ReadyToStop); + } + + void StateManagerImpl::run() { + wp_to_myself = weak_from_this(); + if (wp_to_myself.expired()) { + throw std::logic_error( + "StateManager must be instantiated on shared pointer before run"); + } + + doPrepare(); + + doLaunch(); + + if (state_.load() == State::Works) { + SL_TRACE(logger_, "All components started; waiting shutdown request…"); + shutdownRequestWaiting(); + } + + SL_TRACE(logger_, "Start doing shutdown…"); + doShutdown(); + SL_TRACE(logger_, "Shutdown is done"); + + if (state_.load() != State::ReadyToStop) { + throw std::logic_error( + "StateManager is expected in stage 'ready to stop'"); + } + } + + void StateManagerImpl::shutdownRequestWaiting() { + std::unique_lock lock(cv_mutex_); + cv_.wait(lock, [&] { return state_ == State::ShuttingDown; }); + } + + void StateManagerImpl::shutdown() { + shuttingDownSignalsDisable(); + if (state_.load() == State::ReadyToStop) { + SL_TRACE(logger_, "Shutting down requested, but app is ready to stop"); + return; + } + + if (state_.load() == State::ShuttingDown) { + SL_TRACE(logger_, "Shutting down requested, but it's in progress"); + return; + } + + SL_TRACE(logger_, "Shutting down requested…"); + std::lock_guard lg(cv_mutex_); + state_.store(State::ShuttingDown); + cv_.notify_one(); + } +} // namespace jam::app diff --git a/src/app/impl/state_manager_impl.hpp b/src/app/impl/state_manager_impl.hpp new file mode 100644 index 0000000..13f5f26 --- /dev/null +++ b/src/app/impl/state_manager_impl.hpp @@ -0,0 +1,84 @@ +/** + * Copyright Quadrivium LLC + * All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +#pragma once + +#include "app/state_manager.hpp" + +#include +#include +#include +#include + +#include "utils/ctor_limiters.hpp" + +namespace soralog { + class Logger; +} // namespace soralog +namespace jam::log { + class LoggingSystem; +} // namespace jam::log + +namespace jam::app { + + class StateManagerImpl final + : Singleton, + public StateManager, + public std::enable_shared_from_this { + public: + StateManagerImpl(std::shared_ptr logging_system); + + ~StateManagerImpl() override; + + void atPrepare(OnPrepare &&cb) override; + void atLaunch(OnLaunch &&cb) override; + void atShutdown(OnShutdown &&cb) override; + + void run() override; + void shutdown() override; + + State state() const override { + return state_; + } + + protected: + void reset(); + + void doPrepare() override; + void doLaunch() override; + void doShutdown() override; + + private: + static std::weak_ptr wp_to_myself; + + static std::atomic_bool shutting_down_signals_enabled; + static void shuttingDownSignalsEnable(); + static void shuttingDownSignalsDisable(); + static void shuttingDownSignalsHandler(int); + + static std::atomic_bool log_rotate_signals_enabled; + static void logRotateSignalsEnable(); + static void logRotateSignalsDisable(); + static void logRotateSignalsHandler(int); + + void shutdownRequestWaiting(); + + std::shared_ptr logger_; + std::shared_ptr logging_system_; + + std::atomic state_ = State::Init; + + std::recursive_mutex mutex_; + + std::mutex cv_mutex_; + std::condition_variable cv_; + + std::queue prepare_; + std::queue launch_; + std::queue shutdown_; + }; + +} // namespace jam::app diff --git a/src/app/impl/watchdog.hpp b/src/app/impl/watchdog.hpp new file mode 100644 index 0000000..b1f6e07 --- /dev/null +++ b/src/app/impl/watchdog.hpp @@ -0,0 +1,186 @@ +/** + * Copyright Quadrivium LLC + * All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +#pragma once + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#include "app/configuration.hpp" +#include "log/logger.hpp" + +#include "injector/dont_inject.hpp" + +namespace soralog { + class Logger; +} // namespace soralog + +namespace jam::log { + class LoggingSystem; +} // namespace jam::log + +#ifdef __APPLE__ + +#include +#include +#include + +namespace { + inline uint64_t getPlatformThreadId() { + thread_identifier_info_data_t info; + mach_msg_type_number_t size = THREAD_IDENTIFIER_INFO_COUNT; + auto r = thread_info(mach_thread_self(), + THREAD_IDENTIFIER_INFO, + (thread_info_t)&info, + &size); + if (r != KERN_SUCCESS) { + throw std::logic_error{"thread_info"}; + } + return info.thread_id; + } +} // namespace + +#else + +#include +#include + +inline uint64_t getPlatformThreadId() { + // NOLINTNEXTLINE(cppcoreguidelines-pro-type-vararg,hicpp-vararg) + return syscall(SYS_gettid); +} + +#endif + +namespace jam { + + constexpr auto kWatchdogDefaultTimeout = std::chrono::minutes{15}; + + class Watchdog { + public: + using Count = uint32_t; + using Atomic = std::atomic; + using Clock = std::chrono::steady_clock; + using Timeout = std::chrono::seconds; + + Watchdog(std::shared_ptr logsys, + std::shared_ptr config) + : logger_(logsys->getLogger("Watchdog", "threads")), + granularity_(1 /*config->granularity()*/) { + // BOOST_ASSERT(granularity != granularity.zero()); + } + + struct Ping { + std::shared_ptr count_; + + void operator()() const { + count_->fetch_add(1); + } + }; + + void checkLoop(Timeout timeout) { + // or `io_context` with timer + while (not stopped_) { + std::this_thread::sleep_for(granularity_); + check(timeout); + } + } + + // periodic check + void check(const Timeout timeout) { + std::unique_lock lock{mutex_}; + const auto now = Clock::now(); + for (auto it = threads_.begin(); it != threads_.end();) { + if (it->second.count.use_count() == 1) { + it = threads_.erase(it); + } else { + auto count = it->second.count->load(); + if (it->second.last_count != count) { + it->second.last_count = count; + it->second.last_time = now; + } else { + auto lag = now - it->second.last_time; + if (lag > timeout) { + std::stringstream s; + s << it->first; + SL_CRITICAL(logger_, + "ALERT Watchdog: " + "thread id={}, platform_id={}, name={} timeout\n", + s.str(), + it->second.platform_id, + it->second.name); + std::abort(); + } + } + ++it; + } + } + } + + [[nodiscard]] Ping add() { + std::unique_lock lock{mutex_}; + auto &thread = threads_[std::this_thread::get_id()]; + if (not thread.count) { + thread = {.last_time = Clock::now(), + .last_count = 0, + .count = std::make_shared(), + .platform_id = getPlatformThreadId(), + .name = soralog::util::getThreadName()}; + } + return Ping{thread.count}; + } + + void run(std::shared_ptr io) { + auto ping = add(); + while (not stopped_ and io.use_count() != 1) { +#define WAIT_FOR_BETTER_BOOST_IMPLEMENTATION +#ifndef WAIT_FOR_BETTER_BOOST_IMPLEMENTATION + // this is the desired implementation + // cannot be used rn due to the method visibility settings + // bug(boost): wait_one is private + boost::system::error_code ec; + io->impl_.wait_one(TODO, ec); + ping(); + io->poll_one(); +#else + // `run_one_for` run time is sum of `wait_one(time)` and `poll_one` + // may cause false-positive timeout + io->run_one_for(granularity_); +#endif + ping(); + io->restart(); + } + } + + void stop() { + stopped_ = true; + } + + private: + struct Thread { + Clock::time_point last_time; + Count last_count = 0; + std::shared_ptr count; + uint64_t platform_id; + std::string name; + }; + + std::shared_ptr logger_; + std::chrono::milliseconds granularity_; + std::mutex mutex_; + std::unordered_map threads_; + std::atomic_bool stopped_ = false; + }; +} // namespace jam diff --git a/src/app/state_manager.hpp b/src/app/state_manager.hpp new file mode 100644 index 0000000..cdfe277 --- /dev/null +++ b/src/app/state_manager.hpp @@ -0,0 +1,130 @@ +/** + * Copyright Quadrivium LLC + * All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +#pragma once + +#include +#include +#include +#include + +namespace jam::app { + + // Concepts that check if an object has a method that is called by app state + // manager. Deliberately avoid checking that the method returns bool, + // because if there's a method with an appropriate name, and it doesn't return + // bool, we want it to be a compile error instead of silently ignoring it + // because the concept is not satisfied. + template + concept AppStatePreparable = requires(T &t) { t.prepare(); }; + template + concept AppStateStartable = requires(T &t) { t.start(); }; + template + concept AppStateStoppable = requires(T &t) { t.stop(); }; + + // if an object is registered with AppStateManager but has no method + // that is called by AppStateManager, there's probably something wrong + template + concept AppStateControllable = + AppStatePreparable || AppStateStoppable || AppStateStartable; + + template + concept ActionRetBool = std::same_as, bool>; + + template + concept ActionRetVoid = std::is_void_v>; + + class Action { + public: + Action(ActionRetBool auto &&f) + : f_([f = std::move(f)]() mutable { return f(); }) {} + + Action(ActionRetVoid auto &&f) + : f_([f = std::move(f)]() mutable { return f(), true; }) {} + + bool operator()() { + return f_(); + } + + private: + std::function f_; + }; + + class StateManager { + public: + using OnPrepare = Action; + using OnLaunch = Action; + using OnShutdown = Action; + + enum class State : uint8_t { + Init, + Prepare, + ReadyToStart, + Starting, + Works, + ShuttingDown, + ReadyToStop, + }; + + virtual ~StateManager() = default; + + /** + * @brief Execute {@param cb} at stage 'preparations' of application + * @param cb + */ + virtual void atPrepare(OnPrepare &&cb) = 0; + + /** + * @brief Execute {@param cb} immediately before start application + * @param cb + */ + virtual void atLaunch(OnLaunch &&cb) = 0; + + /** + * @brief Execute {@param cb} at stage of shutting down application + * @param cb + */ + virtual void atShutdown(OnShutdown &&cb) = 0; + + public: + /** + * @brief Registration special methods (if any) of object as handlers + * for stages of application life-cycle + * @param entity is registered entity + */ + template + void takeControl(Controlled &entity) { + if constexpr (AppStatePreparable) { + atPrepare([&entity] { return entity.prepare(); }); + } + if constexpr (AppStateStartable) { + atLaunch([&entity] { return entity.start(); }); + } + if constexpr (AppStateStoppable) { + atShutdown([&entity] { return entity.stop(); }); + } + } + + /// Start application life cycle + virtual void run() = 0; + + /// Initiate shutting down (at any time) + virtual void shutdown() = 0; + + /// Get current stage + virtual State state() const = 0; + + protected: + virtual void doPrepare() = 0; + virtual void doLaunch() = 0; + virtual void doShutdown() = 0; + }; + + struct AppStateException : public std::runtime_error { + explicit AppStateException(std::string message) + : std::runtime_error("Wrong workflow at " + std::move(message)) {} + }; +} // namespace jam::app diff --git a/src/clock/CMakeLists.txt b/src/clock/CMakeLists.txt new file mode 100644 index 0000000..d77bd65 --- /dev/null +++ b/src/clock/CMakeLists.txt @@ -0,0 +1,9 @@ +# +# Copyright Quadrivium LLC +# All Rights Reserved +# SPDX-License-Identifier: Apache-2.0 +# + +add_library(clock + impl/clock_impl.cpp +) diff --git a/src/clock/clock.hpp b/src/clock/clock.hpp new file mode 100644 index 0000000..9ddd3ce --- /dev/null +++ b/src/clock/clock.hpp @@ -0,0 +1,66 @@ +/** + * Copyright Quadrivium LLC + * All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +#pragma once + +#include + +namespace jam::clock { + + /** + * An interface for a clock + * @tparam clock type is an underlying clock type, such as std::steady_clock + */ + template + class Clock { + public: + /** + * Difference between two time points + */ + using Duration = typename ClockType::duration; + + /** + * A moment in time, stored in milliseconds since Unix epoch start + */ + using TimePoint = typename ClockType::time_point; + + virtual ~Clock() = default; + + /** + * @return a time point representing the current time + */ + [[nodiscard]] virtual TimePoint now() const = 0; + + /** + * @return uint64_t representing number of seconds since the beginning of + * epoch (Jan 1, 1970) + */ + [[nodiscard]] virtual uint64_t nowSec() const = 0; + + /** + * @return uint64_t representing number of milliseconds since the beginning + * of epoch (Jan 1, 1970) + */ + [[nodiscard]] virtual uint64_t nowMsec() const = 0; + + static TimePoint zero() { + return TimePoint{}; + } + }; + + /** + * SystemClock alias over Clock. Should be used when we need to watch current + * time + */ + class SystemClock : public virtual Clock {}; + + /** + * SteadyClock alias over Clock. Should be used when we need to measure + * interval between two moments in time + */ + class SteadyClock : public virtual Clock {}; + +} // namespace jam::clock diff --git a/src/clock/impl/clock_impl.cpp b/src/clock/impl/clock_impl.cpp new file mode 100644 index 0000000..57a97e4 --- /dev/null +++ b/src/clock/impl/clock_impl.cpp @@ -0,0 +1,33 @@ +/** + * Copyright Quadrivium LLC + * All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +#include "clock/impl/clock_impl.hpp" + +namespace jam::clock { + + template + typename Clock::TimePoint ClockImpl::now() const { + return ClockType::now(); + } + + template + uint64_t ClockImpl::nowSec() const { + return std::chrono::duration_cast( + now().time_since_epoch()) + .count(); + } + + template + uint64_t ClockImpl::nowMsec() const { + return std::chrono::duration_cast( + now().time_since_epoch()) + .count(); + } + + template class ClockImpl; + template class ClockImpl; + +} // namespace jam::clock diff --git a/src/clock/impl/clock_impl.hpp b/src/clock/impl/clock_impl.hpp new file mode 100644 index 0000000..904ea65 --- /dev/null +++ b/src/clock/impl/clock_impl.hpp @@ -0,0 +1,26 @@ +/** + * Copyright Quadrivium LLC + * All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +#pragma once + +#include "clock/clock.hpp" + +namespace jam::clock { + + template + class ClockImpl : virtual public Clock { + public: + typename Clock::TimePoint now() const override; + uint64_t nowSec() const override; + uint64_t nowMsec() const override; + }; + + class SystemClockImpl : public SystemClock, + public ClockImpl {}; + class SteadyClockImpl : public SteadyClock, + public ClockImpl {}; + +} // namespace jam::clock diff --git a/src/jam/bandersnatch.hpp b/src/crypto/bandersnatch.hpp similarity index 98% rename from src/jam/bandersnatch.hpp rename to src/crypto/bandersnatch.hpp index 3ed483a..466e747 100644 --- a/src/jam/bandersnatch.hpp +++ b/src/crypto/bandersnatch.hpp @@ -12,7 +12,7 @@ #include #include -namespace jam::bandersnatch { +namespace jam::crypto::bandersnatch { using Output = qtils::BytesN; using Public = qtils::BytesN; using RingCommitment = qtils::BytesN; diff --git a/src/jam/blake.hpp b/src/crypto/blake.hpp similarity index 96% rename from src/jam/blake.hpp rename to src/crypto/blake.hpp index c0ad000..cd22869 100644 --- a/src/jam/blake.hpp +++ b/src/crypto/blake.hpp @@ -9,7 +9,7 @@ #include #include -namespace jam { +namespace jam::crypto { struct Blake { using Hash = qtils::BytesN<32>; blake2b_state state; diff --git a/src/jam/ed25519.hpp b/src/crypto/ed25519.hpp similarity index 97% rename from src/jam/ed25519.hpp rename to src/crypto/ed25519.hpp index 5699d57..224d46c 100644 --- a/src/jam/ed25519.hpp +++ b/src/crypto/ed25519.hpp @@ -7,7 +7,7 @@ #include #include -namespace jam::ed25519 { +namespace jam::crypto::ed25519 { using Secret = qtils::BytesN; using Public = qtils::BytesN; using KeyPair = qtils::BytesN; diff --git a/src/jam/keccak.hpp b/src/crypto/keccak.hpp similarity index 99% rename from src/jam/keccak.hpp rename to src/crypto/keccak.hpp index 028f14a..f9ab2f5 100644 --- a/src/jam/keccak.hpp +++ b/src/crypto/keccak.hpp @@ -10,7 +10,7 @@ * Keccak hash */ -namespace jam { +namespace jam::crypto { struct Keccak { using Hash32 = qtils::BytesN<32>; uint64_t state[5][5] = {}; diff --git a/src/executable/CMakeLists.txt b/src/executable/CMakeLists.txt new file mode 100644 index 0000000..392671f --- /dev/null +++ b/src/executable/CMakeLists.txt @@ -0,0 +1,45 @@ +# +# Copyright Quadrivium LLC +# All Rights Reserved +# SPDX-License-Identifier: Apache-2.0 +# + +set(CMAKE_OSX_ARCHITECTURES "arm64") + +set(LIBRARIES + node_injector +) + +include_directories( + ${PROJECT_SOURCE_DIR} + ${CMAKE_SOURCE_DIR}/src + ${CMAKE_BINARY_DIR}/generated +) + +# fix for dyld bug on mac +# we should actually check the target platform here, not the host platform, +# but not that we explicitly support cross-compilation anyway +if (CMAKE_HOST_APPLE) + option(MACOS_BIG_EXE_USE_DYLIB "compile dylib instead of exe to work around dyld cache error when loading big exe" ON) +endif () +if (MACOS_BIG_EXE_USE_DYLIB) + add_library(jam_node SHARED jam_node.cpp) + set_target_properties(jam_node PROPERTIES PREFIX "" DEBUG_POSTFIX "") + target_compile_definitions(jam_node PRIVATE BUILD_AS_LIBRARY) + target_link_libraries(jam_node ${LIBRARIES}) + + add_executable(jam_node_dlopen dlopen.cpp) + set_target_properties(jam_node_dlopen PROPERTIES OUTPUT_NAME jam_node) + set_target_properties(jam_node_dlopen PROPERTIES LINKER_LANGUAGE CXX) +else () + add_executable(jam_node jam_node.cpp) + target_link_libraries(jam_node ${LIBRARIES}) +endif () + +#if (BACKWARD) +# add_backward(jam_node) +#endif () + + +add_executable(experiment experiment.cpp) +target_link_libraries(experiment scale::scale fmt::fmt) diff --git a/src/executable/dlopen.cpp b/src/executable/dlopen.cpp new file mode 100644 index 0000000..c3a2702 --- /dev/null +++ b/src/executable/dlopen.cpp @@ -0,0 +1,37 @@ +/** + * Copyright Quadrivium LLC + * All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +extern "C" { +#include +#include +#include +#include +#include +} + +int main(int argc, const char **argv, const char **env) { + uint32_t size = 0; + _NSGetExecutablePath(NULL, &size); + char *exe = (char *)malloc(size); + _NSGetExecutablePath(exe, &size); + + const char *suffix = ".dylib"; + char *dylib = (char *)malloc(strlen(exe) + strlen(suffix) + 1); + strcpy(dylib, exe); + strcpy(dylib + strlen(exe), suffix); + + void *lib = dlopen(dylib, RTLD_NOW); + if (lib == NULL) { + printf("dlopen: %s\n", dlerror()); + return -1; + } + void *sym = dlsym(lib, "main"); + if (sym == NULL) { + printf("dlsym: %s\n", dlerror()); + return -1; + } + return ((int (*)(int, const char **, const char **))sym)(argc, argv, env); +} diff --git a/src/executable/experiment.cpp b/src/executable/experiment.cpp new file mode 100644 index 0000000..0ed48f0 --- /dev/null +++ b/src/executable/experiment.cpp @@ -0,0 +1,79 @@ +#include + +#include +#include + +struct S { + explicit operator int() const { + return 1; + } + S(int) {} + S &operator+=(auto i) { + return *this; + } + bool operator~() { + return true; + } +}; + +S &operator+=(S &s, auto i) { + return s; +} + +struct Tag {}; + +// void encode(qtils::is_tagged_v auto &&tagged, scale::ScaleEncoder auto &encoder) { +// encode(untagged(tagged), encoder); +// } +// void decode(qtils::is_tagged_v auto &tagged, scale::ScaleDecoder auto &decoder) { +// decode(untagged(tagged), decoder); +// } + +int main() { + qtils::Tagged x; + qtils::Tagged s(1); + qtils::Tagged, Tag> v(1); + qtils::Tagged ts; + int i; + + auto us = untagged(s); + + auto eq1x = 1 == x; + auto eqx1 = x == 1; + auto eqxx = x == x; + x = 1; + s = 1; + x += 1; + x += x; + s += 1; + s += x; + auto &cx = x; + s += cx; + s += std::move(x); + s += std::move(cx); + s += s; + // auto ss = x << 1; + + ++x; + --x; + auto ix = x++; + auto dx = x--; + + scale::Encoder es; + es << x; + es << v; + + scale::Decoder ds(es.backend().to_vector()); + ds >> x; + + auto lx = x << 1; + auto rx = x >> 1; + + auto &&w = !x; + auto&&ww = s.operator~(); + auto &&wwww = ~x; + auto z = ~i; + auto zzzz = not x; + + return 0; +} diff --git a/src/executable/jam_node.cpp b/src/executable/jam_node.cpp new file mode 100644 index 0000000..af58cdf --- /dev/null +++ b/src/executable/jam_node.cpp @@ -0,0 +1,170 @@ +/** + * Copyright Quadrivium LLC + * All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +#include + +#include +#include + +#include + +#include "app/application.hpp" +#include "app/configuration.hpp" +#include "app/configurator.hpp" +#include "injector/node_injector.hpp" +#include "log/logger.hpp" + +using std::string_view_literals::operator""sv; + +// NOLINTBEGIN(cppcoreguidelines-pro-bounds-pointer-arithmetic) + +namespace { + void wrong_usage() { + std::cerr << "Wrong usage.\n" + "Run with `--help' argument to print usage\n"; + } + + using jam::app::Application; + using jam::app::Configuration; + using jam::injector::NodeInjector; + using jam::log::LoggingSystem; + + int run_node(std::shared_ptr logsys, + std::shared_ptr appcfg) { + auto injector = std::make_unique(logsys, appcfg); + + auto logger = logsys->getLogger("Main", jam::log::defaultGroupName); + + auto app = injector->injectApplication(); + + SL_INFO(logger, "Node started. Version: {} ", appcfg->nodeVersion()); + + app->run(); + + SL_INFO(logger, "Node stopped"); + logger->flush(); + + return EXIT_SUCCESS; + } + +} // namespace + +int main(int argc, const char **argv, const char **env) { + soralog::util::setThreadName("jam-node"); + + qtils::FinalAction flush_std_streams_at_exit([] { + std::cout.flush(); + std::cerr.flush(); + }); + + if (argc == 0) { + // Abnormal run + wrong_usage(); + return EXIT_FAILURE; + } + + if (argc == 1) { + // Run without arguments + wrong_usage(); + return EXIT_FAILURE; + } + + auto app_configurator = + std::make_unique(argc, argv, env); + + // Parse CLI args for help, version and config + if (auto res = app_configurator->step1(); res.has_value()) { + if (res.value()) { + return EXIT_SUCCESS; + } + } else { + return EXIT_FAILURE; + } + + // Setup logging system + auto logging_system = ({ + auto log_config = app_configurator->getLoggingConfig(); + if (log_config.has_error()) { + std::cerr << "Logging config is empty.\n"; + return EXIT_FAILURE; + } + + auto log_configurator = std::make_shared( + std::shared_ptr(nullptr), log_config.value()); + + auto logging_system = + std::make_shared(std::move(log_configurator)); + + auto config_result = logging_system->configure(); + if (not config_result.message.empty()) { + (config_result.has_error ? std::cerr : std::cout) + << config_result.message << '\n'; + } + if (config_result.has_error) { + return EXIT_FAILURE; + } + + std::make_shared(std::move(logging_system)); + }); + + // Parse CLI args for help, version and config + if (auto res = app_configurator->step2(); res.has_value()) { + if (res.value()) { + return EXIT_SUCCESS; + } + } else { + return EXIT_FAILURE; + } + + // Setup config + auto configuration = ({ + auto logger = logging_system->getLogger("Configurator", "jam"); + + auto config_res = app_configurator->calculateConfig(logger); + if (config_res.has_error()) { + auto error = config_res.error(); + SL_CRITICAL(logger, "Failed to calculate config: {}", error); + fmt::println(std::cerr, "Failed to calculate config: {}", error); + fmt::println(std::cerr, "See more details in the log"); + return EXIT_FAILURE; + } + + config_res.value(); + }); + + int exit_code; + + { + std::string_view name{argv[1]}; + + if (name.substr(0, 1) == "-") { + // The first argument isn't subcommand, run as node + exit_code = run_node(logging_system, configuration); + } + + // else if (false and name == "subcommand-1"s) { + // exit_code = execute_subcommend_1(argc - 1, argv + 1); + // } + // + // else if (false and name == "subcommand-2"s) { + // exit_code = execute_subcommend_2(argc - 1, argv + 1); + // } + + else { + // No subcommand, but argument is not a valid option: begins not with dash + wrong_usage(); + return EXIT_FAILURE; + } + } + + auto logger = logging_system->getLogger("Main", jam::log::defaultGroupName); + SL_INFO(logger, "All components are stopped"); + logger->flush(); + + return exit_code; +} + +// NOLINTEND(cppcoreguidelines-pro-bounds-pointer-arithmetic) diff --git a/src/injector/CMakeLists.txt b/src/injector/CMakeLists.txt new file mode 100644 index 0000000..7030c72 --- /dev/null +++ b/src/injector/CMakeLists.txt @@ -0,0 +1,18 @@ +# +# Copyright Quadrivium LLC +# All Rights Reserved +# SPDX-License-Identifier: Apache-2.0 +# + +add_library(node_injector + node_injector.cpp +) +target_link_libraries(node_injector + Boost::Boost.DI + logger + app_configurator + app_state_manager + application + metrics + clock +) diff --git a/src/injector/bind_by_lambda.hpp b/src/injector/bind_by_lambda.hpp new file mode 100644 index 0000000..2ffde14 --- /dev/null +++ b/src/injector/bind_by_lambda.hpp @@ -0,0 +1,69 @@ +/** + * Copyright Quadrivium LLC + * All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +#pragma once + +#include + +namespace jam::injector { + /** + * Creates single instance per injector. + * + * `boost::di::bind.to(callable)` is hardcoded to + * `boost::di::scopes::instance` and prevents scope redefinition with + * `.in(scope)`. + * + * `callable` will be called more than once. Caching result with `static` will + * behave like `boost::di::scopes::singleton`. + * + * `BindByLambda` wraps `boost::di::extension::shared` to create single + * instance per injector, and adds `callable` support from + * `boost::di::scopes::instance`. + */ + struct BindByLambda { + template + struct ProviderF { + auto get() const { + return f(provider.super()); + } + F &f; + const Provider &provider; + }; + + template + struct scope { + explicit scope(const F &f) : f_{f} {} + + template + using is_referable = std::true_type; + + template + static boost::di::wrappers::shared + try_create(const Provider &provider); + + template + auto create(const Provider &provider) & { + return base_.template create( + ProviderF{f_, provider}); + } + + template + auto create(const Provider &provider) && { + return std::move(base_).template create( + ProviderF{f_, provider}); + } + + boost::di::extension::detail::shared::scope base_; + F f_; + }; + }; + + template + auto bind_by_lambda(const F &f) { + return boost::di::core::dependency{f}; + } +} // namespace jam::injector diff --git a/src/injector/dont_inject.hpp b/src/injector/dont_inject.hpp new file mode 100644 index 0000000..b5bdaf9 --- /dev/null +++ b/src/injector/dont_inject.hpp @@ -0,0 +1,18 @@ +/** + * Copyright Quadrivium LLC + * All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +#pragma once + +// Prevent implicit injection. +// Will generate "unresolved symbol" error during linking. +// Not using `= delete` as it would break injector SFINAE. +#define DONT_INJECT(T) explicit T(::jam::injector::DontInjectHelper, ...); + +namespace jam::injector { + struct DontInjectHelper { + explicit DontInjectHelper() = default; + }; +} // namespace jam::injector diff --git a/src/injector/lazy.hpp b/src/injector/lazy.hpp new file mode 100644 index 0000000..f0391b0 --- /dev/null +++ b/src/injector/lazy.hpp @@ -0,0 +1,28 @@ +/** + * Copyright Quadrivium LLC + * All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +#pragma once + +#include + +namespace jam { + + template + using Lazy = boost::di::extension::lazy; + + template + using LazyRef = boost::di::extension::lazy; + + template + using LazyCRef = boost::di::extension::lazy; + + template + using LazySPtr = boost::di::extension::lazy>; + + template + using LazyUPtr = boost::di::extension::lazy>; + +} // namespace jam diff --git a/src/injector/node_injector.cpp b/src/injector/node_injector.cpp new file mode 100644 index 0000000..48ef7d5 --- /dev/null +++ b/src/injector/node_injector.cpp @@ -0,0 +1,101 @@ +/** + * Copyright Quadrivium LLC + * All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +#define BOOST_DI_CFG_DIAGNOSTICS_LEVEL 2 +#define BOOST_DI_CFG_CTOR_LIMIT_SIZE 32 + +#include "injector/node_injector.hpp" + +#include +#include + +#include "app/configuration.hpp" +#include "app/impl/application_impl.hpp" +#include "app/impl/state_manager_impl.hpp" +#include "app/impl/watchdog.hpp" +#include "clock/impl/clock_impl.hpp" +#include "injector/bind_by_lambda.hpp" +#include "log/logger.hpp" +#include "metrics/impl/exposer_impl.hpp" +#include "metrics/impl/prometheus/handler_impl.hpp" + +namespace { + namespace di = boost::di; + namespace fs = std::filesystem; + using namespace jam; // NOLINT + + template + auto useConfig(C c) { + return boost::di::bind >().to( + std::move(c))[boost::di::override]; + } + + using injector::bind_by_lambda; + + template + auto makeApplicationInjector(std::shared_ptr logsys, + std::shared_ptr config, + Ts &&...args) { + // clang-format off + return di::make_injector( + di::bind.to(), + di::bind.to(), + di::bind.to(), + di::bind.to(), + di::bind. to(), + di::bind.to(config), + di::bind.to(logsys), + di::bind.to(), + di::bind.to(), + di::bind.to([](const auto &injector) { + return metrics::Exposer::Configuration{ + {boost::asio::ip::address_v4::from_string("127.0.0.1"), 7777} + // injector + // .template create() + // .openmetricsHttpEndpoint() + }; + }), + + // user-defined overrides... + std::forward(args)...); + // clang-format on + } + + template + auto makeNodeInjector(std::shared_ptr logsys, + std::shared_ptr config, + Ts &&...args) { + return di::make_injector( + makeApplicationInjector(std::move(logsys), std::move(config)), + + // user-defined overrides... + std::forward(args)...); + } +} // namespace + +namespace jam::injector { + class NodeInjectorImpl { + public: + using Injector = + decltype(makeNodeInjector(std::shared_ptr(), + std::shared_ptr())); + + explicit NodeInjectorImpl(Injector injector) + : injector_{std::move(injector)} {} + + Injector injector_; + }; + + NodeInjector::NodeInjector(std::shared_ptr logsys, + std::shared_ptr config) + : pimpl_{std::make_unique( + makeNodeInjector(std::move(logsys), std::move(config)))} {} + + std::shared_ptr NodeInjector::injectApplication() { + return pimpl_->injector_ + .template create >(); + } +} // namespace jam::injector diff --git a/src/injector/node_injector.hpp b/src/injector/node_injector.hpp new file mode 100644 index 0000000..6be0473 --- /dev/null +++ b/src/injector/node_injector.hpp @@ -0,0 +1,37 @@ +/** + * Copyright Quadrivium LLC + * All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +#pragma once + +#include + +namespace jam::log { + class LoggingSystem; +} // namespace jam::log + +namespace jam::app { + class Configuration; + class Application; +} // namespace jam::app + +namespace jam::injector { + + /** + * Dependency injector for a universal node. Provides all major components + * required by the JamNode application. + */ + class NodeInjector final { + public: + explicit NodeInjector(std::shared_ptr logging_system, + std::shared_ptr configuration); + + std::shared_ptr injectApplication(); + + protected: + std::shared_ptr pimpl_; + }; + +} // namespace jam::injector diff --git a/src/jam/empty.hpp b/src/jam/empty.hpp deleted file mode 100644 index e13694b..0000000 --- a/src/jam/empty.hpp +++ /dev/null @@ -1,27 +0,0 @@ -/** - * Copyright Quadrivium LLC - * All Rights Reserved - * SPDX-License-Identifier: Apache-2.0 - */ - -#pragma once - -namespace jam { - - /// Special zero-size-type for some things - /// (e.g. unsupported, experimental or empty). - struct Empty { - constexpr bool operator==(const Empty &) const = default; - - template - friend inline Stream &operator<<(Stream &s, const Empty &) { - return s; - } - - template - friend inline Stream &operator>>(Stream &s, const Empty &) { - return s; - } - }; - -} // namespace jam diff --git a/src/jam/tagged.hpp b/src/jam/tagged.hpp deleted file mode 100644 index 347800e..0000000 --- a/src/jam/tagged.hpp +++ /dev/null @@ -1,89 +0,0 @@ -/** - * Copyright Quadrivium LLC - * All Rights Reserved - * SPDX-License-Identifier: Apache-2.0 - */ - -#pragma once - -#include -#include - -#include - -namespace jam { - - template >> - struct Wrapper { - template - Wrapper(Args &&...args) : value(std::forward(args)...) {} - - protected: - T value; - }; - - template , Wrapper, T>> - class Tagged : public Base { - public: - using type = T; - using tag = Tag; - - Tagged() : Base() {} - - Tagged(T value) : Base(std::move(value)) {} - - // NOLINTNEXTLINE(cppcoreguidelines-rvalue-reference-param-not-moved) - Tagged &operator=(T &&value) noexcept( - not std::is_lvalue_reference_v) { - if constexpr (std::is_scalar_v) { - this->Wrapper::value = std::forward(value); - } else { - static_cast(*this) = std::forward(value); - } - return *this; - } - - template - explicit operator Out() const { - // NOLINTNEXTLINE(readability-else-after-return) - if constexpr (std::is_scalar_v) { - return this->Wrapper::value; - } else { - return *this; - } - } - - // auto operator<=>(const Tagged &other) const = default; - // bool operator==(const Tagged &other) const = default; - }; - - template < - typename T, - typename Tag, - typename Base = std::conditional_t, Wrapper, T>> - std::ostream &operator<<(std::ostream &os, - const Tagged &view) = delete; -} // namespace jam - -template -::scale::ScaleEncoderStream &operator<<(scale::ScaleEncoderStream &s, - const jam::Tagged &tagged) { - if constexpr (std::is_scalar_v) { - return s << tagged.template Wrapper::value; - } else { - return s << static_cast(tagged); - } -} - -template -::scale::ScaleDecoderStream &operator>>(scale::ScaleDecoderStream &s, - jam::Tagged &tagged) { - if constexpr (std::is_scalar_v) { - return s >> tagged.template Wrapper::value; - } else { - return s >> static_cast(tagged); - } -} diff --git a/src/jam/unused.hpp b/src/jam/unused.hpp deleted file mode 100644 index 24f4974..0000000 --- a/src/jam/unused.hpp +++ /dev/null @@ -1,66 +0,0 @@ -/** - * Copyright Quadrivium LLC - * All Rights Reserved - * SPDX-License-Identifier: Apache-2.0 - */ - -#pragma once - -#include - -#include - -#include -#include - -namespace jam { - - enum UnusedError : uint8_t { - AttemptToEncodeUnused = 1, - AttemptToDecodeUnused, - }; - Q_ENUM_ERROR_CODE(UnusedError) { - using E = decltype(e); - switch (e) { - case E::AttemptToEncodeUnused: - return "AttemptToEncodeUnused"; - case E::AttemptToDecodeUnused: - return "AttemptToDecodeUnused"; - } - abort(); - } - - /// Number-based marker-type for using as tag - template - struct NumTag { - private: - static constexpr size_t tag = Num; - }; - - /// Special zero-size-type for some things - /// (e.g., dummy types of variant, unsupported or experimental). - template - using Unused = Tagged>; - - template - constexpr bool operator==(const Unused &, const Unused &) { - return true; - } - - /// To raise failure while attempt to encode unused entity - template - inline ::scale::ScaleEncoderStream &operator<<(::scale::ScaleEncoderStream &s, - const Unused &) { - ::scale::raise(UnusedError::AttemptToEncodeUnused); - return s; - } - - /// To raise failure while attempt to decode unused entity - template - inline ::scale::ScaleDecoderStream &operator>>(::scale::ScaleDecoderStream &s, - const Unused &) { - ::scale::raise(UnusedError::AttemptToDecodeUnused); - return s; - } - -} // namespace kagome diff --git a/src/log/CMakeLists.txt b/src/log/CMakeLists.txt new file mode 100644 index 0000000..f9ce07a --- /dev/null +++ b/src/log/CMakeLists.txt @@ -0,0 +1,16 @@ +# +# Copyright Quadrivium LLC +# All Rights Reserved +# SPDX-License-Identifier: Apache-2.0 +# + +add_library(logger + logger.cpp +) +target_link_libraries(logger + fmt::fmt + soralog::soralog + soralog::configurator_yaml + qtils::qtils + Boost::algorithm + ) diff --git a/src/log/formatters/filepath.hpp b/src/log/formatters/filepath.hpp new file mode 100644 index 0000000..d03c635 --- /dev/null +++ b/src/log/formatters/filepath.hpp @@ -0,0 +1,18 @@ +/** + * Copyright Quadrivium LLC + * All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +#pragma once + +#include +#include + +template <> +struct fmt::formatter + : fmt::formatter { + auto format(const std::filesystem::path &path, format_context &ctx) { + return fmt::formatter::format(path.native(), ctx); + } +}; diff --git a/src/log/formatters/optional.hpp b/src/log/formatters/optional.hpp new file mode 100644 index 0000000..c263221 --- /dev/null +++ b/src/log/formatters/optional.hpp @@ -0,0 +1,36 @@ +/** + * Copyright Quadrivium LLC + * All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +#pragma once + +#include + +#include +#include +#include + +#include + +template