diff --git a/.clang-tidy b/.clang-tidy new file mode 100644 index 0000000..a5ed099 --- /dev/null +++ b/.clang-tidy @@ -0,0 +1,41 @@ +Checks: ' +*, +-llvmlibc-*, +-android-*, +-altera-*, +-fuchsia*, +-google-readability-todo, +-cppcoreguidelines-init-variables, +-cppcoreguidelines-pro-type-reinterpret-cast, +-llvm-namespace-comment, +-readability-implicit-bool-conversion, +-google-explicit-constructor, +-abseil-string-find-str-contains, +-readability-avoid-return-with-void-value, +-readability-convert-member-functions-to-static, + +-google-readability-braces-around-statements, +-google-readability-namespace-comments, +-hicpp-special-member-functions, +-hicpp-braces-around-statements, +-hicpp-explicit-conversions +' + +WarningsAsErrors: 'bugprone-exception-escape' +FormatStyle: 'none' # TODO: Replace with 'file' once we have a proper .clang-format file +InheritParentConfig: true +CheckOptions: + misc-include-cleaner.MissingIncludes: 'false' + misc-include-cleaner.IgnoreHeaders: 'CppSockets/OSDetection.*;.*Version.hpp' + + bugprone-argument-comment.StrictMode: 1 + + # Readability + readability-braces-around-statements.ShortStatementLines: 2 + + readability-identifier-naming.NamespaceCase: CamelCase + + readability-identifier-length.IgnoredVariableNames: "^(fd|nb|n|ss|ec|is|os|_.*)$" + readability-identifier-length.IgnoredParameterNames: "^(fd|n|is|os|_.*)$" + + cppcoreguidelines-special-member-functions.AllowSoleDefaultDtor: true diff --git a/.github/workflows/cmake-multi-platform.yml b/.github/workflows/cmake-multi-platform.yml new file mode 100644 index 0000000..c20c57e --- /dev/null +++ b/.github/workflows/cmake-multi-platform.yml @@ -0,0 +1,113 @@ +name: CMake on multiple platforms + +on: + push: + branches: [ "master" ] + pull_request: + branches: [ "master" ] + types: [ "opened", "reopened", "synchronize", "ready_for_review" ] + +jobs: + build: + runs-on: ${{ matrix.os }} + + strategy: + # ensure we don't stop after 1 failure to always have a complete picture of what is failing + fail-fast: false + + # Set up a matrix to run the following configurations: + # - ubuntu Debug/Release clang/gcc + # - windows Debug/Release cl + # - macos Debug/Release clang + matrix: + os: [ubuntu-latest, macos-latest, windows-latest] + build_type: [Release, Debug] + c_compiler: [gcc, clang, cl] + include: + - os: windows-latest + c_compiler: cl + cpp_compiler: cl + - os: ubuntu-latest + c_compiler: gcc + cpp_compiler: g++ + - os: ubuntu-latest + c_compiler: clang + cpp_compiler: clang++ + - os: macos-latest + c_compiler: clang + cpp_compiler: clang++ + exclude: + - os: windows-latest + c_compiler: gcc + - os: windows-latest + c_compiler: clang + - os: ubuntu-latest + c_compiler: cl + - os: macos-latest + c_compiler: cl + - os: macos-latest + c_compiler: gcc + + steps: + - uses: actions/checkout@v3 + + - name: Set Env + shell: bash + run: | + echo "BUILD_OUTPUT_DIR=${{ github.workspace }}/build" >> "$GITHUB_ENV" + + - name: VCPKG Install (Windows) + if: runner.os == 'Windows' + uses: ./.github/workflows/windows-vcpkg + with: + key: ${{ runner.os }}-${{ matrix.build_type }} + + - name: Configure CMake + # Configure CMake in a 'build' subdirectory. + # `CMAKE_BUILD_TYPE` is only required if you are using a single-configuration generator such as make. + # See https://cmake.org/cmake/help/latest/variable/CMAKE_BUILD_TYPE.html?highlight=cmake_build_type + run: > + cmake -B ${{ env.BUILD_OUTPUT_DIR }} + -DCMAKE_CXX_COMPILER=${{ matrix.cpp_compiler }} + -DCMAKE_C_COMPILER=${{ matrix.c_compiler }} + -DCMAKE_BUILD_TYPE=${{ matrix.build_type }} + -DFILE_SHARE_BUILD_TESTS=TRUE + -S ${{ github.workspace }} + + - name: Build + # Build your program with the given configuration. Note that --config is needed + # because the default Windows generator is a multi-config generator (Visual Studio generator). + run: cmake --build ${{ env.BUILD_OUTPUT_DIR }} --config ${{ matrix.build_type }} + + - uses: actions/upload-artifact@v4 + if: matrix.os == 'ubuntu-latest' && matrix.build_type == 'Release' && matrix.c_compiler == 'clang' + with: + name: compile_commands.json + path: ${{ env.BUILD_OUTPUT_DIR }}/compile_commands.json + + - name: Test + working-directory: ${{ env.BUILD_OUTPUT_DIR }} + # Execute tests defined by the CMake configuration. Note that --build-config is needed + # because the default Windows generator is a multi-config generator (Visual Studio generator). + # See https://cmake.org/cmake/help/latest/manual/ctest.1.html for more detail + run: ctest --build-config ${{ matrix.build_type }} --test-dir tests --output-on-failure + + clang-tidy: + needs: 'build' + runs-on: ubuntu-latest + if: always() && github.event_name == 'pull_request' + + steps: + - name: Checkout Code + uses: actions/checkout@v4 + + - uses: actions/download-artifact@v4 + with: + name: compile_commands.json + + - name: clang-tidy review + uses: ZedThree/clang-tidy-review@v0.21.0 + + # If there are any comments, fail the check + - if: steps.review.outputs.total_comments > 0 + run: exit 1 diff --git a/.github/workflows/code_scanning.yml b/.github/workflows/code_scanning.yml new file mode 100644 index 0000000..38971df --- /dev/null +++ b/.github/workflows/code_scanning.yml @@ -0,0 +1,128 @@ +name: "Code Scanning" + +on: + push: + branches: [ "master" ] + pull_request: + # The branches below must be a subset of the branches above + branches: [ "master" ] + schedule: + - cron: '20 3 * * 0' + +jobs: + codeql: + name: CodeQL + # Runner size impacts CodeQL analysis time. To learn more, please see: + # - https://gh.io/recommended-hardware-resources-for-running-codeql + # - https://gh.io/supported-runners-and-hardware-resources + # - https://gh.io/using-larger-runners + # Consider using larger runners for possible analysis time improvements. + runs-on: ubuntu-latest + timeout-minutes: 360 + permissions: + actions: read + contents: read + security-events: write + + steps: + - name: Checkout repository + uses: actions/checkout@v3 + + # Initializes the CodeQL tools for scanning. + - name: Initialize CodeQL + uses: github/codeql-action/init@v3 + with: + languages: 'c-cpp' + # If you wish to specify custom queries, you can do so here or in a config file. + # By default, queries listed here will override any specified in a config file. + # Prefix the list here with "+" to use these queries and those in the config file. + + # For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs + # queries: security-extended,security-and-quality + + + # Autobuild attempts to build any compiled languages (C/C++, C#, Go, Java, or Swift). + # If this step fails, then you should remove it and run the build manually (see below) + - name: Autobuild + uses: github/codeql-action/autobuild@v3 + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v3 + with: + category: "/language:c-cpp" + + flawfinder: + name: Flawfinder + runs-on: ubuntu-latest + permissions: + actions: read + contents: read + security-events: write + steps: + - name: Checkout code + uses: actions/checkout@v3 + + - name: flawfinder_scan + uses: david-a-wheeler/flawfinder@2.0.19 + with: + arguments: '--sarif ./' + output: 'flawfinder_results.sarif' + + - name: Upload analysis results to GitHub Security tab + uses: github/codeql-action/upload-sarif@v3 + with: + sarif_file: ${{github.workspace}}/flawfinder_results.sarif + + microsoft-analyze: + permissions: + actions: read # only required for a private repository by github/codeql-action/upload-sarif to get the Action run status + contents: read # for actions/checkout to fetch code + security-events: write # for github/codeql-action/upload-sarif to upload SARIF results + name: Microsoft Analyze + runs-on: windows-latest + + env: + # Path to the CMake build directory. + build: '${{ github.workspace }}/build' + config: 'Debug' + + steps: + - name: Checkout repository + uses: actions/checkout@v3 + + - name: VCPKG Install (Windows) + uses: ./.github/workflows/windows-vcpkg + with: + key: ${{ runner.os }}-${{ env.config }} + + - name: Configure CMake + run: cmake -B ${{ env.build }} -DCMAKE_BUILD_TYPE=${{ env.config }} + + # Build is not required unless generated source files are used + # - name: Build CMake + # run: cmake --build ${{ env.build }} --config ${{ env.config }} + + - name: Run MSVC Code Analysis + uses: microsoft/msvc-code-analysis-action@v0.1.1 + # Provide a unique ID to access the sarif output path + id: run-analysis + with: + cmakeBuildDirectory: ${{ env.build }} + buildConfiguration: ${{ env.config }} + # Ruleset file that will determine what checks will be run + ruleset: NativeRecommendedRules.ruleset + # Paths to ignore analysis of CMake targets and includes + # ignoredPaths: ${{ github.workspace }}/dependencies;${{ github.workspace }}/test + + # Upload SARIF file to GitHub Code Scanning Alerts + - name: Upload SARIF to GitHub + uses: github/codeql-action/upload-sarif@v3 + with: + sarif_file: ${{ steps.run-analysis.outputs.sarif }} + + # # Upload SARIF file as an Artifact to download and view + # - name: Upload SARIF as an Artifact + # uses: actions/upload-artifact@v4 + # with: + # name: sarif-file + # path: ${{ steps.run-analysis.outputs.sarif }} diff --git a/.github/workflows/windows-vcpkg/action.yml b/.github/workflows/windows-vcpkg/action.yml new file mode 100644 index 0000000..0feb036 --- /dev/null +++ b/.github/workflows/windows-vcpkg/action.yml @@ -0,0 +1,37 @@ +name: Windows VCPKG + +inputs: + key: + required: true + type: string + +runs: + using: "composite" + steps: + - name: Set Env + shell: powershell + run: | + echo "VCPKG_ROOT=${env:VCPKG_INSTALLATION_ROOT}" | Out-File -FilePath $env:GITHUB_ENV -Append + echo "VCPKG_CACHE=${env:LOCALAPPDATA}\vcpkg\archives" | Out-File -FilePath $env:GITHUB_ENV -Append + + - name: Fetch VCPKG Cache (Windows) + id: fetch-vcpkg-cache + if: runner.os == 'Windows' + uses: actions/cache/restore@v4 + with: + key: ${{ inputs.key }}-vcpkg-${{ hashFiles('vcpkg.json') }} + path: ${{ env.VCPKG_CACHE }} + + - name: VCPKG Install (Windows) + if: runner.os == 'Windows' + shell: powershell + run: | + echo "CMAKE_TOOLCHAIN_FILE=${env:VCPKG_ROOT}\scripts\buildsystems\vcpkg.cmake" | Out-File -FilePath $env:GITHUB_ENV -Append + vcpkg install + + - name: Always Save VCPKG Cache (Windows) + if: always() && runner.os == 'Windows' && steps.fetch-vcpkg-cache.outputs.cache-hit != 'true' + uses: actions/cache/save@v4 + with: + key: ${{ steps.fetch-vcpkg-cache.outputs.cache-primary-key }} + path: ${{ env.VCPKG_CACHE }} diff --git a/.gitignore b/.gitignore index ac6154b..75f7f42 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,8 @@ compile_commands.json *.tmp *.gch +*.pch vgcore.* -.vscode \ No newline at end of file +.vscode/ +.cache/ diff --git a/CMakeLists.txt b/CMakeLists.txt index a5e05fc..07721f2 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -4,7 +4,7 @@ ## Author Francois Michaut ## ## Started on Sat Jan 15 01:19:13 2022 Francois Michaut -## Last update Fri Nov 24 10:00:00 2023 Francois Michaut +## Last update Sun Aug 24 18:59:20 2025 Francois Michaut ## ## CMakeLists.txt : Top level CMake ## @@ -14,17 +14,17 @@ set(CMAKE_CXX_STANDARD 20) set(CMAKE_CXX_STANDARD_REQUIRED True) set(CMAKE_EXPORT_COMPILE_COMMANDS ON) -include(ExternalProject) include(FetchContent) -# TODO: This currently breaks Apple/Windows builds: find another solution -# set_property(GLOBAL PROPERTY ALLOW_DUPLICATE_CUSTOM_TARGETS 1) set(MAKEFLAGS "--no-print-directory") +option(BUILD_CLI "Build the FileShare CLI" ON) +option(BUILD_GUI "Build the FileShare GUI" ON) + if (!WIN32) set(CMAKE_CXX_FLAGS_DEBUG_INIT "-O0 -DDEBUG -g3") set(CMAKE_CXX_FLAGS_RELEASE_INIT "-O3") - add_compile_definitions(_GNU_SOURCE) + add_compile_definitions(_GNU_SOURCE _FILE_OFFSET_BITS=64 _TIME_BITS=64) endif() project(FileShare VERSION 0.1.0 LANGUAGES C CXX) @@ -33,83 +33,79 @@ configure_file(include/FileShareVersion.hpp.in FileShareVersion.hpp) add_subdirectory(dependencies) include_directories( - "${CMAKE_CURRENT_BINARY_DIR}" - "${CMAKE_CURRENT_SOURCE_DIR}/include" + "${CMAKE_CURRENT_BINARY_DIR}" # For the configured Version file + "${CMAKE_CURRENT_SOURCE_DIR}/include" # For the regular includes ) -set(executables "") - -function(add_mvc_source) - set(local_executables ${executables}) +set(mvc_sources "") +function(add_mvc_sources) + set(local_sources ${mvc_sources}) foreach(name IN LISTS ARGN) - set(new_executables + set(new_sources "source/${name}/View.cpp" "source/${name}/Model.cpp" "source/${name}/Controller.cpp" ) - set(local_executables ${local_executables} ${new_executables}) + set(local_sources ${local_sources} ${new_sources}) endforeach() - set(executables ${local_executables} PARENT_SCOPE) + set(mvc_sources ${local_sources} PARENT_SCOPE) endfunction() -add_mvc_source(DeviceList Settings) -add_executable(file-share - source/main.cpp - source/Application.cpp - source/Debug.cpp - source/ThemeManager.cpp - source/Components/Button.cpp - source/Components/Foldout.cpp - source/Components/InputFileDialog.cpp - source/Components/List.cpp - source/Components/ListMenu.cpp - ${executables} -) +if (BUILD_GUI) + add_mvc_sources(DeviceList Settings) + add_executable(file-share + source/main.cpp + source/Application.cpp + source/Debug.cpp + source/ThemeManager.cpp + source/Components/Button.cpp + source/Components/Foldout.cpp + source/Components/InputFileDialog.cpp + source/Components/List.cpp + source/Components/ListMenu.cpp + ${mvc_sources} + ) -target_compile_definitions(file-share PRIVATE _LARGEFILE_SOURCE _FILE_OFFSET_BITS=64) -target_link_libraries(file-share tgui sfml-graphics sfml-window sfml-system sqlite_orm::sqlite_orm fsp) + target_compile_definitions(file-share PRIVATE _LARGEFILE_SOURCE _FILE_OFFSET_BITS=64) + target_link_libraries(file-share tgui sfml-graphics sfml-window sfml-system sqlite_orm::sqlite_orm fsp) -# Platform-specific libraries for theme detection -if(APPLE) + # Platform-specific libraries for theme detection + if(APPLE) target_link_libraries(file-share "-framework CoreFoundation") -elseif(WIN32) + elseif(WIN32) target_link_libraries(file-share winreg) + endif() + + target_compile_options(file-share PRIVATE + $<$,$,$>:-Wall -Wextra> + $<$:/W4> + ) endif() -target_compile_options(file-share PRIVATE - $<$,$,$>:-Wall -Wextra> - $<$:/W4> -) +if (BUILD_CLI) + add_executable(file-share-cli + source/cli/main.cpp + source/cli/interactive.cpp + ) -add_executable(file-share-cli - source/cli/main.cpp -) + target_compile_definitions(file-share-cli PRIVATE _LARGEFILE_SOURCE _FILE_OFFSET_BITS=64) + target_link_libraries(file-share-cli argparse fsp) + if (!APPLE) + target_precompile_headers(file-share-cli + PRIVATE "${argparse_SOURCE_DIR}/include/argparse/argparse.hpp" + ) + endif() -target_compile_definitions(file-share-cli PRIVATE _LARGEFILE_SOURCE _FILE_OFFSET_BITS=64) -target_link_libraries(file-share-cli argparse fsp) -if (!APPLE) - target_precompile_headers(file-share-cli - PRIVATE - "${argparse_SOURCE_DIR}/include/argparse/argparse.hpp" + target_compile_options(file-share-cli PRIVATE + $<$,$,$>:-Wall -Wextra> + $<$:/W4> ) endif() -target_compile_options(file-share-cli PRIVATE - $<$,$,$>:-Wall -Wextra> - $<$:/W4> -) -add_custom_target(test - COMMAND ${CMAKE_COMMAND} --log-level=WARNING - -B "${CMAKE_CURRENT_BINARY_DIR}/tests" - -S "${CMAKE_CURRENT_SOURCE_DIR}/tests" - -G ${CMAKE_GENERATOR} - COMMAND ${CMAKE_COMMAND} -E cmake_echo_color - --switch=$(COLOR) --cyan "Building tests..." - COMMAND ${CMAKE_COMMAND} - --build "${CMAKE_CURRENT_BINARY_DIR}/tests" -- --quiet - COMMAND ${CMAKE_MAKE_PROGRAM} - -C "${CMAKE_CURRENT_BINARY_DIR}/tests" test ARGS=--output-on-failure -) +option(FILE_SHARE_BUILD_TESTS "TRUE to build the FileShare tests" FALSE) +if(FILE_SHARE_BUILD_TESTS) + add_subdirectory(tests) +endif() diff --git a/dependencies/CMakeLists.txt b/dependencies/CMakeLists.txt index 25e5729..e424f01 100644 --- a/dependencies/CMakeLists.txt +++ b/dependencies/CMakeLists.txt @@ -4,7 +4,7 @@ ## Author Francois Michaut ## ## Started on Wed Jul 10 10:27:39 2024 Francois Michaut -## Last update Sat Aug 10 09:39:35 2024 Francois Michaut +## Last update Sun Aug 24 12:51:51 2025 Francois Michaut ## ## CMakeLists.txt : CMake to fetch and build the dependecies of the FileShare executable ## @@ -40,7 +40,7 @@ FetchContent_Declare( FetchContent_Declare( fsp GIT_REPOSITORY https://github.com/FileShare-Project/libfsp.git - GIT_TAG b2272f6740ff1458751d5e09812a2da28afd37b8 + GIT_TAG e36512b59f700abcc5cb379d77ee18aa8b6757fb ) FetchContent_MakeAvailable(fsp argparse SFML sqlite_orm tgui) diff --git a/source/cli/arguments.cpp b/source/cli/arguments.cpp new file mode 100644 index 0000000..f6e88b2 --- /dev/null +++ b/source/cli/arguments.cpp @@ -0,0 +1,10 @@ +/* +** Project FileShare, 2023 +** +** Author Francois Michaut +** +** Started on Sat Nov 11 11:22:07 2023 Francois Michaut +** Last update Sat Nov 11 11:22:14 2023 Francois Michaut +** +** arguments.cpp : Arguments parsing +*/ diff --git a/source/cli/interactive.cpp b/source/cli/interactive.cpp new file mode 100644 index 0000000..4fb565b --- /dev/null +++ b/source/cli/interactive.cpp @@ -0,0 +1,790 @@ +/* +** Project Project FileShare, 2025 +** +** Author Francois Michaut +** +** Started on Wed Jul 23 13:49:52 2025 Francois Michaut +** Last update Sat Aug 23 00:13:51 2025 Francois Michaut +** +** interactive.cpp : Interractive Mode logic +*/ + +#include "FileShare/Config/ServerConfig.hpp" +#include "FileShare/Protocol/Definitions.hpp" +#include "FileShareVersion.hpp" + +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +extern bool server_run; +void signal_handler(int sig); + +enum class InteractiveStateMachine { + MAIN_MENU = 0x00, + EXIT_MENU = 0x01, + CONFIG_MENU = 0x05, + + DISCONNECT_MENU = 0x12, + ACCEPT_MENU = 0x13, + SELECT_MENU = 0x16, + + EXECUTE_MENU = 0x20, + + TRANSFERS_MENU = 0x30, +}; + +auto operator<<(std::ostream &out, InteractiveStateMachine state) -> std::ostream & { + switch (state) { + case InteractiveStateMachine::MAIN_MENU: + out << "MAIN"; + break; + case InteractiveStateMachine::EXIT_MENU: + out << "EXIT"; + break; + case InteractiveStateMachine::CONFIG_MENU: + out << "CONFIG"; + break; + case InteractiveStateMachine::DISCONNECT_MENU: + out << "DISCONNECT"; + break; + case InteractiveStateMachine::ACCEPT_MENU: + out << "ACCEPT"; + break; + case InteractiveStateMachine::SELECT_MENU: + out << "SELECT"; + break; + case InteractiveStateMachine::EXECUTE_MENU: + out << "EXECUTE"; + break; + case InteractiveStateMachine::TRANSFERS_MENU: + out << "TRANSFERS"; + break; + default: + out << "Unkown State: " << static_cast(state); + } + + return out; +} + +struct InteractiveState { + InteractiveState(FileShare::Server &server) : server(server) {} + + FileShare::Server &server; + std::vector pending_events; + std::weak_ptr selected_peer; + + InteractiveStateMachine state = InteractiveStateMachine::MAIN_MENU; +}; + +using CommandMap = std::unordered_map>; +using ConfigCommandMap = std::unordered_map>; + +static void display_interactive_help() { + std::cout << "Available Commands are :" << '\n'; + std::cout << "\t" << "- HELP: Display this help message." << '\n'; + std::cout << "\t" << "- EXIT|QUIT: Stop the program." << '\n'; + std::cout << "\t" << "- SERVER (ON|OFF): Toggle the Server ON or OFF.\n\t\t If Server is OFF, " + "no external peers can connect, but you can still initiate connections (existing " + "connections will not be terminated)." << '\n'; + + std::cout << "\t" << "- CONFIG (SERVER|DEFAULT|) [SUBCOMMAND] []...: " + "(Interractive) Modify configuration on the fly. Available subcommands :" << '\n'; + std::cout << "\t\t" << "- SHOW: (default) Display the selected configuration." << '\n'; + std::cout << "\t\t" << "- SET : Set a simple configuration value." << '\n'; + std::cout << "\t\t" << "- SAVE []: Save the config to a file at PATH. " + "Overwrites file if exists. If PATH is empty, will save into the original file." << '\n'; + std::cout << "\t\t" << "- LOAD []: Replace the current config with the one in the " + "config file at PATH. If PATH is empty, will load from the default location." << '\n'; + // TODO: FileMap config support + + std::cout << "\t" << "- PEER []...: Manage peers. Available subcommands :" + << '\n'; + std::cout << "\t\t" << "- LIST: List currently connected peers." << '\n'; + std::cout << "\t\t" << "- CONNECT []: Connect to a new peer." << '\n'; + std::cout << "\t\t" << "- DISCONNECT []: (Interractive) Disconnect from a connected peer " + "or reject a connection request." << '\n'; + std::cout << "\t\t" << "- ACCEPT []: (Interractive) Accept a connection request from an " + "external peer." << '\n'; + std::cout << "\t\t" << "- SELECT []: (Interractive) Promote a peer as the 'selected' one." + "\n\t\t\t If connected to multiple peers, pre-selecting a peer allows to send multiple " + "commands without having to select it each time." << '\n'; + + // TODO: Where do I select Peer if multiple and None selected ? + std::cout << "\t" << "- EXECUTE []...: (Interractive) Execute a command on a " + "selected peer. Will prompt to select a peer if none selected. Available commands :" + << '\n'; + std::cout << "\t\t" << "- LIST_FILES []: List availables files on the peer. Can be " + "Downloaded using RECEIVE_FILE." << '\n'; + std::cout << "\t\t" << "- RECEIVE_FILE : Request a file download" << '\n'; + std::cout << "\t\t" << "- SEND_FILE : Send a file to the peer." << '\n'; + + std::cout << "\t" << "- TRANSFERS: Display ongoing Downloads and Uploads transfers and their " + "progress." << '\n'; + + std::cout << "\n" << "Interractive commands will prompt for arguments selection or subsequent " + "required inputs and/or display choices to select from." << '\n'; + std::cout << "Some commands accept their arguments optionally and can be both interactive " + "and non-interactive based on if all their arguments have been provided." << '\n'; + std::cout << "Command names and arguments are case-insensitive" << '\n'; + + std::cout << std::flush; +} + +void lowercase(std::string &str) { + std::transform(str.begin(), str.end(), str.begin(), [](unsigned char chr){ return std::tolower(chr); }); +} + +auto operator<<(std::ostream &out, const FileShare::Config &config) -> std::ostream & { + return out << '{' << '\n' << + '\t' << "DOWNLOAD_FOLDER = " << config.get_downloads_folder() << '\n' << + '}'; +} + +auto operator<<(std::ostream &out, const FileShare::ServerConfig &config) -> std::ostream & { + return out << '{' << '\n' << + '\t' << "DEVICE_UUID = " << config.get_uuid() << '\n' << + '\t' << "DEVICE_NAME = " << config.get_device_name() << '\n' << + '\t' << "PRIVATE_KEYS_DIR = " << config.get_private_keys_dir() << '\n' << + '\t' << "PRIVATE_KEY_NAME = " << config.get_private_key_name() << '\n' << + '}'; +} + +static auto get_peer(InteractiveState &state, std::size_t index, bool include_pending = false, bool include_connected = true) -> std::shared_ptr { + auto pending_peers = state.server.get_pending_peers(); + auto peers = state.server.get_peers(); + std::size_t total_peers = (include_pending ? pending_peers.size() : 0) + (include_connected ? peers.size() : 0); + std::string_view peer_prefix = include_pending && !include_connected ? " pending" : ( + !include_pending && include_connected ? " connected" : "" + ); + + if (index <= pending_peers.size()) { + auto peer = *std::next(pending_peers.begin(), static_cast(index - 1)); + + return peer.second; + } + if (index - pending_peers.size() <= peers.size()) { + auto peer = *std::next(peers.begin(), static_cast(index - pending_peers.size() - 1)); + + return peer.second; + } + + std::cerr << "Cannot find peer number " << index << " - there is only " << total_peers << peer_prefix << " peers" << std::endl; + return nullptr; +} + +// TODO: Check for extra args and fail +// TODO: Simplify this mess of copy-pasted commands + +static void exit_command(InteractiveState &state, std::istream &args) { + // TODO: Prompt if pending transfers + server_run = false; +} + +static void server_command(InteractiveState &state, std::istream &args) { + FileShare::Utils::ci_string arg; + bool current_state = !state.server.disabled(); + bool new_state; + + args >> arg; + + if (arg.empty()) { + std::cout << "Server is currently " << (current_state ? "ON" : "OFF") << std::endl; + return; + } + + if (arg == "ON") { + new_state = true; + } else if (arg == "OFF") { + new_state = false; + } else { + std::cerr << "'" << arg << "' is not a valid server status. Please input either ON or OFF." << std::endl; + return; + } + + if (new_state == current_state) { + std::cout << "Server is already " << (current_state ? "ON" : "OFF") << std::endl; + return; + } + state.server.set_disabled(!new_state); + std::cout << "Server is now " << (new_state ? "ON" : "OFF") << std::endl; +} + +// TODO: FIXME: Solve this mess of copy-pasted config commands with https://isocpp.org/wiki/faq/pointers-to-members#functionoids +// Idea is to create a Functionoid that takes a std::shared_ptr, and specialise one of them for +// Config, and one for ServerConfig in the constructor. The interface of the functionoid would allow +// to call all the methods we need, and throw an error if the given method is not supported by that +// type of Config. +static void config_show_command(InteractiveState &state, std::string_view config, std::istream &args) { + if (config == "server") { + std::cout << state.server.get_config() << std::endl; + } else if (config == "default") { + std::cout << state.server.get_peer_config() << std::endl; + } else { + std::size_t index = 0; + auto result = std::from_chars(config.data(), config.data() + config.size(), index); + + if (result.ec == std::errc()) { + auto peer = std::dynamic_pointer_cast(get_peer(state, index)); + + if (peer) { + std::cout << peer->get_config() << std::endl; + } + } else { + std::cerr << "Please input a valid Peer ID" << std::endl; + } + } +} + +static void config_set_command(InteractiveState &state, std::string_view config, std::istream &args) { + using ConfigSetter = std::function; + using ConfigSetterMap = std::unordered_map; + static const ConfigSetterMap setter_map = { + {"device_name", &FileShare::ServerConfig::set_device_name}, + {"private_keys_dir", &FileShare::ServerConfig::set_private_keys_dir}, + {"private_key_name", &FileShare::ServerConfig::set_private_key_name} + }; + + std::string name; + std::string value; + + args >> name; + args >> value; // TODO: Currently doesn't accept spaces in value. Look into std::quoted for that maybe ? + + lowercase(name); + + if (config == "server") { + if (name == "device_uuid") { + std::cerr << "DEVICE_UUID is read-only." << std::endl; + return; + } + auto fun = setter_map.find(name); + + if (fun == setter_map.end()) { + std::cerr << "Unknown Config property " << std::quoted(name, '\'') << std::endl; + return; + } + (*fun).second(&state.server.get_config(), value); + return state.server.restart(); + } + + if (name != "download_folder") { + std::cerr << "Unkown Config property " << std::quoted(name, '\'') << std::endl; + return; + } + + if (config == "default") { + state.server.get_peer_config().set_downloads_folder(value); + } else { + std::size_t index = 0; + auto result = std::from_chars(config.data(), config.data() + config.size(), index); + + if (result.ec == std::errc()) { + auto peer = std::dynamic_pointer_cast(get_peer(state, index)); + + if (peer) { + peer->get_config().set_downloads_folder(value); + } + } else { + std::cerr << "Please input a valid Peer ID" << std::endl; + } + } +} + +static void config_save_command(InteractiveState &state, std::string_view config, std::istream &args) { + std::string path; + + args >> path; + + if (config == "server") { + state.server.get_config().save(path); + } else if (config == "default") { + state.server.get_peer_config().save(path); + } else { + std::size_t index = 0; + auto result = std::from_chars(config.data(), config.data() + config.size(), index); + + if (result.ec == std::errc()) { + auto peer = std::dynamic_pointer_cast(get_peer(state, index)); + + if (peer) { + peer->get_config().save(path); + } + } else { + std::cerr << "Please input a valid Peer ID" << std::endl; + } + } + +} + +static void config_load_command(InteractiveState &state, std::string_view config, std::istream &args) { + std::cerr << "TODO" << std::endl; +} + +static void config_command(InteractiveState &state, std::istream &args) { + static const ConfigCommandMap commands_map = { + {"show", &config_show_command}, + {"set", &config_set_command}, + {"save", &config_save_command}, + {"load", &config_load_command} + }; + + std::string config; + std::string subcommand; + + args >> config; + args >> subcommand; + + lowercase(config); + lowercase(subcommand); + + if (config.empty()) { + std::cerr << "Please select the type of Config you would like to operate on." << std::endl; + return; + } + if (subcommand.empty()) { + subcommand = "show"; + } + auto fun = commands_map.find(subcommand); + + if (fun == commands_map.end()) { + std::cerr << "Unknown Config Subcommand " << std::quoted(subcommand, '\'') << std::endl; + return; + } + return (*fun).second(state, config, args); +} + +static void peer_list_command(InteractiveState &state, bool include_pending = false, bool include_connected = true) { + auto peers = state.server.get_peers(); + auto pending_peers = state.server.get_pending_peers(); + + std::size_t total_peers = (include_pending ? pending_peers.size() : 0) + (include_connected ? peers.size() : 0); + std::size_t idx = 1; + + std::cout << "Currently connected Peers : " << total_peers << '\n'; + if (include_pending) { + for (const auto &peer : pending_peers) { + // TODO: Display more stuff about peer + std::cout << "\t" << idx << ". PENDING_ACCEPT - " << peer.second->get_device_uuid() << '\n'; + idx++; + } + } + + if (include_connected) { + for (const auto &peer : peers) { + // TODO: Display more stuff about peer + std::cout << "\t" << idx << ". - " << peer.second->get_device_uuid() << '\n'; + idx++; + } + } + + std::cout << std::flush; +} + +static void peer_connect_command(InteractiveState &state, std::istream &args) { + std::string ip; + std::uint16_t port = 0; + + args >> ip; + if (ip.empty()) { + std::cerr << "Missing required argument " << std::endl; + return; + } + if (!args.eof()) { + args >> std::ws; + } + if (args.eof()) { + port = 12345; + } else { + args >> port; + } + if (args.fail() || port == 0) { + std::cerr << "Please input a valid port number" << std::endl; + return; + } + state.server.connect(CppSockets::EndpointV4(ip.c_str(), port)); +} + +static void peer_accept_command(InteractiveState &state, std::istream &args) { + std::size_t index = 0; + auto pending_peers = state.server.get_pending_peers(); + + if (!args.eof()) { + args >> std::ws; + } + if (args.eof()) { + state.state = InteractiveStateMachine::ACCEPT_MENU; + peer_list_command(state, true, false); + return; + } + args >> index; + if (args.fail() || index == 0) { + std::cerr << "Please input a valid number" << std::endl; + return; + } + auto peer = std::dynamic_pointer_cast(get_peer(state, index, true, false)); + + if (peer) { + state.server.accept_peer(peer); + } +} + +static void peer_disconnect_command(InteractiveState &state, std::istream &args) { + std::size_t index = 0; + + if (!args.eof()) { + args >> std::ws; + } + if (args.eof()) { + state.state = InteractiveStateMachine::DISCONNECT_MENU; + peer_list_command(state, true); + return; + } + args >> index; + if (args.fail() || index == 0) { + std::cerr << "Please input a valid number" << std::endl; + return; + } + auto peer = get_peer(state, index, true, false); + + if (peer) { + peer->disconnect(); + } +} + +static void peer_select_command(InteractiveState &state, std::istream &args) { + peer_list_command(state); + + // TODO: Need to disable it if it disconnects -> We need events for that too + + // TODO: Activate Peer ID (De-Activate current peer if executed twice.) + // Also displays currently active peer if any, and mentions the De-Activation feature in prompt +} + +static void peer_command(InteractiveState &state, std::istream &args) { + static const CommandMap commands_map = { + {"list", [](InteractiveState &state, std::istream &){ peer_list_command(state, true); }}, + {"connect", &peer_connect_command}, + {"disconnect", &peer_disconnect_command}, + {"accept", &peer_accept_command}, + {"select", &peer_select_command} + }; + + std::string subcommand; + + args >> subcommand; + lowercase(subcommand); + + auto fun = commands_map.find(subcommand); + + if (fun == commands_map.end()) { + std::cerr << "Unknown Peer Subcommand " << std::quoted(subcommand, '\'') << std::endl; + return; + } + return (*fun).second(state, args); +} + +static void execute_list_files_command(InteractiveState &state, std::istream &args) { + auto selected_peer = state.selected_peer.lock(); + FileShare::Protocol::Response> result; + std::string path; + + args >> path; + if (selected_peer) { + result = selected_peer->list_files(path); + } else { + auto peers = state.server.get_peers(); + + if (peers.size() > 1) { + state.state = InteractiveStateMachine::EXECUTE_MENU; + // TODO: Store command state (args) to be re-executed when we get the peer + return; + } + if (peers.size() == 1) { + result = peers.begin()->second->list_files(path); + } else { + std::cerr << "No Connected Peers" << std::endl; + return; + } + } + std::cout << "Listing files in " << std::quoted(path) << " - got " << result.code << ". " << + result.response->size() << " Files : " << '\n'; + for (const auto &file : *result.response) { + std::cout << '\t' << file.file_type << ": " << file.path << '\n'; + } +} + +static void execute_receive_file_command(InteractiveState &state, std::istream &args) { + auto selected_peer = state.selected_peer.lock(); + std::string path; + FileShare::Protocol::Response result; + + args >> path; + if (path.empty()) { + std::cerr << "The filepath of the file to receive is required" << std::endl; + return; + } + + if (selected_peer) { + result = selected_peer->receive_file(path); + } else { + auto peers = state.server.get_peers(); + + if (peers.size() > 1) { + state.state = InteractiveStateMachine::EXECUTE_MENU; + // TODO: Store command state (args) to be re-executed when we get the peer + return; + } + if (peers.size() == 1) { + result = peers.begin()->second->receive_file(path); + } else { + std::cerr << "No Connected Peers" << std::endl; + return; + } + } + std::cout << "Receive File status: " << result.code << std::endl; +} + +static void execute_send_file_command(InteractiveState &state, std::istream &args) { + auto selected_peer = state.selected_peer.lock(); + std::string path; + FileShare::Protocol::Response result; + + args >> path; + if (path.empty()) { + std::cerr << "The filepath of the file to send is required" << std::endl; + return; + } + path = FileShare::Utils::resolve_home_component(path); + + if (selected_peer) { + result = selected_peer->send_file(path); + } else { + auto peers = state.server.get_peers(); + + if (peers.size() > 1) { + state.state = InteractiveStateMachine::EXECUTE_MENU; + // TODO: Store command state (args) to be re-executed when we get the peer + return; + } + if (peers.size() == 1) { + result = peers.begin()->second->send_file(path); + } else { + std::cerr << "No Connected Peers" << std::endl; + return; + } + } + std::cout << "Send File status: " << result.code << std::endl; +} + +static void execute_command(InteractiveState &state, std::istream &args) { + // TODO: Execute CMD ARG1 [ARG2...]; If multiple clients connected, will need to prompt for select. + static const CommandMap commands_map = { + {"list_files", &execute_list_files_command}, + {"receive_file", &execute_receive_file_command}, + {"send_file", &execute_send_file_command} + }; + + std::string subcommand; + + args >> subcommand; + lowercase(subcommand); + + auto fun = commands_map.find(subcommand); + + if (fun == commands_map.end()) { + std::cerr << "Unknown Execute Subcommand " << std::quoted(subcommand, '\'') << std::endl; + return; + } + return (*fun).second(state, args); +} + +static void transfers_command(InteractiveState &state, std::istream &args) { + // TODO: List Transfers +} + +static void handle_main_menu(InteractiveState &state, const std::string &input) { + static const CommandMap commands_map = { + {"help", [](InteractiveState &, std::istream &){ display_interactive_help(); }}, + {"exit", &exit_command}, + {"quit", &exit_command}, + {"server", &server_command}, + {"config", &config_command}, + {"peer", &peer_command}, + {"execute", &execute_command}, + {"transfers", &transfers_command} + }; + + std::stringstream ss(input); + std::string cmd; + + ss >> cmd; + lowercase(cmd); + + if (cmd == "?") { + cmd = "help"; + } + + auto fun = commands_map.find(cmd); + + if (fun == commands_map.end()) { + std::cerr << "Unknown Command " << std::quoted(input, '\'') << std::endl; + std::cerr << "Type HELP or '?' for help." << std::endl; + return; + } + + return (*fun).second(state, ss); +} + +static void handle_interactive_input(InteractiveState &state, const std::string &input) { + switch (state.state) { + case InteractiveStateMachine::MAIN_MENU: + return handle_main_menu(state, input); + case InteractiveStateMachine::EXIT_MENU: + case InteractiveStateMachine::CONFIG_MENU: + case InteractiveStateMachine::DISCONNECT_MENU: + case InteractiveStateMachine::ACCEPT_MENU: + case InteractiveStateMachine::SELECT_MENU: + case InteractiveStateMachine::EXECUTE_MENU: + case InteractiveStateMachine::TRANSFERS_MENU: + std::cout << "TODO - OTHER STATES" << std::endl; + state.state = InteractiveStateMachine::MAIN_MENU; + return; + } +} + +// TODO: Pretty Colors +static void display_prompt(InteractiveState &state, bool &should_prompt) { + if (!should_prompt) { + return; + } + std::stringstream ss; + bool empty = true; + + std::size_t pending_requests = state.pending_events.size(); + std::size_t pending_peers = state.server.get_pending_peers().size(); + + // TODO: Display any event that happened since last prompt (eg: Peer Disconnected, New Requests) + if (pending_peers > 0) { + ss << "Pending Connections: " << pending_peers; + empty = false; + } + + if (pending_requests > 0) { + ss << (empty ? "" : ", ") << "Pending Requests: " << pending_requests; + empty = false; + + std::cout << "TODO: REMOVE - Accepting all pending requests" << std::endl; + for (auto &event : state.pending_events) { + if (event.REQUEST) { + auto peer = std::dynamic_pointer_cast(event.peer()); + + peer->respond_to_request(event.request().value(), FileShare::Protocol::StatusCode::STATUS_OK); + } + } + state.pending_events.clear(); + } + std::cout << ss.str() << (empty ? "" : ".\n"); + + std::cout << "(fs-cli) "; + if (state.state != InteractiveStateMachine::MAIN_MENU) { + std::cout << state.state << " "; + } + std::cout << "> " << std::flush; + should_prompt = false; +} + +void interactive_mode(FileShare::Server &server) { + InteractiveState state(server); + + FileShare::Server::PeerAcceptCallback accept_cb = [&](FileShare::Server &, std::shared_ptr &) { + return false; // We will handle acceptation ourselves + }; + FileShare::Server::PeerRequestCallback request_cb = [&](FileShare::Server &, std::shared_ptr &client, FileShare::Protocol::Request &request) { + state.pending_events.emplace_back(FileShare::Server::Event::REQUEST, client, request); + }; + std::array read_buff = {0}; + struct timespec timeout = {1, 0}; // 1s + std::vector fds; + + fds.emplace_back(pollfd({server.get_socket().get_fd(), POLLIN, 0})); + struct pollfd *stdin_fd = &fds.emplace_back(pollfd({STDIN_FILENO, POLLIN, 0})); + bool should_prompt = true; + int nb_ready = 0; + + std::signal(SIGINT, signal_handler); + + std::cout << "Welcome to the FileShare CLI !" << "\n\n"; + // TODO: Find more stuff to say on startup + // TODO: Would be nice if `?` works without \n + std::cout << "Enter Command (type HELP or '?' for help): " << "\n"; + + server_run = true; + while (server_run) { + display_prompt(state, should_prompt); + + // TODO: Avoid having 2 polls (here + server) + nb_ready = FileShare::Utils::poll(fds, &timeout); + + if (nb_ready == 0) { + continue; + } + // if (nb_ready < 0) { + // // TODO handle Signals or Regular Errors + // } + if (stdin_fd->revents & (POLLIN | POLLHUP)) { + nb_ready--; + if (stdin_fd->revents & (POLLIN)) { + std::size_t nb = ::read(STDIN_FILENO, read_buff.data(), read_buff.size()); + std::string buffer = std::string(read_buff.data(), nb); + + if (buffer.empty()) { // CTRL+D -> Quitting + buffer = "EXIT"; + std::cout << buffer; + } + + should_prompt = true; + if (buffer.find('\n') == std::string::npos) { + std::cout << std::endl; + } + std::stringstream ss(buffer); + do { + std::string input; + + std::getline(ss, input); + if (!input.empty()) { + handle_interactive_input(state, input); + } + } while(ss); + } else { + std::cout << "PULLUP -> Quitting..." << std::endl; + break; + } + } + if (nb_ready > 0) { + server.process_events(accept_cb, request_cb); // Server poll + handle events + + // TODO: Smth happened on server -> \r and refresh screen + } + + // Peers could have connected / disconnected, need to refresh FDs + // TODO: Find a better way. + // Currently outside of if (nb_ready) because we need to run it after PEER CONNECT + fds.clear(); + fds.reserve(server.get_poll_fds().size() + 1); + for (auto poll_fd : server.get_poll_fds()) { + fds.emplace_back(poll_fd); + } + stdin_fd = &fds.emplace_back(pollfd({.fd = STDIN_FILENO, .events = POLLIN, .revents = 0})); + } +} diff --git a/source/cli/main.cpp b/source/cli/main.cpp index 5665b25..28e3854 100644 --- a/source/cli/main.cpp +++ b/source/cli/main.cpp @@ -4,38 +4,50 @@ ** Author Francois Michaut ** ** Started on Sat Nov 11 11:06:03 2023 Francois Michaut -** Last update Thu Nov 23 23:19:41 2023 Francois Michaut +** Last update Fri Aug 22 17:19:40 2025 Francois Michaut ** ** main.cpp : Main entry point of FileShare CLI */ #include "FileShareVersion.hpp" -#include "FileShare/Server.hpp" + +#include +#include +#include #include #include -#include #include +#include -#define DESCRIPTION "" +#define DESCRIPTION "" // TODO #define SERVER_ARG "--server" #define CONNECT_ARG "--connect" #define INTERACTIVE_ARG "--interactive" #define EXECUTE_ARG "--execute" +#define CONFIG_ARG "--config" +#define SERVER_CONFIG_ARG "--server-config" -#include +void interactive_mode(FileShare::Server &server); -static std::shared_ptr setup_args() -{ - std::shared_ptr parser = std::make_shared("file-share-cli", FILE_SHARE_VERSION); +static auto setup_args() -> std::shared_ptr { + auto parser = std::make_shared("file-share-cli", FILE_SHARE_VERSION); parser->add_description(DESCRIPTION); parser->add_argument("-s", SERVER_ARG) .flag() - .help("Enable server, allowing other clients to connect. The program will keep running until killed"); + .help("Enable server, allowing other peers to connect. The program will keep running until killed."); + + parser->add_argument(SERVER_CONFIG_ARG) + .default_value("") + .help("Speficy the path to the server config file."); + + parser->add_argument(CONFIG_ARG) + .default_value("") + .help("Speficy the path to the peer config file."); parser->add_argument("-c", CONNECT_ARG) .nargs(1, 2) @@ -43,16 +55,18 @@ static std::shared_ptr setup_args() parser->add_argument("-e", EXECUTE_ARG) .append() - .help("Add a command to be executed. Repeat the flag to add multiple commands"); + .help("Add a command to be executed. Repeat the flag to add multiple commands."); parser->add_argument("-i", INTERACTIVE_ARG) .flag() - .help("Enable interactive mode. You will be able to change settings and execute commands at will"); + .help("Enable interactive mode. You will be able to change settings and execute commands from a CLI."); + + // parser->add_epilog(); // TODO ? + return parser; } -static void execute_command(FileShare::Server &server, std::shared_ptr &client, std::string cmd) -{ +static void execute_command(std::shared_ptr &peer, const std::string& cmd) { std::stringstream ss(cmd); std::string str; @@ -63,96 +77,105 @@ static void execute_command(FileShare::Server &server, std::shared_ptrsend_file(str); + peer->send_file(str); break; } case FileShare::Protocol::CommandCode::RECEIVE_FILE: { - client->receive_file(str); + peer->receive_file(str); break; } case FileShare::Protocol::CommandCode::LIST_FILES: { - client->list_files(str); + peer->list_files(str); break; } + // case FileShare::Protocol::CommandCode::PING: { + // } default: std::cout << "Failed to run " << command << ". This type of command is not allowed." << std::endl; } } -static bool server_run = true; -static void signal_handler(int sig) { +bool server_run = true; +void signal_handler([[maybe_unused]] int sig) { server_run = false; } // TODO: replace this testing loop with something better static void run_server(FileShare::Server &server) { - FileShare::Server::Event e; + static const FileShare::Server::PeerAcceptCallback accept_cb = [](FileShare::Server &, std::shared_ptr &) { + // We accept everyone + return true; + }; + static const FileShare::Server::PeerRequestCallback request_cb = [](FileShare::Server &, std::shared_ptr &peer, FileShare::Protocol::Request &request){ + if (request.code == FileShare::Protocol::CommandCode::SEND_FILE || request.code == FileShare::Protocol::CommandCode::RECEIVE_FILE) { + // We accept the file transfert + peer->respond_to_request(request, FileShare::Protocol::StatusCode::STATUS_OK); + } else { // (Ping) + // Denying theses requests + peer->respond_to_request(request, FileShare::Protocol::StatusCode::FORBIDDEN); + } + }; std::signal(SIGINT, signal_handler); server_run = true; while (server_run) { - if (server.pull_event(e)) { - if (e.request().has_value()) { - auto &request = e.request().value(); - if (request.code == FileShare::Protocol::CommandCode::SEND_FILE || request.code == FileShare::Protocol::CommandCode::RECEIVE_FILE) { - // We accept the file transfert - e.client()->respond_to_request(request, FileShare::Protocol::StatusCode::STATUS_OK); - } else { - // Denying theses requests - e.client()->respond_to_request(request, FileShare::Protocol::StatusCode::FORBIDDEN); - } - } else { - // We accept everyone - server.accept_client(std::move(e.client())); - } - } + server.process_events(accept_cb, request_cb); } } -static void run(std::shared_ptr &parser) -{ - FileShare::Config config = FileShare::Server::default_config(); +static void run(std::shared_ptr &parser) { + auto server_config_path = parser->get(SERVER_CONFIG_ARG); + auto config_path = parser->get(CONFIG_ARG); auto cmds = parser->get>(EXECUTE_ARG); + FileShare::ServerConfig config = FileShare::ServerConfig::load(server_config_path); + FileShare::Config peer_config = FileShare::Config::load(config_path); + config.set_server_disabled(!parser->get(SERVER_ARG)); - FileShare::Server server(config); + FileShare::Server server(config, peer_config); if (parser->is_used(CONNECT_ARG)) { - std::vector endpoint = parser->get>(CONNECT_ARG); + auto endpoint = parser->get>(CONNECT_ARG); int port = 12345; if (endpoint.size() == 2) { port = std::stoi(endpoint[1]); } - std::shared_ptr client = server.connect(CppSockets::EndpointV4(endpoint[0].c_str(), port)); + std::shared_ptr peer = server.connect(CppSockets::EndpointV4(endpoint[0].c_str(), port)); for (auto &cmd : cmds) { - execute_command(server, client, cmd); + try { + execute_command(peer, cmd); + } catch (const std::exception &err) { + std::cerr << "Failed to execute command: '" << cmd << "': " << err.what() << std::endl; + } } } if (parser->get(INTERACTIVE_ARG)) { - // TODO: interactive mode + interactive_mode(server); } else if (!server.disabled()) { - std::cout << "Running Server: Listening on " << server.get_server_endpoint().toString() << " (Press CTRL+C to stop)" << std::endl; + std::cout << "Running Server: Listening on " << server.get_server_endpoint().to_string() << " (Press CTRL+C to stop)" << std::endl; run_server(server); } } -int main(int argc, char *argv[]) -{ +auto main(int argc, char *argv[]) -> int { std::shared_ptr parser = setup_args(); try { parser->parse_args(argc, argv); if (parser->is_used(EXECUTE_ARG) && !parser->is_used(CONNECT_ARG)) { - throw std::runtime_error(EXECUTE_ARG " cannot be used without a peer to connect to. Use " CONNECT_ARG " to provide it."); + throw std::runtime_error( + EXECUTE_ARG " cannot be used without a peer to connect to. Use " + CONNECT_ARG " to provide it." + ); } } catch (const std::runtime_error &err) { std::cerr << "Argument error: " << err.what() << std::endl; std::cerr << std::endl; std::cerr << "Try -h for more information" << std::endl; - std::exit(1); + return 1; } run(parser); diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 5d68946..5258421 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -4,17 +4,11 @@ ## Author Francois Michaut ## ## Started on Mon Feb 14 19:35:41 2022 Francois Michaut -## Last update Wed May 10 09:47:05 2023 Francois Michaut +## Last update Sun Aug 24 18:50:34 2025 Francois Michaut ## ## CMakeLists.txt : CMake building and running tests for FileShare ## -cmake_minimum_required (VERSION 3.15) -set(CMAKE_CXX_STANDARD 20) -set(CMAKE_CXX_STANDARD_REQUIRED True) - -project(FileShare-Tests VERSION 0.1.0 LANGUAGES C CXX) - include(CTest) # create_test_sourcelist(TestFiles test_driver.cpp @@ -24,8 +18,16 @@ include(CTest) # ${TestFiles} # ) +# target_compile_definitions(unit_tests PRIVATE DEBUG) +# target_link_libraries(unit_tests fsp) + +# target_compile_options(unit_tests PRIVATE +# $<$,$,$>:-UNDEBUG> +# $<$:/UNDEBUG> +# ) + foreach (test ${TestFiles}) - if (NOT ${test} STREQUAL test_driver.cpp) + if (NOT ${test} MATCHES "test_driver.cpp$") get_filename_component (DName ${test} DIRECTORY) get_filename_component (TName ${test} NAME_WE) if (DName STREQUAL "") @@ -35,15 +37,3 @@ foreach (test ${TestFiles}) endif() endif() endforeach () - -file(GLOB LIB_DIRECTORIES "${CMAKE_CURRENT_BINARY_DIR}/lib/*") -foreach (LIB_DIRECTORY IN LISTS LIB_DIRECTORIES) - cmake_path(GET LIB_DIRECTORY FILENAME LIB_NAME) - add_test (NAME ${LIB_NAME}_tests COMMAND ${CMAKE_MAKE_PROGRAM} -C "${LIB_DIRECTORY}" test) -endforeach() - -include_directories( - "${CMAKE_CURRENT_BINARY_DIR}" - "${CMAKE_CURRENT_SOURCE_DIR}" - "${CMAKE_CURRENT_SOURCE_DIR}/../include" -)