web-dev-qa-db-ja.com

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() + ".");
}
_

ほとんどの場合これは問題なく動作します。 数回ただし、例外がスローされます。これは、ディレクトリが存在しないことを意味しますandを作成できませんでした。 100回実行するごとに、95〜96回正常に動作し、4〜5回失敗します。

  • 私はマニフェストでストレージの読み取り/外部ストレージの読み取り/外部ストレージの書き込みのアクセス許可を宣言しましたandランタイムのアクセス許可を求められました。問題はそこにありません。 (もし私がこの時点であまりにも多くの権限を持っている場合:D)。結局のところ、それが許可の問題だった場合、毎回失敗しますが、私の場合、4%または5%の割合で失敗します。
  • 上記のコードで、「Documents」フォルダーを指すファイルを作成しようとしています。私のアプリでは実際にString myPath = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOCUMENTS).getPath();を使用しています
    エラーが発生する特定のデバイスでは、このパスは「/ storage/emulated/0/Documents」であり、-これがハードコードされている理由ですのコード例で提供しています。
  • デバイスでファイルエクスプローラーアプリ(つまり、「アストロファイルマネージャー」)を使用している場合、フォルダーがexistであり、内容がいくつかあり、パスreallyであることも確認できます「/ storage/emulated/0/Documents」です。
  • これは私にはローカルで起こったことはありません。アプリのユーザーのみが問題を経験し、Firebase/Crashlyticsのおかげで問題が存在することを知っています。ユーザーはまったく同じタブレットを持っています。これは、開発に使用しているもの、つまりLenovo TB-8504Xと同じです。 (私は会社で働いており、ソフトウェアとハ​​ードウェアの両方を提供しています)。

それで、この問題が発生する理由について何か考えがありますか?

誰かが似たような経験をしたことがありますか?

「Documents」フォルダへのパスが「/ 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_であるファイルが存在しないと見なします。これが私のトラブルの原因でしょうか?

編集(2019年8月28日):

今日、私はbountyをこの質問に追加して、さらに注目を集めることができます。質問を注意深く読み、コメントを確認することをお勧めします無視これはRealmからのお客様のサポートに関係していると主張しているコメントです。レルムコードは確かに信頼できない方法を使用しているものですが、-知りたいことが方法が信頼できない理由です。 Realmがこれを回避し、代わりに他のコードを使用できるかどうかは、質問の範囲を超えています。私は単にFile.exists()を安全に使用できるかどうかを知りたい、そうでない場合はwhy

改めて、宜しくお願い致します。それが過度に技術的であり、NFSファイルシステム、Java、Android、Linuxなどの深い理解を伴う場合でも、回答を得ることが私にとって本当に重要です。

編集(2019年8月30日):

一部のユーザーはFile.exists()を他のメソッドに置き換えることを提案しているので、この時点で私が興味を持っていることを述べたいと思います理由を過小評価していますメソッドが失敗しますそしてnot代わりに回避策として使用できるもの。

置き換えたい場合でもFile.exists()を他の何かで置き換えます私はできませんこのコードは_RealmConfiguration.Java_にあるため、これを行うことはできませんアプリで使用するレルムライブラリの一部であるファイル(読み取り専用)。

さらにわかりやすくするために、2つのコードを提供します。私の活動で使用するコードと、結果として_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が私のアクティビティに存在し、レルムコードが呼び出されて、突然myFileが存在しなくなります。繰り返しますが、これは一貫性がないことを指摘したいと思います。クラッシュが4〜5%の割合で発生していることに気付いています。これは、ほとんどの場合、myFileがアクティビティ内とレルムコードによるチェックの両方に存在することを意味します。

これがお役に立てば幸いです。

もう一度よろしくお願いします!

16

まず、Androidを使用している場合、Javaバグデータベースのバグレポートは関係ありません。 Androidは、Sun/Oracleコードベースを使用しません。 Androidは、Javaクラスライブラリのクリーンルームでの再実装として始まりました。

したがって、AndroidのFile.exists()にバグがある場合、バグはAndroidコードベースにあり、レポートはAndroidにあります課題トラッカー。

しかし、これを言うとき:

