SSD Nodes Learn 🎉 VPS من $5.50/شهر
الأدلة Matt Connorبقلم Matt Connor

كيف تُبقي أمراً يعمل بعد انقطاع SSH؟

عند انقطاع SSH تُرسل النواة SIGHUP فيتوقف الأمر. تعرّف إلى الفرق العملي بين nohup وdisown وtmux وsystemd-run واختر الأنسب لمهمتك.

لماذا يتوقف الأمر عند انقطاع اتصال SSH

لإبقاء أمر قيد التشغيل بعد انقطاع اتصال SSH، يجب أن ينتهي الأمر في موضع لا تصل إليه إشارة قطع الاتصال. كل طريقة أدناه ترتّب ذلك بطريقة مختلفة، لذا ابدأ بفهم الآلية.

تعمل جلسة تسجيل الدخول على pty (طرفية زائفة)، وهي جهاز طرفية افتراضي ينشئه sshd على الخادم لجلسة عملك. وهي الطرفية المتحكِّمة في shell وفي كل أمر تشغّله من ذلك shell. إذا أردت معرفة بقية هذا المسار، يشرح ما الذي يهيّئه SSH عند تسجيل الدخول ذلك بالتفصيل. عند انقطاع اتصال TCP، يغلق sshd طرفه وتُدمَّر pty. تتعامل النواة مع ذلك على أنه قطع اتصال الطرفية، فترسل SIGHUP إلى مجموعة العمليات الأمامية لتلك الطرفية وإلى قائد الجلسة، وهو shell الخاص بك. الإجراء الافتراضي للإشارة SIGHUP هو إنهاء العملية. كان أمرك ضمن مجموعة العمليات الأمامية، لذلك يتوقف الأمر.

ولا تكون المهام التي تعمل في الخلفية آمنة أيضاً. تعمل المهمة التي تبدأ باستخدام & ضمن مجموعة عمليات خاصة بها، لذلك لا ترسل النواة الإشارة إليها مباشرة. يتولى Bash ذلك. عند تلقي bash تفاعلي للإشارة SIGHUP، يعيد إرسال SIGHUP إلى كل مهمة في جدوله قبل الخروج. ومن جهتك تبدو النتيجة مماثلة تماماً: تختفي المهمة ويتوقف ملف السجل في منتصف السطر.

توجد هنا حالة غير متماثلة تسبب الالتباس. لا يؤدي إدخال exit إلى قطع اتصال مهام الخلفية، لأن bash لا يفعل ذلك إلا عند تفعيل الخيار huponexit، وهو معطّل افتراضياً. أما انقطاع الاتصال، فيؤدي إلى قطعها. قد تنجو المهمة من إغلاقك للطرفية بطريقة سليمة، لكنها قد تتوقف عند انقطاع اتصال Wi-Fi.

تنتج عن ذلك نتيجتان، وهما جوهر الموضوع كله. لن تتلقى العملية قطع الاتصال إذا تجاهلت SIGHUP أو لم تكن لها طرفية متحكِّمة أصلاً. كذلك، إذا ظل خرجها القياسي يشير إلى pty المدمَّرة، فلن تجد مكاناً تكتب فيه: تفشل عملية الكتابة مع EIO (خطأ في الإدخال/الإخراج)، وتخرج معظم البرامج عند تلك النقطة. يجب أن تعالج كلا الجانبين. تعالج وصفات كثيرة الجانب الأول فقط، ولذلك يقول المستخدمون إن "nohup لم يعمل".

إذا انقطع اتصالك عدة مرات يومياً، فأصلح ذلك أيضاً. يمنع ServerAliveInterval 60 في ~/.ssh/config إسقاط الجلسة الخاملة بسبب مهلة NAT (ترجمة عناوين الشبكة) في مكان ما على المسار. أما الجلسة التي لا تُفتح أصلاً، فتمثل عطلاً مختلفاً له أسباب مختلفة؛ وهنا تكمن أهمية الفرق بين رفض الاتصال وانتهاء مهلة الاتصال.

ما الطريقة التي تُبقي الأمر قيد التشغيل بعد انقطاع اتصال SSH؟

