منع التشغيل المتعدّد لتطبيقات Windows ── Mutex المسمّى وتفعيل النافذة عند التشغيل المزدوج
· آخر تحديث: · غو كومورا · منع التشغيل المتعدّد, Mutex, Windows, .NET, C#, SetForegroundWindow, سطح المكتب البعيد, الأنابيب المسمّاة, تطوير Windows, الاستشارات التقنية
سجل التعديلات (2 تحديثات، آخر تحديث 2 Sep، 2026)
سجل بالتغييرات التي أُجريت على هذا المقال. وحيثما حُفظت نسخة سابقة، تبقى متاحة للقراءة عبر رابط دائم يحمل معرّف DOI.
- أُعيدَت الترجمة العربية كترجمة كاملة عن النص الياباني الأصلي، وأُضيفَت خريطة المعرفة.
- أعيدت الترجمة كترجمة كاملة عن النص الياباني الأصلي. كانت النسخة العربية السابقة مختصراً يسقط أبواباً وجداول ورسوم Mermaid وتعليقات الأشكال وFAQ. أُعيدت هذه العناصر وفق الأصل الياباني، والادّعاءات التقنية مطابقة للنسخة اليابانية.
- النشر الأول
الاستشهاد بهذا المقال(DOI: 10.5281/zenodo.21621624)
هذا المقال محفوظ على Zenodo. يرد أدناه معرّف DOI الذي يشير دائمًا إلى أحدث نسخة، ومعرّف DOI المثبَّت على النسخة التي تقرؤها.
غو كومورا (2026). منع التشغيل المتعدّد لتطبيقات Windows ── Mutex المسمّى وتفعيل النافذة عند التشغيل المزدوج. شركة كومورا سوفت ذ.م.م.. https://doi.org/10.5281/zenodo.21621624 https://comcomponent.com/ar/blog/single-instance-mutex-guide/
- DOI (أحدث نسخة)
- 10.5281/zenodo.21621624
- DOI (هذه النسخة)
- 10.5281/zenodo.22241004
«شغّلتُ تطبيقاً للعمل مرّة أخرى عن طريق الخطأ، فحرّرتُ الملفّ نفسه في نافذتين، واختفت تعديلات إحداهما.» «انطلقت أداة مقيمة بنسختين، فعالجت نفس الحدث مرّتين.» التشغيل المتعدّد لتطبيقات سطح المكتب موضوع هامشيّ في الظاهر، لكنّه في العمل الفعليّ يسبّب حوادث بشكل غير متوقَّع. هيكل الحلّ نفسه بسيط: يكفي استخدام Mutex مسمّى (اختصار mutual exclusion = الاستبعاد المتبادل، وهو كائن نواة في Windows بمثابة «مفتاح» لا يملكه في الوقت نفسه سوى خيط واحد) للحكم على «هل أنا النسخة الأولى؟».
لكنّ هذا التنفيذ البسيط تحيط به مطبّات جانبيّة. فخّ مساحة الأسماء الذي يعطّل المنع في بيئة سطح المكتب البعيد، وقاعدة التحرير الخاصّة بـ Mutex والمختلفة عن كائنات المزامنة الأخرى، ومسألة تصميميّة تتعلّق بقيود نافذة المقدّمة في Win32 حول «كيف نظهر النسخة القائمة في المقدّمة؟». ترتّب هذه المقالة الأساسيّات المتعلّقة باكتشاف التشغيل المتعدّد عبر Mutex مسمّى، وصولاً إلى قرارات التصميم الجانبيّة اللازمة في العمل الفعليّ.
1. الخلاصة أوّلاً
- الشكل الأساسيّ لاكتشاف التشغيل المتعدّد هو
new Mutex(true, name, out bool createdNew). لا يحدث استثناء حتّى لو كانMutexبنفس الاسم موجوداً بالفعل، بل تصبحcreatedNewقيمتهاfalseفقط، لذا يُبنى التفريع على هذه القيمة.1 - إن لم تُضف بادئة إلى الاسم، يُنشأ افتراضيّاً في مساحة أسماء
Local\(المحصورة بالجلسة). في بيئة سطح المكتب البعيد التي يملك فيها المستخدم نفسه عدّة جلسات، يُعامَل كلّ جلسة كـMutexمنفصل، فلا يعمل منع التشغيل المتعدّد. لحصر التشغيل في نسخة واحدة عبر الجلسات، بادئةGlobal\إلزاميّة.23 Mutexلا يُحرَّر إلا من الخيط نفسه الذي حصل عليه. استدعاءReleaseMutexمن خيط آخر يُصدرApplicationException. إن انتهت العملية المالكة دون تحرير، يُرسلAbandonedMutexExceptionإلى الخيط التالي الذي يحصل عليه، وهذا إشارة إلى أنّ الانتظار نفسه نجح؛ التعامل الصحيح ألا تبتلعه، بل تتحقّق من سلامة الحالة ثمّ تستخدمه.45- بعد أن تعلم أنّ «التطبيق يعمل بالفعل»، الموضوع هو إظهار نافذة النسخة القائمة في المقدّمة. يقيّد نظام التشغيل استدعاء
SetForegroundWindowمن غير عملية المقدّمة، فالاستدعاء البسيط يفشل ويقتصر الأثر على وميض زرّ شريط المهام.6 الأسلوب المتَّبع أن تسلّم «صلاحية ضبط المقدّمة» التي تملكها العملية الثانية التي بدأت للتوّ إلى النسخة القائمة عبرAllowSetForegroundWindow.7 - لتمرير وسائط التشغيل (مثل مسار ملفّ يجب فتحه) إلى النسخة القائمة، النقل عبر أنبوب مسمّى هو الأسلوب المتَّبع. مقارنة الوسائل في «كيف تختار الاتّصال بين العمليات في Windows»، وتقتصر هذه المقالة على تصميم «اكتشاف ← إشعار ← تفعيل».
- تطبيقات الطرفيّة والخدمات ليست هدفاً يُنقل إليه تصميم هذه المقالة كما هو. الخدمة أصلاً لا يشغّل SCM نسختين بنفس الاسم، فالفكرة مختلفة (الفصل 8).
في المخطّط، يشير الخطّ المتّصل إلى علاقة قائمة دائماً، ويشير الخطّ المتقطّع إلى علاقة مشروطة (شروط قيامها مذكورة في شرح كلّ علاقة في الصفحة التفصيليّة). القائمة الكاملة للعلاقات (المجموع 19، مع الأدلّة ودرجة اليقين) وتعريفات المفاهيم الرئيسة مجمّعة في صفحة تفاصيل خريطة المعرفة (باليابانية). البيانات: JSON-LD / Turtle
2. أساسيّات الاكتشاف عبر Mutex مسمّى
Mutex أصلاً كائن نواة للتحكّم في الاستبعاد كي لا تستخدم عدّة خيوط أو عمليات نفس المورد في الوقت نفسه. إن أنشأته باسم، أمكن لعمليات أخرى تعرف الاسم أن تشير إلى الكائن نفسه. في اكتشاف التشغيل المتعدّد لا نستخدم الاستبعاد نفسه، بل «مشاركة الاسم» فقط.
using var mutex = new Mutex(initiallyOwned: true, name: MutexName, out bool createdNew);
if (!createdNew)
{
// 既に起動している
return;
}
// createdNew が true のときだけ、このスレッドが Mutex を所有している
نقطتان هنا.
- وجود
Mutexبنفس الاسم لا يُصدر استثناء. تدخلfalseفيcreatedNew، ويُعاد مرجع إلى الكائن القائم. التفريع دائماً علىcreatedNew.1 initiallyOwned: trueلا يسري إلا «عندما نجح الإنشاء الجديد». إن كان موجوداً بالفعل (createdNew == false)، لا يصبح هذا الخيط مالكاً تلقائيّاً. في اكتشاف التشغيل المتعدّد هذا الأثر الجانبيّ لا يضرّ، لكن منعاً لحادث «تستدعي العملية الثانيةReleaseMutexخطأً»، من الآمن في فرعcreatedNewالتي قيمتهاfalseألا تلمسMutexإطلاقاً وأن تنهي المعالجة فوراً.1
الاسم يُجعل عادة نصّاً يتضمّن GUID خاصّاً بالمنتج تجنّباً للتصادم مع تطبيقات شركات أخرى ("KomuraSoft.MyApp.SingleInstance.{3F1E2B10-...}" مثلاً). كما في احتلال اسم الأنبوب، مساحة أسماء كائنات النواة المسمّاة مرئيّة لعمليات أخرى على الجهاز، فمعنى أن يكون الاسم صعب التخمين قائم.
3. Global\ وLocal\ ── ماذا يحدث عند عبور الجلسات
تنقسم مساحة أسماء كائنات النواة في Windows إلى مساحة مستقلّة لكلّ جلسة، ومساحة عامّة مشتركة على مستوى النظام. عند إنشاء Mutex مسمّى، إن لم تحدّد بادئة يُنشأ افتراضيّاً في مساحة جلسة المستدعي (Local\)، ولا يُنشأ في المساحة المشتركة بين كلّ الجلسات إلا ببادئة Global\.3 صنف Mutex في .NET يتّبع هذا السلوك كما هو، والوثائق تنصّ على أنّ «Mutex المسمّى بلا بادئة يحمل Local\ افتراضيّاً».2
يصير هذا مشكلة في مواضع كهذه.
- على محطّة إدارة للأعمال، المستخدم الذي سجّل الدخول مباشرة من الطرفيّة يتّصل أيضاً بجلسة أخرى لنفسه عبر سطح المكتب البعيد (شائع في التشغيل).
- نفس المستخدم يقطع اتّصال RDP ويعيد الاتّصال مراراً، فيُسند في كلّ مرّة معرّف جلسة جديد.
إن أنشأت Mutex بـ Local\ (بلا بادئة)، تُعامل هذه كجلسات منفصلة، فيعمل منع التشغيل المتعدّد مستقلّاً في كلّ جلسة. أي «نفس المستخدم لكن الجلسة الثانية تبدأ التشغيل بشكل عاديّ»، عطل لا يعمل فيه المنع كما قُصد. بالمقابل، في استخدام سطح المكتب العاديّ الذي يفترض جلسة واحدة، Local\ (الافتراضيّ) لا ضرر منه.
تصميم الأجهزة التي يتعايش عليها عدّة مستخدمين عولج عموماً في «مدخل إلى ملفّ تعريف مستخدم Windows»، لكن مضيّقاً على موضوع هذه المقالة يلزم الانتباه. Global\ مساحة أسماء واحدة يشترك فيها كلّ المستخدمين وكلّ الجلسات على الجهاز، ولا تعني تلقائيّاً «نسخة واحدة لكلّ مستخدم». إن ثبّتّ الاسم كـ Global\KomuraSoft.MyApp.SingleInstance وأضفت Global\ فقط، مُنع تشغيل المستخدم B أيضاً طالما يعمل المستخدم A، فيصير الأمر عمليّاً «نسخة واحدة للجهاز كلّه» (الصفّ الثالث في جدول الفصل 5). إن أردت «نسخة واحدة لكلّ مستخدم، مع جمع جلسات ذلك المستخدم» فزد على Global\ معرّفاً خاصّاً بالمستخدم (SID مثلاً) في الاسم. بالمقابل إن أردت نسخة واحدة للجهاز كلّه (والمشاركة بين كلّ المستخدمين مقصودة)، يكفي Global\ بلا SID.
4. قواعد تحرير Mutex ── الخيط المالك وAbandonedMutexException
لـ Mutex قيد لا يوجد في كائنات مزامنة أخرى مثل Semaphore وAutoResetEvent. قاعدة تفرض معرّف الخيط: لا يُحرَّر إلا من الخيط نفسه الذي حصل عليه.2 استدعاء ReleaseMutex من خيط آخر يُصدر ApplicationException («خيط المستدعي لا يملك الميوتيكس»).4
في شيفرة async/await في .NET قد تُستأنف المعالجة بعد await على خيط بركة آخر. إن كتبت شيفرة «تحصل على Mutex ثمّ تحشر معالجة غير متزامنة ثمّ تحرّر في الاستكمال»، فقد تخالف هذا القيد بصمت. لاكتشاف التشغيل المتعدّد من الآمن إغلاق الحصول والتحرير في شيفرة متزامنة قصيرة.
نقطة ثانية هي التخلّي (abandoned). إن انتهى الخيط المالك لـ Mutex دون استدعاء ReleaseMutex (انهيار العملية، سقوط باستثناء غير معالَج، إلخ)، صار ذلك Mutex متخلّى عنه. الخيط التالي الذي يحصل عليه يتلقّى AbandonedMutexException، وهذا استثناء يدلّ على أنّ الانتظار نفسه نجح، وأنّ المستدعي حصل بالفعل على ملكيّة Mutex.5 في استخدام اكتشاف التشغيل المتعدّد وحده لا يحدث عادة (لأنّك تحصل ولا تحرّر حتّى انتهاء العملية)، لكن إن أعدت استخدام نفس Mutex لاستبعاد آخر فتعامل كالتالي مع احتمال فساد حالة الهدف المحمي.
try
{
if (mutex.WaitOne(TimeSpan.FromSeconds(5)))
{
// 通常どおりの処理
}
}
catch (AbandonedMutexException)
{
// 待機は成功しており、このスレッドは既に所有権を得ている。
// 保護対象の状態を検査してから使うか、安全に初期化し直す
}
فصل «أتجاهل الاستثناء، أم أفحص الحالة وأواصل، أم أستسلم وأنهي بشكل غير طبيعيّ» ينطبق عليه فكر «جدول قرار إنهاء العملية أو المواصلة عند استثناء غير متوقَّع» في هذه المدوّنة كما هو. AbandonedMutexException استثناء يخبرك صراحة بـ«النطاق الذي قد يكون فسد»، فالخطّ الأساسيّ ألا تبتلعه وأن تفحص ذلك النطاق فقط (بيانات الهدف المحمي).
وفي المسارات التي تنتهي بشكل طبيعيّ ينبغي استدعاء ReleaseMutex صراحة في finally. عندما يختفي الخيط المالك بانتهاء العملية يُعامل Mutex كمتخلّى عنه، فإن لم تحرّر صراحة أحدثت AbandonedMutexException بلا داعٍ عند التشغيل التالي.
5. جدول القرار ── في أيّ نطاق يُستخدم Mutex
بحسب «أيّ وحدة» تريد منع التشغيل المتعدّد عندها، يتغيّر تصميم مساحة الأسماء وجانب الأنبوب.
5.1 تقابل النطاقات الثلاثة
طريقة التسمية تغيّر «كم Mutex يُنشأ»، وذلك العدد هو «الحدّ الأعلى للنسخ التي يمكن تشغيلها معاً». إن افترضنا على الجهاز نفسه جلستين للمستخدم A (طرفيّة وRDP) وجلسة واحدة للمستخدم B، صار الشكل كالتالي.
flowchart TB
subgraph L["على مستوى الجلسة ── بلا بادئة"]
LA1["المستخدم A الجلسة 1"] --> LM1["Mutex الأوّل"]
LA2["المستخدم A الجلسة 2"] --> LM2["Mutex الثاني"]
LB1["المستخدم B الجلسة 3"] --> LM3["Mutex الثالث"]
end
subgraph G["على مستوى المستخدم ── بادئة Global وSID المستخدم"]
GA1["المستخدم A الجلسة 1"] --> GM1["Mutex للمستخدم A"]
GA2["المستخدم A الجلسة 2"] --> GM1
GB1["المستخدم B الجلسة 3"] --> GM2["Mutex للمستخدم B"]
end
subgraph M["على مستوى الجهاز ── بادئة Global فقط"]
MA1["المستخدم A الجلسة 1"] --> MM1["Mutex واحد للجهاز"]
MA2["المستخدم A الجلسة 2"] --> MM1
MB1["المستخدم B الجلسة 3"] --> MM1
end
حيث تتجمّع الأسهم في الصندوق نفسه هو النطاق الذي يُحصر في نسخة واحدة. في هذا المثال: على مستوى الجلسة حتى 3 نسخ، وعلى مستوى المستخدم حتى نسختين، وعلى مستوى الجهاز حتى نسخة واحدة. الصفّ الذي تختاره يتغيّر بحسب ما إذا كان المتطلّب «لا يفتح الشخص نفسه مرّتين» أو «لا يعمل على الجهاز سوى واحد».
5.2 جدول القرار
| الوحدة | السيناريو المتوقَّع | مساحة أسماء Mutex | تمرير وسائط التشغيل | ملاحظات |
|---|---|---|---|---|
| على مستوى الجلسة (الافتراضيّ) | لا يُستخدم RDP، أو يكفي «واحد لكلّ جلسة» في استخدام سطح المكتب العاديّ | بلا بادئة (=Local\) |
CurrentUserOnly مع تضمين معرّف الجلسة في اسم الأنبوب |
في بيئة RDP لا يُمنع التشغيل من جلسة أخرى. إن ملك المستخدم نفسه عدّة جلسات، فبدون معرّف الجلسة في اسم الأنبوب يتصادم الاسم الثابت (الأنبوب المسمّى خارج مساحة جلسة Mutex) |
| على مستوى المستخدم (عبر الجلسات) | نفس المستخدم ينتقل بين الطرفيّة وRDP، أو يكرّر اتّصال RDP | Global\ + SID المستخدم في الاسم + تصريح ACL بـ MutexSecurity |
CurrentUserOnly (يحكم بـ SID المستخدم فيعمل عبر الجلسات) |
الحالة الأكثر لزوماً في العمل. Global\ بلا SID يصير سلوك مستوى الجهاز (الصفّ التالي) دون قصد. SID ليس سرّ المستخدم، ففي أجهزة مشتركة / RDS افترض احتلال الاسم من مستخدم آخر واحمِ بـ ACL |
| على مستوى الجهاز (عبر كلّ المستخدمين) | الرخصة تقيّد الجهاز بنسخة واحدة، أو تريد استبعاد مورد مشترك بين كلّ المستخدمين | Global\ (بلا معلومات خاصّة بالمستخدم) + تصريح ACL بـ MutexSecurity |
أزل CurrentUserOnly وصرِّح بالمستخدمين المسموح لهم بـ PipeSecurity |
في بيئة خدمات سطح المكتب البعيد التي يسجّل فيها عدّة مستخدمين دخولاً متزامناً غالباً سلوك غير مرغوب للعمل، فتأكّد من تطابقه مع المتطلّب. لا تنقل وسائط تشغيل مستخدم آخر كما هي إلى نافذة المستخدم الأوّل. يؤدّي إلى تسرّب مسار الملفّ أو فتح مستند الغير في جلسة غير مقصودة، فارفض طلبات المستخدمين الآخرين أو غيّر التصميم إلى وسيط بلا واجهة |
5.3 مواءمة نطاق أنبوب الإشعار مع Mutex
الأنبوب المسمّى ليس هدفاً لمساحة جلسة Local\/Global\ مثل Mutex، ويُبلغ عبر الجلسات افتراضيّاً. أي حتّى بلا CurrentUserOnly، إن تطابق اسم الأنبوب وصل الاتّصال نفسه إلى النسخة القائمة في جلسة أخرى. CurrentUserOnly هنا ليس حديث الوصول بل حديث التفويض: تحكّم في الوصول يضيّق «اتّصال من يُسمح به» إلى المستخدم الحاليّ (وبنفس مستوى الرفع). إن لم تضعه سمح واصف الأمان الافتراضيّ بالوصول للقراءة لـ Everyone أيضاً، فيصل اتّصال مستخدم آخر غير مقصود. في تصميم «مستوى المستخدم عبر الجلسات» بـ Global\ + SID، من المناسب إضافة CurrentUserOnly لأنبوب الإشعار أيضاً وضيّق مصدر الاتّصال إلى «المستخدم المستهدف فقط» كما في Mutex. لحسن الحظّ يحكم PipeOptions.CurrentUserOnly بـ SID المستخدم (ومستوى الرفع) لا بمعرّف الجلسة، فيُجمع كما هو مع الصفّ الثاني في الجدول أعلاه (مستوى المستخدم عبر الجلسات).
5.4 احتلال الاسم (squatting) وحدود ACL
عند استخدام Mutex بـ Global\ واسم ثابت على مستوى الجهاز (الصفّ الثالث في الجدول) انتبه إلى احتلال الاسم (squatting). إن حاولت حصر التطبيق في نسخة واحدة بـ Mutex مسمّى، أمكن لمستخدم خبيث أن يسبق فينشئ Mutex بنفس الاسم فيعيق تشغيل التطبيق.8 إن صرّحت بـ ACL عند الإنشاء بـ MutexSecurity (MutexAcl.Create)، منعت إن نجحت أنت في الإنشاء أوّلاً مستخدمين آخرين من الاستيلاء على ذلك Mutex أو الإبقاء عليه بغير حقّ. لكن هذا إجراء «ألا يُزاحَم لاحقاً»، ولا يمنع «الاحتلال مسبقاً» نفسه. لا تُطبَّق ACL إلا عندما تنشئ Mutex جديداً أنت، فإن سبقك مستخدم خبيث بنفس الاسم ذهب استدعاؤك (مهما جهّزت ACL) إلى فتح الكائن القائم الذي ضبطه الطرف، فلا تملك إلا اتّباع ACL الطرف. لصعوبة تخمين الاسم نفسها قيمة، لكن إن أردت إقصاء مستخدم خبيث يملك تنفيذ شيفرة محلّيّة بالكامل، فلا تعتمد على احتلال اسم Mutex وحده، وانظر في تصميم يجمع استبعاداً آخر مثل ملفّ قفل تحت دليل محميّ لكلّ مستخدم.9
5.5 فخّ مستوى الرفع ── CurrentUserOnly ومستوى السلامة
لـ CurrentUserOnly قيد يسهل إغفاله. على Windows لا يُسمح بالاتّصال ما لم يتطابق حساب المستخدم ومستوى الرفع أيضاً (هل يعمل كمسؤول).10 كان شائعاً الظنّ أنّ «اسم Mutex يُركَّب من SID المستخدم وحده، فالاكتشاف نفسه يعمل بغضّ النظر عن الرفع»، لكن عند استخدام Mutex مصرَّح له بـ ACL يتأثّر هذا أيضاً بالرفع. Windows افتراضيّاً تلصق كائناً أنشأته عملية بمستوى سلامة مرتفع (تشغيل كمسؤول) بتسمية مستوى سلامة مرتفع، وترفض وصول الكتابة من عملية بمستوى سلامة أدنى. يطلب 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). إن لزم الإشعار عبر مستويات الرفع، لا تستخدم CurrentUserOnly وانتقل إلى تصميم يصرّح بـ ACL مبنيّ على SID المستخدم بـ PipeSecurity.
6. إظهار النسخة القائمة في المقدّمة ── قيود SetForegroundWindow
بعد أن تعلم بـ Mutex أنّ «التطبيق يعمل بالفعل»، تريد كثير من التطبيقات إظهار نافذة النسخة القائمة في المقدّمة. هنا استدعاء SetForegroundWindow ببساطة على العملية القائمة يفشل في معظم الحالات.
Windows تقيّد بشدّة أيّ عملية تستطيع ضبط نافذة المقدّمة. بحسب الوثائق الرسميّة، ما لم ينطبق على عملية المستدعي أحد التالي، لا يقدّم SetForegroundWindow النافذة فعليّاً ويقتصر على وميض زرّ شريط المهام.6
- عملية المستدعي نفسها هي عملية المقدّمة الحاليّة
- عملية المستدعي شغّلتها عملية المقدّمة
- عملية المستدعي تلقّت حدث إدخال أخير
- لا توجد نافذة مقدّمة حالياً
- عملية المقدّمة أو عملية المستدعي قيد التصحيح
في سيناريو اكتشاف التشغيل المتعدّد، النسخة القائمة (العملية الأولى العاملة في الخلفيّة) لا تستوفي هذا الشرط أوّلاً. بالمقابل، العملية الثانية التي بدأها المستخدم للتوّ بالنقر المزدوج غالباً في حالة تلقّت للتوّ حدث إدخال أخير، فتملك صلاحية ضبط المقدّمة. استغلال هذا عدم التماثل هو الأسلوب المتَّبع في العمل.
AllowSetForegroundWindow API يسلّم العملية التي تستطيع ضبط المقدّمة صلاحيتها إلى طرف محدَّد بمعرّف العملية.7 إن سلّمت العملية الثانية صلاحيتها إلى النسخة القائمة ثمّ طلبت التفعيل عبر أنبوب مسمّى، نجح استدعاء SetForegroundWindow في جانب النسخة القائمة.
لا تمرّر هنا ASFW_ANY (-1). المرجع يعرّف: «إن كان هذا المعامل ASFW_ANY، صارت كلّ العمليات قادرة على ضبط نافذة المقدّمة».7 الطرف واحد محدَّد، وأنت توزّع على كلّ العمليات. تسري هذه الإجازة «حتّى يُحدث المستخدم إدخالاً تالياً، أو حتّى يستدعي أيّ عملية AllowSetForegroundWindow تالياً»،7 فيُفتح نافذة تستطيع فيها عملية مقيمة غير ذات صلة انتزاع التركيز في اللحظة نفسها التي يشغّل فيها المستخدم التطبيق. أنت تزيل حماية النظام مؤقّتاً من أجل تقديم تطبيقك.
الطرف الذي يجب التسليم إليه — معرّف عملية النسخة القائمة — يُؤخذ من الأنبوب المتّصل. مرّر مقبض أنبوب العميل إلى GetNamedPipeServerProcessId فيُعاد معرّف عملية خادم ذلك الأنبوب.11 هو «من أنت متّصل به الآن»، فلا حاجة إلى البحث بالاسم أو النافذة.
[DllImport("user32.dll")]
private static extern bool AllowSetForegroundWindow(uint dwProcessId);
[DllImport("kernel32.dll", SetLastError = true)]
private static extern bool GetNamedPipeServerProcessId(
SafePipeHandle pipe, out uint serverProcessId);
// 接続済みのパイプから相手のプロセスIDを取り、その1つにだけ権限を渡す。
// 取れなかったときは前面化を諦める(通知は届くので、主目的は達成できている)
if (GetNamedPipeServerProcessId(pipe.SafePipeHandle, out uint serverProcessId))
{
AllowSetForegroundWindow(serverProcessId);
}
تبقى حالات لا يُنقذها هذا أيضاً (تشغيل عبر جدولة المهام مثلاً، حيث لا تملك العملية الثانية نفسها صلاحية المقدّمة). عندئذ اكتفِ بالإشعار بوميض شريط المهام، ولا تحاول التقديم قسراً. كوسيلة إشعار للمستخدم نفّذ دائماً إشعار التوست وإعادة WindowState من Minimized إلى Normal، واجعل التقديم النهائيّ في موضع «إن أمكن»، فلا ينهار التصميم.
7. تمرير وسائط التشغيل إلى النسخة القائمة ── الأنبوب المسمّى
فضلاً عن اكتشاف التشغيل المتعدّد، شائع متطلّب «إن شُغِّل بوسيطة ملفّ يجب فتحه، تفتح النسخة القائمة ذلك الملفّ». لهذا الغرض يوجد WM_COPYDATA (وسيلة كلاسيكيّة لإرسال بيانات برسالة نافذة) كخيار، لكن ما يُحصل عليه قليل مقابل عناء الحصول على مقبض النافذة وترحيل الرسالة، ففي التصميم الجديد الأنبوب المسمّى أصرح.
التصميم بسيط: العملية الثانية التي علمت بـ Mutex أنّ «التطبيق يعمل بالفعل» تتّصل بأنبوب مسمّى تنتظره النسخة القائمة، وترسل وسائط سطر الأوامر مسلسلة بـ JSON مثلاً. احتلال اسم الأنبوب وتحكّم الوصول بـ CurrentUserOnly وغيرهما من ملاحظات تنفيذ الأنبوب المسمّى نفسه مجمّعة في قسم الأنبوب المسمّى من «كيف تختار الاتّصال بين العمليات في Windows»، فاتّبعه. ملاحظة واحدة خاصّة بسياق اكتشاف التشغيل المتعدّد.
- يجوز معاملة فشل إرسال الإشعار نجاحاً كمنع تشغيل متعدّد. قد لا يصل الإشعار لتوقيت مثل إغلاق خادم الأنبوب أثناء إنهاء النسخة القائمة. حتّى عندئذ الهدف الرئيس «ألا تُشغَّل العملية الثانية» متحقّق، فلا حاجة إلى حوار خطأ بسبب فشل الإشعار.
8. الفرق مع تطبيقات الطرفيّة والخدمات
يفترض تصميم هذه المقالة تطبيق سطح مكتب ذا نافذة. في تطبيقات الطرفيّة وأدوات الدفعة غالباً أوقع أن تصمّم «ليتعايش التشغيل المتعدّد بأمان» لا «لمنعه»، وهذا يرجع عمليّاً إلى حديث التحكّم في التزامن عند تكامل الملفّات.
خدمات Windows أبعد. مدير التحكّم في الخدمات (SCM) أصلاً لا يشغّل خدمتين بنفس الاسم معاً، فالاكتشاف بـ Mutex في هذه المقالة غير لازم أساساً. في تكوين مثل «تطبيق واجهة + خدمة مقيمة» استخدم تصميم هذه المقالة في جانب الواجهة فقط، وفكرة التشغيل المتعدّد في جانب الخدمة حديث آخر. طريقة بناء الخدمة ونقاط التصميم مقالة أخرى.
9. مثال تنفيذ ── الاكتشاف عبر Mutex وطلب التفعيل
9.1 ما يلزم
قبل الشيفرة نصرّح بالافتراضات.
| البند | المحتوى |
|---|---|
| إطار الهدف | TFM موجَّه إلى Windows من .NET 8 فما بعد (net8.0-windows مثلاً). نستخدم WPF فيلزم <UseWPF>true</UseWPF> في ملفّ المشروع |
| حزمة NuGet | System.Threading.AccessControl. MutexAcl.Create وMutexSecurity في تجميعة هذه الحزمة System.Threading.AccessControl.dll، وليسا في المراجع الافتراضيّة12 |
| مساحات الأسماء المستخدمة | System.IO.Pipes / System.Runtime.InteropServices / System.Security.AccessControl / System.Security.Principal / System.Text.Json |
| نظام التشغيل المستهدف | Windows فقط. مساحة Global\ وAllowSetForegroundWindow وCurrentUserOnly كلّها آليّات خاصّة بـ Windows |
| في WinForms | استبدل Application.Current.Dispatcher.Invoke بـ Control.Invoke، وApplication.Current.MainWindow بمرجع النموذج المستهدف، فيُستخدم تقريباً كما هو |
تُضاف الحزمة بـ dotnet add package System.Threading.AccessControl. إن اكتفيت بـ new Mutex(...) الخام بلا تصريح ACL (الصفّ الأوّل في جدول القسم 5.2، مستوى الجلسة)، فهذه الحزمة غير لازمة.
9.2 الهيكل ── الاكتشاف والإشعار وتشغيل الخادم
الشيفرة طويلة، لذا نعرض أوّلاً التدفّق كلّه. الخطوات الفعليّة أربع فقط. محتوى NotifyRunningInstanceAsync وShowMainWindow وStartActivationServer في القسم 9.3.
// args هي وسائط سطر أوامر Main
string mutexName = $@"Global\KomuraSoft.MyApp.SingleInstance.{WindowsIdentity.GetCurrent().User}";
var security = new MutexSecurity();
security.AddAccessRule(new MutexAccessRule(
WindowsIdentity.GetCurrent().User!, MutexRights.FullControl, AccessControlType.Allow));
// 1. الكشف ── احكم بـ createdNew هل أنت المثيل الأوّل
var mutex = MutexAcl.Create(
initiallyOwned: true, name: mutexName, createdNew: out bool createdNew, mutexSecurity: security);
if (!createdNew)
{
// 2. الإشعار ── إن كنت الثاني، أرسل الوسائط إلى المثيل القائم وأنهِ نفسك
NotifyRunningInstanceAsync(args).GetAwaiter().GetResult();
return;
}
// 3. التشغيل ── إن كنت الأوّل، اعرض النافذة ثم أنشئ خادم الأنبوب
ShowMainWindow();
StartActivationServer();
// 4. التنظيف ── حرِّر من نفس الخيط الذي حصل على الـ Mutex ثم أنهِ
mutex.ReleaseMutex();
في الترتيب نقطة واحدة لا تُتنازل: اعرض النافذة ثمّ شغّل خادم الأنبوب. إن عكست، لم يوجد بعد مقصد طلب التفعيل الذي يستقبله الخادم، فيختفي الإشعار بهدوء.
9.3 النسخة الكاملة
الشيفرة التالية تعكس كلّ الملاحظات في الفصول 2 إلى 7 على الهيكل أعلاه. تفترض WPF، لكن في WinForms يكفي الاستبدال وفق جدول القسم 9.1 لتُستخدم تقريباً كما هي.
using System.IO.Pipes;
using Microsoft.Win32.SafeHandles;
using System.Runtime.InteropServices;
using System.Security.AccessControl;
using System.Security.Principal;
using System.Text.Json;
public static class Program
{
// ادمج GUIDاً خاصاً بالمنتج حتى لا يصطدم الاسم بتطبيقات أخرى.
// نريد مثيلاً واحداً لكل مستخدم عبر الجلسات، لذا أضف SID المستخدم إلى الاسم
// مع Global\. إن اقتصرت على Global\ بلا SID، يشارك كل المستخدمين نفس الـ Mutex
// فيصير «مثيلاً واحداً على الجهاز كله» (الفصل 3 والقسم 5.1)
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 صريح يمنح FullControl للمستخدم الحالي وحده يمنع الاستيلاء أو الإعاقة
// من مستخدمين آخرين إن أنشأت أنت أولاً
// (حزمة NuGet System.Threading.AccessControl لازمة).
// غير أن هذا لا يقي من «الاحتلال المسبق» نفسه (القسم 5.4)
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.5). عدّه «مثيلاً آخر بمستوى رفع مختلف موجود سلفاً»،
// وتخلَّ عن الإشعار ومِل إلى الجانب الآمن (لا تشغّل)
return;
}
using var _ = mutex;
if (!createdNew)
{
// يعمل سلفاً. أخطر المثيل القائم وأنهِ نفسك
NotifyRunningInstanceAsync(args).GetAwaiter().GetResult();
return;
}
try
{
// احذف StartupUri من App.xaml. إن بقي، يُنشأ ويُعرض أيضاً نافذة StartupUri
// إلى جانب MainWindow المولَّد يدوياً هنا (ويُكتب app.MainWindow فوقه)،
// فتنفتح نافذتان وتتوجّه طلبات التنشيط إلى الجانب الذي لا يستقبلها
var app = new App();
app.InitializeComponent();
var mainWindow = new MainWindow();
app.MainWindow = mainWindow;
mainWindow.Show();
// شغِّل خادم الأنبوب بعد إنشاء MainWindow وعرضه.
// بالعكس، يصل طلب تنشيط ولا نافذة بعد، فيفشل ActivateMainWindow
// (يُبتلع الاستثناء في catch-all أدناه ويختفي الإشعار فقط). الطلبات
// في هذه الفجوة تُعامَل «الخادم لم يبدأ → فشل الاتصال» كما في تصميم
// الجهد الأفضل لـ NotifyRunningInstanceAsync (الفصل 7)
StartActivationServer();
app.Run();
}
finally
{
// حرِّر صراحة من خيط الملكيّة (هذا الخيط) ثم أنهِ.
// الإنهاء بلا تحرير يصير سبب AbandonedMutexException عند التشغيل التالي
// (الفصل 4)
mutex.ReleaseMutex();
}
}
[DllImport("user32.dll")]
private static extern bool AllowSetForegroundWindow(uint dwProcessId);
// من الأنبوب المتّصل خذ معرّف عملية جانب الخادم (= المثيل القائم)
[DllImport("kernel32.dll", SetLastError = true)]
private static extern bool GetNamedPipeServerProcessId(
SafePipeHandle pipe, out uint serverProcessId);
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).
// تمرير ASFW_ANY(-1) يجعل الهدف «كل العمليات»
if (GetNamedPipeServerProcessId(pipe.SafePipeHandle, out uint serverProcessId))
{
AllowSetForegroundWindow(serverProcessId);
}
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.5)، وغيرها: لا تميّز سبب عدم وصول الإشعار؛
// الغرض الرئيسي لمنع التشغيل المتعدد (عدم تشغيل الثاني) متحقّق (الفصل 7)
}
}
private static void StartActivationServer()
{
// سقف رسالة واحدة. يحمي ذاكرة الخادم حتى إن أرسل مساعد قديم
// لنفس المستخدم أو عميل مكسور بلا حد
const int MaxPayloadBytes = 64 * 1024;
_ = Task.Run(async () =>
{
while (true)
{
try
{
// ضع المنشئ نفسه داخل 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 فقط. مصدر قديم أو حمولة يدوية غير صالحة
// ترسل JSON بلا Args مثل {"Version":1} فيُفكّ Args إلى null
if (req is { Version: 1, Args: not null })
{
// عمليات الواجهة تُعاد إلى خيط الواجهة
Application.Current.Dispatcher.Invoke(() => ActivateMainWindow(req.Args));
}
}
catch (Exception)
{
// انقطع الاتصال، أو تجاوز السقف الزمني/الحجمي، أو الحمولة مكسورة،
// أو وقع استثناء غير متوقَّع أثناء الإرسال: ابتلع كشذوذ لهذا
// الاتصال الواحد أيّاً كان السبب. إن ماتت هذه المهمة نفسها
// تعجز عن قبول أي إشعار لاحق، لذا استمر في الحلقة حتماً.
// لكن إن فشل بناء الأنبوب فوراً قبل 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لا يتأثّر بإعادة التسمية بخلاف اسم المستخدم، ويصير بـToString()سلسلة بصيغةS-1-5-21-....13 تضمين اسم المستخدم مباشرة يؤدّي إلى عطل يتوقّف فيه منع التشغيل المتعدّد عند تغيير اسم الحساب أو ترحيل النطاق. - تحرير Mutex وإيقاف خادم الأنبوب صرِّح بهما كجزء من إنهاء التطبيق. المثال أعلاه يستدعي
ReleaseMutexفيfinally، لكن في تطبيق فعليّ أوقف حلقة خادم الأنبوب أيضاً برمز إلغاء داخل معالجة إغلاق النافذة. - أدخل حقل
versionمن البداية. احتمال تغيير شكل وسائط التشغيل لاحقاً قائم. إن وضعت من البداية حكماً «تجاهل الإصدار غير المعروف»، سلكت بأمان حتّى على أجهزة بقي فيها ملفّ تنفيذيّ بإصدار قديم.
10. خطوات التحقّق من التشغيل
منع التشغيل المتعدّد ميزة «يصعب ملاحظة أنّها لا تعمل». بعد التنفيذ حرّك اليد بهذا الترتيب حتماً. 1 إلى 3 إلزاميّة، ومن 4 فما بعد نفّذ بحسب النطاق الذي اخترته في جدول القسم 5.2.
- تشغيل مزدوج في الجلسة نفسها — شغّل التطبيق واتركه، ثمّ انقر نقراً مزدوجاً على الملفّ التنفيذيّ مرّة أخرى. النجاح ألا تُفتح نافذة ثانية وأن تأتي النافذة القائمة إلى المقدّمة. تأكّد أيضاً في علامة «التفاصيل» في مدير المهام أنّ الملفّ التنفيذيّ المستهدف لم يبقَ إلا سطراً واحداً.
- العودة من التصغير — كرّر 1 والنسخة الأولى مصغَّرة. إن لم تعد من التصغير، فمعالجة إعادة
WindowStateفيActivateMainWindowفي القسم 9.3 لم تُستدعَ. - تمرير وسائط التشغيل — من موجه الأوامر شغّل الثانية بوسيطة مثل
MyApp.exe C:\temp\sample.txt، وتأكّد أنّ النسخة القائمة تعالج ذلك المسار. إن لم يعمل، اشتبه في عدم تطابق اسم الأنبوب أو ترتيب تشغيل خادم الأنبوب (القسم 9.2). - التحقّق عبر الجلسات (إن اخترت مستوى المستخدم أو الجهاز) — أنشئ جلستين لنفس المستخدم، تسجيل دخول طرفيّة واتّصال سطح مكتب بعيد، وشغّل من كلتيهما. قائمة الجلسات الحاليّة ومعرّفاتها تُرى بأمر
qwinsta.14 بلا بادئة يمكن هنا تشغيل اثنتين (الفصل 3). هذا أكثر أشكال عطل منع التشغيل المتعدّد إبلاغاً. - التحقّق عبر المستخدمين (إن اخترت مستوى الجهاز) — أنشئ جلسة أخرى لمستخدم آخر، وتأكّد أنّه لا يشغّل بينما يعمل المستخدم السابق. النتيجة تتغيّر بحسب تضمين SID في
Global\، فقابل مخطّط القسم 5.1. - التحقّق باختلاف مستوى الرفع — اترك الأوّل بصلاحيّات عاديّة، وشغّل الثاني «كمسؤول». قد يسقط في مسار
UnauthorizedAccessExceptionالمذكور في القسم 5.5، فتأكّد عندئذ أنّ الثاني ينتهي بهدوء (لا يسقط بحوار خطأ). - التحقّق من Mutex متخلّى عنه — أنهِ الأوّل قسراً بـ «إنهاء المهمّة» في مدير المهام ثمّ شغّل مرّة أخرى. إن شُغِّل بشكل عاديّ فالأمر سليم. إن تعذّر التشغيل هنا، بقي خيط أو عملية تحمل
Mutex. إن أعدت استخدام نفسMutexلاستبعاد آخر، تحقّق أيضاً من التعامل معAbandonedMutexException(الفصل 4).
إن أردت أن ترى بعينيك بأيّ اسم يوجد Mutex الذي أنشأته فعلاً، افتح مساحة أسماء مدير الكائنات بـ WinObj من Sysinternals وابحث بالاسم، فهذا الأوثق.15 خطأ «ظننت أنّي أضفت Global\ ولم أضفها» يُكشف بهذا دفعة واحدة.
11. الخلاصة
منع التشغيل المتعدّد لتطبيق Windows يبدو بسيطاً إن نظرت إلى بضعة أسطر new Mutex(true, name, out createdNew) فقط. لكن ليعمل بلا حادث في العمل يلزم الإلمام بمعرفة محيطة: اختلاف رؤية الجلسة بـ Global\/Local\، وقيد ملكيّة الخيط الخاصّ بـ Mutex والتعامل مع AbandonedMutexException، وتصميم التفعيل الذي يراعي قيود SetForegroundWindow.
ترتيب التنفيذ: قرّر أوّلاً «بأيّ وحدة (جلسة، مستخدم، جهاز) تمنع التشغيل المتعدّد» (الفصل 5)، وواءم معها مساحة أسماء Mutex ونطاق أنبوب الإشعار. ثمّ لتقديم النسخة القائمة استخدم الأسلوب المتَّبع بتسليم الصلاحية عبر AllowSetForegroundWindow، وإن فشل مع ذلك فلا تقدّم قسراً واكتفِ بالإشعار — بهذا القطع لا ينهار التنفيذ. إن تردّدت بين «نسخة واحدة لكلّ مستخدم أو نسخة واحدة للجهاز كلّه»، نوصي بالتحقّق أوّلاً من بيئة التشغيل (وجود RDP، ووجود تسجيل دخول متزامن لعدّة مستخدمين).
مقالات ذات صلة
- كيف تختار الاتّصال بين العمليات في Windows ── جدول قرار الأنبوب المسمّى / TCP / gRPC / الذاكرة المشتركة / COM
- مدخل إلى ملفّ تعريف مستخدم Windows - AppData وNTUSER.DAT
- جدول قرار إنهاء العملية أو المواصلة عند استثناء غير متوقَّع
مجالات الاستشارة ذات الصلة
تتعامل شركة كومورا سوفت ذ.م.م. مع تصميم وتنفيذ تطبيقات سطح مكتب Windows بما يشمل منع التشغيل المتعدّد والتحكّم في النوافذ، وتحقيق أسباب الأعطال الخاصّة ببيئة سطح المكتب البعيد، ومراجعة تصميم التطبيقات القائمة.
روابط مرجعية
-
Microsoft Learn, Mutex Constructor. حول أنّ createdNew تصير false ولا يُصدر استثناء إن وُجد Mutex مسمّى بالفعل، وأنّ الملكيّة الأوّليّة بـ initiallyOwned لا تسري إلا عندما تكون createdNew قيمتها true. ↩ ↩2 ↩3
-
Microsoft Learn, Mutex Class. حول أنّ Mutex المسمّى بلا بادئة يحمل Local\ افتراضيّاً، واختلاف الرؤية بين جلسات خدمات الطرفيّة بـ Global\/Local\، وأنّ Mutex يفرض الملكيّة على مستوى الخيط (بخلاف كائنات المزامنة الأخرى). ↩ ↩2 ↩3
-
Microsoft Learn, Kernel Object Namespaces. حول بنية مساحة مستقلّة لكلّ جلسة ومساحة عامّة، وطريقة تحديد المساحة ببادئة Global\/Local. ↩ ↩2
-
Microsoft Learn, Mutex.ReleaseMutex Method. حول أنّ خيطاً لا يملك إن استدعى ReleaseMutex أُصدر ApplicationException، وأنّ انتهاء الخيط دون تحرير Mutex يجعل Mutex متخلّى عنه. ↩ ↩2
-
Microsoft Learn, AbandonedMutexException Class. حول إرسال AbandonedMutexException إلى الخيط التالي الذي يحصل على Mutex متخلّى عنه، وأنّ الانتظار نفسه نجح وأنّ المستدعي حصل على ملكيّة Mutex. ↩ ↩2
-
Microsoft Learn, SetForegroundWindow function. حول شروط العملية التي تستطيع ضبط نافذة المقدّمة، وأنّ عدم استيفاء الشروط يقتصر الأثر على وميض زرّ شريط المهام. ↩ ↩2
-
Microsoft Learn, AllowSetForegroundWindow function. حول أنّ العملية التي تستطيع ضبط نافذة المقدّمة تستطيع تسليم تلك الصلاحية إلى عملية محدَّدة بـ
dwProcessId، و«إن كان هذا المعاملASFW_ANYصارت كلّ العمليات قادرة على ضبط نافذة المقدّمة»، وأنّ الصلاحية المسلَّمة تُفقد «عندما يُحدث المستخدم إدخالاً تالياً (عدا إن وُجّه ذلك الإدخال إلى تلك العملية)، أو عندما تستدعي أيّ عمليةAllowSetForegroundWindowتالياً (عدا إن حُدِّدت نفس العملية السابقة)». ↩ ↩2 ↩3 ↩4 -
Microsoft Learn, CreateMutexW function (synchapi.h). حول أنّ حصر التطبيق في نسخة واحدة بـ Mutex مسمّى يتيح لمستخدم خبيث إنشاء Mutex بنفس الاسم مسبقاً فيعيق التشغيل، والبدائل كاسم عشوائيّ أو ملفّ قفل تحت ملفّ تعريف المستخدم إن كانت نسخة واحدة لكلّ مستخدم. ↩
-
Microsoft Learn, Mutexes. حول أنّ Mutex النظام المسمّى مرئيّ من نظام التشغيل كلّه وعامّ، لذا يُوصى بحمايته بأمان التحكّم في الوصول من لحظة الإنشاء، والتحكّم في الوصول بـ
MutexSecurity. ↩ -
Microsoft Learn, PipeOptions Enum. حول أنّ
CurrentUserOnlyعلى Windows يتحقّق من حساب المستخدم ومن مستوى الرفع أيضاً. ↩ -
Microsoft Learn, GetNamedPipeServerProcessId function. حول إمكان الحصول على معرّف عملية جانب الخادم لأنبوب مسمّى محدَّد (من Windows Vista فما بعد). ↩
-
Microsoft Learn, MutexAcl.Create Method. حول توفير
MutexAcl.Createفي مساحة الأسماءSystem.ThreadingوتجميعةSystem.Threading.AccessControl.dllوحزمة NuGetSystem.Threading.AccessControl، وتحديد المساحة ببادئةGlobal\/Local\والافتراضيّLocal\، وأنّ Mutex المسمّى افتراضيّاً يُفتح لغير المنشئ أيضاً لذا يلزم تمريرMutexSecurityلتقييد الوصول. ↩ -
Microsoft Learn, WindowsIdentity.User Property. حول أنّها خاصّيّة تعيد معرّف أمان المستخدم (SID)، وأنّ SID يعرّف مستخدماً أو مجموعة فريداً في كلّ تنفيذات Windows NT. ↩
-
Microsoft Learn, qwinsta. حول أمر يعرض قائمة الجلسات على مضيف جلسة سطح المكتب البعيد (اسم الجلسة والمعرّف والحالة). ↩
-
Microsoft Learn, WinObj - Sysinternals. حول أنّ WinObj أداة تعرض مساحة أسماء مدير كائنات NT، ويمكن بها تصفّح كائنات النواة المسمّاة. ↩
مقالات ذات صلة
أحدث المقالات التي تشترك في نفس الوسوم. عمّق فهمك بمواضيع مرتبطة.
كيف نفهم عزل الجلسات في Windows ── Session 0 وRDP وتشغيل عدة مستخدمين معاً
نوضح مفهوم «الجلسة» الذي يربك كثيراً من مطوّري تطبيقات Windows. نشرح عملياً سبب عزل Session 0 الذي يمنع الخدمة من عرض واجهة، وسلوك الجلسا...
لماذا تتعطّل الوسائط ── قواعد وسائط سطر أوامر Windows
في Windows لا توجد مصفوفة وسائط؛ يصل إلى CreateProcess سلسلة واحدة ويتولّى الطرف المستقبل تقسيمها. نشرح قواعد CommandLineToArgvW وCRT و.N...
استخدام WMI/CIM من C# وPowerShell ── دليل عمليّ لاسترجاع معلومات العتاد ومراقبة العمليّات والاستعلام عن بُعد
الإجابة الكلاسيكيّة للحصول على الرقم التسلسليّ للحاسوب، ومراقبة المساحة الحرّة على القرص، وكشف بدء عمليّة هي WMI/CIM. يشرح المقال أوامر C...
عندما يُعامَل تطبيق Windows الذي طوّرته شركتك بوصفه فيروساً ── التعامل مع الكشف الخاطئ في Microsoft Defender والتعايش مع أثره على الأداء
نرتّب الإجراء الرسميّ عند كشف Microsoft Defender تطبيق Windows الذي طوّرته شركتك خطأً. نشرح آليّة برامج مكافحة الفيروسات الحديثة، والإبلا...
السكون والإسبات وModern Standby والتطبيقات طويلة التشغيل ── منع «التوقّف في منتصف الليل» بالتصميم
نرتّب أسباب وصول تطبيقات Windows طويلة التشغيل إلى حالة «توقّفت عندما نظرت صباحاً»، انطلاقاً من الفرق بين سكون S3 والإسبات وModern Standb...
أين يتصل هذا الموضوع
ترتبط هذه المقالة بشكل طبيعي بصفحات الخدمات التالية.
تطوير تطبيقات ويندوز
ندعم تطوير برامج ويندوز للأعمال، وتكامل الأجهزة، وأدوات التواصل.
الأسئلة الشائعة
أسئلة شائعة حول موضوع هذه المقالة.
- كيف يمكن منع التشغيل المتعدّد لتطبيق في C#؟
- الشكل الأساسيّ هو new Mutex(true, name, out bool createdNew). لا يحدث استثناء حتّى لو كان Mutex بنفس الاسم موجوداً بالفعل، بل تصبح createdNew قيمتها false فقط، لذا يُبنى التفريع على هذه القيمة لإنهاء العملية الثانية. من المعتاد أن يكون الاسم نصّاً يتضمّن GUID خاصّاً بالمنتج، تجنّباً للتصادم مع تطبيقات شركات أخرى. في فرع createdNew التي قيمتها false، من الآمن عدم لمس Mutex إطلاقاً وإنهاء المعالجة فوراً.
- لماذا لا يعمل منع التشغيل المتعدّد في بيئة سطح المكتب البعيد (Remote Desktop)؟
- لأنّه إن لم تُضف بادئة إلى اسم Mutex، فإنّه يُنشأ افتراضيّاً في مساحة أسماء Local\ (المحصورة بالجلسة). في بيئة يملك فيها المستخدم نفسه عدّة جلسات عبر الطرفيّة وRDP، يُعامَل كلّ جلسة كـ Mutex منفصل، ويمكن للجلسة الثانية أن تبدأ التشغيل بشكل عاديّ. لحصر التشغيل في نسخة واحدة عبر الجلسات، لا بدّ من بادئة Global\. لكنّ Global\ وحدها تجعل النطاق على مستوى الجهاز بأكمله ومشتركاً بين كلّ المستخدمين، لذا إن أردتَ نسخة واحدة لكلّ مستخدم، يجب أيضاً تضمين SID الخاصّ بالمستخدم في الاسم.
- كيف يمكن إظهار نافذة النسخة القائمة في المقدّمة؟
- مجرّد استدعاء SetForegroundWindow ببساطة يفشل في معظم الحالات بسبب قيود نظام التشغيل، ويقتصر الأثر على وميض زرّ شريط المهام. الأسلوب المتَّبع هو أن تسلّم صلاحية ضبط المقدّمة التي تملكها العملية الثانية التي بدأها المستخدم للتوّ بالنقر المزدوج إلى النسخة القائمة عبر AllowSetForegroundWindow، ثمّ تطلب التفعيل عبر أنبوب مسمّى. حدّد الجهة بمعرّف العملية، ولا تستخدم ASFW_ANY (السماح لكلّ العمليات). معرّف عملية الطرف يُؤخذ من الأنبوب المتّصل عبر GetNamedPipeServerProcessId. في الحالات التي يفشل فيها ذلك أيضاً (كالتشغيل عبر جدولة المهام)، من المناسب الاكتفاء بالإشعار دون محاولة قسريّة لإظهار النافذة في المقدّمة.
- كيف ينبغي التعامل مع AbandonedMutexException؟
- إن انتهى الخيط المالك لـ Mutex دون استدعاء ReleaseMutex (كانهيار العملية مثلاً)، يُرسل AbandonedMutexException إلى الخيط التالي الذي يحصل على Mutex. هذا استثناء يدلّ على أنّ الانتظار نفسه نجح، وأنّ جهة الاستدعاء حصلت بالفعل على حقّ الملكيّة. التعامل الصحيح هو عدم تجاهله، بل التحقّق من سلامة حالة الهدف المحمي قبل الاستخدام. في المسارات التي تنتهي بشكل طبيعيّ، فإنّ استدعاء ReleaseMutex صراحةً داخل finally يمنع حدوثه بلا داعٍ عند التشغيل التالي.
الملف الشخصي للمؤلف
صفحة الملف الشخصي لمؤلف المقالة.
غو كومورا
مؤسّس شركة كومورا سوفت ذ.م.م.
يركّز على تطوير برامج ويندوز، والاستشارات التقنية، والتحقيق في الأخطاء، ويتميّز في المشاريع التي تبقى فيها الأصول القديمة ناشطة، وفي تشخيص الأعطال التي يصعب تحديد سببها.