1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
|
#!/usr/bin/env python3
"""newOS4 maintenance: drift, bundles, syntax, perms, data hygiene, live health.
Safe fixes only with --fix. Never touches nginx, services, sudo, or user content."""
import glob, json, os, re, subprocess, sys
from datetime import datetime, timezone
ROOT = '/var/www/webx11.duckdns.org/spaces/newos4'
WEB = 'https://webx11.duckdns.org/spaces/newos4'
FIX = '--fix' in sys.argv
DEEP = '--deep' in sys.argv
issues = []
def add(l, m): issues.append((l, m))
def run(cmd, **kw): return subprocess.run(cmd, capture_output=True, text=True, **kw)
def sh(cmd): return run(cmd, shell=True).stdout.strip()
def check_versions():
html = open(os.path.join(ROOT, 'main.html'), encoding='utf-8').read()
loader = open(os.path.join(ROOT, 'modules/core/module-loader.js'), encoding='utf-8').read()
m = re.search(r"const LATEST_VER\s*=\s*'([^']+)'", loader)
ver = m.group(1) if m else '?'
n = html.count(ver)
add('PASS' if n == 3 else 'FAIL', f'version {ver} x{n} in main.html (expect 3)')
def check_bundles():
paths = ('modules/core/core-bundle.js', 'modules/ext/critical-ext-bundle.js')
before = {p: sh(f'md5sum {p}') for p in paths}
if not FIX:
add('PASS', 'bundle check skipped (use --fix)'); return
r = run([sys.executable, 'scripts/rebuild-bundles.py'], cwd=ROOT)
after = {p: sh(f'md5sum {p}') for p in paths}
changed = [p for p in paths if before[p] != after[p]]
add('FAIL' if r.returncode else ('WARN' if changed else 'PASS'),
'bundles ' + ('FAILED' if r.returncode else ('rebuilt: ' + ', '.join(changed) if changed else 'clean')))
def check_syntax():
bad = []
for p in glob.glob(os.path.join(ROOT, 'modules/core/*.js')) + glob.glob(os.path.join(ROOT, 'modules/ext/*.js')):
if os.path.basename(p) in ('core-bundle.js', 'critical-ext-bundle.js'): continue
if run(['node', '--check', p]).returncode: bad.append(os.path.basename(p))
add('PASS' if not bad else 'FAIL', 'syntax ' + ('OK' if not bad else 'fail: ' + ', '.join(bad)))
def check_perms():
targets = glob.glob(os.path.join(ROOT, 'course/*.json')) + glob.glob(os.path.join(ROOT, 'course/chapters/*.json')) \
+ [os.path.join(ROOT, 'modules/ext/course.js'), os.path.join(ROOT, 'course.html'), os.path.join(ROOT, 'api/course/event.php')]
bad = []
for p in targets:
if not os.path.exists(p): continue
if (os.stat(p).st_mode & 0o777) != 0o644 or sh(f'stat -c %G {p}') != 'www-data':
if FIX:
os.chmod(p, 0o644); sh(f'chgrp www-data {p}')
else: bad.append(os.path.basename(p))
add('PASS' if not bad else ('WARN' if FIX else 'FAIL'), 'perms ' + ('clean' if not bad else ('fixed' if FIX else 'bad: ' + ', '.join(bad[:5]))))
def check_index():
idxp = os.path.join(ROOT, 'modules-index.json')
atlas = json.load(open(os.path.join(ROOT, 'course/atlas.json')))
idx = json.load(open(idxp))
cur = len(set(c for m in idx.get('modules', {}).values() for c in m.get('commands', [])))
if cur == atlas['coverage']['commands']:
add('PASS', f'modules-index {cur} commands'); return
add('WARN', f'modules-index stale {cur} vs {atlas["coverage"]["commands"]}')
if not FIX: return
for name, m in atlas['modules'].items():
e = idx['modules'].get(name, {'name': name, 'file': m['file'], 'type': 'core' if m['tier'] == 'core-bundle' else 'ext',
'class': '', 'auto_load': m['tier'] != 'ext-manual', 'description': ''})
e.update({'commands': m['commands'], 'file': m['file'], 'tier': m['tier']})
idx['modules'][name] = e
idx['version'] = datetime.now(timezone.utc).strftime('%Y-%m-%d') + '-atlas'
tmp = idxp + '.tmp'
with open(tmp, 'w', encoding='utf-8') as f: json.dump(idx, f, ensure_ascii=False, indent=1)
os.replace(tmp, idxp); os.chmod(idxp, 0o644); sh(f'chgrp www-data {idxp}')
add('PASS', 'modules-index regenerated')
def check_data():
err = os.path.join(ROOT, 'data/debug/errors.json')
if os.path.exists(err) and os.path.getsize(err) > 5 * 1024 * 1024:
if FIX:
arc = os.path.join(ROOT, 'data/debug/archive'); os.makedirs(arc, exist_ok=True)
os.replace(err, os.path.join(arc, 'errors-' + datetime.now().strftime('%Y%m') + '.json'))
open(err, 'w').write('[]'); os.chmod(err, 0o664); sh(f'chgrp www-data {err}')
add('PASS', 'errors.json rotated')
else: add('WARN', 'errors.json >5MB')
else: add('PASS', 'debug data OK')
def check_live():
for url in (f'{WEB}/main.html', f'{WEB}/course.html', f'{WEB}/course/course.json', f'{WEB}/modules/core/core-bundle.js'):
code = sh(f"curl -s -o /dev/null -w '%{{http_code}}' '{url}'")
add('PASS' if code == '200' else 'FAIL', f'HTTP {code} {url.replace(WEB, "")}')
def check_course():
r = run([sys.executable, 'scripts/course-validate.py'], cwd=ROOT)
add('PASS' if r.returncode == 0 else 'FAIL', 'course ' + ('valid' if r.returncode == 0 else 'INVALID: ' + r.stdout[-160:]))
def check_links():
if not DEEP: return
broken = []
for page in ('main.html', 'course.html', 'help.html', 'modules.html', 'guide.html'):
p = os.path.join(ROOT, page)
if not os.path.exists(p): continue
for href in re.findall(r'(?:href|src)="([^"#]+)"', open(p, encoding='utf-8').read()):
if any(x in href for x in ("'", '+', '{', '}')): continue
if href.startswith(('http', '//', 'mailto:', 'javascript:', 'data:', '/api/')): continue
target = os.path.normpath(os.path.join(ROOT, href.split('?')[0]))
if not os.path.exists(target): broken.append(f'{page}:{href}')
add('PASS' if not broken else 'WARN', 'links ' + ('OK' if not broken else f'{len(broken)} broken (first: {broken[0]})'))
def main():
os.chdir(ROOT)
check_versions(); check_bundles(); check_syntax(); check_perms(); check_index(); check_data(); check_live(); check_course(); check_links()
fails = [m for l, m in issues if l == 'FAIL']
print('newOS4 maintenance — ' + datetime.now(timezone.utc).strftime('%Y-%m-%d %H:%MZ') + (' [fix]' if FIX else '') + (' [deep]' if DEEP else ''))
for l, m in issues: print(f'{l}: {m}')
print('maintenance result: ' + ('FAIL' if fails else 'OK'))
print('needs OK: ' + ('none' if not fails else '; '.join(fails)[:400]))
return 1 if fails else 0
if __name__ == '__main__':
sys.exit(main())
|