中野智文のブログ

データ・マエショリストのメモ

gdrive の import ができなくなった。

背景

もともと、

github.com

の問題に対応するため、パッチを作った(つもりだった)。

症状

別環境で試すことになったので、もう一度 build してみると、なんと動かなくなっているんだよね。 (すなわち issue の状態)

調査

バックアップ

とりあえず、現行のうまく動くバイナリ(?)をバックアップした。(これは大正解)

オリジナルのレポジトリの upstream merge してbuild

オリジナルのレポジトリを upstream merge して(これが余分だった) build してみてもNG。

Merge remote-tracking branch 'upstream/master' · tomofumi-nakano/gdrive@eef35d2 · GitHub

以前の環境で上記の merge を反映させたソースで build

以前の環境で最新のレポジトリに反映させて build を試みるも次のようなエラーが。

handlers_drive.go:12:2: cannot find package "github.com/prasmussen/gdrive/auth" in any of:
        /usr/local/Cellar/go/1.8.1/libexec/src/github.com/prasmussen/gdrive/auth (from $GOROOT)
        /Users/t-nakano/src/github.com/prasmussen/gdrive/auth (from $GOPATH)
gdrive.go:7:2: cannot find package "github.com/prasmussen/gdrive/cli" in any of:
        /usr/local/Cellar/go/1.8.1/libexec/src/github.com/prasmussen/gdrive/cli (from $GOROOT)
        /Users/t-nakano/src/github.com/prasmussen/gdrive/cli (from $GOPATH)
compare.go:5:2: cannot find package "github.com/prasmussen/gdrive/drive" in any of:
        /usr/local/Cellar/go/1.8.1/libexec/src/github.com/prasmussen/gdrive/drive (from $GOROOT)
        /Users/t-nakano/src/github.com/prasmussen/gdrive/drive (from $GOPATH)

うまくいかなったので、

go get github.com/tomofumi-nakano/gdrive
go get golang.org/x/net/context golang.org/x/oauth2 golang.org/x/oauth2/google google.golang.org/api/sheets/v4

ということをやってしまった。すると、build は通ったが、issue の症状となってしまう。

fix時の状態に戻して build してみる。

fix時の状態に戻して build してみる。それでも issue の症状になってしまう。

ところが、バックアップしたバイナリではうまく行った。

  • gdrive のソースが悪いわけではない。となると外部ソース関係(ライブラリなど)が怪しい。
  • 外部ソースは現行のバイナリの中に含まれている。

ということがわかる。

gdb でやってみる。

このコマンドは基本的には、googleapi と通信しているだけなので、その通信内容の違いを見れば、何が違うのかが分かる。 違いが判明すれば、どの部分に問題があるのかが分かるだろう。 当初は wireshark なども検討した。ところが、https で通信していれば、中身を見ることはできないことに気がついた。

というわけで、gdb で見ることにした。

懸念点としては、デバッグ用の文字列が埋め込まれているかどうかであるが…

まず gdb で run すると次のようなありがたいメッセージが

During startup program terminated with signal ?, Unknown signal.

c - Unknown ending signal when using debugger gdb - Stack Overflow

はい、すみません、インストール後、次のようなありがたいメッセージを頂いておりました。

gdb requires special privileges to access Mach ports.
You will need to codesign the binary. For instructions, see:

  https://sourceware.org/gdb/wiki/BuildingOnDarwin

On 10.12 (Sierra) or later with SIP, you need to run this:

  echo "set startup-with-shell off" >> ~/.gdbinit

一つ目は sudo しただけだったけど、二つ目は無視していました。

sudo gdb gdrive.ok
...
(gdb) run import  --mime text/csv file.csv
...
warning: unhandled dyld version (15)
Imported xxxx with mime type: 'application/vnd.google-apps.spreadsheet'
...

おおうまく行った。やはり ok バージョンは問題ない。

次にプログラムが埋め込まれているかどうか。

