المساعد الشخصي الرقمي

مشاهدة النسخة كاملة : برنامج يحذف ذاته !



holly_smoke
04-02-2001, 09:26 PM
برنامج يحذف ذاته !

تحتوي أكثر التطبيقات المصممة للعمل ضمن بيئة ويندوز 95، أو 98، أو إن تي، على برنامج لإزالة التثبيت، يزيل كافة البرامج والملفات والمجلدات المتعلقة بالتطبيق. ويدعى هذا البرنامج، غالباً، Uninstall. لكن المشكلة التي يواجهها المطورون عند كتابة هذا التطبيق، هي كيف يجعلون هذا البرنامج يحذف ذاته أيضاً، من القرص الصلب، في نهاية عملية إزالة التثبيت.

نجد بعد تثبيت النسخة 14 من أوتوكاد، مثلاً، أنه ثبت برنامجاً باسم Uninstall.exe، وهو ينفذ عند إصدار الأمر Unistall AutoCAD R14.0 من قائمة "ابدأ". ووظيفة هذا البرنامج إزالة كل ما يتعلق بأوتوكاد من الحاسوب، بالإضافة إلى أنه يزيل ذاته أيضاً، لأنه جزء من تطبيق أوتوكاد الذي طلبت إزالته.

تعتبر كتابة برنامج يحذف ملفاً معيناً أو برنامجاً آخر، أمراً يسيراً، لكن ليس من السهل أن يحذف البرنامج ذاته، ويحتاج إلى معرفة وخبرة عميقة. وسنقدم في هذا المقال مناقشة للحلول الممكنة لتلك المشكلة، ثم نستخدام برنامجاً بلغة سي ++، وتجد الملفات المصدرية لذلك البرنامج، مع البرنامج ذاته، في القرص المدمج المرفق بالمجلة، وفي الدليل \programming\

البحث عن حل

ربما تعلم أننا يمكن أن نستخدم التعليمة DeleteFile، لحذف ملف معين، وهذا يعني أنه يكفي لحذف الملف التنفيذي الجاري، أن نحصل على المسار الكامل له، باستخدام التعليمة GetModuleFileName، ثم نحذفه بالتعليمات التالية:

TCHAR szEXEPathname[_MAX_PATH];
GetModuleFileName(NULL, szEXEPathname, _MAX_PATH);
DeleteFile(szEXEPathname);

لكن، عند تنفيذ تطبيق معين، يقوم نظام التشغيل ويندوز95/98/إن تي، عملياً، في بداية عملية التنفيذ، بفتح الملف التنفيذي وتحميله إلى الذاكرة، ثم يغلق الملف آلياً عند انتهاء عملية التنفيذ (الخروج من التطبيق). وستفشل لهذا، تعليمة حذف الملف، لأن الملف المطلوب حذفه، مفتوح. وستعيد وظيفة جلب الخطأ GetLastError، بعد فشل عمليّة حذف الملف، الخطأ رقم 5، أي الخطأ ERROR_ACCESS_DENIED، الذي يدل على عدم سماح النظام بالوصول إلى الملف المطلوب (لأنه مفتوح). وهذا يعني أن الطريقة التقليدية لحذف الملف، لن تفيد في حل هذه المسألة.

لنفكر إذاً، في استخدام تعليمة نقل ملف MoveFileEx، لحل المسألة، والتي تمكننا من نقل ملف من دليل إلى آخر، ولها الشكل التالي:

BOOL MoveFileEx(LPCTSTR lpExistingFileName, LPCTSTR lpNewFileName, DWORD dwFlags);

فإذا مررنا البارامتر NULL (بارامتر اللاشيء)، بدلاً من اسم الملف الجديد lpNewFileName، سيحذف النظام الملف تماماً، مثل ما تفعل التعليمة DeleteFile، في الحالات العادية. ونستطيع أن نؤجل تنفيذ التعليمة (سواء في نقل أو حذف الملف) إلى أن نعيد تشغيل النظام، وذلك بتمرير القيمة MOVEFILE_DELAY_UNTIL_REBOOT، مكان البارامتر الأخير dwFlags من التعليمة MoveFileEx. وهي لا تغير شيئاً في البداية، لكنها تطلب من النظام حذف الملف بعد إعادة تشغيله. ولأن الملف الذي نريد حذفه (UnInstall، مثلاً) لا يكون مستعملاً في تلك الحالة (مفتوحاً من قِبلْ النظام)، فإن النظام يستطيع النظام حذفه بدون مشكلات، ونكون قد حللنا المسألة بهذا.

