LoginSignup
1
0

More than 3 years have passed since last update.

Open状態のファイルに対して、Linuxだとos.Renameに成功するが、Windowsだとエラーになる

Posted at

環境情報

この記事は以下の環境で稼働確認を実施しました。

  • Windows
    • Microsoft Windows [Version 10.0.19041.84]
    • go version go1.13.5 windows/amd64
  • Linux
    • Ubuntu 18.04 LTS (Bionic Beaver)
    • go version go1.13.8 linux/amd64

事象

Open状態のファイルに対して、os.Renameを呼び出すと、LinuxとWindowsでは結果が異なります。具体的にはLinuxではos.Renameに成功しますが、Windowsではエラーになります。

以下はOpen状態のファイルに対して、os.Renameを利用するコマンドです。

main.go
package main

import "os"

func main() {
    fp, err := os.Open("old.txt")
    if err != nil {
        panic(err)
    }
    defer fp.Close()

    err = os.Rename("old.txt", "new.txt")
    if err != nil {
        panic(err)
    }
}

これをLinux(Ubuntu)環境でこのコードを実行すると、os.Renameが想定通り動作することが分かります。

$ ls -ltr
total 0
-rwxrwxrwx 1 developer developer 214 Feb 23 22:01 main.go
-rwxrwxrwx 1 developer developer   0 Feb 23 22:05 old.txt
$ go run main.go
$ ls -ltr
total 0
-rwxrwxrwx 1 developer developer 214 Feb 23 22:01 main.go
-rwxrwxrwx 1 developer developer   0 Feb 23 22:05 new.txt

一方、Windows環境でこのプログラムを実行すると、panicが発生しました。

C:\qiita>go run main.go
panic: rename old.txt new.txt: The process cannot access the file because it is being used by another process.

goroutine 1 [running]:
main.main()
        C:/qiita/main.go:14 +0x131
exit status 2

原因

Linux

Linuxの場合、os.Renameの実体はrenameatになります。これは以下のようにstraceを利用すすことで確かめることができます。

$ go build -o main
$ strace -o strace.log ./main

renameatは相対パスでファイル名の変更を行うAPIで、そのmanページを見ると以下の通りに記述されており、Open状態のファイルでも名称変更ができることがわかります。_

Open file descriptors for oldpath are also unaffected.

Windows

「Goならわかるシステムプログラミング 第10回 - ファイルシステムと、その上のGo言語の関数たち(1)」 によると、Windowsでのos.Renameの実体はMoveFileExというWin32 apiとのことです。MoveFileExのドキュメントを見たところ、このAPIは対象ファイルの削除権限を要するとのことでした。

To delete or rename a file, you must have either delete permission on the file or delete child permission in the parent directory.

ここでGo言語本体のソースコードを見たところ、ファイルのオープンにはCreateFileというWin32 APIを利用しているようです。CreateFileは開いたファイルの共有状態を指定するのですが、Go言語ではFILE_SHARE_DELETEを指定していません (該当のソースコードはこのあたり)

つまり削除権限なしの状態でオープンされているファイルに対して、削除権限を要するリネーム処理を行っているわけで、エラーになるのは当然といえます。

対策

作成したプログラムがさまざまなプラットフォーム・OSで利用されることが分かっている場合は、os.Renameの前には必ずファイルを閉じておくことが大事です。

余談1

CreateFileを呼び出す際にFILE_SHARE_DELETEを付与して、WindowsでもLinuxと同じ挙動になるようにしてほしいという要望は実際あるようです(該当のIssueはこれ)。ただこの記事を読むと、単にFILE_SHARE_DELETEを付与すればよいだけでもないらしく、なかなか難しい問題をはらんでいることがが分かります。

余談2

とあるライブラリを利用しているときに、ドキュメントの記述と実際の挙動が一致しなかったことが、今回の問題に行き着いた個人的なきっかけになります。ちなみにそのライブラリのコードは次のようになっていました。

fp, err := os.Open("old.txt")
DoSomething(fp)
os.Rename("old.txt", "new.txt")

Windowsではos.Renameがエラーを返すのですが、そのエラーをハンドリングせずに捨てており、結果として奇妙な動作になっていたのでした。APIが返してくるエラーを無視しないというのも大事な教訓ですね。

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0