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
49 changes: 36 additions & 13 deletions .github/actions/setup/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -80,13 +80,29 @@ runs:
run: |
set -euo pipefail

ENDPOINT="http://${SPICEIO_BIND}"

# Skip if spiceio is already listening on the requested address
if curl -sf -I "$ENDPOINT/" 2>/dev/null | grep -qi "server: spiceio"; then
echo "spiceio already running at $ENDPOINT — skipping start"
echo "endpoint=$ENDPOINT" >> "$GITHUB_OUTPUT"
exit 0
# Check if spiceio is already listening on the requested address
WANT_VERSION=$(spiceio --version | awk '{print $2}')
RUNNING_HEADER=$(curl -sf -I "http://${SPICEIO_BIND}/" 2>/dev/null | grep -i '^server:.*spiceio' | tr -d '\r' || true)
Copy link

Copilot AI Apr 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

curl -sf -I "http://${SPICEIO_BIND}/" uses HEAD on /. The router returns 405 for service-level requests that are not GET (see src/s3/router.rs around the req_bucket.is_empty() match), and -f suppresses headers on 4xx, so RUNNING_HEADER will usually be empty and the version/skip logic won’t trigger. Use a GET request when checking headers (e.g., request / and capture response headers) or remove -f/target a path that supports HEAD (like /$SPICEIO_BUCKET).

Suggested change
RUNNING_HEADER=$(curl -sf -I "http://${SPICEIO_BIND}/" 2>/dev/null | grep -i '^server:.*spiceio' | tr -d '\r' || true)
RUNNING_HEADER=$(curl -sS -D - -o /dev/null "http://${SPICEIO_BIND}/" 2>/dev/null | grep -i '^server:.*spiceio' | tr -d '\r' || true)

