از پیام خطا تا errno؛ ساخت آزمایشگاه قابل بازتولید
در این درس ۸ بخش
این دوره ادامهٔ لینوکس پیشرفته و مسیر استقرار بکاند است. این بار از نصب سرویس و خواندن فهرست فرمانها عبور میکنیم و میپرسیم یک عملیات مشخص چرا شکست خورده است. برنامه میگوید فایل باز نشد؛ آیا مسیر غلط است، مجوز کافی نیست یا امکان ساختن توصیفگر تازه تمام شده؟ برای پاسخ، خطا را در آزمایش کوچک بازتولید میکنیم. هدف، حفظکردن شمارهٔ خطاها نیست؛ باید بتوانید عملیات، شاهد و فرضیه را از هم جدا کنید و آزمایش بعدی را با دلیل انتخاب کنید.
سه لایهای که نباید با هم اشتباه شوند
فراخوانی سیستم یا syscall راه درخواست برخی خدمات هسته، مانند بازکردن فایل و برقراری اتصال است. کتابخانهٔ زبان این رابط را برای برنامهنویس قابل استفادهتر میکند. در Python شکست بسیاری از عملیات سیستمعامل به OSError یا یک زیرنوع آن تبدیل میشود؛ متن ثبتشده توسط برنامه لایهٔ دیگری است. پیام «بارگذاری تنظیمات شکست خورد» میتواند چند علت متفاوت داشته باشد. از سوی دیگر، هر استثنا الزاماً یک شکست syscall نیست؛ خود برنامه هم میتواند ValueError بسازد. مدخلهای #linux-syscall و #linux-errno نام این دو سطح را روشن میکنند.
errno در رابطهای مربوط، علت گزارششدهٔ شکست را مشخص میکند. نامهایی مانند ENOENT از عدد خام خواناترند و شمارهٔ آنها را نباید برای همهٔ معماریها ثابت فرض کرد. در C نیز مقدار errno را فقط با قرارداد نتیجهٔ همان فراخوانی معنا میکنیم؛ یک مقدار باقیمانده پس از عملیات موفق، گواه خطای تازه نیست. در مثال Python از ویژگی error.errno همان استثنای گرفتهشده استفاده میکنیم، نه از حدسزدن یک وضعیت سراسری. وجود نام خطا، علت ریشهای محصول را بهتنهایی اثبات نمیکند.
اولین بازتولید؛ یک فایل که عمداً ساخته نشده است
بلوک بعدی را در examples/01-missing-file.py مییابید و میتوانید مستقل اجرا کنید. پوشهٔ موقت تازه ساخته میشود، اما فایل not-created.txt را نمیسازیم. بنابراین شکست به دادهٔ شخصی، نام فایل دستگاه شما یا زمان اجرای سرویس وابسته نیست. TemporaryDirectory در پایان پوشهٔ اختصاصی را جمع میکند. بهجای ثبت تمام مسیر تصادفی، نوع استثنا و نام نمادین را چاپ میکنیم تا شاهد لازم باقی بماند و خروجی برای مقایسه خوانا باشد.
import errno
from pathlib import Path
from tempfile import TemporaryDirectory
with TemporaryDirectory(prefix="dars-errno-") as folder:
missing = Path(folder) / "not-created.txt"
try:
missing.read_text(encoding="utf-8")
except OSError as error:
assert error.errno == errno.ENOENT
print(type(error).__name__)
print(errno.errorcode[error.errno])
else:
raise AssertionError("The missing path unexpectedly opened")
print("observation-complete")
خروجی سه خط است: FileNotFoundError، سپس ENOENT و بعد observation-complete. عملیات خواندن شکست خورده، اما برنامهٔ مثال شکست مورد انتظار را گرفته و بررسی کرده است. اگر فایل ناخواسته موجود باشد، else عمداً آزمون را رد میکند. این تفاوت مهم است: گرفتن هر استثنا و چاپ «موفق» آزمایش نیست؛ باید همان انتظار مشخص بررسی شود. این نمونه نیز دربارهٔ همهٔ علتهای ENOENT حکم نمیدهد؛ مؤلفهٔ مفقود مسیر را در درس سوم جدا میسنجیم.
خروجی استاندارد، پیام خطا و وضعیت پایان
stdout معمولاً برای نتیجه و stderr برای پیام تشخیصی استفاده میشود، ولی صرف نوشتن روی stderr، کد پایان را ناموفق نمیکند. برنامه میتواند هشدار بدهد و موفق تمام شود یا بیپیام، غیرصفر برگرداند. کد بعد یک فرزند کوچک خودمان را اجرا میکند که دو جریان جدا و خروج هفت دارد. این عدد قرارداد همین نمونه است، نه معنای عمومی «خطای فایل». در عیبیابی باید هر سه شاهد را نگه داریم؛ چسباندن همهٔ خروجیها به یک متن ممکن است مرز آنها را از بین ببرد.
import subprocess
import sys
child_code = (
"import sys; "
"print('report-ready'); "
"print('demo-failure', file=sys.stderr); "
"raise SystemExit(7)"
)
result = subprocess.run(
[sys.executable, "-c", child_code],
capture_output=True, text=True, timeout=3, check=False
)
assert result.stdout == "report-ready\n"
assert result.stderr == "demo-failure\n"
assert result.returncode == 7
print("stdout:", result.stdout.strip())
print("stderr:", result.stderr.strip())
print("exit:", result.returncode)
انتظار داریم stdout: report-ready، سپس stderr: demo-failure و exit: 7 چاپ شود. برنامهٔ والد نتیجهٔ فرزند را صریح بررسی کرده است. check=False یعنی subprocess.run برای غیرصفر بودن نتیجه، خودش CalledProcessError نمیسازد؛ به معنی درستبودن فرزند نیست. timeout سهثانیهای برای محدودکردن همین فرمان کوچک است و به درس بودجهٔ زمانی شبکه تعمیم خودکار ندارد. مثال از shell=True استفاده نمیکند و رشتهٔ تایپشدهٔ کاربر را به دستور پوسته تبدیل نمیکند.
محیط تمرین را پیش از نتیجهگیری شناسایی کنید
بسته برای لینوکس و حساب معمولی نوشته شده و روی Ubuntu 24.04 با Python سیستم قابل استفاده است. اگر Windows دارید، ابتدا محیط لینوکس مناسبِ دورهٔ مقدماتی را آماده کنید؛ اجرای مستقیم همهٔ این قابلیتها در PowerShell همان آزمایش نیست. این پروژه به pip، دسترسی root یا سرویس systemd نیاز ندارد. با حساب فعلی غیرریشه کار کنید. اگر id -u عدد صفر داد، سناریوهای بسته عمداً اجرا نمیشوند؛ برای ردشدن مانع، sudo اضافه نکنید و هویت اجرا را در گزارش پنهان نکنید.
id -u
python3 --version
python3 -c "import sys; print(sys.platform)"
pwd -Pشمارهٔ کاربر و مسیر جاری روی دستگاهها متفاوتاند؛ از آنها تصویر یکسان انتظار نداریم. خروج sys.platform باید با محیط هدف سازگار باشد و نسخهٔ Python باید با README بسته تطبیق کند. ابزارهای خواندنی بالا سیستم را آماده یا امن نمیکنند؛ فقط بخشی از پیششرط را نشان میدهند. اگر فرمان python3 پیدا نشد، از راهنمای نصب رسمی توزیع خود استفاده کنید. نصب تصادفی یک مفسر از فایل ناشناس، مسئلهٔ این درس را حل نمیکند و بخشی از مراحل آزمایش نیست.
بسته را اجرا کنید و معنای موفقیت را بخوانید
ZIP همراه را استخراج کنید و ترمینال را داخل پوشهٔ project دارای lab.py باز کنید. فرمانهای زیر ابتدا قرارداد ورودی را نشان میدهند، سپس سناریوی مسیر را اجرا و آزمونهای پروژه را فراخوانی میکنند. هر فرمان سناریو یک شیء JSON در stdout میدهد. در این دوره فایلهای کد را با نام واقعی میخوانیم؛ متن فارسی، شمارهٔ خط و علامت بلوک کد را داخل فایل Python نگذارید. اگر چند نسخه از بسته دارید، مسیر استخراج و نسخهٔ مورد استفاده را ثبت کنید.
python3 lab.py --help
python3 lab.py path
python3 -m unittest -vدر اجرای معتبر path، مقدار case برابر path و ok برابر true است؛ observed باید خطاهای مورد انتظار و بررسی پوشهٔ کاری را ثبت کند. جزئیات آن را در درس سوم میخوانیم. کد خروج صفر یعنی آزمایشِ کنترلشده مطابق قرارداد انجام شده و پاکسازی موفق بوده است. کد ۷۷ و skipped یعنی آزمایش انجام نشده؛ دو، خطای آرگومان و یک، نتیجهٔ پیشبینینشدهٔ بسته است. مشاهدهٔ یک errno داخل JSON را با خطای اجرای ابزار اشتباه نگیرید و صرف وجود واژهٔ error را ملاک نگذارید.
گزارشی که بتوان دوباره بررسی کرد
برای هر مشاهده، فرمان دقیق، پوشهٔ اجرا، هویت غیرمحرمانه، نسخهٔ ابزار، زمان و نتیجهٔ مورد انتظار را بنویسید. بعد نتیجهٔ واقعی و چیزی را که هنوز نمیدانید اضافه کنید. این بسته فایلهای کوچک موقت، فرزندهای خودش و اتصال loopback میسازد؛ مصرف دیسک یا حافظهٔ سامانه را برای ساخت نمایش پر نمیکند. اجرای موفق روی یک محیط، تمام رفتارهای سرور واقعی را اثبات نمیکند. اگر پیشنیاز رد شد یا نتیجه فرق داشت، همان تفاوت بخشی از گزارش است؛ خروجی را برای شبیهشدن به نمونه ویرایش نکنید.
تمرینها و پاسخ تشریحی
پرسش — آزمایش path خطای ENOENT را ثبت کرده ولی فرمان آزمایش با صفر پایان یافته است. آیا این دو شاهد تناقض دارند؟
پاسخ تشریحی — خیر. ENOENT نتیجهٔ یک عملیات بازکردن مسیر درون سناریوست؛ صفر، قرارداد پایانِ خود ابزار آموزشی است. ابزار انتظار داشت آن خطا رخ دهد، آن را بررسی کرد و منابعش را جمع کرد. برای برنامهٔ واقعی باید قرارداد خودش را بخوانید؛ نمیتوان نتیجهٔ این آزمایش را مجوز نادیدهگرفتن خطای برنامه دانست.
پرسش — همکار فقط نوشته است «عدد خطا ۲ است». چه اطلاعاتی برای تصمیم بعدی کم است؟
پاسخ تشریحی — نام ابزار و عملیات، سیستم و معماری، نام نمادین errno، نوع استثنا، زمان و ورودی لازماند. عدد دو ممکن است کد خروجِ یک ابزار باشد یا مقدار errno در یک محیط. ابتدا معلوم کنید عدد متعلق به کدام لایه است؛ سپس با مرجع همان ابزار و یک بازتولید کوچک، معنایش را بررسی کنید.
پرسش — اگر --help کار کرد ولی اجرای سناریو با ۷۷ و skipped پایان یافت، آیا میتوان نوشت آزمایش گذشت؟
پاسخ تشریحی — خیر. نمایش راهنما فقط بارگذاری و تحلیل آرگومان را نشان میدهد. ۷۷ در قرارداد این بسته یعنی پیشنیازی مانند اجرای غیرریشه یا قابلیت لازم برقرار نیست. علت reason را بخوانید و محدودیت را ثبت کنید. تا زمانی که سناریو واقعاً با ok=true و بدون skipped اجرا نشده، شاهد آزمایش آن خطا را ندارید.
ارتباط با دورههای قبلی و منابع
برای آمادهسازی محیط به درس انتخاب محیط تمرین مراجعه کنید:
https://www.darsavand.com/courses/linux-server-from-zero?lesson=ac71d60a-5b92-4c0c-81ee-a451aea9a798
برای دیدن تفاوت پیام journal و علت شکست، درس لاگهای ساختیافته را مرور کنید:
https://www.darsavand.com/courses/linux-advanced-administration?lesson=32265068-21de-4ef1-86c7-6ae5d7f00827
آن درسها را تکرار نمیکنیم؛ اینجا یک عملیات کوچک را به خطای قابل بازتولید وصل کردیم. #linux-exit-status برای تفکیک نتیجهٔ ابزار از علت عملیات مفید است.
منابع رسمی برای مطالعهٔ تکمیلی:
Linux man-pages — errno(3): https://man7.org/linux/man-pages/man3/errno.3.html
Python 3.12 — errno: https://docs.python.org/3.12/library/errno.html
Python 3.12 — Built-in Exceptions: https://docs.python.org/3.12/library/exceptions.html
Python 3.12 — subprocess: https://docs.python.org/3.12/library/subprocess.html
تألیف، مثالها و پرسشهای اختصاصی: تحریریه درسآوند.
فایل آزمایشگاه، راهنمای اجرا، آزمونها و فرم گزارش رخداد:
https://www.darsavand.com/resources/linux-application-errors-practice-kit
برای دیدن ترتیب دورههای مقدماتی، پیشرفته و این مسیر، نقشهٔ راه لینوکس را بخوانید:
https://www.darsavand.com/resources/linux-learning-roadmap
تألیف و مثالهای اختصاصی: تحریریه درسآوند.
متن درسهای عمومی را بدون ورود بخوانید.