タスクスケジューラで実行すると UI がないため、AuthorizeAsync がブラウザを開こうとして失敗し、OpenWith.exe が暴走する。

「タスクスケジューラ + AuthorizeAsync」でOpenWith.exeが暴走する理由

token.jsonには何が保存されているか

token.jsonが0kbである場合や削除される場合の主因

refresh_token が無効になる理由(要点)

OpenWith.exeが暴走しないAuthorizeAsyncを使用する書き方

タスクスケジューラで送信するプログラムのふたつの選択肢

なぜ「タスクスケジューラ + AuthorizeAsync」で OpenWith.exe が暴走するのか

次のようなコードをタスクスケジューラで実行するとOpenWith.exeが暴走し、メモリリークが起こることがあります。

Imports Google.Apis.Auth.OAuth2
Imports Google.Apis.Gmail.v1
Imports Google.Apis.Gmail.v1.Data
Imports Google.Apis.Services
Imports Newtonsoft.Json
Imports System.IO
Imports System.Text

Public Class GmailSender

    Private Shared ReadOnly Scopes As String() = {GmailService.Scope.GmailSend}
    Private Shared ReadOnly ApplicationName As String = "MyGmailApp"

    Public Shared Sub SendMail()

        Dim strFile As String  = "C:\windowstask\GmailSender\client_secret.json"
        Dim strAuthFile As String = My.Settings.StrAuth

        Dim result As UserCredential

        Using stream As FileStream = New FileStream(strFile, FileMode.Open, FileAccess.Read)

	        Dim folder As String =
Path.Combine(Environment.GetFolderPath(SpecialFolder.Personal), strAuthFile)

	        result = GoogleWebAuthorizationBroker.AuthorizeAsync(
		        GoogleClientSecrets.Load(stream).Secrets, 
		        Scopes,
		        "user", 
		        CancellationToken.None, 
		        New FileDataStore(folder, True)
	        ).Result

	        Console.WriteLine(("Credential file saved to: " & folder))
        End Using

        Dim initializer As New Initializer With { _
	        .HttpClientInitializer = result, _
	        .ApplicationName = ApplicationName _
        }

        Dim serv As New GmailService(initializer)

        Dim msg As New AE.Net.Mail.MailMessage()
        msg.Subject = "テストメール"
        msg.Body = "これはテストメールです。"
        msg.From = New MailAddress("youraddress@example.com")
        msg.To.Add(New MailAddress("target@example.com"))

        Dim msgStr As String
        Using ms As New MemoryStream()
            msg.Save(ms)
            msgStr = Convert.ToBase64String(ms.ToArray()) _
                .Replace("+", "-").Replace("/", "_").Replace("=", "")
        End Using

        Dim gmailMsg As New Message() With {.Raw = msgStr}

        service.Users.Messages.Send(gmailMsg, "me").Execute()

    End Sub

End Class

それは次のような理由からです。

1. AuthorizeAsync は「ブラウザを開いてユーザーにログインさせる」仕組み

GoogleWebAuthorizationBroker.AuthorizeAsync は内部でこう動きます。

  1. 認証用 URL を生成
  2. 既定ブラウザを起動(OpenWith.exe → chrome.exe / edge.exe など)
  3. ユーザーが Google アカウントでログイン
  4. 認証コードをアプリに返す
  5. refresh_token を保存する

つまり ユーザーが操作できる前提の API です。

2. タスクスケジューラは「デスクトップが存在しない」状態で動

タスクスケジューラは以下のような環境で動きます。

  • デスクトップがない(非対話モード)
  • ユーザーの UI セッションが存在しない
  • ブラウザを表示できない

この状態で AuthorizeAsync がブラウザを開こうとすると…

3. Windows は「ブラウザを開けない → OpenWith.exe を起動し続ける」

Windows の挙動はこうです。

  1. AuthorizeAsync が URL を開こうとする
  2. Windows は「UI がないのでブラウザを起動できない」
  3. 代わりに OpenWith.exe(既定アプリ選択ダイアログ) を起動しようとする
  4. しかし UI がないので表示できない
  5. 失敗 → 再試行
  6. 失敗 → 再試行
  7. 失敗 → 再試行
  8. …無限ループ

結果として OpenWith.exe が大量に起動してメモリを食い尽くすという現象が発生します。

token.jsonには何が保存されているか

OAuth 2.0 の4つの重要フィールドの意味

OAuth 認証後に token.json に保存されるのは、主にこの4つです:

  • access_token
  • refresh_token
  • token_type
  • expiry(有効期限)

それぞれの役割を、実際の Gmail API の動作と結びつけて説明します。

1. access_token(アクセストークン)

Google API にアクセスするための 一時的な鍵 です。

特徴

  • 有効期限は 1時間(3600秒)
  • 期限が切れると使えなくなる
  • API 呼び出しのたびに HTTP ヘッダに付けて送る

例えるなら「1時間だけ使える入館証」です。

2. refresh_token(リフレッシュトークン)

access_token が期限切れになったときに、新しい access_token を発行するための鍵

特徴

  • 有効期限は 基本的に無期限(ただし無効化されることはある)
  • ブラウザを開かずに自動更新できる
  • token.json に保存される最重要データ

例えるなら「入館証を再発行するための身分証明書」

これがあるから、
初回だけブラウザで認証 → 以降は自動更新
が可能になります。

3. token_type(トークンの種類)

API に送るときの認証方式を示す文字列。

Google OAuth ではほぼ必ず:

“token_type”: “Bearer”

Bearer とは:

「このトークンを持っている人はアクセスを許可する」

という意味。

例えるなら「この入館証は“提示すれば入れるタイプ”です」という区分。

4. expiry(有効期限)

access_token の期限が いつ切れるか を示す日時。

“expires_in”: 3600, “issued”: “2024-01-01T12:34:56Z”

この2つから、

issued + expires_in = 有効期限

を計算します。

例えるなら「この入館証は13:34:56まで有効です」という情報。

まとめ

項目役割
access_tokenAPI を呼ぶための一時鍵
refresh_tokenaccess_token を再発行するための鍵
token_type認証方式(Bearer)
expiryaccess_token の期限

token.jsonが 0KB のまま残る、または認証ファイルが消えてしまう理由

①token.jsonが 0KB のまま残る理由

これは FileDataStore がトークンを書き込む途中で失敗したときに起きます。

主な原因

1. AuthorizeAsync がブラウザを開こうとして失敗 → 例外発生 → 書き込み途中で中断

タスクスケジューラで UI がない状態で AuthorizeAsync を呼ぶと、

  • ブラウザ起動に失敗
  • OpenWith.exe が暴走
  • AuthorizeAsync が例外を投げる
  • FileDataStore が 空ファイルを作った直後に書き込みできず終了

結果 → 0KB の token.json が残る

2. 書き込み権限の問題(フォルダに書けない)

FileDataStore は内部でこう動きます:

  1. token.json を まず空ファイルとして作成
  2. JSON を書き込む
  3. Close して保存完了

権限不足やウイルス対策ソフトがブロックすると、

  • 空ファイルは作れる
  • 中身を書けない
  • → 0KB のまま残る

3. プロセスが強制終了された(タスクのタイムアウトなど)

書き込み途中でプロセスが落ちると、やはり 空ファイルだけが残る

② 認証ファイルが消えてしまう理由

これは FileDataStore が「壊れたトークン」と判断して削除するときに起きます。

Google API .NET ライブラリは、トークンが壊れている・読み込めない場合に「無効なトークン」と判断して削除するという挙動があります。

主な原因

1. 0KB の token.json を読み込めず “壊れたトークン” と判断される

次の起動時に FileDataStore が token.json を読み込むと、

  • JSON としてパースできない
  • → 「壊れている」と判断
  • 自動的に削除される

つまり、0KB → 次回起動で削除という流れです。

2. refresh_token が無効化されて Google 側が拒否した

Google は以下のとき refresh_token を無効化します:

  • パスワード変更
  • アカウントのセキュリティ警告
  • 長期間未使用
  • 同じ OAuth クライアントで 50 個以上の refresh_token を発行
  • Google 側のセキュリティ判定でブロック