Copilot uses AI. Check for mistakes.
if [[ -n "$RUNNING_HEADER" ]]; then
# Extract version from "Server: spiceio/X.Y.Z"; bare "Server: spiceio"
# (no slash) means a pre-versioned build — treat as outdated.
RUNNING_VERSION="${RUNNING_HEADER##*/}"
RUNNING_VERSION="${RUNNING_VERSION// /}"
if [[ "$RUNNING_HEADER" == */* && "$RUNNING_VERSION" == "$WANT_VERSION" ]]; then
echo "spiceio $WANT_VERSION already running at http://${SPICEIO_BIND} — skipping start"
echo "endpoint=http://${SPICEIO_BIND}" >> "$GITHUB_OUTPUT"
exit 0
fi
# Old or mismatched version — kill it so we can replace it
mapfile -t OLD_PIDS < <(lsof -ti "tcp:${SPICEIO_BIND##*:}" -sTCP:LISTEN 2>/dev/null || true)
if (( ${#OLD_PIDS[@]} > 0 )); then
echo "replacing spiceio ${RUNNING_VERSION:-unknown} (PID(s) ${OLD_PIDS[*]}) with $WANT_VERSION"
kill "${OLD_PIDS[@]}" 2>/dev/null || true
for i in $(seq 1 10); do
lsof -ti "tcp:${SPICEIO_BIND##*:}" -sTCP:LISTEN >/dev/null 2>&1 || break
sleep 1
Comment on lines +96 to +103
Copy link

Copilot AI Apr 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Port extraction via ${SPICEIO_BIND##*:} breaks for IPv6 binds (e.g. [::1]:8333), and the lsof -ti "tcp:..." form is also inconsistent with the repo’s other scripts/workflows (e.g. .github/workflows/ci.yml:80 uses lsof -i ":$PORT" -sTCP:LISTEN -t). Consider parsing the port in an IPv6-safe way and using the same lsof -i ":$PORT" ... pattern here for portability.

Copilot uses AI. Check for mistakes.
done
fi
fi

# Parse smb://user:pass@server:port/share
Expand Down Expand Up @@ -127,21 +143,28 @@ runs:
export SPICEIO_SMB_SERVER SPICEIO_SMB_PORT SPICEIO_SMB_USER SPICEIO_SMB_PASS SPICEIO_SMB_SHARE
export SPICEIO_BUCKET SPICEIO_REGION SPICEIO_BIND

SPICEIO_LOG="${RUNNER_TEMP}/spiceio.log"
: > "$SPICEIO_LOG"
export SPICEIO_LOG_FILE="$SPICEIO_LOG"

spiceio &
PID=$!
echo "endpoint=$ENDPOINT" >> "$GITHUB_OUTPUT"

# Wait for readiness
# Wait for spiceio to report its actual listening address (port may
# auto-increment if the requested port was busy).
echo "Waiting for spiceio on ${SPICEIO_BIND}..."
ENDPOINT=""
for i in $(seq 1 30); do
if curl -sf -o /dev/null "$ENDPOINT/" 2>/dev/null; then
echo "spiceio ready at $ENDPOINT (PID $PID)"
exit 0
fi
if ! kill -0 "$PID" 2>/dev/null; then
echo "::error::spiceio exited unexpectedly"
exit 1
fi
ENDPOINT=$(grep 'listening on' "$SPICEIO_LOG" 2>/dev/null | grep -o 'http://[^ ]*' | tail -1 || true)
if [[ -n "$ENDPOINT" ]] && curl -sf -I "$ENDPOINT/" 2>/dev/null | grep -qi "server: spiceio"; then
Comment thread
lukekim marked this conversation as resolved.
Copy link

Copilot AI Apr 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The readiness probe uses curl -sf -I "$ENDPOINT/" and expects a Server: header. Since / does not support HEAD (router returns 405) and -f hides headers on 4xx, this loop can time out even when the server is up. Consider switching back to a GET-based readiness check and, if you still need headers, capture them from the GET response.

Suggested change
if [[ -n "$ENDPOINT" ]] && curl -sf -I "$ENDPOINT/" 2>/dev/null | grep -qi "server: spiceio"; then
if [[ -n "$ENDPOINT" ]] && curl -sS -D - -o /dev/null "$ENDPOINT/" 2>/dev/null | grep -qi '^server: spiceio'; then

Copilot uses AI. Check for mistakes.
echo "spiceio ready at $ENDPOINT (PID $PID)"
echo "endpoint=$ENDPOINT" >> "$GITHUB_OUTPUT"
exit 0
fi
sleep 1
done
echo "::error::spiceio failed to start within 30s"
Expand Down
33 changes: 26 additions & 7 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,32 @@ async fn main() {

let config = Config::from_env();

// Bind TCP listener early (before SMB setup). If the port is taken,
// auto-increment until an available port is found.
let (listener, bind_addr) = {
let mut addr = config.bind_addr;
let start_port = addr.port();
loop {
match TcpListener::bind(addr).await {
Ok(l) => break (l, addr),
Err(e) if e.kind() == std::io::ErrorKind::AddrInUse => {
let next = match addr.port().checked_add(1) {
Some(n) if n - start_port <= 100 => n,
_ => {
serr!("no available port in range {start_port}–{}", addr.port());
std::process::exit(1);
}
};
addr.set_port(next);
}
Err(e) => {
serr!("failed to bind TCP listener: {e}");
std::process::exit(1);
}
}
}
};

slog!(
"[spiceio] connecting to smb://****@{}:{}/{} ({}x)",
config.smb_server,
Expand Down Expand Up @@ -128,13 +154,6 @@ async fn main() {
multipart: MultipartStore::new(),
});

let bind_addr = config.bind_addr;

// Bind TCP listener
let listener = TcpListener::bind(bind_addr)
.await
.expect("failed to bind TCP listener");

slog!("[spiceio] listening on http://{bind_addr}");
slog!(
"[spiceio] bucket: {} region: {}",
Expand Down
7 changes: 6 additions & 1 deletion src/s3/router.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1402,7 +1402,12 @@ fn with_common_headers(
}
headers.insert(X_AMZ_ID_2, request_id.parse().unwrap());
headers.insert(X_AMZ_BUCKET_REGION, region.parse().unwrap());
headers.insert("Server", "spiceio".parse().unwrap());
headers.insert(
"Server",
concat!("spiceio/", env!("CARGO_PKG_VERSION"))
.parse()
.unwrap(),
);
// CORS allow
if !headers.contains_key("access-control-allow-origin") {
headers.insert("Access-Control-Allow-Origin", "*".parse().unwrap());
Expand Down
Loading