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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
257 changes: 257 additions & 0 deletions .github/workflows/e2e-test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,257 @@
name: E2E Cross-Platform Update Test

on:
push:
branches: [main, master]
pull_request:
branches: [main, master]
workflow_dispatch:

env:
CARGO_TERM_COLOR: always

jobs:
build-and-test:
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-latest]
include:
- os: ubuntu-latest
platform: linux
binary_ext: ""
key_file: owner_key_linux
timeout_cmd: timeout
- os: macos-latest
platform: macos
binary_ext: ""
key_file: owner_key_macos
timeout_cmd: gtimeout
#- os: windows-latest
# platform: win
# binary_ext: ".exe"
# key_file: owner_key_win
# timeout_cmd: timeout

runs-on: ${{ matrix.os }}
timeout-minutes: 30

defaults:
run:
shell: bash

steps:
- name: Checkout repository
uses: actions/checkout@v4

- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@stable

- name: Cache cargo registry
uses: actions/cache@v4
with:
path: ~/.cargo/registry
key: ${{ runner.os }}-cargo-registry-${{ hashFiles('**/Cargo.lock') }}

- name: Cache cargo index
uses: actions/cache@v4
with:
path: ~/.cargo/git
key: ${{ runner.os }}-cargo-index-${{ hashFiles('**/Cargo.lock') }}

- name: Cache cargo build
uses: actions/cache@v4
with:
path: target
key: ${{ runner.os }}-cargo-build-target-${{ hashFiles('**/Cargo.lock') }}

# macOS specific: Install coreutils for gtimeout and verify codesign
- name: Install dependencies (macOS)
if: matrix.os == 'macos-latest'
run: |
brew install coreutils
if ! command -v codesign &> /dev/null; then
echo "codesign command not found!"
exit 1
fi
echo "codesign is available"
codesign --version || true

- name: Build e2e example (initial version)
working-directory: crates/rustpatcher
run: cargo build --release --example e2e

- name: Sign initial binary
working-directory: crates/rustpatcher
run: |
cargo run --release --bin rustpatcher sign \
../../target/release/examples/e2e${{ matrix.binary_ext }} \
--key-file e2e_test/keys/${{ matrix.key_file }}

- name: Save initial binary
run: |
mkdir -p old_version
cp target/release/examples/e2e${{ matrix.binary_ext }} old_version/e2e${{ matrix.binary_ext }}
shell: bash

- name: Read and bump version
id: version
working-directory: crates/rustpatcher
run: |
# Extract current version from Cargo.toml
CURRENT_VERSION=$(grep '^version = ' Cargo.toml | head -1 | sed 's/version = "\(.*\)"/\1/')
echo "Current version: $CURRENT_VERSION"

# Parse version components
MAJOR=$(echo $CURRENT_VERSION | cut -d. -f1)
MINOR=$(echo $CURRENT_VERSION | cut -d. -f2)
PATCH=$(echo $CURRENT_VERSION | cut -d. -f3)

# Bump patch version
NEW_PATCH=$((PATCH + 1))
NEW_VERSION="${MAJOR}.${MINOR}.${NEW_PATCH}"

echo "New version: $NEW_VERSION"
echo "old_version=$CURRENT_VERSION" >> $GITHUB_OUTPUT
echo "new_version=$NEW_VERSION" >> $GITHUB_OUTPUT
shell: bash

- name: Update Cargo.toml version
working-directory: crates/rustpatcher
run: |
# Update version in Cargo.toml
if [ "${{ matrix.os }}" = "windows-latest" ]; then
sed -i 's/version = "${{ steps.version.outputs.old_version }}"/version = "${{ steps.version.outputs.new_version }}"/' Cargo.toml
else
sed -i.bak 's/version = "${{ steps.version.outputs.old_version }}"/version = "${{ steps.version.outputs.new_version }}"/' Cargo.toml
rm -f Cargo.toml.bak
fi
echo "Updated version from ${{ steps.version.outputs.old_version }} to ${{ steps.version.outputs.new_version }}"
grep '^version = ' Cargo.toml
shell: bash

- name: Build e2e example (new version)
working-directory: crates/rustpatcher
run: cargo build --release --example e2e

- name: Sign new binary
working-directory: crates/rustpatcher
run: |
cargo run --release --bin rustpatcher sign \
../../target/release/examples/e2e${{ matrix.binary_ext }} \
--key-file e2e_test/keys/${{ matrix.key_file }}

