You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
~/.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.
ifnotos.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/bashset -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)"forpin /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.
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.
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.
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.
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.
~/.signalk/signalk-serveris the script thatsignalk.serviceruns.signalkPostInstall.pywrites this script with the npm prefix resolved at that moment. It only writes it whensettings.jsondoes 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
nodeandnpmin/usr/local.https://github.com/openplotter/openplotter-signalk-installer/blob/master/openplotterSignalkInstaller/signalkPostInstall.py#L72-L96
npm install -g signalk-serverruns 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.
The script runs
signalkPostInstalltwice. Between the two runs it installs Node from nodejs.org into/usr/local. Nothing else changes.Both runs printed
DONE.Dockerfile
repro.sh
On x86 change
linux-arm64tolinux-x64.Two limits of this reproduction:
systemctlis 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 insignalk.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.jsoncheck so that updates stop deleting user config. Before it, every run didrm -rf skDirand 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_moduleswithnpm 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.
InstalledandCandidatecolumns in Settings show the version of theopenplotter-signalk-installerpackage, not the server. So there is no screen where this problem could become visible.rm -rfon~/.signalkfirst and removes all plugins, their settings and login credentials. There is no backup step.os.mkdir(skDir)on line 75 raises an error when~/.signalkexists butsettings.jsondoes not, which happens after a partly failed install. The outertrycatches it, so the systemd unit files below are never written and the reason is not printed. This is close to missing error handling #2.openplotter-settings/openplotterSettings/selectKey.pyline 29 has the same fixed/usr/lib/node_modulespath, insideexcept: 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.