Skip to content

Commit

Permalink
feat: parsing stdout of steamcmd
Browse files Browse the repository at this point in the history
require a i386 stdbuf (from coreutils) to change buffer strategy of steamcmd
  • Loading branch information
shirok1 committed Jan 31, 2024
1 parent a8ae5b6 commit 9b51a0b
Show file tree
Hide file tree
Showing 6 changed files with 132 additions and 13 deletions.
16 changes: 16 additions & 0 deletions frontend/interfaces/gateway.interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,19 @@ type Player = {
playeruid: string;
steamid: string; // this should be considered unique
};

type UpdateSteamMessage = {
type: "steam_self_update";
status: string;
} | {
type: "update_state";
state_id: number;
state_name: string;
progress: string;
current: number;
total: number;
} | {
type: "success"
} | {
type: "error"; reason: string
};
28 changes: 27 additions & 1 deletion frontend/pages/info.vue
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ const updateDropdownItems = [
}
}]
]
const updateLastMessage = ref<UpdateSteamMessage>()
const update_steam = async (query: { game: false } | { game: true, validate: boolean }) => {
logs.value = ""
updateModal.value = true
Expand All @@ -54,6 +55,20 @@ const update_steam = async (query: { game: false } | { game: true, validate: boo
if (data instanceof Blob) {
const text = await data.text();
logs.value += text;
} else if (typeof data === "string") {
const msg: UpdateSteamMessage = JSON.parse(data);
if (msg.type === "steam_self_update") {
console.log(`Steam self update: ${msg.status}`);
} else if (msg.type === "update_state") {
console.log(`Update state: ${msg.state_name} (${msg.progress}%)`);
} else if (msg.type === "success") {
console.log("Update success");
} else if (msg.type === "error") {
console.log("Update error");
} else {
console.log("Received unknown message:", msg);
}
updateLastMessage.value = msg
} else {
// Handle other data types
console.log("Received non blob data:", data);
Expand Down Expand Up @@ -113,10 +128,21 @@ const shutdownModal = ref(false)
<UModal v-model="updateModal" :ui="{ width: 'w-full sm:max-w-2xl' }" prevent-close>
<UCard :ui="{ ring: '', divide: 'divide-y divide-gray-100 dark:divide-gray-800' }">
<template #header>
Updating...
{{
updateLastMessage?.type === 'update_state' ? `Stage: ${updateLastMessage.state_name} (${updateLastMessage.progress}%)`
: updateLastMessage?.type === 'steam_self_update' ? 'Updating Steam...'
: updateLastMessage?.type === 'success' ? 'Update success'
: updateLastMessage?.type === 'error' ? 'Update error'
: 'Updating...'
}}
</template>
<div class="flex flex-col gap-2">
<UTextarea v-model="logs" disabled autoresize placeholder="Waiting..." />
<UProgress
:color="updateLastMessage?.type === 'error' ? 'red' : updateLastMessage?.type === 'success' ? 'green' : 'primary'"
:value="updateLastMessage?.type === 'update_state' ? (+updateLastMessage.progress)
: updateLastMessage?.type === 'success' ? 100
: updateLastMessage?.type === 'error' ? 100 : undefined" />
</div>
<template #footer>
<div class="flex gap-2 justify-end">
Expand Down
1 change: 1 addition & 0 deletions gateway/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions gateway/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ csv = "1.3.0"
futures-util = { version = "0.3.30", features = ["sink"] }
pest = "2.7.6"
pest_derive = "2.7.6"
regex = "1.10.3"
rust-ini = "0.20.0"
serde = { version = "1.0.195", features = ["derive"] }
serde_json = "1.0.112"
Expand Down
11 changes: 11 additions & 0 deletions gateway/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,24 @@ cargo build --locked --release
cp ./target/release/$APP_NAME /bin/gateway
EOF

# extract stdbuf

# use same base image for convenience
FROM cm2network/steamcmd:latest AS stdbuf

USER root
RUN dpkg --add-architecture i386 && \
apt update && \
apt install -y --allow-remove-essential coreutils:i386

# runtime stage

FROM cm2network/steamcmd:latest AS final
LABEL org.opencontainers.image.source=https://github.com/shirok1/palboard
LABEL org.opencontainers.image.description="PalBoard gateway"
LABEL org.opencontainers.image.licenses=SSPL-1.0

COPY --from=stdbuf /usr/bin/stdbuf /usr/libexec/coreutils/libstdbuf.so /bin/
COPY --from=build /bin/gateway /bin/

EXPOSE 8080
Expand Down
88 changes: 76 additions & 12 deletions gateway/src/steamcmd.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use axum::extract::ws::WebSocket;
use serde::Serialize;
use thiserror::Error;
use tokio::{
io::AsyncBufReadExt,
Expand All @@ -20,11 +21,7 @@ pub enum UpdateType {
Steam,
Game { validate: bool },
}
const STEAMCMD_UPDATE_ARGS: &[&str] = &[
"+login",
"anonymous",
"+quit",
];
const STEAMCMD_UPDATE_ARGS: &[&str] = &["+login", "anonymous", "+quit"];
const STEAMCMD_UPDATE_GAME_ARGS: &[&str] = &[
"+force_install_dir",
"/home/steam/palserver",
Expand Down Expand Up @@ -57,7 +54,9 @@ type SteamCMDResult<T> = Result<T, SteamCMDError>;
pub async fn run_steamcmd(
args: impl IntoIterator<Item = impl AsRef<OsStr>>,
) -> SteamCMDResult<(Child, ReaderStream<ChildStdout>)> {
let mut child = Command::new(STEAMCMD_EXE)
let mut child = Command::new("/bin/stdbuf")
.arg("--output=0")
.arg(STEAMCMD_EXE)
.args(args)
.stdout(Stdio::piped())
.spawn()
Expand All @@ -70,6 +69,66 @@ pub async fn run_steamcmd(
Ok((child, stdout))
}

#[derive(Debug, Serialize)]
#[serde(tag = "type", rename_all = "snake_case")]
enum UpdateSteamMessage {
SteamSelfUpdate {
status: String,
},
UpdateState {
state_id: u32,
state_name: String,
progress: String,
current: u64,
total: u64,
},
Success,
Error {
reason: String,
},
}

fn parse_line(line: &str) -> Option<UpdateSteamMessage> {
// TODO: reusing regexes
let update_state_pattern = regex::Regex::new(r"^ Update state \(0x(?<state_id>[\da-f]+)\) (?<state_name>[\w ]+), progress: (?<progress>\d*\.\d*) \((?<current>\d+) \/ (?<total>\d+)\)$").unwrap();
let steam_self_update_pattern = regex::Regex::new(r"^\[....\] (.+)$").unwrap();
let error_pattern = regex::Regex::new(r"^ERROR!.+\((.+)\)$").unwrap();

if line.starts_with("Success!") {
return Some(UpdateSteamMessage::Success);
}

if let Some(cap) = steam_self_update_pattern.captures(&line) {
let (_, [status]) = cap.extract();
let status = status.to_string();
return Some(UpdateSteamMessage::SteamSelfUpdate { status });
}

if let Some(cap) = error_pattern.captures(&line) {
let (_, [reason]) = cap.extract();
let reason = reason.to_string();
return Some(UpdateSteamMessage::Error { reason });
}

if let Some(cap) = update_state_pattern.captures(&line) {
let (_, [state_id, state_name, progress, current, total]) = cap.extract();
let state_id = u32::from_str_radix(state_id, 16).unwrap();
let current = u64::from_str_radix(current, 10).unwrap();
let total = u64::from_str_radix(total, 10).unwrap();
let state_name = state_name.to_string();
let progress = progress.to_string();
return Some(UpdateSteamMessage::UpdateState {
state_id,
state_name,
progress,
current,
total,
});
}

None
}

#[instrument(skip_all)]
pub async fn update_steam(ws: WebSocket, update_type: UpdateType) {
let (mut child, mut stdout) = run_steamcmd(match update_type {
Expand All @@ -92,12 +151,17 @@ pub async fn update_steam(ws: WebSocket, update_type: UpdateType) {
async move {
while let Some(line) = lines.next_line().await.unwrap() {
debug!("parsing line: {}", line);
ws_lr
.lock()
.await
.send(axum::extract::ws::Message::Text(line))
.await
.unwrap();

if let Some(msg) = parse_line(&line) {
ws_lr
.lock()
.await
.send(axum::extract::ws::Message::Text(
serde_json::to_string(&msg).unwrap(),
))
.await
.unwrap();
}
}
debug!("exit");
}
Expand Down

0 comments on commit 9b51a0b

Please sign in to comment.