197 lines
6.5 KiB
Python
197 lines
6.5 KiB
Python
#!/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'')
|