- name: Save new binary
run: |
mkdir -p new_version
cp target/release/examples/e2e${{ matrix.binary_ext }} new_version/e2e${{ matrix.binary_ext }}
shell: bash

- name: Make binaries executable
if: matrix.os != 'windows-latest'
run: |
chmod +x old_version/e2e
chmod +x new_version/e2e

- name: Run E2E test
timeout-minutes: 5
env:
OLD_VERSION: ${{ steps.version.outputs.old_version }}
NEW_VERSION: ${{ steps.version.outputs.new_version }}
run: |
echo "=== Starting E2E Update Test for ${{ matrix.platform }} ==="
echo "Testing P2P update from $OLD_VERSION to $NEW_VERSION"

# Parse version patterns
OLD_MAJOR=$(echo $OLD_VERSION | cut -d. -f1)
OLD_MINOR=$(echo $OLD_VERSION | cut -d. -f2)
OLD_PATCH=$(echo $OLD_VERSION | cut -d. -f3)
NEW_MAJOR=$(echo $NEW_VERSION | cut -d. -f1)
NEW_MINOR=$(echo $NEW_VERSION | cut -d. -f2)
NEW_PATCH=$(echo $NEW_VERSION | cut -d. -f3)

OLD_PATTERN="Version($OLD_MAJOR, $OLD_MINOR, $OLD_PATCH)"
NEW_PATTERN="Version($NEW_MAJOR, $NEW_MINOR, $NEW_PATCH)"

echo "Expected initial version: $OLD_PATTERN"
echo "Expected after update: $NEW_PATTERN"
echo ""

# Start the NEW version (publisher/server with the update)
echo "--- Starting NEW version binary (publisher) ---"
./new_version/e2e${{ matrix.binary_ext }} > new_output.txt 2>&1 &
NEW_PID=$!
echo "Started new version with PID: $NEW_PID"
sleep 2

# Start the OLD version (client that should update itself)
echo "--- Starting OLD version binary (to be updated) ---"
./old_version/e2e${{ matrix.binary_ext }} > old_output.txt 2>&1 &
OLD_PID=$!
echo "Started old version with PID: $OLD_PID"

# Monitor old version output for up to 120 seconds (2 minutes)
echo ""
echo "Monitoring for update... (timeout: 120 seconds)"
TIMEOUT=120
ELAPSED=0
UPDATE_SUCCESS=false

while [ $ELAPSED -lt $TIMEOUT ]; do
if [ -f old_output.txt ]; then
# Check if we see the NEW version pattern (update succeeded)
if grep -q "$NEW_PATTERN" old_output.txt; then
echo "UPDATE SUCCESSFUL! Old version updated to $NEW_PATTERN"
UPDATE_SUCCESS=true
break
fi
fi
sleep 5
ELAPSED=$((ELAPSED + 5))
echo " ... $ELAPSED seconds elapsed"
done

# Kill both processes
kill $NEW_PID $OLD_PID 2>/dev/null || true
sleep 1

# Show outputs
echo ""
echo "=== New version output ==="
cat new_output.txt || echo "No output"
echo ""
echo "=== Old version output ==="
cat old_output.txt || echo "No output"
echo ""

# Evaluate result
if [ "$UPDATE_SUCCESS" = true ]; then
echo "E2E test PASSED for ${{ matrix.platform }}!"
echo "The old version successfully updated itself via P2P network"
exit 0
else
echo "E2E test FAILED for ${{ matrix.platform }}"
echo "The old version did NOT update to $NEW_PATTERN within 2 minutes"
echo ""
echo "Possible issues:"
echo " - P2P discovery failed"
echo " - Network connectivity issues"
echo " - Update mechanism not triggering"
echo " - Check outputs above for errors"
exit 1
fi

# Upload artifacts for debugging
- name: Upload test artifacts
if: always()
uses: actions/upload-artifact@v4
with:
name: e2e-test-artifacts-${{ matrix.platform }}
path: |
old_version/
new_version/
old_output.txt
new_output.txt
old_error.txt
new_error.txt
retention-days: 7
22 changes: 15 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,17 @@
# Rust Patcher
Secure fully decentralized software updates.

## Supported Platforms

| Platform | Architecture | Supported |
|----------|--------------|-----------|
| Linux | x86_64 | Yes |
| Linux | ARM64 | Yes |
| macOS | x86_64 | Yes |
| macOS | ARM64 | Yes |
| Windows | - | Not yet |

**Note:** windows support will follow, *windows build err: libc is not available in nix pkg*

