Skip to content
Draft
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
77 changes: 76 additions & 1 deletion RMS/Formats/ObservationSummary.py
Original file line number Diff line number Diff line change
Expand Up @@ -996,7 +996,8 @@ def retrieveObservationData(conn, config, night_directory=None, ordering=None):
'capture_duration_from_ephemeris', 'total_expected_fits_ephemeris', 'fits_file_shortfall_ephemeris',
'fits_file_shortfall_as_time_ephemeris',
'detections_after_ml',
'media_backend','protocol_in_use','jitter_quality','dropped_frame_rate']
'media_backend','protocol_in_use','jitter_quality','dropped_frame_rate',
'traceback_count']

# Use this print call to check the ordering
# print("Ordering {}".format(ordering))
Expand Down Expand Up @@ -1202,6 +1203,76 @@ def startObservationSummaryReport(config, duration, force_delete=False):

return "Opening a new observations summary"

def countTracebacksInLogs(config):
"""Count the number of tracebacks in log files from the current session.

Scans all log files in the log directory that were modified after the session's
start_time (from the observation database) for lines containing 'Traceback
(most recent call last)'.

Arguments:
config: [config] RMS configuration instance.

Return:
count: [int] Number of tracebacks found, or 0 if logs cannot be read.
"""

log_dir = os.path.join(config.data_dir, config.log_dir)

if not os.path.isdir(log_dir):
return 0

# Get the session start time from the observation database to filter log files
try:
conn = getObsDBConn(config)
if conn is None:
return 0
cursor = conn.cursor()
row = cursor.execute(
"SELECT Value FROM records WHERE Key = 'start_time' ORDER BY id DESC LIMIT 1"
).fetchone()
conn.close()
if row is None:
return 0
session_start_str = row[0]
# Parse the start time — handle both with and without fractional seconds
for fmt in ("%Y-%m-%d %H:%M:%S%z", "%Y-%m-%d %H:%M:%S"):
try:
session_start = datetime.datetime.strptime(session_start_str.replace("+00:00", ""), fmt)
break
except ValueError:
continue
else:
return 0
Comment on lines +1238 to +1246

Copilot AI Mar 20, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

countTracebacksInLogs claims to handle start times “with and without fractional seconds”, but the parsing formats don’t include %f. Also, session_start_str.replace("+00:00", "") guarantees the "%Y-%m-%d %H:%M:%S%z" parse attempt can never succeed, making that branch dead code. It would be more robust to parse the stored value with datetime.fromisoformat(...) (and strip tzinfo) or explicitly support both %Y-%m-%d %H:%M:%S.%f%z and %Y-%m-%d %H:%M:%S%z without removing the offset first.

Suggested change
# Parse the start time — handle both with and without fractional seconds
for fmt in ("%Y-%m-%d %H:%M:%S%z", "%Y-%m-%d %H:%M:%S"):
try:
session_start = datetime.datetime.strptime(session_start_str.replace("+00:00", ""), fmt)
break
except ValueError:
continue
else:
return 0
# Parse the start time — handle both with and without fractional seconds and optional timezone
try:
# Prefer ISO 8601 parsing when available (Python 3.7+)
session_dt = datetime.datetime.fromisoformat(session_start_str)
except AttributeError:
# Fallback for older Python versions without fromisoformat
session_dt = None
for fmt in (
"%Y-%m-%d %H:%M:%S.%f%z",
"%Y-%m-%d %H:%M:%S%z",
"%Y-%m-%d %H:%M:%S.%f",
"%Y-%m-%d %H:%M:%S",
):
try:
session_dt = datetime.datetime.strptime(session_start_str, fmt)
break
except ValueError:
continue
if session_dt is None:
return 0
except ValueError:
# Malformed timestamp
return 0
# Normalize to naive UTC datetime for comparison with file modification times
if session_dt.tzinfo is not None:
try:
session_dt = session_dt.astimezone(datetime.timezone.utc).replace(tzinfo=None)
except AttributeError:
# datetime.timezone may not be available on very old Python;
# assume stored time is already in UTC and just strip tzinfo.
session_dt = session_dt.replace(tzinfo=None)
session_start = session_dt

Copilot uses AI. Check for mistakes.
except Exception:
return 0

# Find log files modified after the session start
traceback_count = 0
log_pattern = "log_{}_".format(config.stationID)

for filename in sorted(os.listdir(log_dir)):
if not filename.endswith(".log") or log_pattern not in filename:
continue

filepath = os.path.join(log_dir, filename)

# Only consider log files modified after the session started
file_mtime = UTCFromTimestamp.utcfromtimestamp(os.path.getmtime(filepath))
if file_mtime < session_start:
continue

try:
with open(filepath, 'r', errors='replace') as f:
for line in f:
if 'Traceback (most recent call last)' in line:
traceback_count += 1
Comment on lines +1265 to +1269

