OSDN Git Service

ブロックの括弧を独立した行に書く (SA1500, SA1501, SA1502)
[opentween/open-tween.git] / OpenTween / ApplicationEvents.cs
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.
10 //
11 // This file is part of OpenTween.
12 //
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)
16 // any later version.
17 //
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
21 // for more details.
22 //
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.
27
28 #nullable enable
29
30 using System;
31 using System.Collections.Generic;
32 using System.Diagnostics;
33 using System.Globalization;
34 using System.IO;
35 using System.Linq;
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;
44
45 namespace OpenTween
46 {
47     internal class MyApplication
48     {
49         public static readonly CultureInfo[] SupportedUICulture = new[]
50         {
51             new CultureInfo("en"), // 先頭のカルチャはフォールバック先として使用される
52             new CultureInfo("ja"),
53         };
54
55         /// <summary>
56         /// 起動時に指定されたオプションを取得します
57         /// </summary>
58         public static IDictionary<string, string> StartupOptions { get; private set; } = null!;
59
60         /// <summary>
61         /// アプリケーションのメイン エントリ ポイントです。
62         /// </summary>
63         [STAThread]
64         public static int Main(string[] args)
65         {
66             WarnIfApiKeyError();
67             WarnIfRunAsAdministrator();
68
69             if (!CheckRuntimeVersion())
70             {
71                 var message = string.Format(Properties.Resources.CheckRuntimeVersion_Error, ".NET Framework 4.7.2");
72                 MessageBox.Show(message, ApplicationSettings.ApplicationName, MessageBoxButtons.OK, MessageBoxIcon.Error);
73                 return 1;
74             }
75
76             StartupOptions = ParseArguments(args);
77
78             if (!SetConfigDirectoryPath())
79                 return 1;
80
81             SettingManager.LoadAll();
82
83             InitCulture();
84
85             {
86                 // 同じ設定ファイルを使用する OpenTween プロセスの二重起動を防止する
87                 var pt = MyCommon.SettingPath.Replace("\\", "/") + "/" + ApplicationSettings.AssemblyName;
88                 using var mt = new Mutex(false, pt);
89
90                 if (!mt.WaitOne(0, false))
91                 {
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);
94
95                     TryActivatePreviousWindow();
96                     return 1;
97                 }
98
99                 TaskScheduler.UnobservedTaskException += (s, e) =>
100                 {
101                     e.SetObserved();
102                     OnUnhandledException(e.Exception.Flatten());
103                 };
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);
107
108                 Application.EnableVisualStyles();
109                 Application.SetCompatibleTextRenderingDefault(false);
110                 Application.Run(new TweenMain());
111
112                 mt.ReleaseMutex();
113             }
114
115             return 0;
116         }
117
118         private static void WarnIfApiKeyError()
119         {
120             var canDecrypt = ApplicationSettings.TwitterConsumerKey.TryGetValue(out _);
121             if (!canDecrypt)
122             {
123                 var message = Properties.Resources.WarnIfApiKeyError_Message;
124                 MessageBox.Show(message, ApplicationSettings.ApplicationName, MessageBoxButtons.OK, MessageBoxIcon.Warning);
125                 Environment.Exit(-1);
126             }
127         }
128
129         /// <summary>
130         /// OpenTween が管理者権限で実行されている場合に警告を表示します
131         /// </summary>
132         private static void WarnIfRunAsAdministrator()
133         {
134             // UAC が無効なシステムでは警告を表示しない
135             using var lmKey = RegistryKey.OpenBaseKey(RegistryHive.LocalMachine, RegistryView.Registry32);
136             using var systemKey = lmKey.OpenSubKey(@"SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System\");
137
138             var enableLUA = (int?)systemKey?.GetValue("EnableLUA");
139             if (enableLUA != 1)
140                 return;
141
142             using var currentIdentity = WindowsIdentity.GetCurrent();
143             var principal = new WindowsPrincipal(currentIdentity);
144             if (principal.IsInRole(WindowsBuiltInRole.Administrator))
145             {
146                 var message = string.Format(Properties.Resources.WarnIfRunAsAdministrator_Message, ApplicationSettings.ApplicationName);
147                 MessageBox.Show(message, ApplicationSettings.ApplicationName, MessageBoxButtons.OK, MessageBoxIcon.Warning);
148             }
149         }
150
151         /// <summary>
152         /// 動作中の .NET Framework のバージョンが適切かチェックします
153         /// </summary>
154         private static bool CheckRuntimeVersion()
155         {
156             // Mono 上で動作している場合はバージョンチェックを無視します
157             if (Type.GetType("Mono.Runtime", false) != null)
158                 return true;
159
160             // .NET Framework 4.7.2 以降で動作しているかチェックする
161             // 参照: https://docs.microsoft.com/en-us/dotnet/framework/migration-guide/how-to-determine-which-versions-are-installed
162
163             using var lmKey = RegistryKey.OpenBaseKey(RegistryHive.LocalMachine, RegistryView.Registry32);
164             using var ndpKey = lmKey.OpenSubKey(@"SOFTWARE\Microsoft\NET Framework Setup\NDP\v4\Full\");
165
166             var releaseKey = (int)ndpKey.GetValue("Release");
167             return releaseKey >= 461808;
168         }
169
170         /// <summary>
171         /// “/key:value”形式の起動オプションを解釈し IDictionary に変換する
172         /// </summary>
173         /// <remarks>
174         /// 不正な形式のオプションは除外されます。
175         /// また、重複したキーのオプションが入力された場合は末尾に書かれたオプションが採用されます。
176         /// </remarks>
177         internal static IDictionary<string, string> ParseArguments(IEnumerable<string> arguments)
178         {
179             var optionPattern = new Regex(@"^/(.+?)(?::(.*))?$");
180
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);
185         }
186
187         private static void TryActivatePreviousWindow()
188         {
189             // 実行中の同じアプリケーションのウィンドウ・ハンドルの取得
190             var prevProcess = GetPreviousProcess();
191             if (prevProcess == null)
192             {
193                 return;
194             }
195
196             var windowHandle = NativeMethods.GetWindowHandle((uint)prevProcess.Id, ApplicationSettings.ApplicationName);
197             if (windowHandle != IntPtr.Zero)
198             {
199                 NativeMethods.SetActiveWindow(windowHandle);
200             }
201         }
202
203         private static Process? GetPreviousProcess()
204         {
205             var currentProcess = Process.GetCurrentProcess();
206             try
207             {
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));
211             }
212             catch
213             {
214                 return null;
215             }
216         }
217
218         private static void OnUnhandledException(Exception ex)
219         {
220             if (CheckIgnorableError(ex))
221                 return;
222
223             if (MyCommon.ExceptionOut(ex))
224             {
225                 Application.Exit();
226             }
227         }
228
229         /// <summary>
230         /// 無視しても問題のない既知の例外であれば true を返す
231         /// </summary>
232         private static bool CheckIgnorableError(Exception ex)
233         {
234 #if DEBUG
235             return false;
236 #else
237             if (ex is AggregateException aggregated)
238             {
239                 if (aggregated.InnerExceptions.Count != 1)
240                     return false;
241
242                 ex = aggregated.InnerExceptions.Single();
243             }
244
245             switch (ex)
246             {
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)
251                         return true;
252                     break;
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))
260                         return true;
261                     break;
262             }
263
264             return false;
265 #endif
266         }
267
268         public static void InitCulture()
269         {
270             var currentCulture = CultureInfo.CurrentUICulture;
271
272             var settingCultureStr = SettingManager.Common.Language;
273             if (settingCultureStr != "OS")
274             {
275                 try
276                 {
277                     currentCulture = new CultureInfo(settingCultureStr);
278                 }
279                 catch (CultureNotFoundException)
280                 {
281                 }
282             }
283
284             var preferredCulture = GetPreferredCulture(currentCulture);
285             CultureInfo.DefaultThreadCurrentUICulture = preferredCulture;
286             Thread.CurrentThread.CurrentUICulture = preferredCulture;
287         }
288
289         /// <summary>
290         /// サポートしているカルチャの中から、指定されたカルチャに対して適切なカルチャを選択して返します
291         /// </summary>
292         public static CultureInfo GetPreferredCulture(CultureInfo culture)
293         {
294             if (SupportedUICulture.Any(x => x.Contains(culture)))
295                 return culture;
296
297             return SupportedUICulture[0];
298         }
299
300         private static bool SetConfigDirectoryPath()
301         {
302             if (StartupOptions.TryGetValue("configDir", out var configDir) && !MyCommon.IsNullOrEmpty(configDir))
303             {
304                 // 起動オプション /configDir で設定ファイルの参照先を変更できます
305                 if (!Directory.Exists(configDir))
306                 {
307                     var text = string.Format(Properties.Resources.ConfigDirectoryNotExist, configDir);
308                     MessageBox.Show(text, ApplicationSettings.ApplicationName, MessageBoxButtons.OK, MessageBoxIcon.Error);
309                     return false;
310                 }
311
312                 MyCommon.SettingPath = Path.GetFullPath(configDir);
313             }
314             else
315             {
316                 // OpenTween.exe と同じディレクトリに設定ファイルを配置する
317                 MyCommon.SettingPath = Application.StartupPath;
318
319                 SettingManager.LoadAll();
320
321                 try
322                 {
323                     // 設定ファイルが書き込み可能な状態であるかテストする
324                     SettingManager.SaveAll();
325                 }
326                 catch (UnauthorizedAccessException)
327                 {
328                     // 書き込みに失敗した場合 (Program Files 以下に配置されている場合など)
329
330                     // 通常は C:\Users\ユーザー名\AppData\Roaming\OpenTween\ となる
331                     var roamingDir = Path.Combine(new[]
332                     {
333                         Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
334                         ApplicationSettings.ApplicationName,
335                     });
336                     Directory.CreateDirectory(roamingDir);
337
338                     MyCommon.SettingPath = roamingDir;
339
340                     /*
341                      * 書き込みが制限されたディレクトリ内で起動された場合の設定ファイルの扱い
342                      *
343                      *  (A) StartupPath に存在する設定ファイル
344                      *  (B) Roaming に存在する設定ファイル
345                      *
346                      *  1. A も B も存在しない場合
347                      *    => B を新規に作成する
348                      *
349                      *  2. A が存在し、B が存在しない場合
350                      *    => A の内容を B にコピーする (警告を表示)
351                      *
352                      *  3. A が存在せず、B が存在する場合
353                      *    => B を使用する
354                      *
355                      *  4. A も B も存在するが、A の方が更新日時が新しい場合
356                      *    => A の内容を B にコピーする (警告を表示)
357                      *
358                      *  5. A も B も存在するが、B の方が更新日時が新しい場合
359                      *    => B を使用する
360                      */
361                     var startupDirFile = new FileInfo(Path.Combine(Application.StartupPath, "SettingCommon.xml"));
362                     var roamingDirFile = new FileInfo(Path.Combine(roamingDir, "SettingCommon.xml"));
363
364                     if (roamingDirFile.Exists && (!startupDirFile.Exists || startupDirFile.LastWriteTime <= roamingDirFile.LastWriteTime))
365                     {
366                         // 既に Roaming に設定ファイルが存在し、Roaming 内のファイルの方が新しい場合は
367                         // StartupPath に設定ファイルが存在しても無視する
368                         SettingManager.LoadAll();
369                     }
370                     else
371                     {
372                         if (startupDirFile.Exists)
373                         {
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);
377                         }
378
379                         // Roaming に設定ファイルを作成 (StartupPath に読み込みに成功した設定ファイルがあれば内容がコピーされる)
380                         SettingManager.SaveAll();
381                     }
382                 }
383             }
384
385             return true;
386         }
387     }
388 }