أربع إجابات مرتبة حسب أهمية المهمة.

  • nohup أو setsid: لمهمة تُشغّلها الآن مرة واحدة وتقرأ سجلها لاحقاً. أنت تعيد توجيه المخرجات بنفسك.
  • disown: لمهمة بدأتها بالفعل ونسيت حمايتها. تنقذ العملية، لكنها لا تستطيع استعادة المخرجات لك.
  • tmux أو screen: لعمل تحتاج إلى مراقبته وإيقافه مؤقتاً والعودة إليه على مدار عدة أيام.
  • systemd-run أو ملف unit فعلي: لأي شيء يجب أن يستمر بعد انتهاء تسجيل دخولك، مثل rsync يستغرق ست ساعات أو استيراد قاعدة بيانات طوال الليل.

القاعدة التي تستحق التذكر: إذا كان نسيان المهمة سيسبب مشكلة، فمكانها هو systemd وليس tmux. نافذة tmux تتطلب من شخص أن يتذكرها. أما unit فلها اسم وحالة وسجل وسياسة لإعادة التشغيل، ويمكن للشخص التالي العثور عليها دون أن تخبره بها.

nohup وsetsid: شغّل الأمر ثم غادر

nohup ./import.sh > ~/import.log 2>&1 &
echo $! > ~/import.pid

يعيّن nohup تصرّف SIGHUP على التجاهل، ثم يشغّل أمرك. لذلك تصل إشارة hangup من النواة ولا تفعل شيئاً. أمّا إعادة التوجيه فعليك كتابتها. إذا تركت الإخراج القياسي موجهاً إلى الطرفية، يعيد nohup توجيهه تلقائياً إلى nohup.out في الدليل الحالي، ثم ينتقل إلى $HOME/nohup.out عند تعذّر ذلك، ويطبع:

nohup: ignoring input and appending output to 'nohup.out'

من السهل فقدان مسار هذا الملف. لذلك سمّه بنفسك. يحتوي $! على PID (معرّف العملية) لآخر مهمة في الخلفية. يتيح لك حفظه التحقق من حالة المهمة بعد تسجيل الدخول مجدداً.

يعالج setsid المشكلة نفسها من الجانب الآخر. فهو يشغّل الأمر في جلسة جديدة بلا طرفية متحكّمة. لذلك لا توجد طرفية يمكنها إنهاء العملية عند انقطاعها.

setsid --fork ./import.sh > ~/import.log 2>&1

استخدم --fork. من دونه، يستدعي setsid الأمر setsid() في مكانه كلما لم تكن العملية قائداً لمجموعة عمليات، وهذا ما يحدث داخل shell script. عندها يبقى البرنامج النصي متوقفاً. مع --fork يكون السلوك نفسه داخل البرنامج النصي وعند موجّه الأوامر.

تحقق مما حصلت عليه فعلياً:

ps -o pid,ppid,sid,tty,stat,cmd -p "$(cat ~/import.pid)"

تعني قيمة ? في العمود TTY أن العملية لا تملك طرفية متحكّمة. لذلك لا توجد طرفية يمكنها إنهاؤها. في حالة nohup، يظل العمود TTY يعرض قيمة مثل pts/0 ما دمت متصلاً، ثم يصبح ? بعد تدمير pty. كلتا النتيجتين سليمتان. لقد استمرت المهمة.

disown: إنقاذ مهمة بدأت تشغيلها بالفعل

بدأت مهمة تستغرق ساعتين في الواجهة الأمامية، ثم تذكرت هذه المشكلة. لا توقفها وتبدأ من جديد.

# press Ctrl-Z to suspend the job first
bg
jobs -l
disown -h %1

يعلّق Ctrl-Z المهمة، ويستأنف bg تشغيلها في الخلفية، ويطبع jobs -l رقم المهمة بجوار PID الخاص بها. يضع disown -h %1 علامة على المهمة كي لا يرسل إليها bash الإشارة SIGHUP. أما disown %1 المجرد، فيزيل المهمة بالكامل من جدول bash، وله التأثير نفسه عند انقطاع الجلسة، لكنه يجعل jobs يتوقف عن إدراجها.

ما لا يستطيع disown فعله هو نقل المخرجات. تظل العملية محتفظة بـpty كمخرج قياسي لها، وعندما يختفي pty يعيد طلب الكتابة التالي EIO. لذلك ينقذ disown مهمة هادئة بشكل موثوق، مثل عملية compile تكتب في ملف، لكنه يفشل غالباً مع المهمة كثيرة المخرجات. إما أن تستمر المهمة دون مكان لطباعة المخرجات، أو تتوقف عند سطر الإخراج التالي.

