finished actions => handlers refacto, small bux fix in maestro => Test maestro1 OK

This commit is contained in:
STEINNI
2026-06-20 20:10:14 +00:00
parent 44a84c64ec
commit 3066a54a4c
32 changed files with 386 additions and 33 deletions
+1 -1
View File
@@ -8,7 +8,7 @@ import {
positionAt, positionAt,
needsPrismRefresh, needsPrismRefresh,
advanceAgentSegment, advanceAgentSegment,
} from './actions/arena/worldline.js' } from './handlers/arena/worldline.js'
export class gpsServer { export class gpsServer {
+2 -2
View File
@@ -6,8 +6,8 @@ import {RedisConnexion} from '../redisConnexion.js'
import { busReplyRoute } from '../bus/publishActionReply.js' import { busReplyRoute } from '../bus/publishActionReply.js'
import {configHelper} from '../configHelper.js' import {configHelper} from '../configHelper.js'
import {gpsServer} from './gpsServer.js' import {gpsServer} from './gpsServer.js'
import * as systemMesh from './actions/system/index.js' import * as systemMesh from './handlers/system/index.js'
import * as arenaMesh from './actions/arena/index.js' import * as arenaMesh from './handlers/arena/index.js'
const meshModules = { const meshModules = {
system: systemMesh, system: systemMesh,
+13 -3
View File
@@ -1,5 +1,11 @@
#!/bin/sh #!/bin/sh
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
# shellcheck source=../lib/resolveConfigPath.sh
. "$SCRIPT_DIR/../lib/resolveConfigPath.sh"
CONFIG="$(resolveConfigPath "$SCRIPT_DIR" "${1:-${CONFIG:-../config.json}}")"
set -a set -a
. /etc/p42/secrets.env . /etc/p42/secrets.env
set +a set +a
@@ -7,12 +13,16 @@ set +a
daemon=p42Gps daemon=p42Gps
logfile=gps.log logfile=gps.log
if [ ! -f "$CONFIG" ]; then
echo "Config file not found: $CONFIG" >&2
exit 1
fi
pid=$(pgrep -f "$daemon") pid=$(pgrep -f "$daemon")
if [ -z "$pid" ] if [ -z "$pid" ]
then then
node "${daemon}.js" --debug > "$logfile" 2>&1 & node "${daemon}.js" --config "$CONFIG" --debug > "$logfile" 2>&1 &
pid=$! pid=$!
sleep 1 sleep 1
@@ -20,16 +30,16 @@ then
if kill -0 "$pid" 2>/dev/null if kill -0 "$pid" 2>/dev/null
then then
echo "" echo ""
echo "$daemon is now running with PID=$pid" echo "$daemon is now running with PID=$pid (config=$CONFIG)"
echo "" echo ""
else else
echo "" echo ""
echo "Failed to start $daemon. Check gps.log" echo "Failed to start $daemon. Check gps.log"
echo "" echo ""
exit 1
fi fi
else else
echo "" echo ""
echo "$daemon is already running with PID=$pid" echo "$daemon is already running with PID=$pid"
echo "" echo ""
fi fi
+2 -2
View File
@@ -78,9 +78,9 @@ export class maestroServer {
refreshPrepareQuorum() { refreshPrepareQuorum() {
if(!this.arenaCnx) return if(!this.arenaCnx) return
const { prepareAckChannel, readyTimeoutMs } = this.getMaestroSettings() const { lifecycle, readyTimeoutMs } = this.getMaestroSettings()
this.prepareQuorum = new PrepareQuorum({ this.prepareQuorum = new PrepareQuorum({
ackChannel: prepareAckChannel, ackChannel: lifecycle.prepareAckChannel,
timeoutMs: readyTimeoutMs, timeoutMs: readyTimeoutMs,
matchesChan: this.arenaCnx.matchesChan.bind(this.arenaCnx), matchesChan: this.arenaCnx.matchesChan.bind(this.arenaCnx),
debug: this.debug, debug: this.debug,
+2 -2
View File
@@ -6,8 +6,8 @@ import { RedisConnexion } from '../redisConnexion.js'
import { busReplyRoute } from '../bus/publishActionReply.js' import { busReplyRoute } from '../bus/publishActionReply.js'
import { configHelper } from '../configHelper.js' import { configHelper } from '../configHelper.js'
import { maestroServer } from './maestroServer.js' import { maestroServer } from './maestroServer.js'
import * as systemMesh from './actions/system/index.js' import * as systemMesh from './handlers/system/index.js'
import * as arenaMesh from './actions/arena/index.js' import * as arenaMesh from './handlers/arena/index.js'
const meshModules = { const meshModules = {
system: systemMesh, system: systemMesh,
+14 -2
View File
@@ -1,5 +1,11 @@
#!/bin/sh #!/bin/sh
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
# shellcheck source=../lib/resolveConfigPath.sh
. "$SCRIPT_DIR/../lib/resolveConfigPath.sh"
CONFIG="$(resolveConfigPath "$SCRIPT_DIR" "${1:-${CONFIG:-../config.json}}")"
set -a set -a
. /etc/p42/secrets.env . /etc/p42/secrets.env
set +a set +a
@@ -7,11 +13,16 @@ set +a
daemon=p42Maestro daemon=p42Maestro
logfile=maestro.log logfile=maestro.log
if [ ! -f "$CONFIG" ]; then
echo "Config file not found: $CONFIG" >&2
exit 1
fi
pid=$(pgrep -f "$daemon") pid=$(pgrep -f "$daemon")
if [ -z "$pid" ] if [ -z "$pid" ]
then then
node "${daemon}.js" --debug > "$logfile" 2>&1 & node "${daemon}.js" --config "$CONFIG" --debug > "$logfile" 2>&1 &
pid=$! pid=$!
sleep 1 sleep 1
@@ -19,12 +30,13 @@ then
if kill -0 "$pid" 2>/dev/null if kill -0 "$pid" 2>/dev/null
then then
echo "" echo ""
echo "$daemon is now running with PID=$pid" echo "$daemon is now running with PID=$pid (config=$CONFIG)"
echo "" echo ""
else else
echo "" echo ""
echo "Failed to start $daemon. Check maestro.log" echo "Failed to start $daemon. Check maestro.log"
echo "" echo ""
exit 1
fi fi
else else
echo "" echo ""
+1 -1
View File
@@ -1,4 +1,4 @@
import { positionAt } from '../GPS/actions/arena/worldline.js' import { positionAt } from '../GPS/handlers/arena/worldline.js'
export class GpsStorageReader { export class GpsStorageReader {
+2 -2
View File
@@ -6,8 +6,8 @@ import { RedisConnexion } from '../redisConnexion.js'
import { busReplyRoute } from '../bus/publishActionReply.js' import { busReplyRoute } from '../bus/publishActionReply.js'
import { configHelper } from '../configHelper.js' import { configHelper } from '../configHelper.js'
import { observerServer } from './observerServer.js' import { observerServer } from './observerServer.js'
import * as systemMesh from './actions/system/index.js' import * as systemMesh from './handlers/system/index.js'
import * as arenaMesh from './actions/arena/index.js' import * as arenaMesh from './handlers/arena/index.js'
const meshModules = { const meshModules = {
system: systemMesh, system: systemMesh,
+1 -1
View File
@@ -1,4 +1,4 @@
import { positionAt } from '../GPS/actions/arena/worldline.js' import { positionAt } from '../GPS/handlers/arena/worldline.js'
import { Frustum } from './frustum.js' import { Frustum } from './frustum.js'
export class RequestorRegistry { export class RequestorRegistry {
+14 -2
View File
@@ -1,5 +1,11 @@
#!/bin/sh #!/bin/sh
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
# shellcheck source=../lib/resolveConfigPath.sh
. "$SCRIPT_DIR/../lib/resolveConfigPath.sh"
CONFIG="$(resolveConfigPath "$SCRIPT_DIR" "${1:-${CONFIG:-../config.json}}")"
set -a set -a
. /etc/p42/secrets.env . /etc/p42/secrets.env
set +a set +a
@@ -7,11 +13,16 @@ set +a
daemon=p42Observer daemon=p42Observer
logfile=observer.log logfile=observer.log
if [ ! -f "$CONFIG" ]; then
echo "Config file not found: $CONFIG" >&2
exit 1
fi
pid=$(pgrep -f "$daemon") pid=$(pgrep -f "$daemon")
if [ -z "$pid" ] if [ -z "$pid" ]
then then
node "${daemon}.js" --debug > "$logfile" 2>&1 & node "${daemon}.js" --config "$CONFIG" --debug > "$logfile" 2>&1 &
pid=$! pid=$!
sleep 1 sleep 1
@@ -19,12 +30,13 @@ then
if kill -0 "$pid" 2>/dev/null if kill -0 "$pid" 2>/dev/null
then then
echo "" echo ""
echo "$daemon is now running with PID=$pid" echo "$daemon is now running with PID=$pid (config=$CONFIG)"
echo "" echo ""
else else
echo "" echo ""
echo "Failed to start $daemon. Check observer.log" echo "Failed to start $daemon. Check observer.log"
echo "" echo ""
exit 1
fi fi
else else
echo "" echo ""
+110
View File
@@ -0,0 +1,110 @@
{
"accessRights": [
{
"canDo": [
"RELOADCONFIG",
"GETCONFIG"
],
"roles": [
"admin"
]
},
{
"canDo": [
"GETAGENTPOSITION",
"GETAGENTSINFRUSTUM",
"SUBSCRIBEFRUSTUM"
],
"roles": "*"
},
{
"canDo": [
"STARTSIMULATION",
"STOPSIMULATION"
],
"roles": "*"
}
],
"gps": {
"primordialDaemon": true,
"gpsActionsChannel": "system:requests:gps",
"gpsActionsReply": "system:replies:[UID]",
"GPSstorage": {
"agentHashKey": "system:gps:agent:[UID]",
"agentsIndexKey": "system:gps:agents",
"positionsStream": "system:gps:positions",
"streamMaxLen": 100000
},
"agentVectorChangeChannel": "arena:agents:*",
"collisionsChannel": "arena:agents:[UID]",
"lifecycle": {
"arenaChannel": "arena:lifecycle",
"godsReadyChannel": "arena:gods:ready"
},
"arenaStorage": {
"agentHashKey": "arena:agents:[UID]",
"agentsIndexKey": "arena:agents"
},
"senderId": "gps",
"nearMissDistance": 1,
"prismTimeHeight": 60,
"collisionTickMs": 100,
"prismRefreshLeadSeconds": 1
},
"maestro": {
"maestroActionsChannel": "system:requests:maestro",
"maestroActionsReply": "system:replies:[UID]",
"senderId": "maestro",
"lifecycle": {
"arenaChannel": "arena:lifecycle",
"godsReadyChannel": "arena:gods:ready"
},
"readyTimeoutMs": 30000
},
"mysql": {
"socketPath": "/var/run/mysqld/mysqld.sock",
"guiDatabase": "test_p42GUI",
"simDatabase": "test_p42SIM"
},
"observer": {
"primordialDaemon": false,
"observerActionsChannel": "system:requests:observer",
"observerActionsReply": "system:replies:[UID]",
"senderId": "observer",
"scanIntervalMs": 300,
"lifecycle": {
"arenaChannel": "arena:lifecycle",
"godsReadyChannel": "arena:gods:ready"
}
},
"systemMesh": {
"redis": [
{
"redisId": "SYS_1",
"role": "primary",
"host": "127.0.0.1",
"tls": false,
"port": 6380,
"user": "",
"pass": "",
"chansNamespace": "system:",
"basePrefix": "messageBus:"
}
]
},
"arenaMesh": {
"redis": [
{
"redisId": "ARN_1",
"role": "primary",
"host": "127.0.0.1",
"tls": false,
"port": 6379,
"user": "",
"pass": "",
"chansNamespace": "arena:",
"basePrefix": "messageBus:"
}
]
}
}
+22
View File
@@ -0,0 +1,22 @@
#!/bin/sh
# resolveConfigPath BASE PATH
# Turn PATH into an absolute path; relative segments are resolved from BASE.
resolveConfigPath() {
_base=$1
_path=$2
case "$_path" in
/*)
printf '%s\n' "$_path"
;;
*)
_dir=$(dirname "$_path")
_name=$(basename "$_path")
if [ "$_dir" = . ]; then
printf '%s\n' "$_base/$_name"
else
printf '%s\n' "$(cd "$_base/$_dir" && pwd)/$_name"
fi
;;
esac
}
+75
View File
@@ -0,0 +1,75 @@
#!/bin/bash
GOD_FOLDERS="Maestro GPS Observer"
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck source=lib/resolveConfigPath.sh
. "$ROOT/lib/resolveConfigPath.sh"
CONFIG="config.json"
usage() {
echo "Usage: $(basename "$0") [-c|--config PATH]" >&2
echo " PATH is relative to $ROOT unless absolute." >&2
}
while [ $# -gt 0 ]; do
case "$1" in
-c|--config)
if [ -z "${2:-}" ]; then
echo "Missing value for $1" >&2
usage
exit 1
fi
CONFIG="$2"
shift 2
;;
-h|--help)
usage
exit 0
;;
*)
CONFIG="$1"
shift
;;
esac
done
CONFIG="$(resolveConfigPath "$ROOT" "$CONFIG")"
if [ ! -f "$CONFIG" ]; then
echo "Config file not found: $CONFIG" >&2
exit 1
fi
failed=0
for folder in $GOD_FOLDERS; do
dir="$ROOT/$folder"
if [ ! -d "$dir" ]; then
echo "Missing daemon folder: $dir" >&2
failed=1
continue
fi
shopt -s nullglob
scripts=("$dir"/start*.sh)
shopt -u nullglob
if [ ${#scripts[@]} -eq 0 ]; then
echo "No start script in $dir" >&2
failed=1
continue
fi
if [ ${#scripts[@]} -gt 1 ]; then
echo "Multiple start scripts in $dir, using ${scripts[0]}" >&2
fi
echo "=== Starting $folder (config=$CONFIG) ==="
(cd "$dir" && bash "${scripts[0]}" "$CONFIG") || {
echo "Failed to start $folder" >&2
failed=1
}
done
exit $failed
Executable
+38
View File
@@ -0,0 +1,38 @@
#!/bin/bash
GOD_FOLDERS="Maestro GPS Observer"
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
failed=0
read -ra folders <<< "$GOD_FOLDERS"
for((i=${#folders[@]}-1; i>=0; i--)); do
folder="${folders[i]}"
dir="$ROOT/$folder"
if [ ! -d "$dir" ]; then
echo "Missing daemon folder: $dir" >&2
failed=1
continue
fi
shopt -s nullglob
scripts=("$dir"/stop*.sh)
shopt -u nullglob
if [ ${#scripts[@]} -eq 0 ]; then
echo "No stop script in $dir" >&2
failed=1
continue
fi
if [ ${#scripts[@]} -gt 1 ]; then
echo "Multiple stop scripts in $dir, using ${scripts[0]}" >&2
fi
echo "=== Stopping $folder ==="
(cd "$dir" && bash "${scripts[0]}") || {
echo "Failed to stop $folder" >&2
failed=1
}
done
exit $failed
+10 -3
View File
@@ -1,4 +1,11 @@
clear; node test.js --guiDatabase test_p42GUI --simDatabase test_p42SIM maestro1 --userUuid a4f33373-6adf-4d2d-9a6d-7fa0abf8b01f --simulationUuid 0x019ec742e12175c685a97bf9300b6b49 # Test DBs: point all gods at the same config (or edit config.json mysql section).
# Example:
# ./startAllGods.sh --config /opt/p42GodDaemons/config.json
# ./Maestro/startMaestro.sh /opt/p42GodDaemons/config.json
clear; node test.js maestro1 \
--config ../config.json \
--guiDatabase test_p42GUI --simDatabase test_p42SIM \
--fakeAgentsReady --fakeGpsReady \
--userUuid a4f33373-6adf-4d2d-9a6d-7fa0abf8b01f \
--simulationUuid 0x019ec742e12175c685a97bf9300b6b49
+79 -12
View File
@@ -64,6 +64,54 @@ async function findSimulationFixture(ctx) {
}) })
} }
function godsReadyChannel(config) {
return(config.maestro?.lifecycle?.godsReadyChannel
?? config.gps?.lifecycle?.godsReadyChannel
?? 'arena:gods:ready')
}
async function publishFakeReadyToStart(ctx, { sender, simulationId, agentIds }) {
const { arenaCnx } = ctx
const payload = {
success: true,
simulationId,
err: null,
}
if(agentIds) payload.agentIds = agentIds
await arenaCnx.redisPublish(godsReadyChannel(ctx.config), {
eventType: 'readyToStart',
sender,
payload,
})
}
async function publishFakeReadies(ctx, simulationId, agentIds) {
const { argv, config, log } = ctx
if(argv.fakeGpsReady) {
const senderId = config.gps?.senderId ?? 'gps'
log('action', `Faking GPS readyToStart (sender=${senderId})...`)
await publishFakeReadyToStart(ctx, {
sender: senderId,
simulationId,
agentIds,
})
log('success', 'Published fake GPS readyToStart')
}
if(argv.fakeAgentsReady) {
log('action', `Faking readyToStart for ${agentIds.length} agent(s)...`)
for(const agentId of agentIds) {
await publishFakeReadyToStart(ctx, {
sender: agentId,
simulationId,
})
}
log('success', `Published fake readyToStart for ${agentIds.length} agent(s)`)
}
}
function waitForLifecycleEvent(ctx, eventType, timeoutMs) { function waitForLifecycleEvent(ctx, eventType, timeoutMs) {
return(new Promise((resolve, reject) => { return(new Promise((resolve, reject) => {
const lifecyclePattern = ctx.config.maestro.lifecycle.arenaChannel const lifecyclePattern = ctx.config.maestro.lifecycle.arenaChannel
@@ -99,6 +147,16 @@ export function configureYargs(yargsBuilder) {
default: 15000, default: 15000,
type: 'number', type: 'number',
}, },
fakeAgentsReady: {
describe: 'After onYourMarks, publish fake readyToStart for each seeded agent',
type: 'boolean',
default: false,
},
fakeGpsReady: {
describe: 'After onYourMarks, publish fake GPS readyToStart',
type: 'boolean',
default: false,
},
})) }))
} }
@@ -163,24 +221,29 @@ export async function run(ctx) {
log('success', `Received onYourMarks for simulationId=${payload.simulationId}`) log('success', `Received onYourMarks for simulationId=${payload.simulationId}`)
log('action', 'Reading arena store...') const expectedIds = [...expectedById.keys()]
const indexIds = await arenaCnx.redisSmembers(arenaStorage.agentsIndexKey) if(argv.fakeAgentsReady || argv.fakeGpsReady) {
const expectedIds = [...expectedById.keys()].sort() await publishFakeReadies(ctx, payload.simulationId, expectedIds)
const actualIds = [...indexIds].sort()
if(actualIds.length !== expectedIds.length) {
throw(new Error(`Agent index count mismatch: expected ${expectedIds.length}, got ${actualIds.length}`))
} }
for(let i = 0; i < expectedIds.length; i++) { log('action', 'Reading arena store...')
if(actualIds[i] !== expectedIds[i]) { const indexIds = await arenaCnx.redisSmembers(arenaStorage.agentsIndexKey)
throw(new Error(`Agent index mismatch at ${i}: expected ${expectedIds[i]}, got ${actualIds[i]}`)) const sortedExpectedIds = [...expectedIds].sort()
const actualIds = [...indexIds].sort()
if(actualIds.length !== sortedExpectedIds.length) {
throw(new Error(`Agent index count mismatch: expected ${sortedExpectedIds.length}, got ${actualIds.length}`))
}
for(let i = 0; i < sortedExpectedIds.length; i++) {
if(actualIds[i] !== sortedExpectedIds[i]) {
throw(new Error(`Agent index mismatch at ${i}: expected ${sortedExpectedIds[i]}, got ${actualIds[i]}`))
} }
} }
log('success', `Arena agents index contains ${actualIds.length} agent(s)`) log('success', `Arena agents index contains ${actualIds.length} agent(s)`)
for(const agentId of expectedIds) { for(const agentId of sortedExpectedIds) {
const key = agentHashKey(arenaStorage.agentHashKey, agentId) const key = agentHashKey(arenaStorage.agentHashKey, agentId)
const hash = await arenaCnx.redisHgetall(key) const hash = await arenaCnx.redisHgetall(key)
const expected = expectedById.get(agentId) const expected = expectedById.get(agentId)
@@ -218,5 +281,9 @@ export async function run(ctx) {
log('success', `Agent ${agentId}: position, vector, and store values match MySQL`) log('success', `Agent ${agentId}: position, vector, and store values match MySQL`)
} }
log('success', 'Arena store seeded correctly from MySQL (Maestro will wait for agent + primordial daemon readyToStart until timeout)') if(argv.fakeAgentsReady || argv.fakeGpsReady) {
log('success', 'Arena store seeded correctly from MySQL; fake prepare acks published')
} else {
log('success', 'Arena store seeded correctly from MySQL (Maestro will wait for agent + primordial daemon readyToStart until timeout)')
}
} }