معالجة الأخطاء وتصميم إعادة التنفيذ في PowerShell ── من فخّ عدم نجاعة try/catch إلى قواعد exit code وإعادة المحاولة

· آخر تحديث: · · PowerShell, Windows, معالجة الأخطاء, إعادة المحاولة, الأتمتة, تحسين التشغيل, سكربت, Task Scheduler

«فشلت الدفعة الليليّة (night batch)، لكنّ Task Scheduler أظهرها نجحت (0x0) فلم ينتبه أحد» أو «كتبتُ try/catch لكنّه لا يدخل إلى catch» أو «ينهار مرّة واحدة في الشهر بسبب انقطاع الشبكة اللحظيّ» ── متى ما وُضِع سكربت PowerShell في التشغيل الفعليّ، تأتي هذه الأنواع من الاستفسارات حتماً. بين السكربت الذي يُرضيك تشغيله يدويّاً على جهازك، والسكربت الذي يعمل كلّ ليلة دون إشراف بشريّ، يقف حاجز اسمه تصميم معالجة الأخطاء وإعادة التنفيذ (retry).

والمزعج في الأمر أنّ نموذج الأخطاء في PowerShell يختلف قليلاً عن نموذج الاستثناءات في لغات البرمجة العامّة. فظاهرة «تظهر الأخطاء لكن المعالجة تستمرّ» أو «كان من المفترض أن يُلتقَط في catch لكنّه يفلت» ليست عادةً خللاً برمجيّاً، بل سلوكاً مطابقاً لمواصفات PowerShell نفسها في معظم الأحوال، وكتابة الشيفرة دون معرفة هذه الآليّة تُنتج بكثرة «سكربتات تكتم الفشل وتنتهي وكأنّها نجحت».

في هذا المقال، وباستهداف مسؤولي أنظمة المعلومات والمطوّرين الذين يُؤتمِتون الأعمال الروتينيّة داخل الشركة عبر PowerShell، نرتّب - بالاستناد إلى الوثائق الرسميّة - التمييز بين الأخطاء المُنهِية وغير المُنهِية، وطريقة الحكم على نجاح الأوامر الأصليّة أو فشلها، وتصميم exit code الذي يتيح لِـ Task Scheduler وأدوات المراقبة الحكم على النجاح والفشل، وصولاً إلى نمط إعادة المحاولة الذي يتحمَّل الأخطاء المؤقّتة.

1. الخلاصة أوّلاً

  • تنقسم أخطاء PowerShell إلى «أخطاء غير مُنهِية» و«أخطاء مُنهِية (منهية للجملة/منهية للسكربت)». الأخطاء غير المُنهِية تعرض رسالة وتستمرّ في خطّ الأنابيب، ولا تدخل افتراضيّاً إلى try/catch.1
  • القاعدة الثابتة هي إضافة -ErrorAction Stop إلى الأمر الذي تريد التقاطه عبر try/catch. فـ Stop يرفع الخطأ غير المُنهِي إلى خطأ مُنهٍ، ما يجعله قابلاً للمعالجة في catch. توجد أيضاً طريقة ضبط $ErrorActionPreference (القيمة الافتراضيّة Continue) إلى Stop في بداية السكربت.12
  • يتجاوز -ErrorAction قيمة $ErrorActionPreference بالنسبة لأمر واحد فقط. لكن الاثنين ليسا متماثلَين تماماً؛ فما يستطيع -ErrorAction التحكّم فيه هو الأخطاء غير المُنهِية فقط.1
  • فشل الأوامر الأصليّة (native commands) مثل robocopy وgit والملفّات التنفيذيّة الخارجيّة لا يتحوّل افتراضيّاً إلى خطأ في PowerShell. رمز الخروج غير الصفريّ يجعل $? يساوي $false ويدخل في $LASTEXITCODE، لكن لا يُنشَأ ErrorRecord ولا يدخل الأمر إلى catch. احكم على النجاح والفشل عبر $LASTEXITCODE.1
  • أصبح $PSNativeCommandUseErrorActionPreference ميزة رسميّة في PowerShell 7.4. عند ضبطه على $true يُصدِر رمز الخروج غير الصفريّ خطأً غير مُنهٍ، وإذا جُمِع مع $ErrorActionPreference = 'Stop' أمكن التقاطه عبر try/catch (القيمة الافتراضيّة $false).32
  • داخل catch يكون $_ هو ErrorRecord. يعطيك $_.Exception جسم الاستثناء، وإن كان خطأً مرفوعاً (upgraded) يمكن الوصول إلى معلومات الخطأ الأصليّة عبر $_.Exception.ErrorRecord. يمكن باستخدام كتلة catch محدَّدة النوع معالجة الأخطاء المتوقَّعة فقط على حدة.14
  • أبلِغ النجاح أو الفشل دائماً للخارج عبر exit code. حدِّد رمز الخروج عبر الكلمة المفتاحيّة exit، وعند التشغيل عبر pwsh -File / powershell.exe -File تصبح هذه القيمة هي رمز خروج العمليّة. وإن لم يوجد exit فالرمز 0 عند الاكتمال الطبيعيّ و1 عند استثناء غير مُعالَج.56
  • مبادئ إعادة المحاولة الثلاثة هي: الاقتصار على الأخطاء المؤقّتة، ووضع حدّ أعلى، وتصميم idempotent. لا تُخفِ أخطاء الأعمال بإعادة المحاولة، ووسِّع الفاصل الزمنيّ عبر التراجع الأُسّيّ، وصمِّم المعالجة بحيث لا تسبِّب إعادة تنفيذها معالجة مزدوجة. لا يصبح السكربت «سكربتاً يجوز إعادة تنفيذه» إلا باجتماع هذه العناصر الثلاثة.