無効な refresh_token を使うと、

  • トークン更新に失敗
  • FileDataStore が「無効」と判断
  • token.json を削除

3. FileDataStore の内部処理で “再生成” のために削除される

FileDataStore は

  • 読み込み失敗
  • パース失敗
  • トークン更新失敗

のいずれかで 「再生成が必要」と判断すると削除する仕様です。

現象原因
token.json が 0KB のまま残る書き込み途中で失敗(UI なしで AuthorizeAsync、権限不足、プロセス強制終了)
token.json が 消えるFileDataStore が「壊れたトークン」と判断して削除、refresh_token 無効化

実行環境では、次のようなことが起きていることが推測されます。

  1. タスクスケジューラで AuthorizeAsync が走る
  2. UI がないのでブラウザ起動に失敗
  3. FileDataStore が空ファイル(0KB)を作る
  4. 書き込み前に例外で処理が止まる
  5. 次回起動で 0KB を読み込む
  6. FileDataStore が「壊れている」と判断して削除
  7. 再び AuthorizeAsync が走る
  8. OpenWith.exe が暴走
  9. 無限ループ

refresh_token が無効になる理由(要点)

  • パスワード変更
  • セキュリティ警告
  • 長期間未使用
  • 50個以上発行
  • OAuth クライアント再作成
  • Google のセキュリティ判定
  • ユーザーが手動で削除
  • AuthorizeAsync をタスクで毎回実行していることが最大の原因

OpenWith.exeが暴走しないAuthorizeAsyncを使用する書き方

Imports Google.Apis.Auth.OAuth2
Imports Google.Apis.Gmail.v1
Imports Google.Apis.Services
Imports System.IO
Imports Newtonsoft.Json

Public Function GetCredential() As UserCredential

    Dim jsonPath As String = "C:\windowstask\GmailSender\client_secret.json"
    Dim authFolder As String = Path.Combine(
        Environment.GetFolderPath(Environment.SpecialFolder.Personal),
        "MyAuth"
    )
    Dim tokenFile As String = Path.Combine(authFolder, "Google.Apis.Auth.OAuth2.Responses.TokenResponse-user")

    ' ★ 1. token.json が存在しない → 再認証しない(タスクでは絶対に AuthorizeAsync を呼ばない)
    If Not File.Exists(tokenFile) Then
        Throw New Exception("token.json が存在しません。GUI ありで初回認証を行ってください。")
    End If

    ' ★ 2. token.json が 0KB → 壊れているので再認証しない
    Dim fi As New FileInfo(tokenFile)
    If fi.Length < 10 Then
        Throw New Exception("token.json が壊れています(0KB)。GUI ありで再認証してください。")
    End If

    ' ★ 3. token.json が JSON として読めるかチェック
    Try
        Dim json = File.ReadAllText(tokenFile)
        Dim obj = JsonConvert.DeserializeObject(json)
        If obj Is Nothing Then
            Throw New Exception("token.json が破損しています。")
        End If
    Catch ex As Exception
        Throw New Exception("token.json が破損しています。GUI ありで再認証してください。")
    End Try

    ' ★ 4. 壊れていない場合だけ FileDataStore を使って credential を読み込む
    Dim stream = New FileStream(jsonPath, FileMode.Open, FileAccess.Read)
    Dim result = GoogleWebAuthorizationBroker.AuthorizeAsync(
        GoogleClientSecrets.Load(stream).Secrets,
        {GmailService.Scope.GmailSend},
        "user",
        Threading.CancellationToken.None,
        New FileDataStore(authFolder, True)
    ).Result

    Return result

End Function

このようにすれば壊れている token.json を検知したら 送信しないことになります。
→ これは OpenWith.exe 暴走を完全に防ぐための安全策
また、壊れている場合は GUI ありで再認証が必要になります。
→ タスクスケジューラでは絶対に再認証できないため
→ 再認証をタスクでやろうとすると OpenWith.exe が暴走する

タスクスケジューラで送信するプログラムのふたつの選択肢

選択肢 1:Service Account + Domain-wide Delegation に移行する(最強)