توجد أداة لإنقاذ واصفات الملفات. تنقل reptyr عملية قيد التشغيل إلى الطرفية الحالية: ثبّتها باستخدام sudo apt install -y reptyr، ثم شغّل reptyr <pid> من داخل نافذة tmux. تعمل الأداة عبر ptrace، وتوفّر Ubuntu الحزمة kernel.yama.ptrace_scope = 1، التي تسمح بالتتبع لأبنائك فقط، لذلك تحتاج إلى sudo reptyr <pid> إذا كانت العملية موروثة من عملية أخرى. تعامل معها كأداة طوارئ. لا تبنِ عليها إجراءً اعتيادياً.

tmux: العمل الذي تحتاج إلى مراقبته والعودة إليه

يحل tmux (مضاعِف الطرفية) المشكلة في موضع مختلف. بدلاً من حماية العملية من pty، يمنح العملية pty لا ينتمي إلى جلسة SSH. يعمل خادم tmux خارج تلك الجلسة، ويمتلك الطرفيات الخاصة بكل ما يعمل داخله. اتصال SSH الخاص بك ليس سوى عارض متصل به. إذا انقطع الاتصال، فلن يلاحظ الخادم ذلك.

sudo apt update && sudo apt install -y tmux
tmux new -s import

ابدأ المهمة في تلك النافذة، ثم اضغط Ctrl-b متبوعاً بـ d لفصل الجلسة. سجّل الدخول مرة أخرى لاحقاً واستأنفها:

tmux ls
tmux attach -t import

يجب أن يطبع tmux ls سطراً يبدأ بـ import: 1 windows. إذا طبع no server running on /tmp/tmux-1000/default، فلا توجد جلسة للاتصال بها، لأن الجلسة لم تُنشأ أصلاً أو لأن شيئاً ما أنهى الخادم.

ينفّذ screen المهمة نفسها باستخدام تسلسل مفاتيح مختلف. ينشئ screen -S import جلسة، ثم يفصل Ctrl-a الجلسة باستخدام d. يعرض screen -ls الجلسات الموجودة، ويعيد screen -r import إحداها. كلا الأداتين مناسبتان هنا. مفتاح الفصل هو الجزء الذي ينساه الناس.

المضاعِف هو أيضاً المكان المناسب للعمل التفاعلي الذي يجب أن يستمر بعد انقطاع الاتصال، ولذلك فإن تشغيل Claude Code على VPS داخل tmux هو الإعداد المعتاد، وهو ما يجعل إدارة جلسة خادم من هاتف عملية على شبكة محمولة تعيد الاتصال كل بضع دقائق.

systemd-run: تسليم المهمة إلى PID 1

بالنسبة إلى مهمة يجب ألا تعتمد عليك إطلاقاً، سلّمها إلى نظام init.

sudo systemd-run --unit=bigsync --collect /usr/bin/rsync -aH --stats /srv/data/ /mnt/backup/

ينشئ ذلك وحدة خدمة مؤقتة باسم bigsync.service. وتحصل هذه الوحدة على cgroup خاص بها، ولا تملك terminal تحكم، ولا ترتبط بجلسة تسجيل الدخول الخاصة بك. ويعود الأمر مباشرة، ويطبع Running as unit: bigsync.service. راقبها باستخدام أحد الأمرين التاليين:

systemctl status bigsync
journalctl -u bigsync -f

يطلب --collect من systemd إزالة الوحدة بعد انتهائها، حتى إذا فشلت. ومن دونه، تبقى الوحدة المؤقتة الفاشلة محمّلة، ويظل اسمها محجوزاً، لذلك يفشل التشغيل التالي برسالة تفيد بأن الوحدة موجودة مسبقاً. يذهب الإخراج إلى journal، مع طوابع زمنية في كل سطر. ولا تبقى إدخالات journal بعد إعادة التشغيل إلا عند وجود /var/log/journal، لذلك نفّذ sudo mkdir -p /var/log/journal ثم أعد تشغيل systemd-journald إذا أردت الاحتفاظ بها.

كمستخدم عادي، يؤدي استدعاء systemd-run من دون sudo إلى طلب التفويض من polkit، ثم يطبع ==== AUTHENTICATING FOR org.freedesktop.systemd1.manage-units ===. استخدم sudo مع وحدات النظام.

