「タスクスケジューラ + 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 は内部でこう動きます。
- 認証用 URL を生成
- 既定ブラウザを起動(OpenWith.exe → chrome.exe / edge.exe など)
- ユーザーが Google アカウントでログイン
- 認証コードをアプリに返す
- refresh_token を保存する
つまり ユーザーが操作できる前提の API です。
2. タスクスケジューラは「デスクトップが存在しない」状態で動く
タスクスケジューラは以下のような環境で動きます。
- デスクトップがない(非対話モード)
- ユーザーの UI セッションが存在しない
- ブラウザを表示できない
この状態で AuthorizeAsync がブラウザを開こうとすると…
3. Windows は「ブラウザを開けない → OpenWith.exe を起動し続ける」
Windows の挙動はこうです。
- AuthorizeAsync が URL を開こうとする
- Windows は「UI がないのでブラウザを起動できない」
- 代わりに OpenWith.exe(既定アプリ選択ダイアログ) を起動しようとする
- しかし UI がないので表示できない
- 失敗 → 再試行
- 失敗 → 再試行
- 失敗 → 再試行
- …無限ループ
結果として 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_token | API を呼ぶための一時鍵 |
| refresh_token | access_token を再発行するための鍵 |
| token_type | 認証方式(Bearer) |
| expiry | access_token の期限 |
token.jsonが 0KB のまま残る、または認証ファイルが消えてしまう理由
①token.jsonが 0KB のまま残る理由
これは FileDataStore がトークンを書き込む途中で失敗したときに起きます。
主な原因
1. AuthorizeAsync がブラウザを開こうとして失敗 → 例外発生 → 書き込み途中で中断
タスクスケジューラで UI がない状態で AuthorizeAsync を呼ぶと、
- ブラウザ起動に失敗
- OpenWith.exe が暴走
- AuthorizeAsync が例外を投げる
- FileDataStore が 空ファイルを作った直後に書き込みできず終了
結果 → 0KB の token.json が残る
2. 書き込み権限の問題(フォルダに書けない)
FileDataStore は内部でこう動きます:
- token.json を まず空ファイルとして作成
- JSON を書き込む
- 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 無効化 |
実行環境では、次のようなことが起きていることが推測されます。
- タスクスケジューラで AuthorizeAsync が走る
- UI がないのでブラウザ起動に失敗
- FileDataStore が空ファイル(0KB)を作る
- 書き込み前に例外で処理が止まる
- 次回起動で 0KB を読み込む
- FileDataStore が「壊れている」と判断して削除
- 再び AuthorizeAsync が走る
- OpenWith.exe が暴走
- 無限ループ
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
Leave a Comment