これによれば、NFSマウントされたボリュームで操作すると、Java.io.File.existsは最終的にstat(2)を実行します。統計が失敗した場合(いくつかの理由で失敗する場合があります)、_File.exists_は(誤って)統計対象のファイルが存在しないと見なします。

  1. NFSを使用していない限り、thatバグレポートは直接関係ありません。
  2. 間違い・バグではありません。制限です。
  3. ファイルシステムレベルでは、Linuxがさまざまな種類のファイルシステムをサポートし、それらの多くが「通常の」ファイルシステムと比較して予期しない方法で動作するのは現実です。 JVMがJava AP​​Iレベルですべての奇妙なファイルシステム固有のEdgeケースを非表示にすることはできません。
  4. APIレベルでは、_File.exists_cannotはエラーを報告しません。シグネチャは、IOExceptionをスローすることを許可していません。チェックされていない例外をスローすると、breaking変更になります。それが言うことができるすべてはtrueまたはfalseです。
  5. falseのさまざまな理由を区別したい場合は、代わりに新しいFiles.exists(Path, LinkOptions...)メソッドを使用する必要があります。

これが私のトラブルの原因でしょうか?

はい、できます。NFSの場合だけではありません。下記参照。 (_Files.exist_を使用すると、NFS statの失敗はEIOである可能性が高く、IOExceptionを返すのではなくfalseを発生させます。 )


Androidコードベース(バージョンAndroid-4.2.2_r1)の File.Java コードは次のとおりです。

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

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

ErrnoExceptionfalseに変換する方法に注意してください。

もう少し掘り下げてみると、os.access呼び出しがaccess syscallを実行するネイティブコールを実行しており、syscallが失敗するとErrnoExceptionをスローすることがわかります。

したがって、今度は、アクセスシスコールの文書化された動作を確認する必要があります。 _man 2 access_の内容は次のとおりです。

  1. F_OKは、ファイルの存在をテストします。
  2. エラー時(モードの少なくとも1ビットが拒否された許可を要求したか、モードがF_OKでファイルが存在しないか、または他のエラーが発生しました)、-1が返され、 errnoが適切に設定されている
  3. access()は次の場合に失敗します:

    • EACCES要求されたファイルへのアクセスが拒否されるか、pathnameのパス接頭辞にあるディレクトリの1つに対する検索権限が拒否されます。 (path_resolution(7)も参照してください。)

    • ELOOPパス名の解決で遭遇したシンボリックリンクが多すぎます。

    • ENAMETOOLONGパス名が長すぎます

    • ENOENTパス名のコンポーネントが存在しないか、ぶら下がりシンボリックリンクです

    • ENOTDIRパス名でディレクトリとして使用されているコンポーネントは、実際にはディレクトリではありません。

    • EROFS書き込み許可が読み取り専用ファイルシステム上のファイルに要求されました。

  4. access()は次の場合に失敗することがあります:

    • EFAULTパス名がアクセス可能なアドレス空間の外を指しています

    • EINVALモードが誤って指定されました。

    • EIO I/Oエラーが発生しました

    • ENOMEMカーネルメモリが不足しています。

    • ETXTBSY実行中の実行可能ファイルへの書き込みアクセスが要求されました。

技術的に不可能または妥当ではないと思われるエラーを取り消しましたが、まだ考慮すべきことがほとんどありません。


別の可能性は、何か(たとえば、アプリケーションの他の部分)がファイルまたは(仮想)シンボリックリンクを削除または名前変更したり、ファイルのアクセス許可を変更したりすることです...

しかし、私はFile.exist()が壊れているとは思いません1、またはホストOSが壊れている。理論的には可能ですが、理論を裏付けるには明確な証拠が必要です。

1-メソッドの既知の動作と異なる動作をしないという意味では、問題はありません。動作が「正しい」かどうかについて牛が帰ってくるまで議論することができますが、Java 1.0以降はそのようになっており、OpenJDKまたはAndroidでは変更できません。過去20年以上にわたって書かれた何千もの既存のアプリケーションを壊すことなく。それは起こりません。


次はどうする?

まあ、私の推奨は、straceを使用して、アプリが行っているsyscallを追跡し、access syscallが予期しない結果を与えている理由についていくつかの手掛かりを得ることができるかどうかを確認することです。例えばパスとは何か、errnoとは何か。 https://source.Android.com/devices/tech/debug/strace を参照してください。

9
Stephen C

私は同様の問題を抱えていましたが、トラブル率が高く、アンチウイルスがFileSystemをロックしていたため、リクエストを(ほぼ即座に)失敗していました

回避策は、代わりにJava.nio.Files.exists()を使用することでした。

3
Bonatti