File.exists() возвращает false для файла (каталога), который действительно существует

TLDR: File.exists() глючит, и я хотел бы понять почему!


Я столкнулся со странной проблемой (как это часто бывает) в моем приложении для Android. Я постараюсь быть максимально кратким.

Сначала я покажу вам код, а затем предоставлю дополнительную информацию. Это не полный код. Просто суть проблемы.

Пример кода:

String myPath = "/storage/emulated/0/Documents";
File directory= new File(myPath);
if (!directory.exists() && !directory.mkdirs()) {
   throw new IllegalArgumentException("Could not create the specified directory: " + directory.getAbsolutePath() + ".");
}

В большинстве случаев это работает нормально. Однако несколько раз выдается исключение, что означает, что каталог не существует и не может быть создан. Из каждых 100 прогонов он работает нормально 95-96 раз и дает сбой 4-5 раз.

  • Я объявил разрешения для хранения/чтения внешнего хранилища/записи внешнего хранилища в моем манифесте и asked for the permissions on runtime. The problem does not lie there. (If anything i have too many permissions at this point :D). After all, if it was a permission issue it would fail every time but in my case it fails at a rate of 4% or 5%.
  • запросил разрешения во время выполнения. Проблема не в этом. (Во всяком случае, у меня слишком много разрешений на данный момент: D). В конце концов, если бы это была проблема с разрешением, она бы терпела неудачу каждый раз, но в моем случае она терпела неудачу со скоростью 4% или 5%.С помощью приведенного выше кода я пытаюсь создать файл, который указывает на папку "Документы". В моем приложении я на самом деле использую String myPath = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOCUMENTS).getPath();
    В конкретном устройстве, где возникает ошибка, этот путь называется "/storage/emulated/0/Documents" и , поэтому я жестко закодировал его в приведенном мной примере кода.
  • Если я использую на устройстве приложение для просмотра файлов (например, "Astro file manager"), я вижу, что папка существует и имеет некоторое содержимое, а также подтверждаю, что путь действительно равен "/storage/emulated/0/Documents".
  • Это никогда не случалось со мной на месте. Проблема возникает только у пользователей приложения, и я знаю, что проблема существует благодаря Firebase/Crashlytics. У пользователей точно такой же планшет, что и у меня, который я использую для разработки, а именно Lenovo TB-8504X. (Я работаю в компании, и мы предоставляем и программное и аппаратное обеспечение).

Итак, у вас есть какие-либо мысли о том, почему возникает эта проблема?

Кто-нибудь когда-нибудь испытывал нечто подобное?

Может ли путь к папке "Документы" иногда быть "/storage/emulated/0/Documents" и иногда становиться чем-то другим на том же физическом устройстве?

Я опытный разработчик Android, но я довольно новичок в архитектуре Android и файловой системе Android. Может ли быть так, что при запуске (когда устройство включено или после перезагрузки) файловая система еще не "смонтировала" "диск" в тот момент, когда мой код проверяет, существует ли каталог? Здесь я использую термины "монтировать" и "диск" настолько свободно, насколько это возможно. Кроме того, мое приложение на самом деле является приложением для запуска/родительского контроля, поэтому оно запускается первым при запуске устройства. Я почти убежден, что это вообще не имеет смысла, но сейчас я пытаюсь увидеть более полную картину и исследовать решения, которые выходят за рамки типичной разработки для Android.

Я был бы очень признателен за вашу помощь, поскольку этот вопрос начинает действовать мне на нервы.

Ждем любых полезных ответов.

Заранее спасибо.

ОБНОВЛЕНИЕ (27/08/2019):

Я столкнулся с этим Java Bug Report, хотя он довольно устарел. В соответствии с этим, при работе с томами, смонтированными в NFS, java.io.File.exists выполняет stat(2). Если stat дает сбой (что может произойти по нескольким причинам), то File.exists (по ошибке) предполагает, что файл stat'ed не существует. Может ли это быть источником моих неприятностей?

ОБНОВЛЕНИЕ (28/08/2019):

