#!/usr/bin/env python3 """Generate a self-contained fpk for distribution — includes app code + static assets.""" import os, re, struct, zlib, tarfile, io, json, hashlib, time as tmod OUTDIR = '/vol2/1000/HD2/fpk' APP_NAME = 'fnmon' APP_DIR = os.path.dirname(os.path.abspath(__file__)) os.makedirs(OUTDIR, exist_ok=True) # === Auto-version === latest_ver = '0.0.0' # Also account for legacy cpu-monitor-v* fpks for f in os.listdir(OUTDIR): m = re.match(r'(?:fnmon|cpu-monitor)-v(\d+\.\d+\.\d+)\.fpk', f) if m: v = tuple(int(x) for x in m.group(1).split('.')) if v > tuple(int(x) for x in latest_ver.split('.')): latest_ver = m.group(1) v_parts = [int(x) for x in latest_ver.split('.')] v_parts[2] += 1 NEW_VER = '.'.join(str(x) for x in v_parts) OUTPATH = f'{OUTDIR}/fnmon-v{NEW_VER}.fpk' now_ts = int(tmod.time()) def add_file(tar, path, content=None, filepath=None, mode=0o644): """Add a file to the tar archive.""" info = tarfile.TarInfo(name=f'{APP_NAME}/{path}') if filepath: with open(filepath, 'rb') as f: data = f.read() elif isinstance(content, bytes): data = content else: data = content.encode('utf-8') info.size = len(data) info.mode = mode info.mtime = now_ts info.uid = info.gid = 0 info.uname = 'root' info.gname = 'root' tar.addfile(info, io.BytesIO(data)) # === Build manifest === manifest_content = f'''appname = {APP_NAME} version = {NEW_VER} display_name = fnmon - 系统监视器 desc = 实时系统监控面板 - CPU/内存/网络/磁盘/进程 全能监控 platform = all source = thirdparty maintainer = Hermes maintainer_url = https://github.com/nousresearch/hermes-agent distributor = Community os_min_version = 0.8.1 desktop_uidir = ui desktop_applaunchname = {APP_NAME}.Application service_port = 5000 checkport = true ctl_stop = true ''' manifest_checksum = hashlib.md5(manifest_content.encode('utf-8')).hexdigest() manifest_full = manifest_content + f'checksum = {manifest_checksum}\n' # === Build tar === with tarfile.open(OUTPATH, 'w:gz') as tar: add_file(tar, 'manifest', manifest_full) # Config add_file(tar, 'config/privilege', json.dumps({ 'defaults': {'run-as': 'root'}, }, indent=2)) add_file(tar, 'config/resource', '{\n "systemd-unit": {}\n}\n') # Lifecycle scripts scripts = { 'install_init': '#!/bin/bash\nexit 0\n', 'install_callback': f'''#!/bin/bash # Create and enable systemd service cat > /etc/systemd/system/{APP_NAME}.service << 'SERVEOF' [Unit] Description=fnmon - 系统监视器 After=network.target [Service] Type=simple User=root WorkingDirectory=/var/apps/{APP_NAME} ExecStart=/usr/bin/python3 /var/apps/{APP_NAME}/app.py Restart=always RestartSec=5 [Install] WantedBy=multi-user.target SERVEOF systemctl daemon-reload systemctl enable {APP_NAME}.service systemctl start {APP_NAME}.service exit 0 ''', 'uninstall_init': f'#!/bin/bash\nsystemctl stop {APP_NAME}.service 2>/dev/null\nexit 0\n', 'uninstall_callback': f'''#!/bin/bash systemctl disable {APP_NAME}.service 2>/dev/null rm -f /etc/systemd/system/{APP_NAME}.service systemctl daemon-reload exit 0 ''', 'upgrade_init': '#!/bin/bash\nexit 0\n', 'upgrade_callback': f'#!/bin/bash\nsystemctl restart {APP_NAME}.service 2>/dev/null\nexit 0\n', 'config_init': '#!/bin/bash\nexit 0\n', 'config_callback': '#!/bin/bash\nexit 0\n', } for name, content in scripts.items(): add_file(tar, f'cmd/{name}', content, mode=0o755) # Main lifecycle add_file(tar, 'cmd/main', f'''#!/bin/bash case $1 in start) systemctl start {APP_NAME}.service 2>/dev/null sleep 2 curl -s -o /dev/null http://localhost:5000 && exit 0 exit 1 ;; stop) exit 0 ;; status) curl -s -o /dev/null http://localhost:5000 && exit 0 || exit 3 ;; *) exit 1 ;; esac ''', mode=0o755) # === Bundle app code === add_file(tar, 'app.py', filepath=os.path.join(APP_DIR, 'app.py'), mode=0o755) add_file(tar, 'traffic_tracker.py', filepath=os.path.join(APP_DIR, 'traffic_tracker.py'), mode=0o644) add_file(tar, 'app_usage_tracker.py', filepath=os.path.join(APP_DIR, 'app_usage_tracker.py'), mode=0o644) add_file(tar, 'static/index.html', filepath=os.path.join(APP_DIR, 'static/index.html'), mode=0o644) # UI config add_file(tar, 'ui/index.json', json.dumps({ APP_NAME: { 'application': { 'type': 'url', 'protocol': 'http', 'port': 5000, 'url': 'http://${host}:5000', 'title': 'fnmon - 系统监视器', 'description': '实时系统监控面板' } } }, ensure_ascii=False, indent=2)) # Icons for fname, fpk_name in [(f'{APP_NAME}-icon-128.png', 'ICON.PNG'), (f'{APP_NAME}-icon-256.png', 'ICON_256.PNG')]: ipath = os.path.join(OUTDIR, fname) if os.path.exists(ipath): add_file(tar, fpk_name, filepath=ipath) else: # Fallback: simple blue icon add_file(tar, fpk_name, _gen_png(128 if '128' in fname else 256, (80, 120, 255))) # === Done === size = os.path.getsize(OUTPATH) print(f'✅ fpk created: {OUTPATH} ({size} bytes)') print(f' version: v{NEW_VER} (previous: v{latest_ver})') print(f' checksum: {manifest_checksum}') print(f' bundled: app.py + frontend + trackers') with tarfile.open(OUTPATH) as t: for m in t.getmembers(): print(f' {m.name}: {m.size}B') def _gen_png(size, color): """Quick PNG generator (fallback if no icon).""" r, g, b = color def chunk(ctype, data): c = ctype + data return struct.pack('>I', len(data)) + c + struct.pack('>I', zlib.crc32(c) & 0xffffffff) hdr = b'\x89PNG\r\n\x1a\n' ihdr = chunk(b'IHDR', struct.pack('>IIBBBBB', size, size, 8, 2, 0, 0, 0)) raw = b'' for y in range(size): raw += b'\x00' for x in range(size): d = ((x-size/2)**2 + (y-size/2)**2) ** 0.5 rad = size/2 - 4 if d < rad: t = d / rad raw += struct.pack('BBB', int(r-(r-50)*t), int(g-(g-100)*t), int(b-(b-200)*t)) else: raw += b'\x00\x00\x00' return hdr + ihdr + chunk(b'IDAT', zlib.compress(raw)) + chunk(b'IEND', b'')