A method to install jtop on Thor without --break-system-packages

An updated version that installs jtop on Thor without having to “install jetson-stats --break-system-packages”. It now includes fixes to some of the python code to more accurately reflect values of Thor in jtop. Here’s two bash shell scripts that will clone github.com/rbonghi/jetson_stats make modifications to enable JTOP to be run with ‘sudo jtop’ on Jetson Thor and modify the python files to allow jtop to report on Thor. The idea to use NVML to access Thor came from eous’ pr github.com/rbonghi/jetson_stats/pull/698 that I saw after I had done my pr 699 yesterday.

This is the updated setup_Jtop_thor.sh.txt (5.7 KB) that now also calls

patch_thor_jp7_in_repo.sh.txt (13.4 KB)
Save the two files .txt files into the same directory on your Thor as .sh then

chmod +x *.sh
sudo ./setup_Jtop_thor.sh

Then run jtop normally

sudo jtop

The content of the files are:

cat setup_Jtop_thor.sh 

#!/usr/bin/env bash
# Setup jtop in a root-owned venv at /opt/jtop with Thor edits applied
# Tested on Ubuntu 24.04 (JetPack 7.x base)

set -euo pipefail

JTOP_ROOT="/opt/jtop"
VENV_DIR="$JTOP_ROOT/venv"
REPO_DIR="$JTOP_ROOT/jetson_stats"
SERVICE_PATH="/etc/systemd/system/jtop.service"
REPO_URL="https://github.com/rbonghi/jetson_stats.git"

need_root() {
  if [[ "$(id -u)" != "0" ]]; then
    echo "Please run as root (e.g., sudo bash $0)"
    exit 1
  fi
}

log() { echo -e "\e[1;32m==>\e[0m $*"; }
warn() { echo -e "\e[1;33m[warn]\e[0m $*"; }
info() { echo -e "\e[1;34m[info]\e[0m $*"; }

install_prereqs() {
  log "Installing prerequisites (python3.12-venv, git)…"
  apt-get update -y
  # Prefer python3.12-venv (Noble default); fallback to python3-venv if needed
  if ! apt-get install -y python3.12-venv git; then
    warn "python3.12-venv not found; trying python3-venv"
    apt-get install -y python3-venv git
  fi
}

create_venv() {
  log "Creating root-owned venv at $VENV_DIR"
  mkdir -p "$JTOP_ROOT"
  if [[ ! -d "$VENV_DIR" ]]; then
    python3 -m venv "$VENV_DIR"
  else
    info "Venv already exists: $VENV_DIR"
  fi
  "$VENV_DIR/bin/python" -m pip install --upgrade pip setuptools wheel
}

clone_repo() {
  log "Cloning jetson_stats into $REPO_DIR"
  if [[ -d "$REPO_DIR/.git" ]]; then
    info "Repo exists, pulling latest…"
    git -C "$REPO_DIR" fetch --all --tags
    git -C "$REPO_DIR" reset --hard origin/master
  else
    git clone --depth=1 "$REPO_URL" "$REPO_DIR"
  fi
}

apply_thor_edits() {
  log "Applying Thor edits to repository files"
  local f_vars="$REPO_DIR/jtop/core/jetson_variables.py"
  local f_gh="$REPO_DIR/jtop/github.py"
  local f_svc="$REPO_DIR/services/jtop.service"

  for f in "$f_vars" "$f_gh" "$f_svc"; do
    [[ -f "$f" ]] || { echo "ERROR: Missing $f"; exit 1; }
    cp -a "$f" "$f.bak.$(date +%Y%m%d-%H%M%S)"
  done

  # jetson_variables.py edits
  # Add JP/L4T mappings
  grep -qE '^\s*"38\.2\.0":\s*"7\.0",' "$f_vars" || \
    sed -i '/# -------- JP6 --------/a\    "38.2.0": "7.0",' "$f_vars"
  grep -qE '^\s*"36\.4\.4":\s*"6\.2\.1",' "$f_vars" || \
    sed -i '/# -------- JP6 --------/a\    "36.4.4": "6.2.1",' "$f_vars"

  # Add CUDA_TABLE tegra264
  grep -qE "^\s*'tegra264':\s*'13\.0'," "$f_vars" || \
    sed -i "/'tegra234':/i\    'tegra264': '13.0', # JETSON THOR - tegra264" "$f_vars"

  # Add MODULE_NAME_TABLE p3834-0008
  grep -qE "^\s*'p3834-0008':\s*'NVIDIA Jetson AGX Thor  \(Developer kit\)'," "$f_vars" || \
    sed -i "/'p3767-0005':/i\    'p3834-0008': 'NVIDIA Jetson AGX Thor  (Developer kit)'," "$f_vars"

  # services/jtop.service in repo: ensure only venv ExecStart
  # Replace /usr/local/bin path with venv path if present
  if grep -q '^ExecStart=/usr/local/bin/jtop --force$' "$f_svc"; then
    sed -i 's#^ExecStart=/usr/local/bin/jtop --force#ExecStart=/opt/jtop/venv/bin/jtop --force#' "$f_svc"
  fi
  # Remove any duplicate /usr/local/bin ExecStart lines
  sed -i '/^ExecStart=\/usr\/local\/bin\/jtop --force$/d' "$f_svc"
  # Ensure one venv ExecStart exists after Environment= line
  grep -q '^ExecStart=/opt/jtop/venv/bin/jtop --force$' "$f_svc" || \
    sed -i '/^Environment="JTOP_SERVICE=True"$/a\ExecStart=/opt/jtop/venv/bin/jtop --force' "$f_svc"

  log "Thor edits applied."
}

install_python_fixes() {
  set -euo pipefail
  local repo="/opt/jtop/jetson_stats"
  local patch="$(dirname "$0")/patch_thor_jp7_in_repo.sh"  # patch in same dir as instant one.

  log "Installing python script changes for Jetson Thor (JP7.0) in: $repo"

  [[ -d "$repo/.git" && -d "$repo/jtop" ]] || {
    echo "ERROR: $repo does not look like a jetson_stats clone"; return 1; }

  chmod +x "$patch"
  # Run as its own process; it will sudo itself if needed.
  "$patch" "$repo"
}

install_package() {
  log "Installing jetson_stats into the venv"
  "$VENV_DIR/bin/python" -m pip install --no-cache-dir -U "$REPO_DIR"
}

install_nvml() {
  set -euo pipefail
  log "Installing NVML bindings (pynvml) into the venv"

  : "${VENV_DIR:?VENV_DIR must be set (e.g. /opt/jtop/venv)}"
  local py="$VENV_DIR/bin/python"
  if [[ ! -x "$py" ]]; then
    echo "ERROR: $py not found/executable" >&2
    return 1
  fi

  "$py" -m pip install -U nvidia-ml-py3
}

install_service() {
  log "Installing systemd service at $SERVICE_PATH"
  cat >"$SERVICE_PATH" <<'EOF'
# Jetson Stats (jtop) systemd unit using root-owned venv at /opt/jtop/venv
[Unit]
Description=Jetson Stats (jtop)
After=network.target multi-user.target

[Service]
Environment="JTOP_SERVICE=True"
Type=simple
ExecStart=/opt/jtop/venv/bin/jtop --force
Restart=on-failure
RestartSec=10s
TimeoutStartSec=30s
TimeoutStopSec=30s

[Install]
WantedBy=multi-user.target
EOF

  # Wrapper so `sudo jtop` works nicely from PATH
  log "Installing /usr/local/bin/jtop wrapper"
  install -m 0755 -o root -g root /dev/stdin /usr/local/bin/jtop <<'EOF'
#!/bin/sh
exec /opt/jtop/venv/bin/jtop "$@"
EOF

  # Create system group (ok if it already exists)
  groupadd --system jtop 2>/dev/null || true

  log "Reloading/Enabling/Restarting service"
  systemctl daemon-reload
  systemctl enable jtop.service
  systemctl restart jtop.service || true
  systemctl is-active --quiet jtop.service && log "jtop.service is active" || warn "jtop.service not active yet"
}

show_summary() {
  echo
  log "Done."
  echo "Quick checks:"
  echo "  systemctl status jtop.service --no-pager"
  echo "  journalctl -u jtop --no-pager -e"
  echo
  echo "Run interactively with:"
  echo "  sudo jtop"
}

main() {
  need_root
  install_prereqs
  create_venv
  clone_repo
  apply_thor_edits
  install_python_fixes
  install_package
  install_nvml
  install_service
  show_summary
}

main "$@"

#!/usr/bin/env bash
# patch_thor_jp7_in_repo.sh
# Patch a cloned jetson_stats repo for Jetson Thor on JetPack 7.0
# - Writes drop-in GPU shim (NVML-first, devfreq fallback) compatible with legacy GUI/service
# - Updates jetson_variables.py for Thor/JP7 mappings
# - Tweaks jtop/github.py message
# - Don't run script independently. It is called from setup_Jtop_thor.sh
#
set -euo pipefail

if [[ ${EUID:-$(id -u)} -ne 0 ]]; then
  exec sudo -E bash "$0" "$@"
fi

REPO_ROOT="${1:-}"
if [[ -z "$REPO_ROOT" ]]; then
  echo "ERROR: pass path to your cloned jetson_stats repo (e.g., /opt/jtop/jetson_stats)" >&2
  exit 1
fi
REPO_ROOT="${REPO_ROOT%/}"
[[ -d "$REPO_ROOT/jtop" ]] || { echo "ERROR: $REPO_ROOT does not look like a jetson_stats repo"; exit 1; }

ts() { date +%Y%m%d-%H%M%S; }
bk() { local f="$1"; [[ -f "$f" ]] && cp -a "$f" "$f.bak.$(ts)" || true; }

echo "==> Patching repo at: $REPO_ROOT"

# 1) Write drop-in GPU shim
GPU_PY="$REPO_ROOT/jtop/core/gpu.py"
mkdir -p "$(dirname "$GPU_PY")"
bk "$GPU_PY"

cat >"$GPU_PY" <<'PY'
# SPDX-License-Identifier: AGPL-3.0
# GPU shim for Jetson Thor / JetPack 7:
# - Prefer NVML (pynvml) for detection/metrics
# - Thanks to eous' jetson_stats pr that pointed the way to nvidia-ml-py3 (pynvml)
# - Fallback to /sys/class/devfreq (gpu-gpc-0)
# - Exposes legacy hooks/shape used by jtop service & GUI
from __future__ import annotations
import os, re, glob

try:
    from .exceptions import JtopException  # type: ignore
except Exception:  # pragma: no cover
    class JtopException(Exception): pass

def _read_int(p: str, d=None):
    try:
        with open(p) as f: return int(f.read().strip())
    except Exception:
        return d
def _read_str(p: str, d=None):
    try:
        with open(p) as f: return f.read().strip()
    except Exception:
        return d
def _exists(p: str) -> bool:
    try: return os.path.exists(p)
    except Exception: return False

def _compat_find_3d_scaling(sysfs_path: str | None) -> bool:
    try:
        if not sysfs_path: return False
        p = os.path.join(sysfs_path, "device")
        for _ in range(6):
            cand = os.path.join(p, "enable_3d_scaling")
            try:
                with open(cand) as f:
                    v = f.read().strip()
                return v in ("1","Y","y","true","True")
            except Exception:
                p = os.path.dirname(p)
        return False
    except Exception:
        return False

def _compat_find_railgate(sysfs_path: str | None) -> bool:
    try:
        if not sysfs_path: return False
        p = os.path.join(sysfs_path, "device")
        candidates = ("railgate_enable", "railgate", os.path.join("power","railgate"), os.path.join("power","railgate_enable"))
        for _ in range(6):
            for rel in candidates:
                cand = os.path.join(p, rel)
                try:
                    with open(cand) as f:
                        v = f.read().strip()
                    return v in ("1","Y","y","true","True")
                except Exception:
                    pass
            p = os.path.dirname(p)
        return False
    except Exception:
        return False

def _i(x):
    try:
        return 0 if x is None else int(x)
    except Exception:
        return 0

def _build_entry(flat: dict) -> dict:
    """Return nested entry + top-level aliases expected by GUI."""
    t = flat.get("type") or "integrated"
    cur = _i(flat.get("cur_freq"))
    mx  = _i(flat.get("max_freq"))
    mn  = _i(flat.get("min_freq"))
    gov = flat.get("governor")
    online = bool(flat.get("online"))
    load = _i(flat.get("load"))

    scaling3d = _compat_find_3d_scaling(flat.get("sysfs_path"))
    railgate  = _compat_find_railgate(flat.get("sysfs_path"))

    nested = {
        "type": t,
        "status": {
            "online": online,
            "load": load,
            "3d_scaling": scaling3d,
            "railgate": railgate,
            "freq": {"cur": cur, "max": mx, "min": mn},
            "cur_freq": cur,
            "max_freq": mx,
            "min_freq": mn,
            "governor": gov,
        },
        "info": {
            "name":    flat.get("name"),
            "product": flat.get("product"),
            "memory_total": flat.get("memory_total"),
            "memory_used":  flat.get("memory_used"),
            "source":  flat.get("source"),
            "sysfs_path": flat.get("sysfs_path"),
        },
    }
    # Top-level aliases used by older GUI code
    nested["freq"]        = nested["status"]["freq"]
    nested["online"]      = online
    nested["load"]        = load
    nested["cur_freq"]    = cur
    nested["max_freq"]    = mx
    nested["min_freq"]    = mn
    nested["governor"]    = gov
    nested["3d_scaling"]  = scaling3d
    nested["railgate"]    = railgate
    scaling_str  = "Active" if scaling3d else "Disable"
    railgate_str = "Active" if railgate else "Disable"
    nested["power_control"] = f"3D-scaling: {scaling_str} | Railgate: {railgate_str}"
    nested["name"]        = nested["info"]["name"]
    nested["product"]     = nested["info"]["product"]
    return nested

class GPU(dict):
    """NVML-first, devfreq-fallback GPU map with legacy GUI/service compatibility."""
    def __init__(self):
        super().__init__()
        self._nvml_ok = False
        self._nvml = None
        try:
            import pynvml as _nvml
            _nvml.nvmlInit()
            self._nvml = _nvml
            self._nvml_ok = True
        except Exception:
            self._nvml = None
            self._nvml_ok = False
        self.update()

    # discovery paths
    def _read_nvml(self) -> dict:
        nv = self._nvml
        if not nv: return {}
        out = {}
        try:
            count = nv.nvmlDeviceGetCount()
        except Exception:
            return {}
        for idx in range(count):
            try:
                h = nv.nvmlDeviceGetHandleByIndex(idx)
                pname = nv.nvmlDeviceGetName(h)
                if isinstance(pname, bytes): pname = pname.decode(errors="ignore")
                try:
                    util = nv.nvmlDeviceGetUtilizationRates(h).gpu
                except Exception:
                    util = None
                def _clk(t):
                    try: return int(nv.nvmlDeviceGetClockInfo(h, t)) * 1_000_000
                    except Exception: return None
                cur = _clk(nv.NVML_CLOCK_GRAPHICS)
                try:
                    mx = int(nv.nvmlDeviceGetMaxClockInfo(h, nv.NVML_CLOCK_GRAPHICS)) * 1_000_000
                except Exception:
                    mx = None
                try:
                    mem = nv.nvmlDeviceGetMemoryInfo(h)
                    mt, mu = int(mem.total), int(mem.used)
                except Exception:
                    mt = mu = None
                key = f"gpu{idx}"
                out[key] = {
                    "name": key,
                    "product": pname,
                    "source": "nvml",
                    "online": True,
                    "load": 0 if util is None else int(util),
                    "cur_freq": cur,
                    "max_freq": mx,
                    "min_freq": None,
                    "governor": None,
                    "sysfs_path": None,
                    "memory_total": mt,
                    "memory_used":  mu,
                    "type": "integrated",
                }
            except Exception:
                continue
        return out

    def _read_sysfs(self) -> dict:
        base = "/sys/class/devfreq"
        if not _exists(base): return {}
        nodes = []
        for d in glob.glob(os.path.join(base, "*")):
            name = None
            npath = os.path.join(d, "device", "of_node", "name")
            if _exists(npath): name = _read_str(npath, None)
            if not name: name = os.path.basename(d)
            if (re.match(r"^g[a-z0-9]+b$", name or "") is not None) or name == "gpu" or (name or "").startswith("gpu-"):
                nodes.append((name, d))
        out = {}
        for idx, (name, path) in enumerate(nodes):
            key = name if name else f"gpu{idx}"
            out[key] = {
                "name": key,
                "product": None,
                "source": "sysfs",
                "online": True,
                "load": _read_int(os.path.join(path, "load")) or 0,
                "cur_freq": _read_int(os.path.join(path, "cur_freq")),
                "max_freq": _read_int(os.path.join(path, "max_freq")),
                "min_freq": _read_int(os.path.join(path, "min_freq")),
                "governor": _read_str(os.path.join(path, "governor")),
                "sysfs_path": path,
                "memory_total": None,
                "memory_used":  None,
                "type": "integrated",
            }
        return out

    # public / legacy API
    def update(self):
        try:
            flat = self._read_nvml() if self._nvml_ok else {}
            if not flat:
                flat = self._read_sysfs()
        except Exception:
            flat = {}
        try:
            self.clear()
            for k, v in (flat or {}).items():
                self[k] = _build_entry(v if isinstance(v, dict) else {})
        except Exception:
            pass

    refresh = update

    def get_status(self) -> dict:
        """Return nested snapshot (GUI/service expected shape)."""
        try:
            self.update()
        except Exception:
            pass
        return {k: dict(v) if isinstance(v, dict) else v for k, v in self.items()}

    # Legacy hooks
    def _initialize(self, controller):  # jtop.py expects this
        try: self._controller = controller
        except Exception: pass
        try: self.update()
        except Exception: pass

    def _finalize(self):                # jtop.py expects this
        return None

    def _update(self, dest: dict):      # jtop.py expects this
        snap = {}
        try: snap = self.get_status()
        except Exception: pass
        if isinstance(dest, dict):
            dest.clear()
            dest.update(snap or {})
        return dest

# Legacy alias for service.py
class GPUService(GPU):
    """Compatibility alias with same behavior as GPU."""
    pass

# Legacy helper for github.py diagnostics
def get_raw_igpu_devices():
    devices = []
    base = "/sys/class/devfreq"
    if os.path.isdir(base):
        for d in sorted(glob.glob(os.path.join(base, "*"))):
            name_path = os.path.join(d, "device", "of_node", "name")
            name = _read_str(name_path, None) or os.path.basename(d)
            if (re.match(r"^g[a-z0-9]+b$", name or "") is not None) or name == "gpu" or (name or "").startswith("gpu-"):
                devices.append(d)
    try:
        import pynvml as nv
        nv.nvmlInit()
        try:
            count = nv.nvmlDeviceGetCount()
            for i in range(count):
                devices.append(f"nvml:gpu{i}")
        finally:
            try: nv.nvmlShutdown()
            except Exception: pass
    except Exception:
        pass
    return devices
PY

echo "   wrote $GPU_PY"

# 2) Patch jetson_variables.py for (Thor & JP7)
VARS_PY="$REPO_ROOT/jtop/core/jetson_variables.py"
if [[ -f "$VARS_PY" ]]; then
  bk "$VARS_PY"
  # JP/L4T->JetPack mappings
  grep -qE '^\s*"38\.2\.0":\s*"7\.0",' "$VARS_PY" || \
    sed -i '/# -------- JP6 --------/a\    "38.2.0": "7.0",' "$VARS_PY"
  grep -qE '^\s*"36\.4\.4":\s*"6\.2\.1",' "$VARS_PY" || \
    sed -i '/# -------- JP6 --------/a\    "36.4.4": "6.2.1",' "$VARS_PY"
  # CUDA table for Thor
  grep -qE "^\s*'tegra264':\s*'13\.0'," "$VARS_PY" || \
    sed -i "/'tegra234':/i\    'tegra264': '13.0', # JETSON THOR - tegra264" "$VARS_PY"
  # Module name table
  grep -qE "^\s*'p3834-0008':\s*'NVIDIA Jetson AGX Thor  \(Developer kit\)'," "$VARS_PY" || \
    sed -i "/'p3767-0005':/i\    'p3834-0008': 'NVIDIA Jetson AGX Thor  (Developer kit)'," "$VARS_PY"
  echo "   patched $VARS_PY"
else
  echo "   skip: $VARS_PY not found"
fi

# 3) Patch jtop/github.py (insert warning line once)
GH_PY="$REPO_ROOT/jtop/github.py"
if [[ -f "$GH_PY" ]]; then
  bk "$GH_PY"
  if ! grep -q 'rollback jtop to not functioning on Thor' "$GH_PY"; then
    python3 - "$GH_PY" <<'PY'
import re, sys
p=sys.argv[1]
with open(p, 'r', encoding='utf-8') as f:
    s=f.read()

# Try the common anchor first (exact format call), then a looser fallback
anchor1 = r'(print\(\s*"\[\{status\}\]\s\{message\}"\.format\(status=bcolors\.warning\(\),\s*message=message\)\s*\)\s*\n)'
anchor2 = r'(print\(\s*"\[\{status\}\]\s\{message\}"\.format\([^)]*\)\s*\)\s*\n)'
insert  = '    print("  For now, ignore the next 2 lines or it will rollback jtop to not functioning on Thor".format(bold=bcolors.BOLD, reset=bcolors.ENDC))\n'

s2, n = re.subn(anchor1, r'\1' + insert, s, count=1)
if n == 0:
    s2, n = re.subn(anchor2, r'\1' + insert, s, count=1)

if n:
    with open(p, 'w', encoding='utf-8') as f:
        f.write(s2)
    print(f"   patched {p} (inserted 1)")
else:
    sys.stderr.write(f"WARNING: Could not find anchor in {p}; skipping insert.\n")
PY
  else
    echo "   note already present in $GH_PY"
  fi
else
  echo "   skip: $GH_PY not found"
fi

echo "==> Thor/JP7 patching complete."
echo "pip will now install this jetson_stats repo, and 'nvidia-ml-py3*.whl' into the jtop venv."

Hi,
Many thanks for the sharing

May the Lord bless you and keep you.

I didn’t really understand it all but YOLO. Worked like a charm.

HI,

Thanks for sharing this. I followed the steps and was able to run sudo jtop. But after few minutes I got error saying - “Lost connection with jtop server“ .

After that I tried to run the script again, removed jtop, etc but everytime I run “sudo jtop“, it says - “The jtop.service is not active. Please run:
sudo systemctl restart jtop.service“

After running this command, nothing happens.

Any idea how to fix?

try:

sudo systemctl enable jtop.service

sudo systemctl restart jtop.service

Hello,

I’m so proud to have been able to acquire an AGX Thor that I’m participating in the forum. Like you, I had some trouble installing JTOP. Below, you’ll find a complete script that addresses the various issues:
“PR #698
“systemd”
“NVML JP7 fix #698
“master branch”.

Step 5: Enter your GIT email address in the script :

#!/usr/bin/env bash
set -euo pipefail

say() { printf "\e[1;32m%s\e[0m\n" "$*"; }
warn(){ printf "\e[1;33m%s\e[0m\n" "$*"; }
err() { printf "\e[1;31m%s\e[0m\n" "$*" >&2; }

# Détecte l'utilisateur appelant (utile pour l'ajouter à un groupe si besoin)
CALLER="${SUDO_USER:-$USER}"

say "[1/9] Nettoyage de l'ancien jtop/service…"
sudo systemctl stop jtop 2>/dev/null || true
sudo systemctl disable jtop 2>/dev/null || true
sudo rm -f  /usr/local/bin/jtop
sudo rm -rf /etc/jtop /var/log/jtop
sudo rm -rf ~/.local/lib/python*/site-packages/jtop* ~/.local/lib/python*/site-packages/jetson_stats* 2>/dev/null || true

say "[2/9] Réinitialisation du dossier /opt/jtop…"
sudo rm -rf /opt/jtop
sudo mkdir -p /opt/jtop
sudo chown root:root /opt/jtop

say "[3/9] Création du venv…"
cd /opt/jtop
sudo python3 -m venv venv
sudo /opt/jtop/venv/bin/pip install --upgrade pip setuptools wheel

say "[4/9] Clonage jetson_stats…"
sudo git clone https://github.com/rbonghi/jetson_stats.git
cd /opt/jtop/jetson_stats

say "[5/9] Préparation des branches (master + PR #698)…"
# On s'assure d'être sur master à jour
sudo git fetch origin
sudo git checkout master
sudo git reset --hard origin/master

# Config git locale pour permettre le merge
sudo git config user.name  "zzzzzz"
sudo git config user.email "xxxxxx@xxxxx.yyy"

# On récupère la PR 698 dans une branche locale
sudo git fetch origin pull/698/head:pr-698

say "   → Fusion master + pr-698…"
if ! sudo git merge --no-edit -X theirs pr-698; then
  err "   Échec de fusion automatique."
  exit 1
fi

say "[6/9] Installation des dépendances NVML + paquet en mode editable…"
# Installe les deux variantes NVML pour compat
sudo /opt/jtop/venv/bin/pip install --upgrade nvidia-ml-py nvidia-ml-py3
sudo /opt/jtop/venv/bin/pip install -e .

say "[7/9] Wrapper /usr/local/bin/jtop…"
sudo bash -lc 'cat > /usr/local/bin/jtop <<EOS
#!/usr/bin/env bash
exec /opt/jtop/venv/bin/jtop "\$@"
EOS'
sudo chmod +x /usr/local/bin/jtop

say "[8/9] Service systemd jtop.service (daemon via binaire jtop)…"
# Service: on lance le binaire jtop en mode service.
# Le client `jtop` se connectera au socket créé par le daemon.
sudo tee /etc/systemd/system/jtop.service >/dev/null <<'UNIT'
[Unit]
Description=Jetson Stats (jtop) - Thor + NVML
After=network.target multi-user.target

[Service]
Type=simple
Environment=JTOP_SERVICE=True
# lance le binaire jtop en tant que daemon
ExecStart=/opt/jtop/venv/bin/jtop --force
Restart=on-failure
RestartSec=2s
TimeoutStartSec=30s
TimeoutStopSec=30s
StandardOutput=journal
StandardError=journal
WorkingDirectory=/opt/jtop/jetson_stats
# UMask stricte (lecture/écriture root) 
UMask=007

[Install]
WantedBy=multi-user.target
UNIT

sudo systemctl daemon-reload
sudo systemctl enable --now jtop.service || true

say "[9/9] Vérifications…"
# Test NVML simple (Py3.12: pas de .decode())
sudo /opt/jtop/venv/bin/python - <<'PY'
import sys
try:
    import pynvml as n
    n.nvmlInit()
    print("NVML OK - Driver:", n.nvmlSystemGetDriverVersion())
    cnt = n.nvmlDeviceGetCount()
    print("NVML GPUs:", cnt)
    for i in range(cnt):
        h = n.nvmlDeviceGetHandleByIndex(i)
        print(f" - GPU{i}:", n.nvmlDeviceGetName(h))
except Exception as e:
    print("NVML KO:", e, file=sys.stderr)
    sys.exit(1)
PY

# Statut du service
if ! systemctl is-active --quiet jtop; then
  warn "Le service jtop n'est pas actif. Logs récents :"
  sudo journalctl -u jtop -n 80 --no-pager || true
  err  "Corrige l'erreur ci-dessus puis relance: sudo systemctl restart jtop"
  exit 1
fi

say ""
say "Installation OK."
say "   Service actif : sudo systemctl status jtop --no-pager"
say "   Logs          : sudo journalctl -u jtop -e --no-pager"
say "   Client (daemon) : sudo jtop"
say ""
say "Accès sans sudo (optionnel) :"
say "   sudo groupadd --system jtop 2>/dev/null || true"
say "   sudo usermod -a -G jtop ${CALLER}"
say "   # puis déconnexion/reconnexion. Selon les permissions du socket,"
say "   # Tu pourra ajuster UMask= ou ajouter un ExecStartPost= pour chgrp/chmod."



A big thank you to @eousphoros .

Thanks @whitesscott . I was able to run after rebooting.

when I do import jtop , it gives error saying ModuleNotFoundError: No module named ‘jtop’.

Even though I have followed above process and able to run the sudo jtop and able to view everything. I want to print GPU utilization using python script.

Use this python which uses the venv where jtop is.

/opt/jtop/venv/bin/python

Thanks @whitesscott . It worked.

Thanks for sharing!

Updated one line both scripts to have jtop show

Jetpack 7.0 [L4T 38.2.1] or Jetpack 7.0 [L4T 38.2.0]

setup_Jtop_thor.sh.txt (5.7 KB)

patch_thor_jp7_in_repo.sh.txt (13.1 KB)

thanks whitesscott and for your contributions also. We are planning to release on pypi new version