توجد، للأسف، ثلاثة عيوب في طريقة الحل السابقة: الأول، أنها لا تحذف المجلد الذي يتضمن الملف المطلوب حذفه. العيب الثاني، أن الملف لن يُحذف إلا بعد إعادة تشغيل النظام من جديد، بينما تعمل بعض الأنظمة، مثل نظام ويندوز إن تي، بدون توقف أو إعادة تشغيل، طوال عام كامل. وإذا ثبت ثم أزيل تثبيت عدة برامج بالطريقة السابقة، في حذف ملفات التثبيت، لامتلأت أقراص النظام بملفات يفترض أنها محذوفة. العيب الثالث، وهو الأكثر خطورة، أن التعليمة MoveFileEx لا تعمل ضمن النظامين ويندوز95، و98 وتعمل فقط، ضمن نظام ويندوز إن تي.

يمكن التغلب على العيب الثالث في الطريقة المقترحة، والمتعلق بالنظامين ويندوز95، و98، باستخدام تقنية أخرى لحذف، أو نقل، الملفات بعد إعادة تشغيل النظام من جديد، إذ يتم أثناء إقلاع النظام، تنفيذ برنامج اسمه WININIT.EXE، وإحدى المهام المنوطة به، هي نقل أو حذف الملفات التي ترد أسماؤها ضمن الملف WININIT.INI، إن وُجد، في مجلد النظام. يتكون هذا الملف من مجموعة مقاطع، يهمنا منها هنا، المقطع المسمى [Rename]، حيث يندرج تحت هذا المقطع أسماء الملفات التي ستُحذف، أو ستُنقل، أثناء إعادة التشغيل. يكفي لحذف البرنامج الجاري تنفيذه، أو حذف أي ملفات أخرى أثناء إعادة التشغيل، أن نفتح الملف WININIT.INI، إن كان موجوداً في مجلد النظام، أو أن ننشئه، إن لم يكن موجوداً، ثم نضيف إليه مقطعاً جديداً باسم [Rename]، ونسرد تحت هذا المقطع الملفات التي نريد حذفها أو نقلها أثناء إعادة التشغيل، بالشكل التالي:

DestinationFileName=SourceFileName

نضع على يمين علامة المساواة =، موقع الملف الذي نود حذفه أو نقله، ونضع على يسار علامة المساواة الموقع الجديد للملف المطلوب نقلة، أو نضع الرمز (NUL) للملف المراد حذفه.

يغير النظام اسم ذلك الملف إلى WININIT.BAK، بعد تنفيذ عمليات النقل، أو الحذف، المطلوبة فيه.

نحذف الملف C:\Ddj\Uninstall.exe، مثلاً، وننقل ملف الحاسبة Calc.exe من الدليل C:\Windows\ إلى الدليل C:\ بكتابة ما يلي:

[Rename]
NUL=C:\Ddj\Uninstall.exe
C:\Calc.exe=C:\Windows\Calc.exe

ويمكننا حذف أكثر من ملف بهذه الطريقة، ويكون لدينا نتيجة لذلك، أكثر من سطر يندرج تحت مقطع واحد، ويبدأ بالرمز NUL، لهذا السبب لا يمكن استخدام التعليمة القديمة WritePrivateProfileString، لإضافة سطور جديدة إلى الملف WININIT.INI، إذ أنها تسمح بإضافة سطر واحد فقط يبدأ بالرمز ذاته.

نستطيع جمع الحلين السابقين معاً، وهما طريقة MoveFileEx وطريقة WININIT.INI، للحصول على طريقة تعمل تحت الأنظمة الثلاثة 95،و98، وإن تي. وهذا ما فعلناه في الوظيفة DeleteFileOnReboot، التي تجدها في الملف RFoR.c، ضمن القرص المرفق بالمجلة.

