797 lines
23 KiB
Python
797 lines
23 KiB
Python
"""
|
|
Fixture Helpers for E2E Tests
|
|
|
|
Provides helper functions for creating test resources like packs,
|
|
triggers, actions, rules, etc.
|
|
"""
|
|
|
|
import time
|
|
import uuid
|
|
from datetime import datetime, timedelta
|
|
from typing import Any, Dict, Optional
|
|
|
|
from .client import AttuneClient
|
|
|
|
|
|
def unique_ref(prefix: str = "test") -> str:
|
|
"""
|
|
Generate unique reference string
|
|
|
|
Args:
|
|
prefix: Prefix for reference
|
|
|
|
Returns:
|
|
Unique reference string (e.g., "test_abc123")
|
|
"""
|
|
timestamp = int(time.time() * 1000)
|
|
random_part = str(uuid.uuid4())[:8]
|
|
return f"{prefix}_{timestamp}_{random_part}"
|
|
|
|
|
|
def timestamp_now() -> str:
|
|
"""Get current timestamp in ISO format"""
|
|
return datetime.utcnow().isoformat() + "Z"
|
|
|
|
|
|
def timestamp_future(seconds: int) -> str:
|
|
"""
|
|
Get future timestamp in ISO format
|
|
|
|
Args:
|
|
seconds: Seconds in the future
|
|
|
|
Returns:
|
|
ISO timestamp string
|
|
"""
|
|
future = datetime.utcnow() + timedelta(seconds=seconds)
|
|
return future.isoformat() + "Z"
|
|
|
|
|
|
# ============================================================================
|
|
# Pack Creation
|
|
# ============================================================================
|
|
|
|
|
|
def create_test_pack(
|
|
client: AttuneClient,
|
|
pack_ref: Optional[str] = None,
|
|
pack_dir: str = "tests/fixtures/packs/test_pack",
|
|
) -> Dict[str, Any]:
|
|
"""
|
|
Create or get test pack
|
|
|
|
Args:
|
|
client: AttuneClient instance
|
|
pack_ref: Optional pack reference (generated if not provided)
|
|
pack_dir: Path to pack directory
|
|
|
|
Returns:
|
|
Pack data
|
|
"""
|
|
# Extract pack_ref from pack_dir if not provided
|
|
if not pack_ref:
|
|
# Default pack ref is "test_pack" for the standard test pack
|
|
pack_ref = "test_pack"
|
|
|
|
# Always try to get existing pack first
|
|
existing_pack = client.get_pack_by_ref(pack_ref)
|
|
if existing_pack:
|
|
return existing_pack
|
|
|
|
# Register new pack if it doesn't exist
|
|
return client.register_pack(pack_dir, force=True)
|
|
|
|
|
|
def ensure_core_pack(client: AttuneClient) -> Dict[str, Any]:
|
|
"""
|
|
Ensure core pack exists, register it if needed
|
|
|
|
Args:
|
|
client: AttuneClient instance
|
|
|
|
Returns:
|
|
Core pack data
|
|
"""
|
|
# Try to get existing core pack
|
|
try:
|
|
core_pack = client.get_pack_by_ref("core")
|
|
if core_pack:
|
|
return core_pack
|
|
except Exception:
|
|
pass
|
|
|
|
# Core pack doesn't exist, register it
|
|
try:
|
|
return client.register_pack("packs/core", force=True, skip_tests=True)
|
|
except Exception as e:
|
|
# If registration fails, try without skip_tests
|
|
try:
|
|
return client.register_pack("packs/core", force=True)
|
|
except Exception as inner_e:
|
|
raise Exception(f"Failed to register core pack: {inner_e}") from e
|
|
|
|
|
|
# ============================================================================
|
|
# Trigger Creation
|
|
# ============================================================================
|
|
|
|
|
|
def create_interval_timer(
|
|
client: AttuneClient,
|
|
interval_seconds: int = 5,
|
|
name: Optional[str] = None,
|
|
pack_ref: str = "test.test_pack",
|
|
) -> Dict[str, Any]:
|
|
"""
|
|
Create interval timer sensor for timer to actually fire
|
|
|
|
Args:
|
|
client: AttuneClient instance
|
|
interval_seconds: Interval in seconds
|
|
name: Sensor name (generated if not provided)
|
|
pack_ref: Pack reference
|
|
|
|
Returns:
|
|
Dict with trigger and sensor info
|
|
"""
|
|
sensor_name = name or f"interval_{interval_seconds}s_{unique_ref()}"
|
|
|
|
# Ensure core pack exists
|
|
ensure_core_pack(client)
|
|
|
|
# Get or ensure core.intervaltimer trigger exists
|
|
triggers = client.list_triggers()
|
|
core_trigger = None
|
|
for t in triggers:
|
|
if t.get("ref") == "core.intervaltimer":
|
|
core_trigger = t
|
|
break
|
|
|
|
if not core_trigger:
|
|
# Create core.intervaltimer trigger if it doesn't exist
|
|
core_trigger = client.create_trigger(
|
|
ref="core.intervaltimer",
|
|
label="Interval Timer",
|
|
pack_ref="core",
|
|
description="Fires at regular intervals",
|
|
)
|
|
|
|
# Create sensor to make timer actually fire events
|
|
sensor_ref = f"{pack_ref}.{sensor_name}_sensor"
|
|
sensor_config = {"unit": "seconds", "interval": interval_seconds}
|
|
|
|
sensor = client.create_sensor(
|
|
ref=sensor_ref,
|
|
trigger_id=core_trigger["id"],
|
|
trigger_ref=core_trigger["ref"],
|
|
label=f"{sensor_name} Sensor",
|
|
description=f"Sensor for interval timer (every {interval_seconds}s)",
|
|
entrypoint="internal://timer",
|
|
runtime_ref="core.sensor.python3",
|
|
pack_ref=pack_ref,
|
|
enabled=True,
|
|
config=sensor_config,
|
|
)
|
|
|
|
# Restart sensor service to load the new sensor
|
|
restart_sensor_service(wait_seconds=2)
|
|
|
|
# Return dict with both trigger and sensor info
|
|
return {
|
|
"id": core_trigger["id"],
|
|
"ref": core_trigger["ref"],
|
|
"label": sensor["label"],
|
|
"trigger": core_trigger,
|
|
"sensor": sensor,
|
|
"sensor_id": sensor["id"],
|
|
}
|
|
|
|
|
|
def create_date_timer(
|
|
client: AttuneClient,
|
|
fire_at: Optional[str] = None,
|
|
seconds_from_now: int = 5,
|
|
name: Optional[str] = None,
|
|
pack_ref: str = "test.test_pack",
|
|
) -> Dict[str, Any]:
|
|
"""
|
|
Create date timer sensor for timer to actually fire
|
|
|
|
Args:
|
|
client: AttuneClient instance
|
|
fire_at: ISO timestamp when to fire (optional)
|
|
seconds_from_now: Seconds from now to fire (used if fire_at not provided)
|
|
name: Sensor name (generated if not provided)
|
|
pack_ref: Pack reference
|
|
|
|
Returns:
|
|
Dict with trigger and sensor info
|
|
"""
|
|
if not fire_at:
|
|
fire_at = timestamp_future(seconds_from_now)
|
|
|
|
sensor_name = name or f"date_{unique_ref()}"
|
|
|
|
# Ensure core pack exists
|
|
ensure_core_pack(client)
|
|
|
|
# Get or ensure core.datetimetimer trigger exists
|
|
triggers = client.list_triggers()
|
|
core_trigger = None
|
|
for t in triggers:
|
|
if t.get("ref") == "core.datetimetimer":
|
|
core_trigger = t
|
|
break
|
|
|
|
if not core_trigger:
|
|
# Create core.datetimetimer trigger if it doesn't exist
|
|
core_trigger = client.create_trigger(
|
|
ref="core.datetimetimer",
|
|
label="Date/Time Timer",
|
|
pack_ref="core",
|
|
description="Fires at a specific date/time",
|
|
)
|
|
|
|
# Create sensor to make timer actually fire events
|
|
sensor_ref = f"{pack_ref}.{sensor_name}_sensor"
|
|
sensor_config = {"date": fire_at}
|
|
|
|
sensor = client.create_sensor(
|
|
ref=sensor_ref,
|
|
trigger_id=core_trigger["id"],
|
|
trigger_ref=core_trigger["ref"],
|
|
label=f"{sensor_name} Sensor",
|
|
description=f"Sensor for date timer (fires at {fire_at})",
|
|
entrypoint="internal://timer",
|
|
runtime_ref="core.sensor.python3",
|
|
pack_ref=pack_ref,
|
|
enabled=True,
|
|
config=sensor_config,
|
|
)
|
|
|
|
# Restart sensor service to load the new sensor
|
|
restart_sensor_service(wait_seconds=2)
|
|
|
|
# Return dict with both trigger and sensor info
|
|
return {
|
|
"id": core_trigger["id"],
|
|
"ref": core_trigger["ref"],
|
|
"label": sensor["label"],
|
|
"trigger": core_trigger,
|
|
"sensor": sensor,
|
|
"sensor_id": sensor["id"],
|
|
"fire_at": fire_at,
|
|
}
|
|
|
|
|
|
def create_cron_timer(
|
|
client: AttuneClient,
|
|
cron_expression: str = "*/5 * * * * *",
|
|
name: Optional[str] = None,
|
|
pack_ref: str = "test.test_pack",
|
|
timezone: str = "UTC",
|
|
) -> Dict[str, Any]:
|
|
"""
|
|
Create cron timer sensor for timer to actually fire
|
|
|
|
Args:
|
|
client: AttuneClient instance
|
|
cron_expression: Cron expression (6-field with seconds)
|
|
name: Sensor name (generated if not provided)
|
|
pack_ref: Pack reference
|
|
timezone: Timezone for cron evaluation
|
|
|
|
Returns:
|
|
Dict with trigger and sensor info
|
|
"""
|
|
sensor_name = name or f"cron_{unique_ref()}"
|
|
|
|
# Ensure core pack exists
|
|
ensure_core_pack(client)
|
|
|
|
# Get or ensure core.crontimer trigger exists
|
|
triggers = client.list_triggers()
|
|
core_trigger = None
|
|
for t in triggers:
|
|
if t.get("ref") == "core.crontimer":
|
|
core_trigger = t
|
|
break
|
|
|
|
if not core_trigger:
|
|
# Create core.crontimer trigger if it doesn't exist
|
|
core_trigger = client.create_trigger(
|
|
ref="core.crontimer",
|
|
label="Cron Timer",
|
|
pack_ref="core",
|
|
description="Fires based on cron schedule",
|
|
)
|
|
|
|
# Create sensor to make timer actually fire events
|
|
sensor_ref = f"{pack_ref}.{sensor_name}_sensor"
|
|
sensor_config = {"cron": cron_expression, "timezone": timezone}
|
|
|
|
sensor = client.create_sensor(
|
|
ref=sensor_ref,
|
|
trigger_id=core_trigger["id"],
|
|
trigger_ref=core_trigger["ref"],
|
|
label=f"{sensor_name} Sensor",
|
|
description=f"Sensor for cron timer ({cron_expression})",
|
|
entrypoint="internal://timer",
|
|
runtime_ref="core.sensor.python3",
|
|
pack_ref=pack_ref,
|
|
enabled=True,
|
|
config=sensor_config,
|
|
)
|
|
|
|
# Restart sensor service to load the new sensor
|
|
restart_sensor_service(wait_seconds=2)
|
|
|
|
# Return dict with both trigger and sensor info
|
|
return {
|
|
"id": core_trigger["id"],
|
|
"ref": core_trigger["ref"],
|
|
"label": sensor["label"],
|
|
"trigger": core_trigger,
|
|
"sensor": sensor,
|
|
"sensor_id": sensor["id"],
|
|
"cron_expression": cron_expression,
|
|
"timezone": timezone,
|
|
}
|
|
|
|
|
|
def create_webhook_trigger(
|
|
client: AttuneClient,
|
|
name: Optional[str] = None,
|
|
pack_ref: str = "test.test_pack",
|
|
) -> Dict[str, Any]:
|
|
"""
|
|
Create webhook trigger
|
|
|
|
Args:
|
|
client: AttuneClient instance
|
|
name: Trigger name (generated if not provided)
|
|
pack_ref: Pack reference
|
|
|
|
Returns:
|
|
Created trigger data
|
|
"""
|
|
trigger_name = name or f"webhook_{unique_ref()}"
|
|
|
|
return client.create_trigger(
|
|
pack_ref=pack_ref,
|
|
name=trigger_name,
|
|
trigger_type="webhook",
|
|
parameters={},
|
|
)
|
|
|
|
|
|
# ============================================================================
|
|
# Action Creation
|
|
# ============================================================================
|
|
|
|
|
|
def create_simple_action(
|
|
client: AttuneClient,
|
|
name: Optional[str] = None,
|
|
pack_ref: str = "test.test_pack",
|
|
runner_type: str = "python3",
|
|
entrypoint: str = "actions/echo.py",
|
|
param_schema: Optional[Dict[str, Any]] = None,
|
|
) -> Dict[str, Any]:
|
|
"""
|
|
Create simple action
|
|
|
|
Args:
|
|
client: AttuneClient instance
|
|
name: Action name (generated if not provided)
|
|
pack_ref: Pack reference
|
|
runner_type: Runner type
|
|
entrypoint: Entry point path
|
|
param_schema: JSON Schema for parameters
|
|
|
|
Returns:
|
|
Created action data
|
|
"""
|
|
action_name = name or f"action_{unique_ref()}"
|
|
|
|
if param_schema is None:
|
|
param_schema = {
|
|
"type": "object",
|
|
"properties": {"message": {"type": "string", "default": "Hello, World!"}},
|
|
}
|
|
|
|
return client.create_action(
|
|
pack_ref=pack_ref,
|
|
name=action_name,
|
|
runner_type=runner_type,
|
|
entrypoint=entrypoint,
|
|
param_schema=param_schema,
|
|
)
|
|
|
|
|
|
def create_echo_action(
|
|
client: AttuneClient,
|
|
name: Optional[str] = None,
|
|
pack_ref: str = "test.test_pack",
|
|
) -> Dict[str, Any]:
|
|
"""
|
|
Create echo action (simple action that echoes input)
|
|
|
|
Args:
|
|
client: AttuneClient instance
|
|
name: Action name (generated if not provided)
|
|
pack_ref: Pack reference
|
|
|
|
Returns:
|
|
Created action data
|
|
"""
|
|
return create_simple_action(
|
|
client=client,
|
|
name=name or f"echo_{unique_ref()}",
|
|
pack_ref=pack_ref,
|
|
runner_type="python3",
|
|
entrypoint="actions/echo.py",
|
|
param_schema={
|
|
"type": "object",
|
|
"properties": {
|
|
"message": {"type": "string", "default": "echo"},
|
|
"count": {"type": "integer", "default": 1},
|
|
},
|
|
},
|
|
)
|
|
|
|
|
|
def create_failing_action(
|
|
client: AttuneClient,
|
|
name: Optional[str] = None,
|
|
pack_ref: str = "test.test_pack",
|
|
exit_code: int = 1,
|
|
) -> Dict[str, Any]:
|
|
"""
|
|
Create action that always fails
|
|
|
|
Args:
|
|
client: AttuneClient instance
|
|
name: Action name (generated if not provided)
|
|
pack_ref: Pack reference
|
|
exit_code: Exit code to return
|
|
|
|
Returns:
|
|
Created action data
|
|
"""
|
|
action_name = name or f"failing_{unique_ref()}"
|
|
|
|
return client.create_action(
|
|
pack_ref=pack_ref,
|
|
name=action_name,
|
|
runner_type="python3",
|
|
entrypoint="actions/fail.py",
|
|
param_schema={
|
|
"type": "object",
|
|
"properties": {"exit_code": {"type": "integer", "default": exit_code}},
|
|
},
|
|
)
|
|
|
|
|
|
def create_sleep_action(
|
|
client: AttuneClient,
|
|
name: Optional[str] = None,
|
|
pack_ref: str = "test.test_pack",
|
|
default_duration: int = 5,
|
|
) -> Dict[str, Any]:
|
|
"""
|
|
Create action that sleeps for specified duration
|
|
|
|
Args:
|
|
client: AttuneClient instance
|
|
name: Action name (generated if not provided)
|
|
pack_ref: Pack reference
|
|
default_duration: Default sleep duration in seconds
|
|
|
|
Returns:
|
|
Created action data
|
|
"""
|
|
action_name = name or f"sleep_{unique_ref()}"
|
|
|
|
return client.create_action(
|
|
pack_ref=pack_ref,
|
|
name=action_name,
|
|
runner_type="python3",
|
|
entrypoint="actions/sleep.py",
|
|
param_schema={
|
|
"type": "object",
|
|
"properties": {
|
|
"duration": {"type": "integer", "default": default_duration}
|
|
},
|
|
},
|
|
)
|
|
|
|
|
|
# ============================================================================
|
|
# Rule Creation
|
|
# ============================================================================
|
|
|
|
|
|
def create_rule(
|
|
client: AttuneClient,
|
|
trigger_id: int,
|
|
action_ref: str,
|
|
name: Optional[str] = None,
|
|
pack_ref: str = "test.test_pack",
|
|
enabled: bool = True,
|
|
criteria: Optional[str] = None,
|
|
action_parameters: Optional[Dict[str, Any]] = None,
|
|
) -> Dict[str, Any]:
|
|
"""
|
|
Create rule
|
|
|
|
Args:
|
|
client: AttuneClient instance
|
|
trigger_id: Trigger ID to monitor
|
|
action_ref: Action reference to execute
|
|
name: Rule name (generated if not provided)
|
|
pack_ref: Pack reference
|
|
enabled: Whether rule is enabled
|
|
criteria: Optional Jinja2 criteria expression
|
|
action_parameters: Parameters to pass to action
|
|
|
|
Returns:
|
|
Created rule data
|
|
"""
|
|
rule_name = name or f"rule_{unique_ref()}"
|
|
|
|
return client.create_rule(
|
|
name=rule_name,
|
|
pack_ref=pack_ref,
|
|
trigger_id=trigger_id,
|
|
action_ref=action_ref,
|
|
enabled=enabled,
|
|
criteria=criteria,
|
|
action_parameters=action_parameters or {},
|
|
)
|
|
|
|
|
|
def create_timer_automation(
|
|
client: AttuneClient,
|
|
interval_seconds: int = 5,
|
|
pack_ref: str = "test.test_pack",
|
|
action_parameters: Optional[Dict[str, Any]] = None,
|
|
) -> Dict[str, Any]:
|
|
"""
|
|
Create complete timer automation (trigger + action + rule)
|
|
|
|
Args:
|
|
client: AttuneClient instance
|
|
interval_seconds: Timer interval in seconds
|
|
pack_ref: Pack reference
|
|
action_parameters: Parameters to pass to action
|
|
|
|
Returns:
|
|
Dictionary with trigger, action, and rule data
|
|
"""
|
|
# Create timer trigger
|
|
trigger = create_interval_timer(
|
|
client=client, interval_seconds=interval_seconds, pack_ref=pack_ref
|
|
)
|
|
|
|
# Create echo action
|
|
action = create_echo_action(client=client, pack_ref=pack_ref)
|
|
|
|
# Create rule linking them
|
|
rule = create_rule(
|
|
client=client,
|
|
trigger_id=trigger["id"],
|
|
action_ref=action["ref"],
|
|
pack_ref=pack_ref,
|
|
action_parameters=action_parameters,
|
|
)
|
|
|
|
return {"trigger": trigger, "action": action, "rule": rule}
|
|
|
|
|
|
def create_webhook_automation(
|
|
client: AttuneClient,
|
|
pack_ref: str = "test.test_pack",
|
|
action_parameters: Optional[Dict[str, Any]] = None,
|
|
criteria: Optional[str] = None,
|
|
) -> Dict[str, Any]:
|
|
"""
|
|
Create complete webhook automation (trigger + action + rule)
|
|
|
|
Args:
|
|
client: AttuneClient instance
|
|
pack_ref: Pack reference
|
|
action_parameters: Parameters to pass to action
|
|
criteria: Optional rule criteria
|
|
|
|
Returns:
|
|
Dictionary with trigger, action, and rule data
|
|
"""
|
|
# Create webhook trigger
|
|
trigger = create_webhook_trigger(client=client, pack_ref=pack_ref)
|
|
|
|
# Create echo action
|
|
action = create_echo_action(client=client, pack_ref=pack_ref)
|
|
|
|
# Create rule linking them
|
|
rule = create_rule(
|
|
client=client,
|
|
trigger_id=trigger["id"],
|
|
action_ref=action["ref"],
|
|
pack_ref=pack_ref,
|
|
action_parameters=action_parameters,
|
|
criteria=criteria,
|
|
)
|
|
|
|
return {"trigger": trigger, "action": action, "rule": rule}
|
|
|
|
|
|
# ============================================================================
|
|
# Service Management
|
|
# ============================================================================
|
|
|
|
|
|
def restart_sensor_service(wait_seconds: int = 3) -> bool:
|
|
"""
|
|
Restart sensor service to reload sensors
|
|
|
|
This is needed after creating new sensors so they are loaded
|
|
and can start generating events.
|
|
|
|
Works with E2E services managed by start-e2e-services.sh script.
|
|
|
|
Args:
|
|
wait_seconds: Seconds to wait after restart for service to be ready
|
|
|
|
Returns:
|
|
True if restart successful, False otherwise
|
|
"""
|
|
import os
|
|
import signal
|
|
import subprocess
|
|
|
|
try:
|
|
# Check if we're running in docker-compose environment
|
|
if os.path.exists("/.dockerenv") or os.getenv("DOCKER_ENV"):
|
|
# Try to restart via docker-compose
|
|
result = subprocess.run(
|
|
["docker-compose", "restart", "sensor"],
|
|
capture_output=True,
|
|
text=True,
|
|
timeout=30,
|
|
)
|
|
if result.returncode == 0:
|
|
time.sleep(wait_seconds)
|
|
return True
|
|
|
|
# For E2E services, use PID file to restart the sensor service
|
|
# Calculate paths relative to tests directory
|
|
helpers_dir = os.path.dirname(os.path.abspath(__file__)) # tests/helpers
|
|
tests_dir = os.path.dirname(helpers_dir) # tests
|
|
pid_dir = os.path.join(tests_dir, "pids")
|
|
pid_file = os.path.join(pid_dir, "sensor.pid")
|
|
log_dir = os.path.join(tests_dir, "logs")
|
|
log_file = os.path.join(log_dir, "sensor.log")
|
|
|
|
if os.path.exists(pid_file):
|
|
# Read PID and stop the service
|
|
with open(pid_file, "r") as f:
|
|
pid_str = f.read().strip()
|
|
if not pid_str:
|
|
print(f"Warning: PID file {pid_file} is empty")
|
|
os.remove(pid_file)
|
|
return False
|
|
pid = int(pid_str)
|
|
|
|
# Stop the existing process
|
|
stopped = False
|
|
try:
|
|
# Send SIGTERM for graceful shutdown
|
|
os.kill(pid, signal.SIGTERM)
|
|
|
|
# Wait up to 5 seconds for graceful shutdown
|
|
for _ in range(10):
|
|
try:
|
|
os.kill(pid, 0) # Check if process exists
|
|
time.sleep(0.5)
|
|
except ProcessLookupError:
|
|
stopped = True
|
|
break
|
|
|
|
# Force kill if still running
|
|
if not stopped:
|
|
try:
|
|
os.kill(pid, signal.SIGKILL)
|
|
time.sleep(1)
|
|
stopped = True
|
|
except ProcessLookupError:
|
|
stopped = True
|
|
except ProcessLookupError:
|
|
stopped = True # Process doesn't exist
|
|
|
|
# Remove PID file
|
|
if os.path.exists(pid_file):
|
|
os.remove(pid_file)
|
|
|
|
if not stopped:
|
|
print(f"Warning: Failed to stop sensor process {pid}")
|
|
|
|
# Restart the sensor service
|
|
# Get project root
|
|
project_root = os.path.dirname(tests_dir) # project root
|
|
binary_path = os.path.join(project_root, "target", "debug", "attune-sensor")
|
|
config_file = os.path.join(project_root, "config.e2e.yaml")
|
|
|
|
# Verify binary exists
|
|
if not os.path.exists(binary_path):
|
|
print(f"Error: Sensor binary not found at {binary_path}")
|
|
return False
|
|
|
|
env = os.environ.copy()
|
|
env["ATTUNE__ENVIRONMENT"] = "e2e"
|
|
env["ATTUNE_CONFIG"] = config_file
|
|
|
|
with open(log_file, "a") as log:
|
|
log.write(f"\n\n=== Sensor service restarted at {time.time()} ===\n\n")
|
|
log.flush()
|
|
process = subprocess.Popen(
|
|
[binary_path],
|
|
stdout=log,
|
|
stderr=subprocess.STDOUT,
|
|
env=env,
|
|
start_new_session=True,
|
|
cwd=project_root, # Run from project root
|
|
)
|
|
|
|
# Write new PID file
|
|
with open(pid_file, "w") as f:
|
|
f.write(str(process.pid))
|
|
f.flush()
|
|
|
|
# Wait for service to initialize
|
|
time.sleep(wait_seconds)
|
|
|
|
# Verify process is still running
|
|
try:
|
|
os.kill(process.pid, 0)
|
|
|
|
# Additional verification: check if log file is being written to
|
|
if os.path.exists(log_file):
|
|
# Get file size before and after a short wait
|
|
size_before = os.path.getsize(log_file)
|
|
time.sleep(1)
|
|
size_after = os.path.getsize(log_file)
|
|
|
|
if size_after > size_before:
|
|
print(
|
|
f"✓ Sensor service restarted successfully (PID: {process.pid})"
|
|
)
|
|
return True
|
|
else:
|
|
print(
|
|
f"Warning: Sensor service started but not logging (PID: {process.pid})"
|
|
)
|
|
return True # Still return True as process is running
|
|
else:
|
|
print(f"✓ Sensor service restarted (PID: {process.pid})")
|
|
return True
|
|
except ProcessLookupError:
|
|
print(f"✗ Sensor service failed to start (process died immediately)")
|
|
if os.path.exists(pid_file):
|
|
os.remove(pid_file)
|
|
return False
|
|
else:
|
|
print(
|
|
f"Warning: No PID file found at {pid_file}. Sensor service may not be running."
|
|
)
|
|
time.sleep(wait_seconds)
|
|
return False
|
|
|
|
except Exception as e:
|
|
print(f"Warning: Error restarting sensor service: {e}")
|
|
import traceback
|
|
|
|
traceback.print_exc()
|
|
time.sleep(wait_seconds)
|
|
return False
|