OSDN Git Service

アプリケーション名を表す ApplicationSettings.ApplicationName を追加
[opentween/open-tween.git] / OpenTween / ApplicationEvents.cs
index b8d73da..3023ef1 100644 (file)
@@ -28,6 +28,7 @@
 using System;
 using System.Collections.Generic;
 using System.IO;
+using System.Linq;
 using System.Diagnostics;
 using System.Windows.Forms;
 using System.Text.RegularExpressions;
@@ -35,11 +36,20 @@ using System.Threading;
 using System.Threading.Tasks;
 using System.Globalization;
 using System.Reflection;
+using Microsoft.Win32;
+using OpenTween.Setting;
+using System.Security.Principal;
 
 namespace OpenTween
 {
     internal class MyApplication
     {
+        public static readonly CultureInfo[] SupportedUICulture = new[]
+        {
+            new CultureInfo("en"), // 先頭のカルチャはフォールバック先として使用される
+            new CultureInfo("ja"),
+        };
+
         /// <summary>
         /// 起動時に指定されたオプションを取得します
         /// </summary>
@@ -51,12 +61,26 @@ namespace OpenTween
         [STAThread]
         static int Main(string[] args)
         {
+            WarnIfRunAsAdministrator();
+
+            if (!CheckRuntimeVersion())
+            {
+                var message = string.Format(Properties.Resources.CheckRuntimeVersion_Error, ".NET Framework 4.5.1");
+                MessageBox.Show(message, ApplicationSettings.ApplicationName, MessageBoxButtons.OK, MessageBoxIcon.Error);
+                return 1;
+            }
+
             StartupOptions = ParseArguments(args);
 
-            CheckSettingFilePath();
+            if (!SetConfigDirectoryPath())
+                return 1;
+
+            SettingManager.LoadAll();
+
             InitCulture();
 
-            string pt = Application.ExecutablePath.Replace("\\", "/") + "/" + Application.ProductName;
+            // 同じ設定ファイルを使用する OpenTween プロセスの二重起動を防止する
+            string pt = MyCommon.settingPath.Replace("\\", "/") + "/" + Application.ProductName;
             using (Mutex mt = new Mutex(false, pt))
             {
                 if (!mt.WaitOne(0, false))
@@ -64,7 +88,7 @@ namespace OpenTween
                     var text = string.Format(MyCommon.ReplaceAppName(Properties.Resources.StartupText1), MyCommon.GetAssemblyName());
                     MessageBox.Show(text, MyCommon.ReplaceAppName(Properties.Resources.StartupText2), MessageBoxButtons.OK, MessageBoxIcon.Information);
 
-                    ShowPreviousWindow();
+                    TryActivatePreviousWindow();
                     return 1;
                 }
 
@@ -86,110 +110,265 @@ namespace OpenTween
             }
         }
 
-        internal static IDictionary<string, string> ParseArguments(IEnumerable<string> arguments)
+        /// <summary>
+        /// OpenTween が管理者権限で実行されている場合に警告を表示します
+        /// </summary>
+        private static void WarnIfRunAsAdministrator()
         {
-            var results = new Dictionary<string, string>();
-            var optionPattern = new Regex(@"^/(.+?)(?::(.*))?$");
+            // UAC が無効なシステムでは警告を表示しない
+            using (var lmKey = RegistryKey.OpenBaseKey(RegistryHive.LocalMachine, RegistryView.Registry32))
+            using (var systemKey = lmKey.OpenSubKey(@"SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System\"))
+            {
+                var enableLUA = (int?)systemKey?.GetValue("EnableLUA");
+                if (enableLUA != 1)
+                    return;
+            }
 
-            foreach (var arg in arguments)
+            using (var currentIdentity = WindowsIdentity.GetCurrent())
             {
-                var match = optionPattern.Match(arg);
-                if (match == null)
-                    continue;
+                var principal = new WindowsPrincipal(currentIdentity);
+                if (principal.IsInRole(WindowsBuiltInRole.Administrator))
+                {
+                    var message = string.Format(Properties.Resources.WarnIfRunAsAdministrator_Message, ApplicationSettings.ApplicationName);
+                    MessageBox.Show(message, ApplicationSettings.ApplicationName, MessageBoxButtons.OK, MessageBoxIcon.Warning);
+                }
+            }
+        }
 
-                results[match.Groups[1].Value] = match.Groups[2].Value;
+        /// <summary>
+        /// 動作中の .NET Framework のバージョンが適切かチェックします
+        /// </summary>
+        private static bool CheckRuntimeVersion()
+        {
+            // Mono 上で動作している場合はバージョンチェックを無視します
+            if (Type.GetType("Mono.Runtime", false) != null)
+                return true;
+
+            // .NET Framework 4.5.1 以降で動作しているかチェックする
+            // 参照: http://msdn.microsoft.com/en-us/library/hh925568%28v=vs.110%29.aspx
+
+            using (var lmKey = RegistryKey.OpenBaseKey(RegistryHive.LocalMachine, RegistryView.Registry32))
+            using (var ndpKey = lmKey.OpenSubKey(@"SOFTWARE\Microsoft\NET Framework Setup\NDP\v4\Full\"))
+            {
+                var releaseKey = (int)ndpKey.GetValue("Release");
+                return releaseKey >= 378675;
             }
+        }
+
+        /// <summary>
+        /// “/key:value”形式の起動オプションを解釈し IDictionary に変換する
+        /// </summary>
+        /// <remarks>
+        /// 不正な形式のオプションは除外されます。
+        /// また、重複したキーのオプションが入力された場合は末尾に書かれたオプションが採用されます。
+        /// </remarks>
+        internal static IDictionary<string, string> ParseArguments(IEnumerable<string> arguments)
+        {
+            var optionPattern = new Regex(@"^/(.+?)(?::(.*))?$");
 
-            return results;
+            return arguments.Select(x => optionPattern.Match(x))
+                .Where(x => x.Success)
+                .GroupBy(x => x.Groups[1].Value)
+                .ToDictionary(x => x.Key, x => x.Last().Groups[2].Value);
         }
 
-        private static void ShowPreviousWindow()
+        private static void TryActivatePreviousWindow()
         {
             // 実行中の同じアプリケーションのウィンドウ・ハンドルの取得
-            var prevProcess = Win32Api.GetPreviousProcess();
+            var prevProcess = GetPreviousProcess();
             if (prevProcess == null)
-                return;
-
-            if (prevProcess.MainWindowHandle != IntPtr.Zero)
             {
-                // 起動中のアプリケーションを最前面に表示
-                Win32Api.WakeupWindow(prevProcess.MainWindowHandle);
+                return;
             }
-            else
+
+            IntPtr windowHandle = NativeMethods.GetWindowHandle((uint)prevProcess.Id, ApplicationSettings.ApplicationName);
+            if (windowHandle != IntPtr.Zero)
             {
-                //プロセス特定は出来たが、ウィンドウハンドルが取得できなかった(アイコン化されている)
-                //タスクトレイアイコンのクリックをエミュレート
-                //注:アイコン特定はTooltipの文字列で行うため、多重起動時は先に見つけた物がアクティブになる
-                Win32Api.ClickTasktrayIcon(Application.ProductName);
+                NativeMethods.SetActiveWindow(windowHandle);
             }
         }
 
-        private static void OnUnhandledException(Exception ex)
+        private static Process GetPreviousProcess()
         {
-            if (MyCommon.ExceptionOut(ex))
+            var currentProcess = Process.GetCurrentProcess();
+            try
             {
-                Application.Exit();
+                return Process.GetProcessesByName(currentProcess.ProcessName)
+                    .Where(p => p.Id != currentProcess.Id)
+                    .FirstOrDefault(p => p.MainModule.FileName.Equals(currentProcess.MainModule.FileName, StringComparison.OrdinalIgnoreCase));
+            }
+            catch
+            {
+                return null;
             }
         }
 
-        private static bool IsEqualCurrentCulture(string CultureName)
+        private static void OnUnhandledException(Exception ex)
         {
-            return Thread.CurrentThread.CurrentUICulture.Name.StartsWith(CultureName);
-        }
+            if (CheckIgnorableError(ex))
+                return;
 
-        public static string CultureCode
-        {
-            get
+            if (MyCommon.ExceptionOut(ex))
             {
-                if (MyCommon.cultureStr == null)
-                {
-                    var cfgCommon = SettingCommon.Load();
-                    MyCommon.cultureStr = cfgCommon.Language;
-                    if (MyCommon.cultureStr == "OS")
-                    {
-                        if (!IsEqualCurrentCulture("ja") &&
-                           !IsEqualCurrentCulture("en") &&
-                           !IsEqualCurrentCulture("zh-CN"))
-                        {
-                            MyCommon.cultureStr = "en";
-                        }
-                    }
-                }
-                return MyCommon.cultureStr;
+                Application.Exit();
             }
         }
 
-        public static void InitCulture(string code)
+        /// <summary>
+        /// 無視しても問題のない既知の例外であれば true を返す
+        /// </summary>
+        private static bool CheckIgnorableError(Exception ex)
         {
-            try
+#if DEBUG
+            return false;
+#else
+            if (ex is AggregateException aggregated)
             {
-                Thread.CurrentThread.CurrentUICulture = new CultureInfo(code);
+                if (aggregated.InnerExceptions.Count != 1)
+                    return false;
+
+                ex = aggregated.InnerExceptions.Single();
             }
-            catch (Exception)
+
+            switch (ex)
             {
+                case System.Net.WebException webEx:
+                    // SSL/TLS のネゴシエーションに失敗した場合に発生する。なぜかキャッチできない例外
+                    // https://osdn.net/ticket/browse.php?group_id=6526&tid=37432
+                    if (webEx.Status == System.Net.WebExceptionStatus.SecureChannelFailure)
+                        return true;
+                    break;
+                case System.Threading.Tasks.TaskCanceledException cancelEx:
+                    // ton.twitter.com の画像でタイムアウトした場合、try-catch で例外がキャッチできない
+                    // https://osdn.net/ticket/browse.php?group_id=6526&tid=37433
+                    var stackTrace = new System.Diagnostics.StackTrace(cancelEx);
+                    var lastFrameMethod = stackTrace.GetFrame(stackTrace.FrameCount - 1).GetMethod();
+                    if (lastFrameMethod.ReflectedType == typeof(Connection.TwitterApiConnection) &&
+                        lastFrameMethod.Name == nameof(Connection.TwitterApiConnection.GetStreamAsync))
+                        return true;
+                    break;
             }
+
+            return false;
+#endif
         }
+
         public static void InitCulture()
         {
-            try
-            {
-                if (CultureCode != "OS") Thread.CurrentThread.CurrentUICulture = new CultureInfo(CultureCode);
-            }
-            catch (Exception)
+            var currentCulture = CultureInfo.CurrentUICulture;
+
+            var settingCultureStr = SettingManager.Common.Language;
+            if (settingCultureStr != "OS")
             {
+                try
+                {
+                    currentCulture = new CultureInfo(settingCultureStr);
+                }
+                catch (CultureNotFoundException) { }
             }
+
+            var preferredCulture = GetPreferredCulture(currentCulture);
+            CultureInfo.DefaultThreadCurrentUICulture = preferredCulture;
+            Thread.CurrentThread.CurrentUICulture = preferredCulture;
+        }
+
+        /// <summary>
+        /// サポートしているカルチャの中から、指定されたカルチャに対して適切なカルチャを選択して返します
+        /// </summary>
+        public static CultureInfo GetPreferredCulture(CultureInfo culture)
+        {
+            if (SupportedUICulture.Any(x => x.Contains(culture)))
+                return culture;
+
+            return SupportedUICulture[0];
         }
 
-        private static void CheckSettingFilePath()
+        private static bool SetConfigDirectoryPath()
         {
-            if (File.Exists(Path.Combine(Application.StartupPath, "roaming")))
+            if (StartupOptions.TryGetValue("configDir", out var configDir) && !string.IsNullOrEmpty(configDir))
             {
-                MyCommon.settingPath = MySpecialPath.UserAppDataPath();
+                // 起動オプション /configDir で設定ファイルの参照先を変更できます
+                if (!Directory.Exists(configDir))
+                {
+                    var text = string.Format(Properties.Resources.ConfigDirectoryNotExist, configDir);
+                    MessageBox.Show(text, ApplicationSettings.ApplicationName, MessageBoxButtons.OK, MessageBoxIcon.Error);
+                    return false;
+                }
+
+                MyCommon.settingPath = Path.GetFullPath(configDir);
             }
             else
             {
+                // OpenTween.exe と同じディレクトリに設定ファイルを配置する
                 MyCommon.settingPath = Application.StartupPath;
+
+                SettingManager.LoadAll();
+
+                try
+                {
+                    // 設定ファイルが書き込み可能な状態であるかテストする
+                    SettingManager.SaveAll();
+                }
+                catch (UnauthorizedAccessException)
+                {
+                    // 書き込みに失敗した場合 (Program Files 以下に配置されている場合など)
+
+                    // 通常は C:\Users\ユーザー名\AppData\Roaming\OpenTween\ となる
+                    var roamingDir = Path.Combine(new[]
+                    {
+                        Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
+                        ApplicationSettings.ApplicationName,
+                    });
+                    Directory.CreateDirectory(roamingDir);
+
+                    MyCommon.settingPath = roamingDir;
+
+                    /*
+                     * 書き込みが制限されたディレクトリ内で起動された場合の設定ファイルの扱い
+                     *
+                     *  (A) StartupPath に存在する設定ファイル
+                     *  (B) Roaming に存在する設定ファイル
+                     *
+                     *  1. A も B も存在しない場合
+                     *    => B を新規に作成する
+                     *
+                     *  2. A が存在し、B が存在しない場合
+                     *    => A の内容を B にコピーする (警告を表示)
+                     *
+                     *  3. A が存在せず、B が存在する場合
+                     *    => B を使用する
+                     *
+                     *  4. A も B も存在するが、A の方が更新日時が新しい場合
+                     *    => A の内容を B にコピーする (警告を表示)
+                     *
+                     *  5. A も B も存在するが、B の方が更新日時が新しい場合
+                     *    => B を使用する
+                     */
+                    var startupDirFile = new FileInfo(Path.Combine(Application.StartupPath, "SettingCommon.xml"));
+                    var roamingDirFile = new FileInfo(Path.Combine(roamingDir, "SettingCommon.xml"));
+
+                    if (roamingDirFile.Exists && (!startupDirFile.Exists || startupDirFile.LastWriteTime <= roamingDirFile.LastWriteTime))
+                    {
+                        // 既に Roaming に設定ファイルが存在し、Roaming 内のファイルの方が新しい場合は
+                        // StartupPath に設定ファイルが存在しても無視する
+                        SettingManager.LoadAll();
+                    }
+                    else
+                    {
+                        if (startupDirFile.Exists)
+                        {
+                            // StartupPath に設定ファイルが存在し、Roaming 内のファイルよりも新しい場合のみ警告を表示する
+                            var message = string.Format(Properties.Resources.SettingPath_Relocation, Application.StartupPath, MyCommon.settingPath);
+                            MessageBox.Show(message, ApplicationSettings.ApplicationName, MessageBoxButtons.OK, MessageBoxIcon.Information);
+                        }
+
+                        // Roaming に設定ファイルを作成 (StartupPath に読み込みに成功した設定ファイルがあれば内容がコピーされる)
+                        SettingManager.SaveAll();
+                    }
+                }
             }
+
+            return true;
         }
     }
 }