Hosting·5 min lezen

node: command not found in een geplande taak, terwijl het in de terminal werkt

Je script werkt als je het zelf start en doet niets zodra een timer of cronjob het start. Dat komt bijna altijd door de PATH. Zo los je het op, en zo zorg je dat het de volgende keer niet stil misgaat.

Mijn geplande taak draait wel, maar het script doet niets. Als ik hem zelf
in de terminal start, werkt hij gewoon. Zoek uit waarom, kijk naar de
omgeving waarin de taak draait en naar de exitcode. Nog niets wijzigen.

Dit is een van die fouten die je een uur kost en daarna nooit meer. Je hebt een script dat werkt. Je zet het in een timer of in een cronjob. Het draait, het eindigt, en er gebeurt niets. Start je het zelf in de terminal, dan werkt het perfect.

Negen van de tien keer staat in het logboek één regel: node: command not found. En die regel klopt, hoe vreemd dat ook lijkt.

Waarom het verschil er is

Een interactieve shell laadt je profiel. Daarin staan regels die extra mappen aan je PATH toevoegen: de map van een versiebeheerder voor Node, een eigen bin-map, een map met tools. Daarom vindt jouw terminal node en npm.

Een geplande taak laadt dat profiel niet. Een systemd-unit en een cronjob starten je script in een hele kale omgeving, met een korte PATH en zonder jouw instellingen. Alles wat je in je profiel hebt geregeld, bestaat daar niet.

Gebruik je Node via een versiebeheerder, dan staat de echte node ergens onder je eigen home, in een map met het versienummer erin. Die map komt alleen in je PATH omdat je profiel hem er bij elke login in zet. In een timer gebeurt dat niet, dus node bestaat daar simpelweg niet.

Hetzelfde geldt voor een PHP-versie die je zelf hebt gekozen, voor wp en voor elk hulpprogramma dat je ooit met de hand hebt neergezet.

Twee manieren om het goed te doen

Een absoluut pad in de taak. De eenvoudigste en de meest voorspelbare. Je zet in de unit of in de cronregel het volledige pad naar de uitvoerbare file, niet alleen de naam. Dan maakt het niet uit wat de PATH doet.

Nadeel: bij een nieuwe Node-versie staat er een pad naar een versie die er niet meer is. Dat is geen ramp, want je krijgt een duidelijke fout, maar je moet er wel aan denken.

Het pad in het script opzoeken. Iets meer werk, en een stuk duurzamer. Je laat het script zelf uitzoeken welke node het moet gebruiken: eerst kijken of node in de PATH staat, en zo niet, de hoogste versie in de map van je versiebeheerder pakken:

NODE=$(command -v node || ls -1d "$HOME"/.nvm/versions/node/*/bin/node 2>/dev/null | sort -V | tail -1)
[ -x "$NODE" ] || { echo "geen node gevonden" >&2; exit 1; }
"$NODE" mijn-script.js

Die laatste regel met sort -V sorteert op versienummer en niet op tekst, zodat versie 20 na versie 9 komt en niet ervoor. En die controle met -x is belangrijker dan hij lijkt. Zonder die regel gaat het script gewoon door met een lege variabele, en dan krijg je een foutmelding die niets met Node te maken heeft.

Dezelfde aanpak werkt voor elk programma dat uit je profiel komt.

De fout achter de fout: stil mislukken

Dit is de les die ons meer tijd kostte dan de PATH zelf. We hadden een controle die elk uur draaide, met een wrapper eromheen die de uitkomst beoordeelde. Die wrapper keek naar de exitcodes die hij kende: nul was goed, één was een waarschuwing, twee was een probleem.

Toen het script op node: command not found strandde, kwam er exitcode 127 uit. Die stond niet in de lijst. De wrapper viel door naar het geval "niets aan de hand" en meldde niets. De controle lag dagen stil zonder dat iemand het zag.

De regel die we daaruit meenamen: elke onverwachte exitcode moet zelf een melding geven. Niet alleen de codes die je hebt bedacht.

"$NODE" mijn-script.js
rc=$?
case $rc in
  0) ;;
  1) melden "waarschuwing" ;;
  2) melden "probleem" ;;
  *) melden "onverwachte exitcode $rc, controle is NIET gelopen" ;;
esac

Een controle die kan mislukken zonder iets te zeggen, is geen controle. Dat is een gevoel van veiligheid.

Hoe je het in twee minuten vindt

Laat de taak één keer draaien en kijk naar drie dingen: de exitcode, de PATH waarin hij draaide en de laatste regels van het logboek. Code 127 betekent bijna altijd "programma niet gevonden". Code 126 betekent "gevonden, maar niet uitvoerbaar", vaak een rechtenprobleem.

De vergelijking die het bewijst: laat je script de PATH en de gevonden node afdrukken, één keer als jij hem start en één keer als de taak hem start. Zie je in de tweede uitvoer een korte PATH zonder jouw eigen mappen, dan weet je het.

Dit is precies werk dat je kunt opdragen in plaats van zelf uit te zoeken:

Laat deze geplande taak nu één keer draaien, toon de exitcode, de PATH waarin
hij liep en de laatste twintig regels uitvoer. Vergelijk dat met wat ik in de
terminal krijg en zeg welk verschil het probleem veroorzaakt.

Veelgemaakte fouten

De PATH uitbreiden in de unit of in de cronregel. Dat werkt, maar je plakt daarmee een stuk van je profiel in je taakconfiguratie. Bij de volgende versiewissel klopt het weer niet. Een absoluut pad of een resolve in het script blijft houdbaar.

Je profiel laten laden in de taak. Het lijkt verleidelijk om je script met een login-shell te starten zodat alles er is. Je krijgt dan wel je PATH, maar ook alles wat er verder in je profiel gebeurt, en je taak gaat afhangen van een bestand dat je voor interactief gebruik aanpast. Houd de omgeving van een taak klein en expliciet.

Testen door het script zelf te starten. Daarmee test je precies de ene situatie die altijd werkte. Test door de taak te starten, niet het script.

Aannemen dat een draaiende taak ook een geslaagde taak is. Een taak die elke keer netjes start en stopt, kan elke keer op dezelfde regel sneuvelen. Kijk naar de exitcode en naar wat het script oplevert, niet naar of hij gelopen heeft.

Alleen de bekende exitcodes afhandelen. Zie de vorige paragraaf. Dit is de fout die je pas ontdekt als je hem nodig had.

MK
Maarten Keizer

Oprichter van Invoker. Ruim twintig jaar hosting, systeembeheer en webdevelopment; bouwt de Claude-hosting zelf en test alles eerst op de eigen servers.

over maarten