2. نوعا الأخطاء ── لماذا لا يعمل try/catch

تنقسم أخطاء PowerShell إلى ثلاث فئات: الخطأ غير المُنهِي (non-terminating error، يكتفي بالإبلاغ دون إيقاف خطّ الأنابيب)، وخطأ إنهاء الجملة (statement-terminating error، يوقف تلك الجملة فقط وينتقل إلى التالية)، وخطأ إنهاء السكربت (script-terminating error، يُرجِع مكدّس الاستدعاء (call stack) بأكمله).1

الفخّ في العمل الفعليّ هو الخطأ غير المُنهِي. فما تصدره cmdlets مثل Get-Content أو Get-ChildItem عند فشل معالجة إدخال معيّن هو غالباً خطأ غير مُنهٍ، حيث تظهر رسالة الخطأ الحمراء، لكنّ المعالجة تستمرّ، ولا تدخل إلى try/catch ولا إلى trap.1

# 【فخّ】لا يدخل أبداً إلى catch، ويظهر حتّى "اكتمل"
try {
    Get-Content -Path 'C:\Data\ملف-غير-موجود.txt'   # خطأ غير مُنهٍ
    Write-Host 'اكتمل'                                # يُنفَّذ رغم كلّ شيء
}
catch {
    Write-Host 'لا يصل إلى هنا'
}

# 【القاعدة الثابتة】الترقية إلى خطأ مُنهٍ عبر -ErrorAction Stop، ليصبح قابلاً للمعالجة في catch
try {
    Get-Content -Path 'C:\Data\ملف-غير-موجود.txt' -ErrorAction Stop
    Write-Host 'اكتمل'                                # لا يُنفَّذ عند حدوث الخطأ
}
catch {
    Write-Host "تمّ الالتقاط: $($_.Exception.Message)"
}

عندما يكون -ErrorAction Stop أو $ErrorActionPreference = 'Stop' فعّالاً، يلفّ المحرّك (engine) الخطأ غير المُنهِي بـ ActionPreferenceStopException ويرفعه إلى خطأ مُنهٍ. والآليّة الدقيقة هي أنّ هذا الخطأ المرفوع هو ما يصل إلى catch داخل كتلة try.1 في المقابل، استثناءات طرائق .NET (مثل [int]::Parse('abc')) أو فشل تحليل اسم الأمر هي أخطاء مُنهِية منذ البداية، فتدخل إلى catch دون أيّ إجراء إضافيّ.1

الفكرة القائلة «إذن فلنضبط $ErrorActionPreference = 'Stop' دائماً» صحيحة نصفها. ففي السكربتات التي تعمل دون إشراف بشريّ، التوقّف والإبلاغ عن الفشل أكثر أماناً من الاستمرار مع كتم الخطأ، لذا فإنّ ضبطه إلى Stop في البداية قيمة افتراضيّة جيّدة. لكن انتبه إلى أنّ $ErrorActionPreference يؤثِّر في نطاقه (scope) والنطاقات الفرعيّة، فيغيِّر حتّى سلوك الوحدات (modules) والدوال (functions) المستدعاة، وأنّ معالجات التنظيف التي يجوز أن تفشل (مثل حذف الملفّات المؤقّتة) تحتاج إلى إعادة إضافة -ErrorAction SilentlyContinue بشكل منفصل لها.2

3. ما الذي يُقرَأ داخل catch ── طريقة تتبُّع ErrorRecord

يحوي $_ داخل كتلة catch كائن ErrorRecord. ومنه يمكن الحصول على المعلومات التي ينبغي تسجيلها في السجلّ (log).14

try {
    Copy-Item -Path $src -Destination $dest -ErrorAction Stop
}
catch [System.IO.IOException] {
    # عبر catch محدَّد النوع، عالِج «الفشل المتوقَّع» فقط على حدة.
    # حتّى مع الخطأ المرفوع، يطابق المحرّك نوع الاستثناء الأصليّ
    Write-Warning "خطأ I/O: $($_.Exception.Message)"
}
catch {
    # سجِّل غير المتوقَّع مع سياقه الكامل في السجلّ، وأعِد رفعه (لا تكتمه)
    $rec = $_   # $_ هو ErrorRecord
    Write-Warning ('النوع: {0} / الموقع: {1} / الهدف: {2}' -f `
        $rec.Exception.GetType().FullName,
        $rec.InvocationInfo.PositionMessage,
        $rec.TargetObject)
    throw       # throw بلا معامل يعيد نشر الخطأ نفسه إلى الأعلى
}
finally {
    # يُنفَّذ finally عند النجاح والخطأ وحتّى عند الإيقاف بـ Ctrl+C. ضع التنظيف هنا
    if ($tempFile -and (Test-Path $tempFile)) { Remove-Item $tempFile -ErrorAction SilentlyContinue }
}

توجد ثلاث نقاط تستحقّ الانتباه.

  • $_.Exception هو جسم الاستثناء. الخطأ المرفوع عبر -ErrorAction Stop يُلَفّ بـ ActionPreferenceStopException، لكن عند مطابقة النوع في catch يرى المحرّك نوع الاستثناء الأصليّ (مثل ItemNotFoundException)، لذا يمكن كتابة catch محدَّد النوع كما هو. ويمكن الوصول إلى ErrorRecord الأصليّ عبر $_.Exception.ErrorRecord.1
  • يحتوي $_.InvocationInfo.PositionMessage على «أيّ ملفّ، وأيّ سطر، وأيّ أمر»، وفي سجلّات التشغيل دون إشراف يختلف زمن التحقيق باختلاف وجود هذه المعلومة من عدمه بمقدار مرتبة كاملة.
  • تُنفَّذ كتلة finally سواء نجح try أم فشل أم أُوقِف بـ Ctrl+C. ضع في finally معالجات التنظيف مثل إغلاق الاتّصالات وحذف الملفّات المؤقّتة.7

النقاش التصميميّ حول «في أيّ طبقة يُلتقَط الخطأ وأين يُكتَب السجلّ» مشترك عبر اللغات المختلفة. المبادئ التي رُتِّبت في «أين ينبغي وضع catch والسجلّ في معالجة الاستثناءات» (الالتقاط عند الحدود، عدم الكتم، تجنّب ازدواج السجلّ) تنطبق على PowerShell كما هي.

4. نجاح الأوامر الأصليّة أو فشلها ── $? و$LASTEXITCODE والميزة الجديدة في 7.4

الثغرة الكبرى الأخرى هي الأوامر الأصليّة (native commands) مثل robocopy وgit والملفّات التنفيذيّة الداخليّة للشركة. فالبرامج الخارجيّة لا تشارك في نظام أخطاء PowerShell، بل تُبلِّغ عن الفشل عبر رمز الخروج. السلوك الافتراضيّ هو كالتالي.1

الحالة السلوك (افتراضيّاً)
رمز خروج غير صفريّ يصبح $? مساوياً لـ $false، ويُدرَج رمز الخروج في $LASTEXITCODE
إنشاء ErrorRecord لا يحدث (ولا يُضاف إلى $Error أيضاً)
try/catch لا يدخل

بمعنى آخر، فإنّ try { robocopy ... } catch { ... } لا يلتقط شيئاً (افتراضيّاً). اكتب الحكم على نجاح الأوامر الأصليّة أو فشلها باستخدام $LASTEXITCODE. أمّا $? فهو قيمة منطقيّة تعني «هل نجحت العمليّة الأخيرة»، وبالنسبة للأوامر الأصليّة لا يصبح $true إلّا عند رمز خروج 0.1 وتجدر الإشارة إلى أنّه في Windows PowerShell 5.1 كان $? قد يصبح $false لمجرّد كتابة الأمر الأصليّ إلى stderr، لكن في PowerShell 7 عُدِّل السلوك ليصبح $false فقط عند رمز خروج غير صفريّ. وهو تعديل يتّسق مع الواقع، إذ لا يُعامَل الإخراج إلى stderr كفشل.8

# احكم على الأوامر الأصليّة عبر $LASTEXITCODE
robocopy.exe 'D:\Reports' '\\fileserver\reports' /MIR /R:2 /W:5
if ($LASTEXITCODE -ge 8) {
    # في robocopy، القيم من 0 إلى 7 نجاح (معلومات مثل وجود نسخ من عدمه)، و8 فأكثر فشل
    throw "فشل robocopy (ExitCode=$LASTEXITCODE)"
}

ابتداءً من PowerShell 7.4 أصبح بالإمكان استخدام $PSNativeCommandUseErrorActionPreference الذي يغيِّر هذا التعامل. أُضيف كميزة تجريبيّة في 7.3، وأصبح ميزة رسميّة (mainstream) في 7.4.3 عند ضبطه على $true، يُصدِر الأمر الأصليّ ذو رمز الخروج غير الصفريّ خطأً غير مُنهٍ يوضِّح رمز الخروج، وهذا الخطأ يتبع $ErrorActionPreference. أي إنّه بالجمع مع Stop يمكن أن يصل فشل الأوامر الخارجيّة أيضاً إلى try/catch.12

# PowerShell 7.4 فما بعده: مُعالَجة فشل الأوامر الخارجيّة أيضاً عبر try/catch (الافتراضي $false)
$PSNativeCommandUseErrorActionPreference = $true
$ErrorActionPreference = 'Stop'

try {
    git.exe fetch origin
}
catch {
    Write-Warning "فشل git: $($_.Exception.Message)"
    throw
}

& {
    # الأوامر التي لا يعني فيها رمز الخروج غير الصفريّ فشلاً، مثل robocopy،
    # عطِّل الإعداد مؤقّتاً داخل كتلة السكربت واحكم كالمعتاد عبر $LASTEXITCODE (يعود كما كان عند الخروج)
    $PSNativeCommandUseErrorActionPreference = $false
    robocopy.exe 'D:\Reports' '\\fileserver\reports' /MIR
    if ($LASTEXITCODE -ge 8) { throw "فشل robocopy (ExitCode=$LASTEXITCODE)" }
}

كما هو مذكور حرفيّاً في الوثائق الرسميّة في مثال robocopy، توجد أوامر تستخدم رمز الخروج غير الصفريّ كمعلومة طبيعيّة، لذا إذا أردتَ تفعيل الإعداد بشكل شامل فمن الضروريّ تصميم نطاقات استثناء له.2 في الأماكن التي لا يُستخدَم فيها إلّا Windows PowerShell 5.1، لا توجد هذه الميزة أصلاً، فوحِّد الحكم عبر $LASTEXITCODE. واختلاف السلوك بين 5.1 و7 قد يكون فخّاً شائعاً عند الترحيل، لذا راجع أيضاً «الفرق بين Windows PowerShell 5.1 وPowerShell 7 والترحيل بينهما».

5. تصميم exit code ── جعل النجاح والفشل قابلَين للحكم من Task Scheduler وأدوات المراقبة

بعد التقاط الخطأ، تأتي الخطوة التالية وهي الإبلاغ للخارج. الوسيلة الوحيدة عمليّاً التي يعرف بها Task Scheduler أو أدوات المراقبة نجاح السكربت أو فشله هي رمز خروج العمليّة (process exit code). لنضبط المواصفات بدقّة.

  • يمكن تحديد رمز خروج السكربت صراحةً عبر exit <رقم>. كما يضبط exit قيمة في $LASTEXITCODE أيضاً.59
  • عند التشغيل عبر pwsh -File (أو powershell.exe -File)، تصبح القيمة المحدَّدة في exit هي رمز خروج العمليّة نفسه. وإن لم توجد جملة exit، فالرمز 0 عند الاكتمال الطبيعيّ و1 عند الانتهاء باستثناء غير مُعالَج.56
  • عند تشغيل السكربت عبر -Command، لا يُحتفَظ برمز خروج مثل exit 10 داخل السكربت. بل يُقرَّب إلى 0 أو 1 بحسب نجاح آخر أمر أو فشله (إن كُتِب exit 10 مباشرةً في نصّ الأمر، تُعاد تلك القيمة). إذا كان التشغيل يعتمد على التمييز بين رموز خروج مختلفة، فإنّ التشغيل عبر -File هو القاعدة الثابتة.6

بترجمة هذه المواصفات إلى هيكل عمليّ، يصبح قالب السكربت الذي يعمل دون إشراف بشريّ كالتالي.

# Invoke-NightlyExport.ps1 ── هيكل يتيح لِـ Task Scheduler الحكم على النجاح والفشل
[CmdletBinding()]
param()

$ErrorActionPreference = 'Stop'   # في التشغيل دون إشراف، اجعل "التوقّف والإبلاغ" هو الافتراضيّ

# احتفظ بأثر التنفيذ، بما في ذلك الإخراج القياسيّ والأخطاء، كسجلّ (-Append للإضافة إلى ملفّ يوميّ)
Start-Transcript -Path "C:\Logs\NightlyExport_$(Get-Date -Format yyyyMMdd).log" -Append

try {
    Export-DailyData      # جسم معالجة الأعمال (استدعاء دالة مقسَّمة إلى وحدة/module)
    exit 0                # توضيح النجاح
}
catch [System.Net.WebException] {
    Write-Warning "خطأ اتّصال: $($_.Exception.Message)"
    exit 10               # فئة الأخطاء المؤقّتة ── اترك مجالاً لضبط إعادة التنفيذ من جانب المهمّة
}
catch {
    Write-Warning "خطأ غير متوقَّع: $($_.Exception.Message)"
    Write-Warning $_.InvocationInfo.PositionMessage
    exit 1                # خطأ دائم ── لا تُعِد التنفيذ، واترك الأمر للإنسان
}
finally {
    Stop-Transcript       # مع finally يُغلَق الأثر حتّى عند الخروج عبر exit
}

Start-Transcript أمر (cmdlet) يسجِّل كامل إدخال الجلسة وإخراجها في نصّ، ما يتيح إعادة تكوين «ما كان يظهر على الشاشة حينها» دون تجهيز echo أو إعادة توجيه (redirect).10 وهو لا يتعارض مع دوال السجلّ (log) الخاصّة بك، بل يستحقّ الجمع بينهما كخطّ دفاع أخير. تصميم السجلّ ومعالجة تضخّمه مشروحان في «تطبيقات PowerShell المتقدّمة ── أتمتة آمنة لتحقيق السجلّات وأرشفتها وإصدار التقارير عنها».

السرّ في توزيع exit code هو عدم المبالغة في التعقيد. تكفي دقّة بمستوى 0 = نجاح، و1 = خطأ دائم (يراه الإنسان)، وما بين 10 وما فوق = خطأ مؤقّت (يجوز إعادة تنفيذه)، وهذا يكفي لبناء الحكم على النجاح والفشل مباشرةً من «نتيجة التنفيذ الأخيرة» (Last Run Result) في Task Scheduler أو من أدوات إدارة المهام (job management tools). راجع «مهمّة Task Scheduler لا تُنفَّذ أو تنتهي بـ 0x1 ── تفكيك الأسباب وتصميم تشغيل آمن» بخصوص إعدادات جانب المهمّة (إعادة التنفيذ عند الفشل، وطريقة التحقّق من نتيجة التنفيذ).

6. تصميم إعادة المحاولة ── التمييز بين الأخطاء المؤقّتة وأخطاء الأعمال

أخيراً، إعادة التنفيذ (retry). قيمة إعادة المحاولة هي «امتصاص الأخطاء المؤقّتة تلقائيّاً دون إيقاظ أحد في منتصف الليل»، لكنّ إدراجها بلا تروٍّ يُنتج أعطالاً أخرى مثل «إعادة محاولة الفشل الدائم إلى ما لا نهاية» أو «إفساد البيانات بمعالجة مزدوجة». المبادئ ثلاثة.

  • لا تُعِد المحاولة إلّا للأخطاء المؤقّتة. اقصرها على الفشل الذي قد يُحلّ بمرور الوقت، مثل انقطاع الشبكة اللحظيّ، أو القفل المؤقّت للملفّ، أو انتظار بدء تشغيل خدمة يُعتمَد عليها. أمّا إدخال غير صحيح، أو نقص صلاحيّات، أو خطأ في الإعداد، فأفشِلها فوراً وسلِّمها للإنسان عبر exit code والسجلّ.
  • صمِّم حدّاً أعلى وفاصلاً زمنيّاً. حدِّد عدداً أقصى للمحاولات، ووسِّع الفاصل عبر التراجع الأُسّيّ (2 ثانية، 4 ثوانٍ، 8 ثوانٍ…). فاستمرار الطرق على طرف يمرّ بعطل بفاصل ثابت لا يفعل شيئاً سوى إعاقة تعافيه.
  • اجعل المعالجة idempotent (آمنة عند إعادة التنفيذ). كلٌّ من إعادة المحاولة وإعادة تنفيذ Task Scheduler يعنيان «تشغيل المعالجة نفسها مرّة أخرى». يفترض هذا تصميماً مثل نشر الإخراج عبر ملفّ مؤقّت ثمّ إعادة التسمية، أو تسجيل المعرِّفات (IDs) المُعالَجة سلفاً لصدّ الاستيراد المزدوج.

يمكن تلخيص هذا في النمط التالي.

function Invoke-WithRetry {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)] [scriptblock] $Operation,
        # تمرير قيمة 0 أو أقل يعني عدم التنفيذ ولو مرّة والانتهاء بنجاح ظاهريّ، لذا نفرض 1 فأكثر
        [ValidateRange(1, 100)]
        [int] $MaxAttempts = 4,
        # القيمة السالبة تسبِّب خطأً مختلفاً في Start-Sleep عند إعادة المحاولة، لذا نرفضها عند الربط
        [ValidateRange(0, 3600)]
        [int] $BaseDelaySeconds = 2,
        # اذكر فقط أنواع الاستثناءات التي تستحقّ إعادة المحاولة (الافتراضي: الاتّصال وI/O)
        [Type[]] $RetryableExceptions = @([System.IO.IOException], [System.Net.WebException])
    )
    for ($attempt = 1; $attempt -le $MaxAttempts; $attempt++) {
        try {
            # استقبل الإخراج في متغيّر أوّلاً، ثمّ أعِده بعد النجاح. فلو استُخدِم & $Operation
            # مباشرةً مع return، وحدث استثناء بعد إخراج جزئيّ، سيتدفّق ذلك الإخراج الجزئيّ
            # إلى جهة الاستدعاء، فتصل البيانات نفسها مزدوجة عند نجاح إعادة المحاولة
            $output = & $Operation
            return $output
        }
        catch {
            $ex = $_.Exception
            $isRetryable = $RetryableExceptions | Where-Object { $ex -is $_ }
            if (-not $isRetryable -or $attempt -eq $MaxAttempts) {
                throw   # خطأ أعمال، أو بلوغ حدّ إعادة المحاولة ── دعه يفشل كما هو
            }
            # ضع حدّاً أعلى لوقت الانتظار المتزايد أُسّيّاً (حتّى لا يطول الانتظار كثيراً
            # في التركيبات ذات العدد الكبير من المحاولات، ولا يتجاوز نطاق قبول Start-Sleep)
            $delay = [math]::Min($BaseDelaySeconds * [math]::Pow(2, $attempt - 1), 300)
            Write-Warning "فشل (المحاولة ${attempt}): $($ex.Message) ── ستُعاد المحاولة بعد ${delay} ثانية"
            Start-Sleep -Seconds $delay
        }
    }
}

# طريقة الاستخدام: تأكّد من تحويل المعالجة المستهدَفة إلى خطأ مُنهٍ عبر -ErrorAction Stop
Invoke-WithRetry -Operation {
    Copy-Item -Path '\\fileserver\out\daily.csv' -Destination 'D:\Work' -ErrorAction Stop
}

# ملاحظة عند إعادة محاولة Invoke-RestMethod / Invoke-WebRequest في PowerShell 7:
# في PowerShell 7 يصل فشل الاتّصال ضمن نوع HttpRequestException لا WebException كما في 5.1،
# لذا لا تُعاد المحاولة بالإعداد الافتراضيّ. كما تصل استجابات HTTP الدائمة الفشل مثل 404
# بالنوع نفسه، لذا عند وصول استجابة فعليّة، صنِّف بنفسك «هل تستحقّ إعادة المحاولة» عبر رمز الحالة
Invoke-WithRetry -RetryableExceptions ([System.Net.Http.HttpRequestException]) -Operation {
    # عبر -SkipHttpErrorCheck تُستقبَل استجابات الخطأ أيضاً دون تحويلها إلى استثناء، ثمّ تُوزَّع بحسب الرمز
    $r = Invoke-WebRequest -Uri 'https://api.example.co.jp/orders' -TimeoutSec 30 -SkipHttpErrorCheck
    if ($r.StatusCode -in 408, 429, 500, 502, 503, 504) {
        # ارفع الرموز المؤقّتة فقط كـ HttpRequestException ← تُعاد محاولتها
        throw [System.Net.Http.HttpRequestException]::new("خطأ HTTP مؤقّت: $($r.StatusCode)")
    }
    if ($r.StatusCode -ge 400) {
        throw "خطأ HTTP دائم: $($r.StatusCode)"   # لا تُعاد المحاولة لاختلاف النوع
    }
    $r.Content | ConvertFrom-Json
}

النقطة المهمّة هي أنّ هدف إعادة المحاولة يُختار صراحةً عبر نوع الاستثناء. فكتابة «أعِد المحاولة لكلّ ما يُلتقَط في catch» تعني تجربة حتّى الأخطاء الدائمة، مثل خطأ في المعاملات (parameters)، أربع مرّات وانتظاراً بلا فائدة. الطريقة الواقعيّة بعد بدء التشغيل الفعليّ هي إضافة أنواع الأخطاء المؤقّتة المُلاحَظة في السجلّات الفعليّة تدريجيّاً إلى $RetryableExceptions. ومن المفيد فصل مثل هذه الدوال المشتركة إلى وحدة (module) لإعادة استخدامها (راجع «تصميم المعاملات (arguments) والتقسيم إلى وحدات في PowerShell»). كما أنّ منطق إعادة المحاولة وتفريع الأخطاء هو بالضبط الجزء الذي يستحقّ كتابة اختبارات له بواسطة Pester (راجع «إعداد اختبارات PowerShell عبر Pester ── نمط عمليّ يصعب معه كسر سكربتات التشغيل»).

7. القواعد الثابتة في العمل الفعليّ (جدول القرار)

نقطة النقاش الخيارات معيار الحكم
السلوك الافتراضيّ للأخطاء البقاء على Continue / ضبط $ErrorActionPreference = ‘Stop’ في البداية التشغيل دون إشراف بشريّ أكثر أماناً مع «التوقّف والإبلاغ». سكربتات التحقيق التفاعليّة يمكن أن تبقى على Continue2
الموضع المراد التقاطه في catch الأمل بلا ضمان / تحديد -ErrorAction Stop صراحةً معظم أخطاء الـ cmdlets غير مُنهِية. حدِّد Stop صراحةً في السطر الذي تريد التقاطه1
نجاح الأوامر الأصليّة أو فشلها الإهمال / الحكم عبر $LASTEXITCODE / ميزة $PSNativeCommandUseErrorActionPreference في 7.4 في البيئات المختلطة مع 5.1، وحِّد الحكم عبر $LASTEXITCODE. إن كانت 7.4 فما بعده فقط، استخدم الميزة الجديدة مع نطاقات استثناء لأوامر مثل robocopy32
الإبلاغ الخارجيّ عن النجاح والفشل السجلّ فقط / تصميم exit code والتشغيل عبر -File السجلّ للإنسان، وexit code للآلة. الاثنان ضروريّان معاً. التشغيل عبر -Command يفقد رمز الخروج6
أثر التنفيذ (execution trail) السجلّ الخاصّ فقط / الجمع مع Start-Transcript ضمان يحفظ حتّى الإخراج الذي لا يلتقطه السجلّ الخاصّ (كإخراج الأوامر الخارجيّة القياسيّ)10
إعادة المحاولة كلّ الأخطاء هدف / اقتصار على الأخطاء المؤقّتة + تراجع أُسّيّ + idempotent إعادة محاولة أخطاء الأعمال مصدر أعطال. اجمع بين الحدّ الأعلى والفاصل الزمنيّ وidempotent

8. الخلاصة

  • تنقسم أخطاء PowerShell إلى غير مُنهِية ومُنهِية، ولا تدخل الأخطاء غير المُنهِية افتراضيّاً إلى try/catch. القاعدة الثابتة هي تحديد -ErrorAction Stop صراحةً في الأمر الذي تريد التقاطه.
  • القيمة الافتراضيّة لـ $ErrorActionPreference هي Continue. اضبطها إلى Stop في بداية السكربتات التي تعمل دون إشراف بشريّ، لمنع عطل «كتم الفشل والانتهاء وكأنّه نجح» بنيويّاً.
  • فشل الأوامر الأصليّة لا يدخل افتراضيّاً إلى catch. احكم عبر $LASTEXITCODE، أو استفد من $PSNativeCommandUseErrorActionPreference إن كنتَ تستخدم PowerShell 7.4 فما بعده.
  • في catch، سجِّل من $_ (ErrorRecord) نوع الاستثناء ورسالته ومعلومات موقعه في السجلّ، وضع معالجة التنظيف في finally. يُنفَّذ finally حتّى مع Ctrl+C أو exit.
  • أبلِغ النجاح والفشل للخارج عبر exit code. عند التشغيل عبر -File تصبح قيمة exit رمز الخروج نفسه، ما يتيح الحكم على النجاح والفشل من Task Scheduler أو أدوات المراقبة.
  • لتكن إعادة المحاولة وفق المبادئ الثلاثة: الاقتصار على الأخطاء المؤقّتة، والتراجع الأُسّيّ ذو الحدّ الأعلى، وidempotent. أمّا الأخطاء الدائمة فأفشِلها فوراً وسلِّمها للإنسان.

مقالات ذات صلة

مجالات الاستشارة ذات الصلة

تتعامل شركة Komura Soft LLC مع مراجعة تصميم معالجة الأخطاء وإعادة المحاولة في الدُفعات الليليّة (batch) وسكربتات المعالجة الروتينيّة، وتحقيق الأعطال المتقطّعة من نوع «يُعامَل كنجاح رغم الفشل» أو «ينهار مرّة واحدة في الشهر فقط»، وتحسين جودة تشغيل أصول السكربتات القائمة.

المراجع

  1. Microsoft Learn، about_Error_Handling. حول التصنيفات الثلاثة (الخطأ غير المُنهِي، خطأ إنهاء الجملة، خطأ إنهاء السكربت)، وعدم دخول الأخطاء غير المُنهِية افتراضيّاً إلى catch/trap، وآليّة الترقية عبر -ErrorAction Stop (ActionPreferenceStopException و$_.Exception.ErrorRecord)، ومطابقة catch محدَّد النوع مع نوع الاستثناء الأصليّ، ومواصفات $? و$LASTEXITCODE، وعدم إنشاء رمز الخروج غير الصفريّ للأوامر الأصليّة لـ ErrorRecord افتراضيّاً، وسلوك $PSNativeCommandUseErrorActionPreference.  2 3 4 5 6 7 8 9 10 11 12 13 14 15

  2. Microsoft Learn، about_Preference_Variables. حول أنّ القيمة الافتراضيّة لـ $ErrorActionPreference هي Continue، وأولويّة معامل -ErrorAction لكلّ أمر على حدة، وسريان الإعداد على النطاق (scope) والنطاقات الفرعيّة، وأنّ القيمة الافتراضيّة لـ $PSNativeCommandUseErrorActionPreference هي $false، ومثال تعطيله مؤقّتاً داخل كتلة سكربت لأوامر مثل robocopy التي تستخدم رمز الخروج غير الصفريّ كمعلومة.  2 3 4 5 6 7

  3. Microsoft Learn، What’s New in PowerShell 7.4. حول أنّ الميزة التجريبيّة PSNativeCommandErrorActionPreference ($PSNativeCommandUseErrorActionPreference) أصبحت ميزة رسميّة (mainstream) في PowerShell 7.4.  2 3

  4. Microsoft Learn، Everything you wanted to know about exceptions. حول إمكانيّة الوصول إلى معلومات الاستثناء من $_ داخل كتلة catch، وأنّ الأمر المُذيَّل بـ -ErrorAction Stop وأخطاء Write-Error تصبح قابلة للمعالجة في catch، ونمط تحرير الموارد عبر try/finally.  2

  5. Microsoft Learn، about_Language_Keywords. حول أنّ الكلمة المفتاحيّة exit تضبط رمز الخروج وتنعكس أيضاً في $LASTEXITCODE، وأنّ السكربت المُشغَّل عبر pwsh -File يعيد المعامل الرقميّ لـ exit كرمز خروج، وأنّه في حال عدم وجود جملة exit يكون الرمز 0 عند الاكتمال الطبيعيّ و1 عند استثناء غير مُعالَج.  2 3

  6. Microsoft Learn، about_Pwsh. حول طريقة تحديد رمز الخروج عند التشغيل عبر -File، وأنّ التشغيل عبر -Command يحوِّل أيّ رمز خروج غير 0 أو 1 إلى 1، لذا يلزم exit $LASTEXITCODE للحفاظ على رمز الخروج.  2 3 4

  7. Microsoft Learn، about_Try_Catch_Finally. حول تركيب try/catch/finally، وكتلة catch محدَّدة النوع وتعدّد كتل catch، وتنفيذ كتلة finally عند النجاح والخطأ وكذلك عند الإيقاف بـ Ctrl+C أو exit داخل catch. 

  8. Microsoft Learn، Differences between Windows PowerShell 5.1 and PowerShell 7.x. حول أنّه في PowerShell 7 عُدِّل السلوك بحيث لا يصبح $? مساوياً لـ $false لمجرّد الكتابة إلى stderr، بل فقط عند رمز خروج غير صفريّ. 

  9. Microsoft Learn، about_Automatic_Variables. حول أنّ $LASTEXITCODE يحتفظ برمز خروج البرنامج الأصليّ أو السكربت، وأنّه عند الاستدعاء عبر pwsh -File تُضبَط قيمته إلى 1 عند الانتهاء باستثناء، أو إلى قيمة الكلمة المفتاحيّة exit، أو إلى 0 عند الاكتمال الطبيعيّ. 

  10. Microsoft Learn، Start-Transcript. حول تسجيل أوامر الجلسة وإخراج الطرفيّة (console) في ملفّ نصّيّ، والإضافة عبر -Append، ومكان الحفظ الافتراضيّ واسم الملفّ، والإيقاف عبر Stop-Transcript.  2

أحدث المقالات التي تشترك في نفس الوسوم. عمّق فهمك بمواضيع مرتبطة.

ترتبط هذه المقالة بشكل طبيعي بصفحات الخدمات التالية.

الأسئلة الشائعة

أسئلة شائعة حول موضوع هذه المقالة.

لماذا لا يدخل التنفيذ إلى catch رغم كتابة try/catch في PowerShell؟
لأنّ معظم الأخطاء التي تصدرها الـ cmdlets هي أخطاء غير مُنهِية (non-terminating error). فـ try/catch لا يلتقط سوى الأخطاء المُنهِية، أمّا الأخطاء غير المُنهِية فتعرض رسالة وتستمرّ في المعالجة، ولا تدخل إلى catch. القاعدة الثابتة للتعامل مع هذا هي إضافة -ErrorAction Stop إلى الأمر الذي تريد التقاطه (أو ضبط $ErrorActionPreference = 'Stop' في بداية السكربت). فهذا يرفع الخطأ غير المُنهِي إلى خطأ مُنهٍ، فيصبح قابلاً للتعامل معه في try/catch.
كيف يُفرَّق في الاستخدام بين $? و$LASTEXITCODE؟
$? قيمة منطقيّة (boolean) تشير إلى نجاح العمليّة الأخيرة، وتُضبَط سواء للـ cmdlets أم للأوامر الأصليّة (native commands). أمّا $LASTEXITCODE فهو رمز الخروج (exit code) الخاصّ بآخر برنامج أصليّ نُفِّذ (أو سكربت خرج عبر exit)، ولا يتغيّر مع أخطاء الـ cmdlets. عند الحكم على نجاح أو فشل أوامر خارجيّة مثل robocopy أو git، فإنّ الحكم عبر $LASTEXITCODE، الذي يتيح التحقّق حتّى من معنى رمز الخروج، هو الأضمن. انتبه إلى أنّ رمز الخروج غير الصفريّ للأوامر الأصليّة لا يدخل افتراضيّاً إلى catch.
كيف يمكن الحكم على نجاح سكربت PowerShell أو فشله من Task Scheduler؟
بتحديد رمز الخروج صراحةً عبر الكلمة المفتاحيّة exit في نهاية السكربت (وفي كتلة catch)، وبتشغيل المهمّة عبر pwsh -File (أو powershell.exe -File) من جانب Task Scheduler، ومراقبة قيمة «نتيجة التنفيذ الأخيرة» (Last Run Result). عند التشغيل عبر -File، تصبح القيمة المحدَّدة في exit هي رمز خروج العمليّة (process) نفسه؛ فإن لم يوجد exit يكون الرمز 0 عند الاكتمال الطبيعيّ و1 عند استثناء غير مُعالَج. أمّا التشغيل عبر -Command فيحوِّل أيّ رمز خروج غير 0 أو 1 إلى 1، لذا فإنّ التشغيل عبر -File هو القاعدة الثابتة إن أردتَ بناء التشغيل على أساس رمز الخروج.
على أيّ نوع من الأخطاء ينبغي إجراء إعادة المحاولة؟
اقصرها على الأخطاء المؤقّتة التي قد تتغيّر نتيجتها بإعادة المحاولة (كانقطاع الشبكة اللحظيّ، أو القفل المؤقّت للملفّ، أو انتظار بدء تشغيل خدمة). أمّا أخطاء الأعمال أو الأخطاء الدائمة، مثل بيانات الإدخال غير الصحيحة أو نقص الصلاحيّات أو خطأ في الإعداد، فستفشل مهما أُعيدت المحاولة، لذا لا تُعاد محاولتها بل يُفشَّل الأمر فوراً ويُبلَّغ به الإنسان عبر السجلّ (log) وexit code. وحتّى عند إعادة المحاولة، يفترض وضع حدّ أعلى لعدد المرّات والفاصل الزمنيّ، وتوسيع الفاصل عبر التراجع الأُسّيّ (exponential backoff)، وتصميم المعالجة بحيث تكون idempotent (أي لا تسبِّب معالجة مزدوجة عند إعادة التنفيذ).
ما الذي يفعله إعداد $PSNativeCommandUseErrorActionPreference في PowerShell 7.4؟
إعداد يُصدِر خطأ PowerShell (خطأ غير مُنهٍ) عندما ينتهي أمر أصليّ (native command) برمز خروج غير صفريّ. أُضيف كميزة تجريبيّة في PowerShell 7.3، وأصبح ميزة رسميّة في 7.4 (والقيمة الافتراضيّة $false). عند ضبطه على $true فإنّه يتبع $ErrorActionPreference، فإذا جُمِع مع Stop أمكن التقاط فشل الأوامر الخارجيّة عبر try/catch. لكن بما أنّ بعض الأوامر مثل robocopy تستخدم رمز الخروج غير الصفريّ كمعلومة طبيعيّة، فمن الضروريّ التعامل بحذر، كإعادة ضبطه إلى $false في ذلك النطاق فقط.

الملف الشخصي للمؤلف

صفحة الملف الشخصي لمؤلف المقالة.

غو كومورا

مؤسّس شركة كومورا سوفت ذ.م.م.

يركّز على تطوير برامج ويندوز، والاستشارات التقنية، والتحقيق في الأخطاء، ويتميّز في المشاريع التي تبقى فيها الأصول القديمة ناشطة، وفي تشخيص الأعطال التي يصعب تحديد سببها.

روابط عامة

العودة إلى المدونة