(gdb) list
warning: Source file is more recent than executable.
11      const Version = "2.1.0"
12
13      const DefaultMaxFiles = 30
14      const DefaultMaxChanges = 100
....

おーっ、埋め込まれている模様!!!

warning: Source file is more recent than executable.

は多少気になるがとりあえずOK。

(gdb) list drive/upload.go:178
warning: Source file is more recent than executable.
173             dstFile.Parents = args.Parents
174
175             // Chunk size option
176             chunkSize := googleapi.ChunkSize(int(args.ChunkSize))
177
178             // Wrap file in progress reader
179             progressReader := getProgressReader(srcFile, args.Progress, srcFileInfo.Size())
180
181             // Wrap reader in timeout reader
182             reader, ctx := getTimeoutReaderContext(progressReader, args.Timeout)

おやおや?

https://github.com/tomofumi-nakano/gdrive/commit/4540c5c5398d072613cf8c5efba5f0d12dbc8947#diff-e6d188e26b532dc776d44b31e5db6b5aR178

ここで修正されているはずの内容が修正されていませんね…。どういうことなのでしょうか…

strings で各バイナリの違いを見てみる

strings ~/bin/gdrive.ok> ok.txt
strings ~/bin/gdrive.ng> ng.txt
vim -d ok.txt ng.txt

多すぎて判明せず。とりあえず、別のバイナリと思って良さそう。

もう一度原因を見てみる

google-api-go-client/drive-gen.go at 650535c7d6201e8304c92f38c922a9a3a36c6877 · google/google-api-go-client · GitHub

// Media specifies the media to upload in one or more chunks. The chunk
// size may be controlled by supplying a MediaOption generated by
// googleapi.ChunkSize. The chunk size defaults to
// googleapi.DefaultUploadChunkSize.The Content-Type header used in the
// upload request will be determined by sniffing the contents of r,
// unless a MediaOption generated by googleapi.ContentType is
// supplied.
// At most one of Media and ResumableMedia may be set.
func (c *FilesCreateCall) Media(r io.Reader, options ...googleapi.MediaOption) *FilesCreateCall {
    opts := googleapi.ProcessMediaOptions(options)
    chunkSize := opts.ChunkSize
    if !opts.ForceEmptyContentType {
        r, c.mediaType_ = gensupport.DetermineContentType(r, opts.ContentType)
    }
    c.media_, c.mediaBuffer_ = gensupport.PrepareUpload(r, chunkSize)
    return c
}

ここで、DetermineContentType をするために、 opts.ContentTypeを渡している。 これで、変換をするわけだが、それが、

https://github.com/google/google-api-go-client/blob/a69f0f19d246419bb931b0ac8f4f8d3f3e6d4feb/gensupport/media.go#L72-L99

// DetermineContentType determines the content type of the supplied reader.
// If the content type is already known, it can be specified via ctype.
// Otherwise, the content of media will be sniffed to determine the content type.
// If media implements googleapi.ContentTyper (deprecated), this will be used
// instead of sniffing the content.
// After calling DetectContentType the caller must not perform further reads on
// media, but rather read from the Reader that is returned.
func DetermineContentType(media io.Reader, ctype string) (io.Reader, string) {
    // Note: callers could avoid calling DetectContentType if ctype != "",
    // but doing the check inside this function reduces the amount of
    // generated code.
    if ctype != "" {
        return media, ctype
    }

    // For backwards compatability, allow clients to set content
    // type by providing a ContentTyper for media.
    if typer, ok := media.(googleapi.ContentTyper); ok {
        return media, typer.ContentType()
    }

    sniffer := newContentSniffer(media)
    if ctype, ok := sniffer.ContentType(); ok {
        return sniffer, ctype
    }
    // If content type could not be sniffed, reads from sniffer will eventually fail with an error.
    return sniffer, ""
}

により、単に、c.mediaType_opts.ContentType が入ることになる。

とりあえず、デバッグしてみる

