كيفيّة إنشاء خدمات Windows وتشغيلها ── من التمييز عن جدولة المهام إلى تحويل BackgroundService إلى خدمة
· آخر تحديث: · غو كومورا · خدمة Windows, Windows, .NET, C#, BackgroundService, Generic Host, المعالجة المقيمة, التشغيل, جدول القرار, الاستشارات التقنية
«لم تعد جدولة المهام كلّ 5 دقائق كافية» أو «نريد تشغيل خادوم مراقبة مقيم ينتظر باستمرار بيانات قادمة من الجهاز» أو «نريد معالجة الملفّ خلال ثوانٍ قليلة من وضعه في المجلّد». عند متابعة استشارات التنفيذ الدوريّ، يصل الأمر دائماً إلى هذا الموضوع بعد أن تنمو المتطلّبات.
في هذه المدوّنة، كتبنا مقالات متتالية حول الأتمتة، مثل «أتمتة العمل عبر Power Automate» و«تصميم تشغيل آمن لجدولة المهام». وقد أشرنا في الفصل الثامن من مقال جدولة المهام إلى الخط الفاصل: «عندما يلزم الاستقصاء (polling) بالدقيقة أو المراقبة المستمرّة، فذلك مجال العمليّة المقيمة (resident process)»، لكن كيف نُنشئ فعليّاً خدمة Windows وكيف نضعها موضع التشغيل الفعليّ؟ إن تُرك هذا الأمر غامضاً واكتُفي بـ«تحويلها إلى خدمة على أيّ حال»، تنتج «خدمة لا يمكن إيقافها»، و«خدمة تسقط دون أن يلاحظ أحد»، و«خدمة تعمل بحساب LocalSystem الذي يمنحها كلّ الصلاحيّات».
في هذا المقال، سنرتّب بالترتيب الذي يُقرَّر به الأمر في العمل الفعليّ: جدول القرار حول ما إذا كان ينبغي تحويل المعالجة المقيمة إلى خدمة Windows، والحدّ الأدنى من المعرفة بآليّة عمل الخدمات (SCM، وأنواع بدء التشغيل، وSession 0)، والتنفيذ عبر Worker Service في .NET 8، والتسجيل عبر sc.exe وخيارات الاسترداد، واختيار حساب التشغيل، وأخيراً معالجة الإيقاف الآمن.
1. الخلاصة أوّلاً
- محور التمييز بسيط. إذا كانت المعالجة الدوريّة بفاصل زمنيّ لا يقلّ عن بضع دقائق، فجدولة المهام كافية. أمّا إذا دخل ضمن المتطلّبات الانتظار الدائم (المقابس، الأنابيب المسمّاة named pipe، FileSystemWatcher)، أو الاستجابة بالثانية، أو الاسترداد التلقائيّ عند السقوط، فحينها خدمة Windows هي الخيار.
- ابتداءً من Windows Vista، تعمل الخدمات ضمن Session 0، ولا يمكنها التفاعل مباشرةً مع المستخدم (لا يمكنها عرض واجهة مستخدم). إذا لزمت واجهة مستخدم، يُصمَّم الأمر بفصل الخدمة عن تطبيق الواجهة والتخاطب بينهما عبر الاتّصال بين العمليّات (IPC).1
- الطريقة الرسميّة للإنشاء في .NET هي قالب Worker Service +
AddWindowsServiceمن حزمةMicrosoft.Extensions.Hosting.WindowsServices(أوUseWindowsServiceإذا كنتَ تستخدم سلسلةIHostBuilder). وبما أنّه يمكن تشغيله كتطبيق سطر أوامر (console) كما هو، أصبح التطوير والتصحيح أسهل بكثير مقارنةً بتطوير الخدمات التقليديّ.2 - ابتداءً من .NET 6، الاستثناء غير المُعالَج داخل
BackgroundServiceيُوقِف المضيف (host) بالإعداد الافتراضيّ (BackgroundServiceExceptionBehavior.StopHost). لكن بما أنّ هذا «إيقاف طبيعيّ»، فإنّ إعادة التشغيل عبر خيار الاسترداد الخاصّ بـ SCM لا تعمل. الفشل الذي تريد إعادة التشغيل عنده يجب أن يُسقِط العمليّة برمز خروج غير صفريّ.32 - لا تجعل حساب التشغيل LocalSystem بدافع الكسل. الإعداد الافتراضيّ لـ
sc.exe createهو LocalSystem، فإن لم تنتبه ستبدأ الخدمة بالعمل بأقوى صلاحيّة ممكنة.4 إن كانت المعالجة محليّة بالكامل فـ LocalService أو حساب افتراضيّ (virtual account) هو الخيار، وإن كانت تلامس مشاركات أو قواعد بيانات ضمن نطاق (domain)، فـ gMSA هو الترشيح الأساسيّ.5 - خيار الاسترداد (
sc.exe failure) ومعالجة الإيقاف (HostOptions.ShutdownTimeout، والقيمة الافتراضيّة 30 ثانية6) يُصمَّمان وقت التسجيل. «الخدمة التي لا تستجيب للإيقاف» هي أكثر ما يُكرَه في التشغيل الفعليّ.
2. متى ينبغي تحويلها إلى خدمة ── جدول قرار بين جدولة المهام والخدمة والتطبيق المقيم
عند ظهور متطلّبات تشبه المعالجة المقيمة، تكون الخيارات ثلاثة: جدولة المهام، وخدمة Windows، و«التطبيق المقيم المُسجَّل في بدء التشغيل» (النوع الذي يقيم في منطقة الإشعارات). لنعرض خصائصها.
| المعيار | جدولة المهام | خدمة Windows | تطبيق مقيم (مُسجَّل في بدء التشغيل) |
|---|---|---|---|
| توقيت البدء | يبدأ عند كلّ وقت أو حدث محدَّد | مقيم منذ إقلاع نظام التشغيل (قبل تسجيل الدخول) | منذ تسجيل دخول المستخدم |
| هل يلزم تسجيل الدخول | يمكن الاستغناء عنه (تنفيذ غير تفاعليّ) | غير مطلوب | مطلوب (يختفي عند تسجيل الخروج) |
| واجهة المستخدم | لا يمكن عرضها (في التكوين غير التفاعليّ) | لا يمكن عرضها (Session 0) | يمكن عرضها (منطقة الإشعارات، مربّعات الحوار) |
| دائم أم دوريّ | دوريّ (يناسب الفواصل الزمنيّة بالساعة إلى اليوميّة) | دائم | دائم (لكن ضمن جلسة المستخدم) |
| الصلاحيّات | حساب تنفيذ محدَّد لكلّ مهمّة | حساب خدمة (الفصل 6) | صلاحيّات المستخدم المسجّل دخوله |
| المراقبة والاسترداد | سجلّ + إشعار ذاتيّ | خيار الاسترداد الخاصّ بـ SCM (إعادة تشغيل تلقائيّة) | لا يوجد (يُبنى ذاتيّاً) |
| عبء التوزيع | تسجيل عبر XML / PowerShell | sc.exe / برنامج تثبيت | تسجيل في مفتاح Run وما شابه |
محاور القرار هي كالتالي.
- إذا كانت المعالجة الدوريّة بمعدّل عدّة مرّات في اليوم إلى فواصل بالساعة، ولا تحتفظ بحالة بين مرّة وأخرى، فجدولة المهام هي الخيار. تحويلها عمداً إلى خدمة وإدارة مؤقّت ذاتيّ هنا مبالغة، وتصميم التشغيل المكتوب في «مقال جدولة المهام» أرخص بكثير.
- إذا كان جوهر الأمر هو الانتظار الدائم─ انتظار الطلبات عبر TCP أو الأنابيب، أو مراقبة مجلّد عبر FileSystemWatcher، أو معالجة قائمة انتظار (queue) تباعاً، أو الاستجابة بالثانية ─ فخدمة Windows هي الخيار. وإذا بدأتَ بتقطيع الاستقصاء (polling) عبر «مهمّة كلّ 5 دقائق»، فتلك إشارة إلى أنّك تُعيد تنفيذ عمليّة مقيمة بشكل متدهور.
- إذا كانت واجهة المستخدم هي جوهر الأمر (التحكّم من منطقة الإشعارات، النوافذ المنبثقة للمستخدم)، فالتطبيق المقيم هو الخيار. لكنّه لا يعمل دون تسجيل الدخول، فلا يصلح للاستخدامات الشبيهة بالخادم. وإذا كانت المتطلّبات «معالجة خلفيّة دائمة، مع الحاجة إلى واجهة مستخدم أيضاً»، فيُقسَم الأمر إلى عمليّتين، خدمة وتطبيق واجهة، كما في الفصل التالي.
ثمّة عامل قرار آخر يُغفَل عنه غالباً، وهو الاسترداد. مهمّة جدولة المهام، إن فشلت، تُترك حتّى الموعد التالي، أمّا الخدمة فيتولّى SCM إعادة تشغيلها تلقائيّاً (الفصل 5). ومتطلّب مثل «نريدها أن تستعيد نفسها تلقائيّاً بحلول الصباح حتّى لو سقطت ليلاً» يكفي وحده مبرّراً لتحويلها إلى خدمة.
3. الحدّ الأدنى من المعرفة بآليّة عمل الخدمات ── SCM وأنواع بدء التشغيل وSession 0
3.1 SCM وأنواع بدء التشغيل
الجهة المسؤولة عن خدمات Windows هي مدير التحكّم بالخدمات (Service Control Manager - SCM). يدير سجلّ الخدمات، ويتوسّط طلبات البدء والإيقاف، وينفّذ سلوك الاسترداد عند الفشل. يجب أن تكون عمليّة الخدمة مبنيّة بحيث تستطيع التخاطب مع SCM وفق البروتوكول المتّفق عليه، لكنّ هذا الجزء تتكفّل به مكتبات .NET نيابةً عنّا (الفصل 4)، فما نُقرّره نحن كتصميم هو أربع نقاط: «نوع بدء التشغيل»، و«حساب التنفيذ»، و«الاسترداد»، و«الإيقاف».
أنواع بدء التشغيل (نوع Startup) أربعة خيارات.4
| نوع بدء التشغيل | السلوك | موضع الاستخدام |
|---|---|---|
| تلقائيّ (auto) | يبدأ عند إقلاع نظام التشغيل | الأساس للخدمات الدائمة التشغيل |
| تلقائيّ متأخّر (delayed-auto) | يبدأ بعد الخدمات التلقائيّة الأخرى بقليل | الخيار الأوّل للخدمات المؤسّسيّة. يتجنّب ازدحام لحظة الإقلاع وعدم جاهزيّة الجهات المُعتمَد عليها |
| يدويّ (demand) | يبدأ فقط عند الطلب | خدمة مساعدة تُشغَّل من تطبيق آخر |
| معطَّل (disabled) | لا يمكن بدء تشغيله | إجراء إيقاف أو ختم |
اجعل البدء المتأخّر هو الإعداد الافتراضيّ للخدمات المؤسّسيّة. فمباشرةً بعد إقلاع نظام التشغيل، لا تكون الشبكة ولا قاعدة البيانات ولا الخدمات الأخرى جاهزة بعد، لذا فإنّ البدء الأسرع عبر «تلقائيّ» يميل إلى فشل الاتّصال الأوّل. الاستقرار يأتي من نهج مزدوج: إحداث فارق زمنيّ عبر البدء المتأخّر، ثمّ امتصاص فشل الاتّصال عبر إعادة المحاولة داخل ExecuteAsync كما سيُشرَح لاحقاً.
هناك أمر آخر يجب معرفته، وهو مهلة البدء. لا ينتظر SCM تقرير اكتمال بدء الخدمة إلى ما لا نهاية. إذا تجاوز الأمر المهلة الافتراضيّة (ServicesPipeTimeout، 30 ثانية)، يُسجَّل الحدثان 7000 / 7011 وتُعامَل الخدمة على أنّها فشلت في البدء.7 بعبارة أخرى، لا يجوز إجراء تهيئة ثقيلة كإعادة محاولة الاتّصال بقاعدة البيانات أو بناء ذاكرة تخزين مؤقّت كبيرة داخل «معالجة البدء». القاعدة الذهبيّة هي إنهاء البدء بسرعة، وترك المعالجة الثقيلة لحلقة الجسم الرئيسيّ (ExecuteAsync).
3.2 عزل Session 0 ── الخدمة لا تستطيع عرض واجهة مستخدم
ابتداءً من Windows Vista، تعمل الخدمات ضمن جلسة معزولة تُسمّى Session 0، ولا يمكنها التفاعل مباشرةً مع المستخدم.1 استدعاء MessageBox.Show أو عرض نموذج (form) داخل الخدمة لا يُظهِر شيئاً على شاشة المستخدم المسجَّل دخوله. بل إنّ ذلك يتحوّل إلى سبب كلاسيكيّ للتعليق (hang)، إذ تنتظر المعالجة بأكملها زرّ موافقة لا يستطيع أحد الضغط عليه. عند نقل شيفرة قديمة إلى خدمة، تأكّد دائماً من عدم بقاء مربّعات حوار كانت مخصّصة لعرض الأخطاء.
الحلّ الصحيح عند الحاجة إلى واجهة مستخدم هو تكوين يفصل العمليّة إلى خدمة (Session 0) وتطبيق واجهة (جلسة المستخدم)، ويتخاطبان عبر الاتّصال بين العمليّات (IPC). هذا تصميم توصي به Microsoft نفسها، حيث يُستخدَم IPC كالأنابيب المسمّاة، ويُعيد تطبيق الواجهة النتيجة إلى الخدمة.1 أمّا اختيار وسيلة IPC، فقد رتّبناه في المقال الشقيق المنشور في اليوم نفسه «جدول قرار الاتّصال بين العمليّات». ووضع تطبيق الواجهة أيضاً على Generic Host يتيح توحيد بنية DI والسجلّ (log) والإعدادات بين العمليّتين (راجع «استخدام Generic Host وBackgroundService في تطبيق سطح مكتب»).
4. طريقة الإنشاء في .NET ── Worker Service وAddWindowsService
4.1 القالب وProgram.cs
يبدأ تطوير الخدمات في .NET من قالب Worker Service. بما أنّه مُضمَّن في الـ SDK، يمكن إنشاء الهيكل الأساسيّ عبر dotnet new worker، ثمّ إضافة حزمة Microsoft.Extensions.Hosting.WindowsServices واستدعاء AddWindowsService فقط، لتصبح جاهزة للعمل كخدمة Windows.2 وآليّة عمل Generic Host التي تشكّل الأساس مشروحة في «ما هو .NET Generic Host».
using App.MonitorService;
using Microsoft.Extensions.Hosting;
HostApplicationBuilder builder = Host.CreateApplicationBuilder(args);
// دمج دورة الحياة (lifetime) التي تتخاطب مع SCM عند بدء التشغيل كخدمة Windows.
// عند بدء التشغيل كتطبيق سطر أوامر (console)، يستمرّ العمل بدورة ConsoleLifetime المعتادة
builder.Services.AddWindowsService(options =>
{
options.ServiceName = "KsMonitor";
});
builder.Services.AddSingleton<MeasurementQueue>();
builder.Services.AddHostedService<MonitorWorker>();
IHost host = builder.Build();
host.Run();
في المشاريع ذات النمط التقليديّ التي تستخدم Host.CreateDefaultBuilder، استدعِ بدلاً من ذلك امتداد IHostBuilder وهو UseWindowsService(). الدور نفسه.
ثمّة فخّ من النوع نفسه الذي ظهر في مشكلة «البدء (خيارات)» بمقال جدولة المهام. الدليل الحاليّ (current directory) عند التشغيل كخدمة هو C:\Windows\System32. فإذا كانت قراءة appsettings.json وأمثاله تفترض مساراً نسبيّاً، ستقع في مشكلة «يعمل على وحدة التحكّم لكن الإعدادات لا تُقرَأ كخدمة». اجعل حلّ ملفّ الإعدادات مبنيّاً على مسار الملفّ التنفيذيّ، أو مرِّر --contentRoot إلى binpath عند التسجيل لتحديد جذر المحتوى صراحةً، كما توصي التعليمات الرسميّة.2
4.2 ExecuteAsync ── تصميم stoppingToken والاستثناءات
يُكتَب الجسم الرئيسيّ في ExecuteAsync الذي يرث من BackgroundService. ما يجب تقريره هنا يقتصر على نقطتين: «الاستجابة لطلب الإيقاف» و«كيفيّة معالجة الاستثناءات».
namespace App.MonitorService;
public sealed class MonitorWorker(
MeasurementQueue queue,
ILogger<MonitorWorker> logger) : BackgroundService
{
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
try
{
while (!stoppingToken.IsCancellationRequested)
{
try
{
// أخرِج عنصراً واحداً من قائمة الانتظار وعالِجه. مرِّر الرمز (token) دائماً إلى الانتظار أيضاً
var item = await queue.DequeueAsync(stoppingToken);
await ProcessAsync(item, stoppingToken);
}
catch (Exception ex) when (ex is IOException or TimeoutException)
{
// فشل يمكن الاستمرار بعده: سجِّله ثمّ أعِد المحاولة بعد فاصل زمنيّ
logger.LogError(ex, "فشلت المعالجة. ستُعاد المحاولة بعد 30 ثانية.");
await Task.Delay(TimeSpan.FromSeconds(30), stoppingToken);
}
}
}
catch (OperationCanceledException) when (stoppingToken.IsCancellationRequested)
{
// طلب إيقاف من SCM. هذا تدفّق طبيعيّ، فلا داعي لفعل شيء.
// سبب حصر جملة when بالإلغاء الناتج عن stoppingToken تحديداً هو
// منع الخلط بين إلغاء ناتج عن مهلة معالجة داخليّة أو ما شابه وبين «إيقاف طبيعيّ»،
// وهو خلط قد يترك العامل (worker) متوقّفاً بصمت
// بينما يظلّ المضيف (host) قيد التشغيل
}
catch (Exception ex)
{
// فشل غير متوقّع: مع سلوك StopHost الافتراضيّ، يتوقّف المضيف «بشكل طبيعيّ»،
// فلا يُفعَّل خيار الاسترداد الخاصّ بـ SCM (إعادة التشغيل التلقائيّة).
// أسقِط العمليّة برمز خروج غير صفريّ بدلاً من ذلك، لإخبار SCM بأنّ فشلاً حقيقيّاً وقع
logger.LogCritical(ex, "خطأ غير قابل للاسترداد؛ سيتمّ إنهاء الخدمة.");
Environment.Exit(1);
}
}
private Task ProcessAsync(Measurement item, CancellationToken token)
=> throw new NotImplementedException();
}
- مرِّر
stoppingTokenإلى كلّ عمليّة انتظار.Task.Delay، والإدخال/الإخراج، وانتظار قائمة الانتظار. وجود انتظار طويل واحد فقط يتجاهل الرمز (token) كافٍ لأن يصبح مرتعاً لـ«خدمة لا تستجيب للإيقاف» (الفصل 7). - افصل بين الفشل القابل للاستمرار والفشل غير القابل للاسترداد. انقطاع الشبكة أو المهلة الزمنيّة المؤقّتة تُلتقَط داخل الحلقة، وتُسجَّل، ويُعاد المحاولة. أمّا
catch (Exception)الذي يكتفي بابتلاع الاستثناء وتنفيذcontinue، فلا يُنتِج سوى خدمة تدور في فراغ بصمت. طريقة التفكير في كيفيّة التصنيف مرتَّبة في «جدول قرار الإنهاء أو الاستمرار عند استثناء غير متوقّع». - اعرف السلوك الافتراضيّ للاستثناء غير المُعالَج. قبل .NET 6، كان الاستثناء المتسرّب من
ExecuteAsyncيختفي في الظلام، فتبدو الخدمة تعمل بينما لا تفعل شيئاً، أي تصبح «زومبي». ابتداءً من .NET 6، أصبح الإعداد الافتراضيّBackgroundServiceExceptionBehavior.StopHost، حيث يُسجَّل الاستثناء في السجلّ ثمّ يتوقّف المضيف.3 لكن بما أنّ هذا الإيقاف «طبيعيّ»، فلن تُعاد الخدمة للتشغيل حتّى لو ضُبط خيار الاسترداد. الفشل الذي تريد أن يستعيد فيه خيار الاسترداد الخدمة يجب إسقاطه صراحةً بخروج غير طبيعيّ عبرEnvironment.Exit(1)كما في الشيفرة أعلاه، وهذا هو الأسلوب الموافق للتعليمات الرسميّة.2
4.3 يمكن تشغيله كوحدة تحكّم كما هو ── السبب الأكبر لسهولة التطوير
لا يتحوّل AddWindowsService إلى دورة حياة (lifetime) خاصّة بالخدمة إلّا عند التشغيل كخدمة Windows فعليّاً. أي أنّ الملفّ التنفيذيّ نفسه يعمل كتطبيق سطر أوامر (console) عاديّ عند تشغيله عبر F5 في Visual Studio أو dotnet run. تعمل نقاط التوقّف (breakpoints) أيضاً، ويمكن التحقّق محليّاً من تدفّق StopAsync بدءاً من طلب الإيقاف عبر الضغط على Ctrl+C.
لكنّ عمل الأمر كوحدة تحكّم لا يعني بالضرورة عمله كخدمة أيضاً. اختلاف حساب التنفيذ (الفصل 6)، وSession 0، والدليل الحاليّ (current directory) ─ هذه النقاط الثلاث فقط تحتاج تحقّقاً على جهاز فعليّ كخدمة. سبب «يعمل كوحدة تحكّم لكن لا يعمل كخدمة» يعود غالباً إلى هذه النقاط الثلاث.
5. التسجيل والتشغيل ── sc.exe وخيار الاسترداد وسجلّ الأحداث
5.1 التسجيل عبر sc.exe create
يُسجَّل الملفّ التنفيذيّ المنشور (عبر dotnet publish؛ النشر بملفّ واحد أسهل تعاملاً) في SCM عبر sc.exe create. نفّذ الأمر عبر PowerShell كمسؤول.
sc.exe create "KsMonitor" binpath= "C:\Services\KsMonitor\KsMonitor.exe" start= delayed-auto obj= "NT AUTHORITY\LocalService" displayname= "خدمة استيعاب بيانات القياس KS"
sc.exe description "KsMonitor" "خدمة مقيمة تستوعب بيانات القياس وتسجّلها في قاعدة البيانات (المسؤول: قسم نظم المعلومات)"
لنذكر الفخّ الكلاسيكيّ أوّلاً. يلزم وجود مسافة بعد علامة المساواة في binpath= وstart=. الاسم حتّى علامة المساواة هو اسم الخيار، والمسافة بينه وبين القيمة إلزاميّة نحويّاً، وهذه مواصفة خاصّة بـ sc.exe (يفشل الأمر إن حُذفت).4 كذلك، الإعداد الافتراضيّ عند حذف obj= هو LocalSystem.4 فالتسجيل دون تفكير يجعل الخدمة تبدأ بأقوى صلاحيّة، لذا أنجِز اختيار الفصل 6 وقت التسجيل.
ضبط description مهمّ رغم بساطته الظاهريّة. ترك معلومات تتيح لمن يفتح services.msc بعد سنوات أن يحكم «ما هذه الخدمة، وهل يجوز إيقافها، ومن يُسأل عنها» يُلغي تكلفة التحقيق لاحقاً. الحذف يكون بعد الإيقاف عبر sc.exe delete "KsMonitor".2
إذا كانت وجهات التوزيع عدّة أجهزة، أو كان هناك تحديث دوريّ (استبدال ملفّات)، فبدّل مبكّراً إلى تكوين يتولّى فيه برنامج تثبيت (MSI أو ServiceInstall في WiX مثلاً) التسجيل والتحديث والحذف معاً، بدل العمل اليدويّ عبر sc.exe. فدليل التسجيل اليدويّ سيُنتِج حتماً جهازاً تخطّى خطوة واحدة.
5.2 خيار الاسترداد ── اترك «إعادة التشغيل عند السقوط» لـ SCM
من أبرز فوائد تحويل المعالجة إلى خدمة هو خيار الاسترداد القياسيّ الخاصّ بـ SCM. يمكن ضبط السلوك عند الإنهاء غير الطبيعيّ للعمليّة بشكل تصريحيّ (declarative).2
sc.exe failure "KsMonitor" reset= 86400 actions= restart/60000/restart/60000/restart/300000
هذا المثال يعني: «الفشل الأوّل والثاني، إعادة تشغيل بعد 60 ثانية؛ الفشل الثالث فما بعده، إعادة تشغيل بعد 5 دقائق؛ ويُعاد ضبط عدّاد الفشل كلّ 24 ساعة». يمكن إجراء الضبط نفسه عبر الواجهة الرسوميّة (خصائص services.msc ← تبويب «الاسترداد»). استخدم هذه الميزة القياسيّة أوّلاً قبل بناء «مهمّة مراقبة» أو نصّ مراقبة خاصّ بك.
نلفت الانتباه إلى نقطتين. أوّلاً، كما في الفصل السابق، لا يُفعَّل الاسترداد إلّا إذا انتهت العمليّة برمز غير صفريّ. الإيقاف الطبيعيّ عبر StopHost ليس مشمولاً بالاسترداد. ثانياً، إذا كانت الحالة تتسبّب في السقوط حتماً فور البدء (خطأ إعداد، تعذّر الوصول إلى قاعدة البيانات)، فسينتج حلقة إعادة تشغيل. لهذا السبب يُجعَل الفاصل الزمنيّ من المحاولة الثالثة فصاعداً أطول، مع الحرص مسبقاً على تسجيل «سبب السقوط» في سجلّ الأحداث (القسم التالي).
5.3 استخدام سجلّ الأحداث وسجلّ الملفّات معاً
يضيف Host.CreateApplicationBuilder تلقائيّاً مزوِّد سجلّ EventLog على Windows. وما يصل إلى سجلّ الأحداث بالإعداد الافتراضيّ هو مستوى Warning فما فوق فقط.2 كثيراً ما يحدث ارتباك من نوع «بمجرّد التحويل إلى خدمة، اختفت كلّ سجلّات Information»، لكنّها لم تختفِ، بل تصطدم بالمرشِّح الافتراضيّ لسجلّ الأحداث. إن أردتَ تسجيل أحداث تشغيليّة كالبدء والإيقاف أيضاً، حدِّد مستوى مزوِّد EventLog صراحةً في appsettings.json.
{
"Logging": {
"EventLog": {
"SourceName": "KsMonitor",
"LogLevel": {
"Default": "Warning",
"Microsoft.Hosting.Lifetime": "Information"
}
}
}
}
ثمّة نقطة تحتاج انتباهاً. مصدر الحدث (event source) لا يمكن الكتابة إليه ما لم يكن مُسجَّلاً مسبقاً، ويتطلّب التسجيل صلاحيّة المسؤول. وحذف SourceName لا يجعلك تفلت من الأمر ─ لأنّ AddWindowsService يجعل اسم التطبيق هو اسم المصدر بالإعداد الافتراضيّ2، فتبدأ في كلتا الحالتين من «مصدر غير مُسجَّل». والحساب ذو الصلاحيّة الدنيا كـ LocalService لا يستطيع إنشاء المصدر أثناء التشغيل، وإذا فشل الإنشاء، يبدأ التشغيل الفعليّ دون أن يُكتَب أيّ شيء في سجلّ الأحداث. أنجِز التسجيل من جهة برنامج التثبيت (أو معالجة الإعداد بصلاحيّة المسؤول). وهذا الكلام نفسه الذي كُتب في الفصل 6 من «مقال جدولة المهام» بعنوان «افصل CreateEventSource إلى جهة الإعداد».
المعيار التقريبيّ للتمييز هو: سجلّ الأحداث لِـ«ما يجب أن يراه المسؤول عن التشغيل فقط» (البدء، الإيقاف، الفشل، الاسترداد)، وتتبّع تفاصيل المعالجة لسجلّ ملفّات خاصّ بك. الحدّ الأدنى من المتطلّبات عند امتلاك سجلّ ملفّات خاصّ (التدوير rotation، والسلوك عند فشل الكتابة) مُجمَّع في «الحدّ الأدنى لمتطلّبات مسجِّل ذاتيّ الصنع»، وتصميم ترك دليل عند سقوط العمليّة بأكملها في «تصميم يترك السجلّات وملفّات dump عند الانهيار». ففي التكوين الذي يعيد التشغيل تلقائيّاً عبر خيار الاسترداد، يكون وجود سجلّ للحظة السقوط في مكانه هو الدليل الوحيد المتاح.
6. حساب التنفيذ ── لا تختر LocalSystem بدافع الكسل
تعمل الخدمة ضمن السياق الأمنيّ (security context) للحساب المحدَّد، حيث يسجّل SCM الدخول بهذا الحساب عند البدء ويُلحِق رمزاً (token) بالعمليّة.8 هذه هي النسخة الخاصّة بالخدمات من موضوع «مَن يُنفِّذ العمليّة» المكتوب في الفصل 3 من مقال جدولة المهام. لنعرض الخيارات.
| الحساب | الصلاحيّات | الهويّة على الشبكة | إدارة كلمة المرور | موضع الاستخدام |
|---|---|---|---|---|
| LocalSystem | قويّة جدّاً (بمستوى نظام التشغيل) | حساب الحاسوب | غير مطلوبة | تجنّبه من حيث المبدأ. انتبه لأنّه الإعداد الافتراضيّ لـ sc.exe create |
| LocalService | حدّ أدنى | مجهول (لا يمكنه الوصول إلى المشاركات) | غير مطلوبة | الخيار الأوّل للمعالجة المحليّة بالكامل |
| NetworkService | حدّ أدنى | حساب الحاسوب | غير مطلوبة | معالجة خفيفة تلامس موارد ضمن نطاق (domain) |
| حساب افتراضيّ (NT SERVICE\اسم الخدمة) | بقدر ما مُنح | حساب الحاسوب | غير مطلوبة | يمكن منح ACL على مستوى الخدمة. أقلّ صلاحيّة للموارد المحليّة |
| gMSA | بقدر ما مُنح | gMSA نفسه | يديرها DC تلقائيّاً | الخيار الأساسيّ للخدمات التي تلامس مشاركات أو قواعد بيانات ضمن نطاق |
نقاط القرار ثلاث.
- لا تختر LocalSystem لمجرّد «أنّه يعمل». فأيّ ثغرة في الخدمة تتحوّل مباشرةً إلى استيلاء على الجهاز بأكمله. أمّا هل تلزم فعلاً صلاحيّة تعادل صلاحيّة المسؤول، فيمكن استخدام تنظيم «متى تلزم صلاحيّة المسؤول» كما هو. في أغلب الحالات، المطلوب لا يتجاوز «الكتابة إلى مجلّد محدَّد»، ويكفي لذلك منح ACL على المجلّد لحساب افتراضيّ (
NT SERVICE\KsMonitor). الحساب الافتراضيّ لا يحتاج إنشاءً ولا إدارة كلمة مرور.5 - فخّ المشاركات على الشبكة. يصبح LocalService مجهولاً على الشبكة، فيفشل الوصول إلى
\\server\share. أمّا NetworkService والحساب الافتراضيّ وLocalSystem فتظهر على الشبكة كحساب حاسوب (DOMAIN\اسم الجهاز$)5، لذا يلزم منح إذن المشاركة وNTFS معاً لحساب الحاسوب من جهة المشاركة. هذا هو الغالب وراء «منحتُ المستخدم إذناً لكنّ الخدمة وحدها لا تستطيع القراءة». حدِّد أوّلاً «بأيّ هويّة ستظهر على الشبكة»، ثمّ اضبط إعدادات جهة المشاركة. - إذا شُغِّلت بحساب مستخدم عاديّ، فليكن ذلك مع إدارة كلمة المرور. استخدام حساب مخصَّص يتطلّب حقّ «تسجيل الدخول كخدمة»، وإذا انتهت صلاحيّة كلمة المرور، يفشل تسجيل الدخول ويتعذّر بدء الخدمة.8 هذه هي النسخة الخاصّة بالخدمات من مشكلة «المهمّة تموت بصمت عند تغيير كلمة المرور». في بيئة نطاق (domain)، الحلّ الصحيح هو إزالة هذه المشكلة بالكامل عبر gMSA التي يدير النطاق كلمة مرورها تلقائيّاً (وهذه هي النتيجة نفسها الواردة في القسم 3.2 من مقال جدولة المهام).
7. الإيقاف الآمن ── ShutdownTimeout والمعالجة الجارية
لنُلخّص تدفّق الإيقاف. عندما يُصدِر SCM طلب إيقاف عبر services.msc أو sc.exe stop أو إغلاق نظام التشغيل، تحوِّل دورة حياة الخدمة في .NET الأمر إلى إيقاف المضيف (StopApplication)، ويُلغى stoppingToken، ويخرج ExecuteAsync، ثمّ يُستدعى StopAsync لكلّ خدمة. الوقت الذي ينتظره المضيف حتّى اكتمال سلسلة الإيقاف هذه هو HostOptions.ShutdownTimeout، وقيمته الافتراضيّة 30 ثانية.69 إذا لم يكفِ الوقت، يُفرَض الإيقاف دون انتظار اكتمال أعمال التنظيف.
إذا لم تكفِ 30 ثانية لإكمال كتابة عنصر جارٍ واحد، فمدِّد المهلة بالحساب العكسيّ من الوقت اللازم.
builder.Services.Configure<HostOptions>(options =>
{
// احسب المهلة عكسيّاً بناءً على أطول وقت لازم لإكمال كتابة العمل الجاري
options.ShutdownTimeout = TimeSpan.FromSeconds(90);
});
وبناءً على ذلك، تصميم معالجة الإيقاف يقوم على ثلاث نقاط.
- حدِّد مسبقاً ترتيب ما يُنفَّذ بعد طلب الإيقاف. التوقّف عن قبول عناصر جديدة ← إكمال المعالجة الجارية (أو إيقافها عند فاصل آمن وتسجيلها بشكل قابل للاستئناف) ← تنظيف الاتّصالات والملفّات المؤقّتة. دون تحديد هذا الترتيب، ستقع في أحد طرفي النقيض: التخلّي عن كلّ شيء فور رؤية الرمز (token)، أو محاولة إنجاز كلّ شيء وانتهاء المهلة.
- لا تُنشئ «خدمة لا تستجيب للإيقاف». حلقة طويلة واحدة أو انتظار واحد يتجاهل الرمز يكفيان لجعل الخدمة عالقة عند «قيد الإيقاف» في SCM. هذا خلل يُكرَه أكثر من غيره في التشغيل الفعليّ، إذ يعرقل إعادة تشغيل Windows Update ويتطلّب قتلاً (kill) يدويّاً في كلّ إيقاف مخطَّط للخادم. كما في الفصل 4، تحقّق من الرمز عند كلّ فاصل معالجة، ومرِّر
CancellationTokenإلى أيّ إدخال/إخراج طويل. ويمكن اختبار تدفّق الإيقاف هذا محليّاً عبر Ctrl+C عند التشغيل كوحدة تحكّم، ونوصي بإدراج «كم ثانية يستغرق الإنهاء بعد Ctrl+C» ضمن بنود الاختبار قبل الإصدار. - أعطِ الأولويّة لتصميم لا ينكسر حتّى عند القتل (kill)، أكثر من دقّة معالجة الإيقاف. انقطاع الكهرباء أو الإنهاء القسريّ يحدثان مهما أُتقِنت معالجة الإيقاف. المعاملات (transactions)، والكتابة الذرّيّة (atomic) للملفّات، ووحدات المعالجة القابلة لإعادة التشغيل ─ هذه هي الأساس، أي بنية «تتحقّق الاتّساق عند التشغيل التالي حتّى لو ماتت في المنتصف»، أمّا معالجة الإيقاف الدقيقة فهي مسألة حسن سلوك لا أكثر.
8. الخلاصة
القرار بسيط. المعالجة الدوريّة ← جدولة المهام، وإذا لزم الانتظار الدائم والاستجابة بالثانية والاسترداد التلقائيّ ← خدمة Windows، وإذا كانت واجهة المستخدم هي الجوهر ← تطبيق مقيم. وإن قرّرتَ تحويلها إلى خدمة، فالطريق القياسيّ الحاليّ هو «التطوير كوحدة تحكّم، والتوزيع كخدمة» عبر Worker Service الخاصّ بـ .NET وAddWindowsService.
ما يُقرَّر تصميماً وقت التسجيل أربع نقاط ─ نوع بدء التشغيل (البدء المتأخّر للخدمات المؤسّسيّة)، وحساب التنفيذ (تجنّب LocalSystem، واختيار حساب افتراضيّ أو gMSA)، وخيار الاسترداد (sc.exe failure مع Environment.Exit(1) كمجموعة واحدة)، والإيقاف (ShutdownTimeout وتنظيف المعالجة الجارية). بمجرّد ضبط هذه النقاط الأربع، وكتابة ExecuteAsync بأسلوب يحترم stoppingToken، لا تعود خدمة Windows «شيئاً صعب الإنشاء بشكل خاصّ». وعلى العكس، الخدمة التي تبدأ العمل دون تقرير هذه النقاط الأربع تعود بعد سنوات بثلاثيّة المعاناة: «لا يمكن إيقافها، ولا يُلاحَظ سقوطها، والصلاحيّة قويّة جدّاً بحيث لا يمكن الاقتراب منها». وإعادة تأهيل الخدمات القائمة أيضاً أقصر طريق لها هو جرد هذه النقاط الأربع.
مقالات ذات صلة
- مهمّة جدولة المهام لا تُنفَّذ أو تنتهي بالرمز 0x1 ── تحديد السبب وتصميم تشغيل آمن
- ما هو .NET Generic Host
- جدول قرار الاتّصال بين العمليّات
- تصميم يترك السجلّات وملفّات dump عند الانهيار
مجالات الاستشارة ذات الصلة
تتعامل شركة Komura Soft LLC مع مراجعة تصميم المعالجة المقيمة (بما في ذلك قرار الانتقال من جدولة المهام)، والاستشارات حول تحقيق خدمات Windows «التي لا تستجيب للإيقاف» أو «تسقط دون أن يُلاحَظ ذلك» وإعادة تأهيلها.
المراجع
-
Microsoft Learn، Interactive Services. حول عدم قدرة الخدمات على التفاعل المباشر مع المستخدم ابتداءً من Windows Vista، وتشغيلها ضمن Session 0، والتصميم الموصى به بالتعاون مع تطبيق GUI في عمليّة منفصلة عبر IPC عند الحاجة إلى واجهة مستخدم. ↩ ↩2 ↩3
-
Microsoft Learn، Create Windows Service using BackgroundService. البرنامج التعليميّ الرسميّ لتحويل Worker Service إلى خدمة Windows. حول
AddWindowsService، وكون الإعداد الافتراضيّ لمزوِّد EventLog هو Warning، والتسجيل والاسترداد والحذف عبرsc.exe، وضرورة رمز خروج غير صفريّ لخيار الاسترداد. ↩ ↩2 ↩3 ↩4 ↩5 ↩6 ↩7 ↩8 ↩9 -
Microsoft Learn، .NET 6 breaking change: Exception handling in hosting. حول أنّ الاستثناء غير المُعالَج في
BackgroundService.ExecuteAsyncيُسجَّل في السجلّ ثمّ يوقِف المضيف بالإعداد الافتراضيّ ابتداءً من .NET 6 (BackgroundServiceExceptionBehavior.StopHost). ↩ ↩2 -
Microsoft Learn، sc.exe create. حول مواصفات
start=(auto / demand / delayed-auto وغيرها) وobj=(الإعداد الافتراضيّ LocalSystem)، وضرورة وجود مسافة بعد علامة المساواة في الخيارات (يفشل الأمر عند حذفها). ↩ ↩2 ↩3 ↩4 -
Microsoft Learn، Service Accounts in Windows Server. حول أنّ الحساب الافتراضيّ (
NT SERVICE\اسم الخدمة) لا يحتاج إدارة كلمة مرور ويصل إلى الشبكة ببيانات اعتماد حساب الحاسوب، وأنّ كلمة مرور gMSA يديرها النطاق تلقائيّاً. ↩ ↩2 ↩3 -
Microsoft Learn، .NET Generic Host. حول تدفّق معالجة إغلاق المضيف، والانتظار عند الإيقاف بمهلة إغلاق قابلة للضبط (الإعداد الافتراضيّ 30 ثانية). ↩ ↩2
-
Microsoft Learn، A service does not start, and events 7000 and 7011 are logged. حول انتظار SCM بدء الخدمة للمدّة المحدَّدة في
ServicesPipeTimeoutفقط، وتسجيل الحدثين 7000 / 7011 عند تجاوزها، وخطوات رفع المهلة الافتراضيّة. ↩ -
Microsoft Learn، Service User Accounts. حول تنفيذ الخدمة ضمن السياق الأمنيّ للحساب المحدَّد، وقيام SCM بتسجيل الدخول عند البدء، وتعذّر بدء الخدمة عند انتهاء صلاحيّة كلمة المرور، والحسابات الخاصّة LocalService / NetworkService / LocalSystem. ↩ ↩2
-
Microsoft Learn، HostOptions.ShutdownTimeout Property. حول تعريف الخاصيّة التي تتحكّم بالمهلة الافتراضيّة لـ
StopAsync. ↩
مقالات ذات صلة
أحدث المقالات التي تشترك في نفس الوسوم. عمّق فهمك بمواضيع مرتبطة.
التاريخ والوقت والمناطق الزمنية في تطبيقات الأعمال ── من فخاخ DateTime إلى مبدأ التخزين بتوقيت UTC وتصميم الاختبارات
ينزاح الوقت 9 ساعات بعد نقل الخادم ── نرتّب أسباب أعطال التاريخ والوقت انطلاقًا من خاصيّة Kind في DateTime والتحويل الضمنيّ. نشرح التمييز...
استخدام SQLite في تطبيقات C# للأعمال ── وضع WAL، والتحكّم الحصريّ، والوقاية من التلف، والتمييز عن EF Core
نرتّب هنا المعرفة العمليّة اللازمة لدمج SQLite في تطبيقات الأعمال عبر Microsoft.Data.Sqlite. نشرح وضع WAL، وSQLITE_BUSY وتوحيد مسار الكتا...
كيف تختار مكان حفظ بيانات تطبيق Windows محليّاً ── جدول قرار بين SQLite وJSON وRegistry وAccess
أين ينبغي حفظ بيانات تطبيق سطح مكتب Windows، وبأيّ صيغة؟ نرتّب الفرق بين AppData وProgramData، ونقاط قوّة وعيوب كلّ من SQLite وملفّات JSO...
التعامل الآمن مع تطبيق أعمال قديم بلا اختبارات ── الممارسة العمليّة لاختبار التوصيف وإعادة الهيكلة
لإجراء تعديلات آمنة على تطبيق أعمال بلا اختبارات، نشرح خطوات اختبار التوصيف (أسلوب Golden Master) الذي يُثبِّت السلوك الحاليّ، وكيفيّة صن...
إلى متى ستستمرّ تطبيقات VB6 في العمل ── وضع دعم بيئة التشغيل والمسار العمليّ للترحيل إلى .NET
إلى متى تستمرّ تطبيقات VB6 في العمل؟ يُنظّم هذا المقال التناقض بين سياسة دعم بيئة تشغيل VB6 (المدعومة حتّى على Windows 11) وحقيقة انتهاء ...
أين يتصل هذا الموضوع
ترتبط هذه المقالة بشكل طبيعي بصفحات الخدمات التالية.
تطوير تطبيقات ويندوز
ندعم تطوير برامج ويندوز للأعمال، وتكامل الأجهزة، وأدوات التواصل.
الأسئلة الشائعة
أسئلة شائعة حول موضوع هذه المقالة.
- كيف نميّز بين استخدام جدولة المهام وخدمة Windows؟
- إذا كانت المعالجة الدوريّة بفاصل زمنيّ لا يقلّ عن بضع دقائق، ولا تحتفظ بحالة بين مرّة وأخرى، فجدولة المهام كافية. أمّا إذا دخل ضمن المتطلّبات الانتظار الدائم عبر TCP أو الأنابيب، أو مراقبة مجلّد عبر FileSystemWatcher، أو الاستجابة بالثانية، أو الاسترداد التلقائيّ عند السقوط، فخدمة Windows هي الخيار. وإذا بدأتَ بتقطيع الاستقصاء (polling) عبر «مهمّة كلّ 5 دقائق»، فتلك إشارة إلى أنّك تُعيد تنفيذ عمليّة مقيمة بشكل متدهور. وإذا كانت واجهة المستخدم هي الجوهر (كالتحكّم من منطقة الإشعارات)، فالخيار الثالث هو تطبيق مقيم لا يعمل إلّا أثناء تسجيل الدخول.
- هل يمكن لخدمة Windows عرض واجهة مستخدم (شاشة)؟
- لا يمكن ذلك. ابتداءً من Windows Vista، تعمل الخدمات ضمن جلسة معزولة تُسمّى Session 0، ولا يمكنها التفاعل مباشرةً مع المستخدم. استدعاء MessageBox.Show داخل الخدمة لا يُظهِر شيئاً على شاشة المستخدم، بل يتحوّل إلى سبب تعليق (hang) حيث تتوقّف المعالجة منتظرةً زرّ موافقة لا يستطيع أحد الضغط عليه. الحلّ الصحيح عند الحاجة إلى واجهة مستخدم هو فصل العمليّة إلى خدمة وتطبيق واجهة يتخاطبان عبر الاتّصال بين العمليّات كالأنابيب المسمّاة. عند نقل شيفرة قديمة إلى خدمة، تأكّد دائماً من عدم بقاء مربّعات حوار كانت مخصّصة لعرض الأخطاء.
- كيف نُنشئ خدمة Windows في .NET؟
- الطريقة الرسميّة هي البدء من قالب Worker Service (dotnet new worker)، ثمّ إضافة حزمة Microsoft.Extensions.Hosting.WindowsServices واستدعاء AddWindowsService. وبما أنّ الملفّ التنفيذيّ نفسه يعمل كتطبيق سطر أوامر عاديّ عند التشغيل عبر F5 في Visual Studio أو dotnet run، يصبح التطوير والتصحيح أسهل بكثير. يتمّ التسجيل عبر sc.exe create، لكن يجب الانتباه إلى المواصفة الخاصّة التي تتطلّب مسافة بعد علامة المساواة في binpath= وstart=، وإلى أنّ حذف obj= يجعل الإعداد الافتراضيّ LocalSystem (أقوى صلاحيّة). والدليل الحاليّ (current directory) عند بدء الخدمة هو C:\Windows\System32، لذا يُحلّ ملفّ الإعدادات بناءً على مسار الملفّ التنفيذيّ.
- ماذا يحدث للخدمة عند وقوع استثناء في BackgroundService؟
- ابتداءً من .NET 6، الاستثناء غير المُعالَج المتسرّب من ExecuteAsync يُسجَّل في السجلّ ثمّ يوقِف المضيف بالإعداد الافتراضيّ (BackgroundServiceExceptionBehavior.StopHost). لكن بما أنّ هذا يُعامَل «إيقافاً طبيعيّاً»، فلا يُفعَّل خيار الاسترداد الخاصّ بـ SCM (إعادة التشغيل التلقائيّة). الفشل الذي تريد أن يستعيد فيه خيار الاسترداد الخدمة يجب إسقاط العمليّة صراحةً بخروج غير طبيعيّ ورمز غير صفريّ، كما في Environment.Exit(1). أمّا الفشل القابل للاستمرار كانقطاع الشبكة، فيُلتقَط داخل الحلقة ويُسجَّل ويُعاد المحاولة، مع تصميم يميّزه عن الفشل غير القابل للاسترداد.
الملف الشخصي للمؤلف
صفحة الملف الشخصي لمؤلف المقالة.
غو كومورا
مؤسّس شركة كومورا سوفت ذ.م.م.
يركّز على تطوير برامج ويندوز، والاستشارات التقنية، والتحقيق في الأخطاء، ويتميّز في المشاريع التي تبقى فيها الأصول القديمة ناشطة، وفي تشخيص الأعطال التي يصعب تحديد سببها.
روابط عامة