منع التشغيل المتعدّد لتطبيقات Windows ── Mutex المسمّى وتفعيل النافذة عند التشغيل المزدوج
· آخر تحديث: · غو كومورا · منع التشغيل المتعدّد, Mutex, Windows, .NET, C#, SetForegroundWindow, سطح المكتب البعيد, الأنابيب المسمّاة, تطوير Windows, الاستشارات التقنية
«شغَّلتُ تطبيقاً للعمل مرّة أخرى عن طريق الخطأ، فحرَّرتُ الملفّ نفسه في نافذتين، واختفت تعديلات إحداهما.» «انطلقت أداة مقيمة (resident tool) بنسختين، فعالجَت نفس الحدث مرّتين.» يُعدّ التشغيل المتعدّد لتطبيقات سطح المكتب موضوعاً هامشيّاً على ما يبدو، لكنّه في العمل الفعليّ يسبِّب حوادث بشكل غير متوقَّع. هيكل الحلّ نفسه بسيط: يكفي استخدام Mutex مسمّى للحكم على «هل أنا النسخة الأولى؟».
لكنّ هذا التنفيذ البسيط تحيط به مطبّات جانبيّة. فخّ مساحة الأسماء الذي يعطِّل المنع في بيئة سطح المكتب البعيد (Remote Desktop)، وقاعدة التحرير الخاصّة بـ Mutex والمختلفة عن كائنات المزامنة الأخرى، ومسألة تصميميّة تتعلّق بقيود نافذة المقدّمة (foreground window) في Win32 حول «كيف نُظهِر النسخة القائمة الموجودة في المقدّمة؟». يرتّب هذا المقال بشكل شامل الأساسيّات المتعلِّقة باكتشاف التشغيل المتعدّد عبر Mutex مسمّى، وصولاً إلى قرارات التصميم الجانبيّة اللازمة في العمل الفعليّ.
1. الخلاصة أوّلاً
- الصيغة الأساسيّة لاكتشاف التشغيل المتعدّد هي
new Mutex(true, name, out bool createdNew). لا يحدث استثناء حتّى لو كان هناكMutexبنفس الاسم موجوداً بالفعل، بل تصبح قيمةcreatedNewهيfalseفقط، لذا يُبنى التفريع على هذه القيمة.1 - إن لم تُضَف بادئة (prefix) إلى الاسم، يُنشَأ افتراضيّاً في مساحة أسماء
Local\(المحصورة بالجلسة). في بيئة سطح مكتب بعيد يملك فيها المستخدِم نفسه عدّة جلسات، يُعامَل كلّ جلسة كـMutexمنفصل، فلا يعمل منع التشغيل المتعدّد. إن أردتَ حصر التشغيل في نسخة واحدة عبر الجلسات، فبادئةGlobal\ضروريّة.23 - لا يمكن تحرير
Mutexإلّا من نفس الخيط (thread) الذي حصل عليه. استدعاءReleaseMutexمن خيط آخر يؤدّي إلىApplicationException. إن انتهت العمليّة المالكة دون تحريره، يُرسَلAbandonedMutexExceptionإلى الخيط التالي الذي يحصل عليه، وهذا إشارة إلى أنّ الانتظار نفسه نجح، والتعامل الصحيح هو عدم تجاهله والتحقّق من سلامة الحالة قبل الاستخدام.45 - بعد معرفة «أنّه يعمل بالفعل»، يصبح الموضوع الأساسيّ هو إظهار نافذة النسخة القائمة في المقدّمة. يقيِّد نظام التشغيل استدعاء
SetForegroundWindowمن غير العمليّة الموجودة في المقدّمة، فمجرَّد استدعائه ببساطة يفشل ويقتصر الأثر على وميض زرّ شريط المهام.6 الأسلوب المتَّبع هو تسليم «صلاحيّة ضبط المقدّمة» التي تملكها العمليّة الثانية المُشغَّلة للتوّ، إلى النسخة القائمة عبرAllowSetForegroundWindow.7 - لتمرير وسائط التشغيل (مثل مسار الملفّ الواجب فتحه) إلى النسخة القائمة، فإنّ النقل عبر أنبوب مسمّى (named pipe) هو الأسلوب المعتاد. نترك مقارنة الوسائل لمقال «كيف تختار وسيلة الاتّصال بين عمليّات Windows»، ونقتصر في هذا المقال على تصميم «الاكتشاف ← الإشعار ← التفعيل».
- تطبيقات سطر الأوامر والخدمات ليست أهدافاً لتطبيق تصميم هذا المقال مباشرةً. الخدمات تختلف من حيث المبدأ، لأنّ SCM لا يشغِّل أصلاً سوى نسخة واحدة من خدمة بنفس الاسم (الفصل 8).
2. أساسيّات الاكتشاف عبر Mutex مسمّى
Mutex كائن نواة (kernel object)، وعند إنشائه باسم معيّن، يمكن لأيّ عمليّة أخرى تعرف ذلك الاسم أن تشير إلى نفس الكائن. في اكتشاف التشغيل المتعدّد، نستخدم فقط هذه «المشاركة في الاسم».
using var mutex = new Mutex(initiallyOwned: true, name: MutexName, out bool createdNew);
if (!createdNew)
{
// إنّه يعمل بالفعل
return;
}
// هذا الخيط يملك Mutex فقط عندما تكون createdNew تساوي true
النقطتان اللتان ينبغي الانتباه إليهما هنا هما:
- لا يحدث استثناء حتّى لو كان
Mutexبنفس الاسم موجوداً بالفعل. تصبحcreatedNewبقيمةfalseفقط، وتُعاد إشارة إلى الكائن القائم. يجب أن يُبنى التفريع دائماً علىcreatedNew.1 initiallyOwned: trueفعّال فقط عند «نجاح الإنشاء الجديد». إن كان موجوداً بالفعل (createdNew == false)، لا يصبح هذا الخيط مالكاً تلقائيّاً. لا يمثِّل هذا الأثر الجانبيّ مشكلة في اكتشاف التشغيل المتعدّد، لكن لمنع حادث «استدعاءReleaseMutexخطأً من قِبَل العمليّة الثانية»، من الآمن أيضاً في فرعcreatedNewبقيمةfalseعدم لمسMutexإطلاقاً وإنهاء المعالجة فوراً.1
من المعتاد أن يكون الاسم نصّاً يتضمّن GUID خاصّاً بالمنتج، تجنّباً للتصادم مع تطبيقات شركات أخرى (مثل "KomuraSoft.MyApp.SingleInstance.{3F1E2B10-...}"). وكما هو الحال مع احتلال (squatting) اسم الأنبوب، فإنّ مساحة أسماء كائنات النواة المسمّاة مرئيّة أيضاً لعمليّات أخرى على الجهاز نفسه، لذا فإنّ اختيار اسم يصعب تخمينه له معنى.
3. Global\ وLocal\ ── ماذا يحدث عند عبور الجلسات
تنقسم مساحة أسماء كائنات نواة Windows إلى مساحة أسماء مستقلّة لكلّ جلسة، ومساحة أسماء عامّة (global) مشترَكة على مستوى النظام كلّه. عند إنشاء Mutex مسمّى، إن لم تُحدَّد بادئة، يُنشَأ افتراضيّاً في مساحة أسماء جلسة جهة الاستدعاء (Local\)، ولا يُنشَأ في مساحة الأسماء المشترَكة بين كلّ الجلسات إلّا عند إضافة Global\.3 يتبع صفّ Mutex في .NET هذا السلوك كما هو، وتنصّ الوثائق صراحةً على أنّ «Mutex المسمّى دون تحديد بادئة يحمل Local\ افتراضيّاً».2
يصبح هذا مشكلة في مواقف كهذه:
- مستخدِم سجَّل الدخول مباشرةً من الطرفيّة (console) على جهاز إداريّ للعمل، ويتّصل أيضاً بنفسه بجلسة أخرى عبر سطح المكتب البعيد (شائع في العمليّات التشغيليّة).
- المستخدِم نفسه يقطع اتّصال RDP ويعيد الاتّصال بشكل متكرِّر، وفي كلّ مرّة يُخصَّص له معرِّف جلسة (session ID) جديد.
عند إنشاء Mutex بـ Local\ كما هي (دون بادئة)، تُعامَل هذه كجلسات منفصلة، ويعمل منع التشغيل المتعدّد بشكل مستقلّ في كلّ جلسة. بعبارة أخرى، يصبح خللاً لا يعمل فيه منع التشغيل المتعدّد كما هو مقصود: «رغم أنّه نفس المستخدِم، يمكن التشغيل بشكل عاديّ من الجلسة الثانية». وبالعكس، في استخدام سطح مكتب عاديّ يفترض جلسة واحدة فقط، لا يوجد ضرر فعليّ من إبقاء Local\ (الافتراضيّ).
يتناول مقال «مدخل إلى ملفّات تعريف مستخدِمي Windows» التصميم العامّ للأجهزة التي يشترك فيها عدّة مستخدِمين، لكن بالاقتصار على موضوع هذا المقال يلزم الانتباه إلى ما يلي. Global\ مساحة أسماء واحدة مشترَكة بين جميع المستخدِمين وجميع الجلسات على الجهاز، ولا تعني تلقائيّاً «نسخة واحدة لكلّ مستخدِم». بمجرَّد إضافة Global\ مع إبقاء الاسم ثابتاً مثل Global\KomuraSoft.MyApp.SingleInstance، سيُعاق تشغيل المستخدِم B أيضاً بنفس Mutex أثناء تشغيل المستخدِم A، فيصبح الأمر فعليّاً «نسخة واحدة للجهاز بأكمله» (الصفّ الثالث من جدول الفصل 5). إن أردتَ تحقيق «نسخة واحدة لكلّ مستخدِم، مع تجميع جلسات ذلك المستخدِم المتعدّدة»، فيلزم إضافةً إلى Global\، تضمين معرِّف خاصّ بالمستخدِم (مثل SID) في الاسم. وبالعكس، إن أردتَ حصر النسخة الواحدة على مستوى الجهاز بأكمله (المشاركة بين كلّ المستخدِمين مقصودة)، فيمكن إبقاء Global\ دون تضمين SID.
4. قواعد تحرير Mutex ── الخيط المالك وAbandonedMutexException
يحمل Mutex قيداً لا تملكه كائنات مزامنة أخرى مثل Semaphore أو AutoResetEvent. وهو قاعدة تفرض معرِّف الخيط (thread ID)، وهي أنّه لا يمكن تحريره إلّا من نفس الخيط الذي حصل عليه.2 استدعاء ReleaseMutex من خيط آخر يُصدِر ApplicationException (رسالتها «الخيط المستدعي لا يملك الميوتكس»).4
في شيفرة تستخدم async/await في .NET، قد تُستأنَف المعالجة بعد await في خيط مختلف من مجمَّع الخيوط (thread pool). انتبه إلى أنّ كتابة شيفرة من نوع «الحصول على Mutex ثمّ إدراج معالجة غير متزامنة مباشرةً بعده، والتحرير في المتابعة» قد تخالف هذا القيد بصمت. في استخدام اكتشاف التشغيل المتعدّد، من الآمن حصر الحصول والتحرير كليهما في شيفرة متزامنة قصيرة.
نقطة انتباه أخرى هي الترك (abandoned). إن انتهى الخيط المالك لـ Mutex دون استدعاء ReleaseMutex (كانهيار العمليّة أو سقوطها باستثناء غير معالَج)، يصبح ذلك Mutex في حالة متروكة. يُرسَل AbandonedMutexException إلى الخيط التالي الذي يحصل على هذا Mutex، وهو استثناء يدلّ على أنّ الانتظار نفسه نجح، وأنّ جهة الاستدعاء حصلت بالفعل على حقّ ملكيّة Mutex.5 لا يحدث هذا عادةً في استخدام اكتشاف التشغيل المتعدّد وحده (لأنّه يبقى مُحتفَظاً به دون تحرير حتّى انتهاء العمليّة)، لكن إن استُخدِم نفس Mutex أيضاً في تحكّم إقصائيّ (exclusive control) آخر، فيُعامَل كالتالي مع الأخذ بعين الاعتبار احتمال تلف حالة الهدف المحمي.
try
{
if (mutex.WaitOne(TimeSpan.FromSeconds(5)))
{
// المعالجة العاديّة
}
}
catch (AbandonedMutexException)
{
// نجح الانتظار، وهذا الخيط حصل بالفعل على حقّ الملكيّة.
// افحص حالة الهدف المحمي قبل الاستخدام، أو أعِد التهيئة بأمان
}
الفصل بين «تجاهل الاستثناء» أو «فحص الحالة والمتابعة» أو «الاستسلام والإنهاء غير الطبيعيّ» يمكن تطبيقه مباشرةً من فكرة مقالنا «جدول القرار حول الإنهاء أو المتابعة عند استثناء غير متوقَّع». بما أنّ AbandonedMutexException استثناء يُخبِر صراحةً بـ«النطاق الذي قد يكون تالفاً»، فإنّ الأساس هو عدم تجاهله وفحص ذلك النطاق فقط (البيانات المحمية).
كما ينبغي استدعاء ReleaseMutex صراحةً داخل finally في المسارات التي تنتهي بشكل طبيعيّ. فعندما يختفي الخيط المالك بانتهاء العمليّة، يُعامَل Mutex على أنّه «متروك»، وما لم يُحرَّر صراحةً، سيتسبَّب ذلك في AbandonedMutexException غير ضروريّ عند التشغيل التالي.
5. جدول القرار ── في أيّ نطاق يُستخدَم Mutex
يختلف تصميم مساحة الأسماء وجانب الأنبوب باختلاف «الوحدة» التي تريد منع التشغيل المتعدّد على أساسها.
| الوحدة | السيناريو المفترَض | مساحة أسماء Mutex | تمرير وسائط التشغيل | نقاط الانتباه |
|---|---|---|---|---|
| على مستوى الجلسة (الافتراضيّ) | استخدام سطح مكتب عاديّ لا يعتمد RDP، أو يكفي فيه «نسخة واحدة لكلّ جلسة» | دون بادئة (= Local\) |
إضافةً إلى CurrentUserOnly، تضمين معرِّف الجلسة (session ID) في اسم الأنبوب |
في بيئة تجمع بين الاستخدام المحلّي وRDP، لا يمكن منع التشغيل من جلسة أخرى. إن كان للمستخدِم نفسه عدّة جلسات، فعدم تضمين معرِّف الجلسة في اسم الأنبوب يسبِّب تصادم الاسم الثابت (لأنّ الأنبوب المسمّى ليس خاضعاً لمساحة أسماء الجلسة الخاصّة بـ Mutex) |
| على مستوى المستخدِم (عبر الجلسات) | مستخدِم واحد يتنقّل بين الطرفيّة (console) وRDP، أو يكرِّر اتّصال RDP | Global\ + تضمين SID الخاصّ بالمستخدِم في الاسم + تحديد ACL صراحةً عبر MutexSecurity |
CurrentUserOnly (يعمل عبر الجلسات لأنّ الحكم يتمّ عبر SID المستخدِم) |
الحالة الأكثر احتياجاً في العمل الفعليّ. الاكتفاء بـ Global\ دون تضمين SID يؤدّي إلى سلوك على مستوى الجهاز (الصفّ التالي) دون قصد. بما أنّ SID ليس معلومة سرّيّة للمستخدِم، ففي بيئات الحواسيب المشترَكة/RDS يجب أيضاً افتراض احتلال (squatting) الاسم من قِبَل مستخدِمين آخرين والحماية عبر ACL |
| على مستوى الجهاز (عبر كلّ المستخدِمين) | الترخيص يسمح بنسخة واحدة فقط لكلّ جهاز، أو الرغبة في تبادل إقصائيّ (exclusive) لمورد مشترَك بين كلّ المستخدِمين | Global\ (دون تضمين معلومات خاصّة بالمستخدِم) + تحديد ACL صراحةً عبر MutexSecurity |
إزالة CurrentUserOnly، وتحديد المستخدِمين المسموح لهم صراحةً عبر PipeSecurity |
في بيئة خدمات سطح المكتب البعيد التي يسجِّل فيها عدّة مستخدِمين الدخول في آنٍ واحد، يميل السلوك إلى أن يكون غير مرغوب فيه من الناحية التشغيليّة، فيلزم التحقّق من توافقه مع المتطلّبات. لا تُمرِّر وسائط تشغيل مستخدِم آخر كما هي إلى نافذة المستخدِم الأوّل. لأنّ ذلك قد يؤدّي إلى تسرّب مسار ملفّ، أو حادث فتح مستند شخص آخر في جلسة مستخدِم لم يقصد ذلك، لذا يجب رفض طلبات المستخدِمين الآخرين أو تغيير التصميم ليمرّ عبر وسيط (broker) بلا واجهة مستخدِم |
للتوضيح، الأنبوب المسمّى ليس خاضعاً لمساحة أسماء الجلسة Local\/Global\ كما هو الحال في Mutex، بل يمكن الوصول إليه افتراضيّاً عبر الجلسات. بعبارة أخرى، حتّى دون إضافة CurrentUserOnly، يصل الاتّصال نفسه إلى النسخة القائمة في جلسة أخرى ما دام اسم الأنبوب متطابقاً. CurrentUserOnly هنا ليس مسألة إمكانيّة الوصول، بل مسألة التصريح (authorization)، وهو عنصر تحكّم بالوصول يحصر «من يُسمَح باتّصاله» في المستخدِم الحاليّ (وبنفس مستوى الرفع/elevation). دون إضافته، يسمح واصف الأمان (security descriptor) الافتراضيّ بحقّ قراءة حتّى لـ Everyone، فيصل الاتّصال حتّى من مستخدِمين آخرين غير مقصودين. في تصميم يحقِّق «مستوى المستخدِم عبر الجلسات» باستخدام Mutex من نوع Global\ + SID، من المناسب إضافة CurrentUserOnly أيضاً إلى أنبوب الإشعار، وحصر مصدر الاتّصال في «المستخدِم المستهدَف فقط» كما هو الحال مع Mutex. ولحسن الحظّ، يحكم PipeOptions.CurrentUserOnly استناداً إلى SID المستخدِم (ومستوى الرفع) لا معرِّف الجلسة، لذا يمكن استخدامه بالتوليف مباشرةً مع الصفّ الثاني من الجدول أعلاه (مستوى المستخدِم عبر الجلسات).
نقطة أخرى: عند استخدام Mutex باسم Global\ ثابت على مستوى الجهاز (الصفّ الثالث من الجدول)، انتبه أيضاً إلى احتلال الاسم (squatting). عند محاولة حصر التطبيق في نسخة واحدة باستخدام Mutex مسمّى، يمكن لمستخدِم خبيث أن يسبق بإنشاء Mutex بنفس الاسم ويعطِّل تشغيل التطبيق.8 بتحديد ACL صراحةً عند الإنشاء عبر MutexSecurity (MutexAcl.Create)، يمكن، إن نجحتَ في الإنشاء أوّلاً، منع مستخدِمين آخرين من الاستيلاء على ذلك Mutex أو الاحتفاظ به بشكل غير مشروع. لكن انتبه إلى أنّ هذا إجراء ضدّ «عدم التعرّض للإزعاج لاحقاً»، ولا يمنع «الاحتلال المسبق» بحدّ ذاته. لا يُطبَّق ACL إلّا عند إنشائك Mutex جديداً بنفسك، لذا إن كان مستخدِم خبيث قد أنشأ بالفعل Mutex بنفس الاسم قبلك، فإنّ استدعاءك (مهما أعددتَ من ACL) سيكتفي بفتح الكائن القائم الذي ضبطه الطرف الآخر، ولن يكون أمامك سوى الخضوع لـ ACL الخاصّ به. لصعوبة تخمين الاسم نفسه قيمة بذاتها، لكن إن أردتَ إقصاء مستخدِم خبيث يملك صلاحيّة تنفيذ شيفرة محليّة بشكل كامل، ففكِّر في تصميم يجمع بين آليّة تحكّم إقصائيّ أخرى، مثل ملفّ قفل تحت دليل محميّ لكلّ مستخدِم، بدل الاعتماد على احتلال اسم Mutex وحده.9
نقطة أخرى: يحمل CurrentUserOnly قيداً يسهُل إغفاله. في Windows، لا يُسمَح بالاتّصال إلّا إذا تطابق ليس فقط حساب المستخدِم، بل أيضاً مستوى الرفع (elevation) (أي هل يُنفَّذ كمسؤول أم لا).10 قد نميل سابقاً إلى الاعتقاد بأنّ «اسم Mutex يُبنى من SID المستخدِم فقط، لذا فإنّ الاكتشاف نفسه يعمل بصرف النظر عن وجود الرفع أو عدمه»، لكن عند استخدام Mutex مع ACL محدَّد صراحةً، يتأثّر هذا أيضاً بالرفع. يمنح Windows افتراضيّاً الكائنات التي تنشئها عمليّة ذات مستوى نزاهة (integrity level) عالٍ (تعمل كمسؤول) وسماً بمستوى نزاهة عالٍ، ويرفض الوصول الكتابيّ من عمليّات ذات مستوى نزاهة أدنى. يطلب Mutex/MutexAcl.Create في .NET داخليّاً، إلى جانب SYNCHRONIZE وMUTEX_MODIFY_STATE، أيضاً DELETE/READ_CONTROL/WRITE_DAC/WRITE_OWNER (STANDARD_RIGHTS_REQUIRED)، لذا إن شُغِّلت النسخة الأولى «كمسؤول» والثانية بصلاحيّات عاديّة، فقد يُصدِر استدعاء إنشاء أو فتح Mutex نفسه UnauthorizedAccessException. بعبارة أخرى، ينهار الافتراض المذكور في الفصل 7 القائل بأنّ «أنبوب الإشعار وحده يُرفَض بواسطة ACL، بينما ينجح منع التشغيل المتعدّد نفسه»، ويصبح من الممكن أن يُطلَق الاستثناء في مرحلة أبكر من الاكتشاف. من المناسب معاملة هذا المسار أيضاً كإشارة إلى «نسخة أخرى تعمل بالفعل، أو أنّ اختلاف مستوى الرفع يمنع الحكم بالوسائل العاديّة»، وإحاطة استدعاء Mutex/MutexAcl.Create نفسه بـ try/catch (UnauthorizedAccessException)، بحيث عند الاستثناء يُتخلَّى عن التشغيل وتُنهى العمليّة بهدوء (أو، كما في «لا بأس إن لم يصل الإشعار» في الفصل 7، يُفضَّل الجانب الآمن لمنع التشغيل المتعدّد). إن لزم الإشعار عبر مستويات رفع مختلفة، فبدِّل التصميم إلى بناء ACL صريح مستند إلى SID المستخدِم عبر PipeSecurity، بدل استخدام CurrentUserOnly.
6. إظهار النسخة القائمة في المقدّمة ── قيود SetForegroundWindow
بعد معرفة «أنّه يعمل بالفعل» عبر Mutex، تريد معظم التطبيقات إظهار نافذة النسخة القائمة في المقدّمة. لكن مجرَّد استدعاء SetForegroundWindow على العمليّة القائمة يفشل في معظم الحالات.
يقيِّد Windows بشكل صارم أيّ عمليّة يمكنها ضبط نافذة المقدّمة (foreground). وفق التوثيق الرسميّ، ما لم تنطبق على العمليّة المستدعية إحدى الحالات التالية، فإنّ SetForegroundWindow لا يُظهِر النافذة فعليّاً في المقدّمة، بل يقتصر أثره على وميض زرّ شريط المهام.6
- أن تكون العمليّة المستدعية نفسها هي عمليّة المقدّمة الحاليّة
- أن تكون العمليّة المستدعية قد أُطلِقَت بواسطة عمليّة المقدّمة
- أن تكون العمليّة المستدعية قد استقبلت حدث إدخال حديثاً
- ألّا توجد نافذة مقدّمة حاليّاً
- أن تكون عمليّة المقدّمة أو العمليّة المستدعية قيد تصحيح الأخطاء (debugging)
في سيناريو اكتشاف التشغيل المتعدّد، لا تحقِّق النسخة القائمة (العمليّة الأولى العاملة في الخلفيّة) هذا الشرط أصلاً. في المقابل، العمليّة الثانية التي شغَّلها المستخدِم للتوّ بالنقر المزدوج غالباً ما تكون في حالة استقبلت فيها حدث إدخال حديثاً، وتملك صلاحيّة ضبط المقدّمة. الاستفادة من عدم التناظر هذا هو الأسلوب المعتاد في العمل الفعليّ.
AllowSetForegroundWindow هي واجهة API تسمح للعمليّة القادرة على ضبط المقدّمة بتسليم تلك الصلاحيّة إلى عمليّة أخرى.7 إذا سلَّمت العمليّة الثانية صلاحيّتها عبر ASFW_ANY (السماح لأيّ عمليّة)، ثمّ طلبت التفعيل من النسخة القائمة عبر أنبوب مسمّى، ينجح استدعاء SetForegroundWindow من جانب النسخة القائمة.
[DllImport("user32.dll")]
private static extern bool AllowSetForegroundWindow(int dwProcessId);
private const int ASFW_ANY = -1;
// بافتراض أنّ العمليّة الثانية (نحن) تملك صلاحيّة ضبط المقدّمة،
// نسلِّم تلك الصلاحيّة دون قيد أو شرط. هذا يجعل استدعاء
// SetForegroundWindow من جانب النسخة القائمة ينجح
AllowSetForegroundWindow(ASFW_ANY);
تبقى هناك حالات لا يمكن إنقاذها حتّى بهذا (كالتشغيل عبر جدولة المهام، حيث لا تملك العمليّة الثانية نفسها صلاحيّة المقدّمة أيضاً). في هذه الحالة، من التصميم المناسب الاكتفاء بالإشعار عبر وميض شريط المهام دون محاولة قسريّة لإظهار النافذة في المقدّمة. من المفيد دائماً كوسيلة إشعار للمستخدِم استخدام إشعار توست (toast)، أو إعادة WindowState الخاصّة بالنافذة من Minimized إلى Normal، مع اعتبار الإظهار النهائيّ في المقدّمة أمراً «يُنفَّذ إن أمكن» فقط، فلا ينهار التصميم.
7. تمرير وسائط التشغيل إلى النسخة القائمة ── الأنبوب المسمّى
إلى جانب اكتشاف التشغيل المتعدّد، من الشائع أيضاً وجود متطلَّب من نوع «عند التشغيل بوسيطة ملفّ يجب فتحه، تفتح النسخة القائمة ذلك الملفّ». يوجد WM_COPYDATA (الوسيلة الكلاسيكيّة لإرسال بيانات عبر رسالة نافذة) كخيار لهذا الغرض أيضاً، لكن الفائدة قليلة مقابل تعقيد الحصول على مقبض النافذة (window handle) وترتيب الرسالة (marshaling)، لذا فإنّ استخدام الأنبوب المسمّى هو الخيار المباشر في التصميم الجديد.
التصميم بسيط: تتّصل العمليّة الثانية التي عرفت «أنّه يعمل بالفعل» عبر Mutex بالأنبوب المسمّى الذي تنتظره النسخة القائمة، وترسل وسائط سطر الأوامر بعد تسلسلها (serialize) بصيغة JSON أو ما شابه. أمّا نقاط الانتباه على مستوى تنفيذ الأنبوب المسمّى نفسه، مثل التصدّي لاحتلال اسم الأنبوب (squatting) والتحكّم بالوصول عبر CurrentUserOnly، فهي مُجمَّعة في قسم الأنبوب المسمّى من مقال «كيف تختار وسيلة الاتّصال بين عمليّات Windows»، فاتّبع ما ورد هناك. أمّا نقطة الانتباه الخاصّة بسياق اكتشاف التشغيل المتعدّد فهي هذه فقط:
- حتّى لو فشل إرسال الإشعار، يمكن معاملته كنجاح من منظور منع التشغيل المتعدّد. قد لا يصل الإشعار بسبب مشكلة توقيت، كأن تكون النسخة القائمة قد أغلقت خادم الأنبوب أثناء معالجة الإنهاء. حتّى في هذه الحالة، يكون الهدف الرئيسيّ «عدم السماح بتشغيل العمليّة الثانية» قد تحقَّق، لذا لا داعي لإظهار مربّع حوار خطأ بسبب فشل الإشعار.
8. الفرق مع تطبيقات سطر الأوامر والخدمات
يفترض تصميم هذا المقال تطبيق سطح مكتب يملك نافذة. أمّا في تطبيقات سطر الأوامر أو أدوات الدُفعات (batch)، فغالباً ما يكون التصميم الأكثر واقعيّة هو «جعل التشغيل المتعدّد المتزامن قادراً على التعايش بأمان» بدل «منع التشغيل المتعدّد»، وهذا يعود من الناحية الفعليّة إلى مسألة التحكّم الإقصائيّ (exclusive control) في التعاون على الملفّات.
تختلف خدمات Windows بشكل أكبر. لا يشغِّل مدير التحكّم بالخدمات (SCM) أصلاً خدمتين بنفس الاسم في آنٍ واحد، لذا فإنّ الاكتشاف عبر Mutex في هذا المقال غير ضروريّ من حيث المبدأ. في بنية مثل «تطبيق واجهة مستخدِم + خدمة مقيمة»، يُستخدَم تصميم هذا المقال على جانب الواجهة فقط، بينما يصبح التفكير في التشغيل المتعدّد لجانب الخدمة موضوعاً منفصلاً. نترك طريقة بناء الخدمات ونقاط تصميمها لمقال آخر.
9. مثال تنفيذ ── الاكتشاف عبر Mutex وطلب التفعيل
هذا تكوين عمليّ أدنى يلخِّص محتوى الفصول من 2 إلى 7. يفترض WPF، لكن يمكن استخدامه في WinForms أيضاً كما هو تقريباً، بمجرَّد استبدال Application.Current.Dispatcher بـ Control.Invoke.
using System.IO.Pipes;
using System.Runtime.InteropServices;
using System.Security.AccessControl;
using System.Security.Principal;
using System.Text.Json;
public static class Program
{
// ندمج GUID خاصّاً بالمنتج لتجنّب تصادم الاسم مع تطبيقات أخرى.
// بما أنّنا نريد حصر النسخة في «مستوى المستخدِم عبر الجلسات»، ندمج إلى جانب Global\
// معرِّف SID الخاصّ بالمستخدِم في الاسم. الاكتفاء بـ Global\ دون تضمين SID يجعل
// كلّ المستخدِمين يشتركون في نفس Mutex ويتحوّل إلى «نسخة واحدة للجهاز بأكمله» (الفصل 3)
private static readonly string MutexName =
$@"Global\KomuraSoft.MyApp.SingleInstance.{{3F1E2B10-9C2E-4B7E-8B1E-8B4F2D6A5C10}}.{WindowsIdentity.GetCurrent().User}";
// اجعل أنبوب الإشعار أيضاً بنفس نطاق Mutex (مستوى المستخدِم). إن أُبقي الاسم ثابتاً
// دون تضمين SID، فقد يحدث تصادم أو تداخل عندما ينشئ مستخدِم آخر خادماً
// بنفس الاسم (الفصل 5، CurrentUserOnly يحصر ACL فقط ولا يفصل الأسماء)
private static readonly string PipeName =
$"KomuraSoft.MyApp.Activate.{{3F1E2B10-9C2E-4B7E-8B1E-8B4F2D6A5C10}}.{WindowsIdentity.GetCurrent().User}";
[STAThread]
private static void Main(string[] args)
{
// تحديد ACL يسمح بالتحكّم الكامل للمستخدِم الحاليّ فقط عند الإنشاء يمنع،
// إن نجحنا في الإنشاء أوّلاً، الاستيلاء أو الإعاقة من قِبَل مستخدِمين آخرين
// (تلزم حزمة NuGet باسم System.Threading.AccessControl).
// لكن هذا لا يشكِّل حماية ضدّ «الاحتلال المسبق» بحدّ ذاته (الفصل 5)
var mutexSecurity = new MutexSecurity();
mutexSecurity.AddAccessRule(new MutexAccessRule(
WindowsIdentity.GetCurrent().User!, MutexRights.FullControl, AccessControlType.Allow));
bool createdNew;
Mutex mutex;
try
{
// فقط عندما تكون createdNew تساوي true، نكون قد حصلنا على Mutex بهذا الاستدعاء
mutex = MutexAcl.Create(
initiallyOwned: true, name: MutexName, createdNew: out createdNew, mutexSecurity: mutexSecurity);
}
catch (UnauthorizedAccessException)
{
// حتّى مع نفس المستخدِم، قد يُرفَض فتح Mutex القائم إن اختلف مستوى الرفع
// (الفصل 5). نعتبر ذلك «توجد نسخة أخرى مختلفة في مستوى الرفع بالفعل»،
// ونتخلّى عن الإشعار ونميل إلى الجانب الآمن (عدم التشغيل)
return;
}
using var _ = mutex;
if (!createdNew)
{
// إنّه يعمل بالفعل. نُشعِر النسخة القائمة وننهي أنفسنا
NotifyRunningInstanceAsync(args).GetAwaiter().GetResult();
return;
}
try
{
// احرص على حذف StartupUri من App.xaml. فإن أُبقي عليه، إلى جانب
// MainWindow المُنشَأة يدويّاً هنا، ستُنشَأ وتُعرَض تلقائيّاً نافذة StartupUri
// أيضاً (وتُستبدَل app.MainWindow بتلك الأخيرة)، فتُفتَح نافذتان وينتهي
// الأمر بحادث يتّجه فيه طلب التفعيل إلى النافذة الخطأ
var app = new App();
app.InitializeComponent();
var mainWindow = new MainWindow();
app.MainWindow = mainWindow;
mainWindow.Show();
// شغِّل خادم الأنبوب بعد انتهاء إنشاء وعرض MainWindow.
// فإن عُكِس الترتيب، قد يصل طلب التفعيل قبل وجود النافذة أصلاً،
// فيفشل استدعاء ActivateMainWindow (يُبتلَع الاستثناء في catch-all
// أدناه، فيختفي الإشعار فقط دون أثر). الطلبات الواصلة خلال هذه الفترة
// تُعامَل كـ «الخادم لم يبدأ بعد ← فشل الاتّصال» ضمن تصميم أفضل جهد
// (best effort) الخاصّ بـ NotifyRunningInstanceAsync (الفصل 7)
StartActivationServer();
app.Run();
}
finally
{
// نحرِّر صراحةً من الخيط المالك (هذا الخيط) قبل الإنهاء.
// الإنهاء دون تحرير يتسبَّب في AbandonedMutexException
// عند التشغيل التالي (الفصل 4)
mutex.ReleaseMutex();
}
}
private const int ASFW_ANY = -1;
[DllImport("user32.dll")]
private static extern bool AllowSetForegroundWindow(int dwProcessId);
private static async Task NotifyRunningInstanceAsync(string[] args)
{
try
{
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(3));
using var pipe = new NamedPipeClientStream(
".", PipeName, PipeDirection.Out, PipeOptions.Asynchronous | PipeOptions.CurrentUserOnly);
await pipe.ConnectAsync(cts.Token);
// نحن (العمليّة الثانية المُشغَّلة للتوّ) استقبلنا حدث إدخال حديثاً
// وبدأنا التشغيل للتوّ غالباً، لذا نملك صلاحيّة ضبط المقدّمة.
// نسلِّم تلك الصلاحيّة دون قيد أو شرط، لننجح في استدعاء
// SetForegroundWindow من جانب النسخة القائمة (الفصل 6)
AllowSetForegroundWindow(ASFW_ANY);
byte[] payload = JsonSerializer.SerializeToUtf8Bytes(new ActivateRequest(1, args));
await pipe.WriteAsync(payload, cts.Token);
}
catch (Exception ex) when (ex is IOException or UnauthorizedAccessException or OperationCanceledException)
{
// سواء لم تستجب النسخة القائمة لأنّها في طور الإنهاء (IOException)،
// أو فشل تصريح CurrentUserOnly لاختلاف مستوى الرفع (UnauthorizedAccessException،
// انظر ملحق الفصل 5)، أو غير ذلك، دون تمييز سبب عدم وصول الإشعار،
// فإنّ الهدف الرئيسيّ لمنع التشغيل المتعدّد (عدم تشغيل العمليّة الثانية) متحقِّق (الفصل 7)
}
}
private static void StartActivationServer()
{
// الحدّ الأقصى لحجم الرسالة الواحدة. يحمي ذاكرة الخادم حتّى لو استمرّ
// مساعد قديم أو عميل معطوب لنفس المستخدِم في الإرسال بلا حدود
const int MaxPayloadBytes = 64 * 1024;
_ = Task.Run(async () =>
{
while (true)
{
try
{
// ضع المُنشئ (constructor) نفسه أيضاً داخل try. بما أنّ
// maxNumberOfServerInstances تساوي 1، فقد يُصدِر هذا IOException
// في لحظة لم تنتهِ فيها بعد تنظيفات الاتّصال السابق، وإن وُضِع
// خارج try فسيوقف هذا الفشل الواحد المهمّة الخلفيّة بأكملها
using var pipe = new NamedPipeServerStream(
PipeName, PipeDirection.In, 1,
PipeTransmissionMode.Byte, PipeOptions.Asynchronous | PipeOptions.CurrentUserOnly);
await pipe.WaitForConnectionAsync();
// بما أنّ maxNumberOfServerInstances تساوي 1، فإن ثبت هذا الاتّصال
// على عميل لا يستجيب، لن يُقبَل أيّ طلب تشغيل شرعيّ لاحقاً.
// نضع حدّاً زمنيّاً أقصى لكلّ اتّصال على حدة
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5));
using var ms = new MemoryStream();
var buffer = new byte[4096];
int n;
while ((n = await pipe.ReadAsync(buffer, cts.Token)) > 0)
{
ms.Write(buffer, 0, n);
if (ms.Length > MaxPayloadBytes)
throw new IOException("تجاوزت الحمولة الحجم الأقصى المسموح به.");
}
var req = JsonSerializer.Deserialize<ActivateRequest>(ms.ToArray());
// نتحقّق من Args أيضاً وليس Version فقط. إن أرسل مصدر بإصدار قديم
// أو حمولة مصنوعة يدويّاً غير سليمة مثل {"Version":1} تفتقر إلى
// Args، فستبقى Args بقيمة null بعد إلغاء التسلسل
if (req is { Version: 1, Args: not null })
{
// نُرجِع عمليّات واجهة المستخدِم إلى خيط واجهة المستخدِم
Application.Current.Dispatcher.Invoke(() => ActivateMainWindow(req.Args));
}
}
catch (Exception)
{
// بصرف النظر عن السبب - انقطاع الاتّصال، تجاوز الحدّ الزمنيّ أو
// حدّ الحجم، تلف الحمولة، استثناء غير متوقَّع أثناء التوزيع (dispatch)،
// إلخ - نتجاهل هذا كخلل خاصّ بهذا الاتّصال فقط. إماتة هذه المهمّة
// نفسها ستمنع قبول كلّ الإشعارات لاحقاً، لذا يجب أن تستمرّ الحلقة دائماً.
// لكن لتجنّب الدوران الساخن (hot spin) في حال استمرّ إنشاء الأنبوب نفسه
// بالفشل فوراً قبل await (كأن تحتكر عمليّة أخرى نطاق النسخة الواحدة)،
// نأخذ نفَساً واحداً قبل الحلقة التالية دائماً
await Task.Delay(TimeSpan.FromSeconds(1));
}
}
});
}
[DllImport("user32.dll")]
private static extern bool SetForegroundWindow(IntPtr hWnd);
private static void ActivateMainWindow(string[] args)
{
var window = Application.Current.MainWindow;
if (window is null) return;
if (window.WindowState == System.Windows.WindowState.Minimized)
window.WindowState = System.Windows.WindowState.Normal;
window.Show();
window.Activate();
// تستدعي Activate() في WPF داخليّاً SetForegroundWindow، لكنّها قد تفشل
// بسبب القيود (الفصل 6)، لذا نستدعيها أيضاً صراحةً بعد تنفيذ AllowSetForegroundWindow
var hwnd = new System.Windows.Interop.WindowInteropHelper(window).Handle;
SetForegroundWindow(hwnd);
if (args.Length > 0)
{
// مثال: معاملة args[0] كمسار الملفّ الواجب فتحه، وما شابه من معالجة خاصّة بالتطبيق
}
}
private sealed record ActivateRequest(int Version, string[] Args);
}
ثلاث ملاحظات تكميليّة حول قرارات التصميم.
- اختر معرِّف مستخدِم لا يتغيّر اسمه لإضافته إلى
Global\. يختلفSecurityIdentifierالذي يُعيدهWindowsIdentity.GetCurrent().Userعن اسم المستخدِم في أنّه لا يتأثّر بإعادة التسمية، ويصبح نصّاً بصيغةS-1-5-21-...عبرToString().11 تضمين اسم المستخدِم مباشرةً قد يؤدّي إلى حادث يتعطّل فيه منع التشغيل المتعدّد عند تغيير اسم الحساب أو ترحيل النطاق (domain). - قم بتحرير Mutex وإيقاف خادم الأنبوب صراحةً كجزء من معالجة إنهاء التطبيق. في المثال أعلاه، يُستدعى
ReleaseMutexداخلfinally، لكن في تطبيق فعليّ، اجعل حلقة خادم الأنبوب تتوقّف أيضاً باستخدام رمز إلغاء (cancellation token) ضمن معالجة إغلاق النافذة. - ضع حقل
versionمنذ البداية. من المحتمل جدّاً تغيير صيغة وسائط التشغيل مستقبلاً. بإدراج حكم «تجاهل الإصدارات غير المعروفة» منذ البداية، يمكن التصرّف بأمان حتّى في الأجهزة التي لا تزال تحتفظ بملفّ تنفيذيّ لإصدار قديم.
10. الخلاصة
منع التشغيل المتعدّد لتطبيقات Windows بسيط إن نظرنا إليه فقط من زاوية أسطر قليلة مثل new Mutex(true, name, out createdNew). لكن لجعله يعمل دون حوادث في العمل الفعليّ، يلزم الإلمام بمعرفة محيطة كاملة: الفرق في رؤية الجلسات بين Global\/Local\، وقيد ملكيّة الخيط الخاصّ بـ Mutex والتعامل مع AbandonedMutexException، ووصولاً إلى تصميم التفعيل مع مراعاة قيود SetForegroundWindow.
من حيث ترتيب التنفيذ، حدِّد أوّلاً «على أيّ وحدة (جلسة، مستخدِم، جهاز) تريد منع التشغيل المتعدّد» (الفصل 5)، ثمّ اجعل مساحة أسماء Mutex ونطاق أنبوب الإشعار متّسقَين مع ذلك. بعد ذلك، استخدم الأسلوب المعتاد بتفويض الصلاحيّة عبر AllowSetForegroundWindow لإظهار النسخة القائمة في المقدّمة، مع تبنّي مبدأ أنّه في الحالات التي يفشل فيها ذلك أيضاً، يُكتفى بالإشعار دون محاولة قسريّة للإظهار في المقدّمة، وبذلك لا ينهار التنفيذ. إن ترددتَ بين «نسخة واحدة لكلّ مستخدِم» أو «نسخة واحدة للجهاز بأكمله»، يُنصَح بالتحقّق أوّلاً من بيئة التشغيل (وجود استخدام مزدوج لـ RDP من عدمه، وتسجيل دخول عدّة مستخدِمين في آنٍ واحد من عدمه).
مقالات ذات صلة
- كيف تختار وسيلة الاتّصال بين عمليّات Windows ── جدول قرار بين الأنبوب المسمّى / TCP / gRPC / الذاكرة المشترَكة / COM
- مدخل إلى ملفّات تعريف مستخدِمي Windows - AppData وNTUSER.DAT
- جدول القرار حول الإنهاء أو المتابعة عند استثناء غير متوقَّع
مجالات الاستشارة ذات الصلة
تتعامل شركة Komura Soft LLC مع تصميم وتنفيذ تطبيقات سطح مكتب Windows، بما في ذلك منع التشغيل المتعدّد والتحكّم بالنوافذ، بالإضافة إلى تحقيق أسباب الأعطال الخاصّة ببيئة سطح المكتب البعيد، ومراجعة تصميم التطبيقات القائمة.
مراجع
-
Microsoft Learn، Mutex Constructor. حول أنّه إذا كان Mutex المسمّى موجوداً بالفعل، تصبح createdNew بقيمة false دون حدوث استثناء، وأنّ حقّ الملكيّة الأوّليّ عبر initiallyOwned فعّال فقط عندما تكون createdNew بقيمة true. ↩ ↩2 ↩3
-
Microsoft Learn، Mutex Class. حول أنّ Mutex المسمّى دون تحديد بادئة يحمل Local\ افتراضيّاً، والفرق في الرؤية بين جلسات خدمات الطرفيّة (terminal services) عبر Global\/Local\، وأنّ Mutex يفرض الملكيّة على مستوى الخيط (بخلاف كائنات المزامنة الأخرى). ↩ ↩2 ↩3
-
Microsoft Learn، Kernel Object Namespaces. حول بنية مساحة الأسماء المستقلّة لكلّ جلسة ومساحة الأسماء العامّة (global)، وطريقة تحديد مساحة الأسماء عبر بادئتَي Global\/Local. ↩ ↩2
-
Microsoft Learn، Mutex.ReleaseMutex Method. حول أنّ استدعاء ReleaseMutex من خيط لا يملك Mutex يُصدِر ApplicationException، وأنّه إذا انتهى الخيط دون تحرير Mutex يصبح في حالة متروكة. ↩ ↩2
-
Microsoft Learn، AbandonedMutexException Class. حول أنّ AbandonedMutexException يُرسَل إلى الخيط التالي الذي يحصل على Mutex متروك، وأنّ الانتظار نفسه نجح وأنّ جهة الاستدعاء حصلت على حقّ ملكيّة Mutex. ↩ ↩2
-
Microsoft Learn، SetForegroundWindow function. حول شروط العمليّة القادرة على ضبط نافذة المقدّمة، وأنّه في حال عدم تحقّق الشروط يقتصر الأثر على وميض زرّ شريط المهام. ↩ ↩2
-
Microsoft Learn، AllowSetForegroundWindow function. حول إمكانيّة تسليم العمليّة القادرة على ضبط نافذة المقدّمة لتلك الصلاحيّة إلى عمليّة أخرى، وأنّ تحديد ASFW_ANY يسمح لأيّ عمليّة. ↩ ↩2
-
Microsoft Learn، CreateMutexW function (synchapi.h). حول أنّه عند تقييد التطبيق بنسخة واحدة عبر Mutex مسمّى، يمكن لمستخدِم خبيث إنشاء Mutex بنفس الاسم مسبقاً وإعاقة تشغيل التطبيق، وبدائل للحماية كاستخدام اسم عشوائيّ أو ملفّ قفل تحت ملفّ تعريف المستخدِم في حالة نسخة واحدة لكلّ مستخدِم. ↩
-
Microsoft Learn، Mutexes. حول أنّ Mutex النظام المسمّى مرئيّ وعامّ (global) على مستوى نظام التشغيل بأكمله، لذا يُنصَح بحمايته بأمان التحكّم بالوصول منذ لحظة الإنشاء، والتحكّم بالوصول عبر MutexSecurity. ↩
-
Microsoft Learn، PipeOptions Enum. حول أنّ CurrentUserOnly على Windows يتحقّق أيضاً من مستوى الرفع إضافةً إلى حساب المستخدِم. ↩
-
Microsoft Learn، WindowsIdentity.User Property. خاصيّة تُعيد معرِّف الأمان (SID) الخاصّ بالمستخدِم، وأنّ SID يحدِّد المستخدِم أو المجموعة بشكل فريد في جميع تطبيقات Windows NT. ↩
مقالات ذات صلة
أحدث المقالات التي تشترك في نفس الوسوم. عمّق فهمك بمواضيع مرتبطة.
كيف تفهم عزل الجلسات في Windows — Session 0 وRDP وتشغيل عدة مستخدمين في وقت واحد
يوضح هذا المقال مفهوم «الجلسة» (session) في Windows، وهو موضوع يسبب ارتباكًا مستمرًا لمطوري تطبيقات Windows. يتناول المقال سبب وجود عزل S...
عندما يُعامَل تطبيق Windows الذي طوّرته شركتك بوصفه فيروساً ── التعامل مع الكشف الخاطئ في Microsoft Defender والتعايش مع أثره على الأداء
نرتّب الإجراء الرسميّ عند كشف Microsoft Defender تطبيق Windows الذي طوّرته شركتك خطأً. نشرح آليّة برامج مكافحة الفيروسات الحديثة، والإبلا...
السكون والإسبات وModern Standby والتطبيقات طويلة التشغيل ── منع «التوقّف في منتصف الليل» بالتصميم
نرتّب أسباب وصول تطبيقات Windows طويلة التشغيل إلى حالة «توقّفت عندما نظرت صباحاً»، انطلاقاً من الفرق بين سكون S3 والإسبات وModern Standb...
هل تعمل تطبيقات الأعمال على Windows بإصدار Arm؟ ── واقع محاكاة x64 (Prism) ومكتبات DLL وCOM الأصليّة
نجيب المطوّرين ومسؤولي الأنظمة عن سؤال «هل تعمل تطبيقات الأعمال على Windows بإصدار Arm؟». نستعرض آليّة محاكاة x64 (Prism)، والطبقات التي ...
مطبّات محرك الشبكة ومسار UNC ── الممارسة العمليّة للتعامل مع خادم الملفّات (المجلّد المشترك) في تطبيقات الأعمال
نرتّب الأعطال الشائعة عند إخراج البيانات إلى مجلّد مشترك أو مراقبته من تطبيق أعمال. نشرح سبب عدم ظهور حرف القرص (Z:) من الخدمة، والصلاحيّ...
أين يتصل هذا الموضوع
ترتبط هذه المقالة بشكل طبيعي بصفحات الخدمات التالية.
تطوير تطبيقات ويندوز
ندعم تطوير برامج ويندوز للأعمال، وتكامل الأجهزة، وأدوات التواصل.
الأسئلة الشائعة
أسئلة شائعة حول موضوع هذه المقالة.
- كيف يمكن منع التشغيل المتعدّد لتطبيق في C#؟
- الصيغة الأساسيّة هي new Mutex(true, name, out bool createdNew). لا يحدث استثناء حتّى لو كان هناك Mutex بنفس الاسم موجوداً بالفعل، بل تصبح createdNew قيمتها false فقط، لذا يُبنى التفريع على هذه القيمة لإنهاء العمليّة (process) الثانية. من المعتاد أن يكون الاسم نصّاً يتضمّن GUID خاصّاً بالمنتج، تجنّباً للتصادم مع تطبيقات شركات أخرى. في فرع createdNew التي قيمتها false، من الآمن عدم لمس Mutex إطلاقاً وإنهاء المعالجة فوراً.
- لماذا لا يعمل منع التشغيل المتعدّد في بيئة سطح المكتب البعيد (Remote Desktop)؟
- لأنّه إن لم تُضَف بادئة (prefix) إلى اسم Mutex، فإنّه يُنشَأ افتراضيّاً في مساحة أسماء Local\ (المحصورة بالجلسة). في بيئة تملك فيها المستخدِم نفسه عدّة جلسات عبر الطرفيّة (console) وRDP، يُعامَل كلّ جلسة كـ Mutex منفصل، ويمكن للجلسة الثانية أن تبدأ التشغيل بشكل عاديّ. لحصر التشغيل في نسخة واحدة عبر الجلسات، لا بدّ من بادئة Global\. لكنّ Global\ وحدها تجعل النطاق على مستوى الجهاز بأكمله ومشترَكاً بين كلّ المستخدِمين، لذا إن أردتَ نسخة واحدة لكلّ مستخدِم، يجب أيضاً تضمين SID الخاصّ بالمستخدِم في الاسم.
- كيف يمكن إظهار نافذة النسخة القائمة في المقدّمة؟
- مجرَّد استدعاء SetForegroundWindow ببساطة يفشل في معظم الحالات بسبب قيود نظام التشغيل، ويقتصر الأثر على وميض زرّ شريط المهام. الأسلوب المتَّبع هو تصميم يُسلِّم فيه صلاحيّة ضبط المقدّمة (foreground) التي تملكها العمليّة الثانية التي بدأها المستخدِم للتوّ بالنقر المزدوج، إلى النسخة القائمة عبر AllowSetForegroundWindow(ASFW_ANY)، ثمّ يطلب التفعيل عبر أنبوب مسمّى (named pipe). في الحالات التي يفشل فيها ذلك أيضاً (كالتشغيل عبر جدولة المهام)، من المناسب الاكتفاء بالإشعار دون محاولة قسريّة لإظهار النافذة في المقدّمة.
- كيف ينبغي التعامل مع AbandonedMutexException؟
- إن انتهى الخيط (thread) المالك لـ Mutex دون استدعاء ReleaseMutex (كانهيار العمليّة مثلاً)، يُرسَل AbandonedMutexException إلى الخيط التالي الذي يحصل على Mutex. هذا استثناء يدلّ على أنّ الانتظار نفسه نجح، وأنّ جهة الاستدعاء حصلت بالفعل على حقّ الملكيّة. التعامل الصحيح هو عدم تجاهله، بل التحقّق من سلامة حالة الهدف المحمي قبل الاستخدام. في المسارات التي تنتهي بشكل طبيعيّ، فإنّ استدعاء ReleaseMutex صراحةً داخل finally يمنع حدوثه بلا داعٍ عند التشغيل التالي.
الملف الشخصي للمؤلف
صفحة الملف الشخصي لمؤلف المقالة.
غو كومورا
مؤسّس شركة كومورا سوفت ذ.م.م.
يركّز على تطوير برامج ويندوز، والاستشارات التقنية، والتحقيق في الأخطاء، ويتميّز في المشاريع التي تبقى فيها الأصول القديمة ناشطة، وفي تشخيص الأعطال التي يصعب تحديد سببها.
روابط عامة