$ sudo gdb gdrive.ok
...
(gdb) break upload.go:187
...
(gdb) run import --mime text/csv file.csv
...
Thread 3 hit Breakpoint 1, github.com/prasmussen/gdrive/drive.(*Drive).uploadFile (self=0xc42002e1e8, args=..., ~r1=0x0, ~r2=0, ~r3=...) at /Users/t-nakano/src/github.com/prasmussen/gdrive/drive/upload.go:187
...
(gdb) p args.Mime
$2 = 0xc4203485d0 "application/vnd.google-apps.spreadsheet"

上記は正常な方。

そして、次は、修正されている方…。

(gdb) list upload.go:187
182             reader, ctx := getTimeoutReaderContext(progressReader, args.Timeout)
183
184             fmt.Fprintf(args.Out, "Uploading %s\n", args.Path)
185             started := time.Now()
186
187             f, err := self.service.Files.Create(dstFile).Fields("id", "name", "size", "md5Checksum", "webContentLink").Context(ctx).Media(reader, chunkSize).Do()
188             if err != nil {
189                     if isTimeoutError(err) {
190                             return nil, 0, fmt.Errorf("Failed to upload file: timeout, no data was transferred for %v", args.Timeout)
191                     }

修正されていない!

いったいどうなっているのか…。なぜ修正されていない…。

もう一度はじめのエラーを思い出してみる。

go get とかで、色々取得していたが…。

そして upload.go を見つける。

見つかった。とりあえず、github.com/prasmussen/を消してbuild してみる。

go build -gcflags "-N -l"
handlers_drive.go:12:2: cannot find package "github.com/prasmussen/gdrive/auth" in any of:
        /usr/local/Cellar/go/1.8.1/libexec/src/github.com/prasmussen/gdrive/auth (from $GOROOT)
        /Users/t-nakano/src/github.com/prasmussen/gdrive/auth (from $GOPATH)
gdrive.go:7:2: cannot find package "github.com/prasmussen/gdrive/cli" in any of:
        /usr/local/Cellar/go/1.8.1/libexec/src/github.com/prasmussen/gdrive/cli (from $GOROOT)
        /Users/t-nakano/src/github.com/prasmussen/gdrive/cli (from $GOPATH)
compare.go:5:2: cannot find package "github.com/prasmussen/gdrive/drive" in any of:
        /usr/local/Cellar/go/1.8.1/libexec/src/github.com/prasmussen/gdrive/drive (from $GOROOT)
        /Users/t-nakano/src/github.com/prasmussen/gdrive/drive (from $GOPATH)
make: *** [gdrive] Error 1

きた、これだ。handlers_drive.go:12: に何が書いてあるんだっけ。

     12         "github.com/prasmussen/gdrive/auth"
     13         "github.com/prasmussen/gdrive/cli"
     14         "github.com/prasmussen/gdrive/drive"

ぐふっ。

そして golang の import のショボさについて知ることになる…

  • 相対パスによる local package はNG(理由はよくわらないが)
  • github 上の push してから go get により、使うのが良いとされる。
  • github で fork した場合(今回の自分)、対象なるパッケージ名も github.com/<user_name> となるはず。
  • そこを修正していなかった!

あらためて、import 先を修正。すると、

(gdb) list upload.go:187
182             progressReader := getProgressReader(srcFile, args.Progress, srcFileInfo.Size())
183
184             // Wrap reader in timeout reader
185             reader, ctx := getTimeoutReaderContext(progressReader, args.Timeout)
186
187             fmt.Fprintf(args.Out, "Uploading %s\n", args.Path)
188             started := time.Now()
189
190             f, err := self.service.Files.Create(dstFile).Fields("id", "name", "size", "md5Checksum", "webContentLink").Context(ctx).Media(reader, chunkSize, contentType).Do()
191             if err != nil {

おー!。思った通りに反映されている!

うーん、去年の11月にPR送ったのに、こういった話があってか、うまくできんといわれて、PRが採用されないんだよね…