Сегодня я могу добавить награду bounty к этому вопросу, пытаясь привлечь больше внимания. Я бы посоветовал вам внимательно прочитать вопрос, просмотреть комментарии , игнорируя тот, который утверждает, что это связано с поддержкой клиентов из Realm. Код области действительно используется ненадежным методом, но я хочу знать, поэтому метод ненадежен. Вопрос о том, может ли Realm обойти это и использовать какой-то другой код, выходит за рамки вопроса. Я просто хочу знать, можно ли безопасно использовать File.exists(), а если нет, , почему?

Еще раз спасибо всем заранее. Для меня было бы очень важно получить ответ, даже если он слишком технический и предполагает более глубокое понимание файловых систем NFS, Java, Android, Linux или чего-либо еще!

ОБНОВЛЕНИЕ (30/08/2019):

Поскольку некоторые пользователи предлагают заменить File.exists() каким-либо другим методом, я хотел бы заявить, что то, что меня интересует в данный момент, подчеркивает, почему метод терпит неудачу , а не какой можно использовать вместо этого в качестве обходного пути.

Даже если бы я захотел заменить File.exists() чем-то другим, я не смогу сделать это, потому что этот фрагмент кода находится в файле RealmConfiguration.java (только для чтения), который является частью Библиотека областей, которую я использую в своем приложении.

Чтобы сделать вещи еще яснее, я приведу два куска кода. Код, который я использую в своей деятельности, и метод, который вызывается в RealmConfiguration.java как следствие:

Код, который я использую в своей деятельности:

File myfile = new File("/storage/emulated/0/Documents");
if(myFile.exists()){        //<---- Notice that myFile exists at this point.        
   Realm.init(this);

   config = new RealmConfiguration.Builder()
   .name(".TheDatabaseName")
   .directory(myFile)       //<---- Notice this line of code.
   .schemaVersion(7)
   .migration(new MyMigration())
   .build();

   Realm.setDefaultConfiguration(config);
   realm = Realm.getDefaultInstance();        
}

В этот момент myFile существует, и вызывается код, который находится в RealmConfiguration.java.

Сбой метода RealmConfiguration.java:

    /**
         * Specifies the directory where the Realm file will be saved. The default value is {@code context.getFilesDir()}.
         * If the directory does not exist, it will be created.
         *
         * @param directory the directory to save the Realm file in. Directory must be writable.
         * @throws IllegalArgumentException if {@code directory} is null, not writable or a file.
         */
        public Builder directory(File directory) {
            //noinspection ConstantConditions
            if (directory == null) {
                throw new IllegalArgumentException("Non-null 'dir' required.");
            }
            if (directory.isFile()) {
                throw new IllegalArgumentException("'dir' is a file, not a directory: " + directory.getAbsolutePath() + ".");
            }
------>     if (!directory.exists() && !directory.mkdirs()) {   //<---- Here is the problem
                throw new IllegalArgumentException("Could not create the specified directory: " + directory.getAbsolutePath() + ".");
            }
            if (!directory.canWrite()) {
                throw new IllegalArgumentException("Realm directory is not writable: " + directory.getAbsolutePath() + ".");
            }
            this.directory = directory;
            return this;
        }

Итак, myFile существует в моей деятельности, вызывается код Realm, и внезапно myFile перестает существовать. Снова хочу отметить, что это не соответствует. Я замечаю сбои со скоростью 4-5%, что означает, что myFile большую часть времени существует как в действии, так и когда код области проверяет его.

Я надеюсь, что это будет полезно.

Еще раз спасибо заранее!

Ответы

Ответ 1

Прежде всего, если вы используете Android, отчеты об ошибках в базе данных Java Bugs не актуальны. Android не использует кодовую базу Sun/Oracle. Android начинался как ре-реализация чистых комнат библиотек классов Java.

Так что, если в Android File.exists() есть ошибки, они будут в кодовой базе Android, а любые отчеты будут в трекере проблем Android.

Но когда вы говорите это:

