1 // OpenTween - Client of Twitter
2 // Copyright (c) 2007-2012 kiri_feather (@kiri_feather) <kiri.feather@gmail.com>
3 // (c) 2008-2012 Moz (@syo68k)
4 // (c) 2008-2012 takeshik (@takeshik) <http://www.takeshik.org/>
5 // (c) 2010-2012 anis774 (@anis774) <http://d.hatena.ne.jp/anis774/>
6 // (c) 2010-2012 fantasticswallow (@f_swallow) <http://twitter.com/f_swallow>
7 // (c) 2012 Egtra (@egtra) <http://dev.activebasic.com/egtra/>
8 // (c) 2012 kim_upsilon (@kim_upsilon) <https://upsilo.net/~upsilon/>
9 // All rights reserved.
11 // This file is part of OpenTween.
13 // This program is free software; you can redistribute it and/or modify it
14 // under the terms of the GNU General public License as published by the Free
15 // Software Foundation; either version 3 of the License, or (at your option)
18 // This program is distributed in the hope that it will be useful, but
19 // WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
20 // or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General public License
23 // You should have received a copy of the GNU General public License along
24 // with this program. If not, see <http://www.gnu.org/licenses/>, or write to
25 // the Free Software Foundation, Inc., 51 Franklin Street - Fifth Floor,
26 // Boston, MA 02110-1301, USA.
31 using System.Collections.Generic;
32 using System.Diagnostics;
33 using System.Globalization;
36 using System.Reflection;
37 using System.Security.Principal;
38 using System.Text.RegularExpressions;
39 using System.Threading;
40 using System.Threading.Tasks;
41 using System.Windows.Forms;
42 using Microsoft.Win32;
43 using OpenTween.Setting;
47 internal class MyApplication
49 public static readonly CultureInfo[] SupportedUICulture = new[]
51 new CultureInfo("en"), // 先頭のカルチャはフォールバック先として使用される
52 new CultureInfo("ja"),
56 /// 起動時に指定されたオプションを取得します
58 public static IDictionary<string, string> StartupOptions { get; private set; } = null!;
61 /// アプリケーションのメイン エントリ ポイントです。
64 public static int Main(string[] args)
67 WarnIfRunAsAdministrator();
69 if (!CheckRuntimeVersion())
71 var message = string.Format(Properties.Resources.CheckRuntimeVersion_Error, ".NET Framework 4.7.2");
72 MessageBox.Show(message, ApplicationSettings.ApplicationName, MessageBoxButtons.OK, MessageBoxIcon.Error);
76 StartupOptions = ParseArguments(args);
78 if (!SetConfigDirectoryPath())
81 SettingManager.LoadAll();
86 // 同じ設定ファイルを使用する OpenTween プロセスの二重起動を防止する
87 var pt = MyCommon.SettingPath.Replace("\\", "/") + "/" + ApplicationSettings.AssemblyName;
88 using var mt = new Mutex(false, pt);
90 if (!mt.WaitOne(0, false))
92 var text = string.Format(MyCommon.ReplaceAppName(Properties.Resources.StartupText1), ApplicationSettings.AssemblyName);
93 MessageBox.Show(text, MyCommon.ReplaceAppName(Properties.Resources.StartupText2), MessageBoxButtons.OK, MessageBoxIcon.Information);
95 TryActivatePreviousWindow();
99 TaskScheduler.UnobservedTaskException += (s, e) =>
102 OnUnhandledException(e.Exception.Flatten());
104 Application.ThreadException += (s, e) => OnUnhandledException(e.Exception);
105 AppDomain.CurrentDomain.UnhandledException += (s, e) => OnUnhandledException((Exception)e.ExceptionObject);
106 AsyncTimer.UnhandledException += (s, e) => OnUnhandledException(e.Exception);
108 Application.EnableVisualStyles();
109 Application.SetCompatibleTextRenderingDefault(false);
110 Application.Run(new TweenMain());
118 private static void WarnIfApiKeyError()
120 var canDecrypt = ApplicationSettings.TwitterConsumerKey.TryGetValue(out _);
123 var message = Properties.Resources.WarnIfApiKeyError_Message;
124 MessageBox.Show(message, ApplicationSettings.ApplicationName, MessageBoxButtons.OK, MessageBoxIcon.Warning);
125 Environment.Exit(-1);
130 /// OpenTween が管理者権限で実行されている場合に警告を表示します
132 private static void WarnIfRunAsAdministrator()
134 // UAC が無効なシステムでは警告を表示しない
135 using var lmKey = RegistryKey.OpenBaseKey(RegistryHive.LocalMachine, RegistryView.Registry32);
136 using var systemKey = lmKey.OpenSubKey(@"SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System\");
138 var enableLUA = (int?)systemKey?.GetValue("EnableLUA");
142 using var currentIdentity = WindowsIdentity.GetCurrent();
143 var principal = new WindowsPrincipal(currentIdentity);
144 if (principal.IsInRole(WindowsBuiltInRole.Administrator))
146 var message = string.Format(Properties.Resources.WarnIfRunAsAdministrator_Message, ApplicationSettings.ApplicationName);
147 MessageBox.Show(message, ApplicationSettings.ApplicationName, MessageBoxButtons.OK, MessageBoxIcon.Warning);
152 /// 動作中の .NET Framework のバージョンが適切かチェックします
154 private static bool CheckRuntimeVersion()
156 // Mono 上で動作している場合はバージョンチェックを無視します
157 if (Type.GetType("Mono.Runtime", false) != null)
160 // .NET Framework 4.7.2 以降で動作しているかチェックする
161 // 参照: https://docs.microsoft.com/en-us/dotnet/framework/migration-guide/how-to-determine-which-versions-are-installed
163 using var lmKey = RegistryKey.OpenBaseKey(RegistryHive.LocalMachine, RegistryView.Registry32);
164 using var ndpKey = lmKey.OpenSubKey(@"SOFTWARE\Microsoft\NET Framework Setup\NDP\v4\Full\");
166 var releaseKey = (int)ndpKey.GetValue("Release");
167 return releaseKey >= 461808;
171 /// “/key:value”形式の起動オプションを解釈し IDictionary に変換する
174 /// 不正な形式のオプションは除外されます。
175 /// また、重複したキーのオプションが入力された場合は末尾に書かれたオプションが採用されます。
177 internal static IDictionary<string, string> ParseArguments(IEnumerable<string> arguments)
179 var optionPattern = new Regex(@"^/(.+?)(?::(.*))?$");
181 return arguments.Select(x => optionPattern.Match(x))
182 .Where(x => x.Success)
183 .GroupBy(x => x.Groups[1].Value)
184 .ToDictionary(x => x.Key, x => x.Last().Groups[2].Value);
187 private static void TryActivatePreviousWindow()
189 // 実行中の同じアプリケーションのウィンドウ・ハンドルの取得
190 var prevProcess = GetPreviousProcess();
191 if (prevProcess == null)
196 var windowHandle = NativeMethods.GetWindowHandle((uint)prevProcess.Id, ApplicationSettings.ApplicationName);
197 if (windowHandle != IntPtr.Zero)
199 NativeMethods.SetActiveWindow(windowHandle);
203 private static Process? GetPreviousProcess()
205 var currentProcess = Process.GetCurrentProcess();
208 return Process.GetProcessesByName(currentProcess.ProcessName)
209 .Where(p => p.Id != currentProcess.Id)
210 .FirstOrDefault(p => p.MainModule.FileName.Equals(currentProcess.MainModule.FileName, StringComparison.OrdinalIgnoreCase));
218 private static void OnUnhandledException(Exception ex)
220 if (CheckIgnorableError(ex))
223 if (MyCommon.ExceptionOut(ex))
230 /// 無視しても問題のない既知の例外であれば true を返す
232 private static bool CheckIgnorableError(Exception ex)
237 if (ex is AggregateException aggregated)
239 if (aggregated.InnerExceptions.Count != 1)
242 ex = aggregated.InnerExceptions.Single();
247 case System.Net.WebException webEx:
248 // SSL/TLS のネゴシエーションに失敗した場合に発生する。なぜかキャッチできない例外
249 // https://osdn.net/ticket/browse.php?group_id=6526&tid=37432
250 if (webEx.Status == System.Net.WebExceptionStatus.SecureChannelFailure)
253 case System.Threading.Tasks.TaskCanceledException cancelEx:
254 // ton.twitter.com の画像でタイムアウトした場合、try-catch で例外がキャッチできない
255 // https://osdn.net/ticket/browse.php?group_id=6526&tid=37433
256 var stackTrace = new System.Diagnostics.StackTrace(cancelEx);
257 var lastFrameMethod = stackTrace.GetFrame(stackTrace.FrameCount - 1).GetMethod();
258 if (lastFrameMethod.ReflectedType == typeof(Connection.TwitterApiConnection) &&
259 lastFrameMethod.Name == nameof(Connection.TwitterApiConnection.GetStreamAsync))
268 public static void InitCulture()
270 var currentCulture = CultureInfo.CurrentUICulture;
272 var settingCultureStr = SettingManager.Common.Language;
273 if (settingCultureStr != "OS")
277 currentCulture = new CultureInfo(settingCultureStr);
279 catch (CultureNotFoundException)
284 var preferredCulture = GetPreferredCulture(currentCulture);
285 CultureInfo.DefaultThreadCurrentUICulture = preferredCulture;
286 Thread.CurrentThread.CurrentUICulture = preferredCulture;
290 /// サポートしているカルチャの中から、指定されたカルチャに対して適切なカルチャを選択して返します
292 public static CultureInfo GetPreferredCulture(CultureInfo culture)
294 if (SupportedUICulture.Any(x => x.Contains(culture)))
297 return SupportedUICulture[0];
300 private static bool SetConfigDirectoryPath()
302 if (StartupOptions.TryGetValue("configDir", out var configDir) && !MyCommon.IsNullOrEmpty(configDir))
304 // 起動オプション /configDir で設定ファイルの参照先を変更できます
305 if (!Directory.Exists(configDir))
307 var text = string.Format(Properties.Resources.ConfigDirectoryNotExist, configDir);
308 MessageBox.Show(text, ApplicationSettings.ApplicationName, MessageBoxButtons.OK, MessageBoxIcon.Error);
312 MyCommon.SettingPath = Path.GetFullPath(configDir);
316 // OpenTween.exe と同じディレクトリに設定ファイルを配置する
317 MyCommon.SettingPath = Application.StartupPath;
319 SettingManager.LoadAll();
323 // 設定ファイルが書き込み可能な状態であるかテストする
324 SettingManager.SaveAll();
326 catch (UnauthorizedAccessException)
328 // 書き込みに失敗した場合 (Program Files 以下に配置されている場合など)
330 // 通常は C:\Users\ユーザー名\AppData\Roaming\OpenTween\ となる
331 var roamingDir = Path.Combine(new[]
333 Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
334 ApplicationSettings.ApplicationName,
336 Directory.CreateDirectory(roamingDir);
338 MyCommon.SettingPath = roamingDir;
341 * 書き込みが制限されたディレクトリ内で起動された場合の設定ファイルの扱い
343 * (A) StartupPath に存在する設定ファイル
344 * (B) Roaming に存在する設定ファイル
349 * 2. A が存在し、B が存在しない場合
350 * => A の内容を B にコピーする (警告を表示)
352 * 3. A が存在せず、B が存在する場合
355 * 4. A も B も存在するが、A の方が更新日時が新しい場合
356 * => A の内容を B にコピーする (警告を表示)
358 * 5. A も B も存在するが、B の方が更新日時が新しい場合
361 var startupDirFile = new FileInfo(Path.Combine(Application.StartupPath, "SettingCommon.xml"));
362 var roamingDirFile = new FileInfo(Path.Combine(roamingDir, "SettingCommon.xml"));
364 if (roamingDirFile.Exists && (!startupDirFile.Exists || startupDirFile.LastWriteTime <= roamingDirFile.LastWriteTime))
366 // 既に Roaming に設定ファイルが存在し、Roaming 内のファイルの方が新しい場合は
367 // StartupPath に設定ファイルが存在しても無視する
368 SettingManager.LoadAll();
372 if (startupDirFile.Exists)
374 // StartupPath に設定ファイルが存在し、Roaming 内のファイルよりも新しい場合のみ警告を表示する
375 var message = string.Format(Properties.Resources.SettingPath_Relocation, Application.StartupPath, MyCommon.SettingPath);
376 MessageBox.Show(message, ApplicationSettings.ApplicationName, MessageBoxButtons.OK, MessageBoxIcon.Information);
379 // Roaming に設定ファイルを作成 (StartupPath に読み込みに成功した設定ファイルがあれば内容がコピーされる)
380 SettingManager.SaveAll();