fix: embed icon in Windows exe via direct resource.o linker arg
This commit is contained in:
parent
4353045b1a
commit
5124856b72
4 changed files with 13 additions and 429 deletions
|
|
@ -1,2 +1,6 @@
|
||||||
[target.x86_64-unknown-linux-gnu]
|
[target.x86_64-unknown-linux-gnu]
|
||||||
linker = "x86_64-linux-gnu-gcc"
|
linker = "x86_64-linux-gnu-gcc"
|
||||||
|
|
||||||
|
[target.x86_64-pc-windows-gnu]
|
||||||
|
linker = "x86_64-w64-mingw32-gcc"
|
||||||
|
ar = "x86_64-w64-mingw32-ar"
|
||||||
|
|
|
||||||
|
|
@ -22,14 +22,21 @@ fn main() {
|
||||||
"windres"
|
"windres"
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let manifest_dir = std::env::var("CARGO_MANIFEST_DIR").unwrap();
|
||||||
|
let out_dir = std::env::var("OUT_DIR").unwrap();
|
||||||
|
|
||||||
let mut res = winres::WindowsResource::new();
|
let mut res = winres::WindowsResource::new();
|
||||||
res.set_icon("../../assets/icon.ico");
|
res.set_icon(&format!("{}/../../assets/icon.ico", manifest_dir));
|
||||||
res.set_toolkit_path("/usr");
|
res.set_toolkit_path("/usr");
|
||||||
res.set_windres_path(windres);
|
res.set_windres_path(windres);
|
||||||
res.set_ar_path("x86_64-w64-mingw32-ar");
|
res.set_ar_path("x86_64-w64-mingw32-ar");
|
||||||
|
|
||||||
match res.compile() {
|
match res.compile() {
|
||||||
Ok(_) => println!("cargo:warning=Icon embedded successfully via {windres}"),
|
Ok(_) => {
|
||||||
|
println!("cargo:warning=Icon embedded successfully via {windres}");
|
||||||
|
// Pass resource.o directly as linker arg (avoids ld skipping unreferenced archive members)
|
||||||
|
println!("cargo:rustc-link-arg={}/resource.o", out_dir);
|
||||||
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
println!("cargo:warning=winres failed: {e}");
|
println!("cargo:warning=winres failed: {e}");
|
||||||
println!("cargo:warning=windres path used: {windres}");
|
println!("cargo:warning=windres path used: {windres}");
|
||||||
|
|
|
||||||
BIN
remote
Executable file
BIN
remote
Executable file
Binary file not shown.
427
remote.py
427
remote.py
|
|
@ -1,427 +0,0 @@
|
||||||
#!/usr/bin/env python3
|
|
||||||
"""
|
|
||||||
helios-remote CLI - Control devices connected to the Helios Remote Relay Server.
|
|
||||||
|
|
||||||
Device labels are the sole identifiers. No session UUIDs.
|
|
||||||
Labels must be lowercase, no whitespace, only a-z 0-9 - _
|
|
||||||
"""
|
|
||||||
|
|
||||||
import argparse
|
|
||||||
import base64
|
|
||||||
import os
|
|
||||||
import re
|
|
||||||
import sys
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
# ── Load config.env ──────────────────────────────────────────────────────────
|
|
||||||
_config_path = Path(__file__).parent / "config.env"
|
|
||||||
_config: dict = {}
|
|
||||||
if _config_path.exists():
|
|
||||||
for _line in _config_path.read_text().splitlines():
|
|
||||||
_line = _line.strip()
|
|
||||||
if _line and not _line.startswith("#") and "=" in _line:
|
|
||||||
k, v = _line.split("=", 1)
|
|
||||||
_config[k.strip()] = v.strip()
|
|
||||||
|
|
||||||
BASE_URL = _config.get("HELIOS_REMOTE_URL", "").rstrip("/")
|
|
||||||
API_KEY = _config.get("HELIOS_REMOTE_API_KEY", "")
|
|
||||||
|
|
||||||
if not BASE_URL or not API_KEY:
|
|
||||||
sys.exit(f"[helios-remote] ERROR: config.env missing or incomplete at {_config_path}")
|
|
||||||
|
|
||||||
try:
|
|
||||||
import requests
|
|
||||||
except ImportError:
|
|
||||||
sys.exit("[helios-remote] ERROR: 'requests' not installed. Run: pip install requests")
|
|
||||||
|
|
||||||
|
|
||||||
LABEL_RE = re.compile(r'^[a-z0-9][a-z0-9_-]*$')
|
|
||||||
|
|
||||||
def validate_label(label: str) -> str:
|
|
||||||
"""Validate a device label."""
|
|
||||||
if not LABEL_RE.match(label):
|
|
||||||
sys.exit(
|
|
||||||
f"[helios-remote] Invalid label '{label}'. "
|
|
||||||
f"Must be lowercase, no whitespace, only a-z 0-9 - _"
|
|
||||||
)
|
|
||||||
return label
|
|
||||||
|
|
||||||
|
|
||||||
# ── HTTP helpers ─────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
def _headers() -> dict:
|
|
||||||
return {"X-Api-Key": API_KEY, "Content-Type": "application/json"}
|
|
||||||
|
|
||||||
|
|
||||||
def _req(method: str, path: str, timeout: int = 30, **kwargs):
|
|
||||||
url = f"{BASE_URL}{path}"
|
|
||||||
try:
|
|
||||||
resp = requests.request(method, url, headers=_headers(), timeout=timeout, **kwargs)
|
|
||||||
except requests.exceptions.ConnectionError as exc:
|
|
||||||
sys.exit(f"[helios-remote] CONNECTION ERROR: Cannot reach {url}\n → {exc}")
|
|
||||||
except requests.exceptions.Timeout:
|
|
||||||
sys.exit(f"[helios-remote] TIMEOUT: {url} did not respond within {timeout} s")
|
|
||||||
|
|
||||||
if not resp.ok:
|
|
||||||
body = resp.text[:1000] if resp.text else "(empty body)"
|
|
||||||
sys.exit(
|
|
||||||
f"[helios-remote] HTTP {resp.status_code} {resp.reason}\n"
|
|
||||||
f" URL : {url}\n"
|
|
||||||
f" Method : {method.upper()}\n"
|
|
||||||
f" Body : {body}"
|
|
||||||
)
|
|
||||||
return resp
|
|
||||||
|
|
||||||
|
|
||||||
# ── Window resolution ────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
def resolve_window(device: str, window_label: str) -> int:
|
|
||||||
"""Resolve a window label to its HWND. Searches by label field."""
|
|
||||||
resp = _req("GET", f"/devices/{device}/windows")
|
|
||||||
windows = resp.json().get("windows", [])
|
|
||||||
query = window_label.lower()
|
|
||||||
# Exact label match first
|
|
||||||
for w in windows:
|
|
||||||
if w.get("visible") and w.get("label", "") == query:
|
|
||||||
return int(w["id"])
|
|
||||||
# Substring match on label
|
|
||||||
matches = [w for w in windows if w.get("visible") and query in w.get("label", "")]
|
|
||||||
if not matches:
|
|
||||||
# Fallback: substring match on title
|
|
||||||
matches = [w for w in windows if w.get("visible") and query in w.get("title", "").lower()]
|
|
||||||
if not matches:
|
|
||||||
raise SystemExit(f"[helios-remote] No visible window matching '{window_label}'")
|
|
||||||
if len(matches) > 1:
|
|
||||||
print(f"[helios-remote] Multiple matches for '{window_label}', using first:")
|
|
||||||
for w in matches:
|
|
||||||
print(f" {w.get('label', '?'):<30} {w['title']}")
|
|
||||||
return int(matches[0]["id"])
|
|
||||||
|
|
||||||
|
|
||||||
# ── Commands ─────────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
def cmd_devices(_args):
|
|
||||||
"""List all connected devices."""
|
|
||||||
resp = _req("GET", "/devices")
|
|
||||||
data = resp.json()
|
|
||||||
devices = data.get("devices", [])
|
|
||||||
if not devices:
|
|
||||||
print("No devices connected.")
|
|
||||||
return
|
|
||||||
print(f"{'Device':<30}")
|
|
||||||
print("-" * 30)
|
|
||||||
for d in devices:
|
|
||||||
print(d.get("label", "?"))
|
|
||||||
|
|
||||||
|
|
||||||
def cmd_screenshot(args):
|
|
||||||
"""Capture a screenshot. Argument is 'screen' for full screen or a window label."""
|
|
||||||
device = validate_label(args.device)
|
|
||||||
target = args.target
|
|
||||||
out_path = Path("/tmp/helios-remote-screenshot.png")
|
|
||||||
|
|
||||||
if target == "screen":
|
|
||||||
resp = _req("POST", f"/devices/{device}/screenshot")
|
|
||||||
else:
|
|
||||||
# Resolve window label to HWND
|
|
||||||
wid = resolve_window(device, target)
|
|
||||||
resp = _req("POST", f"/devices/{device}/windows/{wid}/screenshot")
|
|
||||||
|
|
||||||
data = resp.json()
|
|
||||||
b64 = (
|
|
||||||
data.get("image_base64")
|
|
||||||
or data.get("screenshot")
|
|
||||||
or data.get("image")
|
|
||||||
or data.get("data")
|
|
||||||
or data.get("png")
|
|
||||||
)
|
|
||||||
if not b64:
|
|
||||||
sys.exit(f"[helios-remote] ERROR: No image in response. Keys: {list(data.keys())}")
|
|
||||||
if "," in b64:
|
|
||||||
b64 = b64.split(",", 1)[1]
|
|
||||||
out_path.write_bytes(base64.b64decode(b64))
|
|
||||||
print(str(out_path))
|
|
||||||
|
|
||||||
|
|
||||||
def cmd_exec(args):
|
|
||||||
"""Run a shell command on the remote device."""
|
|
||||||
device = validate_label(args.device)
|
|
||||||
command = " ".join(args.parts) if isinstance(args.parts, list) else args.parts
|
|
||||||
body = {"command": command}
|
|
||||||
if args.timeout:
|
|
||||||
body["timeout_ms"] = args.timeout * 1000
|
|
||||||
resp = _req("POST", f"/devices/{device}/exec", json=body,
|
|
||||||
timeout=max(35, (args.timeout or 30) + 5))
|
|
||||||
data = resp.json()
|
|
||||||
|
|
||||||
stdout = data.get("stdout") or data.get("output") or ""
|
|
||||||
stderr = data.get("stderr") or ""
|
|
||||||
exit_code = data.get("exit_code")
|
|
||||||
|
|
||||||
if stdout:
|
|
||||||
print(stdout, end="" if stdout.endswith("\n") else "\n")
|
|
||||||
if stderr:
|
|
||||||
print("[stderr]", stderr, file=sys.stderr)
|
|
||||||
if exit_code is not None and int(exit_code) != 0:
|
|
||||||
print(f"[helios-remote] Command exited with code {exit_code}", file=sys.stderr)
|
|
||||||
sys.exit(int(exit_code))
|
|
||||||
|
|
||||||
|
|
||||||
def cmd_windows(args):
|
|
||||||
"""List windows on the remote device (visible only)."""
|
|
||||||
device = validate_label(args.device)
|
|
||||||
resp = _req("GET", f"/devices/{device}/windows")
|
|
||||||
data = resp.json()
|
|
||||||
windows = data.get("windows", [])
|
|
||||||
windows = [w for w in windows if w.get("visible")]
|
|
||||||
if not windows:
|
|
||||||
print("No windows returned.")
|
|
||||||
return
|
|
||||||
print(f"{'Label':<30} Title")
|
|
||||||
print("-" * 70)
|
|
||||||
for w in windows:
|
|
||||||
label = w.get("label", "?")
|
|
||||||
title = w.get("title", "")
|
|
||||||
print(f"{label:<30} {title}")
|
|
||||||
|
|
||||||
|
|
||||||
def cmd_minimize_all(args):
|
|
||||||
"""Minimize all windows on the remote device."""
|
|
||||||
device = validate_label(args.device)
|
|
||||||
_req("POST", f"/devices/{device}/windows/minimize-all")
|
|
||||||
print(f"All windows minimized on {device}.")
|
|
||||||
|
|
||||||
|
|
||||||
def cmd_focus(args):
|
|
||||||
"""Bring a window to the foreground (by label)."""
|
|
||||||
device = validate_label(args.device)
|
|
||||||
wid = resolve_window(device, args.window)
|
|
||||||
_req("POST", f"/devices/{device}/windows/{wid}/focus")
|
|
||||||
print(f"Window '{args.window}' focused on {device}.")
|
|
||||||
|
|
||||||
|
|
||||||
def cmd_maximize(args):
|
|
||||||
"""Maximize and focus a window (by label)."""
|
|
||||||
device = validate_label(args.device)
|
|
||||||
wid = resolve_window(device, args.window)
|
|
||||||
_req("POST", f"/devices/{device}/windows/{wid}/maximize")
|
|
||||||
print(f"Window '{args.window}' maximized on {device}.")
|
|
||||||
|
|
||||||
|
|
||||||
def cmd_version(args):
|
|
||||||
"""Show relay, remote.py, and client commit — check if all are in sync."""
|
|
||||||
device = validate_label(args.device)
|
|
||||||
|
|
||||||
# 1. Relay (public, no auth)
|
|
||||||
try:
|
|
||||||
r = requests.get(f"{BASE_URL}/version", timeout=10)
|
|
||||||
relay = r.json() if r.ok else {"commit": f"HTTP {r.status_code}"}
|
|
||||||
except Exception as e:
|
|
||||||
relay = {"commit": str(e)}
|
|
||||||
|
|
||||||
# 2. remote.py commit
|
|
||||||
import subprocess
|
|
||||||
try:
|
|
||||||
r = subprocess.run(
|
|
||||||
["git", "log", "-1", "--format=%h", "--", __file__],
|
|
||||||
capture_output=True, text=True,
|
|
||||||
cwd=os.path.dirname(os.path.abspath(__file__))
|
|
||||||
)
|
|
||||||
script_commit = r.stdout.strip() or "unknown"
|
|
||||||
except Exception:
|
|
||||||
script_commit = "unknown"
|
|
||||||
|
|
||||||
# 3. Client
|
|
||||||
try:
|
|
||||||
r2 = _req("GET", f"/devices/{device}/version")
|
|
||||||
client = r2.json()
|
|
||||||
except SystemExit:
|
|
||||||
client = {"commit": "unreachable"}
|
|
||||||
|
|
||||||
relay_commit = relay.get("commit", "?")
|
|
||||||
client_commit = client.get("commit", "?")
|
|
||||||
all_same = relay_commit == script_commit == client_commit
|
|
||||||
|
|
||||||
print(f" relay {relay_commit}")
|
|
||||||
print(f" remote.py {script_commit}")
|
|
||||||
print(f" client {client_commit}")
|
|
||||||
print(f" {'✅ all in sync' if all_same else '⚠️ OUT OF SYNC'}")
|
|
||||||
|
|
||||||
|
|
||||||
def cmd_logs(args):
|
|
||||||
"""Fetch the last N lines of the client log file."""
|
|
||||||
device = validate_label(args.device)
|
|
||||||
resp = _req("GET", f"/devices/{device}/logs", params={"lines": args.lines})
|
|
||||||
data = resp.json()
|
|
||||||
if "error" in data:
|
|
||||||
sys.exit(f"[helios-remote] {data['error']}")
|
|
||||||
print(f"# {data.get('log_path', '?')} (last {args.lines} lines)")
|
|
||||||
print(data.get("content", ""))
|
|
||||||
|
|
||||||
|
|
||||||
def cmd_upload(args):
|
|
||||||
"""Upload a local file to the remote device."""
|
|
||||||
device = validate_label(args.device)
|
|
||||||
local_path = Path(args.local_path)
|
|
||||||
if not local_path.exists():
|
|
||||||
sys.exit(f"[helios-remote] ERROR: Local file not found: {local_path}")
|
|
||||||
content_base64 = base64.b64encode(local_path.read_bytes()).decode()
|
|
||||||
_req("POST", f"/devices/{device}/upload", json={
|
|
||||||
"path": args.remote_path,
|
|
||||||
"content_base64": content_base64,
|
|
||||||
})
|
|
||||||
print(f"Uploaded {local_path} → {args.remote_path} on {device}.")
|
|
||||||
|
|
||||||
|
|
||||||
def cmd_download(args):
|
|
||||||
"""Download a file from the remote device to a local path."""
|
|
||||||
device = validate_label(args.device)
|
|
||||||
from urllib.parse import quote
|
|
||||||
encoded_path = quote(args.remote_path, safe="")
|
|
||||||
resp = _req("GET", f"/devices/{device}/download?path={encoded_path}")
|
|
||||||
data = resp.json()
|
|
||||||
b64 = data.get("content_base64", "")
|
|
||||||
if not b64:
|
|
||||||
sys.exit(f"[helios-remote] ERROR: No content in download response.")
|
|
||||||
local_path = Path(args.local_path)
|
|
||||||
local_path.parent.mkdir(parents=True, exist_ok=True)
|
|
||||||
local_path.write_bytes(base64.b64decode(b64))
|
|
||||||
size = data.get("size", len(base64.b64decode(b64)))
|
|
||||||
print(f"Downloaded {args.remote_path} → {local_path} ({size} bytes).")
|
|
||||||
|
|
||||||
|
|
||||||
def cmd_prompt(args):
|
|
||||||
"""Show a MessageBox on the remote PC asking the user to do something."""
|
|
||||||
device = validate_label(args.device)
|
|
||||||
body = {"message": args.message}
|
|
||||||
if args.title:
|
|
||||||
body["title"] = args.title
|
|
||||||
resp = _req("POST", f"/devices/{device}/prompt", json=body)
|
|
||||||
data = resp.json()
|
|
||||||
answer = data.get("answer", "")
|
|
||||||
if answer:
|
|
||||||
print(answer)
|
|
||||||
else:
|
|
||||||
print("Prompt confirmed.")
|
|
||||||
|
|
||||||
|
|
||||||
def cmd_run(args):
|
|
||||||
"""Launch a program on the remote device (fire-and-forget)."""
|
|
||||||
device = validate_label(args.device)
|
|
||||||
_req("POST", f"/devices/{device}/run", json={"program": args.program, "args": args.args})
|
|
||||||
print(f"Started {args.program!r} on {device}.")
|
|
||||||
|
|
||||||
|
|
||||||
def cmd_clipboard_get(args):
|
|
||||||
"""Get the clipboard contents from the remote device."""
|
|
||||||
device = validate_label(args.device)
|
|
||||||
resp = _req("GET", f"/devices/{device}/clipboard")
|
|
||||||
data = resp.json()
|
|
||||||
print(data.get("text", ""))
|
|
||||||
|
|
||||||
|
|
||||||
def cmd_clipboard_set(args):
|
|
||||||
"""Set the clipboard contents on the remote device."""
|
|
||||||
device = validate_label(args.device)
|
|
||||||
_req("POST", f"/devices/{device}/clipboard", json={"text": args.text})
|
|
||||||
print(f"Clipboard set ({len(args.text)} chars) on {device}.")
|
|
||||||
|
|
||||||
|
|
||||||
# ── CLI wiring ────────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
def build_parser() -> argparse.ArgumentParser:
|
|
||||||
p = argparse.ArgumentParser(
|
|
||||||
prog="remote.py",
|
|
||||||
description="Control devices connected to the Helios Remote Relay Server.",
|
|
||||||
)
|
|
||||||
sub = p.add_subparsers(dest="subcmd", required=True)
|
|
||||||
|
|
||||||
sub.add_parser("devices", help="List all connected devices")
|
|
||||||
|
|
||||||
sp = sub.add_parser("screenshot", help="Capture screenshot (screen or window label)")
|
|
||||||
sp.add_argument("device", help="Device label")
|
|
||||||
sp.add_argument("target", help="'screen' for full screen, or a window label")
|
|
||||||
|
|
||||||
ep = sub.add_parser("exec", help="Run a shell command on the remote device")
|
|
||||||
ep.add_argument("device", help="Device label")
|
|
||||||
ep.add_argument("parts", nargs=argparse.REMAINDER, metavar="command",
|
|
||||||
help="Command (and arguments) to execute")
|
|
||||||
ep.add_argument("--timeout", type=int, default=None, metavar="SECONDS",
|
|
||||||
help="Timeout in seconds (default: 30)")
|
|
||||||
|
|
||||||
wp = sub.add_parser("windows", help="List all visible windows on the remote device")
|
|
||||||
wp.add_argument("device", help="Device label")
|
|
||||||
|
|
||||||
mp = sub.add_parser("minimize-all", help="Minimize all windows")
|
|
||||||
mp.add_argument("device", help="Device label")
|
|
||||||
|
|
||||||
fp = sub.add_parser("focus", help="Bring a window to the foreground")
|
|
||||||
fp.add_argument("device", help="Device label")
|
|
||||||
fp.add_argument("window", help="Window label")
|
|
||||||
|
|
||||||
xp = sub.add_parser("maximize", help="Maximize and focus a window")
|
|
||||||
xp.add_argument("device", help="Device label")
|
|
||||||
xp.add_argument("window", help="Window label")
|
|
||||||
|
|
||||||
vp = sub.add_parser("version", help="Compare relay, remote.py, and client commits")
|
|
||||||
vp.add_argument("device", help="Device label")
|
|
||||||
|
|
||||||
lgp = sub.add_parser("logs", help="Fetch last N lines of client log file")
|
|
||||||
lgp.add_argument("device", help="Device label")
|
|
||||||
lgp.add_argument("--lines", type=int, default=100, metavar="N")
|
|
||||||
|
|
||||||
up = sub.add_parser("upload", help="Upload a local file to the remote device")
|
|
||||||
up.add_argument("device", help="Device label")
|
|
||||||
up.add_argument("local_path")
|
|
||||||
up.add_argument("remote_path")
|
|
||||||
|
|
||||||
dp = sub.add_parser("download", help="Download a file from the remote device")
|
|
||||||
dp.add_argument("device", help="Device label")
|
|
||||||
dp.add_argument("remote_path")
|
|
||||||
dp.add_argument("local_path")
|
|
||||||
|
|
||||||
pp = sub.add_parser("prompt", help="Show a MessageBox asking the user to do something")
|
|
||||||
pp.add_argument("device", help="Device label")
|
|
||||||
pp.add_argument("message")
|
|
||||||
pp.add_argument("--title", default=None)
|
|
||||||
|
|
||||||
rp = sub.add_parser("run", help="Launch a program (fire-and-forget)")
|
|
||||||
rp.add_argument("device", help="Device label")
|
|
||||||
rp.add_argument("program")
|
|
||||||
rp.add_argument("args", nargs=argparse.REMAINDER)
|
|
||||||
|
|
||||||
cgp = sub.add_parser("clipboard-get", help="Get clipboard contents")
|
|
||||||
cgp.add_argument("device", help="Device label")
|
|
||||||
|
|
||||||
csp = sub.add_parser("clipboard-set", help="Set clipboard contents")
|
|
||||||
csp.add_argument("device", help="Device label")
|
|
||||||
csp.add_argument("text")
|
|
||||||
|
|
||||||
return p
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
parser = build_parser()
|
|
||||||
args = parser.parse_args()
|
|
||||||
|
|
||||||
{
|
|
||||||
"devices": cmd_devices,
|
|
||||||
"screenshot": cmd_screenshot,
|
|
||||||
"exec": cmd_exec,
|
|
||||||
"windows": cmd_windows,
|
|
||||||
"minimize-all": cmd_minimize_all,
|
|
||||||
"focus": cmd_focus,
|
|
||||||
"maximize": cmd_maximize,
|
|
||||||
"version": cmd_version,
|
|
||||||
"logs": cmd_logs,
|
|
||||||
"upload": cmd_upload,
|
|
||||||
"download": cmd_download,
|
|
||||||
"prompt": cmd_prompt,
|
|
||||||
"run": cmd_run,
|
|
||||||
"clipboard-get": cmd_clipboard_get,
|
|
||||||
"clipboard-set": cmd_clipboard_set,
|
|
||||||
}[args.subcmd](args)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue