Skip to content

Signal K updates do not change the running server when the npm prefix changes #6

Description

@mark-brannan

~/.signalk/signalk-server is the script that signalk.service runs. signalkPostInstall.py writes this script with the npm prefix resolved at that moment. It only writes it when settings.json does not exist, which means only on a first install.

If the npm prefix changes later, every update installs a new signalk-server under the new prefix. The wrapper still points at the old prefix, so the server keeps starting the old copy. The installer prints DONE.

Installing Node from nodejs.org is enough to change the prefix, because it puts node and npm in /usr/local.

https://github.com/openplotter/openplotter-signalk-installer/blob/master/openplotterSignalkInstaller/signalkPostInstall.py#L72-L96

if not os.path.exists(skDir+'/settings.json'):   # true only on a first install
    ...
    node_path = subprocess.check_output(['npm', 'config', 'get', 'prefix'])...
    fo = open(skDir+'/signalk-server', "w")
    fo.write('#!/bin/sh\n'+node_path+'/lib/node_modules/signalk-server/bin/signalk-server ...')

npm install -g signalk-server runs above this, outside the check, so the install always happens. Only the wrapper is skipped.

Reproduction

This runs on Debian bookworm in Docker. A Raspberry Pi is not needed.

docker build -t op-skrepro . && docker run --rm op-skrepro

The script runs signalkPostInstall twice. Between the two runs it installs Node from nodejs.org into /usr/local. Nothing else changes.

RUN 1  wrapper -> /usr/lib/node_modules/signalk-server/bin/signalk-server
RUN 2  wrapper -> /usr/lib/node_modules/signalk-server/bin/signalk-server   (not changed)

       /usr/lib/node_modules/signalk-server:        2.14.4   <- this is what runs
       /usr/local/lib/node_modules/signalk-server:  2.30.0   <- this is what run 2 installed

Both runs printed DONE.

Dockerfile
FROM debian:bookworm
ENV DEBIAN_FRONTEND=noninteractive

RUN apt-get update && apt-get install -y --no-install-recommends \
      ca-certificates curl gnupg xz-utils procps python3 apt-transport-https \
    && rm -rf /var/lib/apt/lists/*

RUN curl -fsSL https://raw.githubusercontent.com/openplotter/openplotter-settings/master/openplotterSettings/data/sources/openplotter.gpg.key \
      | gpg --dearmor > /etc/apt/trusted.gpg.d/openplotter.gpg \
 && echo 'deb https://dl.cloudsmith.io/public/openplotter/openplotter/deb/debian bookworm main' \
      > /etc/apt/sources.list.d/openplotter.list

RUN apt-get update && apt-get install -y --no-install-recommends \
      openplotter-settings openplotter-signalk-installer \
    && rm -rf /var/lib/apt/lists/*

# signalkPostInstall calls systemctl as its first action inside the try block.
# Without a stub it raises FileNotFoundError and the block stops there.
RUN printf '#!/bin/sh\nexit 0\n' > /usr/local/sbin/systemctl && chmod +x /usr/local/sbin/systemctl

# conf.Conf() reads USER, and only reads SUDO_USER when USER is 'root'.
RUN useradd -m -s /bin/bash pi
ENV USER=root
ENV SUDO_USER=pi

COPY repro.sh /repro.sh
RUN chmod +x /repro.sh
ENTRYPOINT ["/repro.sh"]
repro.sh
#!/bin/bash
set -u
banner() { echo; echo "############ $* ############"; echo; }

state() {
  echo "--- npm: $(command -v npm)   prefix: $(npm config get prefix)"
  echo "--- wrapper /home/pi/.signalk/signalk-server:"
  sed 's/^/       /' /home/pi/.signalk/signalk-server 2>/dev/null || echo "       (absent)"
  for p in /usr /usr/local; do
    v=$(python3 -c "import json;print(json.load(open('$p/lib/node_modules/signalk-server/package.json'))['version'])" 2>/dev/null)
    echo "--- $p/lib/node_modules/signalk-server: ${v:-(absent)}"
  done
}

banner "RUN 1 - first install, stock nodesource node"
signalkPostInstall 2>&1 | tail -5
state

banner "Age the install - stand in for a Pi imaged a year ago"
# Run 1 installs today's release. Roll the /usr copy back so the version
# difference is visible. On real hardware this is just what you already had.
# The bug does not depend on this step.
/usr/bin/npm install -g signalk-server@2.14.4 >/dev/null 2>&1

banner "Install Node from nodejs.org into /usr/local"
NODE=v22.14.0
curl -fsSL "https://nodejs.org/dist/$NODE/node-$NODE-linux-arm64.tar.xz" \
  | tar -xJ -C /usr/local --strip-components=1 \
      --exclude=CHANGELOG.md --exclude=LICENSE --exclude=README.md
hash -r

banner "RUN 2 - OpenPlotter 'Update', nothing else changed"
signalkPostInstall 2>&1 | tail -5
state

On x86 change linux-arm64 to linux-x64.

Two limits of this reproduction: systemctl is a stub that exits 0, so it shows the wrapper going stale but not the service starting the old copy. That part is one line in signalk.service: ExecStart=/home/pi/.signalk/signalk-server. And it is Debian bookworm, not Raspberry Pi OS.

How this happened

Two changes that were each correct on their own.

3b7379a (2019) added the settings.json check so that updates stop deleting user config. Before it, every run did rm -rf skDir and wrote everything again, so the wrapper was always current. The wrapper generation ended up inside the block that the check protects.

#1 (2020) replaced the fixed path /usr/lib/node_modules with npm config get prefix. This also went inside the protected block. From then on the prefix is read once and kept forever.

On my own boat

Signal K 2.14.4 was installed in mid-2025 under /usr/lib/node_modules. A later manual Node install moved npm's prefix to /usr/local. Updates after that installed as far as 2.30.0 into /usr/local/lib/node_modules. The server ran 2.14.4 the whole time and every update reported success. I found it only because I went looking at the wrapper file.

Related things I noticed

These are separate from the fix and I have not touched them.

  1. No Signal K server version is displayed anywhere in OpenPlotter. The Installed and Candidate columns in Settings show the version of the openplotter-signalk-installer package, not the server. So there is no screen where this problem could become visible.
  2. The only way to rewrite the wrapper today is Reinstall, which runs rm -rf on ~/.signalk first and removes all plugins, their settings and login credentials. There is no backup step.
  3. os.mkdir(skDir) on line 75 raises an error when ~/.signalk exists but settings.json does not, which happens after a partly failed install. The outer try catches it, so the systemd unit files below are never written and the reason is not printed. This is close to missing error handling #2.
  4. openplotter-settings/openplotterSettings/selectKey.py line 29 has the same fixed /usr/lib/node_modules path, inside except: pass. With a different prefix the Signal K key list is silently empty in every app that uses it.

Happy to open separate issues for any of these if useful.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions