Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
*.tar.gz
*.zip
*.so
!cli/embedded/
!cli/embedded/*.so
*.swp
.DS_Store
cli/remagic
Expand Down
30 changes: 30 additions & 0 deletions cli/appload.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package main

import (
_ "embed"
"strconv"
"strings"
)

// Upstream v0.5.3 hooks target OS <=3.27. On 3.28+ the sidebar QML moved and
// appload panics with "Couldn't resolve the hashed identifier … AppLoad hooks".
// Built from asivery/rm-appload PR #59 until a stable upstream release ships.
//
//go:embed embedded/appload-3.28-aarch64.so
var appload328 []byte

func osNeedsAppLoad328(osv string) bool {
parts := strings.Split(strings.TrimSpace(osv), ".")
if len(parts) < 2 || parts[0] != "3" {
return false
}
minor, err := strconv.Atoi(parts[1])
return err == nil && minor >= 28
}

func apploadSOForOS(osv string) ([]byte, string) {
if osNeedsAppLoad328(osv) {
return appload328, "3.28 (PR #59 build — upstream v0.5.3 breaks on this OS)"
}
return nil, ""
}
Binary file added cli/embedded/appload-3.28-aarch64.so
Binary file not shown.
118 changes: 118 additions & 0 deletions cli/hashtab.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
package main

import (
"strings"

"github.com/maximerivest/remagic/cli/internal/device"
)

// rebuildHashtabScript drives xochitl with only qt-resource-rebuilder loaded
// and waits until the per-OS-version hashtab file exists. Upstream's
// rebuild_hashtable blocks on "press enter" and remagic's old setup used
// `timeout … rebuild_hashtable </dev/null`, which fails on device images
// without coreutils timeout — leaving AppLoad to crash-loop xochitl on
// OS 3.27+ when the hashtab is missing.
const rebuildHashtabScript = `
set -u
xovi=/home/root/xovi
qrr_so="$xovi/extensions.d/qt-resource-rebuilder.so"
qrr_dir="$xovi/exthome/qt-resource-rebuilder"
hashtab="$qrr_dir/hashtab"
xovi_root=/tmp/xovi-hashtab-build
log=/tmp/xovi-hashtab.log

if [ ! -f "$qrr_so" ]; then
echo "qt-resource-rebuilder not installed" >&2
exit 1
fi

systemctl stop xochitl 2>/dev/null || true
for sig in 15 9; do
pid=$(pidof xochitl 2>/dev/null || true)
[ -n "$pid" ] || break
kill -$sig $pid 2>/dev/null || true
sleep 1
done

rm -rf "$xovi_root" "$log"
mkdir -p "$xovi_root/extensions.d" "$qrr_dir"
ln -sf "$qrr_so" "$xovi_root/extensions.d/"
rm -f "$hashtab"

export XOVI_ROOT="$xovi_root"
QMLDIFF_HASHTAB_CREATE="$hashtab" \
QML_DISABLE_DISK_CACHE=1 \
LD_PRELOAD="$xovi/xovi.so" \
/usr/bin/xochitl >>"$log" 2>&1 &
xpid=$!

deadline=$(($(date +%s) + 180))
ok=0
while [ "$(date +%s)" -lt "$deadline" ]; do
if [ -s "$hashtab" ]; then
ok=1
break
fi
if grep -qF "[qmldiff]: Hashtab saved to $hashtab" "$log" 2>/dev/null; then
ok=1
break
fi
if ! kill -0 "$xpid" 2>/dev/null; then
break
fi
sleep 1
done

pid=$(pidof xochitl 2>/dev/null || true)
[ -n "$pid" ] && kill -15 $pid 2>/dev/null || true
sleep 1
pid=$(pidof xochitl 2>/dev/null || true)
[ -n "$pid" ] && kill -9 $pid 2>/dev/null || true

rm -rf "$xovi_root"

if [ "$ok" = 1 ] && [ -s "$hashtab" ]; then
echo "Hashtab rebuilt at $hashtab"
exit 0
fi

echo "Hashtab rebuild failed." >&2
echo "--- last log lines ---" >&2
tail -30 "$log" 2>/dev/null >&2 || true
exit 1

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Restart stock xochitl when hashtab rebuild fails

If the rebuild times out or xochitl exits before writing the hashtab, this failure path returns after the script has already run systemctl stop xochitl and killed any remaining process, and cmdSetup then skips /home/root/xovi/start when hashtabOK is false. That leaves the tablet with no running xochitl/UI until the user reboots or manually starts it; the duplicated scripts/rebuild-hashtab.sh has the same failure path. Please start stock xochitl (or otherwise restore the prior UI state) before exiting with failure.

Useful? React with 👍 / 👎.

`

const hashtabPath = "/home/root/xovi/exthome/qt-resource-rebuilder/hashtab"

// rebuildHashtab runs the non-interactive rebuild on the tablet. Returns true
// when qt-resource-rebuilder is absent (nothing to do) or the hashtab exists.
func rebuildHashtab(d *device.Device) bool {
if _, err := d.Run("test -f /home/root/xovi/extensions.d/qt-resource-rebuilder.so"); err != nil {
ok("not needed (no qt-resource-rebuilder in this bundle)")
return true
}
out, err := d.RunIn("bash -s", strings.NewReader(rebuildHashtabScript))
out = strings.TrimSpace(out)
if err != nil {
if out != "" {
for _, line := range strings.Split(out, "\n") {
warn("%s", strings.TrimSpace(line))
}
}
warn("AppLoad needs this hashtab on OS 3.27+ — without it xochitl crash-loops")
warn("retry with: remagic rebuild-hashtab")
return false
}
if out != "" {
ok("%s", out)
} else {
ok("hashtable rebuilt")
}
return true
}

// hasHashtab reports whether the on-device hashtab file exists and is non-empty.
func hasHashtab(d *device.Device) bool {
_, err := d.Run("test -s " + hashtabPath)
return err == nil
}
31 changes: 31 additions & 0 deletions cli/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ usage: remagic <command> [args]
config <app> configure an app from your browser — prints a QR
code so you can use your phone's keyboard
wifi on|off|status SSH over Wi-Fi, so the cable is only ever needed once
rebuild-hashtab rebuild the Qt resource hashtab (needed after OS updates)
repair-ssh fix the "connection reset" SSH wedge in-band
publish [folder] zip an app folder, create a GitHub release (via gh),
and print/update its catalog entry
Expand Down Expand Up @@ -126,6 +127,8 @@ func main() {
die("usage: remagic wifi on|off|status")
}
cmdWifi(mustConnect(host), args[0])
case "rebuild-hashtab":
cmdRebuildHashtab(mustConnect(host))
case "repair-ssh":
cmdRepairSSH(mustConnect(host))
case "publish":
Expand Down Expand Up @@ -235,6 +238,19 @@ func cmdDoctor(d *device.Device) {
}
check("xovi installed", "test -d /home/root/xovi")
check("AppLoad installed", "test -f /home/root/xovi/extensions.d/appload.so")
if _, err := d.Run("test -f /home/root/xovi/extensions.d/qt-resource-rebuilder.so"); err == nil {
if hasHashtab(d) {
ok("Qt resource hashtab present")
} else {
warn("Qt resource hashtab missing — AppLoad will crash xochitl on OS 3.27+")
warn("Run: remagic rebuild-hashtab")
}
}
if _, err := d.Run(`pid=$(pidof xochitl 2>/dev/null | awk '{print $1}') && test -n "$pid" && grep -q xovi.so /proc/$pid/maps`); err == nil {
ok("xovi active in running xochitl")
} else {
warn("xovi not loaded in xochitl (stock mode, or crash-looping)")
}
// Any of the community persistence flavors counts.
if _, err := d.Run("systemctl is-enabled xovi-tripletap 2>/dev/null || systemctl is-enabled xovi-always-on 2>/dev/null || systemctl is-enabled xovi-boot 2>/dev/null"); err == nil {
ok("boot persistence (tripletap / always-on unit)")
Expand Down Expand Up @@ -547,6 +563,21 @@ func cmdWifi(d *device.Device, action string) {
// dropbear host-key bind mount down with it — new connections then die at key
// exchange until reboot. Restores the /etc overlay, the key mount, and
// read-only /.
func cmdRebuildHashtab(d *device.Device) {
defer d.Close()
step("rebuilding the Qt resource hashtable")
if !rebuildHashtab(d) {
die("hashtab rebuild failed — see messages above")
}
step("starting xovi")
if out, err := d.Run(`systemctl stop xovi-firststart 2>/dev/null; systemctl reset-failed xovi-firststart 2>/dev/null; \
systemd-run --unit=xovi-firststart --collect --service-type=oneshot /home/root/xovi/start`); err != nil {
warn("couldn't start xovi (%s) — triple-press power", tail(out, 120))
} else {
ok("xovi started — AppLoad should appear on the tablet")
}
}

func cmdRepairSSH(d *device.Device) {
defer d.Close()
step("repairing the SSH key store")
Expand Down
79 changes: 50 additions & 29 deletions cli/setup.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ package main
// 2. ssh key (generated if you have none, then installed)
// 3. xovi bundle (loader + scripts, from asivery's pinned release)
// 4. AppLoad (only appload.so into extensions.d; shims into exthome)
// 5. hashtable (best-effort; only UI/QML mods need it)
// 5. hashtable (required for AppLoad on OS 3.27+)
// 6. tripletap (power-button persistence; script streamed from the laptop
// over verified TLS, executed on the device)
// 7. ssh repair (tripletap's installer knocks over the host-key mount on
Expand Down Expand Up @@ -89,42 +89,60 @@ mv -f /home/root/xovi/inactive-extensions/xovi-message-broker.so /home/root/xovi
ok("xovi installed at /home/root/xovi")

// ---- 4. AppLoad ---------------------------------------------------------
step("downloading AppLoad")
appload, err := fetchURL(apploadURL)
if err != nil {
die("download AppLoad: %v", err)
}
ok("%d KB", len(appload)/1024)
step("installing the AppLoad launcher")
if err := d.Push(appload, "/tmp/appload.zip", "644"); err != nil {
die("%v", err)
}
// ONLY appload.so is a xovi extension → extensions.d/. The zip also
// carries shims/qtfb-shim*.so (runtime pieces for windowed apps) which
// must NOT land in extensions.d/ or xovi tries to load them and errors.
if out, err := d.Run(`cd /tmp && rm -rf appload-unz && mkdir appload-unz && \
osv, _ := d.Run(`. /etc/os-release 2>/dev/null; printf %s "$IMG_VERSION"`)
osv = strings.TrimSpace(osv)
if blob, label := apploadSOForOS(osv); blob != nil {
step("installing AppLoad for OS %s", label)
if err := d.Push(blob, "/home/root/xovi/extensions.d/appload.so", "755"); err != nil {
die("%v", err)
}
step("downloading AppLoad shims")
apploadZip, err := fetchURL(apploadURL)
if err != nil {
die("download AppLoad shims: %v", err)
}
if err := d.Push(apploadZip, "/tmp/appload.zip", "644"); err != nil {
die("%v", err)
}
if out, err := d.Run(`cd /tmp && rm -rf appload-unz && mkdir appload-unz && \
(unzip -oq appload.zip -d appload-unz || busybox unzip -o appload.zip -d appload-unz) && \
cp -f appload-unz/appload.so /home/root/xovi/extensions.d/ && \
mkdir -p /home/root/xovi/exthome/appload && \
if [ -d appload-unz/shims ]; then cp -rf appload-unz/shims /home/root/xovi/exthome/appload/; fi && \
if [ -d appload-unz/exthome ]; then cp -rf appload-unz/exthome/. /home/root/xovi/exthome/; fi && \
rm -rf appload-unz appload.zip`); err != nil {
die("install AppLoad: %v: %s", err, out)
}
ok("AppLoad installed")

// ---- 5. Qt hashtable (best-effort; only UI/QML mods need it) -----------
step("rebuilding the Qt resource hashtable (best-effort)")
if _, err := d.Run("test -x /home/root/xovi/rebuild_hashtable"); err == nil {
if _, err := d.Run("timeout 120 /home/root/xovi/rebuild_hashtable </dev/null 2>/dev/null"); err == nil {
ok("hashtable rebuilt")
} else {
warn("couldn't rebuild non-interactively — AppLoad works without it")
die("install AppLoad shims: %v: %s", err, out)
}
ok("AppLoad installed (3.28-compatible build)")
} else {
ok("not needed by this bundle")
step("downloading AppLoad")
appload, err := fetchURL(apploadURL)
if err != nil {
die("download AppLoad: %v", err)
}
ok("%d KB", len(appload)/1024)
step("installing the AppLoad launcher")
if err := d.Push(appload, "/tmp/appload.zip", "644"); err != nil {
die("%v", err)
}
// ONLY appload.so is a xovi extension → extensions.d/. The zip also
// carries shims/qtfb-shim*.so (runtime pieces for windowed apps) which
// must NOT land in extensions.d/ or xovi tries to load them and errors.
if out, err := d.Run(`cd /tmp && rm -rf appload-unz && mkdir appload-unz && \
(unzip -oq appload.zip -d appload-unz || busybox unzip -o appload.zip -d appload-unz) && \
cp -f appload-unz/appload.so /home/root/xovi/extensions.d/ && \
mkdir -p /home/root/xovi/exthome/appload && \
if [ -d appload-unz/shims ]; then cp -rf appload-unz/shims /home/root/xovi/exthome/appload/; fi && \
if [ -d appload-unz/exthome ]; then cp -rf appload-unz/exthome/. /home/root/xovi/exthome/; fi && \
rm -rf appload-unz appload.zip`); err != nil {
die("install AppLoad: %v: %s", err, out)
}
ok("AppLoad installed")
}

// ---- 5. Qt hashtable (AppLoad hard-crashes xochitl without it on 3.27+) -
step("rebuilding the Qt resource hashtable")
hashtabOK := rebuildHashtab(d)

// ---- 6. tripletap persistence -------------------------------------------
step("installing power-button persistence (xovi-tripletap)")
fmt.Println(" triple-press the power button to toggle xovi — survives reboots,")
Expand All @@ -151,7 +169,10 @@ rm -rf appload-unz appload.zip`); err != nil {

// ---- 8. start xovi now ---------------------------------------------------
step("starting xovi (no reboot needed)")
if out, err := d.Run(`systemctl stop xovi-firststart 2>/dev/null; systemctl reset-failed xovi-firststart 2>/dev/null; \
if !hashtabOK {
warn("skipping xovi start — without the hashtab AppLoad crash-loops xochitl on this OS")
warn("after rebuild: remagic rebuild-hashtab then triple-press power")
} else if out, err := d.Run(`systemctl stop xovi-firststart 2>/dev/null; systemctl reset-failed xovi-firststart 2>/dev/null; \
systemd-run --unit=xovi-firststart --collect --service-type=oneshot /home/root/xovi/start`); err != nil {
warn("couldn't start immediately (%s) — triple-press power, or reboot", tail(out, 120))
} else {
Expand Down
32 changes: 19 additions & 13 deletions scripts/03-install-xovi.sh
Original file line number Diff line number Diff line change
Expand Up @@ -55,22 +55,24 @@ rm_ssh 'cd /tmp && rm -rf appload-unz && mkdir appload-unz && \
|| die "failed to install AppLoad"
ok "AppLoad installed."

# --- 3. rebuild the Qt hashtable (needed by qt-resource-rebuilder) -----------
# --- 3. rebuild the Qt hashtable (required for AppLoad on OS 3.27+) ----------
# qt-resource-rebuilder needs a per-OS-version hashtable, rebuilt on first
# install and after every OS update. The bundle ships upstream's
# `rebuild_hashtable`, which drives xochitl to capture it. AppLoad itself does
# not require it — only UI/QML mods do — so this is best-effort and never fatal.
# install and after every OS update. Upstream's rebuild_hashtable blocks on
# "press enter"; without the hashtab AppLoad crash-loops xochitl on 3.27+.
step "Rebuilding the Qt resource hashtable"
if rm_ssh 'test -x /home/root/xovi/rebuild_hashtable'; then
if rm_ssh '/home/root/xovi/rebuild_hashtable </dev/null' 2>/dev/null; then
HASHTAB_OK=0
if rm_ssh 'test -f /home/root/xovi/extensions.d/qt-resource-rebuilder.so'; then
if rm_scp "$HERE/rebuild-hashtab.sh" "$(rm_dest):/tmp/rebuild-hashtab.sh" \
&& rm_ssh 'bash /tmp/rebuild-hashtab.sh && rm -f /tmp/rebuild-hashtab.sh'; then
ok "Hashtable rebuilt."
HASHTAB_OK=1
else
warn "Couldn't rebuild the hashtable non-interactively."
info "AppLoad still works. For UI/QML mods, run this once by hand:"
info " ssh $(rm_dest) '/home/root/xovi/rebuild_hashtable' then reboot."
warn "Hashtable rebuild failed — AppLoad will crash xochitl without it."
info "Retry from your computer: remagic rebuild-hashtab"
fi
else
info "No rebuild_hashtable in this bundle — skipping (AppLoad doesn't need it)."
info "No qt-resource-rebuilder in this bundle — skipping."
HASHTAB_OK=1
fi

# --- 4. persistence: tripletap (blessed) -------------------------------------
Expand Down Expand Up @@ -102,9 +104,13 @@ fi

# --- 5. start it now ---------------------------------------------------------
step "Starting xovi now (no reboot needed)"
rm_ssh 'systemd-run --unit=xovi-firststart --collect --service-type=oneshot /home/root/xovi/start 2>/dev/null' \
&& ok "xovi started." \
|| warn "Couldn't start immediately; triple-press power, or reboot."
if [ "$HASHTAB_OK" = 1 ]; then
rm_ssh 'systemd-run --unit=xovi-firststart --collect --service-type=oneshot /home/root/xovi/start 2>/dev/null' \
&& ok "xovi started." \
|| warn "Couldn't start immediately; triple-press power, or reboot."
else
warn "Skipping xovi start until the hashtab is rebuilt (remagic rebuild-hashtab)."
fi

cat <<EOF

Expand Down
Loading