ऐप Android 9 पर चलता था और Android 10 अपडेट के बाद उसी फ़ोन पर मर जाता है। आपकी प्रोफ़ाइल अछूती है, प्रमाणपत्र वही हैं, और क्रैश किसी भी प्रमाणीकरण ट्रैफ़िक से पहले आता है। logcat पहले एक शून्य पॉइंटर दिखाता है, और उससे कुछ पंक्तियाँ ऊपर है Cannot run program ...: error=13, Permission denied। वह संदेश किसी फ़ाइल को चलाने के बारे है, आपके कॉन्फ़िगरेशन को पढ़ने के बारे में नहीं।

त्रुटि 13 वास्तव में क्या अस्वीकार करती है

Android 10, API स्तर 29, ने ऐप्स को उन फ़ाइलों को चलाने से रोक दिया जो उन्होंने अपनी ही डेटा निर्देशिका में लिखी थीं। नियम उस स्टोरेज को लक्षित करता है जो पहले लिखने योग्य और फिर चलाने योग्य हो, और यह ठीक वही है जो एसेट्स से कॉपी करने वाला लॉन्चर बनाता है। ics-openvpn कोर के पुराने बिल्ड पहले रन पर APK से एक openvpn निष्पादन योग्य फ़ाइल निकालते हैं और उसे 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 के साथ माउंट होता है, और वहाँ रखी सहायक फ़ाइल को कोई भी ऐप शुरू नहीं कर सकता। ऐप डेटा साफ़ करना भी पुराना व्यवहार बहाल नहीं करता, क्योंकि अगली बार चलाने पर वही गैर-निष्पादन योग्य प्रतिलिपि फिर बन जाती है।

लॉन्चर को पैकेज्ड लाइब्रेरी की ओर इंगित करें

सहायक का नाम बदलकर साझा-लाइब्रेरी का नाम रखें और जिस भी ABI का आप समर्थन करते हैं उसके लिए उसे jniLibs के अंतर्गत भेजें, फिर लॉन्चर को 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" जोड़ें। application एलिमेंट संदर्भ दस्तावेज़ करता है कि यह विशेषता क्या नियंत्रित करती है।

लॉन्चर को हाथ से पैच करते समय दोहराए गए लोकल वेरिएबल से सावधान रहें। पुराने स्रोत उसी विधि में बाद में nativeAPI घोषित करते हैं, और दूसरी घोषणा रनटाइम त्रुटि नहीं बल्कि कंपाइल त्रुटि है, इसलिए बिल्ड जल्दी विफल होता है और गलती आसानी से दिख जाती है। Gradle ब्लॉक की ABI सूची को अपने जोड़े गए फ़ोल्डरों के साथ मेल में रखें।

रिलीज़ आर्टिफ़ैक्ट को टेस्ट बिल्ड से मेल कराएँ

डीबग APK टनल उठा सकता है जबकि रिलीज़ बंडल अभी भी क्रैश होता है, क्योंकि बंडल नेटिव लाइब्रेरी को अलग आकार में पैक करता है और उन्हें संग्रह से संपीड़ित रूप में परोस सकता है। अगर केवल बंडल-जनित बिल्ड विफल होते हैं, तो यह gradle.properties में जोड़ें और फिर से बिल्ड करें:

android.bundle.enableUncompressedNativeLibs=false

फिर पुष्टि करें कि स्थापित पैकेज वही ABI बताता है जिसकी आप अपेक्षा करते हैं, और अगर दोनों भेजते हैं तो 32-बिट और 64-बिट डिवाइस पर जाँच दोहराएँ। पैकेज मैनेजर अपने चुने हुए ABI का नाम बताता है, इसलिए 32-बिट पर लौटा डिवाइस क्रैश रिपोर्ट में नहीं, यहाँ दिखता है:

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

एक ABI के लिए गायब लाइब्रेरी मूल अनुमति त्रुटि को केवल उसी डिवाइस पर पुनरुत्पादित करती है, इसीलिए एक सफल परीक्षण फ़ोन प्रमाण नहीं है। मैनिफ़ेस्ट फ़्लैग, लाइब्रेरी का नाम और बंडल सेटिंग साथ भेजें; एक बदलकर बाकी भूल जाने से आप उसी क्रैश पर लौट आते हैं। स्थायी समाधान यह है कि पैच को हमेशा ढोने के बजाय किसी अनुरक्षित कोर रिलीज़ पर जाएँ।

संबंधित लेख