diff --git a/.github/workflows/L1-Test.yaml b/.github/workflows/L1-Test.yaml new file mode 100644 index 0000000..951bdc3 --- /dev/null +++ b/.github/workflows/L1-Test.yaml @@ -0,0 +1,70 @@ +name: L1 Testing & Coverage + +on: + pull_request: + branches: [ main, develop ] + push: + branches: [ main, develop ] + +env: + CARGO_TERM_COLOR: always + RUST_BACKTRACE: 1 + +jobs: + test-and-coverage: + name: Test & Coverage + runs-on: ubuntu-latest + container: + image: ghcr.io/rdkcentral/docker-device-mgt-service-test/native-platform:latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Install Rust Toolchain components + run: | + if ! command -v cargo &> /dev/null; then + curl https://sh.rustup.rs -sSf | sh -s -- -y + export PATH="$HOME/.cargo/bin:$PATH" + fi + rustup update stable + rustup default stable + rustup component add llvm-tools-preview + + - name: Add Rust to PATH + run: echo "$HOME/.cargo/bin" >> $GITHUB_PATH + + - name: Install cargo-llvm-cov + run: cargo install cargo-llvm-cov + + - name: Install additional dependencies + run: | + apt-get update && apt-get install -y git pkg-config libssl-dev || true + + - name: Run all tests + run: cargo test --all-features --workspace --verbose + + - name: Run tests with llvm-cov for coverage + run: | + mkdir -p ./coverage + # Generate both XML and summary report + cargo llvm-cov --all-features --workspace --cobertura --output-path ./coverage/cobertura.xml + # Print coverage summary to console + cargo llvm-cov --all-features --workspace --summary-only + + # - name: Upload coverage to Codecov + # uses: codecov/codecov-action@v3 + # with: + # file: ./coverage/cobertura.xml + # flags: unittests + # name: codecov-umbrella + # fail_ci_if_error: false + + # - name: Archive code coverage results + # uses: actions/upload-artifact@v4 + # with: + # name: code-coverage-report + # path: coverage/ + + - name: Clean target directory (ensure it's not persisted) + run: rm -rf target/ diff --git a/.github/workflows/native_full_build.yml b/.github/workflows/native_full_build.yml new file mode 100644 index 0000000..23b9bf7 --- /dev/null +++ b/.github/workflows/native_full_build.yml @@ -0,0 +1,52 @@ +name: Build Crash Upload Component in Native Environment + +on: + pull_request: + branches: [ main, develop ] + push: + branches: [ main, develop ] + +env: + CARGO_TERM_COLOR: always + RUST_BACKTRACE: 1 + +jobs: + build: + name: Build Check + runs-on: ubuntu-latest + container: + image: ghcr.io/rdkcentral/docker-device-mgt-service-test/native-platform:latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Install Rust Toolchain components + run: | + if ! command -v cargo &> /dev/null; then + curl https://sh.rustup.rs -sSf | sh -s -- -y + export PATH="$HOME/.cargo/bin:$PATH" + fi + rustup update stable + rustup default stable + + - name: Add Rust to PATH + run: echo "$HOME/.cargo/bin" >> $GITHUB_PATH + + - name: Install additional components + run: rustup component add clippy + + - name: Run cargo check + run: cargo check --all-targets --all-features + + - name: Run clippy (do not fail on warnings) + run: cargo clippy --all-targets --all-features -- -D warnings || true + + - name: Build all libraries and binaries (debug) + run: cargo build --all-targets --all-features + + - name: Build all libraries and binaries (release) + run: cargo build --release --all-targets --all-features + + - name: Clean target directory (ensure it's not persisted) + run: rm -rf target/ diff --git a/.github/workflows/release_docs.yaml b/.github/workflows/release_docs.yaml new file mode 100644 index 0000000..2dcdb69 --- /dev/null +++ b/.github/workflows/release_docs.yaml @@ -0,0 +1,175 @@ +name: Release Documentation + +#on: + push: + branches: [ main ] + release: + types: [ published ] + +env: + CARGO_TERM_COLOR: always + +permissions: + contents: write + pages: write + id-token: write + +jobs: + docs: + name: Generate and Deploy Documentation + runs-on: ubuntu-latest + container: + image: rust:latest + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + token: ${{ secrets.GITHUB_TOKEN }} + + - name: Install dependencies + run: | + apt-get update && apt-get install -y git pkg-config libssl-dev + cargo install cargo-readme + + - name: Generate documentation + run: | + cargo doc --all-features --no-deps --workspace + echo '' > target/doc/index.html + + - name: Copy additional files to docs + run: | + cp README.md target/doc/ 2>/dev/null || true + cp LICENSE* target/doc/ 2>/dev/null || true + + - name: Setup Pages + uses: actions/configure-pages@v4 + + - name: Upload artifact + uses: actions/upload-pages-artifact@v3 + with: + path: 'target/doc' + + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v4 + + - name: Generate README from lib.rs + run: | + # Configure git for commit + git config --global --add safe.directory /__w/*/ + git config --local user.email "action@github.com" + git config --local user.name "GitHub Action" + + # Find the main library crate and generate README + if [ -f "src/lib.rs" ]; then + cargo readme --input src/lib.rs --output README.md + elif find . -name "lib.rs" -path "*/src/lib.rs" | head -1 | xargs -I{} dirname {} | xargs -I{} dirname {} | head -1; then + # Handle workspace with multiple crates - use first one found + CRATE_DIR=$(find . -name "lib.rs" -path "*/src/lib.rs" | head -1 | xargs -I{} dirname {} | xargs -I{} dirname {}) + cd "$CRATE_DIR" + cargo readme --input src/lib.rs --output ../README.md + cd - > /dev/null + fi + + - name: Check if README changed and commit + run: | + if [ -f "README.md" ]; then + if ! git diff --quiet README.md 2>/dev/null; then + git add README.md + git commit -m "docs: auto-update README.md from lib.rs documentation" || echo "No changes to commit" + git push || echo "Nothing to push" + else + echo "README.md unchanged" + fi + else + echo "README.md not found" + fi + + - name: Clean target directory (ensure it's not persisted) + run: rm -rf target/ }} + + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + token: ${{ secrets.GITHUB_TOKEN }} + + - name: Install Rust toolchain + uses: dtolnay/rust-toolchain@stable + + - name: Cache cargo + uses: actions/cache@v4 + with: + path: | + ~/.cargo/registry/index/ + ~/.cargo/registry/cache/ + ~/.cargo/git/db/ + target/ + key: ${{ runner.os }}-cargo-docs-${{ hashFiles('**/Cargo.lock') }} + + - name: Install cargo-readme + run: cargo install cargo-readme + + - name: Generate documentation + run: | + cargo doc --all-features --no-deps --workspace + echo '' > target/doc/index.html + + - name: Copy additional files to docs + run: | + cp README.md target/doc/ || true + cp LICENSE* target/doc/ || true + + - name: Setup Pages + uses: actions/configure-pages@v4 + + - name: Upload artifact + uses: actions/upload-pages-artifact@v3 + with: + path: 'target/doc' + + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v4 + + - name: Generate README from lib.rs + run: | + # Find the main library crate + if [ -f "src/lib.rs" ]; then + cargo readme --input src/lib.rs --output README.md + elif [ -f "*/src/lib.rs" ]; then + # Handle workspace with multiple crates + for crate_dir in */; do + if [ -f "${crate_dir}src/lib.rs" ] && [ -f "${crate_dir}Cargo.toml" ]; then + cd "$crate_dir" + cargo readme --input src/lib.rs --output ../README.md + cd .. + break + fi + done + fi + + - name: Check if README changed + id: readme_check + run: | + if git diff --quiet README.md; then + echo "changed=false" >> $GITHUB_OUTPUT + else + echo "changed=true" >> $GITHUB_OUTPUT + fi + + - name: Commit updated README + if: steps.readme_check.outputs.changed == 'true' + run: | + git config --local user.email "action@github.com" + git config --local user.name "GitHub Action" + git add README.md + git commit -m "docs: auto-update README.md from lib.rs documentation" + git push + + - name: Clean target directory + run: cargo clean \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b83d222 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/target/ diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..7ae65eb --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,352 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "autocfg" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" + +[[package]] +name = "bumpalo" +version = "3.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "793db76d6187cd04dff33004d8e6c9cc4e05cd330500379d2394209271b4aeee" + +[[package]] +name = "cc" +version = "1.2.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "956a5e21988b87f372569b66183b78babf23ebc2e744b733e4350a752c4dafac" +dependencies = [ + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9555578bc9e57714c812a1f84e4fc5b4d21fcb063490c624de019f7464c91268" + +[[package]] +name = "chrono" +version = "0.4.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c469d952047f47f91b68d1cba3f10d63c11d73e4636f24f08daf0278abf01c4d" +dependencies = [ + "android-tzdata", + "iana-time-zone", + "js-sys", + "num-traits", + "wasm-bindgen", + "windows-link", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "crash-upload" +version = "0.1.0" +dependencies = [ + "chrono", + "curl-interface", + "json-parser-interface", + "platform-interface", + "utils", +] + +[[package]] +name = "crash-upload-test" +version = "0.1.0" +dependencies = [ + "crash-upload", +] + +[[package]] +name = "curl-interface" +version = "0.1.0" + +[[package]] +name = "curl-interface-test" +version = "0.1.0" +dependencies = [ + "curl-interface", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.63" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0c919e5debc312ad217002b8048a17b7d83f80703865bbfcfebb0458b0b27d8" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "js-sys" +version = "0.3.77" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "json-parser-interface" +version = "0.1.0" + +[[package]] +name = "json-parser-interface-test" +version = "0.1.0" +dependencies = [ + "json-parser-interface", +] + +[[package]] +name = "libc" +version = "0.2.172" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa" + +[[package]] +name = "log" +version = "0.4.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + +[[package]] +name = "platform-interface" +version = "0.1.0" + +[[package]] +name = "platform-interface-test" +version = "0.1.0" +dependencies = [ + "platform-interface", +] + +[[package]] +name = "proc-macro2" +version = "1.0.95" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rustversion" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a0d197bd2c9dc6e53b84da9556a69ba4cdfab8619eb41a8bd1cc2027a0f6b1d" + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "syn" +version = "2.0.102" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6397daf94fa90f058bd0fd88429dd9e5738999cca8d701813c80723add80462" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "unicode-ident" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" + +[[package]] +name = "utils" +version = "0.1.0" + +[[package]] +name = "utils-test" +version = "0.1.0" +dependencies = [ + "utils", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" +dependencies = [ + "bumpalo", + "log", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "windows-core" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-implement" +version = "0.60.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-interface" +version = "0.59.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-link" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3bfe459f85da17560875b8bf1423d6f113b7a87a5d942e7da0ac71be7c61f8b" + +[[package]] +name = "windows-result" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" +dependencies = [ + "windows-link", +] \ No newline at end of file diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..de5fc8e --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,18 @@ +[workspace] +members = [ + "crash-upload", + "crates/curl-interface", + "crates/json-parser-interface", + "crates/platform-interface", + "crates/utils", + "test/crash-upload-test", + "test/curl-interface-test", + "test/json-parser-interface-test", + "test/platform-interface-test", + "test/utils-test", +] + +[profile.release] +opt-level = "z" +codegen-units = 1 +lto = true diff --git a/TODO.md b/TODO.md new file mode 100644 index 0000000..6c1df62 --- /dev/null +++ b/TODO.md @@ -0,0 +1,44 @@ +TODO: +- Capture existing uploadDumps.sh and uploadDumpsToS3.sh functionality wise in detail +- Same process multiple crashes, Multiple process crashes (corner cases) +- Separate Logging log.rs, with WARN, INFO, ERROR, etc log levels +- get_sha1() -> using sha1 crate/ process::Command() if single use +- get_last_modified_time_of_file() -> using chrono crate/ process::Command() if single use +- ~~args parsing in main app~~ + +#### Function Implementations +---- +**COMPLETED** +- ~~setLogFile()~~ +- ~~create_lock_or_exit()~~ +- ~~create_lock_or_wait()~~ +- ~~remove_lock()~~ +- ~~sanitize()~~ +- ~~deleteAllButTheMostRecentFiles()~~ +- ~~cleanup()~~ +- ~~finalize()~~ +- ~~sigkill_function()~~ +- ~~sigterm_function()~~ +- ~~logUploadTimestamp()~~ +- ~~truncateTimeStampFile()~~ +- ~~isUploadLimitReached()~~ +- ~~setRecoveryTime()~~ +- ~~isRecoveryTimeReached()~~ +- ~~removePendingDumps()~~ +- ~~is_box_rebooting()~~ +- ~~shouldProcessFile()~~ +- ~~get_crashed_log_file()~~ +- ~~processCrashTelemtryInfo()~~ +- ~~add_crashed_log_file()~~ - optional args +- ~~copy_log_files_tmp_dir()~~ - optional args +- ~~processDumps()~~ - requires add_crashed_log_file() and copy_log_files_tmp_dir() +- ~~saveDump()~~ +- ~~markAsCrashLoopedAndUpload()~~ +- checkParameter() - NA +- logMessage() - Not req +- tlsLog() - Not req +- logStdout() - Not req +---- +**TODO** +- Signal Handling TRAPs +---- \ No newline at end of file diff --git a/coredump-upload.service b/coredump-upload.service index 25ca2f0..2e9fc18 100644 --- a/coredump-upload.service +++ b/coredump-upload.service @@ -26,4 +26,4 @@ Requires=network-online.target #Environment="TS=" #Type=oneshot #RemainAfterExit=yes -ExecStart=/bin/busybox sh -c '/lib/rdk/uploadDumps.sh "" 0 ' +ExecStart=/usr/bin/crash-upload 1 "" "" diff --git a/crash-upload/Cargo.toml b/crash-upload/Cargo.toml new file mode 100644 index 0000000..c984544 --- /dev/null +++ b/crash-upload/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "crash-upload" +version = "0.1.0" +edition = "2021" +description = "Crash dump uploader" +repository = "https://github.com/rdkcentral/crashupload/tree/feature-sample-rust/crash-upload" +homepage = "https://github.com/rdkcentral/crashupload/tree/feature-sample-rust/crash-upload" + +[dependencies] +# Internal crates +curl-interface = { path = "../crates/curl-interface" } +json-parser-interface = { path = "../crates/json-parser-interface" } +platform-interface = { path = "../crates/platform-interface" } +utils = { path = "../crates/utils" } +chrono = "0.4" diff --git a/crash-upload/src/constants.rs b/crash-upload/src/constants.rs new file mode 100644 index 0000000..17eba72 --- /dev/null +++ b/crash-upload/src/constants.rs @@ -0,0 +1,230 @@ +//! Crash upload system constants and configuration structures. +//! +//! This module defines constants and configuration structs used throughout the crash upload system, +//! including file paths, RFC names, and device/dump metadata. +//! +//! ## Constants Categories +//! +//! * **Log Files & Paths**: Constants for log file locations and related configurations +//! * **Property Files**: Paths to device and system property files +//! * **Feature Toggle Files**: Files that enable/disable certain features like secure dump +//! * **Network & System Time**: Constants related to network availability and system time checks +//! * **TR-181 Parameters**: RFC parameter paths for various configuration options +//! +//! ## Configuration Structures +//! +//! * [`DumpPaths`]: Holds paths and file extensions for dump processing operations +//! * [`DeviceData`]: Contains device-specific metadata like MAC address, model number, etc. + +/// Path to the log mapper configuration file. +pub const LOGMAPPER_FILE: &str = "/etc/breakpad-logmapper.conf"; +/// Path to the file listing minidump log files. +pub const LOG_FILES: &str = "/tmp/minidump_log_files.txt"; + +/// Base path for RDK logs. +pub const LOG_PATH: &str = "/opt/rdk"; +/// Path to the core dump log file. +pub const CORE_LOG: &str = "/opt/logs/core_log.txt"; + +/// Path to the device properties file. +pub const DEVICE_PROP_FILE: &str = "/etc/device.properties"; + +/// Path to the Telemetry2 shared script. +pub const T2_SHARED_SCRIPT: &str = "/lib/rdk/t2Shared_api.sh"; +/// Maximum number of core files to process. +pub const MAX_CORE_FILES: usize = 4; +/// Path to the script for uploading dumps to S3. +pub const S3_UPLOAD_SCRIPT: &str = "/lib/rdk/uploadDumpsToS3.sh"; + +/// TR-181 parameter name for secure dump feature toggle. +pub const SECUREDUMP_TR181_NAME: &str = "Device.DeviceInfo.X_RDKCENTRAL-COM_RFC.Feature.SecDump.Enable"; +/// File indicating secure dump is enabled. +pub const SECUREDUMP_ENABLE_FILE: &str = "/tmp/.SecureDumpEnable"; +/// File indicating secure dump is disabled. +pub const SECUREDUMP_DISABLE_FILE: &str = "/tmp/.SecureDumpDisable"; +/// Flag file to trigger reboot after crash upload. +pub const CRASH_UPLOAD_REBOOT_FLAG: &str = "/tmp/set_crash_reboot_flag"; +/// File containing timestamp until which dump uploads are denied. +pub const DENY_UPLOAD_FILE: &str = "/tmp/.deny_dump_uploads_till"; +/// File containing parameters for S3 uploads. +pub const S3_UPLOAD_PARAM_FILE: &str = "/tmp/uploadtos3params"; + +/// Flag file to indicate crash uploads should happen on startup. +pub const UPLOAD_ON_STARTUP: &str = "/opt/.upload_on_startup"; +/// Base path for file indicating startup dumps were cleaned up. +pub const ON_STARTUP_DUMPS_CLEANED_UP_BASE: &str = "/tmp/.on_startup_dumps_cleaned_up"; +/// File to flag crash loop detection (TODO: implement). +pub const CRASH_LOOP_FLAG_FILE: &str = ""; // TODO + +/// File used for coredump mutex coordination. +pub const COREDUMP_MTX_FILE: &str = "/tmp/coredump_mutex_release"; + +/// File indicating network route availability. +pub const NETWORK_FILE: &str = "/tmp/route_available"; +/// File indicating system time has been received. +pub const SYSTEM_TIME_FILE: &str = "/tmp/stt_received"; +/// Number of iterations for network availability check. +pub const NETWORK_CHECK_ITERATION: usize = 18; +/// Timeout in seconds between network check iterations. +pub const NETWORK_CHECK_TIMEOUT: usize = 10; +/// Number of iterations for system time check. +pub const SYSTEM_TIME_ITERATION: usize = 10; +/// Timeout in seconds between system time check iterations. +pub const SYSTEM_TIME_TIMEOUT: usize = 1; + +pub const ENCRYPTION_RFC: &str = "Device.DeviceInfo.X_RDKCENTRAL-COM_RFC.CrashUpload.encryptionEnabled"; +pub const CRASH_PORTAL_URL_RFC: &str = "Device.DeviceInfo.X_RDKCENTRAL-COM_RFC.CrashUpload.crashPortalSTBUrl"; +pub const ENABLE_OSCP_STAPLING: &str = "/tmp/.EnableOCSPStapling"; +pub const ENABLE_OSCP: &str = "/tmp/.EnableOCSPCA"; + +/* UNUSED +pub const INCLUDE_PROP_FILE: &str = "/opt/include.properties"; +pub const COREDUMP_PROP_FILE: &str = "/opt/coredump.properties"; +pub const HTTP_CODE_FILE: &str = "/tmp/httpcode"; +pub const CURL_UPLOAD_TIMEOUT: usize = 45; +pub const S3_FILENAME: &str = "s3filename"; + +pub const POTOMAC_USER: &str = "ccpstbscp"; +pub const SHA1_DEFAULT_VALUE: &str = "0000000000000000000000000000000000000000"; +pub const TIMESTAMP_DEFAULT_VALUE: &str = "2000-01-01-00-00-00"; +pub const MAC_DEFAULT_VALUE: &str = "000000000000"; +pub const MODEL_NUM_DEFAULT_VALUE: &str = "UNKNOWN"; +*/ + +/// Holds all relevant paths and extensions for dump processing. +/// +/// This structure stores all the path information needed for processing crash dumps, +/// including source and destination directories, file extensions, and working directories. +#[derive(Debug)] +pub struct DumpPaths { + pub dump_name: String, + pub core_path: String, + pub minidumps_path: String, + pub core_back_path: String, + pub persistent_sec_path: String, + pub working_dir: String, + pub dumps_extn: String, + pub tar_extn: String, + pub lock_dir_prefix: String, + pub crash_portal_path: String, + pub ts_file: String, +} + +impl DumpPaths { + /// Creates a new `DumpPaths` instance with default values. + pub fn new() -> Self { + Self { + dump_name: String::new(), + core_path: String::new(), + minidumps_path: String::new(), + core_back_path: String::new(), + persistent_sec_path: String::new(), + working_dir: String::new(), + dumps_extn: String::new(), + tar_extn: String::new(), + lock_dir_prefix: String::new(), + crash_portal_path: String::new(), + ts_file: String::new(), + } + } + // Getters + pub fn get_dump_name(&self) -> &str { &self.dump_name } + pub fn get_core_path(&self) -> &str { &self.core_path } + pub fn get_minidumps_path(&self) -> &str { &self.minidumps_path } + pub fn get_core_back_path(&self) -> &str { &self.core_back_path } + pub fn get_persistent_sec_path(&self) -> &str { &self.persistent_sec_path } + pub fn get_working_dir(&self) -> &str { &self.working_dir } + pub fn get_dumps_extn(&self) -> &str { &self.dumps_extn } + pub fn get_tar_extn(&self) -> &str { &self.tar_extn } + pub fn get_lock_dir_prefix(&self) -> &str { &self.lock_dir_prefix } + pub fn get_crash_portal_path(&self) -> &str { &self.crash_portal_path } + pub fn get_ts_file(&self) -> &str { &self.ts_file } + + // Setters + pub fn set_dump_name(&mut self, value: impl Into) { self.dump_name = value.into(); } + pub fn set_core_path(&mut self, value: impl Into) { self.core_path = value.into(); } + pub fn set_minidumps_path(&mut self, value: impl Into) { self.minidumps_path = value.into(); } + pub fn set_core_back_path(&mut self, value: impl Into) { self.core_back_path = value.into(); } + pub fn set_persistent_sec_path(&mut self, value: impl Into) { self.persistent_sec_path = value.into(); } + pub fn set_working_dir(&mut self, value: impl Into) { self.working_dir = value.into(); } + pub fn set_dumps_extn(&mut self, value: impl Into) { self.dumps_extn = value.into(); } + pub fn set_tar_extn(&mut self, value: impl Into) { self.tar_extn = value.into(); } + pub fn set_lock_dir_prefix(&mut self, value: impl Into) { self.lock_dir_prefix = value.into(); } + pub fn set_crash_portal_path(&mut self, value: impl Into) { self.crash_portal_path = value.into(); } + pub fn set_ts_file(&mut self, value: impl Into) { self.ts_file = value.into(); } +} + +/// Holds device-specific metadata and configuration. +/// +/// This structure contains information about the device itself, such as its +/// type, model, identifiers, and configuration settings that affect the +/// crash upload process. +#[derive(Debug)] +pub struct DeviceData { + pub device_type: String, + pub box_type: String, + pub model_num: String, + pub sha1: String, + pub mac_addr: String, + pub is_t2_enabled: bool, + pub is_tls_enabled: String, + pub is_encryption_enabled: bool, + pub build_type: String, + pub portal_url: String, + //pub device_name: String, +} + +impl DeviceData { + /// Creates a new `DeviceData` instance with default values. + pub fn new() -> Self { + Self { + device_type: String::new(), + box_type: String::new(), + model_num: String::new(), + sha1: String::new(), + mac_addr: String::new(), + is_t2_enabled: false, + is_tls_enabled: String::new(), + is_encryption_enabled: false, + build_type: String::new(), + portal_url: String::new(), + //device_name: String::new(), + } + } + // Getters + pub fn get_device_type(&self) -> &str { &self.device_type } + pub fn get_box_type(&self) -> &str { &self.box_type } + pub fn get_model_num(&self) -> &str { &self.model_num } + pub fn get_sha1(&self) -> &str { &self.sha1 } + pub fn get_mac_addr(&self) -> &str { &self.mac_addr } + pub fn get_is_t2_enabled(&self) -> bool { self.is_t2_enabled } + pub fn get_is_tls_enabled(&self) -> &str { &self.is_tls_enabled } + pub fn get_is_encryption_enabled(&self) -> bool { self.is_encryption_enabled } + pub fn get_build_type(&self) -> &str { &self.build_type } + pub fn get_portal_url(&self) -> &str { &self.portal_url } + //pub fn device_name(&self) -> &str { &self.device_name } + + // Setters + pub fn set_device_type(&mut self, value: impl Into) { self.device_type = value.into(); } + pub fn set_box_type(&mut self, value: impl Into) { self.box_type = value.into(); } + pub fn set_model_num(&mut self, value: impl Into) { self.model_num = value.into(); } + pub fn set_sha1(&mut self, value: impl Into) { self.sha1 = value.into(); } + pub fn set_mac_addr(&mut self, value: impl Into) { self.mac_addr = value.into(); } + pub fn set_t2_enabled(&mut self, value: bool) { self.is_t2_enabled = value; } + pub fn set_tls(&mut self, value: impl Into) { self.is_tls_enabled = value.into(); } + pub fn set_encryption_enabled(&mut self, value: bool) { self.is_encryption_enabled = value; } + pub fn set_build_type(&mut self, value: impl Into) { self.build_type = value.into(); } + pub fn set_portal_url(&mut self, value: impl Into) { self.portal_url = value.into(); } + //pub fn set_device_name(&mut self, value: impl Into) { self.device_name = value.into(); } +} + +/// Enum representing the type of signal received. +/// +/// Used to differentiate between different types of termination signals +/// that may be received by the crash upload process. +pub enum CrashSignal { + /// Normal termination signal (SIGTERM) + Term, + /// Kill signal (SIGKILL) + Kill, +} diff --git a/crash-upload/src/crashupload_utils.rs b/crash-upload/src/crashupload_utils.rs new file mode 100644 index 0000000..6388ada --- /dev/null +++ b/crash-upload/src/crashupload_utils.rs @@ -0,0 +1,2310 @@ +//! Crash upload utility functions and implementation. +//! +//! This module provides the core functionality for the crash upload system, including: +//! +//! - **Dump file detection and processing**: Functions to identify, sanitize, and process +//! various types of crash dumps (minidumps and coredumps) +//! +//! - **Tarball creation and management**: Utilities to compress dump files with relevant +//! logs into tarballs for upload +//! +//! - **Upload control and rate limiting**: Functions to manage upload rates, recovery time, +//! and crash loop detection +//! +//! - **Security and privacy controls**: Implementation of secure dump handling and privacy +//! mode checks +//! +//! - **Lock management**: Mutex-like functionality to prevent concurrent execution of +//! critical sections +//! +//! - **File and path utilities**: Specialized functions for file manipulation, path handling, +//! and directory management +//! +//! - **Telemetry integration**: Functions to report crash events and upload status to the +//! Telemetry2 (T2) system +//! +//! ## Key Workflows +//! +//! The main workflow in this module is the dump processing pipeline: +//! 1. Detecting crash dumps in configured directories +//! 2. Sanitizing file names and paths +//! 3. Collecting relevant log files and device information +//! 4. Creating compressed archives (tarballs) containing dumps and logs +//! 5. Uploading archives to cloud storage (S3) with retries +//! 6. Managing cleanup and retention of local dumps +//! +//! ## Integration Points +//! +//! This module integrates with several platform components: +//! - **TR-181/RFC system** (via platform-interface): For configuration parameters +//! - **Telemetry2 (T2)**: For reporting crash and upload events +//! - **S3 storage**: For uploading crash dumps to the cloud +//! - **Device property system**: For obtaining device metadata +//! +//! Most functions in this module are designed to be self-contained and follow +//! the single responsibility principle for maintainability. + +// crashupload/src/crashupload_utils.rs +use chrono::{DateTime, Local}; +use std::fs::{self, File, OpenOptions}; +use std::io::{self, BufRead, BufReader, Write}; +use std::os::unix::fs::PermissionsExt; +use std::path::{Path, PathBuf}; +use std::process::Command; +use std::time::{Duration, SystemTime, UNIX_EPOCH}; +use std::process; +use std::{thread, time}; + +use crate::constants::*; +use platform_interface::*; +use utils::*; + +/// Determines if a file is a minidump based on its name. +/// +/// Checks if the filename contains the substring ".dmp" anywhere in the string, +/// which matches the shell pattern "*.dmp*". +/// +/// # Arguments +/// * `name` - The filename to check. +/// +/// # Returns +/// * `true` if the filename contains ".dmp", `false` otherwise. +#[inline] +fn is_minidump_file(name: &str) -> bool { + // *.dmp* matches any file with ".dmp" after the first character + name.find(".dmp").is_some() +} + +/// Determines if a file is a core pattern file based on its name. +/// +/// A file is considered a core pattern file if it contains "_core" followed by +/// a dot somewhere after the "_core" substring. This matches the shell pattern +/// "*_core*.*". +/// +/// # Arguments +/// * `name` - The filename to check. +/// +/// # Returns +/// * `true` if the filename matches the core pattern format, `false` otherwise. +pub fn is_core_pattern_file(name: &str) -> bool { + if let Some(core_idx) = name.find("_core") { + // There must be a '.' after the '_core' + let after_core = &name[core_idx + 5..]; // 5 = len("_core") + after_core.contains('.') + } else { + false + } +} + +/// Checks if a directory is empty or cannot be read. +/// +/// This function tries to read the directory entries. If the directory cannot be read +/// (due to permissions or other issues), it's treated as if it were empty. +/// +/// # Arguments +/// * `dir` - Path to the directory to check. +/// +/// # Returns +/// * `true` if the directory is empty or unreadable, `false` otherwise. +pub fn is_dir_empty_or_unreadable>(dir: P) -> bool { + match fs::read_dir(dir) { + Ok(mut entries) => entries.next().is_none(), + Err(_) => true, // treat unreadable as empty + } +} + +/// Safely renames a file, with fallback to copy-and-delete if rename fails with EXDEV error. +/// +/// This function attempts to rename a file from `src` to `dst`. If the rename fails with +/// error code 18 (EXDEV, "Cross-device link"), it falls back to copying the file and then +/// deleting the original. This handles cases where the source and destination are on +/// different filesystems. +/// +/// # Arguments +/// * `src` - Source path. +/// * `dst` - Destination path. If relative, it's resolved relative to `src`'s parent. +/// +/// # Returns +/// * `Ok(())` on success, or an error if both rename and copy-delete fail. +pub fn safe_rename, D: AsRef>(src: S, dst: D) -> io::Result<()> { + let src_path = src.as_ref(); + let dst_path = { + let dst_ref = dst.as_ref(); + if dst_ref.is_absolute() { + dst_ref.to_path_buf() + } else { + src_path.parent().unwrap_or_else(|| Path::new("")).join(dst_ref) + } + }; + match fs::rename(&src_path, &dst_path) { + Ok(_) => Ok(()), + Err(e) if e.raw_os_error() == Some(18) => { + fs::copy(&src_path, &dst_path)?; + fs::remove_file(&src_path)?; + Ok(()) + } + Err(e) => Err(e), + } +} + +/// Extracts the basename (filename) from a path. +/// +/// This function extracts just the filename portion from a path. If the path doesn't +/// have a valid filename component, returns the entire path as a string. +/// +/// # Arguments +/// * `path` - Path to extract the basename from. +/// +/// # Returns +/// * A `String` containing the basename (filename without directory components). +#[inline] +pub fn basename>(path: P) -> String { + path.as_ref() + .file_name() + .and_then(|n| n.to_str()) + .map(|s| s.to_string()) + .unwrap_or_else(|| path.as_ref().to_string_lossy().to_string()) +} + +/// Ensures the core log file exists, creating it with appropriate permissions if needed. +/// +/// This function checks if the core log file exists at the path defined by `CORE_LOG`. +/// If not, it creates the file with 0666 permissions (readable and writable by everyone). +/// It then uses `touch` to update the file's timestamp regardless. +/// +/// # Side Effects +/// - May create a new file at `CORE_LOG` path. +/// - Updates the file's timestamp. +fn ensure_core_log_exists() { + if !Path::new(CORE_LOG).exists() { + if let Ok(f) = OpenOptions::new().create(true).write(true).open(CORE_LOG) { + let _ = f.set_permissions(fs::Permissions::from_mode(0o666)); + } + } + let _ = Command::new("touch").arg(CORE_LOG).status(); +} + +#[cfg(not(feature = "shared_api"))] +/// Writes parameters for S3 upload to a parameter file for shell script consumption. +/// +/// Creates a space-separated parameter string with all necessary arguments for the S3 upload +/// process and writes it to the file specified by `S3_UPLOAD_PARAM_FILE`. Used when the +/// `shared_api` feature is not enabled. +/// +/// # Arguments +/// * `dump_paths` - Reference to crash dump path configuration. +/// * `device_data` - Reference to device metadata. +/// * `partner_id` - Partner identifier string. +/// * `enable_ocsp_stapling` - OCSP stapling configuration flag. +/// * `enable_ocsp` - OCSP configuration flag. +/// * `curl_log_option` - Curl logging option string. +/// +/// # Returns +/// * `Ok(())` on success, or an error if file write fails. +fn write_uploadtos3params( + dump_paths: &DumpPaths, + device_data: &DeviceData, + partner_id: &str, + enable_ocsp_stapling: &str, + enable_ocsp: &str, + curl_log_option: &str, +) -> std::io::Result<()> { + let params = format!( + "{} {} {} {} {} {} {} {} {} {} {} {}", + dump_paths.get_working_dir(), + partner_id, + dump_paths.get_dump_name(), + device_data.get_device_type(), + VERSION_FILE, + if device_data.get_is_encryption_enabled() { "true" } else { "false" }, + enable_ocsp_stapling, + enable_ocsp, + device_data.get_is_tls_enabled(), + device_data.get_build_type(), + device_data.get_model_num(), + curl_log_option + ); + std::fs::write(S3_UPLOAD_PARAM_FILE, params) +} + +/// Populates a [`DeviceData`] struct with device properties from system files and TR-181. +/// +/// This function reads various device properties from multiple sources: +/// - Device properties file (box type, model number, device type, build type) +/// - Version file (SHA1 value) +/// - Network configuration (MAC address) +/// - System files (T2 enablement, TLS configuration, encryption settings) +/// - TR-181 parameters (portal URL) +/// +/// The collected information is essential for crash upload identification, processing, and routing. +/// +/// # Arguments +/// * `device_data` - Mutable reference to a [`DeviceData`] struct to populate. +/// +/// # Side Effects +/// - Reads from filesystem and TR-181 parameters +/// - May set RFC parameters if encryption is enabled +/// - No files are modified +pub fn set_device_data(device_data: &mut DeviceData) { + // Populate from device.properties + let mut box_type = String::new(); + let mut model_num = String::new(); + let mut device_type = String::new(); + let mut build_type = String::new(); + get_property_value_from_file(DEVICE_PROP_FILE, "BOX_TYPE", &mut box_type); + get_property_value_from_file(DEVICE_PROP_FILE, "MODEL_NUM", &mut model_num); + get_property_value_from_file(DEVICE_PROP_FILE, "DEVICE_TYPE", &mut device_type); + get_property_value_from_file(DEVICE_PROP_FILE, "BUILD_TYPE", &mut build_type); + device_data.set_box_type(box_type); + device_data.set_model_num(model_num); + device_data.set_device_type(device_type); + device_data.set_build_type(build_type); + + // SHA1 from version.txt + let mut sha1 = String::new(); + get_sha1_value(&mut sha1); + device_data.set_sha1(sha1); + + // MAC address + let mut mac_addr = String::new(); + let _ = get_device_mac(&mut mac_addr); + device_data.set_mac_addr(mac_addr); + + // T2 enabled if script exists + device_data.set_t2_enabled(Path::new(T2_SHARED_SCRIPT).exists()); // TODO: Check T2 Binary instead of script? + + // TLS flag for Yocto + let tls = if Path::new("/etc/os-release").exists() { "--tlsv1.2" } else { "" }; + device_data.set_tls(tls); + + // Encryption enabled if file exists, and set RFC param + let encryption_enabled = if Path::new("/etc/os-release").exists() { + if set_rfc_param(ENCRYPTION_RFC, "true") { true } + else { false } + } else { + false + }; + device_data.set_encryption_enabled(encryption_enabled); + + // Portal URL from TR-181 + let mut portal_url = String::new(); + get_rfc_param(CRASH_PORTAL_URL_RFC, &mut portal_url); + device_data.set_portal_url(portal_url); +} + +/// Checks if any crash dump files exist in the specified directories. +/// +/// This function examines two types of dumps in their respective directories: +/// - Minidumps: Files containing ".dmp" in their name in the minidumps directory +/// - Core dumps: Files matching the "*_core*.*" pattern in the core dump directory +/// +/// # Arguments +/// * `minidumps_path` - Directory path to search for minidump files +/// * `core_path` - Directory path to search for core dump files +/// +/// # Returns +/// * `true` if at least one dump file (of either type) exists, `false` otherwise +pub fn check_dumps_exist(minidumps_path: &str, core_path: &str) -> bool { + let minidumps_dir = std::path::Path::new(minidumps_path); + let core_dir = std::path::Path::new(core_path); + + let minidump_exists = minidumps_dir.is_dir() && std::fs::read_dir(minidumps_dir) + .map(|iter| iter.flatten().any(|entry| { + let name = entry.file_name(); + let name = name.to_string_lossy(); + // Faithful to *.dmp* shell pattern + is_minidump_file(&name) + })) + .unwrap_or(false); + + let core_exists = core_dir.is_dir() && std::fs::read_dir(core_dir) + .map(|iter| iter.flatten().any(|entry| { + let name = entry.file_name(); + let name = name.to_string_lossy(); + // Faithful to *_core*.* shell pattern + is_core_pattern_file(&name) + })) + .unwrap_or(false); + + minidump_exists || core_exists +} + +/// Configures secure or non-secure dump paths based on SecureDump enablement status. +/// +/// This function determines if SecureDump is enabled by checking flag files and TR-181 parameters, +/// then updates the dump paths accordingly: +/// - When enabled: Uses paths under /opt/secure/ +/// - When disabled: Uses standard paths under /opt/ and /var/ +/// +/// It also manages the appropriate flag files to maintain the system's secure dump state. +/// +/// # Arguments +/// * `dump_paths` - Mutable reference to a [`DumpPaths`] struct to update with appropriate paths +/// +/// # Side Effects +/// - Creates or removes SecureDump enable/disable flag files +/// - Updates all path settings in the provided `dump_paths` struct +/// - Logs the SecureDump status +pub fn get_secure_dump_status(dump_paths: &mut DumpPaths) { + println!("get_secure_dump_status: Checking SecureDump status..."); + let mut is_sec_dump_enabled = false; + set_secure_dump_flag(&mut is_sec_dump_enabled); + + if !is_sec_dump_enabled { + if Path::new(SECUREDUMP_ENABLE_FILE).exists() { + touch(SECUREDUMP_DISABLE_FILE); + println!("get_secure_dump_status: Disabled"); + } + if Path::new(SECUREDUMP_ENABLE_FILE).exists() { + let _ = fs::remove_file(SECUREDUMP_ENABLE_FILE); + } + dump_paths.set_core_path("/var/lib/systemd/coredump"); + dump_paths.set_minidumps_path("/opt/minidumps"); + dump_paths.set_core_back_path("/opt/corefiles_back"); + dump_paths.set_persistent_sec_path("/opt"); + println!("get_secure_dump_status: Dump location changed /opt."); + } else { + if !Path::new(SECUREDUMP_ENABLE_FILE).exists() { + touch(SECUREDUMP_ENABLE_FILE); + println!("get_secure_dump_status: Enabled"); + } + if Path::new(SECUREDUMP_DISABLE_FILE).exists() { + let _ = fs::remove_file(SECUREDUMP_DISABLE_FILE); + } + dump_paths.set_core_path("/opt/secure/corefiles"); + dump_paths.set_minidumps_path("/opt/secure/minidumps"); + dump_paths.set_core_back_path("/opt/secure/corefiles_back"); + dump_paths.set_persistent_sec_path("/opt/secure"); + println!("get_secure_dump_status: Dump location changed /opt/secure."); + } +} + +/// Determines if SecureDump is enabled by checking flag files or TR-181 parameters. +/// +/// This function implements a priority-based check for SecureDump enablement: +/// 1. If SECUREDUMP_ENABLE_FILE exists, SecureDump is enabled +/// 2. If SECUREDUMP_DISABLE_FILE exists, SecureDump is disabled +/// 3. Otherwise, check the TR-181 parameter for the setting +/// +/// # Arguments +/// * `is_sec_dump_enabled` - Mutable reference to a boolean to set based on the determination +/// +/// # Side Effects +/// - Reads from the filesystem and possibly TR-181 configuration +/// - Modifies the passed boolean reference +fn set_secure_dump_flag(is_sec_dump_enabled: &mut bool) { + if Path::new(SECUREDUMP_ENABLE_FILE).exists() { + *is_sec_dump_enabled = true; + } else if Path::new(SECUREDUMP_DISABLE_FILE).exists() { + *is_sec_dump_enabled = false; + } else { + let mut rfc_value = String::new(); + if get_rfc_param(SECUREDUMP_TR181_NAME, &mut rfc_value) { + *is_sec_dump_enabled = rfc_value.trim().eq_ignore_ascii_case("true"); + } + } +} + +/// Creates a standard lock directory path for a given resource path. +/// +/// This function converts any path into a corresponding lock directory path +/// by changing its extension to ".lock.d". This lock directory is used +/// to implement a filesystem-based mutex mechanism. +/// +/// # Arguments +/// * `path` - Path to be locked (can be any file or directory path) +/// +/// # Returns +/// * `PathBuf` representing the derived lock directory path +#[inline] +fn lock_path>(path: P) -> PathBuf { + let mut p = path.as_ref().to_path_buf(); + p.set_extension("lock.d"); + p +} + +/// Checks if another process instance is running by testing for an existing lock directory. +/// +/// This function is used to implement mutual exclusion between process instances. +/// It determines if another instance is running by checking for the existence of +/// a lock directory associated with the given path. +/// +/// # Arguments +/// * `path` - Path to check for an existing lock +/// # Returns +/// * `true` if a lock directory exists (another instance is running), `false` otherwise +#[inline] +fn is_another_instance_running>(path: P) -> bool { + lock_path(path).is_dir() +} + +/// Creates a lock directory or exits the process if already locked. +/// +/// This function implements an "exit on contention" locking strategy: +/// - If another instance is running (lock exists), it logs a message, +/// potentially sends a T2 notification, and exits the process +/// - Otherwise, it creates the lock directory and returns true on success +/// +/// # Arguments +/// * `path` - Path to lock +/// * `is_t2_enabled` - Whether T2 telemetry is enabled (for sending notifications) +/// +/// # Returns +/// * `true` if lock was created successfully +/// * `false` if lock creation failed due to filesystem errors +/// +/// # Side Effects +/// - May exit the process if lock already exists +/// - May send T2 telemetry notification if enabled +/// - Creates a lock directory on the filesystem if successful +pub fn create_lock_or_exit>(path: P, is_t2_enabled: bool) -> bool { + let lock = lock_path(&path); + if is_another_instance_running(&path) { + if is_t2_enabled { + t2_count_notify("SYST_WARN_NoMinidump", Some("1")); + } + println!("create_lock_or_exit: crash-upload is already running. {:?}. Skip launching another instance...", lock); + // TODO: add wait + process::exit(0); + } + + match fs::create_dir(&lock) { + Ok(_) => true, + Err(err) => { + println!("create_lock_or_exit: Error creating {:?}: {}", lock, err); + false + } + } +} + +/// Creates a lock directory, waiting and retrying if already locked. +/// +/// This function implements a "wait on contention" locking strategy: +/// - If another instance is running (lock exists), it waits and retries every 2 seconds +/// - It continues retrying indefinitely until either the lock is acquired or creation fails +/// +/// # Arguments +/// * `path` - Path to lock +/// +/// # Returns +/// * `true` if lock was created successfully +/// * `false` if lock creation failed due to filesystem errors +/// +/// # Side Effects +/// - May block thread execution while waiting for lock +/// - Creates a lock directory on the filesystem if successful +pub fn create_lock_or_wait>(path: P) -> bool { + let lock = lock_path(&path); + loop { + if is_another_instance_running(&path) { + println!("create_lock_or_wait: crash-upload is already running. {:?}. Waiting to launch another instance...", lock); + thread::sleep(time::Duration::from_secs(2)); + continue; + } + + match fs::create_dir(&lock) { + Ok(_) => return true, + Err(err) => { + println!("create_lock_or_wait: Error creating {:?}: {}", lock, err); + return false; + } + } + } +} + +/// Removes a lock directory for the given resource path. +/// +/// This function cleans up the lock directory created by `create_lock_or_exit` or +/// `create_lock_or_wait`, effectively releasing the lock. It silently handles the +/// case where the lock directory doesn't exist. +/// +/// # Arguments +/// * `path` - Path whose associated lock should be removed +/// +/// # Side Effects +/// - Deletes the lock directory from the filesystem if it exists +/// - Logs any errors that occur during removal +pub fn remove_lock>(path: P) { + let lock = lock_path(&path); + if lock.is_dir() { + if let Err(err) = fs::remove_dir(&lock) { + println!("remove_lock: Error deleting {:?}: {}", lock, err); + } + } +} + +/// Generates a standardized log file name for a crash, or returns the original if already processed. +/// +/// If the file name already contains any of the tags `_mac`, `_dat`, `_box`, or `_mod`, +/// it is considered already processed and returned as-is. Otherwise, constructs a new +/// file name using device metadata and the original file name. +/// +/// # Arguments +/// * `device_data` - Reference to the device metadata. +/// * `log_mod_ts` - Last modified timestamp string. +/// * `line` - The original file path or name. +/// +/// # Returns +/// * A `String` with the new or original file name. +pub fn set_log_file(device_data: &DeviceData, log_mod_ts: &str, line: &str) -> String { + let file_name = line.split('/').next_back().unwrap_or(line); + if file_name.contains("_mac") + || file_name.contains("_dat") + || file_name.contains("_box") + || file_name.contains("_mod") + { + println!("set_log_file: Core name is already processed: {}", file_name); + return String::from(file_name); + } + format!( + "{}_mac{}_dat{}_box{}_mod{}_{}", + device_data.sha1, + device_data.mac_addr, + log_mod_ts, + device_data.box_type, + device_data.model_num, + file_name + ) +} + +/// Checks if the box is currently rebooting by looking for the reboot flag file. +/// +/// If the reboot flag exists, logs a message, sends a T2 notification, and returns `true`. +/// Otherwise, returns `false`. +/// +/// # Arguments +/// * `is_t2_enabled` - Whether T2 telemetry is enabled (for sending notifications) +/// +/// # Returns +/// * `true` if the box is rebooting (flag file exists), `false` otherwise. +/// # Side Effects +/// - May send T2 telemetry notification if enabled +pub fn is_box_rebooting(is_t2_enabled: bool) -> bool { + if Path::new(CRASH_UPLOAD_REBOOT_FLAG).exists() { + println!("is_box_rebooting: Skipping Upload, Since the box is rebooting now..."); + if is_t2_enabled { + t2_count_notify("SYST_INFO_CoreUpldSkipped", Some("1")); + } + println!("is_box_rebooting: Upload will happen on next reboot"); + // TODO: Add wait + return true; + } + false +} + +/// Sanitizes a string by removing all characters except a safe set. +/// +/// Only allows alphanumeric characters and the following: `/ :+._,=-` +/// This matches the shell's `sed` logic for cleaning file/process names. +/// +/// # Arguments +/// * `input` - The input string to sanitize. +/// +/// # Returns +/// * A sanitized `String` containing only allowed characters. +pub fn sanitize(input: &str) -> String { + input + .chars() + .filter(|c| matches!( + c, + 'a'..='z' + | 'A'..='Z' + | '0'..='9' + | '/' + | ' ' + | ':' + | '+' + | '.' + | '_' + | ',' + | '=' + | '-' + )) + .collect() +} + +/// Checks if the recovery time (upload denial window) has been reached or expired. +/// +/// Reads the timestamp from the deny upload file. If the file does not exist or contains +/// invalid data, returns `true` (uploads allowed). If the current time is greater than the +/// stored timestamp, returns `true` (uploads allowed). Otherwise, returns `false`. +/// +/// # Returns +/// * `true` if uploads are allowed, `false` if still in the denial window. +/// +/// # Side Effects +/// - Reads from the deny upload file on the filesystem +pub fn is_recovery_time_reached() -> bool { + let path = Path::new(DENY_UPLOAD_FILE); + if !path.exists() { + return true; + } + let content = match fs::read_to_string(path) { + Ok(val) => val.trim().to_string(), + Err(_) => return true, + }; + let upload_denied_till = match content.parse::() { + Ok(val) => val, + Err(_) => return true, + }; + let now = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap_or_default() + .as_secs(); + now > upload_denied_till +} + +/// Sets the recovery time (upload denial window) to now + 10 minutes. +/// +/// Writes the future timestamp to the deny upload file. This prevents further uploads +/// until the specified time has passed. +/// +/// # Side Effects +/// - Overwrites the deny upload file with the new timestamp. +pub fn set_recovery_time() { + const DONT_UPLOAD_FOR_SEC: u64 = 600; + let now = SystemTime::now() + .duration_since(UNIX_EPOCH) + .expect("SystemTime before UNIX_EPOCH") + .as_secs(); + let recovery_time = now + DONT_UPLOAD_FOR_SEC; + let _ = fs::write(DENY_UPLOAD_FILE, recovery_time.to_string()); +} + +/// Determines if the upload rate limit has been reached (10 uploads in 10 minutes). +/// +/// Checks the timestamp file for the last 10 upload times. If there are fewer than 10, +/// returns `false`. If the 10th newest timestamp is less than 10 minutes ago, returns `true`. +/// +/// # Arguments +/// * `ts_file` - Path to the timestamp file. +/// +/// # Returns +/// * `true` if the upload rate limit is reached, `false` otherwise. +/// +/// # Side Effects +/// - Acquires and releases a lock on the timestamp file +/// - Reads from the timestamp file +pub fn is_upload_limit_reached(ts_file: &str) -> bool { + create_lock_or_wait(ts_file); + + const LIMIT_SECONDS: u64 = 600; + let path = Path::new(ts_file); + + + if OpenOptions::new().create(true).append(true).open(path).is_err() { + println!("is_upload_limit_reached: Failed to create or open {}", ts_file); + remove_lock(ts_file); + return false; + } + + let file = match File::open(path) { + Ok(f) => f, + Err(_) => { + remove_lock(ts_file); + return false; + } + }; + let reader = BufReader::new(&file); + + // Collect upto 10 timestamps (oldest first) + let lines: Vec = reader + .lines() + .filter_map(|line| line.ok()?.split_whitespace().next()?.parse().ok()) + .collect(); + + if lines.len() < 10 { + remove_lock(ts_file); + return false; + } + + let tenth_newest_crash_time = lines[0]; + let now = SystemTime::now() + .duration_since(UNIX_EPOCH) + .expect("SystemTime before UNIX_EPOCH") + .as_secs(); + + let limit_reached = now.saturating_sub(tenth_newest_crash_time) < LIMIT_SECONDS; + if limit_reached { + println!("is_upload_limit_reached: Not uploading the dump. Too many dumps."); + } + remove_lock(ts_file); + limit_reached +} + +/// Removes the crash loop flag and all relevant lock files for the given dump paths. +/// +/// This function is inlined for performance, as it is small and called from multiple places. +/// +/// # Arguments +/// * `dump_paths` - Reference to the dump paths struct. +/// +/// # Side Effects +/// - Deletes the crash loop flag file if it exists +/// - Removes lock directories associated with the dump paths +#[inline] +fn remove_crash_locks_and_flag(dump_paths: &DumpPaths) { + let loop_file = Path::new(CRASH_LOOP_FLAG_FILE); + if loop_file.exists() { + rm_rf(loop_file); + } + remove_lock(dump_paths.get_lock_dir_prefix()); + remove_lock(dump_paths.get_ts_file()); +} + +/// Finalizes the crash upload process by cleaning up dumps, removing locks, and clearing the crash loop flag. +/// +/// This function should be called on normal exit to ensure all resources are released and +/// any temporary files or locks are cleaned up. Uses only getters for `DumpPaths`. +/// +/// # Arguments +/// * `dump_paths` - Reference to the dump paths struct. +/// +/// # Side Effects +/// - Cleans up dump files in the working directory +/// - Removes lock files and crash loop flags +pub fn finalize(dump_paths: &DumpPaths) { + println!("finalize: ##DEBUG## Skipping cleanup and removing crash locks and flag for debugging purposes"); + if false { + let _ = cleanup( + dump_paths.get_working_dir(), + dump_paths.get_dump_name(), + dump_paths.get_dumps_extn(), + ); + remove_crash_locks_and_flag(dump_paths); +} +} + +/// Handles cleanup on receiving a crash-related signal (SIGTERM or SIGKILL). +/// +/// Logs the signal type, removes the crash loop flag, and removes all relevant locks. +/// Uses only getters for `DumpPaths`. +/// +/// # Arguments +/// * `signal` - The signal type (CrashSignal::Term or CrashSignal::Kill). +/// * `dump_paths` - Reference to the dump paths struct. +/// +/// # Side Effects +/// - Logs the signal type +/// - Removes crash loop flag and lock files +pub fn handle_crash_signal(signal: CrashSignal, dump_paths: &DumpPaths) { + match signal { + CrashSignal::Term => println!("systemd terminating, Removing the script locks"), + CrashSignal::Kill => println!("systemd killing, Removing the script locks"), + } + remove_crash_locks_and_flag(dump_paths); +} + +/// Determines if a dump file should be processed/uploaded based on dump type, device type, and file name. +/// +/// - Always returns `true` for minidumps. +/// - For coredumps, returns `true` if not a "prod" build or if the file name does not contain "Receiver". +/// - Otherwise, logs and returns `false`. +/// +/// # Arguments +/// * `dump_name` - The dump type ("coredump" or "minidump"). +/// * `build_type` - The build type (e.g., "prod"). +/// * `file_name` - The file name to check. +/// +/// # Returns +/// * `true` if the file should be processed, `false` otherwise. +pub fn should_process_dump(dump_name: &str, build_type: &str, file_name: &str) -> bool { + if dump_name == "minidump" || build_type != "prod" || !file_name.contains("Receiver") { + true + } else { + println!("should_process_dump: Not processing dump file {}", file_name); + false + } +} + +/// Determines if a file name matches a specified pattern. +/// +/// This function implements shell-like wildcard pattern matching for specific crash-related +/// file patterns. It supports the following patterns: +/// - "*.dmp*": Files containing ".dmp" anywhere in the name +/// - "*.dmp.tgz": Files ending with ".dmp.tgz" +/// - "*core.prog*.gz*": Files containing "core.prog" followed by ".gz" somewhere later +/// - "*.core.tgz": Files ending with ".core.tgz" +/// +/// # Arguments +/// * `name` - The file name to check against the pattern. +/// * `pattern` - The pattern to match against (one of the supported patterns above). +/// +/// # Returns +/// * `true` if the file name matches the pattern, `false` otherwise. +fn matches_pattern(name: &str, pattern: &str) -> bool { + match pattern { + "*.dmp*" => name.contains(".dmp"), + "*.dmp.tgz" => name.ends_with(".dmp.tgz"), + "*core.prog*.gz*" => { + if let Some(core_idx) = name.find("core.prog") { + name[core_idx + "core.prog".len()..].contains(".gz") + } else { + false + } + }, + "*.core.tgz" => name.ends_with(".core.tgz"), + _ => false, + } +} + +/// Counts the number of dump files in a directory matching a given pattern. +/// +/// # Arguments +/// * `dir` - Directory path to search. +/// * `pattern` - Pattern to match in file names (e.g., "*.dmp*" or "*.core.tgz"). +/// +/// # Returns +/// * `Ok(count)` with the number of matching files, or an error if the directory can't be read. +pub fn get_file_count(dir: &str, pattern: &str) -> std::io::Result { + let dir_path = std::path::Path::new(dir); + if !dir_path.is_dir() { + return Ok(0); + } + let mut count = 0; + for entry in std::fs::read_dir(dir_path)? { + let entry = entry?; + let name = entry.file_name(); + let name = name.to_string_lossy(); + if matches_pattern(&name, pattern) { + count += 1; + } + } + Ok(count) +} + +/// Removes pending dump files (matching extension or .tgz) from the given directory. +/// +/// This function is used when the upload limit is reached, the build is blacklisted, +/// or TelemetryOptOut is set. It removes all files matching the given extension or `.tgz`. +/// +/// # Arguments +/// * `path` - Directory path to search. +/// * `extn` - Extension to match (e.g., "dmp" or "core"). +/// +/// # Returns +/// * `Ok(())` on success, or an error if file operations fail. +/// +/// # Side Effects +/// - Deletes files from the filesystem that match the criteria +pub fn remove_pending_dumps(path: &str, extn: &str) -> io::Result<()> { + println!("remove_pending_dumps: ###DEBUG### Skipping pending dumps removal for debugging purposes"); + if false { + let dir_path = Path::new(path); + if !dir_path.exists() || !dir_path.is_dir() { + return Ok(()); + } + for entry in fs::read_dir(dir_path)? { + let entry = entry?; + let file_path = entry.path(); + if file_path.is_file() { + let file_name = file_path.file_name().and_then(|n| n.to_str()).unwrap_or(""); + if file_name.ends_with(extn) || file_name.ends_with(".tgz") { + println!("remove_pending_dumps: Removing {} because upload limit has reached or build is blacklisted or TelemetryOptOut is set", basename(&file_path)); + let _ = fs::remove_file(&file_path); + } + } + } + } + Ok(()) +} + +/// Processes crash telemetry info for a given dump file. +/// +/// - Detects if the file is a tarball and logs accordingly. +/// - Extracts container crash info if present in the filename. +/// - Sends T2 notifications for container/app/process info. +/// - Calls `get_crashed_log_file` for further log mapping. +/// +/// # Arguments +/// * `file_path` - Path to the crash dump file (as &str). +/// * `is_t2_enabled` - Whether T2 telemetry is enabled (for sending notifications) +/// +/// # Side Effects +/// - May send multiple T2 telemetry notifications +/// - Logs crash information +/// - Calls `get_crashed_log_file` which may write to log files +pub fn process_crash_t2_info(file_path: &str, is_t2_enabled: bool) { + println!("process_crash_t2_info: Processing the crash telemetry info"); + let file = Path::new(file_path); + let mut file_name_str = file_path.to_string(); + let container_delimiter = "<#=#>"; + + // Check if file is a tarball + if file.extension().and_then(|e| e.to_str()) == Some("tgz") { + println!("process_crash_t2_info: The File is already a tarball, this might be a retry or crash during shutdown"); + if let Some(pos) = file_name_str.find("_mod_") { + file_name_str = file_name_str.split_off(pos + "_mod_".len()); + } + println!("process_crash_t2_info: Original Filename: {}", basename(file)); + println!("process_crash_t2_info: Removing the meta information New Filename: {}", basename(&file_name_str)); + println!("process_crash_t2_info: This could be a retry or crash from previous boot; the appname can be truncated"); + t2_count_notify("SYS_INFO_TGZDUMP", Some("1")); + } + + // Check for container delimiter in file name + if file_name_str.contains(container_delimiter) { + let parts: Vec<&str> = file_name_str.split(container_delimiter).collect(); + println!("process_crash_t2_info: From the file name crashed process is a container"); + + if parts.len() >= 2 { + let container_name = parts[0]; + let container_status = if parts.len() > 2 { parts[1] } else { "unknown" }; + let app_name = container_name.split('_').nth(1).unwrap_or(container_name); + let process_name = container_name.split('_').next().unwrap_or(container_name); + + t2_count_notify("SYS_INFO_CrashedContainer", Some("1")); + + println!("process_crash_t2_info: Container crash info Basic: {}, {}", app_name, process_name); + println!("process_crash_t2_info: Container crash info Advanced: {}, {}", container_name, container_status); + println!("process_crash_t2_info: NEW Appname, Process_Crashed, Status = {}, {}, {}", app_name, process_name, container_status); + + t2_val_notify("APP_ERROR_Crashed_split", &[app_name, process_name, container_status]); + t2_val_notify("APP_ERROR_Crashed_accum", &[app_name, process_name, container_status]); + + println!("process_crash_t2_info: NEW Processname, App Name, AppState = {}, {}, {}", process_name, app_name, container_status); + println!("process_crash_t2_info: ContainerName, ContainerStatus = {}, {}", container_name, container_status); + t2_val_notify("APP_ERROR_CrashInfo_accum", &[container_name, container_status]); + } + } + let _ = get_crashed_log_file(&file_name_str, is_t2_enabled); + +} + +/// Renames a tarball to mark it as crashlooped and (optionally) uploads it to the crash portal. +/// +/// # Arguments +/// * `tgz_file` - Path to the tarball file. +/// +/// # Side Effects +/// - Renames the file to `.crashloop.dmp.tgz`. +/// - Logs the rename operation and any errors. +pub fn mark_as_crash_loop_and_upload(tgz_file: &str) { // portal_url: &str, crash_portal_path: &str) { + let tgz_path = Path::new(tgz_file); + let new_tgz_name = tgz_path.with_extension("crashloop.dmp.tgz"); + println!("mark_as_crash_loop_and_upload: Renaming {} to {}", tgz_path.display(), new_tgz_name.display()); + if let Err(e) = safe_rename(tgz_path, &new_tgz_name) { + println!("mark_as_crash_loop_and_upload: Failed to rename crashloop tarball: {}", e); + } +} + +/// Finds the oldest file in a directory. +/// +/// Scans a directory for files, sorts them by modification time, and returns the oldest one. +/// This is typically used for retention policies and cleanup of old dumps. +/// +/// # Arguments +/// * `dir` - Directory path to search. +/// +/// # Returns +/// * `Ok(Some(path))` with the oldest file, or `Ok(None)` if no files found or directory is empty. +/// * `Err` if there was an error reading the directory or file metadata. +pub fn find_oldest_dump(dir: &str) -> io::Result> { + let mut files: Vec<_> = fs::read_dir(dir)? + .filter_map(|entry| entry.ok()) + .map(|entry| entry.path()) + .filter(|path| path.is_file()) + .collect(); + + files.sort_by_key(|path| fs::metadata(path).and_then(|m| m.modified()).ok()); + Ok(files.into_iter().next()) +} + +/// Saves a dump file, renaming if needed, and ensures only the most recent 5 minidumps are kept. +/// +/// This function has two main responsibilities: +/// 1. Renames the dump file to preserve container information (if `new_name` is provided) +/// 2. Enforces a retention policy by keeping only the 5 most recent dump files +/// +/// # Arguments +/// * `minidumps_path` - Directory containing minidumps +/// * `s3_filename` - The current filename of the dump +/// * `new_name` - Optional new name to rename to (to retain container info) +/// +/// # Side Effects +/// - May rename files in the minidumps directory +/// - May delete old dump files to maintain the retention limit +pub fn save_dump(minidumps_path: &str, s3_filename: &str, new_name: Option<&str>) { + if let Some(new_name) = new_name { + println!("save_dump: Saving dump with original name to retain container info: {}", basename(new_name)); + let original_path = format!("{}/{}", minidumps_path, s3_filename); + let new_path = format!("{}/{}", minidumps_path, new_name); + if let Err(e) = safe_rename(&original_path, &new_path) { + println!("save_dump: Failed to rename file: {}", e); + } + } + + let dump_extn = "dmp.tgz"; + let mut count = get_file_count(minidumps_path, dump_extn).unwrap_or(0); + + while count > 5 { + match find_oldest_dump(minidumps_path) { + Ok(Some(oldest)) => { + println!("save_dump: Removing old dump {}", oldest.display()); + if let Err(e) = fs::remove_file(&oldest) { + println!("Failed to remove old dump: {}", e); + } + count -= 1; + } + _ => break, + } + } + println!("save_dump: Total pending Minidumps: {}", count); +} + +/// Logs the current upload timestamp to the timestamp file and truncates it to the last 10 entries. +/// +/// This function is used to maintain a record of recent uploads for rate limiting purposes. +/// For production builds, it adds the current time to the timestamp file and calls +/// `truncate_timestamp_file()` to keep only the 10 most recent entries. +/// +/// # Arguments +/// * `ts_file` - Path to the timestamp file +/// +/// # Side Effects +/// - Acquires and releases a lock on the timestamp file +/// - Appends the current timestamp to the file +/// - Truncates the file to keep only the 10 most recent entries +pub fn log_upload_timestamp(ts_file: &str) { + create_lock_or_wait(ts_file); + + let mut build_type = String::new(); + get_property_value_from_file("/etc/device.properties", "BUILD_TYPE", &mut build_type); + if build_type == "prod" { + let now = SystemTime::now() + .duration_since(UNIX_EPOCH) + .expect("SystemTime before UNIX_EPOCH") + .as_secs(); + if let Err(e) = OpenOptions::new().append(true).create(true).open(ts_file) + .and_then(|mut f| writeln!(f, "{}", now)) { + println!("log_upload_timestamp: Failed to write timestamp: {}", e); + } + if let Err(e) = truncate_timestamp_file(ts_file) { + println!("log_upload_timestamp: Failed to truncate timestamp file: {}", e); + } + } + remove_lock(ts_file); +} + +/// Truncates the timestamp file to the last 10 lines. +/// +/// This function maintains a fixed-size history of upload timestamps by keeping +/// only the 10 most recent entries. It reads all lines, selects the 10 newest, +/// and overwrites the file with just those lines. +/// +/// # Arguments +/// * `ts_file` - Path to the timestamp file +/// +/// # Returns +/// * `Ok(())` on success, or an error if file operations fail +/// +/// # Side Effects +/// - Acquires and releases a lock on the timestamp file +/// - Modifies the content of the timestamp file +pub fn truncate_timestamp_file(ts_file: &str) -> io::Result<()> { + create_lock_or_wait(ts_file); + let ts_path = Path::new(ts_file); + let file = File::open(ts_path)?; + let reader = BufReader::new(file); + + let lines: Vec = reader.lines().map_while(Result::ok).collect(); + let last_10_lines = lines.iter().rev().take(10).cloned().collect::>(); + let mut tmp_file = OpenOptions::new().write(true).truncate(true).open(ts_path)?; + + for line in last_10_lines.into_iter().rev() { + writeln!(tmp_file, "{}", line)?; + } + + remove_lock(ts_file); + Ok(()) +} + +/// Gets the last modified time of a file as a formatted string. +/// +/// Retrieves the file's modification timestamp and formats it as +/// "YYYY-MM-DD-HH-MM-SS" for use in log file naming and comparisons. +/// +/// # Arguments +/// * `path` - Path to the file +/// +/// # Returns +/// * `Some(String)` with the formatted time string, or `None` if: +/// - The path doesn't exist +/// - The path isn't a regular file +/// - The metadata or modification time couldn't be retrieved +pub fn get_last_modified_time_of_file(path: &str) -> Option { + let path_ref = Path::new(path); + if !path_ref.is_file() { + return None; + } + let metadata = fs::metadata(path_ref).ok()?; + let modified_time = metadata.modified().ok()?; + let datetime: DateTime = modified_time.into(); + Some(datetime.format("%Y-%m-%d-%H-%M-%S").to_string()) +} + +/// Maps a crashed process to its log files using the logmapper config and writes them to LOG_FILES. +/// +/// This function performs several key operations: +/// 1. Extracts the process name from the crashed file path +/// 2. Sends telemetry notifications about the crashed process +/// 3. Looks up associated log files in the LOGMAPPER_FILE configuration +/// 4. Writes the full paths of those log files to the LOG_FILES file +/// +/// # Arguments +/// * `file_path` - Path or name of the crashed file +/// * `is_t2_enabled` - Whether T2 telemetry is enabled (for sending notifications) +/// +/// # Returns +/// * `Ok(())` on success, or an error if file operations fail +/// +/// # Side Effects +/// - May send multiple T2 telemetry notifications +/// - Writes to the LOG_FILES file +pub fn get_crashed_log_file(file_path: &str, is_t2_enabled: bool) -> io::Result<()> { + let basename = basename(file_path); + + let process_name = match basename.rfind('_') { + Some(idx) => &basename[..idx], + None => &basename, + }; + + println!("get_crashed_log_file: Process crashed = {}", process_name); + + if is_t2_enabled { + t2_val_notify("processCrash_split", &[process_name]); + t2_val_notify("SYST_ERR_Process_Crash_accum", &[process_name]); + t2_count_notify("SYST_ERR_ProcessCrash", Some("1")); + } + + let app_name = basename + .split('_') + .nth(1) + .and_then(|s| s.split('-').next()) + .unwrap_or(""); + + let breakpad_mapper = BufReader::new(File::open(LOGMAPPER_FILE)?); + let mut log_files = String::new(); + for line in breakpad_mapper.lines() { + let line = line?; + if let Some((key, val)) = line.split_once('=') { + if key.contains(process_name) { + log_files = val.to_string(); + break; + } + } + } + println!("get_crashed_log_file: Crashed process log file(s): {}", log_files); + if !app_name.is_empty() { + println!("get_crashed_log_file: Appname, Process_Crashed = {}, {}", app_name, process_name); + } + // Write each log file to LOG_FILES + let mut output = OpenOptions::new() + .create(true) + .append(true) + .open(LOG_FILES)?; + + for log_file in log_files + .split(',') + .map(str::trim) + .filter(|s| !s.is_empty()) + { + writeln!(output, "{}/{}", LOG_PATH, log_file)?; + } + + Ok(()) +} + +/// Deletes all but the most recent N files in a directory, sorted by modification time (descending). +/// +/// This function is used to enforce a retention policy for core/minidump files, +/// keeping only the newest `MAX_CORE_FILES` files and deleting the rest. +/// +/// # Arguments +/// * `dir_path` - Directory path as &str +/// +/// # Returns +/// * `Ok(())` on success, or an error if file operations fail +/// +/// # Side Effects +/// - Deletes files from the filesystem that exceed the retention limit +/// - Logs the deletion operations or their failure +pub fn delete_all_but_most_recent_files(dir_path: &str) -> io::Result<()> { + let path = Path::new(dir_path); + if !path.is_dir() { + return Ok(()); + } + + // Collect all files with their modification times + let mut files: Vec<_> = fs::read_dir(path)? + .filter_map(|entry| { + let entry = entry.ok()?; + let meta = entry.metadata().ok()?; + let mtime = meta.modified().ok()?; + if meta.is_file() { + Some((entry.path(), mtime)) + } else { + None + } + }) + .collect(); + + files.sort_by(|a, b| b.1.cmp(&a.1)); + + // Calculate number of files to delete + if files.len() > MAX_CORE_FILES { + // Delete the oldest files + for (file_path, _) in files.iter().skip(MAX_CORE_FILES) { + if let Err(e) = fs::remove_file(file_path) { + println!("delete_all_but_most_recent_files: Failed to delete file {:?}: {}", file_path, e); + } else { + println!("delete_all_but_most_recent_files: Deleted old dump file: {:?}", file_path); + } + } + } else { + println!("delete_all_but_most_recent_files: No files need to be deleted. Total files: {}", files.len()); + } + Ok(()) +} + +/// Cleans up the working directory by removing old, unfinished, and non-dump files, +/// and limits the number of dump files to the configured maximum. +/// +/// This function performs several cleanup operations: +/// - Removes files matching `*_mac*_dat*` older than 2 days +/// - On first startup, removes unfinished and non-dump files, and limits dump file count +/// - Removes version.txt if not uploading on startup +/// +/// # Arguments +/// * `work_dir` - Working directory path +/// * `dump_name` - Dump type ("coredump" or "minidump") +/// * `dump_extn` - Dump file extension pattern (e.g., "*.dmp*") +/// +/// # Returns +/// * `Ok(())` on success, or an error if file operations fail +/// +/// # Side Effects +/// - Deletes files from the filesystem based on various criteria +/// - Creates a flag file to indicate cleanup has been performed +pub fn cleanup(work_dir: &str, dump_name: &str, dump_extn: &str) -> std::io::Result<()> { + let work_dir_path = Path::new(work_dir); + + // Early exit if directory is missing or empty + if !work_dir_path.exists() || !work_dir_path.is_dir() || work_dir_path.read_dir()?.next().is_none() + { + println!("cleanup: Working directory {} is empty", work_dir); + return Ok(()); + } + + println!("cleanup: Cleaning up {} directory {}", dump_name, work_dir); + + // Remove files matching '*_mac*_dat*' older than 2 days + let cutoff = SystemTime::now() - Duration::from_secs(60 * 60 * 24 * 2); // 2 days + for entry in fs::read_dir(work_dir_path)? { + let entry = entry?; + let path = entry.path(); + if path.is_file() { + let fname = path.file_name().and_then(|n| n.to_str()).unwrap_or(""); + if fname.contains("_mac") && fname.contains("_dat") { + if let Ok(meta) = fs::metadata(&path) { + if let Ok(modified) = meta.modified() { + if modified < cutoff { + fs::remove_file(&path)?; + println!("cleanup: Removed file: {}", path.display()); + } + } + } + } + } + } + + // find and while loop logic + if !Path::new(UPLOAD_ON_STARTUP).exists() { + let version_txt = Path::new(work_dir).join("version.txt"); + if version_txt.exists() { + println!("cleanup: Removing version.txt file: {}", version_txt.display()); + let _ = fs::remove_file(&version_txt); + } + + let on_startup_flag = format!( + "{}_{}", + ON_STARTUP_DUMPS_CLEANED_UP_BASE, + if dump_name == "coredump" { "1" } else { "" } + ); + let on_startup_flag_path = Path::new(&on_startup_flag); + + if !on_startup_flag_path.exists() { + // Remove unfinished files: '*_mac*_dat*' + for entry in fs::read_dir(work_dir_path)? { + let entry = entry?; + let path = entry.path(); + let fname = path.file_name().and_then(|n| n.to_str()).unwrap_or(""); + if fname.contains("_mac") && fname.contains("_dat") { + fs::remove_file(&path)?; + println!("cleanup: Deleting unfinished file: {}", path.display()); + } + } + + // Remove non-dump files (not matching dump_extn) + for entry in fs::read_dir(work_dir_path)? { + let entry = entry?; + let path = entry.path(); + if path.is_file() { + let fname = path.file_name().and_then(|n| n.to_str()).unwrap_or(""); + if !fname.contains(dump_extn.trim_matches('*')) { + fs::remove_file(&path)?; + println!("cleanup: Deleting non-dump file: {}", path.display()); + } + } + } + + // Limit number of dump files + let _ = delete_all_but_most_recent_files(work_dir); + + touch(&on_startup_flag); + } + } else if dump_name == "coredump" { + let _ = fs::remove_file(UPLOAD_ON_STARTUP); + } + Ok(()) +} + +/// Returns the usage percent of the /tmp partition by invoking `df`. +/// +/// Mimics the shell logic: `df -h /tmp | grep '\tmp' | awk '{print $5}'` +/// This is used to determine if there's enough space to copy log files to /tmp. +/// +/// # Returns +/// * `Some(u8)` with the usage percent (0-100), or `None` if: +/// - The `df` command fails to execute +/// - The output doesn't contain expected data +/// - The percentage value cannot be parsed +#[inline] +fn get_tmp_usage_percent() -> Option { + let output = Command::new("df") + .arg("-h") + .arg("/tmp") + .output() + .ok()?; + + let stdout = String::from_utf8_lossy(&output.stdout); + + // Skip the header, look for the line containing "/tmp" + for line in stdout.lines().skip(1) { + if line.contains("/tmp") { + let fields: Vec<&str> = line.split_whitespace().collect(); + if let Some(usage) = fields.get(4) { + // usage is like "12%" + return usage.trim_end_matches('%').parse::().ok(); + } + } + } + None +} + +/// Adds crashed log files to the minidump tarball, processing each log file as needed. +/// +/// For each log file: +/// 1. Gets the file's last modified timestamp +/// 2. Creates a sanitized process log file name using device metadata +/// 3. Extracts the last N lines (N=500 for prod, 5000 otherwise) +/// 4. Writes those lines to a new file in the working directory +/// +/// # Arguments +/// * `device_data` - Reference to device metadata (for log file naming) +/// * `log_files` - Slice of log file paths to process and add +/// * `working_dir` - Directory where processed log files will be written +/// +/// # Returns +/// * `Ok(())` on success, or an error if file operations fail +/// +/// # Side Effects +/// - Creates new log files in the working directory +/// - Logs each file addition +pub fn add_crashed_log_file(device_data: &DeviceData, log_files: &[&str], working_dir: &str) -> io::Result<()> { + let line_count = if device_data.build_type == "prod" { 500 } else { 5000 }; + + for file_path in log_files { + let path = Path::new(file_path); + if path.is_file() { + if let Some(log_mod_ts) = get_last_modified_time_of_file(file_path) { + let process_log_name = set_log_file(device_data, &log_mod_ts, file_path); + let process_log_path = Path::new(working_dir).join(&process_log_name); + + let file = File::open(path)?; + let lines: Vec = BufReader::new(file) + .lines() + .map_while(Result::ok) + .collect(); + + let start = lines.len().saturating_sub(line_count); + let mut output = File::create(&process_log_path)?; + for line in &lines[start..] { + writeln!(output, "{}", line)?; + } + + println!("add_crashed_log_file: Adding File: {} to minidump tarball", process_log_path.display()); + } + } + } + Ok(()) +} + +/// Copies log files to a temporary directory under /tmp if there is enough free space. +/// +/// This function implements a space-aware strategy for log file handling: +/// - If /tmp usage is below 70%, copies each log file to a temp directory +/// - Otherwise, uses the original file paths to avoid filling /tmp +/// +/// # Arguments +/// * `tmp_dir_name` - Name for the temporary directory under /tmp +/// * `logfiles` - Slice of log file paths to potentially copy +/// +/// # Returns +/// * `Vec` containing paths to use for archiving (either in /tmp or original) +/// +/// # Side Effects +/// - May create a directory in /tmp +/// - May copy files to the temporary directory +pub fn copy_log_files_to_tmp(tmp_dir_name: &str, logfiles: &[&str]) -> Vec { + let tmp_dir = format!("/tmp/{}", tmp_dir_name); + let usage_percent = get_tmp_usage_percent().unwrap_or(0); + let limit = 70; + let mut out_files = Vec::new(); + + if usage_percent >= limit { + println!( + "copy_log_files_to_tmp: Skipping copying logs to tmp dir due to limited memory (used: {}%, limit: {}%)", + usage_percent, limit + ); + for &file in logfiles { + if Path::new(file).exists() { + out_files.push(file.to_string()); + } + } + } else { + println!( + "copy_log_files_to_tmp: Copying logs to tmp dir as memory available (used: {}%, limit: {}%)", + usage_percent, limit + ); + if let Err(e) = fs::create_dir_all(&tmp_dir) { + println!("copy_log_files_to_tmp: Failed to create tmp dir {}: {}", tmp_dir, e); + } + for &file in logfiles { + let src = Path::new(file); + if src.exists() { + let dest = Path::new(&tmp_dir).join(src.file_name().unwrap()); + if let Err(e) = fs::copy(src, &dest) { + println!("copy_log_files_to_tmp: Failed to copy {:?} to {:?}: {}", src, dest, e); + } else { + out_files.push(dest.to_string_lossy().to_string()); + } + } + } + println!("copy_log_files_to_tmp: Logs copied to {} temporary", tmp_dir); + } + out_files +} + +/// Uploads a crash dump file to S3 using the Rust implementation from crash-upload-cpc. +/// +/// This function is only available when the "shared_api" feature is enabled. +/// It implements the actual upload logic, handling encryption, TLS configuration, +/// and authentication based on device settings. +/// +/// # Arguments +/// * `args` - Arguments for the upload, with args[0] being the file to upload +/// * `dump_paths` - Reference to dump paths configuration +/// * `device_data` - Reference to device metadata +/// * `curl_log_option` - Curl logging options string +/// +/// # Returns +/// * `Ok(())` on successful upload, or an error if upload fails +/// +/// # Side Effects +/// - Creates a backup of the file before uploading +/// - Logs upload status and details +/// - Makes network requests to S3 +#[cfg(feature = "shared_api")] +pub fn upload_to_s3_lib(args: &[&str], dump_paths: &DumpPaths, device_data: &DeviceData, curl_log_option: &str) -> std::io::Result<()> { + // Call the Rust implementation from crash-upload-cpc + println!("upload_to_s3_lib: Uploading to S3 using Rust implementation"); + println!("upload_to_s3_lib: Tars to be uploaded: {:#?}", args); + let file = args.get(0).copied().unwrap_or(""); + println!("upload_to_s3_lib: ###DEBUG### File to upload: {:#?}", file); + if !file.is_empty() { + let backup_path = format!("{}.backup", file); + println!("upload_to_s3_lib: ###DEBUG### Backing up file to: {}", backup_path); + if let Err(e) = std::fs::copy(file, &backup_path) { + println!("upload_to_s3_lib: Warning - Failed to create backup: {}", e); + // Continue with upload even if backup fails + } + } + let mut force_mtls = String::new(); + let _ = utils::get_property_value_from_file(DEVICE_PROP_FILE, "FORCE_MTLS", &mut force_mtls); + let image_name = std::fs::read_to_string("/version.txt").unwrap().lines().next().unwrap().split("imagename:").nth(1).unwrap().trim().to_string(); + crash_upload_cpc::upload_to_s3(file, + dump_paths.get_working_dir(), + "", + dump_paths.get_dump_name(), + device_data.get_device_type(), + VERSION_FILE, + &image_name, + device_data.get_is_encryption_enabled(), + ENABLE_OSCP_STAPLING, + ENABLE_OSCP, + device_data.get_is_tls_enabled(), + device_data.get_build_type(), + device_data.get_model_num(), + curl_log_option, + device_data.get_is_t2_enabled(), + force_mtls.as_str(), + ) +} + +/// Calls the uploadDumpsToS3.sh script with the given arguments if it exists. +/// +/// This function is only available when the "shared_api" feature is NOT enabled. +/// It delegates the upload responsibility to a shell script. +/// +/// # Arguments +/// * `args` - Arguments to pass to the script, with args[0] being the file to upload +/// +/// # Returns +/// * `Ok(exit_status)` if the script ran successfully +/// * `Err` if the script wasn't found or failed with a non-zero exit code +/// +/// # Side Effects +/// - Executes an external shell script +/// - The shell script will make network requests and manipulate files +#[cfg(not(feature = "shared_api"))] +pub fn upload_to_s3_script(args: &[&str]) -> std::io::Result { + if Path::new(S3_UPLOAD_SCRIPT).exists() { + let mut cmd_args: Vec<&str> = args.to_vec(); + cmd_args.push("crash-upload"); + let status = std::process::Command::new(S3_UPLOAD_SCRIPT).args(&cmd_args).status()?; + if status.success() { + Ok(status) + } else { + Err(std::io::Error::new( + std::io::ErrorKind::Other, + format!("Script failed with status: {:?}", status), + )) + } + } else { + Err(std::io::Error::new( + std::io::ErrorKind::NotFound, + "uploadDumpsToS3.sh not found", + )) + } +} + +/// Calls getPrivacyControlMode from utils.sh and returns its output as a String. +/// +/// This function interfaces with the platform's privacy control system by +/// sourcing the utils.sh script and calling its getPrivacyControlMode function. +/// +/// # Returns +/// * `Some(String)` containing the privacy mode ("DO_NOT_SHARE", etc.) if successful +/// * `None` if: +/// - The utils.sh script doesn't exist +/// - The command execution fails +/// - The command exits with a non-zero status +/// +/// # Side Effects +/// - Executes a shell command +pub fn get_privacy_control_mode() -> Option { + let script = "/lib/rdk/utils.sh"; + if Path::new(script).exists() { + let output = Command::new("sh") + .arg("-c") + .arg(format!(". {}; getPrivacyControlMode", script)) + .output() + .ok()?; + if output.status.success() { + Some(String::from_utf8_lossy(&output.stdout).trim().to_string()) + } else { + None + } + } else { + None + } +} + +/// Processes all dump files in the working directory: sanitizes, compresses, and uploads or saves them. +/// +/// This function implements the main dump processing workflow: +/// 1. Finds all dump files in the working directory +/// 2. For each file: +/// - Sanitizes and renames it +/// - Processes telemetry info for minidumps +/// - Creates a tarball with the dump and relevant log files +/// - Cleans up temporary files +/// 3. Hands off tarballs for upload or local storage +/// +/// # Arguments +/// * `device_data` - Reference to device metadata (for naming and telemetry) +/// * `dump_paths` - Reference to dump paths and configuration +/// * `crash_ts` - Crash timestamp string for naming files +/// * `no_network` - If true, skips upload and just saves the dump locally +/// +/// # Side Effects +/// - Modifies files in the working directory (renames, compresses, deletes) +/// - May create or remove log files and temporary directories +/// - Calls other functions that may upload files or send telemetry +pub fn process_dumps( + device_data: &DeviceData, + dump_paths: &DumpPaths, + crash_ts: &str, + no_network: bool, +) { + utils::flush_logger(); + let work_dir = dump_paths.get_working_dir(); + + let files = match find_dump_files(work_dir, dump_paths.get_dumps_extn()) { + Ok(f) => f, + Err(e) => { + println!("process_dumps: Error finding dump files: {}", e); + return; + } + }; + + for file in files { + println!("process_dumps: -- Processing file: {}", file.display()); + let orig_file_name = basename(&file); + + if !should_process_dump(dump_paths.get_dump_name(), device_data.get_build_type(), &orig_file_name) { + println!("process_dumps: Skipping file {} ", orig_file_name); + continue; + } + let sanitized = match sanitize_and_rename(&file) { + Ok(f) => f, + Err(e) => { + println!("process_dumps: Sanitize/rename failed for {:?}: {}", file, e); + continue; + } + }; + + if dump_paths.get_dump_name() != "coredump" { + process_crash_t2_info(&sanitized.to_string_lossy(), device_data.is_t2_enabled); + } + + if file.is_file() { + if is_tarball(&sanitized) { + println!( + "process_dumps: Skip archiving {} as it is a tarball already.", + basename(&sanitized) + ); + continue; + } + + let mod_date = get_last_modified_time_of_file(&sanitized.to_string_lossy()) + .unwrap_or_else(|| crash_ts.to_string()); + let ts_for_naming = if crash_ts.is_empty() { &mod_date } else { crash_ts }; + + let mut dump_file_name = + set_log_file(device_data, ts_for_naming, &sanitized.to_string_lossy()); + + if dump_file_name.len() >= 135 { + if let Some(pos) = dump_file_name.find('_') { + dump_file_name = dump_file_name[pos + 1..].to_string(); + } + } + let dump_file_name = dump_file_name.replace("<#=#>", "_"); + + let dump_dir = Path::new(work_dir); + let dump_file_path = dump_dir.join(&dump_file_name); + println!("process_dumps: Dump file path: {}", dump_file_path.display()); + println!("process_dumps: Dump File Name: {}", dump_file_name); + + + if sanitized != dump_file_path { + if let Err(e) = safe_rename(&sanitized, &dump_file_path) { + println!( + "process_dumps: Failed to rename {} to {}: {}", + basename(&sanitized), + basename(&dump_file_name), + e + ); + continue; + } + } + + ensure_core_log_exists(); + + let version_file_path = Path::new(work_dir).join("version.txt"); + if !version_file_path.exists() { + let _ = fs::copy(VERSION_FILE, &version_file_path); + println!("process_dumps: Version file path: {}", version_file_path.display()); + } + + if let Ok(metadata) = std::fs::metadata(&dump_file_path) { + println!( + "process_dumps: Size of the file: {} bytes", + metadata.len() + ); + } + + if device_data.get_is_t2_enabled() && !dump_paths.get_dump_name().is_empty() { + t2_count_notify("SYST_ERR_MINIDPZEROSIZE", Some("1")); + } + + let logfiles: Vec = if dump_paths.dump_name == "coredump" { + vec![VERSION_FILE.to_string(), CORE_LOG.to_string()] + } else { + let crash_url_file = format!("{}/crashed_url.txt", LOG_PATH); + let crashed_url_file = if Path::new(&crash_url_file).exists() { + crash_url_file.clone() + } else { + "".to_string() + }; + vec![VERSION_FILE.to_string(), CORE_LOG.to_string(), crashed_url_file] + }; + + let logfiles_abs: Vec = logfiles + .iter() + .filter(|s| !s.is_empty()) + .map(|s| { + if Path::new(s).is_absolute() { + s.clone() + } else { + format!("{}/{}", LOG_PATH, s) + } + }) + .collect(); + + let logfiles_refs: Vec<&str> = logfiles_abs.iter().map(|s| s.as_str()).collect(); + + println!( + "process_dumps: Log files (REF) to be added: \n{:#?}\n", + logfiles_refs + ); + + if dump_paths.dump_name == "minidump" { + if let Err(e) = add_crashed_log_file(device_data, &logfiles_refs, work_dir) { + println!("process_dumps: Failed to add crashed log file: {}", e); + } + } + + let tgz_file = if dump_paths.dump_name == "coredump" { + dump_dir.join(format!("{}.core.tgz", dump_file_name)) + } else { + dump_dir.join(format!("{}.tgz", dump_file_name)) + }; + + let tar_result = compress_files( + tgz_file.to_str().unwrap(), + &[dump_file_path.to_str().unwrap()], + &logfiles_refs, + ); + + if tar_result.is_ok() { + println!( + "process_dumps: Success Compressing the files, {}\n {}\n {}\n {}\n", + basename(&tgz_file), + basename(&dump_file_path), + VERSION_FILE, + basename(CORE_LOG) + ); + } else { + println!("process_dumps: Compression failed, will retry after copying logs to /tmp"); + let out_files = copy_log_files_to_tmp(&dump_file_name, &logfiles_refs); + let tmp_dir = format!("/tmp/{}", dump_file_name); + let tmp_dump_file = Path::new(&tmp_dir).join(&dump_file_name); + if let Err(e) = fs::copy(&dump_file_path, &tmp_dump_file) { + println!("process_dumps: Failed to copy dump file to tmp: {}", e); + } + let mut retry_files = vec![tmp_dump_file.to_str().unwrap()]; + retry_files.extend(out_files.iter().map(|s| s.as_str())); + let retry_tar_result = compress_files( + tgz_file.to_str().unwrap(), + &retry_files, + &[], + ); + if retry_tar_result.is_ok() { + println!( + "process_dumps: Success Compressing the files, {} {}", + basename(&tgz_file), + basename(&tmp_dump_file) + ); + } else { + println!("process_dumps: Compression Failed ."); + } + } + + if let Ok(metadata) = std::fs::metadata(&tgz_file) { + println!( + "process_dumps: Size of the compressed file: {} bytes", + metadata.len() + ); + } + + // 21. Remove /tmp/ temp dir if it exists + let tmp_dir = format!("/tmp/{}", dump_file_name); + if Path::new(&tmp_dir).is_dir() { + rm_rf(&tmp_dir); + println!( + "process_dumps: Temporary Directory Deleted: {}", + basename(&tmp_dir) + ); + } + + // 22. Remove the original dump file after compression + println!("process_dumps: Skipping original dump file removal for debugging purposes"); + if false { + rm_rf(dump_file_path.to_str().unwrap()); + } + + // 23. For minidump, remove logs from working dir + if dump_paths.dump_name != "coredump" { + println!("process_dumps: ###DEBUG### Skipping log removal in working dir for debugging purposes "); + if false { + let _ = remove_logs(work_dir); + } + } + } + } + handle_tarballs(device_data, dump_paths, no_network, crash_ts); +} + +/// Compresses the given files into a tarball using the `tar` command. +/// +/// This function creates a gzipped tarball containing both the primary dump files +/// and any associated log files by executing the system `tar` command. +/// +/// # Arguments +/// * `tarball_path` - Output tarball file path +/// * `dump_files` - Primary dump files to include in the tarball +/// * `log_files` - Additional log files to include in the tarball +/// +/// # Returns +/// * `Ok(())` if compression succeeded, error otherwise +/// +/// # Side Effects +/// - Creates a new tarball file at the specified path +/// - Executes the system `tar` command +#[inline] +pub fn compress_files( + tarball_path: &str, + dump_files: &[&str], + log_files: &[&str], +) -> std::io::Result<()> { + let mut args = vec!["-zcvf", tarball_path]; + for file in dump_files { + args.push(file); + } + for file in log_files { + args.push(file); + } + let status = Command::new("tar").args(&args).status()?; + + if status.success() { + Ok(()) + } else { + Err(io::Error::new( + io::ErrorKind::Other, + "compress_files: tar compression failed", + )) + } +} + +/// Checks if the given file is a tarball (ends with .tgz). +/// +/// A simple utility function that examines the file extension to determine +/// if a file is a compressed tarball archive. +/// +/// # Arguments +/// * `file` - Reference to a `Path` to check +/// +/// # Returns +/// * `true` if the file has a .tgz extension, `false` otherwise +#[inline] +fn is_tarball(file: &Path) -> bool { + file.extension().map(|e| e == "tgz").unwrap_or(false) +} + +/// Sanitizes the file name and renames the file if needed. +/// +/// This function sanitizes the file path by removing unsafe characters, then +/// renames the file on disk if the sanitized name differs from the original. +/// This ensures filenames are compatible with upload and storage systems. +/// +/// # Arguments +/// * `file` - Reference to the `Path` of the file to sanitize +/// +/// # Returns +/// * `Ok(PathBuf)` containing the sanitized path (which may be the same as the +/// original if no sanitization was needed) +/// * `Err` if the rename operation fails +/// +/// # Side Effects +/// - May rename the file on the filesystem if sanitization changes the name +#[inline] +fn sanitize_and_rename(file: &Path) -> io::Result { + let orig = file.to_string_lossy(); + let sanitized = sanitize(&orig); + if sanitized != orig { + safe_rename(&*orig, &sanitized)?; + Ok(PathBuf::from(sanitized)) + } else { + Ok(file.to_path_buf()) + } +} + +/// Removes all .log and .txt files from the given directory. +/// +/// This function is used to clean up log files after they've been packaged +/// into a tarball. It scans the directory and deletes any file with a .log or +/// .txt extension. +/// +/// # Arguments +/// * `working_dir` - Directory path to clean +/// +/// # Returns +/// * `Ok(())` on success, or an error if directory operations fail +/// +/// # Side Effects +/// - Deletes .log and .txt files from the specified directory +#[inline] +fn remove_logs(working_dir: &str) -> io::Result<()> { + for entry in fs::read_dir(working_dir)? { + let entry = entry?; + let path = entry.path(); + if path.is_file() { + let name = path.file_name().unwrap_or_default().to_string_lossy(); + if name.ends_with(".log") || name.ends_with(".txt") { + println!("remove_logs: Removing {}", path.display()); + fs::remove_file(path)?; + } + } + } + Ok(()) +} + +/// Finds all files in a directory matching the given pattern. +/// +/// This function scans a directory and collects all files whose names match +/// the specified pattern (using the `matches_pattern` function). +/// +/// # Arguments +/// * `dir` - Directory path to search +/// * `pattern` - Pattern to match in file names (e.g., "*.dmp*", "*.core.tgz") +/// +/// # Returns +/// * `Ok(Vec)` containing paths to all matching files +/// * `Err` if there was an error reading the directory +pub fn find_dump_files(dir: &str, pattern: &str) -> std::io::Result> { + let dir_path = std::path::Path::new(dir); + let mut files = Vec::new(); + if !dir_path.is_dir() { + return Ok(files); + } + for entry in std::fs::read_dir(dir_path)? { + let entry = entry?; + let name = entry.file_name(); + let name = name.to_string_lossy(); + if matches_pattern(&name, pattern) { + files.push(entry.path()); + } + } + Ok(files) +} + +/// Handles all tarballs in the working directory: applies rate limiting, privacy checks, +/// uploads with retries, and performs post-upload cleanup. +/// +/// This function is the main coordinator for tarball processing after dumps have been +/// compressed. It finds all tarballs in the working directory and processes each one +/// through `handle_single_tarball()`. +/// +/// # Arguments +/// * `device_data` - Reference to device metadata for upload identification +/// * `dump_paths` - Reference to dump paths and configuration +/// * `no_network` - Whether to skip upload and just save dumps locally +/// * `crash_ts` - Crash timestamp string for file naming +/// +/// # Side Effects +/// - May upload files to S3 +/// - May delete or rename files +/// - May send telemetry notifications +/// - Logs processing status for each tarball +fn handle_tarballs(device_data: &DeviceData, dump_paths: &DumpPaths, no_network: bool, crash_ts: &str) { + if is_box_rebooting(device_data.is_t2_enabled) { + return; + } + + let tarballs = match find_dump_files(dump_paths.get_working_dir(), dump_paths.get_tar_extn()) { + Ok(t) => t, + Err(e) => { + println!("handle_tarballs: Error finding tarballs: {}", e); + return; + } + }; + println!("handle_tarballs: Found tarballs: {:?}", tarballs); + + for tarball in tarballs { + if let Err(e) = handle_single_tarball(device_data, dump_paths, &tarball, no_network, crash_ts) { + println!("handle_tarballs: Error handling tarball {:?}: {}", tarball, e); + } + } +} + +/// Handles a single tarball: checks rate limits, privacy, uploads with retries, and cleans up. +/// +/// This function implements the complete processing workflow for a single tarball: +/// 1. Checks recovery time and rate limits +/// 2. Handles no-network mode if enabled +/// 3. Checks privacy settings +/// 4. Sanitizes the filename for S3 +/// 5. Uploads the tarball with retries +/// 6. Performs post-upload cleanup +/// +/// # Arguments +/// * `device_data` - Reference to device metadata for upload identification +/// * `dump_paths` - Reference to dump paths and configuration +/// * `tarball` - Path to the tarball file to process +/// * `no_network` - Whether to skip upload and just save dumps locally +/// * `crash_ts` - Crash timestamp string for file naming +/// +/// # Returns +/// * `Ok(())` on success, or an error if any operation fails +/// +/// # Side Effects +/// - May upload files to S3 +/// - May delete or rename files +/// - May set recovery time +/// - May remove pending dumps +fn handle_single_tarball(device_data: &DeviceData, dump_paths: &DumpPaths, tarball: &Path, no_network: bool, crash_ts: &str) -> std::io::Result<()> { + let tarball_str = tarball.to_string_lossy(); + let s3_filename = tarball.file_name().and_then(|n| n.to_str()).unwrap_or(""); + let s3_filename_sanitized = s3_filename.replace("<#=#>", "_"); + let parent = tarball.parent().unwrap_or_else(|| Path::new(dump_paths.get_working_dir())); + let s3_full_path = parent.join(&s3_filename_sanitized); + + // 1. Rate limiting and recovery time + if is_recovery_time_reached() { + rm_rf(DENY_UPLOAD_FILE); + } else { + println!("handle_single_tarball: Shifting the recovery time forward."); + set_recovery_time(); + let _ = remove_pending_dumps( + dump_paths.get_working_dir(), + dump_paths.get_dumps_extn(), + ); + return Ok(()); + // TODO: Should Exit? + } + + if dump_paths.get_dump_name() == "minidump" && is_upload_limit_reached(dump_paths.get_ts_file()) + { + println!("handle_single_tarball: Upload rate limit has been reached."); + mark_as_crash_loop_and_upload(&tarball_str); + set_recovery_time(); + let _ = remove_pending_dumps( + dump_paths.get_working_dir(), + dump_paths.get_dumps_extn(), + ); + return Ok(()); + // TODO: Should Exit? + } + + // 2. no_network logic: skip upload and just save the dump + if dump_paths.get_dump_name() == "minidump" && no_network { + println!("handle_single_tarball: Network is not available, skipping upload and saving dump."); + save_dump(dump_paths.get_minidumps_path(), s3_filename, Some(crash_ts)); + return Ok(()); + } + + // 3. Privacy mode check + if device_data.get_device_type() == "mediaclient" && is_privacy_mode_do_not_share() { + println!("handle_single_tarball: Privacy Mode is DO_NOT_SHARE. Stop Uploading the data to the cloud"); + let _ = remove_pending_dumps( + dump_paths.get_working_dir(), + dump_paths.get_dumps_extn(), + ); + return Ok(()); + } + + // 4. Ensure tarball filename is sanitized for S3 + if s3_filename != s3_filename_sanitized { + rename_tarball_for_s3(tarball, &s3_filename_sanitized)?; + } + + // 5. Upload with retries + let upload_success = upload_tarball_with_retries(s3_full_path.to_str().unwrap(), dump_paths, device_data); + + // 6. Post-upload cleanup + post_upload_cleanup(upload_success, dump_paths, &s3_full_path, &s3_filename_sanitized); + + Ok(()) +} + +/// Returns true if privacy mode is DO_NOT_SHARE (from utils.sh). +/// +/// This function checks if the device's privacy mode is set to "DO_NOT_SHARE", +/// which indicates that no data should be uploaded to the cloud. +/// +/// # Returns +/// * `true` if privacy mode is "DO_NOT_SHARE", `false` otherwise +/// +/// # Side Effects +/// - Calls `get_privacy_control_mode()` which executes a shell command +#[inline] +fn is_privacy_mode_do_not_share() -> bool { + matches!(get_privacy_control_mode().as_deref(), Some("DO_NOT_SHARE")) +} + +/// Renames a tarball file to a sanitized name for S3 upload. +/// +/// This function replaces special characters in the tarball filename that might +/// cause problems during S3 upload. It performs the actual filesystem rename +/// operation. +/// +/// # Arguments +/// * `tarball` - Path to the original tarball +/// * `sanitized_name` - Sanitized file name to use +/// +/// # Returns +/// * `Ok(())` on success, or an error if the rename fails +/// +/// # Side Effects +/// - Renames a file on the filesystem +fn rename_tarball_for_s3(tarball: &Path, sanitized_name: &str) -> std::io::Result<()> { + let parent = tarball.parent().unwrap_or_else(|| Path::new("")); + let orig_path = parent.join(tarball.file_name().unwrap()); + let new_path = parent.join(sanitized_name); + safe_rename(&orig_path, &new_path) +} + +/// Uploads a tarball to S3, retrying up to 3 times. +/// +/// This function attempts to upload a tarball to S3, with up to 3 retry attempts +/// if the upload fails. It handles both the Rust and shell script upload implementations +/// based on the feature flags, and logs the upload status. +/// +/// # Arguments +/// * `s3_full_path` - Full path to the tarball file to upload +/// * `dump_paths` - Reference to dump paths and configuration +/// * `device_data` - Reference to device metadata for upload identification +/// +/// # Returns +/// * `true` if any upload attempt succeeded, `false` if all attempts failed +/// +/// # Side Effects +/// - Makes network requests to S3 +/// - May create temporary files for upload parameters +/// - May send telemetry notifications +/// - Logs upload status +fn upload_tarball_with_retries( + s3_full_path: &str, + dump_paths: &DumpPaths, + device_data: &DeviceData, +) -> bool { + let mut upload_status = false; + for attempt in 1..=3 { + println!("upload_tarball_with_retries: {}: {} S3 Upload Attempt {}", attempt, dump_paths.get_dump_name(), s3_full_path); + + let curl_log_option = "%{remote_ip} %{remote_port}"; + + let upload_res: Result<(), String> = { + #[cfg(feature = "shared_api")] + { + match upload_to_s3_lib(&[s3_full_path], dump_paths, device_data, curl_log_option) { + Ok(()) => Ok(()), + Err(e) => Err(format!("upload_to_s3 failed: {}", e)), + } + } + #[cfg(not(feature = "shared_api"))] + { + if let Err(e) = write_uploadtos3params(dump_paths, device_data, "", ENABLE_OSCP_STAPLING, ENABLE_OSCP, curl_log_option) { + println!("upload_tarball_with_retries: Failed to write upload parameters: {}", e); + return false; + } + let res = upload_to_s3_script(&[s3_full_path]); + if let Err(e) = std::fs::remove_file(S3_UPLOAD_PARAM_FILE) { + println!("upload_tarball_with_retries: Failed to remove {}: {}", S3_UPLOAD_PARAM_FILE, e); + } + match res { + Ok(exit_status) if exit_status.success() => Ok(()), + Ok(exit_status) => Err(format!( + "Script ran but failed with status: {:?}", + exit_status + )), + Err(e) => Err(format!("upload_to_s3_script failed: {}", e)), + } + } + }; + + match upload_res { + Ok(()) => { + println!( + "upload_tarball_with_retries: {} uploadToS3 SUCCESS", + dump_paths.get_dump_name() + ); + upload_status = true; + if dump_paths.get_dump_name() == "minidump" + && device_data.get_is_t2_enabled() + { + t2_count_notify("SYST_INFO_minidumpUpld", Some("1")); + } + break; + } + Err(ref e) if e.contains("Script ran but failed with status") => { + println!( + "upload_tarball_with_retries: S3 Amazon Upload of {} failed with script exit status: {}", + dump_paths.get_dump_name(), + e + ); + } + Err(e) => { + println!("upload_tarball_with_retries: Upload to S3 failed: {}", e); + } + } + std::thread::sleep(std::time::Duration::from_secs(2)); + } + upload_status +} + +/// Cleans up after upload: removes tarball if successful, logs timestamp, or saves/removes on failure. +/// +/// This function handles the post-upload cleanup process, with different behavior based on +/// whether the upload succeeded: +/// - On success: Removes the tarball and logs the upload timestamp +/// - On failure: For minidumps, saves the dump locally; for other types, removes the file +/// +/// # Arguments +/// * `upload_success` - Whether the upload succeeded +/// * `dump_paths` - Reference to dump paths and configuration +/// * `tarball` - Path to the tarball file +/// * `s3_filename` - Name of the tarball file +/// +/// # Side Effects +/// - May delete files from the filesystem +/// - May log timestamps to the timestamp file +/// - May save dumps to the minidumps directory +fn post_upload_cleanup(upload_success: bool, dump_paths: &DumpPaths, tarball: &Path, s3_filename: &str) { + let parent = tarball.parent().unwrap_or_else(|| Path::new("")); + let s3_path = parent.join(s3_filename); + println!("post_upload_cleanup: s3_path: {}", s3_path.display()); + println!("post_upload_cleanup: s3_filename: {}", s3_filename); + + println!("post_upload_cleanup: ##DEBUG## Skipping Cleanup intentionally for debugging purposes"); + if false { + + if upload_success { + println!("post_upload_cleanup: Removing file {}", s3_filename); + let _ = fs::remove_file(&s3_path); + log_upload_timestamp(dump_paths.get_ts_file()); + } else { + println!("post_upload_cleanup: S3 Amazon Upload of {} Failed..!", dump_paths.get_dump_name()); + if dump_paths.get_dump_name() == "minidump" { + println!("post_upload_cleanup: Check and save the dump {}", s3_filename); + save_dump(dump_paths.get_minidumps_path(), s3_filename, None); + } else { + println!("post_upload_cleanup: Removing file {}", s3_filename); + let _ = fs::remove_file(&s3_path); + } + } + // TODO: Should exit? + } +} diff --git a/crash-upload/src/main.rs b/crash-upload/src/main.rs new file mode 100644 index 0000000..55b30f8 --- /dev/null +++ b/crash-upload/src/main.rs @@ -0,0 +1,290 @@ +//! Crash Upload Binary - Main Entry Point +//! +//! This module provides the entry point for the crash upload utility, which detects, +//! processes, and uploads crash dumps (minidumps and coredumps) to cloud storage. +//! +//! # Command-Line Usage +//! +//! The binary accepts three required command-line arguments: +//! ``` +//! crash-upload +//! ``` +//! +//! - `dump_flag`: Integer (1 for coredump processing, any other value for minidump) +//! - `upload_flag`: String ("secure" for secure paths, any other value for standard paths) +//! - `wait_for_lock`: String ("wait_for_lock" to wait if locked, any other value to exit) +//! +//! # Workflow +//! +//! The main workflow consists of: +//! 1. Configuration and initialization (loading device data, setting paths) +//! 2. Early exit checks (no dumps exist, box is rebooting) +//! 3. Network and system time verification +//! 4. Dump processing (finding, compressing, and uploading dumps) +//! 5. Cleanup and finalization +//! +//! # Integration Points +//! +//! - Device property system (for device metadata) +//! - Filesystem (for finding and manipulating dump files) +//! - Network subsystem (for upload capabilities) +//! - System time service (for timestamp generation) +//! - Mutex-like locking (for preventing concurrent execution) +//! +// standard library imports +use std::path::Path; +use std::time::Duration; +use std::{env, fs, thread}; + +// external crate imports +use chrono::Local; + +// crashupload internal module imports +mod constants; +mod crashupload_utils; + +// Utility crates +use crashupload_utils::*; + +/// Main entry point for the crash upload binary. +/// +/// This function implements the complete crash upload workflow: +/// 1. Parse command-line arguments and initialize configuration +/// 2. Configure paths based on dump type (minidump or coredump) +/// 3. Check for early exit conditions (no dumps, box rebooting) +/// 4. Wait for network and system time if needed +/// 5. Process and upload dumps with retry capability +/// 6. Clean up and exit +/// +/// # Command-Line Arguments +/// +/// * `args[1]`: Dump flag (1 for coredump, any other value for minidump) +/// * `args[2]`: Upload flag ("secure" for secure paths, any other value for standard) +/// * `args[3]`: Wait flag ("wait_for_lock" to wait if locked, any other to exit) +/// +/// # Side Effects +/// +/// - Creates and removes lock files +/// - May upload files to cloud storage +/// - May delete or compress files on the filesystem +/// - Logs progress to stdout +/// - May exit process early under certain conditions +fn main() { + println!("main(): Starting Crash Upload Binary..."); + + // TODO: Signal handling (trap/finalize on SIGTERM/SIGKILL/EXIT) + + // Parse command-line arguments + let args: Vec = env::args().collect(); + if args.len() != 4 { + println!("Usage: {} ", args[0]); + std::process::exit(1); + } + + let dump_flag = args[1].parse::().expect("Dump flag must be a number"); + let upload_flag = &args[2]; + let wait_for_lock = &args[3]; + + // Instantiate configuration structs + println!("main(): Instantiating DumpPaths and DeviceData instances..."); + let mut device_data = constants::DeviceData::new(); + let mut dump_paths = constants::DumpPaths::new(); + + // Populate device data from system properties + crashupload_utils::set_device_data(&mut device_data); + + // Set dump paths based on upload flag + if upload_flag == "secure" { + dump_paths.set_core_path("/opt/secure/corefiles"); + dump_paths.set_minidumps_path("/opt/secure/minidumps"); + } else { + dump_paths.set_core_path("/var/lib/systemd/coredump"); + dump_paths.set_minidumps_path("/opt/minidumps"); + } + + // Exit early if no dumps exist + if !crashupload_utils::check_dumps_exist(dump_paths.get_minidumps_path(), dump_paths.get_core_path()) { + println!("main(): No dumps found in {} or {}. Exiting...", dump_paths.get_minidumps_path(), dump_paths.get_core_path()); + std::process::exit(0); + } + + // Set secure dump status if needed + crashupload_utils::get_secure_dump_status(&mut dump_paths); + + // Generate crash timestamp + let crash_ts = Local::now().format("%Y-%m-%d-%H-%M-%S").to_string(); + + // Configure dump paths and metadata based on dump_flag + if dump_flag == 1 { + println!("main(): Starting coredump processing..."); + dump_paths.set_dump_name("coredump"); + let core_path = dump_paths.get_core_path().to_string(); + dump_paths.set_working_dir(&core_path); + dump_paths.set_dumps_extn("*core.prog*.gz*"); + dump_paths.set_tar_extn("*.core.tgz"); + dump_paths.set_lock_dir_prefix("/tmp/.uploadCoredumps"); + dump_paths.set_crash_portal_path("/opt/crashportal_uploads/coredumps/"); + } else { + println!("main(): Starting minidump processing..."); + dump_paths.set_dump_name("minidump"); + let minidumps_path = dump_paths.get_minidumps_path().to_string(); + dump_paths.set_working_dir(&minidumps_path); + dump_paths.set_dumps_extn("*.dmp*"); + dump_paths.set_tar_extn("*.dmp.tgz"); + dump_paths.set_lock_dir_prefix("/tmp/.uploadMinidumps"); + dump_paths.set_crash_portal_path("/opt/crashportal_uploads/coredumps/"); + thread::sleep(Duration::from_secs(5)); + }; + dump_paths.set_ts_file(format!("/tmp/.{}_upload_timestamps", dump_paths.get_dump_name())); + + println!("main(): [DEBUG] dump_paths: {:#?}\n", dump_paths); + println!("main(): [DEBUG] device_data: {:#?}\n ", device_data); + + // Locking logic + if wait_for_lock == "wait_for_lock" { + crashupload_utils::create_lock_or_wait(dump_paths.get_lock_dir_prefix()); + } else { + crashupload_utils::create_lock_or_exit(dump_paths.get_lock_dir_prefix(), device_data.get_is_t2_enabled()); + } + + // Defer upload if device just booted (hybrid/mediaclient) + let tmp_device_type = device_data.get_device_type(); + if tmp_device_type == "hybrid" || tmp_device_type == "mediaclient" { + let uptime_str = fs::read_to_string("/proc/uptime").expect("Unable to read uptime"); + let uptime_val = uptime_str + .split('.') + .next() + .unwrap_or("0") + .trim() + .parse::() + .unwrap_or(0); + if uptime_val < 480 { + let sleep_time = 480 - uptime_val; + println!("main(): Deferring reboot for {} seconds", sleep_time); + thread::sleep(Duration::from_secs(sleep_time)); + if Path::new(constants::CRASH_UPLOAD_REBOOT_FLAG).exists() { + println!("main(): Process crashed exiting from the Deferring reboot"); + finalize(&dump_paths); + std::process::exit(0); + } + } + } + + // Check if working directory is empty + let w_dir = dump_paths.get_working_dir(); + if crashupload_utils::is_dir_empty_or_unreadable(w_dir) { + println!("main(): Working directory is empty or unreadable: {}", w_dir); + crashupload_utils::finalize(&dump_paths); + std::process::exit(0); // or exit(1) if you want to signal error + } + + // Network availability check + let mut counter = 1; + let mut no_network = false; + let route_file = Path::new(constants::NETWORK_FILE); + + while counter <= constants::NETWORK_CHECK_ITERATION { + println!("main(): Check network status count {}", counter); + if route_file.exists() { + println!("main(): Route is Available break the loop"); + break; + } else { + println!( + "main(): Route is not available, Sleep for {} seconds", + constants::NETWORK_CHECK_TIMEOUT + ); + thread::sleep(Duration::from_secs(constants::NETWORK_CHECK_TIMEOUT as u64)); + counter += 1; + } + } + + if !route_file.exists() { + println!("main(): Route is not available. tar dump and save it, as max wait reached"); + no_network = true; + } + + // System time availability check + println!("main(): IP Acquisition completed, Test if system time is received"); + let stt_file = Path::new(constants::SYSTEM_TIME_FILE); + if !stt_file.exists() { + while counter <= constants::SYSTEM_TIME_ITERATION { + if !stt_file.exists() { + println!("main(): Waiting for STT, iteration {}", counter); + thread::sleep(Duration::from_secs(constants::SYSTEM_TIME_TIMEOUT as u64)); + } else { + println!("main(): Received {} flag", constants::SYSTEM_TIME_FILE); + break; + } + + if counter == constants::SYSTEM_TIME_ITERATION { + println!("main(): Continue without {} flag", constants::SYSTEM_TIME_FILE); + } + counter += 1; + } + } else { + println!("main(): Received {} flag", constants::SYSTEM_TIME_FILE); + } + + // trap finalize EXIT + + // Wait for coredump completion if needed + if !Path::new(constants::COREDUMP_MTX_FILE).exists() && dump_flag == 1 { + println!("main(): Waiting for Coredump completion"); + thread::sleep(Duration::from_secs(21)); + } + + // Early exit if box is rebooting + if is_box_rebooting(device_data.get_is_t2_enabled()) { + crashupload_utils::finalize(&dump_paths); + std::process::exit(0); + } + + // Print device MAC address + println!("main(): Mac Address is {}", device_data.get_mac_addr()); + + // Count dumps using utility function + let dump_count = match crashupload_utils::get_file_count( + dump_paths.get_working_dir(), + dump_paths.get_dumps_extn(), + ) { + Ok(dump_cnt) => dump_cnt, + Err(_) => 0, + }; + if dump_count == 0 { + println!("main(): No {} for uploading exist", dump_paths.get_dump_name()); + crashupload_utils::finalize(&dump_paths); + std::process::exit(0); + } + + // Cleanup old dumps using utility function + let _ = cleanup( + dump_paths.get_working_dir(), + dump_paths.get_dump_name(), + dump_paths.get_dumps_extn(), + ); + + // Print portal URL and build ID using getters + println!("main(): Portal URL {}", device_data.get_portal_url()); + println!("main(): buildID is {}", device_data.get_sha1()); + + // Final check: working directory must be a directory + if !Path::new(w_dir).is_dir() { + std::process::exit(1); + } + let image_name_main = std::fs::read_to_string("/version.txt").unwrap().lines().next().unwrap().split("imagename:").nth(1).unwrap().trim().to_string(); + println!("main(): Device Firmware: {}", image_name_main); + + // Main processing loop (up to 3 attempts, as in shell script) + for _ in 0..3 { + let files = crashupload_utils::find_dump_files( + dump_paths.get_working_dir(), + dump_paths.get_dumps_extn(), + ).unwrap_or_default(); + if files.is_empty() { + break; + } + crashupload_utils::process_dumps(&device_data, &dump_paths, &crash_ts, no_network); + } + + finalize(&dump_paths); +} diff --git a/crates/curl-interface/Cargo.toml b/crates/curl-interface/Cargo.toml new file mode 100644 index 0000000..e057a09 --- /dev/null +++ b/crates/curl-interface/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "curl-interface" +version = "0.1.0" +edition = "2021" +description = "Curl interface" +repository = "https://github.com/rdkcentral/crashupload/tree/feature-sample-rust/crates/curl-interface" +homepage = "https://github.com/rdkcentral/crashupload/tree/feature-sample-rust/crates/curl-interface" + +[dependencies] +# Uncomment to Link this library dynamically +#crate_type = ["dynlib"] diff --git a/crates/curl-interface/src/lib.rs b/crates/curl-interface/src/lib.rs new file mode 100644 index 0000000..35b19ea --- /dev/null +++ b/crates/curl-interface/src/lib.rs @@ -0,0 +1,16 @@ +pub fn add(left: u64, right: u64) -> u64 { + left + right +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn it_works() { + let result = add(2, 2); + assert_eq!(result, 4); + } +} + +// Placeholder for future functionality \ No newline at end of file diff --git a/crates/json-parser-interface/Cargo.toml b/crates/json-parser-interface/Cargo.toml new file mode 100644 index 0000000..d87de3f --- /dev/null +++ b/crates/json-parser-interface/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "json-parser-interface" +version = "0.1.0" +edition = "2021" +description = "JSON Parsing interface" +repository = "https://github.com/rdkcentral/crashupload/tree/feature-sample-rust/crates/json-parser-interface" +homepage = "https://github.com/rdkcentral/crashupload/tree/feature-sample-rust/crates/json-parser-interface" + +[dependencies] +# Uncomment to Link this library dynamically +#crate_type = ["dynlib"] diff --git a/crates/json-parser-interface/src/lib.rs b/crates/json-parser-interface/src/lib.rs new file mode 100644 index 0000000..a5e0f28 --- /dev/null +++ b/crates/json-parser-interface/src/lib.rs @@ -0,0 +1,16 @@ +pub fn add(left: u64, right: u64) -> u64 { + left + right +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn it_works() { + let result = add(2, 2); + assert_eq!(result, 4); + } +} + +// TODO: Placeholder for future functionality \ No newline at end of file diff --git a/crates/platform-interface/Cargo.toml b/crates/platform-interface/Cargo.toml new file mode 100644 index 0000000..d4facec --- /dev/null +++ b/crates/platform-interface/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "platform-interface" +version = "0.1.0" +edition = "2021" +description = "Platform abstraction interface" +repository = "https://github.com/rdkcentral/crashupload/tree/feature-sample-rust/crates/platform-interface" +homepage = "https://github.com/rdkcentral/crashupload/tree/feature-sample-rust/crates/platform-interface" + +[dependencies] +# Uncomment to Link this library dynamically +#crate_type = ["dynlib"] \ No newline at end of file diff --git a/crates/platform-interface/src/lib.rs b/crates/platform-interface/src/lib.rs new file mode 100644 index 0000000..e06fede --- /dev/null +++ b/crates/platform-interface/src/lib.rs @@ -0,0 +1,29 @@ +//! Platform interface library. +//! +//! This crate provides APIs for interacting with platform features such as RFC (TR-181) and Telemetry2 (T2). +//! It serves as an abstraction layer between the application and underlying platform-specific mechanisms. +//! +//! ## Modules +//! +//! - [`rfc_api`]: Functions for interacting with TR-181 parameters through Remote Feature Control (RFC) +//! Used for reading and writing device configuration values at runtime. +//! +//! - [`t2_api`]: Functions for sending telemetry markers to the Telemetry2 system +//! Used for reporting events and metrics to the telemetry collection service. +//! +//! ## Usage +//! +//! This library re-exports all functions from its modules at the crate root for convenience. +//! This allows for simpler imports like `use platform_interface::set_rfc_param` instead of +//! `use platform_interface::rfc_api::set_rfc_param`. +pub mod rfc_api; +pub mod t2_api; + +/// Platform interface library version. +pub const PLATFORM_LIB_VER: &str = "v1.0"; + +// Re-export RFC API functions for external use. +pub use rfc_api::*; + +// Re-export T2 API functions for external use. +pub use t2_api::*; diff --git a/crates/platform-interface/src/rfc_api.rs b/crates/platform-interface/src/rfc_api.rs new file mode 100644 index 0000000..379e217 --- /dev/null +++ b/crates/platform-interface/src/rfc_api.rs @@ -0,0 +1,164 @@ +//! RFC (Remote Feature Control) API integration for TR-181 parameter access. +//! +//! This module provides functions to get and set TR-181 parameters using the `tr181` binary. +//! It is used for interacting with device configuration at runtime. +//! +//! ## TR-181 and RFC +//! TR-181 is a standard for device data model used in broadband networks. Remote Feature +//! Control (RFC) allows configuring device behavior through standardized parameters. +//! This module abstracts the interaction with these parameters through simple get/set functions. +//! +//! ## Key Functions +//! - [`set_rfc_param`]: Set a TR-181 parameter to a specified value +//! - [`get_rfc_param`]: Get the value of a TR-181 parameter +//! - [`dmcli_get`]: Alternative method to get parameter values using the dmcli utility +//! +//! ## Common Parameter Paths +//! TR-181 parameters typically follow a hierarchical structure such as: +//! `Device.DeviceInfo.X_RDKCENTRAL-COM_RFC.Feature..Enable` + +use std::path::Path; +use std::process::Command; + +/// Path to the TR-181 binary used for RFC parameter access. +const TR181_BIN: &str = "tr181"; +// const TR181_SET_BIN: &str = "tr181Set"; // TODO: Implement if needed + +/// Returns the path to the TR-181 binary as a `Path`. +#[inline] +fn rfc_bin_path() -> &'static Path { + Path::new(TR181_BIN) +} + +/// Sets a TR-181 RFC parameter to a specified value. +/// +/// This function invokes the `tr181` binary with the `-s` (set) and `-v` (value) flags +/// to set the given RFC parameter to the provided value. +/// +/// # Arguments +/// * `rfc` - The TR-181 parameter name. +/// * `value` - The value to set for the parameter. +/// +/// # Returns +/// * `true` if the parameter was successfully set, `false` otherwise. +/// +/// # Example +/// ``` +/// use platform_interface::rfc_api::set_rfc_param; +/// let success = set_rfc_param("Device.DeviceInfo.X_RDKCENTRAL-COM_RFC.Feature.Foo.Enable", "true"); +/// ``` +pub fn set_rfc_param, V: AsRef>(rfc: R, value: V) -> bool { + let rfc_bin = rfc_bin_path(); + if rfc_bin.exists() { + match Command::new(rfc_bin) + .arg("-s") + .arg("-v") + .arg(value.as_ref()) + .arg(rfc.as_ref()) + .spawn() + { + Ok(_) => true, + Err(err) => { + println!("{} set failed with {}", rfc_bin.display(), err); + false + } + } + } else { + false + } +} + +/// Gets a TR-181 RFC parameter value into a mutable string. +/// +/// This function invokes the `tr181` binary with the `-g` (get) flag to retrieve +/// the value of the given RFC parameter and stores it in the provided mutable string reference. +/// +/// # Arguments +/// * `rfc` - The TR-181 parameter name. +/// * `res` - Mutable reference to a string to store the retrieved value. +/// +/// # Returns +/// * `true` if the parameter was successfully retrieved, `false` otherwise. +/// +/// # Example +/// ``` +/// use platform_interface::rfc_api::get_rfc_param; +/// let mut value = String::new(); +/// if get_rfc_param("Device.DeviceInfo.X_RDKCENTRAL-COM_RFC.Feature.Foo.Enable", &mut value) { +/// println!("RFC value: {}", value); +/// } +/// ``` +pub fn get_rfc_param>(rfc: R, res: &mut String) -> bool { + let rfc_bin = rfc_bin_path(); + if rfc_bin.exists() { + match Command::new(rfc_bin).arg("-g").arg(rfc.as_ref()).output() { + Ok(output) => { + if output.status.success() { + // Convert command output to string and update res + let output_str = String::from_utf8_lossy(&output.stdout); + *res = output_str.trim().to_string(); // Update the mutable reference + true + } else { + false + } + } + Err(err) => { + println!("{} get failed with {}", TR181_BIN, err); + false + } + } + } else { + false + } +} + +/// Retrieves a parameter value using the `dmcli` command-line utility. +/// +/// This function executes the `dmcli` command with the "eRT getv" arguments to retrieve +/// the value of the specified parameter. It then parses the output to extract just the +/// parameter value from the response string. +/// +/// # Arguments +/// * `param` - The parameter name to retrieve. +/// * `result` - Mutable reference to a string where the retrieved value will be stored. +/// +/// # Example +/// ``` +/// use platform_interface::rfc_api::dmcli_get; +/// let mut value = String::new(); +/// dmcli_get("Device.DeviceInfo.ModelName", &mut value); +/// println!("Model name: {}", value); +/// ``` +/// +/// # Notes +/// The function specifically looks for lines containing "string" in the output +/// and extracts the value using string manipulation equivalent to: +/// `grep string | cut -d":" -f3- | cut -d" " -f2- | tr -d ' '` +pub fn dmcli_get(param: &str, result: &mut String) { + let output = Command::new("dmcli") + .args(["eRT", "getv", param]) + .output(); + + if let Ok(output) = output { + if output.status.success() { + let stdout = String::from_utf8_lossy(&output.stdout); + // Faithfully mimic: grep string | cut -d":" -f3- | cut -d" " -f2- | tr -d ' ' + for line in stdout.lines() { + if line.contains("string") { + // Split by ':' and get the 3rd field onward + let after_colon = line.splitn(4, ':').nth(3).unwrap_or("").trim(); + // Split by space and get the 2nd field onward + let after_space = after_colon.splitn(2, ' ').nth(1).unwrap_or("").trim(); + // Remove all spaces + let cleaned = after_space.replace(' ', ""); + *result = cleaned; + return; + } + } + } else { + eprintln!("dmcli_get: dmcli command failed: {}", String::from_utf8_lossy(&output.stderr)); + } + } else { + eprintln!("dmcli_get: failed to execute dmcli command"); + } +} diff --git a/crates/platform-interface/src/t2_api.rs b/crates/platform-interface/src/t2_api.rs new file mode 100644 index 0000000..6ebab45 --- /dev/null +++ b/crates/platform-interface/src/t2_api.rs @@ -0,0 +1,106 @@ +//! Telemetry2 (T2) API integration for sending telemetry markers to the platform. +//! +//! This module provides functions to notify T2 markers with count or value semantics +//! by invoking the `telemetry2_0_client` binary if present on the system. +//! +//! ## Telemetry2 Overview +//! +//! Telemetry2 is a platform component used for collecting and aggregating operational +//! metrics from devices in the field. These metrics help monitor system health, track +//! feature usage, and identify potential issues. +//! +//! ## Marker Types +//! +//! This module supports two types of telemetry markers: +//! - **Count markers**: Simple counters that increment a value on the server +//! - **Value markers**: Send specific values or strings to the telemetry system +//! +//! ## Usage Notes +//! +//! The telemetry client must be installed at `/usr/bin/telemetry2_0_client` for these +//! functions to work. If the client is not found, the functions will return `false`. +use std::path::Path; +use std::process::Command; + +/// Path to the Telemetry2 client binary. +const T2_MSG_CLIENT_PATH: &str = "/usr/bin/telemetry2_0_client"; + +/// Returns the path to the Telemetry2 client as a `Path`. +#[inline] +fn t2_msg_client_path() -> &'static Path { + Path::new(T2_MSG_CLIENT_PATH) +} + +/// Notify a telemetry marker with a count value. +/// +/// This function invokes the T2 client with the given marker and count. +/// If `count` is `None`, a default value of `"1"` is used. +/// +/// # Arguments +/// * `marker` - The telemetry marker name. +/// * `count` - Optional count value as a string (defaults to `"1"`). +/// +/// # Returns +/// * `true` if the notification was successfully sent, `false` otherwise. +/// +/// # Example +/// ``` +/// use platform_interface::t2_api::t2_count_notify; +/// t2_count_notify("SYST_INFO_minidumpUpld", None::<&str>); +/// ``` +pub fn t2_count_notify, C: AsRef>(marker: M, count: Option) -> bool { + let t2_client = t2_msg_client_path(); + if t2_client.exists() { + let count_val = count.as_ref().map(|c| c.as_ref()).unwrap_or("1"); + match Command::new(t2_client) + .arg(marker.as_ref()) + .arg(count_val) + .spawn() + { + Ok(_) => true, + Err(err) => { + println!("{} execution failed with {}", t2_client.display(), err); + false + } + } + } else { + false + } +} + +/// Notify a telemetry marker with one or more values (comma-separated). +/// +/// This function invokes the T2 client with the given marker and values joined +/// with commas. It's used for reporting specific values rather than simple counts. +/// +/// # Arguments +/// * `marker` - The telemetry marker name. +/// * `values` - The values to associate with the marker (comma-separated). +/// +/// # Returns +/// * `true` if the notification was successfully sent, `false` otherwise. +/// +/// # Example +/// ``` +/// use platform_interface::t2_api::t2_val_notify; +/// t2_val_notify("SYST_INFO_crashUploadStatus", &["success", "minidump"]); +/// ``` +pub fn t2_val_notify>(marker: M, values: &[&str]) -> bool { + let t2_client = t2_msg_client_path(); + if t2_client.exists() { + let joined = values.join(", "); + match Command::new(t2_client) + .arg(marker.as_ref()) + .arg(&joined) + .spawn() + { + Ok(_) => true, + Err(err) => { + println!("{} execution failed with {}", t2_client.display(), err); + false + } + } + } else { + false + } +} diff --git a/crates/utils/Cargo.toml b/crates/utils/Cargo.toml new file mode 100644 index 0000000..ca124e4 --- /dev/null +++ b/crates/utils/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "utils" +version = "0.1.0" +edition = "2021" +description = "Common Utility crates" +repository = "https://github.com/rdkcentral/crashupload/tree/feature-sample-rust/crates/utils" +homepage = "https://github.com/rdkcentral/crashupload/tree/feature-sample-rust/crates/utils" + +[dependencies] +# Uncomment to Link this library dynamically +#crate_type = ["dynlib"] \ No newline at end of file diff --git a/crates/utils/src/command.rs b/crates/utils/src/command.rs new file mode 100644 index 0000000..d666c17 --- /dev/null +++ b/crates/utils/src/command.rs @@ -0,0 +1,90 @@ +//! File and system operation utilities. +//! +//! This module provides utility functions for common operations: +//! - File operations such as creating files (touch) and removing files/directories +//! - System operations such as flushing system logs +//! +//! These functions provide Rust implementations of common shell operations +//! to avoid spawning external processes when possible. + +use std::fs::{self, OpenOptions}; +use std::path::Path; +use std::process::Command; + +use crate::get_property_value_from_file; + +/// Creates a file if it does not exist, or updates its modification time if it does (like Unix `touch`). +/// +/// This function creates an empty file at the specified path if it doesn't exist, +/// or updates the modification time if the file already exists. +/// +/// # Arguments +/// * `path` - Path to the file to touch. +/// +/// # Example +/// ``` +/// use utils::touch; +/// touch("/tmp/somefile"); +/// ``` +pub fn touch>(path: P) { + let _ = OpenOptions::new().create(true).truncate(true).write(true).open(path); +} + +/// Recursively removes a directory and its contents, or removes a file if the path is not a directory. +/// Ignores errors if the path does not exist. +/// +/// This function behaves like the Unix `rm -rf` command, removing files or directories +/// regardless of their contents, and suppressing errors for non-existent paths. +/// +/// # Arguments +/// * `path` - Path to the directory or file to remove. +/// +/// # Example +/// ``` +/// use utils::rm_rf; +/// rm_rf("/tmp/somedir"); +/// rm_rf("/tmp/somefile"); +/// ``` +pub fn rm_rf>(path: P) { + let path_ref = path.as_ref(); + let _ = if path_ref.is_dir() { + fs::remove_dir_all(path_ref) + } else { + let _ = fs::remove_file(path); + Ok(()) + }; +} + +/// Flushes system logs to ensure they're written to disk. +/// +/// This function implements the behavior of the flushLogger shell function: +/// - Logs a message indicating the function was called +/// - Flushes journald buffers if the system uses journald +/// - Calls dumpLogs.sh if SYSLOG_NG_ENABLED is not set to "true" in device.properties +/// +/// # Example +/// ``` +/// use utils::flush_logger; +/// flush_logger(); +/// ``` +/// +/// # Notes +/// This function is primarily used to ensure logs are persisted before potentially +/// disruptive operations like reboots or crashes. +pub fn flush_logger() { + println!("flush_logger is called"); + + // Flush journald if available + if Path::new("/etc/os-release").exists() && Command::new("which").arg("journalctl").output().is_ok() { + let _ = Command::new("journalctl").args(["--sync", "--flush"]).status(); + } + + // Check SYSLOG_NG_ENABLED from device.properties + let mut syslog_ng_enabled = String::new(); + let _ = get_property_value_from_file( "/etc/device.properties", "SYSLOG_NG_ENABLED", &mut syslog_ng_enabled); + if syslog_ng_enabled.trim() != "true" { + let _ = Command::new("nice") + .args(["-n", "19", "/lib/rdk/dumpLogs.sh"]) + .status(); + } +} diff --git a/crates/utils/src/device_info.rs b/crates/utils/src/device_info.rs new file mode 100644 index 0000000..8db3bd9 --- /dev/null +++ b/crates/utils/src/device_info.rs @@ -0,0 +1,133 @@ +//! Device information utilities. +//! +//! This module provides functions to retrieve device properties such as the MAC address, +//! version file SHA1, and arbitrary property values from property files. These utilities +//! are essential for device identification and configuration in the crash upload system. + +use std::fs::File; +use std::io::{self, BufRead, BufReader, Read}; +use std::path::Path; +use std::process::Command; + +/// Path to the file containing the device MAC address. +/// This file is expected to contain the device's MAC address in colon-separated format. +pub const DEVICE_MAC_FILE: &str = "/tmp/.macAddress"; + +/// Path to the file containing the image version. +/// This file contains version information for the device firmware/software. +pub const VERSION_FILE: &str = "/version.txt"; + +/// Retrieves the value for a given key from a property file. +/// +/// Property files are expected to contain key-value pairs in the format `KEY=VALUE`. +/// This function searches for the specified key and returns its associated value. +/// +/// # Arguments +/// * `path` - Path to the property file. +/// * `key` - The property key to search for. +/// * `val` - Mutable reference to a string to store the value if found. +/// +/// # Returns +/// * `true` if the key is found and value is non-empty, `false` otherwise. +/// +/// # Example +/// ``` +/// use utils::get_property_value_from_file; +/// let mut value = String::new(); +/// let found = get_property_value_from_file("/opt/device.properties", "MODEL_NUM", &mut value); +/// if found { +/// println!("Model number: {}", value); +/// } +/// ``` +pub fn get_property_value_from_file, K: AsRef>(path: P, key: K, val: &mut String) -> bool { + let key = key.as_ref().trim(); + *val = String::new(); + let prop_file = match File::open(path) { + Ok(f) => f, + Err(_) => return false, + }; + let reader = BufReader::new(prop_file); + for line in reader.lines().map_while(Result::ok) { + if let Some((k, v)) = line.split_once('=') { + if k.trim() == key { + let value = v.trim(); + if !value.is_empty() { + val.push_str(value); + return true; + } else { + return false; + } + } + } + } + false +} + +/// Calculates the SHA1 hash of the version file and stores it in `sha1_val`. +/// +/// This function executes the `sha1sum` command on the VERSION_FILE constant +/// and parses the output to extract the hash. +/// +/// # Arguments +/// * `sha1_val` - Mutable reference to a string to store the SHA1 hash. +/// +/// # Returns +/// * `true` if the SHA1 hash was successfully calculated, `false` otherwise. +/// +/// # Example +/// ``` +/// use utils::get_sha1_value; +/// let mut sha1 = String::new(); +/// if get_sha1_value(&mut sha1) { +/// println!("SHA1: {}", sha1); +/// } +/// ``` +pub fn get_sha1_value(sha1_val: &mut String) -> bool { + let output = match Command::new("sha1sum").arg(VERSION_FILE).output() { + Ok(output) => output, + Err(_) => return false, + }; + + let stdout = String::from_utf8_lossy(&output.stdout); + let sha1_hash = stdout.split_whitespace().next().unwrap_or("").to_string(); + *sha1_val = sha1_hash; + true +} + +/// Reads the device MAC address from the device MAC file and stores it in `mac`. +/// +/// This function opens the file located at `DEVICE_MAC_FILE` ("/tmp/.macAddress"), +/// reads its contents, and processes the MAC address by removing any colons. +/// The processed MAC address is then stored in the provided string reference. +/// +/// # Arguments +/// * `mac` - Mutable reference to a string where the processed MAC address will be stored. +/// The function will clear any existing content in this string. +/// +/// # Returns +/// * `Ok(())` if the MAC address was successfully read and processed +/// * `Err(io::Error)` if the file cannot be opened or read +/// +/// # Example +/// ``` +/// use utils::device_info::get_device_mac; +/// +/// let mut mac = String::new(); +/// match get_device_mac(&mut mac) { +/// Ok(()) => println!("Device MAC: {}", mac), +/// Err(e) => eprintln!("Failed to get MAC address: {}", e), +/// } +/// ``` +/// +/// # Note +/// The function removes all colons from the MAC address, converting a format +/// like "00:11:22:33:44:55" to "001122334455". +pub fn get_device_mac(mac: &mut String) -> io::Result<()> { + let mut mac_val = String::new(); + let mut file = File::open(DEVICE_MAC_FILE)?; + + file.read_to_string(&mut mac_val)?; + + *mac = mac_val.trim().replace(":", ""); + Ok(()) +} diff --git a/crates/utils/src/lib.rs b/crates/utils/src/lib.rs new file mode 100644 index 0000000..f4a7004 --- /dev/null +++ b/crates/utils/src/lib.rs @@ -0,0 +1,23 @@ +//! Utilities library for the crash upload system. +//! +//! This crate provides various utility functions and helpers used throughout the crash upload system: +//! +//! - **File operations**: Functions for creating files, removing files/directories, and other +//! filesystem operations (see the [`command`] module) +//! +//! - **Device information**: Utilities for retrieving device properties such as MAC address, +//! version information, and config values from property files (see the [`device_info`] module) +//! +//! Most commonly used functions are re-exported at the crate root for convenience. + +pub mod command; +pub mod device_info; + +/// Utilities library version. +pub const UTILS_LIB_VER: &str = "v1.0"; + +// Re-export file operation helpers. +pub use command::*; + +// Re-export all device information utilities. +pub use device_info::*; diff --git a/minidump-on-bootup-upload.service b/minidump-on-bootup-upload.service index 7b92149..ec57ab6 100644 --- a/minidump-on-bootup-upload.service +++ b/minidump-on-bootup-upload.service @@ -24,5 +24,5 @@ Requires=network-online.target [Service] #Environment="TS=" -ExecStart=/bin/sh -c '/lib/rdk/uploadDumps.sh "" 0 ' +ExecStart=/usr/bin/crash-upload 2 "" "" ExecStop=/bin/sh -c 'x=`/bin/pidof uploadDumps.sh` ; if [ $? -eq 0 ] ; then /bin/kill -9 $x; fi ' diff --git a/test/crash-upload-test/Cargo.toml b/test/crash-upload-test/Cargo.toml new file mode 100644 index 0000000..82a0280 --- /dev/null +++ b/test/crash-upload-test/Cargo.toml @@ -0,0 +1,7 @@ +[package] +name = "crash-upload-test" +version = "0.1.0" +edition = "2021" + +[dependencies] +crash-upload = {path = "../../crash-upload"} diff --git a/test/crash-upload-test/src/main.rs b/test/crash-upload-test/src/main.rs new file mode 100644 index 0000000..891ede1 --- /dev/null +++ b/test/crash-upload-test/src/main.rs @@ -0,0 +1,3 @@ +fn main() { + println!("Hello, World!") +} diff --git a/test/curl-interface-test/Cargo.toml b/test/curl-interface-test/Cargo.toml new file mode 100644 index 0000000..3e80c55 --- /dev/null +++ b/test/curl-interface-test/Cargo.toml @@ -0,0 +1,7 @@ +[package] +name = "curl-interface-test" +version = "0.1.0" +edition = "2021" + +[dependencies] +curl-interface = {path = "../../crates/curl-interface"} \ No newline at end of file diff --git a/test/curl-interface-test/src/main.rs b/test/curl-interface-test/src/main.rs new file mode 100644 index 0000000..e7a11a9 --- /dev/null +++ b/test/curl-interface-test/src/main.rs @@ -0,0 +1,3 @@ +fn main() { + println!("Hello, world!"); +} diff --git a/test/json-parser-interface-test/Cargo.toml b/test/json-parser-interface-test/Cargo.toml new file mode 100644 index 0000000..84b42b2 --- /dev/null +++ b/test/json-parser-interface-test/Cargo.toml @@ -0,0 +1,7 @@ +[package] +name = "json-parser-interface-test" +version = "0.1.0" +edition = "2021" + +[dependencies] +json-parser-interface = {path = "../../crates/json-parser-interface"} \ No newline at end of file diff --git a/test/json-parser-interface-test/src/main.rs b/test/json-parser-interface-test/src/main.rs new file mode 100644 index 0000000..e7a11a9 --- /dev/null +++ b/test/json-parser-interface-test/src/main.rs @@ -0,0 +1,3 @@ +fn main() { + println!("Hello, world!"); +} diff --git a/test/platform-interface-test/Cargo.toml b/test/platform-interface-test/Cargo.toml new file mode 100644 index 0000000..ef1bd9d --- /dev/null +++ b/test/platform-interface-test/Cargo.toml @@ -0,0 +1,7 @@ +[package] +name = "platform-interface-test" +version = "0.1.0" +edition = "2021" + +[dependencies] +platform-interface = {path = "../../crates/platform-interface"} \ No newline at end of file diff --git a/test/platform-interface-test/src/main.rs b/test/platform-interface-test/src/main.rs new file mode 100644 index 0000000..e7a11a9 --- /dev/null +++ b/test/platform-interface-test/src/main.rs @@ -0,0 +1,3 @@ +fn main() { + println!("Hello, world!"); +} diff --git a/test/utils-test/Cargo.toml b/test/utils-test/Cargo.toml new file mode 100644 index 0000000..98bd8a7 --- /dev/null +++ b/test/utils-test/Cargo.toml @@ -0,0 +1,7 @@ +[package] +name = "utils-test" +version = "0.1.0" +edition = "2021" + +[dependencies] +utils = {path = "../../crates/utils"} diff --git a/test/utils-test/src/main.rs b/test/utils-test/src/main.rs new file mode 100644 index 0000000..6343731 --- /dev/null +++ b/test/utils-test/src/main.rs @@ -0,0 +1,5 @@ +#[cfg(test)] +mod tests { +} + +fn main() {} diff --git a/uploadDumps.sh b/uploadDumps.sh index 720f932..4f849f1 100755 --- a/uploadDumps.sh +++ b/uploadDumps.sh @@ -804,23 +804,25 @@ processDumps() mv "$f" "$f1" f="$f1" fi + if [ "$DUMP_FLAG" == "0" ]; then processCrashTelemtryInfo "$f" fi + if [ -f "$f" ]; then # Checking whether is it a tarball so we should continue without further processing - #if [[ "$f" =~ '.+_mac.+_dat.+_box.+_mod.+' ]]; then - ext=${f##*.} - if [[ "$ext" = 'tgz' ]]; then - logMessage "Skip archiving $f as it is a tarball already." - continue - fi - #last modification date of a core dump, to ease refusing of already uploaded core dumps on a server side + #if [[ "$f" =~ '.+_mac.+_dat.+_box.+_mod.+' ]]; then + ext=${f##*.} + if [[ "$ext" = 'tgz' ]]; then + logMessage "Skip archiving $f as it is a tarball already." + continue + fi + #last modification date of a core dump, to ease refusing of already uploaded core dumps on a server side modDate=`getLastModifiedTimeOfFile $f` if [ -z "$CRASHTS" ]; then - CRASHTS=$modDate - # Ensure timestamp is not empty - checkParameter CRASHTS + CRASHTS=$modDate + # Ensure timestamp is not empty + checkParameter CRASHTS fi if [ "$DUMP_FLAG" == "1" ] ; then @@ -830,18 +832,18 @@ processDumps() else dumpName=`setLogFile $sha1 $MAC $CRASHTS $boxType $modNum $f` fi - if [ "${#dumpName}" -ge "135" ]; then - #Removing the HEADER of the corefile due to ecryptfs limitation as file can't be open when it exceeds 140 characters. - dumpName="${dumpName#*_}" - fi + if [ "${#dumpName}" -ge "135" ]; then + #Removing the HEADER of the corefile due to ecryptfs limitation as file can't be open when it exceeds 140 characters. + dumpName="${dumpName#*_}" + fi tgzFile=$dumpName".core.tgz" else dumpName=`setLogFile $sha1 $MAC $CRASHTS $boxType $modNum $f` - if [ "${#dumpName}" -ge "135" ]; then - #Removing the HEADER of the corefile due to ecryptfs limitation as file can't be open when it exceeds 140 characters. - dumpName="${dumpName#*_}" - fi - tgzFile=$dumpName".tgz" + if [ "${#dumpName}" -ge "135" ]; then + #Removing the HEADER of the corefile due to ecryptfs limitation as file can't be open when it exceeds 140 characters. + dumpName="${dumpName#*_}" + fi + tgzFile=$dumpName".tgz" fi #remove <#=#> characters from the dumpname to avoid processing issues. @@ -853,20 +855,46 @@ processDumps() logMessage "Size of the file: $(ls -l $dumpName)" +<<<<<<< feature-sample-rust + if [ "$DUMP_FLAG" == "1" ] ; then + logfiles="$VERSION_FILE $CORE_LOG" +======= if [ "$IS_T2_ENABLED" == "true" ] && [ ! -s "$dumpName" ]; then t2CountNotify "SYST_ERR_MINIDPZEROSIZE" fi if [ "$DUMP_FLAG" == "1" ] ; then logfiles="$VERSION_FILE $CORE_LOG" +>>>>>>> develop if [ -f /tmp/set_crash_reboot_flag ];then - logMessage "Compression without nice" - tar -zcvf $tgzFile $dumpName $logfiles 2>&1 | logStdout - else - logMessage "Compression with nice" - nice -n 19 tar -zcvf $tgzFile $dumpName $logfiles 2>&1 | logStdout + logMessage "Compression without nice" + tar -zcvf $tgzFile $dumpName $logfiles 2>&1 | logStdout + else + logMessage "Compression with nice" + nice -n 19 tar -zcvf $tgzFile $dumpName $logfiles 2>&1 | logStdout fi else +<<<<<<< feature-sample-rust + crashedUrlFile=$LOG_PATH/crashed_url.txt + files="$VERSION_FILE $CORE_LOG $crashedUrlFile" + add_crashed_log_file $files + nice -n 19 tar -zcvf $tgzFile $dumpName $files 2>&1 | logStdout + fi + + if [ $? -eq 0 ]; then + logMessage "Success Compressing the files, $tgzFile $dumpName $VERSION_FILE $CORE_LOG " + else + # If the tar creation failed then will create new tar after copying logs files to /tmp + OUT_FILES="$dumpName" + [ "$DUMP_FLAG" == "1" ] && copy_log_files_tmp_dir $logfiles || copy_log_files_tmp_dir $files + nice -n 19 tar -zcvf $tgzFile $OUT_FILES 2>&1 | logStdout + if [ $? -eq 0 ]; then + logMessage "Success Compressing the files, $tgzFile $OUT_FILES" + else + logMessage "Compression Failed ." + fi + fi +======= #Assignee crashedUrlFile only if the file exist to avoid tar errors. [ -f "$LOG_PATH/crashed_url.txt" ] && crashedUrlFile="$LOG_PATH/crashed_url.txt" files="$VERSION_FILE $CORE_LOG $crashedUrlFile" @@ -890,12 +918,14 @@ processDumps() fi fi fi +>>>>>>> develop logMessage "Size of the compressed file: $(ls -l $tgzFile)" - if [ ! -z "$TMP_DIR_NAME" ] && [ -d "/tmp/$TMP_DIR_NAME" ]; then - rm -rf /tmp/$TMP_DIR_NAME - logMessage "Temporary Directory Deleted:/tmp/$TMP_DIR_NAME" + if [ ! -z "$TMP_DIR_NAME" ] && [ -d "/tmp/$TMP_DIR_NAME" ]; then + rm -rf /tmp/$TMP_DIR_NAME + logMessage "Temporary Directory Deleted:/tmp/$TMP_DIR_NAME" fi + rm $dumpName if [ "$DUMP_FLAG" == "0" ]; then @@ -922,18 +952,20 @@ processDumps() removePendingDumps exit fi + if [ "$DUMP_NAME" = "minidump" ]; then - if isUploadLimitReached; then + if isUploadLimitReached; then logMessage "Upload rate limit has been reached." markAsCrashLoopedAndUpload $f logMessage "Setting recovery time" setRecoveryTime removePendingDumps exit - fi + fi else logMessage "Coredump File `echo $f`" fi + S3_FILENAME=`echo ${f##*/}` count=1 @@ -964,61 +996,63 @@ processDumps() fi logMessage "[$0]: $count: $DUMP_NAME S3 Upload " - if [ -f /lib/rdk/uploadDumpsToS3.sh ]; then + if [ -f /lib/rdk/uploadDumpsToS3.sh ]; then echo "$WORKING_DIR $partnerId $DUMP_NAME $DEVICE_TYPE $VERSION_FILE $encryptionEnable $EnableOCSPStapling $EnableOCSP $TLS $BUILD_TYPE $modNum ${CURL_LOG_OPTION}" > /tmp/uploadtos3params uploadToS3 "`echo $S3_FILENAME`" status=$? - rm /tmp/uploadtos3params - else + rm /tmp/uploadtos3params + else # A secure upload logic is required to upload the crash dumps to the cloud database server. - # The upload script can take in parameters such as Device model, upload options related to security and the dump file to be uploaded - # The return value from the upload logic can be checked to determine a successful coredump upload to server. - # Retries can be triggered at regular intervals if needed, in case the upload fails at the initial try. - echo "A secure core/minidump upload logic can be implemented here to upload the crash dumps to cloud database." - echo "Parameters such as device type, secure upload options and the file to be uploaded can be passed to the upload function/script." - fi + # The upload script can take in parameters such as Device model, upload options related to security and the dump file to be uploaded + # The return value from the upload logic can be checked to determine a successful coredump upload to server. + # Retries can be triggered at regular intervals if needed, in case the upload fails at the initial try. + echo "A secure core/minidump upload logic can be implemented here to upload the crash dumps to cloud database." + echo "Parameters such as device type, secure upload options and the file to be uploaded can be passed to the upload function/script." + fi while [ $count -le 3 ] do # S3 amazon fail over recovery - count=$(( count +1)) + count=$(( count +1)) if [ $status -ne 0 ];then - logMessage "[$0]: Execution Status: $status, S3 Amazon Upload of $DUMP_NAME Failed" - logMessage "[$0]: $count: (Retry), $DUMP_NAME S3 Upload" - sleep 2 - if [ -f /lib/rdk/uploadDumpsToS3.sh ]; then - echo "$WORKING_DIR $partnerId $DUMP_NAME $DEVICE_TYPE $VERSION_FILE $encryptionEnable $EnableOCSPStapling $EnableOCSP $TLS $BUILD_TYPE $modNum ${CURL_LOG_OPTION}" > /tmp/uploadtos3params - uploadToS3 "`echo $S3_FILENAME`" - status=$? - rm /tmp/uploadtos3params - else - # A secure upload logic is required to upload the crash dumps to the cloud database server. - # The upload script can take in parameters such as Device model, upload options related to security and the dump file to be uploaded - # The return value from the upload logic can be checked to determine a successful coredump upload to server. - # Retries can be triggered at regular intervals if needed, in case the upload fails at the initial try. - echo "A secure core/minidump upload logic can be implemented here to upload the crash dumps to cloud database." - echo "Parameters such as device type, secure upload options and the file to be uploaded can be passed to the upload function/script." - fi + logMessage "[$0]: Execution Status: $status, S3 Amazon Upload of $DUMP_NAME Failed" + logMessage "[$0]: $count: (Retry), $DUMP_NAME S3 Upload" + sleep 2 + if [ -f /lib/rdk/uploadDumpsToS3.sh ]; then + echo "$WORKING_DIR $partnerId $DUMP_NAME $DEVICE_TYPE $VERSION_FILE $encryptionEnable $EnableOCSPStapling $EnableOCSP $TLS $BUILD_TYPE $modNum ${CURL_LOG_OPTION}" > /tmp/uploadtos3params + uploadToS3 "`echo $S3_FILENAME`" + status=$? + rm /tmp/uploadtos3params + else + # A secure upload logic is required to upload the crash dumps to the cloud database server. + # The upload script can take in parameters such as Device model, upload options related to security and the dump file to be uploaded + # The return value from the upload logic can be checked to determine a successful coredump upload to server. + # Retries can be triggered at regular intervals if needed, in case the upload fails at the initial try. + echo "A secure core/minidump upload logic can be implemented here to upload the crash dumps to cloud database." + echo "Parameters such as device type, secure upload options and the file to be uploaded can be passed to the upload function/script." + fi else - logMessage "[$0]: $DUMP_NAME uploadToS3 SUCESS: status: $status" - if [ "$DUMP_NAME" == "minidump" ] && [ "$IS_T2_ENABLED" == "true" ]; then - t2CountNotify "SYST_INFO_minidumpUpld" - fi - break + logMessage "[$0]: $DUMP_NAME uploadToS3 SUCESS: status: $status" + if [ "$DUMP_NAME" == "minidump" ] && [ "$IS_T2_ENABLED" == "true" ]; then + t2CountNotify "SYST_INFO_minidumpUpld" + fi + break fi done + if [ $status -ne 0 ];then - logMessage "[$0]: S3 Amazon Upload of $DUMP_NAME Failed..!" - if [ "$DUMP_NAME" == "minidump" ]; then - logMessage "Check and save the dump $S3_FILENAME" - saveDump "$ORGINAL_FILENAME" - else - logMessage "Removing file $S3_FILENAME" - rm -f $S3_FILENAME - fi - exit 1 + logMessage "[$0]: S3 Amazon Upload of $DUMP_NAME Failed..!" + if [ "$DUMP_NAME" == "minidump" ]; then + logMessage "Check and save the dump $S3_FILENAME" + saveDump "$ORGINAL_FILENAME" + else + logMessage "Removing file $S3_FILENAME" + rm -f $S3_FILENAME + fi + exit 1 else - echo "[$0]: Execution Status: $status, S3 Amazon Upload of $DUMP_NAME Success" + echo "[$0]: Execution Status: $status, S3 Amazon Upload of $DUMP_NAME Success" fi + ORGINAL_FILENAME="" logMessage "Removing file $S3_FILENAME" rm -f $S3_FILENAME