Copilot AI Mar 20, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This file still contains Python 2 compatibility branches, but open(..., errors='replace') is Python 3-only. On Python 2 this will raise TypeError and be swallowed by the except, causing traceback scanning to silently do nothing. If Python 2 support is still required here, use io.open(..., errors='replace') (or conditional handling) to keep the feature working across versions.

Copilot uses AI. Check for mistakes.
except Exception:
continue

return traceback_count


def finalizeObservationSummary(config, night_data_dir, platepar=None):

""" Enters the parameters known at the end of observation into the database.
Expand Down Expand Up @@ -1267,6 +1338,10 @@ def finalizeObservationSummary(config, night_data_dir, platepar=None):
addObsParam(obs_db_conn, "remote_branch", os.path.basename(remote_branch))
except:
addObsParam(obs_db_conn, "repository_lag_remote_days", "Not determined")

# Scan log files for tracebacks during this session
addObsParam(obs_db_conn, "traceback_count", countTracebacksInLogs(config))

Comment on lines +1341 to +1344

Copilot AI Mar 20, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The PR description says to add traceback_count to the observation summary when > 0, but the current implementation always writes the key (including when the count is 0). If you want the field omitted unless tracebacks exist, compute the count and only call addObsParam when it’s > 0.

Copilot uses AI. Check for mistakes.
obs_db_conn.close()

writeToFile(config, getRMSStyleFileName(night_data_dir, "observation_summary.txt"), night_data_dir)
Expand Down
44 changes: 42 additions & 2 deletions Scripts/RMS_Update.sh
Original file line number Diff line number Diff line change
Expand Up @@ -1547,8 +1547,48 @@ PY
print_status "info" "Backup files are available in: $RMSBACKUPDIR"
exit 1
fi

print_status "success" "Build completed successfully"

# Verify compiled extensions are actually importable.
# pip install -e can exit 0 even when C/C++ compilation fails silently.
print_status "info" "Verifying compiled extensions..."
local verify_output
verify_output=$(python << 'VERIFY_EOF'
import sys, importlib, re
# Parse extension module names from setup.py without importing it
# (importing setup.py would re-run the entire build).
with open("setup.py") as f:
src = f.read()
names = re.findall(r'Extension\(\s*["\x27]([\w.]+)["\x27]', src)
if not names:
print("WARN:no extensions parsed from setup.py")
sys.exit(0)
failed = []
for name in names:
try:
importlib.import_module(name)
except ImportError:
failed.append(name)
Comment on lines +1569 to +1570

Copilot AI Mar 20, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The verification helper only catches ImportError, but compiled extensions often fail with OSError/ModuleNotFoundError for missing shared libs or symbol errors. In those cases the script may exit without emitting a FAILED: line (and stderr isn’t captured), so the bash-side parsing can’t report what went wrong. Consider catching a broader exception (or printing the exception details) and capturing stderr (e.g., redirect 2>&1) so failures are actionable.

Suggested change
except ImportError:
failed.append(name)
except (ImportError, ModuleNotFoundError, OSError) as e:
# Record the module name along with basic error details so the caller can see why it failed.
failed.append(f"{name} ({e.__class__.__name__}: {e})")
except Exception as e:
# Catch any other unexpected exceptions to ensure we still emit a FAILED: line.
failed.append(f"{name} (Unexpected {e.__class__.__name__}: {e})")

Copilot uses AI. Check for mistakes.
if failed:
print("FAILED:" + ",".join(failed))
sys.exit(1)
print("OK:" + str(len(names)))
VERIFY_EOF
)

if [ $? -ne 0 ]; then
Comment on lines +1576 to +1578

Copilot AI Mar 20, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because this script runs with set -Eeuo pipefail (errexit), a non-zero exit from the python command inside verify_output=$(...) will cause the script to abort immediately (triggering the ERR trap) before the subsequent if [ $? -ne 0 ] block can print the list of failed modules. Wrap the command substitution in a conditional (e.g., if ! verify_output=$(python ...); then ... fi) or temporarily disable errexit around it so the intended error reporting executes.

Suggested change
)
if [ $? -ne 0 ]; then
) || verify_status=$?
if [ "${verify_status-0}" -ne 0 ]; then

Copilot uses AI. Check for mistakes.
# Extract the failed module names from the output
local failed_modules="${verify_output#FAILED:}"
print_status "error" "Build reported success but these compiled extensions failed to import:"
IFS=',' read -ra modules <<< "$failed_modules"
for mod in "${modules[@]}"; do
print_status "error" " - $mod"
done
print_status "error" "The C/C++ compiler may have failed silently during pip install."
print_status "info" "Backup files are available in: $RMSBACKUPDIR"
exit 1
fi

print_status "success" "Build completed successfully (all extensions verified)"

# Print the update report
print_update_report
Expand Down
Loading