From cb9e045c7f6c9b6449f035f68b40525180b9f758 Mon Sep 17 00:00:00 2001 From: clueless <14300910+theClueless@users.noreply.github.com> Date: Fri, 21 Feb 2020 23:12:58 +0200 Subject: [PATCH 1/5] catch plugin init fails - stop Wox from not starting up and crushing and disable and explain the user what is the probalmatic plugin --- Wox.Core/Plugin/PluginManager.cs | 35 +++++++++++++++++++----- Wox.Infrastructure/Logger/Log.cs | 47 +++++++++++++++++++++++++++++--- Wox.Plugin/IPublicAPI.cs | 2 +- Wox.Plugin/PluginMetadata.cs | 3 ++ Wox/PublicAPIInstance.cs | 6 ++-- 5 files changed, 78 insertions(+), 15 deletions(-) diff --git a/Wox.Core/Plugin/PluginManager.cs b/Wox.Core/Plugin/PluginManager.cs index fc9afb740..27e2d4851 100644 --- a/Wox.Core/Plugin/PluginManager.cs +++ b/Wox.Core/Plugin/PluginManager.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Concurrent; using System.Collections.Generic; using System.IO; using System.Linq; @@ -92,21 +93,36 @@ namespace Wox.Core.Plugin Settings.UpdatePluginSettings(_metadatas); AllPlugins = PluginsLoader.Plugins(_metadatas, Settings); } + + /// + /// Call initialize for all plugins + /// + /// return the list of failed to init plugins or null for none public static void InitializePlugins(IPublicAPI api) { API = api; + var failedPlugins = new ConcurrentQueue(); Parallel.ForEach(AllPlugins, pair => { - var milliseconds = Stopwatch.Debug($"|PluginManager.InitializePlugins|Init method time cost for <{pair.Metadata.Name}>", () => + try { - pair.Plugin.Init(new PluginInitContext + var milliseconds = Stopwatch.Debug($"|PluginManager.InitializePlugins|Init method time cost for <{pair.Metadata.Name}>", () => { - CurrentPluginMetadata = pair.Metadata, - API = API + pair.Plugin.Init(new PluginInitContext + { + CurrentPluginMetadata = pair.Metadata, + API = API + }); }); - }); - pair.Metadata.InitTime += milliseconds; - Log.Info($"|PluginManager.InitializePlugins|Total init cost for <{pair.Metadata.Name}> is <{pair.Metadata.InitTime}ms>"); + pair.Metadata.InitTime += milliseconds; + Log.Info($"|PluginManager.InitializePlugins|Total init cost for <{pair.Metadata.Name}> is <{pair.Metadata.InitTime}ms>"); + } + catch (Exception e) + { + Log.Exception(nameof(PluginManager), $"Fail to Init plugin: {pair.Metadata.Name}", e); + pair.Metadata.Disabled = true; // TODO: not sure this really disable it later on + failedPlugins.Enqueue(pair); + } }); _contextMenuPlugins = GetPluginsForInterface(); @@ -121,6 +137,11 @@ namespace Wox.Core.Plugin .ForEach(x => NonGlobalPlugins[x] = plugin); } + if (failedPlugins.Any()) + { + var failed = string.Join(",", failedPlugins.Select(x => x.Metadata.Name)); + API.ShowMsg($"Fail to Init Plugins", $"Plugins: {failed} - fail to load and would be disabled, please contact plugin creator for help", "", false); + } } public static void InstallPlugin(string path) diff --git a/Wox.Infrastructure/Logger/Log.cs b/Wox.Infrastructure/Logger/Log.cs index 10e8128c5..349920076 100644 --- a/Wox.Infrastructure/Logger/Log.cs +++ b/Wox.Infrastructure/Logger/Log.cs @@ -11,7 +11,7 @@ namespace Wox.Infrastructure.Logger { public const string DirectoryName = "Logs"; - public static string CurrentLogDirectory { get; private set; } + public static string CurrentLogDirectory { get; } static Log() { @@ -53,6 +53,14 @@ namespace Wox.Infrastructure.Logger [MethodImpl(MethodImplOptions.Synchronized)] public static void Exception(string className, string message, System.Exception exception, [CallerMemberName] string methodName = "") + { + var classNameWithMethod = CheckClassAndMessageAndReturnFullClassWithMethod(className, message, methodName); + + ExceptionInternal(classNameWithMethod, message, exception); + } + + private static string CheckClassAndMessageAndReturnFullClassWithMethod(string className, string message, + string methodName) { if (string.IsNullOrWhiteSpace(className)) { @@ -60,16 +68,17 @@ namespace Wox.Infrastructure.Logger } if (string.IsNullOrWhiteSpace(message)) - { // todo: not sure we really need that + { + // todo: not sure we really need that LogFaultyFormat($"Fail to specify a message during logging"); } if (!string.IsNullOrWhiteSpace(methodName)) { - className += "." + methodName; + return className + "." + methodName; } - ExceptionInternal(className, message, exception); + return className; } private static void ExceptionInternal(string classAndMethod, string message, System.Exception e) @@ -140,18 +149,48 @@ namespace Wox.Infrastructure.Logger LogInternal(message, LogLevel.Error); } + public static void Error(string className, string message, [CallerMemberName] string methodName = "") + { + LogInternal(LogLevel.Error, className, message, methodName); + } + + private static void LogInternal(LogLevel level, string className, string message, [CallerMemberName] string methodName = "") + { + var classNameWithMethod = CheckClassAndMessageAndReturnFullClassWithMethod(className, message, methodName); + + var logger = LogManager.GetLogger(classNameWithMethod); + + System.Diagnostics.Debug.WriteLine($"{level.Name}|{message}"); + logger.Log(level, message); + } + + public static void Debug(string className, string message, [CallerMemberName] string methodName = "") + { + LogInternal(LogLevel.Debug, className, message, methodName); + } + /// example: "|prefix|unprefixed" public static void Debug(string message) { LogInternal(message, LogLevel.Debug); } + public static void Info(string className, string message, [CallerMemberName] string methodName = "") + { + LogInternal(LogLevel.Info, className, message, methodName); + } + /// example: "|prefix|unprefixed" public static void Info(string message) { LogInternal(message, LogLevel.Info); } + public static void Warn(string className, string message, [CallerMemberName] string methodName = "") + { + LogInternal(LogLevel.Warn, className, message, methodName); + } + /// example: "|prefix|unprefixed" public static void Warn(string message) { diff --git a/Wox.Plugin/IPublicAPI.cs b/Wox.Plugin/IPublicAPI.cs index eb3f18fa7..8dc838963 100644 --- a/Wox.Plugin/IPublicAPI.cs +++ b/Wox.Plugin/IPublicAPI.cs @@ -76,7 +76,7 @@ namespace Wox.Plugin /// Message title /// Message subtitle /// Message icon path (relative path to your plugin folder) - void ShowMsg(string title, string subTitle = "", string iconPath = ""); + void ShowMsg(string title, string subTitle = "", string iconPath = "", bool useMainWindowAsOwner = true); /// /// Open setting dialog diff --git a/Wox.Plugin/PluginMetadata.cs b/Wox.Plugin/PluginMetadata.cs index 5b4feb88e..64b8b032c 100644 --- a/Wox.Plugin/PluginMetadata.cs +++ b/Wox.Plugin/PluginMetadata.cs @@ -46,6 +46,9 @@ namespace Wox.Plugin [Obsolete("Use IcoPath")] public string FullIcoPath => IcoPath; + /// + /// Init time include both plugin load time and init time + /// [JsonIgnore] public long InitTime { get; set; } [JsonIgnore] diff --git a/Wox/PublicAPIInstance.cs b/Wox/PublicAPIInstance.cs index ab198d6bf..314fc13b6 100644 --- a/Wox/PublicAPIInstance.cs +++ b/Wox/PublicAPIInstance.cs @@ -91,12 +91,12 @@ namespace Wox _mainVM.MainWindowVisibility = Visibility.Visible; } - public void ShowMsg(string title, string subTitle = "", string iconPath = "") + public void ShowMsg(string title, string subTitle = "", string iconPath = "", bool useMainWindowAsOwner = true) { Application.Current.Dispatcher.Invoke(() => { - var m = new Msg { Owner = Application.Current.MainWindow }; - m.Show(title, subTitle, iconPath); + var msg = useMainWindowAsOwner ? new Msg {Owner = Application.Current.MainWindow} : new Msg(); + msg.Show(title, subTitle, iconPath); }); } From e1e7387c73e928c4c05b6d73ce09a52f7cbf003e Mon Sep 17 00:00:00 2001 From: clueless <14300910+theClueless@users.noreply.github.com> Date: Sat, 22 Feb 2020 11:02:07 +0200 Subject: [PATCH 2/5] create query builder and re-enable tests. make the plugin manager plugin data the source for disabled plugins --- Plugins/Wox.Plugin.PluginIndicator/Main.cs | 3 +- Wox.Core/Plugin/PluginManager.cs | 31 +---------- Wox.Core/Plugin/QueryBuilder.cs | 50 ++++++++++++++++++ Wox.Core/Wox.Core.csproj | 1 + .../UserSettings/PluginSettings.cs | 8 ++- Wox.Test/QueryBuilderTest.cs | 51 +++++++++++++++++++ Wox.Test/QueryTest.cs | 33 ------------ Wox.Test/Wox.Test.csproj | 2 +- Wox/SettingWindow.xaml.cs | 3 +- Wox/ViewModel/MainViewModel.cs | 5 +- Wox/ViewModel/SettingWindowViewModel.cs | 25 ++------- 11 files changed, 120 insertions(+), 92 deletions(-) create mode 100644 Wox.Core/Plugin/QueryBuilder.cs create mode 100644 Wox.Test/QueryBuilderTest.cs delete mode 100644 Wox.Test/QueryTest.cs diff --git a/Plugins/Wox.Plugin.PluginIndicator/Main.cs b/Plugins/Wox.Plugin.PluginIndicator/Main.cs index 5f9ec07be..54dcf007a 100644 --- a/Plugins/Wox.Plugin.PluginIndicator/Main.cs +++ b/Plugins/Wox.Plugin.PluginIndicator/Main.cs @@ -13,8 +13,7 @@ namespace Wox.Plugin.PluginIndicator var results = from keyword in PluginManager.NonGlobalPlugins.Keys where keyword.StartsWith(query.Terms[0]) let metadata = PluginManager.NonGlobalPlugins[keyword].Metadata - let disabled = PluginManager.Settings.Plugins[metadata.ID].Disabled - where !disabled + where !metadata.Disabled select new Result { Title = keyword, diff --git a/Wox.Core/Plugin/PluginManager.cs b/Wox.Core/Plugin/PluginManager.cs index 27e2d4851..0166690b7 100644 --- a/Wox.Core/Plugin/PluginManager.cs +++ b/Wox.Core/Plugin/PluginManager.cs @@ -120,7 +120,7 @@ namespace Wox.Core.Plugin catch (Exception e) { Log.Exception(nameof(PluginManager), $"Fail to Init plugin: {pair.Metadata.Name}", e); - pair.Metadata.Disabled = true; // TODO: not sure this really disable it later on + pair.Metadata.Disabled = true; failedPlugins.Enqueue(pair); } }); @@ -149,35 +149,6 @@ namespace Wox.Core.Plugin PluginInstaller.Install(path); } - public static Query QueryInit(string text) //todo is that possible to move it into type Query? - { - // replace multiple white spaces with one white space - var terms = text.Split(new[] { Query.TermSeperater }, StringSplitOptions.RemoveEmptyEntries); - var rawQuery = string.Join(Query.TermSeperater, terms); - var actionKeyword = string.Empty; - var search = rawQuery; - var actionParameters = terms.ToList(); - if (terms.Length == 0) return null; - if (NonGlobalPlugins.ContainsKey(terms[0]) && - !Settings.Plugins[NonGlobalPlugins[terms[0]].Metadata.ID].Disabled) - { - actionKeyword = terms[0]; - actionParameters = terms.Skip(1).ToList(); - search = string.Join(Query.TermSeperater, actionParameters.ToArray()); - } - var query = new Query - { - Terms = terms, - RawQuery = rawQuery, - ActionKeyword = actionKeyword, - Search = search, - // Obsolete value initialisation - ActionName = actionKeyword, - ActionParameters = actionParameters - }; - return query; - } - public static List ValidPluginsForQuery(Query query) { if (NonGlobalPlugins.ContainsKey(query.ActionKeyword)) diff --git a/Wox.Core/Plugin/QueryBuilder.cs b/Wox.Core/Plugin/QueryBuilder.cs new file mode 100644 index 000000000..b8ac41988 --- /dev/null +++ b/Wox.Core/Plugin/QueryBuilder.cs @@ -0,0 +1,50 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Wox.Plugin; + +namespace Wox.Core.Plugin +{ + public static class QueryBuilder + { + public static Query Build(string text, Dictionary nonGlobalPlugins) + { + // replace multiple white spaces with one white space + var terms = text.Split(new[] { Query.TermSeperater }, StringSplitOptions.RemoveEmptyEntries); + if (terms.Length == 0) + { // nothing was typed + return null; + } + + var rawQuery = string.Join(Query.TermSeperater, terms); + string actionKeyword, search; + string possibleActionKeyword = terms[0]; + List actionParameters; + if (nonGlobalPlugins.TryGetValue(possibleActionKeyword, out var pluginPair) && !pluginPair.Metadata.Disabled) + { // use non global plugin for query + actionKeyword = possibleActionKeyword; + actionParameters = terms.Skip(1).ToList(); + search = rawQuery.Substring(actionKeyword.Length + 1); + } + else + { // non action keyword + actionKeyword = string.Empty; + actionParameters = terms.ToList(); + search = rawQuery; + } + + var query = new Query + { + Terms = terms, + RawQuery = rawQuery, + ActionKeyword = actionKeyword, + Search = search, + // Obsolete value initialisation + ActionName = actionKeyword, + ActionParameters = actionParameters + }; + + return query; + } + } +} \ No newline at end of file diff --git a/Wox.Core/Wox.Core.csproj b/Wox.Core/Wox.Core.csproj index c1758fdb3..bb5fbdc3c 100644 --- a/Wox.Core/Wox.Core.csproj +++ b/Wox.Core/Wox.Core.csproj @@ -53,6 +53,7 @@ + diff --git a/Wox.Infrastructure/UserSettings/PluginSettings.cs b/Wox.Infrastructure/UserSettings/PluginSettings.cs index c47fe7c15..346d98e53 100644 --- a/Wox.Infrastructure/UserSettings/PluginSettings.cs +++ b/Wox.Infrastructure/UserSettings/PluginSettings.cs @@ -28,7 +28,7 @@ namespace Wox.Infrastructure.UserSettings { ID = metadata.ID, Name = metadata.Name, - ActionKeywords = metadata.ActionKeywords, + ActionKeywords = metadata.ActionKeywords, Disabled = metadata.Disabled }; } @@ -39,7 +39,11 @@ namespace Wox.Infrastructure.UserSettings { public string ID { get; set; } public string Name { get; set; } - public List ActionKeywords { get; set; } + public List ActionKeywords { get; set; } // a reference of the action keywords from plugin manager + + /// + /// Used only to save the state of the plugin in settings + /// public bool Disabled { get; set; } } } diff --git a/Wox.Test/QueryBuilderTest.cs b/Wox.Test/QueryBuilderTest.cs new file mode 100644 index 000000000..0402d2ae8 --- /dev/null +++ b/Wox.Test/QueryBuilderTest.cs @@ -0,0 +1,51 @@ +using System.Collections.Generic; +using NUnit.Framework; +using Wox.Core.Plugin; +using Wox.Plugin; + +namespace Wox.Test +{ + public class QueryBuilderTest + { + [Test] + public void ExclusivePluginQueryTest() + { + var nonGlobalPlugins = new Dictionary + { + {">", new PluginPair {Metadata = new PluginMetadata {ActionKeywords = new List {">"}}}} + }; + + Query q = QueryBuilder.Build("> file.txt file2 file3", nonGlobalPlugins); + + Assert.AreEqual("file.txt file2 file3", q.Search); + Assert.AreEqual(">", q.ActionKeyword); + } + + [Test] + public void ExclusivePluginQueryIgnoreDisabledTest() + { + var nonGlobalPlugins = new Dictionary + { + {">", new PluginPair {Metadata = new PluginMetadata {ActionKeywords = new List {">"}, Disabled = true}}} + }; + + Query q = QueryBuilder.Build("> file.txt file2 file3", nonGlobalPlugins); + + Assert.AreEqual("> file.txt file2 file3", q.Search); + } + + [Test] + public void GenericPluginQueryTest() + { + Query q = QueryBuilder.Build("file.txt file2 file3", new Dictionary()); + + Assert.AreEqual("file.txt file2 file3", q.Search); + Assert.AreEqual("", q.ActionKeyword); + + Assert.AreEqual("file.txt", q.FirstSearch); + Assert.AreEqual("file2", q.SecondSearch); + Assert.AreEqual("file3", q.ThirdSearch); + Assert.AreEqual("file2 file3", q.SecondToEndSearch); + } + } +} diff --git a/Wox.Test/QueryTest.cs b/Wox.Test/QueryTest.cs deleted file mode 100644 index b7a0917ef..000000000 --- a/Wox.Test/QueryTest.cs +++ /dev/null @@ -1,33 +0,0 @@ -using NUnit.Framework; -using Wox.Core.Plugin; -using Wox.Plugin; - -namespace Wox.Test -{ - public class QueryTest - { - [Test] - [Ignore("Current query is tightly integrated with GUI, can't be tested.")] - public void ExclusivePluginQueryTest() - { - Query q = PluginManager.QueryInit("> file.txt file2 file3"); - - Assert.AreEqual(q.FirstSearch, "file.txt"); - Assert.AreEqual(q.SecondSearch, "file2"); - Assert.AreEqual(q.ThirdSearch, "file3"); - Assert.AreEqual(q.SecondToEndSearch, "file2 file3"); - } - - [Test] - [Ignore("Current query is tightly integrated with GUI, can't be tested.")] - public void GenericPluginQueryTest() - { - Query q = PluginManager.QueryInit("file.txt file2 file3"); - - Assert.AreEqual(q.FirstSearch, "file.txt"); - Assert.AreEqual(q.SecondSearch, "file2"); - Assert.AreEqual(q.ThirdSearch, "file3"); - Assert.AreEqual(q.SecondToEndSearch, "file2 file3"); - } - } -} diff --git a/Wox.Test/Wox.Test.csproj b/Wox.Test/Wox.Test.csproj index 7a3ef1f0c..2b1e6a78a 100644 --- a/Wox.Test/Wox.Test.csproj +++ b/Wox.Test/Wox.Test.csproj @@ -45,7 +45,7 @@ - + diff --git a/Wox/SettingWindow.xaml.cs b/Wox/SettingWindow.xaml.cs index 67d67351e..52addf362 100644 --- a/Wox/SettingWindow.xaml.cs +++ b/Wox/SettingWindow.xaml.cs @@ -215,7 +215,8 @@ namespace Wox private void OnPluginToggled(object sender, RoutedEventArgs e) { var id = _viewModel.SelectedPlugin.PluginPair.Metadata.ID; - _settings.PluginSettings.Plugins[id].Disabled = _viewModel.SelectedPlugin.PluginPair.Metadata.Disabled; + // used to sync the current status from the plugin manager into the setting to keep consistency after save + _settings.PluginSettings.Plugins[id].Disabled = _viewModel.SelectedPlugin.PluginPair.Metadata.Disabled; } private void OnPluginActionKeywordsClick(object sender, MouseButtonEventArgs e) diff --git a/Wox/ViewModel/MainViewModel.cs b/Wox/ViewModel/MainViewModel.cs index bd1314a27..acdd7c040 100644 --- a/Wox/ViewModel/MainViewModel.cs +++ b/Wox/ViewModel/MainViewModel.cs @@ -377,7 +377,7 @@ namespace Wox.ViewModel ProgressBarVisibility = Visibility.Hidden; _isQueryRunning = true; - var query = PluginManager.QueryInit(QueryText.Trim()); + var query = QueryBuilder.Build(QueryText.Trim(), PluginManager.NonGlobalPlugins); if (query != null) { // handle the exclusiveness of plugin using action keyword @@ -401,8 +401,7 @@ namespace Wox.ViewModel { Parallel.ForEach(plugins, parallelOptions, plugin => { - var config = _settings.PluginSettings.Plugins[plugin.Metadata.ID]; - if (!config.Disabled) + if (!plugin.Metadata.Disabled) { var results = PluginManager.QueryForPlugin(plugin, query); UpdateResultView(results, plugin.Metadata, query); diff --git a/Wox/ViewModel/SettingWindowViewModel.cs b/Wox/ViewModel/SettingWindowViewModel.cs index 923e456b0..ce2c10a20 100644 --- a/Wox/ViewModel/SettingWindowViewModel.cs +++ b/Wox/ViewModel/SettingWindowViewModel.cs @@ -150,26 +150,11 @@ namespace Wox.ViewModel { get { - var plugins = PluginManager.AllPlugins; - var settings = Settings.PluginSettings.Plugins; - plugins.Sort((a, b) => - { - var d1 = settings[a.Metadata.ID].Disabled; - var d2 = settings[b.Metadata.ID].Disabled; - if (d1 == d2) - { - return string.Compare(a.Metadata.Name, b.Metadata.Name, StringComparison.CurrentCulture); - } - else - { - return d1.CompareTo(d2); - } - }); - - var metadatas = plugins.Select(p => new PluginViewModel - { - PluginPair = p, - }).ToList(); + var metadatas = PluginManager.AllPlugins + .OrderBy(x => x.Metadata.Disabled) + .ThenBy(y => y.Metadata.Name) + .Select(p => new PluginViewModel { PluginPair = p}) + .ToList(); return metadatas; } } From cecb65cd406bc32b8d50296812eed3255062c515 Mon Sep 17 00:00:00 2001 From: Jeremy Wu Date: Tue, 25 Feb 2020 21:08:51 +1100 Subject: [PATCH 3/5] Installed & portable mode: Enhance auto updates (#142) * Add handling of portable mode when updating Additionally: 1. Changed getting version from internally rather than from RELEASES file 2. Added PortableDataPath as a constant so can be used to determine if portable mode is used * Fix incorrectly wired auto update event handler * Fix Sys plugin missing Shutdown command icon * Add check update command to Sys plugin * Add message if current Wox version is latest * Add default silent when auto checking updates in background Silent when current is still the latest version * Move UserData folder to new version location * Changes per review 1. Move IsPortableMode to Constant 2. Merge if statement * Per comment- change variables to be more descriptive UpdateInfo and UpdateManager renamed * Per comment- Add exception handling and message if failed. --- Plugins/Wox.Plugin.Sys/Images/checkupdate.png | Bin 0 -> 23216 bytes Plugins/Wox.Plugin.Sys/Main.cs | 14 +++ Plugins/Wox.Plugin.Sys/Wox.Plugin.Sys.csproj | 6 ++ Wox.Core/Updater.cs | 81 ++++++++------ Wox.Infrastructure/Wox.cs | 22 ++-- Wox.Plugin/IPublicAPI.cs | 5 + Wox.Plugin/SharedCommands/FilesFolders.cs | 101 ++++++++++++++++++ Wox.Plugin/Wox.Plugin.csproj | 1 + Wox/PublicAPIInstance.cs | 6 ++ Wox/SettingWindow.xaml | 6 +- Wox/ViewModel/SettingWindowViewModel.cs | 2 +- 11 files changed, 201 insertions(+), 43 deletions(-) create mode 100644 Plugins/Wox.Plugin.Sys/Images/checkupdate.png create mode 100644 Wox.Plugin/SharedCommands/FilesFolders.cs diff --git a/Plugins/Wox.Plugin.Sys/Images/checkupdate.png b/Plugins/Wox.Plugin.Sys/Images/checkupdate.png new file mode 100644 index 0000000000000000000000000000000000000000..955f6fdbb7be9c75a03961cf065679a5b698f978 GIT binary patch literal 23216 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%0Dvx=x8MNt zwoEU|_B-*;yKD!Q`d8p5%r9?-M0m_X1L)C9!ST>%^#xG z2IkMaIDm*+U`UXz(=_fKiZnp{QSS!nnL{?3J zZj+t$it5TTgTcGEMkba{nR^`DCVDWTe;cOKfzw_bd?=zMxkxmAH`uU=&lS!oA+E z$LhQ*nj3jbAOZacxi}4Lpoa6L^4#tmp}4>$MG@=_t}+|}A1D-xpWMcdLU<&ioELZI(uzg@*uuz-Cr&)Z~jAXT`zm3^m7V&Z5Rsj5AMq%En;fC zFu(5~KWFwE)6=#jtWa~^Py)fn>z1iwnHvp|g(iR$px&m{@@iSJkTm%Mn~J*39q|iQ zY?6iOus=^_ha{)Ifb}0-zdzB$&S8<2M_XE|z9L|m9+<4^!bt9_bw8n7-TUZ5vy9|> zq^)0j1`%%M0;T4DmsAZ+>~((@>b_)eC$9SV(5~OC|BjWBgU?#GNJwDQd~)V4M7LJT z`O!T!#do0!6k(=F|5{qVf4oUw{xIl%b=V4(kpvRl9hqCC3JUrttuM!P*B^3QwX-+`*=nL9=ISw+w%aV4o0X zMUgRzm?E6L`I~2D?qE&J1h&VHX}{Qz5V(y&{m6+*e>ZC(zE>B{7d58oDFF)4)E?wl z@4BSjh!|J{{by(o$$tpu6g6)A#^^TXgU6$XJ27ptZO|fhhWd5i>uPbjsE(1fOszMm z`M&_l&==a;RvUd=nc2M@Uar#CEp<>i)jynQca?B#brH-|w~a^@Hye1HJ~!x`I@f=h zdVs1)dgq^D!E`0>->f(K`BE5pcFg1y9Wn%GLr&u^d!y|U7wsL@M~N|Wz@(4YLY%#X z5qWe}>*yCrEN0FpERM6Bf8+AI!>^3Upz&|!$IEVp$1$E|uMxKO?^W&UemCw!X4h`5 zPpk0z5?_~7GP}Ipp?sbNCw=0}KpvWMoK4R-+>3HyX||sc1v5q_)>{|#*txP8CFjXF z(}Z+V(qzVVoz0XGsu&n?_#o*<-#p#E)|mLvTS(;MX1tRvI9-vXaL<{KKXdPP~=$Wyeu|5)HmPdV*( zlCoWfVXIhh_D<(Gt#jLO#tRbVTRs4lZN6ul5mc6Uj;V7&v5?It_T#A8k88`rUJMM( zA9pw%P#Ox1*c3`6J=q2 zKo6;HVObw2ycvG$SymV!sa2#TL8j6YF$CWeO&d#>dTm#-QCoP zD_zaiq-AvKkFVmxsuv|?OaCYZLTC?qn3d#Mhu}m5MV(2dUWdAmzO}<$u1-fAy7uP; zRf4BUyXwhh7I{C!Mu?EP7#&p6YHXUu5a>xmMH^SW>+5__b?)t;vb7}5ffprU>SZa& zzVd3OA(|VZ9OcfHzp8vKh9_~C!*W#*RqdC52s|M82hw z=tI_W&~1)ssCq{v$h6THx?w@un&gZ9wU~i}q>Y735B!)CaQ~8g6ufpt7!w&< z2eD}GxE-rynNflIA4ozqz1o}P4Q)6ZUG!Ih3Z`iTMUrJ*!=E#dccnNssk|KP_$rSvNVE1hwues8=mZut`!(>ig}WJ~O1G%(VP z`{B@>B|Wj%q=Sb(Du6@aMeuz;r?ShDf#oQ%<8*v2j5^L~pyQrM$uDwc25vns?35KC zA=Nx{c!F~Kjsuag7CivV9jh;BWU77u+m$M;&ThiBSghLgD6LO>vD2;GH<|x#yg=T6 z?!8LRx3L-HdfXZAuy`*suB$}mOt0jP3A64(EZt{fQX@g9U+sv&|!&CGj$n>h9+1=D|1T@99Y z8w!U#^5^bZz5_%HK>Dx4rIck{w3={;_XMiP4CCJC<+%(sX_*apRmwM zwHjS86Ul<+)Cor=yBDl_92gcDQf8>vZX@9N2H+0J=pae8ju9rcOdH&g*+`~o7>lXt zRX9&f4Z_rV22Oyvy>XA?N*+qS|M%<$1-2>?26}!!Z@VeIVTGg^6+y2(92`j7U1G|E zXS&gepnq2-Y!RYB#q1n!s{v*zrSY*B+Ze@DQ|yAt-j;$T*NcPmmUq$qW`J@3H5aIe zqMqw+ue*J3BxG+3V=TP=g~+ocwkC(l>Cb2+|L(=3ki)2;ey?B8bak{f)*^FPt0)P% z$1%IROi0P_E`Bbgb_2kQN!?o8Vz=+D9tr}WMX)~<#=#KO^ zCL}FR#_;6r^2{@dI(x?5P=G0!KOCo*a1(3xuEI%rb|qdZsD>m(!BF9fUKN39MN=xN z3v^Cy6A7r-pQ#;Ys$}n|Cpb4=3A_08knb4ns&5okOK`Z!p=&-vaRmD>p`BS(E_kp$ zZTZ1%4u)<|*9$r%)I@imEa8bmO-X@kf7N!J9_KEj#?8?VZY5&<{j-G5Ei>nvD0&= zS+L#CXWr^3V8|zDv5m)h1wA-_JmlW>vU2n+O*FvbV)|iF!D>0teOqB4wLEkytsJ+X z=gsUNQ2s6a8K*o7)UWnKARnbM+f{!^CZtj0N%QvQr5kPUEw^HgH44x@2mL);*v@^B zjheAax@s@UwN&=K$xP`6=GyK_vLKfxM^j7Vy}w404|*uhOr9J+4qQiPC3{vz%Rfec z-ku85|L`(a{+q4kUyOiYaPU}Qq<6I)!sbu{!9=AJ72;@qEo^A62|L!w$=m+x+WAe! zN*6*>myx$;nA|3TijW6+wm1-mHpo|?6@U8zn2(L?LtPTuqI6m;OS7N!;Z7RC6K9US zN~^YeQ&D4ZU;M8d5TU@t!{e_I)c`w4!S90+*_xNeuyNqSl^-KQXG-Zt`Y{gwWXl_Y zQ;YgJXj+-suCCB=Ypd?|Iu^&^gD-2n%_$`CWkG*VR_}@J3sn4Av1FArS+H7s{v;a{ zJIR(xx!~r3Gsj=gDmJec(*mUj5zSpjaxF_8iN-49li!wt$>>%bFWAuk$~LOiB*eqQ zU*}gp@m0bUlJUJ1_qw!pBrgbPv;RTw7OO_1AUN=(d*7xWZPPdP;R~0B84mjLkLu>P z@rw>p%&b;d%u*AVoFz!A*P+-$Ei5jT-yX#KmY9z&;v+YAR~+b<-d+(D=?CTGf(g0| zjW>I30+@#JT4!cr1S6iz6GaOAb$0=xa=Ij;Yb2#ua48Ub&>j&B*I3dqquWBq28=V za@W5Rmw0cK#v7QbDaSF5W#Rv)q_O3Yc!rGvDa6W;D}-_UBRX~u_az+i4cQ6)bBXY2 zAxyiGYig_N85MU5?ZQC@@8!Qkw8QzH zPeX=~@9yfNXb;*gZu`RV{uix z%KKw4D-ekrP~bC_a53LYT%Z}K|EM^#Br|Gmn#L8jOVtb)2Z>2K8 z`|L7MTuD8@)mH=VmVOhYEYpp{!jOHTfxCeX<5EX;VM?k3C<`=CBSK(!I!XJzzAaYe zQ$~i!+2?Y~_4<$d8Lld)A&_+Iqal6V0zyes7v@;Xgi8(@DLItyq2{IOkviPjn^fYO z9S@WVCa9uI5Z$7jH{)T0Y_DGut24F{Erx3&i#Yluad_@n7rzpdlhI3Zn>_ zrB13Fj}cRvnFfEi8GPx8t5gA0gGzm>L*fd<8s<@rS6Yjeo$@vbcB$7?3%Z|&%|irLIQKoe12kE~L-;!kR;93SAg+@{Eju9H~=0Vc%w*UUWc6cuB zYQ6ov@U6h*O6KRLqs}y1hENgJSYR&b9r1&wDZuEJp=DRo``)XCO}yf@th&LEd~&n7 z7b178P*J{sso%Xr0g+7rMV=yK{)?c1*y;0|1;4)Q&k1d`IiT0pC{LbZf%*&R#$t%~ zq`Gewg(XdEJGFcQ@VXkSfKn!)&6h8zAkJNDZ);&Z$-A|BClKP^yncLI0E)XwVa0-^xP|N_|mPQUL zXaL=q0BQChL7K73=IL6F{Sj&F-izqA^@NLO4lqy(HsBYeYcawZq36J-zV#I)9g^M7 z!m75GPIuJ!fd%6X!|V9y5XIjl_~`B?kD((iE*Z6ls77YXYo2UXZ2gvhop(D2Y31{1vjHRzDdC@N)rcKxHRF#w{YFnktl z$jei2Y$=5-sM@ZWRDI{jsN1Ivl1EIQAonhgDi?7_>rd-ykT-u63<_fOvTS+}u9-qC zV_D3gOCm-OhvQ&78#?J%cj$;gi1zhZKZCyqC=SI;vQfiKq~oh-fUn3$Tu*RDIB*QT;^lTTV%-@PnL5Jvf{yDLzgUdq?}e;pyFf z{#$y=onV6to#_a37!Wg^xvejAYvvuH|NHZ8iee_aS6MSOONp2PeU^(AYaL67mz5%Y zm%c#^fQEks`Th~T^3GG(1Cff~{pgu5;(>d^n%jrnG_(B^pnd|x#x429v_+iy)AL?j z5ce!4YFTOlRmHzJY7WNBHLMF{btHz^J~_2~QE3SL+(5e&7eH5kvDC^@!5+f*ba;1Q z0C_n~kX=P}nC2ks3p=EG3OvyN#_{-oMraCaQ+P*!lswPWvSZkgAfww8nxCNmi4fFz zRxwoX*ujrj@<`Bm;Dn3|A&1B}{~N$3-$6Rzwt{1?gbSxphou`JMEu5%W z)!zS;8I3ERdFyjt|78r-Fcu9TOKclFL60?@eovvAW7O>1A}i`YBPvjGD-h{wFAA*r z4HRy%xV#vlByfB<DZ8UKNgVAKIvAM;GIwLRZdQn&(|4Bv|t+n31Ehw3qV?Ii$&-e9;zNaGSXR zh8%aE{3JgTg|BQ!mn$mXS9h7!WH514MA_~liY>{?_#Y7N%1`^_KbH?$*Ph49tzZmA zzaXv%FUA{J>8mkNxIi*8GU{LQhA^%zN!>+uP@qX~pw9XZj;oVB`byV6|6!sbLt4JS zilZ6fTLfj5U*M-31N`xphTRXSrT&em7l=tzU94zG{cHnCN z_aKfC75OSr??k5U!mAvhjz2;Nl?LxxWH6@j*8BrS+#~s~O4E5Ve4=g@g3o()2wNX1 z$mRM+hc@LS%7#-Wc^mSOKz;r0Gmu`5KQ+e$zWC-v#Beh!FG{G;*J%TJmI#!TgLCRW zHon0?GCRWqfc%2!X@`SRl7EX31^pamgc582=vX%ly|lC8WsLHE0Kgj94QbflOs2y3 zh5DOXE-2ibf?5KvfOnE^pL02Wg0=$E>C`E&krsQB^Huq!hth&nZ_VK*(QBB#f~n>8 zsdfY~?eo7QO?hO|mW1{?AJ^|#?3aufcp4#=4gF&53`fln51=5xZtou1=c6?EaK}e@ z<3GP)S!+Hm-1>3ll?%tQqm`}`+u*Sf3;jH2OUHx^2Y_NgQax5;zW-yw z`Hy8lAY*M>DBeos2u*knlLBnSr&!!2Wp1eYm#A+1D#xvvQ9$Zf79E8e3pM@CE%jEP zN{lW!5wp&#yd~y8j0Lp*S_PPy#Mf#bR#yPNpDQILw@qFxt_}4jHmd86pX6$CBnJNL~xk4Dl({jni1OQ$0DCghP9{^RRk z=W-T(j)AwodZ7ON&aESWBULe=t&c8K<)Ldbj>zH5b2;;^%IR3A*>FYYBpgtA3B}q2 zB=-W53K|s%U?lI&!<&t@TZfLBo;A}hVws6QQcL;GUc7wEih%#!OZyfxBDrq#<+mtb zEo?sMQ>ZaxgNSmjube$n&`3vZw{!b3bph{MdTueYi|$ zOmE_<#1G%-#mTv5yz>t%DH5)271;o)tpZTN zoibLqMHbtrF|mRrbsOo=rg_QyKi2_)#E;yqfvFP{f*dq3=+?FsKHwVE>+xuTcU@pn z!=2sN#>EuqK@&bsvj7m_sE%Pk(b8)urXO||LNl}O>}AM4?H6sD>DW+l;4=`z$ z#DMHxp%VuV<-Frq;br5z{w6f}*`RPXR1Lhp7G^>ZduE{WEeZ41lM67y(SuhnIXXjv z1;g~H*+HB~Tq#aKkld@feDVIon1nf5+F0?&E)Yl!{Y9W$7oQIacF0`<)ah=e#YcgW z4B^6f8}E+p)=j3rPZ6BtKxhrLyXQPRVk^ENt*x`mAc#X9 z>|i5bRVq;7vdt8(vqbBiv@ZWN_BxXrg{__R(bg%kXcbVM=rIrureNlva1P||l<%hc?#SZLQi=}n zs{pB03=O7QNU<8V5*2ouvry}!yNVg+zLCgEr>2`s{-1%Ki^jMzROc$+n*ned_}^9p zUZbTF;8$lDGJATHfIF^i=0MOb8l!N{;eq9w`~+2-hxmvh0GD;-pu#(9e>w~P6TmWK z85g_bp{kht1F5sDAC)HXRn(PIE_rUb#L9}g4gkr&RLj+;MT`I7ou>rwjw-*BF(-lw z;`jF*PVb-$jA~HIx8n{&WtIBoW(z`sR}9Qxnzt#z=@swJQv;A{})_Hi8?2xO#Rr`l8(c-OLjTrg{xTt>)n!CsP@~#Z1|E-gG;P(mqwI8}CBaim4tc}u$k3j3P z+fbhU8iz)M%i~ui0_;Gb^FrSo+D6WTGCmLV+)qZA1m${kLo=)m+*>voLk42g)@`LA zKyNhYd13F2I7he#_i57IECQb*Ei@@h02?i{a@K@=8MWjv#M=xIAwpJ?ueX|@Tt3q zVP^)&)WSH()hgZ7*_SP@PgP^h>R2^{qDEhG5GD}eF(w81&GsRD<}8#G^k1Vf1*V?` zP2+3M-N_fF@gFC&HqsIGCd#T=Pw&vDt>ZWvasfIJXWq+93n_$=#x4_AN;b}=(#@T=F+D`!?XEJc%Y9$JrJtENB=8O5bM$5 zh3--XYETNq4l_v#B_Zx+GJ56e={I18$)yUeX{z9%&Ukqese^+ta#QK04hZH@#8=(| zwrbn5x3K-`3FOmMsKW3OhgJfKM-4}t@078vCo(ND7XdO`1Oqbo-qMS;wi=#Ga-M@RaBMtev^E4> z=cYypXh(l|mf6&!^W5i_G(4!wNhuP8c!+4J6D}J! zFhQwIE_7?JJIWM_1FLwzHal9U)=)ogws=c z_P11aI|dh^Kt?(KmUq!~Ms)C;o(#g`5<4ap7ekwlPMaXd8V<6aPie$SWdvu^QbeJR zF}*+riPjW|YyjJpz7o&q5z^hNTD&l2kTFA^O<^vb^IQdTB9q^9qI3=t-{`oM;a;7>T(54!p>Twc^@l0G4-{6$1vAP?{VL^;0}>x{STT!cyR2~?7*_>~x`-oE%LLjo z5A&A0FW-k-L{>#X0QxU%$Xk|3*XCO|!$%pD#yOj?U^iT#9*RSR_LC@6Tg%4pNJjX= zyFgn`l5y~hr~l-Rc3y>hcrlc1I=x@kgV(gy!PjG8&%#&4>a`8_8+wJbco(9Uv@3Khlv3$QUVPxzjy_p-Efu3 z%fGCDY4XP_?(`7*th$207$l@|TIps=J9A?U{F6qV>ay>bEYqSDVCgS6GR9B9VA!P= zVV3}-7=ERr#2L}x&nu?sK`|=2!Rp_)95I=|jcpTVNTdcpHV$*6w8;78W?X~o=_nq3EbjBoT+_BE3s`);fGeLhN?Tl>AnS~S%NBw9XI-8e^+GkUf-pG zbCtp-)}9OM%-`F8kd*R;%&{vhcEAqL_wA6@n7S$|qruGr+ETB*Vn|K)sbN+BjWM$b zXfrklX5{>5ywLOKR3XA|q`JV)4=|-^Hb#dOvzG{@wwDg)paW{$fyX2Ebh^YY!#Cf@Jay)gw!zc;6~x?_ujwXgeflgZF%^Y9_*Ys z{Tf*k_iNE)R3M{w&vFMS`WLHOla3m;m5S?`jT{OfLU&X@#m97Irt&TRie|ZxH;H2&yk>$GfK3~Y#6A8~ zYMKx}_&&T;roMxoUuM=NYN1_UStXWAa~w&hNl1Ua(5Vq~UZf&3m0!Yy?yYnjuxoA; zwz@uE_I+RAP-bd`+;J*cT*-fPtu<|!{p-#Myhn`G`*h+U+oB@qdeh@N&BOTZeh8LE zw$V_-1Lo$`3jpSqJ?ZK&(G;;F9oRh!K3PFNStm4|(}WZI>vjC~U+@xA+5Wln#JjQT z%A+{9l!+yB8~Hk*{ynP6cNnz`cbU)LB3War&o{=5PnhrseBRg_+3ND0X|RRdmg`Wd zu$+Hp{7kM`4qKd7p63yNfe~Qwm!hR5w5m6UTc89HhP8Kl%W1{(A&K1`wm=OSYiAtT z(!-tO6y1M%)xmiJr|SR18D7#b6%FBHo+0aDzBkU@4rzaSq2e~vdm$NlM}J&b%rfBp zwUW;a&3kfVJ%~U42>Dh$Iv$(9i^+K5^YJ`5=TmUc( z8I3EaUOp*Stw*49u129XeBo07=I~^T zP|kKA@ihm^qLTitboU!weB$)$dKp&VCIpTVhh>R^*k|WB$7$2_R4-ELWkjDDCQhl} zmjDb{0WofNyTpgNV@KX`IOj=k&Ddb7_>LGzQ} zdg=EkxR`HceK2-oCcWdBd{}CCoE#B-F-!CriD3bqk{;T%l64w41LSRYVlncnmts<6 z+xVBrK2$>{RBN|UM9CJ-0%nohfBKt^Wm`!M`w%LC3!YwcpUz#xA}Ozni~#G6w?(& ze%3jyZIVA`@kZJRNF2iQ-P~)75k*DFu9Z})IzG#mHxJKgFY$F>;o+%tSwX z^^A$OQ;Fj-Pf|<6s-f)(szwT%(!}Cj_T^^8VFa5ZAknC9-I5bMwNcQ^(C_JKz-;5Xhg4nQ%J4 zS+d2*EUt8jgv*FJKI$tWQu8o~X(-nJv<&JN!|_ty7ObxJY0)up)YHD&b4z2Xx>E1d xxik6###G;n$dI;pF7*{fR-C0h+L?K$X9vEpB$W;$Fu(?&yV?d?Wg50G{|5`u872S# literal 0 HcmV?d00001 diff --git a/Plugins/Wox.Plugin.Sys/Main.cs b/Plugins/Wox.Plugin.Sys/Main.cs index e888dbb37..f6be3a435 100644 --- a/Plugins/Wox.Plugin.Sys/Main.cs +++ b/Plugins/Wox.Plugin.Sys/Main.cs @@ -234,6 +234,20 @@ namespace Wox.Plugin.Sys context.API.GetTranslation("wox_plugin_sys_dlgtext_all_applicableplugins_reloaded")); return true; } + }, + new Result + { + Title = "Check For Update", + SubTitle = "Check for new Wox update", + IcoPath = "Images\\checkupdate.png", + Action = c => + { + Application.Current.MainWindow.Hide(); + context.API.CheckForNewUpdate(); + context.API.ShowMsg("Please wait...", + "Checking for new update"); + return true; + } } }); return results; diff --git a/Plugins/Wox.Plugin.Sys/Wox.Plugin.Sys.csproj b/Plugins/Wox.Plugin.Sys/Wox.Plugin.Sys.csproj index 09a1e761f..ffdfa5972 100644 --- a/Plugins/Wox.Plugin.Sys/Wox.Plugin.Sys.csproj +++ b/Plugins/Wox.Plugin.Sys/Wox.Plugin.Sys.csproj @@ -71,9 +71,15 @@ + + PreserveNewest + PreserveNewest + + PreserveNewest + PreserveNewest diff --git a/Wox.Core/Updater.cs b/Wox.Core/Updater.cs index 971995f5e..9262d78f4 100644 --- a/Wox.Core/Updater.cs +++ b/Wox.Core/Updater.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Net; using System.Net.Http; @@ -10,9 +10,11 @@ using JetBrains.Annotations; using Squirrel; using Newtonsoft.Json; using Wox.Core.Resource; +using Wox.Plugin.SharedCommands; using Wox.Infrastructure; using Wox.Infrastructure.Http; using Wox.Infrastructure.Logger; +using System.IO; namespace Wox.Core { @@ -25,14 +27,14 @@ namespace Wox.Core GitHubRepository = gitHubRepository; } - public async Task UpdateApp() + public async Task UpdateApp(bool silentIfLatestVersion = true) { - UpdateManager m; - UpdateInfo u; + UpdateManager updateManager; + UpdateInfo newUpdateInfo; try { - m = await GitHubUpdateManager(GitHubRepository); + updateManager = await GitHubUpdateManager(GitHubRepository); } catch (Exception e) when (e is HttpRequestException || e is WebException || e is SocketException) { @@ -43,42 +45,61 @@ namespace Wox.Core try { // UpdateApp CheckForUpdate will return value only if the app is squirrel installed - u = await m.CheckForUpdate().NonNull(); + newUpdateInfo = await updateManager.CheckForUpdate().NonNull(); } catch (Exception e) when (e is HttpRequestException || e is WebException || e is SocketException) { Log.Exception($"|Updater.UpdateApp|Check your connection and proxy settings to api.github.com.", e); - m.Dispose(); + updateManager.Dispose(); return; } - var fr = u.FutureReleaseEntry; - var cr = u.CurrentlyInstalledVersion; - Log.Info($"|Updater.UpdateApp|Future Release <{fr.Formatted()}>"); - if (fr.Version > cr.Version) + var newReleaseVersion = Version.Parse(newUpdateInfo.FutureReleaseEntry.Version.ToString()); + var currentVersion = Version.Parse(Constant.Version); + + Log.Info($"|Updater.UpdateApp|Future Release <{newUpdateInfo.FutureReleaseEntry.Formatted()}>"); + + if (newReleaseVersion <= currentVersion) { - try - { - await m.DownloadReleases(u.ReleasesToApply); - } - catch (Exception e) when (e is HttpRequestException || e is WebException || e is SocketException) - { - Log.Exception($"|Updater.UpdateApp|Check your connection and proxy settings to github-cloud.s3.amazonaws.com.", e); - m.Dispose(); - return; - } - - await m.ApplyReleases(u); - await m.CreateUninstallerRegistryEntry(); - - var newVersionTips = this.NewVersinoTips(fr.Version.ToString()); - - MessageBox.Show(newVersionTips); - Log.Info($"|Updater.UpdateApp|Update success:{newVersionTips}"); + if (!silentIfLatestVersion) + MessageBox.Show("You already have the latest Wox version"); + updateManager.Dispose(); + return; } + try + { + await updateManager.DownloadReleases(newUpdateInfo.ReleasesToApply); + } + catch (Exception e) when (e is HttpRequestException || e is WebException || e is SocketException) + { + Log.Exception($"|Updater.UpdateApp|Check your connection and proxy settings to github-cloud.s3.amazonaws.com.", e); + updateManager.Dispose(); + return; + } + + await updateManager.ApplyReleases(newUpdateInfo); + + if (Constant.IsPortableMode) + { + var targetDestination = updateManager.RootAppDirectory + $"\\app-{newReleaseVersion.ToString()}\\{Constant.PortableFolderName}"; + FilesFolders.Copy(Constant.PortableDataPath, targetDestination); + if (!FilesFolders.VerifyBothFolderFilesEqual(Constant.PortableDataPath, targetDestination)) + MessageBox.Show(string.Format("Wox was not able to move your user profile data to the new update version. Please manually" + + "move your profile data folder from {0} to {1}", Constant.PortableDataPath, targetDestination)); + } + else + { + await updateManager.CreateUninstallerRegistryEntry(); + } + + var newVersionTips = NewVersinoTips(newReleaseVersion.ToString()); + + MessageBox.Show(newVersionTips); + Log.Info($"|Updater.UpdateApp|Update success:{newVersionTips}"); + // always dispose UpdateManager - m.Dispose(); + updateManager.Dispose(); } [UsedImplicitly] diff --git a/Wox.Infrastructure/Wox.cs b/Wox.Infrastructure/Wox.cs index 396ee0bb1..d09e7b904 100644 --- a/Wox.Infrastructure/Wox.cs +++ b/Wox.Infrastructure/Wox.cs @@ -7,12 +7,22 @@ namespace Wox.Infrastructure { public static class Constant { + public const string Wox = "Wox"; + public const string Plugins = "Plugins"; + + private static readonly Assembly Assembly = Assembly.GetExecutingAssembly(); + public static readonly string ProgramDirectory = Directory.GetParent(Assembly.Location.NonNull()).ToString(); + public static readonly string ExecutablePath = Path.Combine(ProgramDirectory, Wox + ".exe"); + + public static bool IsPortableMode; + public const string PortableFolderName = "UserData"; + public static string PortableDataPath = Path.Combine(ProgramDirectory, PortableFolderName); public static string DetermineDataDirectory() { - string portableDataPath = Path.Combine(ProgramDirectory, "UserData"); - if (Directory.Exists(portableDataPath)) + if (Directory.Exists(PortableDataPath)) { - return portableDataPath; + IsPortableMode = true; + return PortableDataPath; } else { @@ -20,12 +30,6 @@ namespace Wox.Infrastructure } } - public const string Wox = "Wox"; - public const string Plugins = "Plugins"; - - private static readonly Assembly Assembly = Assembly.GetExecutingAssembly(); - public static readonly string ProgramDirectory = Directory.GetParent(Assembly.Location.NonNull()).ToString(); - public static readonly string ExecutablePath = Path.Combine(ProgramDirectory, Wox + ".exe"); public static readonly string DataDirectory = DetermineDataDirectory(); public static readonly string PluginsDirectory = Path.Combine(DataDirectory, Plugins); public static readonly string PreinstalledDirectory = Path.Combine(ProgramDirectory, Plugins); diff --git a/Wox.Plugin/IPublicAPI.cs b/Wox.Plugin/IPublicAPI.cs index 8dc838963..754b76d36 100644 --- a/Wox.Plugin/IPublicAPI.cs +++ b/Wox.Plugin/IPublicAPI.cs @@ -70,6 +70,11 @@ namespace Wox.Plugin /// void ReloadAllPluginData(); + /// + /// Check for new Wox update + /// + void CheckForNewUpdate(); + /// /// Show message box /// diff --git a/Wox.Plugin/SharedCommands/FilesFolders.cs b/Wox.Plugin/SharedCommands/FilesFolders.cs new file mode 100644 index 000000000..584a83bc6 --- /dev/null +++ b/Wox.Plugin/SharedCommands/FilesFolders.cs @@ -0,0 +1,101 @@ +using System; +using System.IO; +using System.Windows; + +namespace Wox.Plugin.SharedCommands +{ + public static class FilesFolders + { + public static void Copy(this string sourcePath, string targetPath) + { + // Get the subdirectories for the specified directory. + DirectoryInfo dir = new DirectoryInfo(sourcePath); + + if (!dir.Exists) + { + throw new DirectoryNotFoundException( + "Source directory does not exist or could not be found: " + + sourcePath); + } + + try + { + DirectoryInfo[] dirs = dir.GetDirectories(); + // If the destination directory doesn't exist, create it. + if (!Directory.Exists(targetPath)) + { + Directory.CreateDirectory(targetPath); + } + + // Get the files in the directory and copy them to the new location. + FileInfo[] files = dir.GetFiles(); + foreach (FileInfo file in files) + { + string temppath = Path.Combine(targetPath, file.Name); + file.CopyTo(temppath, false); + } + + // Recursively copy subdirectories by calling itself on each subdirectory until there are no more to copy + foreach (DirectoryInfo subdir in dirs) + { + string temppath = Path.Combine(targetPath, subdir.Name); + Copy(subdir.FullName, temppath); + } + } + catch (Exception e) + { +#if DEBUG + throw e; +#else + MessageBox.Show(string.Format("Copying path {0} has failed, it will now be deleted for consistency", targetPath)); + RemoveFolder(targetPath); +#endif + } + + } + + public static bool VerifyBothFolderFilesEqual(this string fromPath, string toPath) + { + try + { + var fromDir = new DirectoryInfo(fromPath); + var toDir = new DirectoryInfo(toPath); + + if (fromDir.GetFiles("*", SearchOption.AllDirectories).Length != toDir.GetFiles("*", SearchOption.AllDirectories).Length) + return false; + + if (fromDir.GetDirectories("*", SearchOption.AllDirectories).Length != toDir.GetDirectories("*", SearchOption.AllDirectories).Length) + return false; + + return true; + } + catch (Exception e) + { +#if DEBUG + throw e; +#else + MessageBox.Show(string.Format("Unable to verify folders and files between {0} and {1}", fromPath, toPath)); + return false; +#endif + } + + } + + public static void RemoveFolder(this string path) + { + try + { + if (Directory.Exists(path)) + Directory.Delete(path, true); + } + catch (Exception e) + { +#if DEBUG + throw e; +#else + MessageBox.Show(string.Format("Not able to delete folder {0}, please go to the location and manually delete it", path)); +#endif + } + } + } +} diff --git a/Wox.Plugin/Wox.Plugin.csproj b/Wox.Plugin/Wox.Plugin.csproj index ceba2bbf0..b0b457e98 100644 --- a/Wox.Plugin/Wox.Plugin.csproj +++ b/Wox.Plugin/Wox.Plugin.csproj @@ -65,6 +65,7 @@ + diff --git a/Wox/PublicAPIInstance.cs b/Wox/PublicAPIInstance.cs index 314fc13b6..58915f87b 100644 --- a/Wox/PublicAPIInstance.cs +++ b/Wox/PublicAPIInstance.cs @@ -5,6 +5,7 @@ using System.Net; using System.Threading.Tasks; using System.Windows; using Squirrel; +using Wox.Core; using Wox.Core.Plugin; using Wox.Core.Resource; using Wox.Helper; @@ -65,6 +66,11 @@ namespace Wox UpdateManager.RestartApp(); } + public void CheckForNewUpdate() + { + _settingsVM.UpdateApp(); + } + public void SaveAppAllSettings() { _mainVM.Save(); diff --git a/Wox/SettingWindow.xaml b/Wox/SettingWindow.xaml index a6f23814f..1eddd8ab6 100644 --- a/Wox/SettingWindow.xaml +++ b/Wox/SettingWindow.xaml @@ -33,7 +33,8 @@ - + @@ -51,8 +52,7 @@ - + diff --git a/Wox/ViewModel/SettingWindowViewModel.cs b/Wox/ViewModel/SettingWindowViewModel.cs index 923e456b0..c93cdd413 100644 --- a/Wox/ViewModel/SettingWindowViewModel.cs +++ b/Wox/ViewModel/SettingWindowViewModel.cs @@ -44,7 +44,7 @@ namespace Wox.ViewModel public async void UpdateApp() { - await _updater.UpdateApp(); + await _updater.UpdateApp(false); } public void Save() From 8ec78688b80e8d1c92cc71a61a442e5c5c17c974 Mon Sep 17 00:00:00 2001 From: Jeremy Wu Date: Thu, 27 Feb 2020 09:14:45 +1100 Subject: [PATCH 4/5] Add empty actionParameters check (#148) --- Wox.Core/Plugin/QueryBuilder.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Wox.Core/Plugin/QueryBuilder.cs b/Wox.Core/Plugin/QueryBuilder.cs index b8ac41988..b01a93291 100644 --- a/Wox.Core/Plugin/QueryBuilder.cs +++ b/Wox.Core/Plugin/QueryBuilder.cs @@ -24,7 +24,7 @@ namespace Wox.Core.Plugin { // use non global plugin for query actionKeyword = possibleActionKeyword; actionParameters = terms.Skip(1).ToList(); - search = rawQuery.Substring(actionKeyword.Length + 1); + search = actionParameters.Count > 0 ? rawQuery.Substring(actionKeyword.Length + 1) : string.Empty; } else { // non action keyword From 3184f8d275decd7d8a068e193d797b3cf8d03d75 Mon Sep 17 00:00:00 2001 From: Jeremy Wu Date: Thu, 27 Feb 2020 09:16:09 +1100 Subject: [PATCH 5/5] Update readme with main features released from this fork (#149) * Update readme with main features released from this fork * update --- README.md | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 2b9da637f..00069024a 100644 --- a/README.md +++ b/README.md @@ -19,11 +19,22 @@ Features - Search for everything—applications, **uwp**, folders, files and more. - Use *pinyin* to search for programs / 支持用 **拼音** 搜索程序 - wyy / wangyiyun → 网易云音乐 -- Keyword plugin search - - search google with `g search_term` +- Keyword plugin search `g search_term` +- Search youtube, google, twitter and many more - Build custom themes at http://www.wox.one/theme/builder - Install plugins from http://www.wox.one/plugin +**New from this fork:** +- Portable mode +- Drastically improved search experience +- Search all subfolders and files +- Option to always run CMD or Powershell as administrator +- Run CMD, Powershell and programs as a different user +- Manage what programs should be loaded +- Highlighting of how results are matched during query search +- Open web search result as a tab or a new window +- Automatic update +- Reload/update plugin data Installation ------------ @@ -42,8 +53,8 @@ Versions marked as **pre-release** are unstable pre-release versions. - Requirements: - .net >= 4.5.2 - - [everything](https://www.voidtools.com/): `.exe` installer + use x64 if your windows is x64 + everything service is running - - [python3](https://www.python.org/downloads/): `.exe` installer + add it to `%PATH%` or set it in WoX settings + - If you want to integrate with [everything](https://www.voidtools.com/): `.exe` installer + use x64 if your windows is x64 + everything service is running. Supported version is 1.3.4.686 + - If you use python plugins, install [python3](https://www.python.org/downloads/): `.exe` installer + add it to `%PATH%` or set it in WoX settings Usage -----