برنامه در Android 9 کار می‌کرد و پس از به‌روزرسانی به Android 10 روی همان گوشی از کار می‌افتد. نمایه شما دست‌نخورده است، گواهی‌ها همان‌ها هستند و خرابی پیش از هر ترافیک احراز هویت رخ می‌دهد. logcat ابتدا یک اشاره‌گر تهی نشان می‌دهد و چند خط بالاتر از آن نوشته شده Cannot run program ...: error=13, Permission denied. آن پیام درباره اجرای یک فایل است، نه خواندن پیکربندی شما.

خطای 13 در واقع چه چیزی را رد می‌کند

Android 10، سطح API 29، برنامه‌ها را از اجرای فایل‌هایی که در پوشه داده خودشان می‌نوشتند بازداشت. این قاعده فضای ذخیره‌سازی‌ای را هدف می‌گیرد که نخست قابل نوشتن و سپس قابل اجراست، و این دقیقاً همان چیزی است که یک راه‌انداز کپی‌کننده از assets می‌سازد. ساخت‌های قدیمی‌تر هسته 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" را کنار آن اضافه کنید. مرجع عنصر application مستند می‌کند که این ویژگی چه چیزی را کنترل می‌کند.

هنگام وصله کردن دستی راه‌انداز مراقب متغیر محلی تکراری باشید. منابع قدیمی‌تر nativeAPI را بعداً در همان متد اعلام می‌کنند، و اعلام دوم خطای کامپایل است نه خطای زمان اجرا، پس ساخت سریع شکست می‌خورد و خطا آسان دیده می‌شود. فهرست ABI را در بلوک Gradle با پوشه‌هایی که اضافه می‌کنید هماهنگ نگه دارید.

محصول نهایی انتشار را با ساخت آزمایشی هماهنگ کنید

یک APK اشکال‌زدایی می‌تواند تونل را بالا بیاورد در حالی که bundle انتشار هنوز خراب می‌شود، چون bundle کتابخانه‌های بومی را به شکل دیگری بسته‌بندی می‌کند و می‌تواند آن‌ها را فشرده از آرشیو ارائه دهد. اگر فقط ساخت‌های برگرفته از bundle شکست می‌خورند، این را به gradle.properties اضافه کنید و دوباره بسازید:

android.bundle.enableUncompressedNativeLibs=false

سپس تأیید کنید که بسته نصب‌شده همان ABI مورد انتظار شما را گزارش می‌کند، و اگر هر دو را ارائه می‌دهید بررسی را روی دستگاه 32 بیتی و 64 بیتی تکرار کنید. مدیر بسته ABIای را که انتخاب کرده نام می‌برد، بنابراین دستگاهی که به 32 بیت بازگشته اینجا دیده می‌شود نه در گزارش خرابی:

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

کتابخانه‌ای که برای یک ABI وجود ندارد خطای دسترسی اصلی را فقط روی همان دستگاه بازتولید می‌کند، و به همین دلیل یک گوشی آزمایشی موفق دلیل کافی نیست. پرچم مانیفست، نام کتابخانه و تنظیم bundle را با هم ارائه دهید؛ تغییر یکی و فراموش کردن بقیه شما را به همان خرابی برمی‌گرداند. راه‌حل پایدار گذار به یک نسخه هسته پشتیبانی‌شده است، نه حمل ابدی وصله.

مقالات مرتبط