يمكنك أيضاً تشغيل المهمة ضمن user manager الخاص بك:

systemd-run --user --unit=bigsync --collect /usr/bin/rsync -aH /srv/data/ /mnt/backup/

لكن هذا يتضمن مشكلة. يتوقف user manager الخاص بكل مستخدم، user@1000.service، عادةً عند انتهاء جلستك الأخيرة، ويوقف معه كل وحدات المستخدم. فعّل lingering مرة واحدة:

loginctl enable-linger "$USER"
loginctl show-user "$USER" --property=Linger

يفترض أن يطبع الأمر الثاني Linger=yes. عند تفعيل lingering، يبدأ user manager الخاص بك عند الإقلاع ويستمر في العمل سواء سجلت الدخول أم لا. ومن دونه، لا يضيف systemd-run --user شيئاً إلى ما يوفره nohup.

systemd-run --scope مختلف عن ذلك. فهو يشغّل الأمر في الواجهة الأمامية، متصلاً بـterminal الخاص بك، ولذلك لا يفيد في هذه الحالة.

بالنسبة إلى أي مهمة ستشغّلها أكثر من مرة، اكتب الوحدة في ملف بدلاً من كتابة وحدة مؤقتة في كل مرة.

وحدة دائمة لمهمة ستشغّلها مجدداً
[Unit]
Description=Nightly data sync
Wants=network-online.target
After=network-online.target

[Service]
Type=oneshot
User=deploy
WorkingDirectory=/srv/data
ExecStart=/usr/local/bin/nightly-sync.sh

احفظها باسم /etc/systemd/system/nightly-sync.service، ثم نفّذ sudo systemctl daemon-reload، وبعد ذلك شغّلها باستخدام sudo systemctl start nightly-sync واقرأها مجدداً باستخدام journalctl -u nightly-sync. أضف ملف .timer مطابقاً عندما ينبغي تشغيلها وفق جدول زمني بدلاً من تشغيلها عند الطلب.

يتناول كتابة وحدة خدمة systemd وملف timer الخاص بها تنسيق الملف وبنية الجدول الزمني بالكامل.

إلى أين يذهب الخرج، ولماذا يختفي

ترتيب عمليات إعادة التوجيه مهم. يوجّه > file 2>&1 الخرج المعياري إلى الملف أولاً، ثم يوجّه الخطأ المعياري إلى المكان نفسه. أما 2>&1 > file فينفّذ ذلك بالترتيب العكسي: يظل الخطأ المعياري متجهاً إلى الطرفية، والطرفية هي التي ستختفي بعد قليل. ويقبل Bash أيضاً استخدام &> file لتوجيه التدفقين معاً.

المفاجأة الثانية هي التخزين المؤقت. عندما يكون الخرج المعياري موجهاً إلى طرفية، تفرغ مكتبة C كل سطر. وعندما يكون موجهاً إلى ملف، تنتقل المكتبة إلى مخزن مؤقت على شكل كتل بحجم بضعة كيلوبايت، لذلك لا يعرض tail -f ~/import.log أي شيء لعدة دقائق، ويبدو أن المهمة توقفت. فعّل التخزين المؤقت سطراً بسطر باستخدام stdbuf -oL ./import.sh > ~/import.log 2>&1، أو استخدم الخيار الخاص بالبرنامج، مثل python3 -u أو grep --line-buffered.

تجنب النمط التالي:

nohup ./import.sh 2>&1 | tee ~/import.log &

يحمي nohup الأمر import.sh فقط، ولا يحمي أي شيء آخر. أما tee فهي عملية منفصلة في خط الأنابيب نفسه، وستتوقف أيضاً عند انقطاع الاتصال. ثم يكتب import.sh إلى أنبوب لا يقرأ منه أي طرف، لذلك يتلقى SIGPIPE ويتوقف. ضع خط الأنابيب بأكمله داخل setsid bash -c '...'، أو اكتب مباشرةً إلى الملف وشغّل tail -f عليه عند إعادة الاتصال.

هناك تفصيل إضافي يتعلق تحديداً بـrsync. يكتب --info=progress2 دفقاً من محارف إرجاع العربة، ويبدو ذلك صحيحاً على الطرفية، لكنه يتحول إلى سطر واحد ضخم في ملف السجل أو في journal. عند التشغيل دون مراقبة، احذفه واستخدم --stats بدلاً منه.