نحاول في الحل الذي وصلنا إليه، حذف الملف المعني باستخدام التعليمة MoveFileEx، وتنتهي العملية، إذا نجحنا، وإلا فهذا يعني أننا لسنا ضمن بيئة ويندوز إن تي، لذلك نفتح أو نشئ الملف WININIT.INI ضمن مجلد النظام، ونضيف إليه سطراً لحذف الملف المعني تحت المقطع [Rename]. استخدمنا الملفات المخطوطة في الذاكرة (Memory Mapped Files) لتسهيل التعامل مع الملف، وبحثنا عن المقطع [Rename] ببساطة، باستخدام التعليمة strstr، ثم أضفنا السطر الجديد بالتعليمة memcpy.

إذاً استطعنا تجاوز العيب الثالث، لكن بقي العيبان الأول والثاني بدون حل، إذ أن البرنامج السابق يجبرنا على إعادة تشغيل النظام، كي نتمكّن من حذف الملف الذي نريد، فهل توجد طريقة أفضل، تعمل على حذف الملف بشكل فوري، أثناء التنفيذ؟

تتلخص الطريقة التي نقترحها، معاملة البرنامج التنفيذي المطلوب حذفه، معاملة مكتبة الربط الديناميكية DLL، التي يمكن تحميلها وإزالتها إلى ومن الذاكرة ديناميكياً (أثناء التنفيذ)، باستخدام التعليمتين (LoadLibrary) و (FreeLibrary). فإذا أزلنا البرنامج التنفيذي من الذاكرة، يمكننا حذفه باستخدام التعليمة DeleteFile، لأنه خارج الذاكرة. ويتلخص الحل بالتعليمات التالية:

int WINAPI WinMain (HINSTANCE hinstExe,
HINSTANCE hinstExePrev,
LPSTR lpszCmdLine, int nCmdShow) {
TCHAR szEXEPathname[_MAX_PATH];
GetModuleFileName(NULL, szEXEPathname, _MAX_PATH);
FreeLibrary(hinstExe);
DeleteFile(szEXEPathname);
return(0);
}

لكن مشكلة وحيدة تنشأ عند العودة من تنفيذ التعليمة FreeLibrary، إذ يكون البرنامج خارج الذاكرة، فتظهر رسالة خطأ. ويمكن تجنب المشكلة، بإدراج تعليمات جديدة أثناء التنفيذ وتنفيذ تلك التعليمات.

يتضمن التابع DeleteExecutable الحل السابق، وهو موجود في الملف DelExe.c ضمن القرص المرافق للمجلة. لاحظ أن هذا الحل يمكننا أيضاً، من حذف دليل الملف التنفيذي.

وبقي أن نقول أن هذه الطريقة لا تعمل على نظام إن تي، إذ أنه لا يسمح للتعليمة FreeLibrary، أن تنفذ على برنامج تنفيذي exe، بدلاً من مكتبة ربط ديناميكية DLL!

تعتمد الطريقة الأخرى التي نقترحها، والتي تعمل تحت الأنظمة الثلاثة 95، 98، إن تي. وتحذف الملف فوراً، بدون انتظار إعادة تشغيل النظام، على استخدام ملفات باتش (Batch File)، والتي تمتلك خاصة فريدة، هي قدرتها على حذف ذاتها!

فإذا أنشأنا ملفاً دفعيّاً يحوي التعليمة التالية:

del %0.bat

فنجد عند تنفيذه أنه حذف ذاته كلياً، مع إظهار رسالة الخطأ التالية:

The batch file cannot be found.

لكن رسالة الخطأ السابقة لاتضر، ونستطيع إخفاءها، كما سنرى لاحقاً.

ويعني ذلك أن جعل برنامج تنفيذي معبن يحذف ذاته (Uninstall.exe، مثلاً)، يتطلب جعله يولد ملف باتش (باسم DelAll.bat، مثلاً)، يتضمن تعليمة لحذف ملف البرنامج Uninstall.exe، وأخرى لحذف الملف الدفعي ذاته DelAll.bat، ثم يطلب تنفيذ الملف الدفعي والخروج (إنهاء التنفيذ). وعندما يُنفذ الملف الدفعي، يبدأ بانتظار البرنامج Uninstall.exe الذي يريد حذفه، حتى ينتهي تنفيذه قبل أن يتمكن من حذفه، وعندما ينجح الملف الدفعي أخيراً، في حذف الملف Uninstall.exe، فإنه يحذف ذاته. ويبدو لنا كأن البرنامج Uninstall.exe، هو الذي حذف ذاته.

فإذا كان برنامج إزالة التثبيت Uninstall.exe، موجوداً في الدليل C:\Ddj، فإنه يجب أن يولد عند تنفيده الملف الدفعي التال،ي قبل أن ينتهي من التنفيذ:

:Repeat
del " C:\Ddj\Uninstall.exe"
if exist "Uninstall.exe" goto Repeat
rmdir "C:\Ddj"
del "\DelAll.bat"

يستخدم الملف الدفعي طريقة الاقتراع (polls)، لحذف البرنامج التنفيذي Uninstall.exe، حيث يحاول الحذف أولاً، فإذا فشل (يختبر إذا كان البرنامج التنفيذي السابق ما زال موجوداً)، فهذا يعني أن البرنامج التنفيذي لم ينته من التنفيذ بعد. ويكرر المحاولة حتى ينجح في حذف الملف، ثم يحذف دليل البرنامج التنفيذي Uninstall.exe، ويحذف أخيراً ذاته.

تستهلك طريقة الاقتراع هذه وقتاً كبيراً في انتظار البرنامج التنفيذي، Uninstall.exe، كي ينهي تنفيذه، إذ أن الملفات الدفعية لا تدعم أي طريقة أخرى، كأن نجعل النظام يخبرنا عن لحظة انتهاء البرنامج التنفيذي، باستخدام إحدى التعليمتين WaitForSingleObject، أو WaitForMultipleObjects، ونوفر وقت الانتظار.

إلا أننا نستطيع تخفيض زمن تنفيذ الملف الدفعي لتوفير الوقت، بإعطاء أفضلية منخفضة للعملية process، التي تنشأ لتنفيذ الملف الدفعي. وهذا موضح في التابع DeleteSelfBatch، الذي تجده في الملف DelExeBF.c ضمن القرص المرافق للمجلة، حيث أنشأنا فيه الملف الدفعي، وكتبنا محتوياته بالتعليمة WriteFile، ثم حددنا نافذة تنفيذ مخفية (SW_HIDE)، وأنشأنا عملية (process) بالتعليمة (CreateProcess)، وهي العملية المسؤولة عن تنفيذ الملف الدفعي، ثم جعلنا البريمج معلقاً (بحالة انتظار، لا يُنفّذ شيئاً)، ريثما نهيئ عمله (CREATE_SUSPENDED)، وأعطيناه أفضلية تنفيذ منخفضة (IDLE_PRIORITY_CLASS). ثم رفعنا أفضلية التنفيذ للبرنامج الحالي Uninstall.exe، بالتعليمتين SetThreadPriority, SetPriorityClass، كي ينهي تنفيذه بأسرع وقت ممكن. نستأنف أخيراً، العملية المعلقة، والمسؤولة عن تنفيذ الملف الدفعي بالتعليمة ResumeThread، فيعمل بوتيرة بطيئة نسبياً، ويقترع على حذف البرنامج Uninstall.exe، إلى أن ينجح في ذلك ثم يحذف ذاته.

الخلاصة: وجدنا أنه لا يمكن لبرنامج أن يحذف ذاته، إلا إذا أجلّنا عملية الحذف إلى ما بعد إعادة تشغيل النظام، ثم ناقشنا تقنية تخدع النظام بإقحام تعليمات من خارج النظام، إلا أنها لم تستطع خداع النظام إن تي. وكان الحل أخيراً، بأن نسلم المهمة إلى برنامج آخر يعمل على تقنية مختلفة تسمح له بحذف ذاته، وتعتمد على استخدام ملف دفعي (Batch File).

وضعنا في القرص المرافق للمجلة برنامج (DelExe) مع الملفات الأصل (source code)، وهو يعمل على حذف ذاته باستخدام التقنيات الثلاث، الموضحة في هذه المقالة.

CHALLENGER
05-02-2001, 12:59 PM
و الله شكراً يا هولي على هالموضوع الحلو مررررة