Capture current mission, AI, map and administration development
Include the current addon, terrain integrations, tools, regression tests and recorded development evidence.
This commit is contained in:
@@ -0,0 +1,89 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Read-only Linux Reforger memory sampler; one bounded JSONL file, no game RPCs.
|
||||
|
||||
Run on the server host: python3 sample_server_memory.py --output /tmp/escape-memory.jsonl
|
||||
Records all Reforger processes so a host OOM can be distinguished from a container
|
||||
limit. Stops after 24 hours by default. Never reads command arguments or secrets.
|
||||
"""
|
||||
import argparse
|
||||
import datetime
|
||||
import json
|
||||
from pathlib import Path
|
||||
import time
|
||||
|
||||
|
||||
def fields(text):
|
||||
result = {}
|
||||
for line in text.splitlines():
|
||||
key, sep, value = line.partition(':')
|
||||
if sep and value.split() and value.split()[0].isdigit():
|
||||
result[key] = int(value.split()[0])
|
||||
return result
|
||||
|
||||
|
||||
def snapshot(proc=Path('/proc'), cgroups=Path('/sys/fs/cgroup')):
|
||||
memory = fields((proc / 'meminfo').read_text())
|
||||
result = {'utc': datetime.datetime.now(datetime.timezone.utc).isoformat(),
|
||||
'host_kb': {k: memory.get(k) for k in ('MemTotal', 'MemAvailable', 'SwapTotal', 'SwapFree')},
|
||||
'processes': []}
|
||||
for directory in proc.iterdir():
|
||||
if not directory.name.isdigit():
|
||||
continue
|
||||
try:
|
||||
if 'ArmaReforgerServer' not in (directory / 'exe').readlink().name:
|
||||
continue
|
||||
status = fields((directory / 'status').read_text())
|
||||
# PID + kernel start tick distinguish process restarts/PID reuse.
|
||||
start = (directory / 'stat').read_text().rsplit(')', 1)[1].split()[19]
|
||||
row = {'pid': int(directory.name), 'start_ticks': int(start),
|
||||
'kb': {k: status.get(k) for k in ('VmRSS', 'VmHWM', 'VmSize', 'RssAnon', 'RssFile', 'VmSwap')}}
|
||||
for line in (directory / 'cgroup').read_text().splitlines():
|
||||
if not line.startswith('0::'):
|
||||
continue
|
||||
group = cgroups / line[3:].lstrip('/')
|
||||
row['cgroup'] = line[3:]
|
||||
for name in ('memory.current', 'memory.max', 'memory.peak', 'memory.events', 'memory.stat'):
|
||||
path = group / name
|
||||
if not path.exists():
|
||||
continue
|
||||
text = path.read_text().strip()
|
||||
if name in ('memory.events', 'memory.stat'):
|
||||
stats = dict(part.split() for part in text.splitlines())
|
||||
if name == 'memory.stat':
|
||||
stats = {k: v for k, v in stats.items() if k in ('anon', 'file', 'kernel', 'sock')}
|
||||
row[name] = {k: int(v) for k, v in stats.items()}
|
||||
else:
|
||||
row[name] = int(text) if text.isdigit() else text
|
||||
result['processes'].append(row)
|
||||
except (OSError, ValueError, IndexError):
|
||||
# Processes may exit at any point while /proc is being read.
|
||||
continue
|
||||
return result
|
||||
|
||||
|
||||
def append(path, record, limit=2 * 1024 * 1024):
|
||||
if path.exists() and path.stat().st_size >= limit:
|
||||
path.replace(path.with_name(path.name + '.1'))
|
||||
with path.open('a') as stream:
|
||||
stream.write(json.dumps(record, separators=(',', ':')) + '\n')
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument('--output', type=Path, required=True)
|
||||
parser.add_argument('--interval', type=int, default=60)
|
||||
parser.add_argument('--duration', type=int, default=86400)
|
||||
args = parser.parse_args()
|
||||
if args.interval < 30 or not 1 <= args.duration <= 172800:
|
||||
parser.error('interval must be >=30 seconds; duration must be 1..172800 seconds')
|
||||
deadline = time.monotonic() + args.duration
|
||||
while True:
|
||||
append(args.output, snapshot())
|
||||
remaining = deadline - time.monotonic()
|
||||
if remaining <= 0:
|
||||
break
|
||||
time.sleep(min(args.interval, remaining))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
Reference in New Issue
Block a user