mirror of
https://github.com/nolen777/eagle0.git
synced 2026-07-28 22:35:42 +00:00
Admin console shows all games, auto-install s3cmd, restart nginx properly (#5238)
* Fix: Move S3 backup to BEFORE stopping old server Critical fix: The old server may wipe games.e0es on shutdown if it doesn't have the save() merge fix. The backup must happen BEFORE stopping to capture valid data. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * Admin console shows all games, auto-install s3cmd, restart nginx properly 1. Admin console now shows all games from games.e0es, not just loaded ones: - Added getAllRunningGamesSummary() to GamesManager - Updated getRunningGames() to include unloaded games with "[Not loaded]" status - Clicking into a game triggers lazy loading via getGameHistory() 2. Deploy script improvements: - Auto-install s3cmd if not present (via pip or apt) - Auto-configure s3cmd for DigitalOcean Spaces from .env - Use nginx restart instead of reload to ensure all workers pick up new config 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -228,21 +228,6 @@ main() {
|
||||
log_info "Stopping eagle-${active} (flushing state to storage)..."
|
||||
docker compose -f "${COMPOSE_FILE}" stop "eagle-${active}"
|
||||
|
||||
# Backup games.e0es to S3 before switching traffic
|
||||
# This creates a point-in-time backup we can recover from if something goes wrong
|
||||
local backup_timestamp
|
||||
backup_timestamp=$(date +%Y%m%d_%H%M%S)
|
||||
log_info "Creating S3 backup of games.e0es (backup_${backup_timestamp})..."
|
||||
if command -v s3cmd &> /dev/null; then
|
||||
if s3cmd put "${APP_DIR}/saves/games.e0es" "s3://eagle0/eagle/save/backups/games.e0es.${backup_timestamp}" 2>/dev/null; then
|
||||
log_info "Backup created: s3://eagle0/eagle/save/backups/games.e0es.${backup_timestamp}"
|
||||
else
|
||||
log_warn "S3 backup failed (s3cmd put), continuing deployment"
|
||||
fi
|
||||
else
|
||||
log_warn "s3cmd not found, skipping S3 backup"
|
||||
fi
|
||||
|
||||
# Note: No need to explicitly reload games - Eagle uses lazy loading.
|
||||
# Games are loaded on-demand when users reconnect after nginx switches traffic.
|
||||
# This ensures games are always loaded from the freshest storage state.
|
||||
@@ -255,9 +240,10 @@ main() {
|
||||
sed -i.bak 's/eagle-green:40032/eagle-blue:40032/g' "${NGINX_CONF}"
|
||||
fi
|
||||
|
||||
# Reload nginx
|
||||
log_info "Reloading nginx..."
|
||||
docker compose -f "${COMPOSE_FILE}" exec nginx nginx -s reload
|
||||
# Restart nginx to ensure fresh config is loaded
|
||||
# Note: We use restart instead of reload to ensure all workers pick up the new config
|
||||
log_info "Restarting nginx..."
|
||||
docker compose -f "${COMPOSE_FILE}" restart nginx
|
||||
|
||||
# Clean up old instance and its jfr-sidecar
|
||||
log_info "Removing old eagle-${active} container..."
|
||||
@@ -290,6 +276,59 @@ main() {
|
||||
log_info "Active instance: eagle-${staging}"
|
||||
}
|
||||
|
||||
# Ensure s3cmd is installed and configured for S3 backups
|
||||
ensure_s3cmd() {
|
||||
# Install s3cmd if not present
|
||||
if ! command -v s3cmd &> /dev/null; then
|
||||
log_info "Installing s3cmd for S3 backups..."
|
||||
if command -v pip3 &> /dev/null; then
|
||||
pip3 install --quiet s3cmd
|
||||
elif command -v pip &> /dev/null; then
|
||||
pip install --quiet s3cmd
|
||||
elif command -v apt-get &> /dev/null; then
|
||||
sudo apt-get update -qq && sudo apt-get install -y -qq s3cmd
|
||||
else
|
||||
log_warn "Cannot install s3cmd (no pip or apt-get), S3 backups will be skipped"
|
||||
return 1
|
||||
fi
|
||||
|
||||
if ! command -v s3cmd &> /dev/null; then
|
||||
log_warn "s3cmd installation failed, S3 backups will be skipped"
|
||||
return 1
|
||||
fi
|
||||
log_info "s3cmd installed successfully"
|
||||
fi
|
||||
|
||||
# Configure s3cmd for DigitalOcean Spaces if not already configured
|
||||
local s3cfg="${HOME}/.s3cfg"
|
||||
if [ ! -f "${s3cfg}" ]; then
|
||||
# Load credentials from .env
|
||||
local env_file="${APP_DIR}/.env"
|
||||
if [ -f "${env_file}" ]; then
|
||||
# shellcheck source=/dev/null
|
||||
source "${env_file}"
|
||||
fi
|
||||
|
||||
if [ -z "${DO_SPACES_ACCESS_KEY:-}" ] || [ -z "${DO_SPACES_SECRET_KEY:-}" ]; then
|
||||
log_warn "DO_SPACES_ACCESS_KEY or DO_SPACES_SECRET_KEY not set, S3 backups will be skipped"
|
||||
return 1
|
||||
fi
|
||||
|
||||
log_info "Configuring s3cmd for DigitalOcean Spaces..."
|
||||
cat > "${s3cfg}" << EOF
|
||||
[default]
|
||||
access_key = ${DO_SPACES_ACCESS_KEY}
|
||||
secret_key = ${DO_SPACES_SECRET_KEY}
|
||||
host_base = sfo3.digitaloceanspaces.com
|
||||
host_bucket = %(bucket)s.sfo3.digitaloceanspaces.com
|
||||
use_https = True
|
||||
EOF
|
||||
log_info "s3cmd configured successfully"
|
||||
fi
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
# Check for required tools
|
||||
check_requirements() {
|
||||
if ! command -v docker &> /dev/null; then
|
||||
@@ -311,6 +350,9 @@ check_requirements() {
|
||||
log_error "docker-compose file not found at ${COMPOSE_FILE}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Ensure s3cmd is available for backups (non-fatal if it fails)
|
||||
ensure_s3cmd || true
|
||||
}
|
||||
|
||||
# Run
|
||||
|
||||
@@ -619,10 +619,15 @@ class EagleServiceImpl(
|
||||
override def getRunningGames(
|
||||
request: GetRunningGamesRequest
|
||||
): Future[GetRunningGamesResponse] = Future {
|
||||
val games = gamesManager.gameControllerInfos.map {
|
||||
case (gameId, controllerInfo) =>
|
||||
val controller = controllerInfo.controller
|
||||
val gameState = controller.engine.currentState
|
||||
// Get all games from storage (both loaded and unloaded)
|
||||
val allGamesSummary = gamesManager.getAllRunningGamesSummary
|
||||
|
||||
val games = allGamesSummary.map { summary =>
|
||||
if summary.isLoaded then {
|
||||
// Game is loaded - return full info
|
||||
val controllerInfo = gamesManager.gameControllerInfos(summary.gameId)
|
||||
val controller = controllerInfo.controller
|
||||
val gameState = controller.engine.currentState
|
||||
|
||||
val players = gameState.factions.map {
|
||||
case (factionId, faction) =>
|
||||
@@ -640,13 +645,34 @@ class EagleServiceImpl(
|
||||
}.toSeq
|
||||
|
||||
RunningGameInfo(
|
||||
gameId = gameId,
|
||||
gameId = summary.gameId,
|
||||
currentRound = gameState.currentRoundId,
|
||||
actionCount = controller.engine.history.count,
|
||||
players = players,
|
||||
runStatus = gameState.runStatus.toString
|
||||
)
|
||||
}.toSeq
|
||||
} else {
|
||||
// Game is not loaded - return limited info from games.e0es
|
||||
val players = summary.userToPid.map {
|
||||
case (userName, factionId) =>
|
||||
RunningGamePlayerInfo(
|
||||
factionId = factionId,
|
||||
factionName = "[Not loaded]",
|
||||
leaderName = "[Not loaded]",
|
||||
isHuman = true,
|
||||
userName = userName
|
||||
)
|
||||
}.toSeq
|
||||
|
||||
RunningGameInfo(
|
||||
gameId = summary.gameId,
|
||||
currentRound = 0, // Unknown until loaded
|
||||
actionCount = 0, // Unknown until loaded
|
||||
players = players,
|
||||
runStatus = "[Not loaded - click to load]"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
GetRunningGamesResponse(games = games)
|
||||
}
|
||||
@@ -654,6 +680,9 @@ class EagleServiceImpl(
|
||||
override def getGameHistory(
|
||||
request: GetGameHistoryRequest
|
||||
): Future[GetGameHistoryResponse] = Future {
|
||||
// Try to load the game if not already loaded
|
||||
gamesManager.loadGame(request.gameId)
|
||||
|
||||
gamesManager.gameControllerInfos.get(request.gameId) match {
|
||||
case Some(controllerInfo) =>
|
||||
val history = controllerInfo.controller.engine.history
|
||||
|
||||
@@ -1285,4 +1285,37 @@ class GamesManager(
|
||||
// explicit flush calls here if needed
|
||||
SimpleTimedLogger.printLogger.logLine("Flush complete")
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns summary info for all running games from storage, including both loaded and unloaded games. Used by admin
|
||||
* console to show all games without requiring them to be loaded into memory.
|
||||
*/
|
||||
def getAllRunningGamesSummary: Vector[RunningGameSummary] = this.synchronized {
|
||||
val runningGames = readRunningGamesFromStorage()
|
||||
|
||||
runningGames.map { rg =>
|
||||
val isLoaded = gameControllerInfos.contains(rg.gameId)
|
||||
RunningGameSummary(
|
||||
gameId = rg.gameId,
|
||||
userToPid = rg.userToPid,
|
||||
isLoaded = isLoaded
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads a game into memory if not already loaded. Used by admin console when clicking into a game.
|
||||
*
|
||||
* @return
|
||||
* true if game is now loaded (or was already loaded), false if game doesn't exist
|
||||
*/
|
||||
def loadGame(gameId: GameId): Boolean = this.synchronized {
|
||||
ensureGameLoaded(gameId)
|
||||
}
|
||||
}
|
||||
|
||||
case class RunningGameSummary(
|
||||
gameId: GameId,
|
||||
userToPid: Map[String, FactionId],
|
||||
isLoaded: Boolean
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user