كان التطبيق يعمل على Android 9 ثم تعطّل على الهاتف نفسه بعد التحديث إلى Android 10. ملفك التعريفي لم يُمَس، والشهادات هي نفسها، والانهيار يقع قبل أي حركة مصادقة. يعرض logcat أولًا مؤشرًا فارغًا، وعلى بعد أسطر قليلة فوقه يظهر Cannot run program ...: error=13, Permission denied. تلك الرسالة تتعلق بتنفيذ ملف، لا بقراءة إعداداتك.

ما الذي يرفضه الخطأ 13 فعلاً

أوقف Android 10، مستوى API 29، التطبيقات عن تنفيذ ملفات كتبتها في دليل بياناتها الخاص. تستهدف القاعدة التخزين القابل للكتابة ثم للتنفيذ، وهو بالضبط ما ينشئه مُشغّل ينسخ الملف من الأصول. تنسخ إصدارات أقدم من نواة ics-openvpn ملفًا تنفيذيًا openvpn من حزمة APK عند أول تشغيل وتشغّله عبر ProcessBuilder. فك الحزمة ما زال ينجح؛ أما التنفيذ فهو ما يُرفض. أي تطبيق يستهدف API 29 يرث القاعدة حتى لو كان يعمل على جهاز صدر بإصدار أقدم، ولهذا يمكن لتحديث واحد أن يعطّل بناءً لم يمسّه أحد.

لا شيء في الرفض خاص باسم مجلد واحد. يُعامل أي ملف أنشأه التطبيق تحت دليل بياناته كغير قابل للتنفيذ بصرف النظر عن المسار، لذا فإن getFilesDir() و getCacheDir() وأي مجلد فرعي مخصص كلها تفشل بالطريقة نفسها.

افصل انهيار الملف المساعد عن مشكلة الملف التعريفي

اقرأ السجل حول لحظة بدء العملية بدلاً من التمرير من الأعلى. البصمة التي تبحث عنها هي سطر error=13، ويذكر تتبّع الاستدعاء فوقه عادةً دالة المُشغّل التي استدعت ProcessBuilder:

adb logcat -d | grep -n -i -E "cannot run program|permission denied|startOpenVPN"

إذا لم يبدأ الملف المساعد أبدًا فلن ترى أي مخرجات سجل من OpenVPN على الإطلاق: لا مفاوضة على الشيفرة، ولا مسارات مدفوعة، ولا إعداد DNS. وإذا ظهرت تلك الأسطر وجاء الفشل لاحقًا، فقد عمل الملف التنفيذي والمشكلة في الملف التعريفي أو الخادم أو طبقة التوجيه. هذا التمييز وحده يوفّر وقتًا طويلاً كان سيُضاع في تحرير ملفات .ovpn لم تكن مذنبة قط. ويخبرك أيضًا أي الإصلاحين أدناه تحتاج فعلاً.

تحقّق أيضًا من مستوى API على الجهاز الذي تختبره، لا على المستوى المذكور في إعدادات البناء. صورة محاكي على API 28 لا تعيد إنتاج شيئًا، لأن القيد لا يؤثر إلا ابتداءً من 29 صاعدًا. وتحقق من SDK الهدف في ملف البناء في الوقت نفسه: رفعه هو ما يستجلب القاعدة.

adb shell getprop ro.build.version.sdk

لماذا لا يساعد نسخ الملف التنفيذي إلى مكان آخر

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

التخزين الخارجي أسوأ: فهو مُثبّت بخيار noexec، والملف المساعد الموضوع فيه لا يمكن لأي تطبيق تشغيله. ومسح بيانات التطبيق لا يعيد السلوك القديم أيضًا، لأن التشغيل التالي ينشئ ببساطة النسخة نفسها غير القابلة للتنفيذ.

وجّه المُشغّل إلى مكتبة محزومة

أعد تسمية الملف المساعد إلى اسم مكتبة مشتركة ووزّعه تحت jniLibs لكل ABI تدعمه، ثم اجعل المُشغّل يعيد مسار المكتبة المثبتة ابتداءً من API 28:

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
    return new File(context.getApplicationInfo().nativeLibraryDir,
                    "libovpnexec.so").getPath();
}

يجب فك المكتبة عند التثبيت حتى يحتوي ذلك المسار على ملف حقيقي، لذا عيّن android:extractNativeLibs="true" على عنصر <application>. وإذا جلب دامج البيان قيمة مختلفة، فأضف tools:replace="android:extractNativeLibs" بجانبه. توثّق مرجع عنصر التطبيق ما تتحكم به هذه السمة.

احترس من متغير محلي مكرر عند ترقيع المُشغّل يدويًا. تصرّح المصادر الأقدم بـ nativeAPI لاحقًا في الدالة نفسها، والتصريح الثاني خطأ في الترجمة لا خطأ في وقت التشغيل، لذلك يفشل البناء سريعًا ويسهل اكتشاف الخطأ. أبقِ قائمة ABI في كتلة Gradle متوافقة مع المجلدات التي تضيفها.

اجعل ناتج الإصدار مطابقًا لبناء الاختبار

يمكن لحزمة APK للتصحيح أن تبدأ النفق بينما تظل حزمة الإصدار تنهار، لأن الحزمة تحزم المكتبات الأصلية بشكل مختلف وقد تقدّمها مضغوطة من الأرشيف. إذا كانت بناءات الحزمة وحدها تفشل، فأضف هذا إلى gradle.properties وأعد البناء:

android.bundle.enableUncompressedNativeLibs=false

ثم تأكد من أن الحزمة المثبتة تعلن ABI الذي تتوقعه، وكرّر الفحص على جهاز 32 بت وجهاز 64 بت إذا كنت توزّع الاثنين. تذكر مديرية الحزم ABI الذي اختارته، لذا يظهر هنا الجهاز الذي تراجع إلى 32 بت بدل أن يظهر في تقرير انهيار:

adb shell dumpsys package com.example.openvpn | grep -i primaryCpuAbi

مكتبة مفقودة لـ ABI واحد تعيد إنتاج خطأ الإذن الأصلي على ذلك الجهاز وحده، ولهذا لا يكفي هاتف اختبار واحد ناجح كدليل. وزّع علامة البيان واسم المكتبة وإعداد الحزمة معًا؛ فتغيير واحد ونسيان البقية يعيدك إلى الانهيار نفسه. الإصلاح الدائم هو الانتقال إلى إصدار نواة مُصان بدلاً من حمل الترقيع إلى الأبد.

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