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
8 changes: 4 additions & 4 deletions build/dependencies.lock
Original file line number Diff line number Diff line change
Expand Up @@ -207,10 +207,10 @@ legacy_channels = ["development"]
[legacy_inventory]
manifest = "build/legacy-manifest.json"
schema = 1
component_count = 332
file_count = 2131
byte_count = 314694727
source_build_components = 228
component_count = 333
file_count = 2132
byte_count = 314698725
source_build_components = 229
unresolved_components = 104
inherited_baseline = "07505ea58c7bba698d6b9220ff43946a43cac76b"

Expand Down
21 changes: 17 additions & 4 deletions build/legacy-manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -488,8 +488,8 @@
"kind": "runtime",
"path": "static/build/.tmp_update/bin/bloom-dev-deploy",
"file_count": 1,
"byte_count": 32634,
"tree_sha256": "14aa71a938a0545d2e747efc3e7eecea17973404034098b488c9fd613c86ac18",
"byte_count": 32728,
"tree_sha256": "fc439fab7129c7c67ad7c4a3cc7e910f27bebc08097ffe0eb7f33c4b9d7ec0bd",
"source": "https://github.com/boburning/BloomOS",
"source_revision": "release-commit",
"license": "GPL-3.0-only",
Expand Down Expand Up @@ -613,6 +613,19 @@
"build_recipe": "Makefile",
"resolution": "source-build"
},
{
"id": "runtime-tmp-update-bin-bloom-network-metadata",
"kind": "runtime",
"path": "static/build/.tmp_update/bin/bloom-network-metadata",
"file_count": 1,
"byte_count": 3711,
"tree_sha256": "4fabe59df76cb5a9a3d9efa39e0cd5a8570b0dd84b94eaca880da5cb60a8dfdc",
"source": "https://github.com/boburning/BloomOS",
"source_revision": "release-commit",
"license": "GPL-3.0-only",
"build_recipe": "Makefile",
"resolution": "source-build"
},
{
"id": "runtime-tmp-update-bin-bloom-network-services",
"kind": "runtime",
Expand Down Expand Up @@ -982,8 +995,8 @@
"kind": "runtime",
"path": "static/build/.tmp_update/bin/bloom-wifi",
"file_count": 1,
"byte_count": 10100,
"tree_sha256": "fbb865dbb7c93af5a1ad4b4a36a4098450d076db3bc9f2699bf7df28fa1adf94",
"byte_count": 10293,
"tree_sha256": "11378a22655246e12060c3c87ce2c7a7ef0ee105dddd8a1c70d52dabf46e4adc",
"source": "https://github.com/boburning/BloomOS",
"source_revision": "release-commit",
"license": "GPL-3.0-only",
Expand Down
13 changes: 12 additions & 1 deletion docs/ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -969,7 +969,18 @@ DHCP recovery or fresh station start, `associated` only after both carrier and
global IPv4 are present, and `offline` after radio shutdown. Preserving an
already-healthy station is an idempotent associated publication. Notification
failure is deliberately secondary: it cannot restart or roll back a healthy
station. Individual client consumption remains the next integration step.
station.

`bloom-network-metadata` is the first fixed consumer. It validates the complete
schema-1 metadata event, asks `bloom-library` to apply only that associated or
offline observation to its durable job scheduler, and acknowledges only the
same exact generation after a valid scheduler result. The scheduler moves only
due network-required work between `waiting-network` and its runnable state;
local, future-deadline, leased, completed, failed, and cancelled work is not
claimed or run by this boundary. Invalid event or scheduler output, a busy or
failed database, and acknowledgement failure leave the generation pending for
a later idempotent reconciliation. Delivery remains secondary to Wi-Fi health.
Update and sync consumption remain separate follow-up integration steps.

Bounded `request enable` and `request disable` operations persist only
`device.wifi_enabled` through the active `bloom-settings` authority before
Expand Down
61 changes: 37 additions & 24 deletions src/bloomLibrary/bloom_library_jobs.c
Original file line number Diff line number Diff line change
Expand Up @@ -521,6 +521,41 @@ int bloom_library_job_recover_expired(sqlite3 *database, const char *owner, cons
return finish(database, result);
}

static int network_observe_locked(sqlite3 *database, sqlite3_int64 now,
int network_available, int *changed)
{
const char *sql = network_available
? "UPDATE metadata_jobs SET state='queued',updated_at=?1 WHERE "
"requires_network=1 AND state='waiting-network' AND "
"next_attempt_at<=?1"
: "UPDATE metadata_jobs SET state='waiting-network',updated_at=?1 "
"WHERE requires_network=1 AND state IN('queued','retry-wait') AND "
"next_attempt_at<=?1";
sqlite3_stmt *statement = NULL;
int result = sqlite3_prepare_v2(database, sql, -1, &statement, NULL);
if (result == SQLITE_OK)
result = sqlite3_bind_int64(statement, 1, now);
if (result == SQLITE_OK)
result = sqlite3_step(statement) == SQLITE_DONE ? SQLITE_OK : sqlite3_errcode(database);
if (result == SQLITE_OK && changed != NULL)
*changed = sqlite3_changes(database);
sqlite3_finalize(statement);
return result;
}

int bloom_library_job_network_observe(sqlite3 *database, sqlite3_int64 now,
int network_available, int *changed)
{
if (database == NULL || changed == NULL || now < 0 ||
(network_available != 0 && network_available != 1))
return SQLITE_MISUSE;
*changed = 0;
int result = begin(database);
if (result == SQLITE_OK)
result = network_observe_locked(database, now, network_available, changed);
return finish(database, result);
}

int bloom_library_job_claim(sqlite3 *database, const char *owner, const char *token,
sqlite3_int64 now, int network_available, int lease_seconds,
BloomLibraryJob *job, int *found)
Expand All @@ -537,30 +572,8 @@ int bloom_library_job_claim(sqlite3 *database, const char *owner, const char *to
if (result == SQLITE_OK)
result = writer_valid(database, owner, token, now, &writer_expires);
sqlite3_stmt *statement = NULL;
if (result == SQLITE_OK && network_available)
result = sqlite3_prepare_v2(
database,
"UPDATE metadata_jobs SET state='queued',updated_at=?1 WHERE "
"state='waiting-network' AND next_attempt_at<=?1",
-1, &statement, NULL);
if (result == SQLITE_OK && network_available)
result = sqlite3_bind_int64(statement, 1, now);
if (result == SQLITE_OK && network_available)
result = sqlite3_step(statement) == SQLITE_DONE ? SQLITE_OK : sqlite3_errcode(database);
sqlite3_finalize(statement);
statement = NULL;
if (result == SQLITE_OK && !network_available)
result = sqlite3_prepare_v2(
database,
"UPDATE metadata_jobs SET state='waiting-network',updated_at=?1 WHERE "
"requires_network=1 AND state IN('queued','retry-wait') AND next_attempt_at<=?1",
-1, &statement, NULL);
if (result == SQLITE_OK && !network_available)
result = sqlite3_bind_int64(statement, 1, now);
if (result == SQLITE_OK && !network_available)
result = sqlite3_step(statement) == SQLITE_DONE ? SQLITE_OK : sqlite3_errcode(database);
sqlite3_finalize(statement);
statement = NULL;
if (result == SQLITE_OK)
result = network_observe_locked(database, now, network_available, NULL);
sqlite3_int64 job_id = 0;
if (result == SQLITE_OK)
result = sqlite3_prepare_v2(
Expand Down
4 changes: 4 additions & 0 deletions src/bloomLibrary/bloom_library_jobs.h
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,10 @@ int bloom_library_job_review_list(sqlite3 *database, sqlite3_int64 after_job_id,
int bloom_library_job_retry(sqlite3 *database, sqlite3_int64 job_id, sqlite3_int64 now,
int *changed);

/* Applies one exact connectivity observation without claiming or running work. */
int bloom_library_job_network_observe(sqlite3 *database, sqlite3_int64 now,
int network_available, int *changed);

int bloom_library_job_writer_acquire(sqlite3 *database, const char *owner, const char *token,
sqlite3_int64 now, int lease_seconds, int *acquired);
int bloom_library_job_writer_renew(sqlite3 *database, const char *owner, const char *token,
Expand Down
26 changes: 23 additions & 3 deletions src/bloomLibrary/main.c
Original file line number Diff line number Diff line change
Expand Up @@ -797,6 +797,9 @@ int main(int argc, char **argv)
int review_request = argc >= 2 && strcmp(argv[1], "review") == 0;
int retry_request = argc >= 2 && strcmp(argv[1], "retry") == 0;
int storage_policy_request = argc >= 2 && strcmp(argv[1], "storage-policy") == 0;
int network_observe_request =
argc == 3 && strcmp(argv[1], "network-observe") == 0 &&
(strcmp(argv[2], "associated") == 0 || strcmp(argv[2], "offline") == 0);
int import_command = argc == 2 && strcmp(argv[1], "import-onion") == 0;
int import_legacy = argc == 2 && strcmp(argv[1], "import-legacy") == 0;
int import_gamelist = argc == 6 && strcmp(argv[1], "import-gamelist") == 0 &&
Expand All @@ -816,16 +819,17 @@ int main(int argc, char **argv)
int mutate_favorite = argc >= 2 && strcmp(argv[1], "favorite") == 0;
int mutate_recent = argc >= 2 && strcmp(argv[1], "recent") == 0;
if (!status_command && !operations_request && !review_request && !retry_request &&
!storage_policy_request && !import_command && !import_legacy && !import_gamelist &&
!scrape_local && !artwork_local_all && !artwork_local_system && !scan_all &&
!scan_system && !query_games && !mutate_favorite && !mutate_recent) {
!storage_policy_request && !network_observe_request && !import_command && !import_legacy &&
!import_gamelist && !scrape_local && !artwork_local_all && !artwork_local_system &&
!scan_all && !scan_system && !query_games && !mutate_favorite && !mutate_recent) {
fprintf(stderr,
"Usage: bloom-library status|operations|review --limit N [--after JOB_ID]|"
"retry JOB_ID|storage-policy --quota-mib N --reserve-mib N|"
"import-onion|import-legacy|scan "
"--changed|--all|--system SYSTEM|games --limit N [--after GAME_ID]|games "
"--system SYSTEM --limit N [--after GAME_ID]|favorite set GAME_ID true|false|"
"recent record GAME_ID|import-gamelist --system SYSTEM --file FILE|"
"network-observe associated|offline|"
"scrape-missing --local --system SYSTEM|"
"artwork-fallback --libretro-local [--system SYSTEM]|"
"fingerprint-benchmark --root ROOT --path RELATIVE --buffer BYTES|"
Expand All @@ -844,6 +848,22 @@ int main(int argc, char **argv)
fprintf(stderr, "{\"schema\":1,\"error\":{\"code\":\"library_unavailable\"}}\n");
return 1;
}
if (network_observe_request) {
int changed = 0;
int available = strcmp(argv[2], "associated") == 0;
int result = bloom_library_job_network_observe(database, current_time_seconds(), available,
&changed);
sqlite3_close(database);
if (result != SQLITE_OK) {
fprintf(stderr,
"{\"schema\":1,\"error\":{\"code\":\"network_observation_failed\"}}\n");
return 1;
}
printf("{\"schema\":1,\"service\":\"bloom-library\",\"operation\":"
"\"network-observe\",\"connectivity\":\"%s\",\"jobs_changed\":%d}\n",
argv[2], changed);
return 0;
}
if (import_command) {
BloomLibraryImportResult imported;
char error[160] = {0};
Expand Down
2 changes: 2 additions & 0 deletions static/build/.tmp_update/bin/bloom-dev-deploy
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ allowed_path() {
launch-session:bin/bloom-ra-session-prepare|\
launch-session:script/bloom-launch-trace.sh) return 0 ;;
network-helpers:bin/bloom-settings|network-helpers:bin/bloom-network|\
network-helpers:bin/bloom-network-metadata|\
network-helpers:bin/bloom-network-services|network-helpers:bin/bloom-network-transaction|\
network-helpers:bin/bloom-platform|network-helpers:bin/bloom-dev-ssh|\
network-helpers:bin/bloom-sftp-gate|network-helpers:bin/bloom-sftp-server|\
Expand Down Expand Up @@ -191,6 +192,7 @@ validate_manifest() {
expected_paths="$(printf '%s\n' \
bin/bloom-settings \
bin/bloom-network \
bin/bloom-network-metadata \
bin/bloom-network-services \
bin/bloom-network-transaction \
bin/bloom-platform \
Expand Down
94 changes: 94 additions & 0 deletions static/build/.tmp_update/bin/bloom-network-metadata
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
#!/bin/sh

set -u

SYSTEM_ROOT=${BLOOM_NETWORK_METADATA_SYSTEM_ROOT:-/mnt/SDCARD/.tmp_update}
NETWORK=${BLOOM_NETWORK_METADATA_NETWORK_BIN:-$SYSTEM_ROOT/bin/bloom-network}
LIBRARY=${BLOOM_NETWORK_METADATA_LIBRARY_BIN:-$SYSTEM_ROOT/bin/bloom-library}
JQ=${BLOOM_NETWORK_METADATA_JQ_BIN:-$SYSTEM_ROOT/bin/jq}
LIB_DIR=${BLOOM_NETWORK_METADATA_LIB_DIR:-$SYSTEM_ROOT/lib}

result() {
printf '{"schema":1,"service":"bloom-network-metadata","applied":%s,"state":"%s"}\n' \
"$1" "$2"
}

reconcile() {
[ -x "$NETWORK" ] && [ -x "$LIBRARY" ] && [ -x "$JQ" ] || {
result false dependency_unavailable
return 1
}
event=$($NETWORK events status metadata 2>/dev/null) || {
result false event_unavailable
return 1
}
fields=$(printf '%s\n' "$event" | "$JQ" -er '
if .schema == 1 and .service == "bloom-network-events" and
.operation == "status" and .client == "metadata" and
(.connectivity == "unknown" or .connectivity == "associated" or
.connectivity == "offline") and
(.generation | type) == "number" and .generation >= 0 and
.generation <= 2147483647 and (.generation | floor) == .generation and
(.pending | type) == "boolean"
then "\(.connectivity) \(.generation) \(.pending)"
else error("invalid metadata network event") end
' 2>/dev/null) || {
result false event_invalid
return 1
}
set -- $fields
[ "$#" -eq 3 ] || { result false event_invalid; return 1; }
connectivity=$1
generation=$2
pending=$3
if [ "$pending" = false ]; then
result false no_pending_event
return 0
fi
case "$connectivity:$generation" in
associated:[1-9]*|offline:[1-9]*) ;;
*) result false event_invalid; return 1 ;;
esac
observation=$(LD_LIBRARY_PATH="$LIB_DIR${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}" \
"$LIBRARY" network-observe "$connectivity" 2>/dev/null) || {
result false observation_failed
return 1
}
jobs_changed=$(printf '%s\n' "$observation" | "$JQ" -er \
'if .schema == 1 and .service == "bloom-library" and
.operation == "network-observe" and
(.connectivity == "associated" or .connectivity == "offline") and
(.jobs_changed | type) == "number" and .jobs_changed >= 0 and
.jobs_changed <= 2147483647 and (.jobs_changed | floor) == .jobs_changed
then .jobs_changed else error("invalid metadata observation") end' 2>/dev/null) || {
result false observation_invalid
return 1
}
[ "$(printf '%s\n' "$observation" | "$JQ" -r '.connectivity')" = "$connectivity" ] || {
result false observation_invalid
return 1
}
acknowledgement=$($NETWORK events ack metadata "$generation" 2>/dev/null) || {
result false acknowledgement_failed
return 1
}
printf '%s\n' "$acknowledgement" | "$JQ" -e \
--argjson generation "$generation" --arg connectivity "$connectivity" '
.schema == 1 and .service == "bloom-network-events" and
.operation == "ack" and .client == "metadata" and
.connectivity == $connectivity and .generation == $generation and
.pending == false
' >/dev/null 2>&1 || {
result false acknowledgement_invalid
return 1
}
printf '{"schema":1,"service":"bloom-network-metadata","applied":true,'
printf '"state":"acknowledged","connectivity":"%s","generation":%s,' \
"$connectivity" "$generation"
printf '"jobs_changed":%s}\n' "$jobs_changed"
}

case "$#:${1:-}" in
1:reconcile) reconcile ;;
*) result false invalid_arguments; exit 2 ;;
esac
6 changes: 5 additions & 1 deletion static/build/.tmp_update/bin/bloom-wifi
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ SLEEP=${BLOOM_WIFI_SLEEP_BIN:-sleep}
START_SLEEP=${BLOOM_WIFI_START_SLEEP_BIN:-sleep}
SERVICES=${BLOOM_WIFI_SERVICES_BIN:-/mnt/SDCARD/.tmp_update/bin/bloom-network-services}
NETWORK=${BLOOM_WIFI_NETWORK_BIN:-/mnt/SDCARD/.tmp_update/bin/bloom-network}
METADATA=${BLOOM_WIFI_METADATA_EVENT_BIN:-/mnt/SDCARD/.tmp_update/bin/bloom-network-metadata}
NOHUP=${BLOOM_WIFI_NOHUP_BIN:-/usr/bin/nohup}
SELF=${BLOOM_WIFI_SELF:-/mnt/SDCARD/.tmp_update/bin/bloom-wifi}
INTERFACE=${BLOOM_WIFI_INTERFACE:-wlan0}
Expand Down Expand Up @@ -55,7 +56,10 @@ require_executable() { [ -x "$1" ]; }

notify_connectivity() {
[ -x "$NETWORK" ] || return 0
"$NETWORK" events publish "$1" >/dev/null 2>&1 || true
if "$NETWORK" events publish "$1" >/dev/null 2>&1; then
[ ! -x "$METADATA" ] || "$METADATA" reconcile >/dev/null 2>&1 || true
fi
return 0
}

launch_dhcp() {
Expand Down
1 change: 1 addition & 0 deletions test/shell/dev_deploy.bats
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,7 @@ stage_network_bundle() {
for path in \
bin/bloom-settings \
bin/bloom-network \
bin/bloom-network-metadata \
bin/bloom-network-services \
bin/bloom-network-transaction \
bin/bloom-platform \
Expand Down
4 changes: 2 additions & 2 deletions test/shell/legacy_manifest.bats
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
--manifest /workspace/build/legacy-manifest.json

[ "$status" -eq 0 ]
[[ "$output" == "legacy manifest validate: 332 components" ]]
[[ "$output" == "legacy manifest validate: 333 components" ]]
[ ! -e "/workspace/static/packages/RApp/SNK - Neo Geo (GnGeo)" ]
[ ! -e "/workspace/static/packages/RApp/PICO-8 (PICO-8 standalone)" ]
[ ! -e "/workspace/static/packages/RApp/SCUMM (ScummVM standalone)" ]
Expand Down Expand Up @@ -104,7 +104,7 @@ import sys
components = json.load(open(sys.argv[1], encoding="utf-8"))["components"]
runtime = [item for item in components if item["id"].startswith("runtime-")]
bloom = [item for item in runtime if item["source"] == "https://github.com/boburning/BloomOS"]
assert len(bloom) == 55
assert len(bloom) == 56
assert all(item["resolution"] == "source-build" for item in bloom)
assert all(item["source_revision"] == "release-commit" for item in bloom)
PY
Expand Down
Loading
Loading