لماذا تنجح مهمة في shell الخاص بك وتفشل عند تشغيلها عبر systemd أو cron

يقرأ shell التفاعلي لديك /etc/profile و~/.profile و~/.bashrc، ولذلك يحتوي على PATH، وshims الخاصة بمدير الإصدارات، والمتغيرات المصدَّرة. لا يقرأ systemd unit أياً من هذه الملفات. ولا يقرأ cron أياً منها أيضاً؛ ففي Debian وUbuntu، يشغّل cron المهام باستخدام SHELL=/bin/sh وPATH=/usr/bin:/bin.

يظهر العَرَض في systemd على شكل systemctl status يعرض (code=exited, status=203/EXEC). وهذا يعني أن systemd لم يتمكن من تنفيذ الملف أصلاً، لأن المسار غير صحيح أو لأن الملف غير معلَّم كملف قابل للتنفيذ. أما في cron، فعادةً يكون العَرَض هو command not found، ويُرسَل عبر البريد المحلي، أو لا يُرسَل إلى أي مكان عند عدم تثبيت نظام بريد.

أثبت شكل البيئة قبل أن تقضي ساعة في التخمين:

sudo systemd-run --collect --wait --unit=envtest /usr/bin/env
journalctl -u envtest --no-pager

يطبع ذلك البيئة المطابقة تماماً للبيئة التي ستُشغَّل بها مهمتك. أصلح الفجوة بعد ذلك. استخدم المسارات المطلقة لأي شيء تملكه، لأن systemd يحل rsync غير المسبوق بمسار كامل باستخدام قائمة مسارات نظام ثابتة، ولا يستخدم PATH الخاص بـshell لديك. مرّر المتغيرات المطلوبة باستخدام -p Environment="KEY=value" في سطر الأوامر، أو باستخدام EnvironmentFile=/etc/default/myjob في unit file. عندما تحتاج المهمة فعلاً إلى بيئة تسجيل الدخول الخاصة بك، شغّلها باستخدام /bin/bash -lc 'my-command'، وتقبّل أن المهمة ستعتمد عندئذٍ على dotfiles الخاصة بك.

ما الذي لا يزال ينهي المهمة المنفصلة

  • إعادة التشغيل. لا يستمر شيء من tmux بعدها، لأن الخادم عملية عادية والجلسات هي حالته الموجودة في الذاكرة. تتطلب تحديثات النواة إعادة التشغيل، لذلك ضع المهمة التي لا يمكنك إعادة تشغيلها بسهولة في وحدة يمكنك systemctl enable.
  • أداة إنهاء العمليات عند نفاد الذاكرة. يعرضها dmesg -T | grep -i 'killed process'، بما في ذلك اسم العملية التي اختارتها. وتكون عملية استيراد كبيرة على VPS صغير هدفاً شائعاً لها.
  • تنظيف logind. إذا ضبط /etc/systemd/logind.conf القيمة KillUserProcesses=yes، تُنهى العمليات المتبقية عند انتهاء جلستك الأخيرة، ويشمل ذلك خادم tmux. تحقّق من الإعداد الحالي باستخدام loginctl show --property=KillUserProcesses، واستثنِ مستخدمك باستخدام loginctl enable-linger "$USER".
  • امتلاء القرص. تتوقف المهمة لأن السجل الذي أعدت توجيهه ملأ نظام الملفات، وليس لأنك غادرت. شغّل df -h قبل أن تلقي اللوم على الإشارة.

بدء مهمة عبر SSH من دون إبقاء الاتصال قائماً

ssh vps 'sudo systemd-run --unit=import --collect /usr/local/bin/import.sh'

يعود systemd-run بمجرد بدء الوحدة، ولذلك يعود الأمر ssh أيضاً، ولا تكون للمهمة صلة بالجلسة التي شغّلتها. هذه هي الطريقة النظيفة.

تحتاج طريقة nohup إلى مزيد من العناية:

ssh vps 'nohup ./import.sh > ~/import.log 2>&1 < /dev/null &'

من دون عمليات إعادة التوجيه، يبدو الأمر وكأنه يتوقف. يُبقي sshd القناة مفتوحة ما دام أي process يحتفظ بإخراج الأمر البعيد القياسي أو بخطئه القياسي، وترث المهمة التي شُغّلت في الخلفية كليهما. لا يصلح nohup المشكلة وحده، لأن nohup يعيد توجيه الإخراج فقط عندما يكون هذا الإخراج طرفية، بينما هو هنا pipe عائد إلى العميل لديك. تؤدي إضافة < /dev/null إلى إغلاق جهة الإدخال أيضاً. وينفّذ ssh -n المهمة نفسها من جهة العميل.