В соответствии с этим при работе с томами, смонтированными в NFS, файл java.io.File.exists в конечном итоге выполняет stat (2). Если статистика терпит неудачу (что может быть сделано по нескольким причинам), то File.exists (по ошибке) предполагает, что файл статистики не существует.

  1. Если вы не используете NFS, этот отчет об ошибке не имеет прямого отношения.
  2. Это не ошибка/ошибка. Это ограничение.
  3. На уровне файловой системы является фактом, что Linux поддерживает множество различных типов файловых систем, и что многие из них ведут себя неожиданным образом... по сравнению с "обычной" файловой системой. JVM не может скрыть все странные крайние случаи, специфичные для файловой системы, на уровне API Java.
  4. На уровне API File.exists не может сообщать о каких-либо ошибках. Подпись не позволяет ему выбросить IOException, и выбрасывание непроверенного исключения будет серьезным изменением. Все, что он может сказать, это true или false.
  5. Если вы хотите различить различные причины для false, вам следует использовать более новый метод Files.exists(Path, LinkOptions...).

Может ли это быть источником моих неприятностей?

Да, может, и не только в случае с NFS! Смотри ниже. (С Files.exist сбой NFS stat, скорее всего, будет EIO, и это вызовет IOException, а не возврат false.)


Код File.java в базе кода Android (версия android-4.2.2_r1):

public boolean exists() {
    return doAccess(F_OK);
}

private boolean doAccess(int mode) {
    try {
        return Libcore.os.access(path, mode);
    } catch (ErrnoException errnoException) {
        return false;
    }
}

Обратите внимание, как он превращает любой ErrnoException в false.

Немного больше копаний показывает, что вызов os.access выполняет собственный вызов, который выполняет системный вызов access и выдает ErrnoException в случае сбоя системного вызова.

Итак, теперь нам нужно взглянуть на документированное поведение системного вызова доступа. Вот что говорит man 2 access:

  1. F_OK проверяет наличие  файл.
  2. При ошибке (хотя бы один бит в режиме  запрашивается разрешение, которое отказано, или режим F_OK и файл  не существует или произошла какая-либо другая ошибка), возвращается -1 и  errno установлен соответствующим образом.
  3. access() завершится ошибкой, если:

    • EACCES Запрашиваемый доступ будет запрещен для файла, или поиск     миссия запрещена для одного из каталогов в префиксе пути     пути (См. также path_resolution (7).)

    • ELOOP Слишком много символических ссылок было найдено при разрешении пути.

    • ENAMETOOLONG     путь слишком длинный.

    • ENOENT Компонент pathname не существует или является висящим символом     ссылка.

    • ENOTDIR     Компонент, используемый в качестве каталога в pathname, на самом деле не является     Каталог.

    • EROFS Запрошено разрешение на запись для файла только для чтения     файловая система.

  4. access() может завершиться ошибкой, если:

    • EFAULT pathname указывает за пределы вашего доступного адресного пространства.

    • EINVAL режим был указан неверно.

    • EIO Произошла ошибка ввода/вывода.

    • ENOMEM Недостаточно памяти ядра.

    • ETXTBSY     Запрошен доступ для записи исполняемого файла, который выполняется.

Я вычеркнул ошибки, которые, на мой взгляд, технически невозможны или неправдоподобны, но все же остается совсем немного, чтобы их рассмотреть.


Другая возможность - что-то (например, какая-то другая часть вашего приложения) - удаление или переименование файла или (гипотетической) символической ссылки, или изменение прав доступа к файлу... за вашей спиной.

Но я не думаю, что File.exist() не работает 1 или что операционная система не работает. Это теоретически возможно, но вам понадобятся четкие доказательства в поддержку теории.

1 - It is not broken in the sense that it is not behaving differently to the known behavior of the method. You could argue until the cows come home about whether the behavior is "correct", but it has been like that since Java 1.0 and it can't be changed in OpenJDK or in Android without breaking thousands of existing applications written over the last 20+ years. It won't happen.


Что делать дальше?

Ну, я бы порекомендовал использовать strace для отслеживания системных вызовов, которые создает ваше приложение, и посмотреть, сможете ли вы получить некоторые подсказки о том, почему некоторые системные вызовы access дают вам неожиданные результаты; например что такое пути и что такое errno. Смотрите https://source.android.com/devices/tech/debug/strace.

Ответ 2

У меня была похожая проблема, но с более высоким уровнем неполадок, когда Антивирус блокировал FileSystem и, таким образом, сбивал любые запросы (почти мгновенно)

вместо этого использовался java.nio.Files.exists().