これなら:

  • refresh_token 不要
  • token.json 不要
  • AuthorizeAsync 不要
  • ブラウザ不要
  • OpenWith.exe 暴走ゼロ
  • タスクスケジューラで 24 時間安定稼働

つまり 壊れる要素がゼロになります。

Google Workspace を使っているなら、これが圧倒的に正解です。

選択肢 2:token.json をバックアップして自動復旧する

もし Workspace ではなく個人 Gmail を使っている場合は、Service Account が使えません。その場合は、

  • 初回認証後の token.json をバックアップ
  • 壊れていたらバックアップを復元
  • AuthorizeAsync は絶対に呼ばない

という構成にできます。

Imports Google.Apis.Auth.OAuth2
Imports Google.Apis.Gmail.v1
Imports Google.Apis.Gmail.v1.Data
Imports Google.Apis.Services
Imports Newtonsoft.Json
Imports System.IO
Imports System.Text

Public Class GmailSender

    Private Shared ReadOnly Scopes As String() = {GmailService.Scope.GmailSend}
    Private Shared ReadOnly ApplicationName As String = "MyGmailApp"

    Public Shared Sub SendMail()

        Dim clientSecretPath As String = "C:\windowstask\GmailSender\client_secret.json"
        Dim authFolder As String = Path.Combine(
          Environment.GetFolderPath(Environment.SpecialFolder.Personal),
            "MyAuth"
        )
        Dim tokenFile As String = Path.Combine(authFolder, "Google.Apis.Auth.OAuth2.Responses.TokenResponse-user")
        Dim backupFile As String = tokenFile & ".bak"

        ' ★ 1. token.json が存在しない → タスクでは絶対に再認証しない
        If Not File.Exists(tokenFile) Then
            Throw New Exception("token.json が存在しません。GUI ありで初回認証を行ってください。")
        End If

        ' ★ 2. token.json が 0KB → 壊れている
        If New FileInfo(tokenFile).Length < 10 Then
            If File.Exists(backupFile) Then
                File.Copy(backupFile, tokenFile, True)
            Else
                Throw New Exception("token.json が壊れています。GUI ありで再認証してください。")
            End If
        End If

        ' ★ 3. JSON として読めるかチェック
        Try
            Dim json = File.ReadAllText(tokenFile)
            Dim obj = JsonConvert.DeserializeObject(json)
            If obj Is Nothing Then Throw New Exception()
        Catch
            If File.Exists(backupFile) Then
                File.Copy(backupFile, tokenFile, True)
            Else
                Throw New Exception("token.json が破損しています。GUI ありで再認証してください。")
            End If
        End Try

        ' ★ 4. 正常な token.json を読み込む(ここで初めて AuthorizeAsync)
        Dim credential = GoogleWebAuthorizationBroker.AuthorizeAsync(
            GoogleClientSecrets.Load(New FileStream(clientSecretPath, FileMode.Open, FileAccess.Read)).Secrets,
            Scopes,
            "user",
            Threading.CancellationToken.None,
            New FileDataStore(authFolder, True)
        ).Result

        ' ★ 5. token.json をバックアップ
        File.Copy(tokenFile, backupFile, True)

        ' ★ 6. GmailService 初期化
        Dim service As New GmailService(New BaseClientService.Initializer() With {
            .HttpClientInitializer = credential,
            .ApplicationName = ApplicationName
        })

        ' ★ 7. メール作成
        Dim msg As New AE.Net.Mail.MailMessage()
        msg.Subject = "テストメール"
        msg.Body = "これはテストメールです。"
        msg.From = New MailAddress("youraddress@example.com")
        msg.To.Add(New MailAddress("target@example.com"))

        Dim msgStr As String
        Using ms As New MemoryStream()
            msg.Save(ms)
            msgStr = Convert.ToBase64String(ms.ToArray()) _
                .Replace("+", "-").Replace("/", "_").Replace("=", "")
        End Using

        Dim gmailMsg As New Message() With {.Raw = msgStr}

        ' ★ 8. Gmail API で送信
        service.Users.Messages.Send(gmailMsg, "me").Execute()

    End Sub

End Class

More Reading

Post navigation

Leave a Comment

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です