FAQ

لماذا يتوقف الأمر الذي أشغّله عند انقطاع اتصال SSH؟

يُدمَّر الـpty (الطرفية الوهمية) الذي كانت جلستك تستخدمه، ويرسل kernel الإشارة SIGHUP إلى مجموعة العمليات الموجودة في الواجهة الأمامية على تلك الطرفية. الإجراء الافتراضي للإشارة SIGHUP هو إنهاء العملية. تنتهي المهام التي تعمل في الخلفية أيضاً، لأن bash يعيد إرسال SIGHUP إلى كل مهمة في جدوله قبل أن يخرج. لا يتأثر الأمر الذي يتجاهل SIGHUP، مثل الأمر الذي بدأته باستخدام nohup، ولا الأمر الذي لم يشارك جلستك أصلاً، مثل systemd unit.

هل tmux أم systemd-run أفضل لمزامنة rsync تستغرق ست ساعات؟

systemd-run. تعتمد جلسة tmux على عملية خادم بدأتها أنت، لذلك تنتهي عند إعادة التشغيل التالية، ولا يمكن لأي شخص لا يعرف تشغيل tmux ls رؤيتها. يمنحك تشغيل sudo systemd-run --unit=bigsync --collect /usr/bin/rsync -aH --stats /srv/data/ /mnt/backup/ قيمة systemctl status bigsync للحالة وقيمة journalctl -u bigsync للمخرجات، ويمكن للمسؤول التالي العثور على كلتيهما دون أن تخبره بذلك. استخدم tmux عندما تحتاج إلى مراقبة الشاشة والكتابة فيها.

كيف أرى مخرجات مهمة نسيت إعادة توجيهها؟

عادةً لا يمكنك ذلك، لأن تلك المخرجات أُرسلت إلى طرفية لم تعد موجودة. ما دامت العملية قيد التشغيل، يمكنك فحص الملفات المفتوحة لديها باستخدام sudo ls -l /proc/<pid>/fd أو مراقبة استدعاءات النظام الخاصة بها باستخدام sudo strace -p <pid>، لكن النص الذي كُتب مسبقاً فُقد. يمكن لـreptyr <pid> نقل العملية إلى طرفية جديدة، ويعني kernel.yama.ptrace_scope = 1 في Ubuntu أنها تحتاج إلى sudo عندما لا تكون العملية ابناً لك. لتجنب كل ذلك، أعد توجيه المخرجات إلى ملف عند بدء المهمة، ثم tail -f ذلك الملف.

هل تبقى جلسة tmux المنفصلة بعد إعادة التشغيل؟

لا. خادم tmux عملية عادية، والجلسات هي حالته الموجودة في الذاكرة، لذلك تنهي إعادة التشغيل كليهما. وتنتهي الجلسة أيضاً عندما يضبط /etc/systemd/logind.conf القيمة KillUserProcesses=yes وتسجّل خروجك من جلستك الأخيرة، وهو ما يمنعه loginctl enable-linger "$USER". بالنسبة إلى العمل الذي يجب أن يعود تلقائياً بعد إعادة التشغيل، اكتب systemd unit ثم systemctl enable إياه.

لماذا يعمل البرنامج النصي في shell لكنه يفشل باعتباره systemd unit؟

لا يقرأ الـunit الملفين /etc/profile أو ~/.bashrc، لذلك لا يحتوي على إضافاتك PATH ولا على المتغيرات التي صدّرتها. يعني ظهور systemctl status مع (code=exited, status=203/EXEC) أن systemd لم يتمكن من تنفيذ الملف أصلاً، لذا استخدم مساراً مطلقاً وتحقق من بت التنفيذ. شغّل sudo systemd-run --collect --wait --unit=envtest /usr/bin/env، ثم اقرأ النتيجة باستخدام journalctl -u envtest، وستحصل على البيئة الدقيقة التي تحصل عليها مهمتك. زوّدها بما ينقص باستخدام Environment= أو EnvironmentFile=.

#SSH#tmux#nohup#systemd#long-running-jobs