## Implementation Flow

Expand All @@ -30,12 +41,9 @@ async fn main() -> anyhow::Result<()> {
println!("my version is {:?}", rustpatcher::Version::current()?);

// your app code after this
loop {
tokio::select! {
_ = tokio::signal::ctrl_c() => {
println!("Exiting on Ctrl-C");
break;
}
tokio::select! {
_ = tokio::signal::ctrl_c() => {
println!("Exiting on Ctrl-C");
}
}
Ok(())
Expand Down Expand Up @@ -229,4 +237,4 @@ build:

publish:
target/release/<your-bin>
```
```
7 changes: 6 additions & 1 deletion crates/rustpatcher/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "rustpatcher"
version = "0.2.0"
version = "0.2.1"
edition = "2024"
description = "distributed patching system for single binary applications"
license = "MIT"
Expand All @@ -26,6 +26,7 @@ rand = "0.8"
z32 = "1"
clap = { version = "4", features = ["derive"] }
once_cell = "1"
goblin = "0.10"
tempfile = "3"
nix = { version = "0.30", features = ["process"] }
self-replace = "1"
Expand All @@ -46,6 +47,10 @@ path = "examples/platforms.rs"
name = "simple"
path = "examples/simple.rs"

[[example]]
name = "e2e"
path = "e2e_test/e2e.rs"

[[bin]]
name = "rustpatcher"
path = "xtask/sign.rs"
21 changes: 15 additions & 6 deletions crates/rustpatcher/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,17 @@
# Rust Patcher
Secure fully decentralized software updates.

## Supported Platforms

| Platform | Architecture | Supported |
|----------|--------------|-----------|
| Linux | x86_64 | Yes |
| Linux | ARM64 | Yes |
| macOS | x86_64 | Yes |
| macOS | ARM64 | Yes |
| Windows | - | Not yet |

**Note:** windows support will follow, *windows build err: libc is not available in nix pkg*

## Implementation Flow

Expand All @@ -13,6 +24,7 @@ Secure fully decentralized software updates.
# Cargo.toml
[dependencies]
rustpatcher = "0.2"
rustpatcher-macros = "0.2"
tokio = { version = "1", features = ["rt-multi-thread","macros"] }
```

Expand All @@ -29,12 +41,9 @@ async fn main() -> anyhow::Result<()> {
println!("my version is {:?}", rustpatcher::Version::current()?);

// your app code after this
loop {
tokio::select! {
_ = tokio::signal::ctrl_c() => {
println!("Exiting on Ctrl-C");
break;
}
tokio::select! {
_ = tokio::signal::ctrl_c() => {
println!("Exiting on Ctrl-C");
}
}
Ok(())
Expand Down
18 changes: 18 additions & 0 deletions crates/rustpatcher/e2e_test/e2e.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
#[cfg(target_os = "macos")]
const PUBLIC_KEY: &str = "9mrnh6bhosexei8ciwe1gm7kqitg7y3rbzjbezqbncpg1sk6sq6o";
#[cfg(target_os = "linux")]
const PUBLIC_KEY: &str = "6qdxs69eg39f1iu79sza56tqbzzgur4gteowp9fa8dwfpakc3ngy";
#[cfg(target_os = "windows")]
const PUBLIC_KEY: &str = "bhafqhm8k9e7fzab7i7h6gie6oedncwyffautkngqsa9d1ohzuho";

#[tokio::main]
#[rustpatcher::public_key(PUBLIC_KEY)]
async fn main() -> anyhow::Result<()> {

rustpatcher::spawn(rustpatcher::UpdaterMode::Now).await?;
println!("{:?}", rustpatcher::Version::current()?);

tokio::signal::ctrl_c()
.await
.map_err(|e| anyhow::anyhow!(e))
}
1 change: 1 addition & 0 deletions crates/rustpatcher/e2e_test/keys/owner_key_linux
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
97zzdmnb7p6mt8zsqc74wtqqqzaw39on4jsoiriozdiq3kquteso
1 change: 1 addition & 0 deletions crates/rustpatcher/e2e_test/keys/owner_key_macos
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
7xqhfrze8we3dcuywwm74ctbbcnefyer4u6iph4nccontc7k67yy
1 change: 1 addition & 0 deletions crates/rustpatcher/e2e_test/keys/owner_key_win
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
1swm1b9affb4c59c8yh69wey4axji7fhyjnwhtmnce69m8tspfmy
Loading