diff --git a/.gitignore b/.gitignore index 7cce0eeb..9b01fc8d 100644 --- a/.gitignore +++ b/.gitignore @@ -45,3 +45,4 @@ RefitStubs.cs SyncTrayzorPortable *.pfx *.pvk +/src/SyncTrayzor/Icons/*.png diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 1915333f..7ffbd260 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,6 +1,14 @@ Changelog ========= +v1.0.14 +------- + + - Give Syncthing more than 10 seconds to start, fixing crash (#47, #48, #50) + - Better Syncthing API management in general + - Add support for 150% and 200% DPI to tray icon + - Slightly improve UI + v1.0.13 ------- diff --git a/Rakefile b/Rakefile index 4caf0548..671f74d8 100644 --- a/Rakefile +++ b/Rakefile @@ -44,6 +44,7 @@ class ArchDirConfig end ARCH_CONFIG = [ArchDirConfig.new('x64'), ArchDirConfig.new('x86')] +ASSEMBLY_INFOS = FileList['**/AssemblyInfo.cs'] namespace :build do ARCH_CONFIG.each do |arch_config| @@ -130,7 +131,9 @@ namespace :portable do Dir.mktmpdir do |tmp| portable_dir = File.join(tmp, arch_config.portable_output_dir) Dir.chdir(arch_config.bin_dir) do - files = FileList['**/*'].exclude('*.xml', '*.vshost.*', '*.log', '*.Installer.config', '*/FluentValidation.resources.dll', '*/System.Windows.Interactivity.resources.dll', 'syncthing.exe') + files = FileList['**/*'].exclude( + '*.xml', '*.vshost.*', '*.log', '*.Installer.config', '*/FluentValidation.resources.dll', + '*System.Windows.Interactivity.resources.dll', 'syncthing.exe', 'data') files.each do |file| cp_to_portable(portable_dir, file) @@ -200,3 +203,13 @@ end desc 'Build installer and portable for all architectures' task :package => ARCH_CONFIG.map{ |x| :"package:#{x.arch}" } + +desc "Bump version number" +task :version, [:version] do |t, args| + ASSEMBLY_INFOS.each do |info| + content = IO.read(info) + content[/\[assembly: AssemblyVersion\(\"(.+?).0\"\)\]/, 1] = args[:version] + content[/\[assembly: AssemblyFileVersion\(\"(.+?).0\"\)\]/, 1] = args[:version] + File.open(info, 'w'){ |f| f.write(content) } + end +end \ No newline at end of file diff --git a/installer/x64/installer-x64.iss b/installer/x64/installer-x64.iss index c048d6bf..dbb63a3c 100644 --- a/installer/x64/installer-x64.iss +++ b/installer/x64/installer-x64.iss @@ -54,7 +54,7 @@ Name: "syncthing0p11"; Description: "Syncthing v0.11 (beta)"; GroupDescription: Name: "{userappdata}\{#AppDataFolder}" [Files] -Source: "{#AppBin}\*"; DestDir: "{app}"; Excludes: "*.xml,*.vshost.*,*.config,*.log,FluentValidation.resources.dll,System.Windows.Interactivity.resources.dll,syncthing.exe"; Flags: ignoreversion recursesubdirs +Source: "{#AppBin}\*"; DestDir: "{app}"; Excludes: "*.xml,*.vshost.*,*.config,*.log,FluentValidation.resources.dll,System.Windows.Interactivity.resources.dll,syncthing.exe,data"; Flags: ignoreversion recursesubdirs Source: "{#AppBin}\SyncTrayzor.exe.Installer.config"; DestDir: "{app}"; DestName: "SyncTrayzor.exe.config"; Flags: ignoreversion Source: "{#AppSrc}\Icons\default.ico"; DestDir: "{app}"; Flags: ignoreversion Source: "{#AppRoot}\*.md"; DestDir: "{app}"; Flags: ignoreversion @@ -92,6 +92,10 @@ var begin if CurStep = ssInstall then begin + { We might be being run from ProcessRunner.exe, *and* we might be trying to update it. Funsies. Let's rename it (which Windows lets us do) } + DeleteFile(ExpandConstant('{app}\ProcessRunner.exe.old')); + RenameFile(ExpandConstant('{app}\ProcessRunner.exe'), ExpandConstant('{app}\ProcessRunner.exe.old')); + { This is really evil, but CefSharp.BrowserSubprocess.exe doesn't like to exit if we ask it nicely, so we have to kill it } ShellExec('open', 'taskkill.exe', '/f /t /im SyncTrayzor.exe', '', SW_HIDE, ewNoWait, ResultCode); end @@ -109,5 +113,6 @@ begin end; [UninstallDelete] +Type: files; Name: "{app}\ProcessRunner.exe.old" Type: filesandordirs; Name: "{userappdata}\{#AppDataFolder}" Type: filesandordirs; Name: "{userappdata}\{#AppDataFolder}" \ No newline at end of file diff --git a/installer/x86/installer-x86.iss b/installer/x86/installer-x86.iss index e8d04742..f1859c78 100644 --- a/installer/x86/installer-x86.iss +++ b/installer/x86/installer-x86.iss @@ -52,7 +52,7 @@ Name: "syncthing0p11"; Description: "Syncthing v0.11 (beta)"; GroupDescription: Name: "{userappdata}\{#AppDataFolder}" [Files] -Source: "{#AppBin}\*"; DestDir: "{app}"; Excludes: "*.xml,*.vshost.*,*.config,*.log,FluentValidation.resources.dll,System.Windows.Interactivity.resources.dll,syncthing.exe"; Flags: ignoreversion recursesubdirs +Source: "{#AppBin}\*"; DestDir: "{app}"; Excludes: "*.xml,*.vshost.*,*.config,*.log,FluentValidation.resources.dll,System.Windows.Interactivity.resources.dll,syncthing.exe,data"; Flags: ignoreversion recursesubdirs Source: "{#AppBin}\SyncTrayzor.exe.Installer.config"; DestDir: "{app}"; DestName: "SyncTrayzor.exe.config"; Flags: ignoreversion Source: "{#AppSrc}\Icons\default.ico"; DestDir: "{app}"; Flags: ignoreversion Source: "{#AppRoot}\*.md"; DestDir: "{app}"; Flags: ignoreversion @@ -90,6 +90,10 @@ var begin if CurStep = ssInstall then begin + { We might be being run from ProcessRunner.exe, *and* we might be trying to update it. Funsies. Let's rename it (which Windows lets us do) } + DeleteFile(ExpandConstant('{app}\ProcessRunner.exe.old')); + RenameFile(ExpandConstant('{app}\ProcessRunner.exe'), ExpandConstant('{app}\ProcessRunner.exe.old')); + { This is really evil, but CefSharp.BrowserSubprocess.exe doesn't like to exit if we ask it nicely, so we have to kill it } ShellExec('open', 'taskkill.exe', '/f /t /im SyncTrayzor.exe', '', SW_HIDE, ewNoWait, ResultCode); end @@ -107,5 +111,6 @@ begin end; [UninstallDelete] +Type: files; Name: "{app}\ProcessRunner.exe.old" Type: filesandordirs; Name: "{userappdata}\{#AppDataFolder}" Type: filesandordirs; Name: "{userappdata}\{#AppDataFolder}" \ No newline at end of file diff --git a/server/version_check.php b/server/version_check.php index ca37c444..0da7cd57 100644 --- a/server/version_check.php +++ b/server/version_check.php @@ -65,25 +65,43 @@ function get_with_wildcard($src, $value, $default = null) } $versions = [ + '1.0.14' => [ + 'installed' => [ + 'direct_download_url' => [] + ], + 'release_page_url' => 'https://github.com/canton7/SyncTrayzor/releases/tag/v1.0.14', + 'release_notes' => "- Give Syncthing more than 10 seconds to start, fixing crash (#47, #48, #50)\n- Better Syncthing API management in general\n- Add support for 150% and 200% DPI to tray icon\n- Slightly improve UI\n", + ], + '1.0.13' => [ - // No direct_download_url, as it turns out the 1.0.12 auto-upgrader is a bit broken (will restart SyncTrayzor as admin) - // 'installed' => [ - // 'direct_download_url' => [ - // 'x64' => 'https://github.com/canton7/SyncTrayzor/releases/download/v1.0.13/SyncTrayzorSetup-x64.exe', - // 'x86' => 'https://github.com/canton7/SyncTrayzor/releases/download/v1.0.13/SyncTrayzorSetup-x86.exe' - // ], - // ], + 'installed' => [ + 'direct_download_url' => [] + ], 'release_page_url' => 'https://github.com/canton7/SyncTrayzor/releases/tag/v1.0.13', 'release_notes' => "- Fix crash if 'Show tray icon only on close' is checked (#45)\n- Fix undocumented REST API change in Syncthing 0.11 (#46)\n- Check for updates on resume from sleep\n- Ensure SyncTrayzor is started as original user after auto-update", ], ]; $upgrades = [ - '1.0.12' => ['to' => '1.0.13', 'formatter' => '1'], + '1.0.13' => ['to' => '1.0.14', 'formatter' => '2'], + '1.0.12' => ['to' => '1.0.14', 'formatter' => '1'], ]; $response_formatters = [ + // 1.0.12 and 1.0.13 shouldn't download installers directly, as they doesn't know how to run them properly '1' => function($arch, $variant, $to_version, $to_version_info, $overrides) + { + $data = [ + 'version' => $to_version, + 'direct_download_url' => null, + 'release_page_url' => $to_version_info['release_page_url'], + 'release_notes' => isset($overrides['release_notes']) ? $overrides['release_notes'] : $to_version_info['release_notes'], + ]; + + return $data; + }, + // For when everything's working.... + '2' => function($arch, $variant, $to_version, $to_version_info, $overrides) { $variant_info = isset($overrides[$variant]) ? get_with_wildcard($overrides, $variant) : get_with_wildcard($to_version_info, $variant); diff --git a/src/InstallerRunner/Program.cs b/src/InstallerRunner/Program.cs deleted file mode 100644 index 235dd7c4..00000000 --- a/src/InstallerRunner/Program.cs +++ /dev/null @@ -1,80 +0,0 @@ -using System; -using System.Collections.Generic; -using System.ComponentModel; -using System.Diagnostics; -using System.IO; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace InstallerRunner -{ - class Program - { - private const int ERROR_CANCELLED = 1223; - - static int Main(string[] argsIn) - { - var args = new List(argsIn); - - string launch = null; - var indexOfLaunch = args.IndexOf("-launch"); - if (indexOfLaunch > -1) - { - if (indexOfLaunch >= args.Count - 1) - { - Console.Error.WriteLine("Must provide an argument to -launch"); - return 1; - } - - launch = args[indexOfLaunch + 1]; - args.RemoveAt(indexOfLaunch + 1); - args.RemoveAt(indexOfLaunch); - } - - if (args.Count == 0) - { - Console.Error.WriteLine("Must provide at least one command-line argument"); - return 1; - } - - if (!File.Exists(args[0])) - { - Console.Error.WriteLine("Could not find {0}", args[0]); - return 4; - } - - Console.WriteLine(String.Join(", ", args)); - - var startInfo = new ProcessStartInfo() - { - FileName = args[0], - Arguments = String.Join(" ", args.Skip(1)), - UseShellExecute = true, - Verb = "runas", - }; - - try - { - var process = Process.Start(startInfo); - process.WaitForExit(); - - if (!String.IsNullOrWhiteSpace(launch)) - Process.Start(launch); - } - catch (Win32Exception e) - { - if (e.NativeErrorCode == ERROR_CANCELLED) - return 2; - else - return 3; - } - catch (Exception) - { - return 3; - } - - return 0; - } - } -} diff --git a/src/InstallerRunner/App.config b/src/ProcessRunner/App.config similarity index 100% rename from src/InstallerRunner/App.config rename to src/ProcessRunner/App.config diff --git a/src/ProcessRunner/Options.cs b/src/ProcessRunner/Options.cs new file mode 100644 index 00000000..0fd21ab5 --- /dev/null +++ b/src/ProcessRunner/Options.cs @@ -0,0 +1,1366 @@ +// +// Options.cs +// +// Authors: +// Jonathan Pryor +// Federico Di Gregorio +// Rolf Bjarne Kvinge +// +// Copyright (C) 2008 Novell (http://www.novell.com) +// Copyright (C) 2009 Federico Di Gregorio. +// Copyright (C) 2012 Xamarin Inc (http://www.xamarin.com) +// +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// + +// Compile With: +// gmcs -debug+ -r:System.Core Options.cs -o:NDesk.Options.dll +// gmcs -debug+ -d:LINQ -r:System.Core Options.cs -o:NDesk.Options.dll +// +// The LINQ version just changes the implementation of +// OptionSet.Parse(IEnumerable), and confers no semantic changes. + +// +// A Getopt::Long-inspired option parsing library for C#. +// +// NDesk.Options.OptionSet is built upon a key/value table, where the +// key is a option format string and the value is a delegate that is +// invoked when the format string is matched. +// +// Option format strings: +// Regex-like BNF Grammar: +// name: .+ +// type: [=:] +// sep: ( [^{}]+ | '{' .+ '}' )? +// aliases: ( name type sep ) ( '|' name type sep )* +// +// Each '|'-delimited name is an alias for the associated action. If the +// format string ends in a '=', it has a required value. If the format +// string ends in a ':', it has an optional value. If neither '=' or ':' +// is present, no value is supported. `=' or `:' need only be defined on one +// alias, but if they are provided on more than one they must be consistent. +// +// Each alias portion may also end with a "key/value separator", which is used +// to split option values if the option accepts > 1 value. If not specified, +// it defaults to '=' and ':'. If specified, it can be any character except +// '{' and '}' OR the *string* between '{' and '}'. If no separator should be +// used (i.e. the separate values should be distinct arguments), then "{}" +// should be used as the separator. +// +// Options are extracted either from the current option by looking for +// the option name followed by an '=' or ':', or is taken from the +// following option IFF: +// - The current option does not contain a '=' or a ':' +// - The current option requires a value (i.e. not a Option type of ':') +// +// The `name' used in the option format string does NOT include any leading +// option indicator, such as '-', '--', or '/'. All three of these are +// permitted/required on any named option. +// +// Option bundling is permitted so long as: +// - '-' is used to start the option group +// - all of the bundled options are a single character +// - at most one of the bundled options accepts a value, and the value +// provided starts from the next character to the end of the string. +// +// This allows specifying '-a -b -c' as '-abc', and specifying '-D name=value' +// as '-Dname=value'. +// +// Option processing is disabled by specifying "--". All options after "--" +// are returned by OptionSet.Parse() unchanged and unprocessed. +// +// Unprocessed options are returned from OptionSet.Parse(). +// +// Examples: +// int verbose = 0; +// OptionSet p = new OptionSet () +// .Add ("v", v => ++verbose) +// .Add ("name=|value=", v => Console.WriteLine (v)); +// p.Parse (new string[]{"-v", "--v", "/v", "-name=A", "/name", "B", "extra"}); +// +// The above would parse the argument string array, and would invoke the +// lambda expression three times, setting `verbose' to 3 when complete. +// It would also print out "A" and "B" to standard output. +// The returned array would contain the string "extra". +// +// C# 3.0 collection initializers are supported and encouraged: +// var p = new OptionSet () { +// { "h|?|help", v => ShowHelp () }, +// }; +// +// System.ComponentModel.TypeConverter is also supported, allowing the use of +// custom data types in the callback type; TypeConverter.ConvertFromString() +// is used to convert the value option to an instance of the specified +// type: +// +// var p = new OptionSet () { +// { "foo=", (Foo f) => Console.WriteLine (f.ToString ()) }, +// }; +// +// Random other tidbits: +// - Boolean options (those w/o '=' or ':' in the option format string) +// are explicitly enabled if they are followed with '+', and explicitly +// disabled if they are followed with '-': +// string a = null; +// var p = new OptionSet () { +// { "a", s => a = s }, +// }; +// p.Parse (new string[]{"-a"}); // sets v != null +// p.Parse (new string[]{"-a+"}); // sets v != null +// p.Parse (new string[]{"-a-"}); // sets v == null +// + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.ComponentModel; +using System.Globalization; +using System.IO; +using System.Runtime.Serialization; +using System.Security.Permissions; +using System.Text; +using System.Text.RegularExpressions; + +#if LINQ +using System.Linq; +#endif + +#if TEST +using NDesk.Options; +#endif + +#if NDESK_OPTIONS +namespace NDesk.Options +#else +namespace Mono.Options +#endif +{ + static class StringCoda { + + public static IEnumerable WrappedLines (string self, params int[] widths) + { + IEnumerable w = widths; + return WrappedLines (self, w); + } + + public static IEnumerable WrappedLines (string self, IEnumerable widths) + { + if (widths == null) + throw new ArgumentNullException ("widths"); + return CreateWrappedLinesIterator (self, widths); + } + + private static IEnumerable CreateWrappedLinesIterator (string self, IEnumerable widths) + { + if (string.IsNullOrEmpty (self)) { + yield return string.Empty; + yield break; + } + using (IEnumerator ewidths = widths.GetEnumerator ()) { + bool? hw = null; + int width = GetNextWidth (ewidths, int.MaxValue, ref hw); + int start = 0, end; + do { + end = GetLineEnd (start, width, self); + char c = self [end-1]; + if (char.IsWhiteSpace (c)) + --end; + bool needContinuation = end != self.Length && !IsEolChar (c); + string continuation = ""; + if (needContinuation) { + --end; + continuation = "-"; + } + string line = self.Substring (start, end - start) + continuation; + yield return line; + start = end; + if (char.IsWhiteSpace (c)) + ++start; + width = GetNextWidth (ewidths, width, ref hw); + } while (start < self.Length); + } + } + + private static int GetNextWidth (IEnumerator ewidths, int curWidth, ref bool? eValid) + { + if (!eValid.HasValue || (eValid.HasValue && eValid.Value)) { + curWidth = (eValid = ewidths.MoveNext ()).Value ? ewidths.Current : curWidth; + // '.' is any character, - is for a continuation + const string minWidth = ".-"; + if (curWidth < minWidth.Length) + throw new ArgumentOutOfRangeException ("widths", + string.Format ("Element must be >= {0}, was {1}.", minWidth.Length, curWidth)); + return curWidth; + } + // no more elements, use the last element. + return curWidth; + } + + private static bool IsEolChar (char c) + { + return !char.IsLetterOrDigit (c); + } + + private static int GetLineEnd (int start, int length, string description) + { + int end = System.Math.Min (start + length, description.Length); + int sep = -1; + for (int i = start; i < end; ++i) { + if (description [i] == '\n') + return i+1; + if (IsEolChar (description [i])) + sep = i+1; + } + if (sep == -1 || end == description.Length) + return end; + return sep; + } + } + + public class OptionValueCollection : IList, IList { + + List values = new List (); + OptionContext c; + + internal OptionValueCollection (OptionContext c) + { + this.c = c; + } + + #region ICollection + void ICollection.CopyTo (Array array, int index) {(values as ICollection).CopyTo (array, index);} + bool ICollection.IsSynchronized {get {return (values as ICollection).IsSynchronized;}} + object ICollection.SyncRoot {get {return (values as ICollection).SyncRoot;}} + #endregion + + #region ICollection + public void Add (string item) {values.Add (item);} + public void Clear () {values.Clear ();} + public bool Contains (string item) {return values.Contains (item);} + public void CopyTo (string[] array, int arrayIndex) {values.CopyTo (array, arrayIndex);} + public bool Remove (string item) {return values.Remove (item);} + public int Count {get {return values.Count;}} + public bool IsReadOnly {get {return false;}} + #endregion + + #region IEnumerable + IEnumerator IEnumerable.GetEnumerator () {return values.GetEnumerator ();} + #endregion + + #region IEnumerable + public IEnumerator GetEnumerator () {return values.GetEnumerator ();} + #endregion + + #region IList + int IList.Add (object value) {return (values as IList).Add (value);} + bool IList.Contains (object value) {return (values as IList).Contains (value);} + int IList.IndexOf (object value) {return (values as IList).IndexOf (value);} + void IList.Insert (int index, object value) {(values as IList).Insert (index, value);} + void IList.Remove (object value) {(values as IList).Remove (value);} + void IList.RemoveAt (int index) {(values as IList).RemoveAt (index);} + bool IList.IsFixedSize {get {return false;}} + object IList.this [int index] {get {return this [index];} set {(values as IList)[index] = value;}} + #endregion + + #region IList + public int IndexOf (string item) {return values.IndexOf (item);} + public void Insert (int index, string item) {values.Insert (index, item);} + public void RemoveAt (int index) {values.RemoveAt (index);} + + private void AssertValid (int index) + { + if (c.Option == null) + throw new InvalidOperationException ("OptionContext.Option is null."); + if (index >= c.Option.MaxValueCount) + throw new ArgumentOutOfRangeException ("index"); + if (c.Option.OptionValueType == OptionValueType.Required && + index >= values.Count) + throw new OptionException (string.Format ( + c.OptionSet.MessageLocalizer ("Missing required value for option '{0}'."), c.OptionName), + c.OptionName); + } + + public string this [int index] { + get { + AssertValid (index); + return index >= values.Count ? null : values [index]; + } + set { + values [index] = value; + } + } + #endregion + + public List ToList () + { + return new List (values); + } + + public string[] ToArray () + { + return values.ToArray (); + } + + public override string ToString () + { + return string.Join (", ", values.ToArray ()); + } + } + + public class OptionContext { + private Option option; + private string name; + private int index; + private OptionSet set; + private OptionValueCollection c; + + public OptionContext (OptionSet set) + { + this.set = set; + this.c = new OptionValueCollection (this); + } + + public Option Option { + get {return option;} + set {option = value;} + } + + public string OptionName { + get {return name;} + set {name = value;} + } + + public int OptionIndex { + get {return index;} + set {index = value;} + } + + public OptionSet OptionSet { + get {return set;} + } + + public OptionValueCollection OptionValues { + get {return c;} + } + } + + public enum OptionValueType { + None, + Optional, + Required, + } + + public abstract class Option { + string prototype, description; + string[] names; + OptionValueType type; + int count; + string[] separators; + bool hidden; + + protected Option (string prototype, string description) + : this (prototype, description, 1, false) + { + } + + protected Option (string prototype, string description, int maxValueCount) + : this (prototype, description, maxValueCount, false) + { + } + + protected Option (string prototype, string description, int maxValueCount, bool hidden) + { + if (prototype == null) + throw new ArgumentNullException ("prototype"); + if (prototype.Length == 0) + throw new ArgumentException ("Cannot be the empty string.", "prototype"); + if (maxValueCount < 0) + throw new ArgumentOutOfRangeException ("maxValueCount"); + + this.prototype = prototype; + this.description = description; + this.count = maxValueCount; + this.names = (this is OptionSet.Category) + // append GetHashCode() so that "duplicate" categories have distinct + // names, e.g. adding multiple "" categories should be valid. + ? new[]{prototype + this.GetHashCode ()} + : prototype.Split ('|'); + + if (this is OptionSet.Category) + return; + + this.type = ParsePrototype (); + this.hidden = hidden; + + if (this.count == 0 && type != OptionValueType.None) + throw new ArgumentException ( + "Cannot provide maxValueCount of 0 for OptionValueType.Required or " + + "OptionValueType.Optional.", + "maxValueCount"); + if (this.type == OptionValueType.None && maxValueCount > 1) + throw new ArgumentException ( + string.Format ("Cannot provide maxValueCount of {0} for OptionValueType.None.", maxValueCount), + "maxValueCount"); + if (Array.IndexOf (names, "<>") >= 0 && + ((names.Length == 1 && this.type != OptionValueType.None) || + (names.Length > 1 && this.MaxValueCount > 1))) + throw new ArgumentException ( + "The default option handler '<>' cannot require values.", + "prototype"); + } + + public string Prototype {get {return prototype;}} + public string Description {get {return description;}} + public OptionValueType OptionValueType {get {return type;}} + public int MaxValueCount {get {return count;}} + public bool Hidden {get {return hidden;}} + + public string[] GetNames () + { + return (string[]) names.Clone (); + } + + public string[] GetValueSeparators () + { + if (separators == null) + return new string [0]; + return (string[]) separators.Clone (); + } + + protected static T Parse (string value, OptionContext c) + { + Type tt = typeof (T); + bool nullable = tt.IsValueType && tt.IsGenericType && + !tt.IsGenericTypeDefinition && + tt.GetGenericTypeDefinition () == typeof (Nullable<>); + Type targetType = nullable ? tt.GetGenericArguments () [0] : typeof (T); + TypeConverter conv = TypeDescriptor.GetConverter (targetType); + T t = default (T); + try { + if (value != null) + t = (T) conv.ConvertFromString (value); + } + catch (Exception e) { + throw new OptionException ( + string.Format ( + c.OptionSet.MessageLocalizer ("Could not convert string `{0}' to type {1} for option `{2}'."), + value, targetType.Name, c.OptionName), + c.OptionName, e); + } + return t; + } + + internal string[] Names {get {return names;}} + internal string[] ValueSeparators {get {return separators;}} + + static readonly char[] NameTerminator = new char[]{'=', ':'}; + + private OptionValueType ParsePrototype () + { + char type = '\0'; + List seps = new List (); + for (int i = 0; i < names.Length; ++i) { + string name = names [i]; + if (name.Length == 0) + throw new ArgumentException ("Empty option names are not supported.", "prototype"); + + int end = name.IndexOfAny (NameTerminator); + if (end == -1) + continue; + names [i] = name.Substring (0, end); + if (type == '\0' || type == name [end]) + type = name [end]; + else + throw new ArgumentException ( + string.Format ("Conflicting option types: '{0}' vs. '{1}'.", type, name [end]), + "prototype"); + AddSeparators (name, end, seps); + } + + if (type == '\0') + return OptionValueType.None; + + if (count <= 1 && seps.Count != 0) + throw new ArgumentException ( + string.Format ("Cannot provide key/value separators for Options taking {0} value(s).", count), + "prototype"); + if (count > 1) { + if (seps.Count == 0) + this.separators = new string[]{":", "="}; + else if (seps.Count == 1 && seps [0].Length == 0) + this.separators = null; + else + this.separators = seps.ToArray (); + } + + return type == '=' ? OptionValueType.Required : OptionValueType.Optional; + } + + private static void AddSeparators (string name, int end, ICollection seps) + { + int start = -1; + for (int i = end+1; i < name.Length; ++i) { + switch (name [i]) { + case '{': + if (start != -1) + throw new ArgumentException ( + string.Format ("Ill-formed name/value separator found in \"{0}\".", name), + "prototype"); + start = i+1; + break; + case '}': + if (start == -1) + throw new ArgumentException ( + string.Format ("Ill-formed name/value separator found in \"{0}\".", name), + "prototype"); + seps.Add (name.Substring (start, i-start)); + start = -1; + break; + default: + if (start == -1) + seps.Add (name [i].ToString ()); + break; + } + } + if (start != -1) + throw new ArgumentException ( + string.Format ("Ill-formed name/value separator found in \"{0}\".", name), + "prototype"); + } + + public void Invoke (OptionContext c) + { + OnParseComplete (c); + c.OptionName = null; + c.Option = null; + c.OptionValues.Clear (); + } + + protected abstract void OnParseComplete (OptionContext c); + + public override string ToString () + { + return Prototype; + } + } + + public abstract class ArgumentSource { + + protected ArgumentSource () + { + } + + public abstract string[] GetNames (); + public abstract string Description { get; } + public abstract bool GetArguments (string value, out IEnumerable replacement); + + public static IEnumerable GetArgumentsFromFile (string file) + { + return GetArguments (File.OpenText (file), true); + } + + public static IEnumerable GetArguments (TextReader reader) + { + return GetArguments (reader, false); + } + + // Cribbed from mcs/driver.cs:LoadArgs(string) + static IEnumerable GetArguments (TextReader reader, bool close) + { + try { + StringBuilder arg = new StringBuilder (); + + string line; + while ((line = reader.ReadLine ()) != null) { + int t = line.Length; + + for (int i = 0; i < t; i++) { + char c = line [i]; + + if (c == '"' || c == '\'') { + char end = c; + + for (i++; i < t; i++){ + c = line [i]; + + if (c == end) + break; + arg.Append (c); + } + } else if (c == ' ') { + if (arg.Length > 0) { + yield return arg.ToString (); + arg.Length = 0; + } + } else + arg.Append (c); + } + if (arg.Length > 0) { + yield return arg.ToString (); + arg.Length = 0; + } + } + } + finally { + if (close) + reader.Close (); + } + } + } + + public class ResponseFileSource : ArgumentSource { + + public override string[] GetNames () + { + return new string[]{"@file"}; + } + + public override string Description { + get {return "Read response file for more options.";} + } + + public override bool GetArguments (string value, out IEnumerable replacement) + { + if (string.IsNullOrEmpty (value) || !value.StartsWith ("@")) { + replacement = null; + return false; + } + replacement = ArgumentSource.GetArgumentsFromFile (value.Substring (1)); + return true; + } + } + + [Serializable] + public class OptionException : Exception { + private string option; + + public OptionException () + { + } + + public OptionException (string message, string optionName) + : base (message) + { + this.option = optionName; + } + + public OptionException (string message, string optionName, Exception innerException) + : base (message, innerException) + { + this.option = optionName; + } + + protected OptionException (SerializationInfo info, StreamingContext context) + : base (info, context) + { + this.option = info.GetString ("OptionName"); + } + + public string OptionName { + get {return this.option;} + } + + [SecurityPermission (SecurityAction.LinkDemand, SerializationFormatter = true)] + public override void GetObjectData (SerializationInfo info, StreamingContext context) + { + base.GetObjectData (info, context); + info.AddValue ("OptionName", option); + } + } + + public delegate void OptionAction (TKey key, TValue value); + + public class OptionSet : KeyedCollection + { + public OptionSet () + : this (delegate (string f) {return f;}) + { + } + + public OptionSet (Converter localizer) + { + this.localizer = localizer; + this.roSources = new ReadOnlyCollection(sources); + } + + Converter localizer; + + public Converter MessageLocalizer { + get {return localizer;} + } + + List sources = new List (); + ReadOnlyCollection roSources; + + public ReadOnlyCollection ArgumentSources { + get {return roSources;} + } + + + protected override string GetKeyForItem (Option item) + { + if (item == null) + throw new ArgumentNullException ("option"); + if (item.Names != null && item.Names.Length > 0) + return item.Names [0]; + // This should never happen, as it's invalid for Option to be + // constructed w/o any names. + throw new InvalidOperationException ("Option has no names!"); + } + + [Obsolete ("Use KeyedCollection.this[string]")] + protected Option GetOptionForName (string option) + { + if (option == null) + throw new ArgumentNullException ("option"); + try { + return base [option]; + } + catch (KeyNotFoundException) { + return null; + } + } + + protected override void InsertItem (int index, Option item) + { + base.InsertItem (index, item); + AddImpl (item); + } + + protected override void RemoveItem (int index) + { + Option p = Items [index]; + base.RemoveItem (index); + // KeyedCollection.RemoveItem() handles the 0th item + for (int i = 1; i < p.Names.Length; ++i) { + Dictionary.Remove (p.Names [i]); + } + } + + protected override void SetItem (int index, Option item) + { + base.SetItem (index, item); + AddImpl (item); + } + + private void AddImpl (Option option) + { + if (option == null) + throw new ArgumentNullException ("option"); + List added = new List (option.Names.Length); + try { + // KeyedCollection.InsertItem/SetItem handle the 0th name. + for (int i = 1; i < option.Names.Length; ++i) { + Dictionary.Add (option.Names [i], option); + added.Add (option.Names [i]); + } + } + catch (Exception) { + foreach (string name in added) + Dictionary.Remove (name); + throw; + } + } + + public OptionSet Add (string header) + { + if (header == null) + throw new ArgumentNullException ("header"); + Add (new Category (header)); + return this; + } + + internal sealed class Category : Option { + + // Prototype starts with '=' because this is an invalid prototype + // (see Option.ParsePrototype(), and thus it'll prevent Category + // instances from being accidentally used as normal options. + public Category (string description) + : base ("=:Category:= " + description, description) + { + } + + protected override void OnParseComplete (OptionContext c) + { + throw new NotSupportedException ("Category.OnParseComplete should not be invoked."); + } + } + + + public new OptionSet Add (Option option) + { + base.Add (option); + return this; + } + + sealed class ActionOption : Option { + Action action; + + public ActionOption (string prototype, string description, int count, Action action) + : this (prototype, description, count, action, false) + { + } + + public ActionOption (string prototype, string description, int count, Action action, bool hidden) + : base (prototype, description, count, hidden) + { + if (action == null) + throw new ArgumentNullException ("action"); + this.action = action; + } + + protected override void OnParseComplete (OptionContext c) + { + action (c.OptionValues); + } + } + + public OptionSet Add (string prototype, Action action) + { + return Add (prototype, null, action); + } + + public OptionSet Add (string prototype, string description, Action action) + { + return Add (prototype, description, action, false); + } + + public OptionSet Add (string prototype, string description, Action action, bool hidden) + { + if (action == null) + throw new ArgumentNullException ("action"); + Option p = new ActionOption (prototype, description, 1, + delegate (OptionValueCollection v) { action (v [0]); }, hidden); + base.Add (p); + return this; + } + + public OptionSet Add (string prototype, OptionAction action) + { + return Add (prototype, null, action); + } + + public OptionSet Add (string prototype, string description, OptionAction action) + { + return Add (prototype, description, action, false); + } + + public OptionSet Add (string prototype, string description, OptionAction action, bool hidden) { + if (action == null) + throw new ArgumentNullException ("action"); + Option p = new ActionOption (prototype, description, 2, + delegate (OptionValueCollection v) {action (v [0], v [1]);}, hidden); + base.Add (p); + return this; + } + + sealed class ActionOption : Option { + Action action; + + public ActionOption (string prototype, string description, Action action) + : base (prototype, description, 1) + { + if (action == null) + throw new ArgumentNullException ("action"); + this.action = action; + } + + protected override void OnParseComplete (OptionContext c) + { + action (Parse (c.OptionValues [0], c)); + } + } + + sealed class ActionOption : Option { + OptionAction action; + + public ActionOption (string prototype, string description, OptionAction action) + : base (prototype, description, 2) + { + if (action == null) + throw new ArgumentNullException ("action"); + this.action = action; + } + + protected override void OnParseComplete (OptionContext c) + { + action ( + Parse (c.OptionValues [0], c), + Parse (c.OptionValues [1], c)); + } + } + + public OptionSet Add (string prototype, Action action) + { + return Add (prototype, null, action); + } + + public OptionSet Add (string prototype, string description, Action action) + { + return Add (new ActionOption (prototype, description, action)); + } + + public OptionSet Add (string prototype, OptionAction action) + { + return Add (prototype, null, action); + } + + public OptionSet Add (string prototype, string description, OptionAction action) + { + return Add (new ActionOption (prototype, description, action)); + } + + public OptionSet Add (ArgumentSource source) + { + if (source == null) + throw new ArgumentNullException ("source"); + sources.Add (source); + return this; + } + + protected virtual OptionContext CreateOptionContext () + { + return new OptionContext (this); + } + + public List Parse (IEnumerable arguments) + { + if (arguments == null) + throw new ArgumentNullException ("arguments"); + OptionContext c = CreateOptionContext (); + c.OptionIndex = -1; + bool process = true; + List unprocessed = new List (); + Option def = Contains ("<>") ? this ["<>"] : null; + ArgumentEnumerator ae = new ArgumentEnumerator (arguments); + foreach (string argument in ae) { + ++c.OptionIndex; + if (argument == "--") { + process = false; + continue; + } + if (!process) { + Unprocessed (unprocessed, def, c, argument); + continue; + } + if (AddSource (ae, argument)) + continue; + if (!Parse (argument, c)) + Unprocessed (unprocessed, def, c, argument); + } + if (c.Option != null) + c.Option.Invoke (c); + return unprocessed; + } + + class ArgumentEnumerator : IEnumerable { + List> sources = new List> (); + + public ArgumentEnumerator (IEnumerable arguments) + { + sources.Add (arguments.GetEnumerator ()); + } + + public void Add (IEnumerable arguments) + { + sources.Add (arguments.GetEnumerator ()); + } + + public IEnumerator GetEnumerator () + { + do { + IEnumerator c = sources [sources.Count-1]; + if (c.MoveNext ()) + yield return c.Current; + else { + c.Dispose (); + sources.RemoveAt (sources.Count-1); + } + } while (sources.Count > 0); + } + + IEnumerator IEnumerable.GetEnumerator () + { + return GetEnumerator (); + } + } + + bool AddSource (ArgumentEnumerator ae, string argument) + { + foreach (ArgumentSource source in sources) { + IEnumerable replacement; + if (!source.GetArguments (argument, out replacement)) + continue; + ae.Add (replacement); + return true; + } + return false; + } + + private static bool Unprocessed (ICollection extra, Option def, OptionContext c, string argument) + { + if (def == null) { + extra.Add (argument); + return false; + } + c.OptionValues.Add (argument); + c.Option = def; + c.Option.Invoke (c); + return false; + } + + private readonly Regex ValueOption = new Regex ( + @"^(?--|-|/)(?[^:=]+)((?[:=])(?.*))?$"); + + protected bool GetOptionParts (string argument, out string flag, out string name, out string sep, out string value) + { + if (argument == null) + throw new ArgumentNullException ("argument"); + + flag = name = sep = value = null; + Match m = ValueOption.Match (argument); + if (!m.Success) { + return false; + } + flag = m.Groups ["flag"].Value; + name = m.Groups ["name"].Value; + if (m.Groups ["sep"].Success && m.Groups ["value"].Success) { + sep = m.Groups ["sep"].Value; + value = m.Groups ["value"].Value; + } + return true; + } + + protected virtual bool Parse (string argument, OptionContext c) + { + if (c.Option != null) { + ParseValue (argument, c); + return true; + } + + string f, n, s, v; + if (!GetOptionParts (argument, out f, out n, out s, out v)) + return false; + + Option p; + if (Contains (n)) { + p = this [n]; + c.OptionName = f + n; + c.Option = p; + switch (p.OptionValueType) { + case OptionValueType.None: + c.OptionValues.Add (n); + c.Option.Invoke (c); + break; + case OptionValueType.Optional: + case OptionValueType.Required: + ParseValue (v, c); + break; + } + return true; + } + // no match; is it a bool option? + if (ParseBool (argument, n, c)) + return true; + // is it a bundled option? + if (ParseBundledValue (f, string.Concat (n + s + v), c)) + return true; + + return false; + } + + private void ParseValue (string option, OptionContext c) + { + if (option != null) + foreach (string o in c.Option.ValueSeparators != null + ? option.Split (c.Option.ValueSeparators, c.Option.MaxValueCount - c.OptionValues.Count, StringSplitOptions.None) + : new string[]{option}) { + c.OptionValues.Add (o); + } + if (c.OptionValues.Count == c.Option.MaxValueCount || + c.Option.OptionValueType == OptionValueType.Optional) + c.Option.Invoke (c); + else if (c.OptionValues.Count > c.Option.MaxValueCount) { + throw new OptionException (localizer (string.Format ( + "Error: Found {0} option values when expecting {1}.", + c.OptionValues.Count, c.Option.MaxValueCount)), + c.OptionName); + } + } + + private bool ParseBool (string option, string n, OptionContext c) + { + Option p; + string rn; + if (n.Length >= 1 && (n [n.Length-1] == '+' || n [n.Length-1] == '-') && + Contains ((rn = n.Substring (0, n.Length-1)))) { + p = this [rn]; + string v = n [n.Length-1] == '+' ? option : null; + c.OptionName = option; + c.Option = p; + c.OptionValues.Add (v); + p.Invoke (c); + return true; + } + return false; + } + + private bool ParseBundledValue (string f, string n, OptionContext c) + { + if (f != "-") + return false; + for (int i = 0; i < n.Length; ++i) { + Option p; + string opt = f + n [i].ToString (); + string rn = n [i].ToString (); + if (!Contains (rn)) { + if (i == 0) + return false; + throw new OptionException (string.Format (localizer ( + "Cannot use unregistered option '{0}' in bundle '{1}'."), rn, f + n), null); + } + p = this [rn]; + switch (p.OptionValueType) { + case OptionValueType.None: + Invoke (c, opt, n, p); + break; + case OptionValueType.Optional: + case OptionValueType.Required: { + string v = n.Substring (i+1); + c.Option = p; + c.OptionName = opt; + ParseValue (v.Length != 0 ? v : null, c); + return true; + } + default: + throw new InvalidOperationException ("Unknown OptionValueType: " + p.OptionValueType); + } + } + return true; + } + + private static void Invoke (OptionContext c, string name, string value, Option option) + { + c.OptionName = name; + c.Option = option; + c.OptionValues.Add (value); + option.Invoke (c); + } + + private const int OptionWidth = 29; + private const int Description_FirstWidth = 80 - OptionWidth; + private const int Description_RemWidth = 80 - OptionWidth - 2; + + public void WriteOptionDescriptions (TextWriter o) + { + foreach (Option p in this) { + int written = 0; + + if (p.Hidden) + continue; + + Category c = p as Category; + if (c != null) { + WriteDescription (o, p.Description, "", 80, 80); + continue; + } + + if (!WriteOptionPrototype (o, p, ref written)) + continue; + + if (written < OptionWidth) + o.Write (new string (' ', OptionWidth - written)); + else { + o.WriteLine (); + o.Write (new string (' ', OptionWidth)); + } + + WriteDescription (o, p.Description, new string (' ', OptionWidth+2), + Description_FirstWidth, Description_RemWidth); + } + + foreach (ArgumentSource s in sources) { + string[] names = s.GetNames (); + if (names == null || names.Length == 0) + continue; + + int written = 0; + + Write (o, ref written, " "); + Write (o, ref written, names [0]); + for (int i = 1; i < names.Length; ++i) { + Write (o, ref written, ", "); + Write (o, ref written, names [i]); + } + + if (written < OptionWidth) + o.Write (new string (' ', OptionWidth - written)); + else { + o.WriteLine (); + o.Write (new string (' ', OptionWidth)); + } + + WriteDescription (o, s.Description, new string (' ', OptionWidth+2), + Description_FirstWidth, Description_RemWidth); + } + } + + void WriteDescription (TextWriter o, string value, string prefix, int firstWidth, int remWidth) + { + bool indent = false; + foreach (string line in GetLines (localizer (GetDescription (value)), firstWidth, remWidth)) { + if (indent) + o.Write (prefix); + o.WriteLine (line); + indent = true; + } + } + + bool WriteOptionPrototype (TextWriter o, Option p, ref int written) + { + string[] names = p.Names; + + int i = GetNextOptionIndex (names, 0); + if (i == names.Length) + return false; + + if (names [i].Length == 1) { + Write (o, ref written, " -"); + Write (o, ref written, names [0]); + } + else { + Write (o, ref written, " --"); + Write (o, ref written, names [0]); + } + + for ( i = GetNextOptionIndex (names, i+1); + i < names.Length; i = GetNextOptionIndex (names, i+1)) { + Write (o, ref written, ", "); + Write (o, ref written, names [i].Length == 1 ? "-" : "--"); + Write (o, ref written, names [i]); + } + + if (p.OptionValueType == OptionValueType.Optional || + p.OptionValueType == OptionValueType.Required) { + if (p.OptionValueType == OptionValueType.Optional) { + Write (o, ref written, localizer ("[")); + } + Write (o, ref written, localizer ("=" + GetArgumentName (0, p.MaxValueCount, p.Description))); + string sep = p.ValueSeparators != null && p.ValueSeparators.Length > 0 + ? p.ValueSeparators [0] + : " "; + for (int c = 1; c < p.MaxValueCount; ++c) { + Write (o, ref written, localizer (sep + GetArgumentName (c, p.MaxValueCount, p.Description))); + } + if (p.OptionValueType == OptionValueType.Optional) { + Write (o, ref written, localizer ("]")); + } + } + return true; + } + + static int GetNextOptionIndex (string[] names, int i) + { + while (i < names.Length && names [i] == "<>") { + ++i; + } + return i; + } + + static void Write (TextWriter o, ref int n, string s) + { + n += s.Length; + o.Write (s); + } + + private static string GetArgumentName (int index, int maxIndex, string description) + { + if (description == null) + return maxIndex == 1 ? "VALUE" : "VALUE" + (index + 1); + string[] nameStart; + if (maxIndex == 1) + nameStart = new string[]{"{0:", "{"}; + else + nameStart = new string[]{"{" + index + ":"}; + for (int i = 0; i < nameStart.Length; ++i) { + int start, j = 0; + do { + start = description.IndexOf (nameStart [i], j); + } while (start >= 0 && j != 0 ? description [j++ - 1] == '{' : false); + if (start == -1) + continue; + int end = description.IndexOf ("}", start); + if (end == -1) + continue; + return description.Substring (start + nameStart [i].Length, end - start - nameStart [i].Length); + } + return maxIndex == 1 ? "VALUE" : "VALUE" + (index + 1); + } + + private static string GetDescription (string description) + { + if (description == null) + return string.Empty; + StringBuilder sb = new StringBuilder (description.Length); + int start = -1; + for (int i = 0; i < description.Length; ++i) { + switch (description [i]) { + case '{': + if (i == start) { + sb.Append ('{'); + start = -1; + } + else if (start < 0) + start = i + 1; + break; + case '}': + if (start < 0) { + if ((i+1) == description.Length || description [i+1] != '}') + throw new InvalidOperationException ("Invalid option description: " + description); + ++i; + sb.Append ("}"); + } + else { + sb.Append (description.Substring (start, i - start)); + start = -1; + } + break; + case ':': + if (start < 0) + goto default; + start = i + 1; + break; + default: + if (start < 0) + sb.Append (description [i]); + break; + } + } + return sb.ToString (); + } + + private static IEnumerable GetLines (string description, int firstWidth, int remWidth) + { + return StringCoda.WrappedLines (description, firstWidth, remWidth); + } + } +} + diff --git a/src/InstallerRunner/InstallerRunner.csproj b/src/ProcessRunner/ProcessRunner.csproj similarity index 94% rename from src/InstallerRunner/InstallerRunner.csproj rename to src/ProcessRunner/ProcessRunner.csproj index fb001f9e..29daae85 100644 --- a/src/InstallerRunner/InstallerRunner.csproj +++ b/src/ProcessRunner/ProcessRunner.csproj @@ -4,11 +4,11 @@ Debug AnyCPU - {E33FE8E5-18A5-4D1B-A5D9-21851DC0B1D5} + {692BB2F9-CD24-482F-B13A-4335F27F6EC2} Exe Properties - InstallerRunner - InstallerRunner + ProcessRunner + ProcessRunner v4.5 512 @@ -62,6 +62,7 @@ + diff --git a/src/ProcessRunner/Program.cs b/src/ProcessRunner/Program.cs new file mode 100644 index 00000000..734f58c2 --- /dev/null +++ b/src/ProcessRunner/Program.cs @@ -0,0 +1,98 @@ +using Mono.Options; +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Diagnostics; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace ProcessRunner +{ + class Program + { + private const int ERROR_CANCELLED = 1223; + + static int Main(string[] args) + { + bool showHelp = false; + bool runas = false; + string launch = null; + bool shell = false; + bool noWindow = false; + + var options = new OptionSet() + .Add("help|h", "Show this help", v => showHelp = true) + .Add("runas", "Run as Administrator (implies --shell)", v => runas = true) + .Add("launch=", "Executable to run afterwards", v => launch = v) + .Add("shell", "Set UseShellExecute = true", v => shell = true) + .Add("nowindow", "Set CreateNoWindow = true", v => noWindow = true); + + var pivotIndex = Array.IndexOf(args, "--"); + if (pivotIndex < 0) + { + Console.Error.WriteLine("Must specify a command to run, see --help"); + return 1; + } + + var remainder = args.Skip(pivotIndex + 1).ToList(); + var unknownArgs = options.Parse(args.Take(pivotIndex)); + + if (unknownArgs.Count > 0) + { + Console.Error.WriteLine("Unknown argument {0}. See --help", unknownArgs[0]); + return 1; + } + + if (showHelp) + { + Console.WriteLine("Usage: ProcessRunner.exe [options] -- command"); + options.WriteOptionDescriptions(Console.Out); + return 0; + } + + if (remainder.Count == 0) + { + Console.Error.WriteLine("Must specify a command, See --help"); + return 1; + } + + var startInfo = new ProcessStartInfo() + { + FileName = remainder[0], + Arguments = String.Join(" ", remainder.Skip(1).Select(x => x.Contains(' ') ? String.Format("\"{0}\"", x) : x)), + UseShellExecute = shell, + CreateNoWindow = noWindow, + }; + + if (runas) + { + startInfo.Verb = "runas"; + startInfo.UseShellExecute = true; + } + + try + { + var process = Process.Start(startInfo); + if (!String.IsNullOrWhiteSpace(launch)) + { + process.WaitForExit(); + Process.Start(launch); + } + } + catch (Win32Exception e) + { + if (e.NativeErrorCode == ERROR_CANCELLED) + return 2; + else + return 3; + } + catch (Exception) + { + return 3; + } + + return 0; + } + } +} diff --git a/src/InstallerRunner/Properties/AssemblyInfo.cs b/src/ProcessRunner/Properties/AssemblyInfo.cs similarity index 78% rename from src/InstallerRunner/Properties/AssemblyInfo.cs rename to src/ProcessRunner/Properties/AssemblyInfo.cs index 2f83dae3..9d106997 100644 --- a/src/InstallerRunner/Properties/AssemblyInfo.cs +++ b/src/ProcessRunner/Properties/AssemblyInfo.cs @@ -5,12 +5,12 @@ // General Information about an assembly is controlled through the following // set of attributes. Change these attribute values to modify the information // associated with an assembly. -[assembly: AssemblyTitle("InstallerRunner")] +[assembly: AssemblyTitle("SyncTrayzor ProcessRunner")] [assembly: AssemblyDescription("")] [assembly: AssemblyConfiguration("")] [assembly: AssemblyCompany("")] -[assembly: AssemblyProduct("InstallerRunner")] -[assembly: AssemblyCopyright("Copyright © 2015")] +[assembly: AssemblyProduct("SyncTrayzor")] +[assembly: AssemblyCopyright("Copyright © Antony Male 2015")] [assembly: AssemblyTrademark("")] [assembly: AssemblyCulture("")] @@ -20,7 +20,7 @@ [assembly: ComVisible(false)] // The following GUID is for the ID of the typelib if this project is exposed to COM -[assembly: Guid("d5f01e2c-d8a1-4ce9-85ab-4e06dacd4bac")] +[assembly: Guid("36988a78-a078-4de7-b23f-8a39646ee687")] // Version information for an assembly consists of the following four values: // @@ -32,5 +32,5 @@ // You can specify all the values or you can default the Build and Revision Numbers // by using the '*' as shown below: // [assembly: AssemblyVersion("1.0.*")] -[assembly: AssemblyVersion("1.0.0.0")] -[assembly: AssemblyFileVersion("1.0.0.0")] +[assembly: AssemblyVersion("1.0.14.0")] +[assembly: AssemblyFileVersion("1.0.14.0")] diff --git a/src/SyncTrayzor.sln b/src/SyncTrayzor.sln index 4820d2eb..5b8731cc 100644 --- a/src/SyncTrayzor.sln +++ b/src/SyncTrayzor.sln @@ -5,10 +5,10 @@ VisualStudioVersion = 12.0.31101.0 MinimumVisualStudioVersion = 10.0.40219.1 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SyncTrayzor", "SyncTrayzor\SyncTrayzor.csproj", "{D1F89B3D-7967-4DC6-AE45-50A7817FE54F}" ProjectSection(ProjectDependencies) = postProject - {E33FE8E5-18A5-4D1B-A5D9-21851DC0B1D5} = {E33FE8E5-18A5-4D1B-A5D9-21851DC0B1D5} + {692BB2F9-CD24-482F-B13A-4335F27F6EC2} = {692BB2F9-CD24-482F-B13A-4335F27F6EC2} EndProjectSection EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "InstallerRunner", "InstallerRunner\InstallerRunner.csproj", "{E33FE8E5-18A5-4D1B-A5D9-21851DC0B1D5}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ProcessRunner", "ProcessRunner\ProcessRunner.csproj", "{692BB2F9-CD24-482F-B13A-4335F27F6EC2}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -26,14 +26,14 @@ Global {D1F89B3D-7967-4DC6-AE45-50A7817FE54F}.Release|x64.Build.0 = Release|x64 {D1F89B3D-7967-4DC6-AE45-50A7817FE54F}.Release|x86.ActiveCfg = Release|x86 {D1F89B3D-7967-4DC6-AE45-50A7817FE54F}.Release|x86.Build.0 = Release|x86 - {E33FE8E5-18A5-4D1B-A5D9-21851DC0B1D5}.Debug|x64.ActiveCfg = Debug|x64 - {E33FE8E5-18A5-4D1B-A5D9-21851DC0B1D5}.Debug|x64.Build.0 = Debug|x64 - {E33FE8E5-18A5-4D1B-A5D9-21851DC0B1D5}.Debug|x86.ActiveCfg = Debug|x86 - {E33FE8E5-18A5-4D1B-A5D9-21851DC0B1D5}.Debug|x86.Build.0 = Debug|x86 - {E33FE8E5-18A5-4D1B-A5D9-21851DC0B1D5}.Release|x64.ActiveCfg = Release|x64 - {E33FE8E5-18A5-4D1B-A5D9-21851DC0B1D5}.Release|x64.Build.0 = Release|x64 - {E33FE8E5-18A5-4D1B-A5D9-21851DC0B1D5}.Release|x86.ActiveCfg = Release|x86 - {E33FE8E5-18A5-4D1B-A5D9-21851DC0B1D5}.Release|x86.Build.0 = Release|x86 + {692BB2F9-CD24-482F-B13A-4335F27F6EC2}.Debug|x64.ActiveCfg = Debug|x64 + {692BB2F9-CD24-482F-B13A-4335F27F6EC2}.Debug|x64.Build.0 = Debug|x64 + {692BB2F9-CD24-482F-B13A-4335F27F6EC2}.Debug|x86.ActiveCfg = Debug|x86 + {692BB2F9-CD24-482F-B13A-4335F27F6EC2}.Debug|x86.Build.0 = Debug|x86 + {692BB2F9-CD24-482F-B13A-4335F27F6EC2}.Release|x64.ActiveCfg = Release|x64 + {692BB2F9-CD24-482F-B13A-4335F27F6EC2}.Release|x64.Build.0 = Release|x64 + {692BB2F9-CD24-482F-B13A-4335F27F6EC2}.Release|x86.ActiveCfg = Release|x86 + {692BB2F9-CD24-482F-B13A-4335F27F6EC2}.Release|x86.Build.0 = Release|x86 EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/src/SyncTrayzor/App.config b/src/SyncTrayzor/App.config index c5409c55..c46ebff8 100644 --- a/src/SyncTrayzor/App.config +++ b/src/SyncTrayzor/App.config @@ -73,6 +73,9 @@ + + 600 + @@ -81,11 +84,11 @@ fileName="${gdc:item=LogFilePath}\SyncTrayzor.log" createDirs="True" archiveFileName="${gdc:item=LogFilePath}\logs archive\SyncTrayzor.{#}.log" - archiveAboveSize="10485760" + archiveAboveSize="52428800" archiveEvery="Day" archiveNumbering="Sequence" - maxArchiveFiles="5" - AutoFlush="True" + maxArchiveFiles="1" + autoFlush="True" layout="${longdate} [${level}] ${logger}: ${message} ${exception:format=type,message,method,stacktrace,tostring:maxInnerExceptionLevel=10:innerFormat=shortType,message,method}"/> @@ -93,10 +96,10 @@ fileName="${gdc:item=LogFilePath}\syncthing.log" createDirs="True" archiveFileName="${gdc:item=LogFilePath}\logs archive\syncthing.{#}.log" - archiveAboveSize="10485760" + archiveAboveSize="52428800" archiveEvery="Day" archiveNumbering="Sequence" - maxArchiveFiles="5" + maxArchiveFiles="1" layout="${message}"/> diff --git a/src/SyncTrayzor/Icons/default_tray.ico b/src/SyncTrayzor/Icons/default_tray.ico index 3958cbe5..c38a398e 100644 Binary files a/src/SyncTrayzor/Icons/default_tray.ico and b/src/SyncTrayzor/Icons/default_tray.ico differ diff --git a/src/SyncTrayzor/Icons/stopped.ico b/src/SyncTrayzor/Icons/stopped.ico index 4f1f937f..4b533f6e 100644 Binary files a/src/SyncTrayzor/Icons/stopped.ico and b/src/SyncTrayzor/Icons/stopped.ico differ diff --git a/src/SyncTrayzor/Icons/syncing_1.svg b/src/SyncTrayzor/Icons/syncing_1.svg deleted file mode 100644 index 83908269..00000000 --- a/src/SyncTrayzor/Icons/syncing_1.svg +++ /dev/null @@ -1,135 +0,0 @@ - - - -image/svg+xml \ No newline at end of file diff --git a/src/SyncTrayzor/Icons/syncing_2.ico b/src/SyncTrayzor/Icons/syncing_2.ico index cea9e8a9..4ce5cde1 100644 Binary files a/src/SyncTrayzor/Icons/syncing_2.ico and b/src/SyncTrayzor/Icons/syncing_2.ico differ diff --git a/src/SyncTrayzor/Icons/syncing_3.ico b/src/SyncTrayzor/Icons/syncing_3.ico index c00d737e..5c1333d0 100644 Binary files a/src/SyncTrayzor/Icons/syncing_3.ico and b/src/SyncTrayzor/Icons/syncing_3.ico differ diff --git a/src/SyncTrayzor/Icons/syncing_4.ico b/src/SyncTrayzor/Icons/syncing_4.ico index 6036625c..b98158be 100644 Binary files a/src/SyncTrayzor/Icons/syncing_4.ico and b/src/SyncTrayzor/Icons/syncing_4.ico differ diff --git a/src/SyncTrayzor/NotifyIcon/NotifyIconResolutionUtilities.cs b/src/SyncTrayzor/NotifyIcon/NotifyIconResolutionUtilities.cs new file mode 100644 index 00000000..104e45dc --- /dev/null +++ b/src/SyncTrayzor/NotifyIcon/NotifyIconResolutionUtilities.cs @@ -0,0 +1,175 @@ +using Hardcodet.Wpf.TaskbarNotification; +using System; +using System.Collections.Generic; +using System.Drawing; +using System.Linq; +using System.Runtime.InteropServices; +using System.Text; +using System.Threading.Tasks; +using System.Windows; +using System.Windows.Media; +using System.Windows.Resources; + +namespace SyncTrayzor.NotifyIcon +{ + public class NotifyIconResolutionUtilities + { + public static ImageSource GetIconSource(DependencyObject obj) + { + return (ImageSource)obj.GetValue(IconSourceProperty); + } + + public static void SetIconSource(DependencyObject obj, ImageSource value) + { + obj.SetValue(IconSourceProperty, value); + } + + // Using a DependencyProperty as the backing store for IconSource. This enables animation, styling, binding, etc... + public static readonly DependencyProperty IconSourceProperty = + DependencyProperty.RegisterAttached("IconSource", typeof(ImageSource), typeof(NotifyIconResolutionUtilities), new PropertyMetadata(null, (d, e) => + { + var taskbarIcon = d as TaskbarIcon; + var imageSource = e.NewValue as ImageSource; + + if (taskbarIcon == null) + return; + + SetIconFromImageSource(taskbarIcon, imageSource); + })); + + + private static void SetIconFromImageSource(TaskbarIcon taskbarIcon, ImageSource imageSource) + { + // Copied from Hardcodet.Wpf.TaskbarNotification.Util.ToIcon + Icon icon = null; + if (imageSource != null) + { + Uri uri = new Uri(imageSource.ToString()); + StreamResourceInfo streamInfo = Application.GetResourceStream(uri); + + if (streamInfo == null) + { + string msg = "The supplied image source '{0}' could not be resolved."; + msg = String.Format(msg, imageSource); + throw new ArgumentException(msg); + } + + icon = new Icon(streamInfo.Stream, idealIconSize, idealIconSize); + } + + taskbarIcon.Icon = icon; + } + + private static int idealIconSize; + + static NotifyIconResolutionUtilities() + { + idealIconSize = GetSystemMetrics(SystemMetric.SM_CXSMICON); + } + + [DllImport("user32.dll")] + private static extern int GetSystemMetrics(SystemMetric smIndex); + + private enum SystemMetric + { + SM_CXSCREEN = 0, // 0x00 + SM_CYSCREEN = 1, // 0x01 + SM_CXVSCROLL = 2, // 0x02 + SM_CYHSCROLL = 3, // 0x03 + SM_CYCAPTION = 4, // 0x04 + SM_CXBORDER = 5, // 0x05 + SM_CYBORDER = 6, // 0x06 + SM_CXDLGFRAME = 7, // 0x07 + SM_CXFIXEDFRAME = 7, // 0x07 + SM_CYDLGFRAME = 8, // 0x08 + SM_CYFIXEDFRAME = 8, // 0x08 + SM_CYVTHUMB = 9, // 0x09 + SM_CXHTHUMB = 10, // 0x0A + SM_CXICON = 11, // 0x0B + SM_CYICON = 12, // 0x0C + SM_CXCURSOR = 13, // 0x0D + SM_CYCURSOR = 14, // 0x0E + SM_CYMENU = 15, // 0x0F + SM_CXFULLSCREEN = 16, // 0x10 + SM_CYFULLSCREEN = 17, // 0x11 + SM_CYKANJIWINDOW = 18, // 0x12 + SM_MOUSEPRESENT = 19, // 0x13 + SM_CYVSCROLL = 20, // 0x14 + SM_CXHSCROLL = 21, // 0x15 + SM_DEBUG = 22, // 0x16 + SM_SWAPBUTTON = 23, // 0x17 + SM_CXMIN = 28, // 0x1C + SM_CYMIN = 29, // 0x1D + SM_CXSIZE = 30, // 0x1E + SM_CYSIZE = 31, // 0x1F + SM_CXSIZEFRAME = 32, // 0x20 + SM_CXFRAME = 32, // 0x20 + SM_CYSIZEFRAME = 33, // 0x21 + SM_CYFRAME = 33, // 0x21 + SM_CXMINTRACK = 34, // 0x22 + SM_CYMINTRACK = 35, // 0x23 + SM_CXDOUBLECLK = 36, // 0x24 + SM_CYDOUBLECLK = 37, // 0x25 + SM_CXICONSPACING = 38, // 0x26 + SM_CYICONSPACING = 39, // 0x27 + SM_MENUDROPALIGNMENT = 40, // 0x28 + SM_PENWINDOWS = 41, // 0x29 + SM_DBCSENABLED = 42, // 0x2A + SM_CMOUSEBUTTONS = 43, // 0x2B + SM_SECURE = 44, // 0x2C + SM_CXEDGE = 45, // 0x2D + SM_CYEDGE = 46, // 0x2E + SM_CXMINSPACING = 47, // 0x2F + SM_CYMINSPACING = 48, // 0x30 + SM_CXSMICON = 49, // 0x31 + SM_CYSMICON = 50, // 0x32 + SM_CYSMCAPTION = 51, // 0x33 + SM_CXSMSIZE = 52, // 0x34 + SM_CYSMSIZE = 53, // 0x35 + SM_CXMENUSIZE = 54, // 0x36 + SM_CYMENUSIZE = 55, // 0x37 + SM_ARRANGE = 56, // 0x38 + SM_CXMINIMIZED = 57, // 0x39 + SM_CYMINIMIZED = 58, // 0x3A + SM_CXMAXTRACK = 59, // 0x3B + SM_CYMAXTRACK = 60, // 0x3C + SM_CXMAXIMIZED = 61, // 0x3D + SM_CYMAXIMIZED = 62, // 0x3E + SM_NETWORK = 63, // 0x3F + SM_CLEANBOOT = 67, // 0x43 + SM_CXDRAG = 68, // 0x44 + SM_CYDRAG = 69, // 0x45 + SM_SHOWSOUNDS = 70, // 0x46 + SM_CXMENUCHECK = 71, // 0x47 + SM_CYMENUCHECK = 72, // 0x48 + SM_SLOWMACHINE = 73, // 0x49 + SM_MIDEASTENABLED = 74, // 0x4A + SM_MOUSEWHEELPRESENT = 75, // 0x4B + SM_XVIRTUALSCREEN = 76, // 0x4C + SM_YVIRTUALSCREEN = 77, // 0x4D + SM_CXVIRTUALSCREEN = 78, // 0x4E + SM_CYVIRTUALSCREEN = 79, // 0x4F + SM_CMONITORS = 80, // 0x50 + SM_SAMEDISPLAYFORMAT = 81, // 0x51 + SM_IMMENABLED = 82, // 0x52 + SM_CXFOCUSBORDER = 83, // 0x53 + SM_CYFOCUSBORDER = 84, // 0x54 + SM_TABLETPC = 86, // 0x56 + SM_MEDIACENTER = 87, // 0x57 + SM_STARTER = 88, // 0x58 + SM_SERVERR2 = 89, // 0x59 + SM_MOUSEHORIZONTALWHEELPRESENT = 91, // 0x5B + SM_CXPADDEDBORDER = 92, // 0x5C + SM_DIGITIZER = 94, // 0x5E + SM_MAXIMUMTOUCHES = 95, // 0x5F + + SM_REMOTESESSION = 0x1000, // 0x1000 + SM_SHUTTINGDOWN = 0x2000, // 0x2000 + SM_REMOTECONTROL = 0x2001, // 0x2001 + + + SM_CONVERTABLESLATEMODE = 0x2003, + SM_SYSTEMDOCKED = 0x2004, + } + } +} diff --git a/src/SyncTrayzor/NotifyIcon/NotifyIconViewModel.cs b/src/SyncTrayzor/NotifyIcon/NotifyIconViewModel.cs index 524ec205..e2f368cb 100644 --- a/src/SyncTrayzor/NotifyIcon/NotifyIconViewModel.cs +++ b/src/SyncTrayzor/NotifyIcon/NotifyIconViewModel.cs @@ -1,5 +1,6 @@ using Stylet; using SyncTrayzor.Pages; +using SyncTrayzor.Services; using SyncTrayzor.SyncThing; using SyncTrayzor.Utils; using System; @@ -17,6 +18,7 @@ public class NotifyIconViewModel : PropertyChangedBase private readonly IWindowManager windowManager; private readonly ISyncThingManager syncThingManager; private readonly Func settingsViewModelFactory; + private readonly IProcessStartProvider processStartProvider; public bool Visible { get; set; } public bool MainWindowVisible { get; set; } @@ -30,7 +32,7 @@ public class NotifyIconViewModel : PropertyChangedBase public bool SyncThingStarted { - get { return this.SyncThingState != SyncThingState.Stopped; } + get { return this.SyncThingState == SyncThingState.Running; } } public bool SyncThingSyncing { get; private set; } @@ -38,11 +40,13 @@ public bool SyncThingStarted public NotifyIconViewModel( IWindowManager windowManager, ISyncThingManager syncThingManager, - Func settingsViewModelFactory) + Func settingsViewModelFactory, + IProcessStartProvider processStartProvider) { this.windowManager = windowManager; this.syncThingManager = syncThingManager; this.settingsViewModelFactory = settingsViewModelFactory; + this.processStartProvider = processStartProvider; this.syncThingManager.StateChanged += (o, e) => { @@ -61,7 +65,7 @@ public NotifyIconViewModel( this.syncThingManager.DataLoaded += (o, e) => { this.Folders = new BindableCollection(this.syncThingManager.FetchAllFolders() - .Select(x => new FolderViewModel(x))); + .Select(x => new FolderViewModel(x, this.processStartProvider))); }; } @@ -144,11 +148,14 @@ private void OnExitRequested() public class FolderViewModel : ICommand { private readonly Folder folder; + private readonly IProcessStartProvider processStartProvider; + public string FolderId { get { return this.folder.FolderId; } } - public FolderViewModel(Folder folder) + public FolderViewModel(Folder folder, IProcessStartProvider processStartProvider) { this.folder = folder; + this.processStartProvider = processStartProvider; } public event EventHandler CanExecuteChanged { add { } remove { } } @@ -156,7 +163,7 @@ public event EventHandler CanExecuteChanged { add { } remove { } } public void Execute(object parameter) { - Process.Start("explorer.exe", this.folder.Path); + this.processStartProvider.StartDetached("explorer.exe", this.folder.Path); } } } diff --git a/src/SyncTrayzor/NotifyIcon/TaskbarIconResources.xaml b/src/SyncTrayzor/NotifyIcon/TaskbarIconResources.xaml index 2e2401c0..82e8ed8d 100644 --- a/src/SyncTrayzor/NotifyIcon/TaskbarIconResources.xaml +++ b/src/SyncTrayzor/NotifyIcon/TaskbarIconResources.xaml @@ -2,6 +2,7 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="https://github.com/canton7/Stylet" xmlns:l="clr-namespace:SyncTrayzor.Localization" + xmlns:ni="clr-namespace:SyncTrayzor.NotifyIcon" xmlns:tb="http://www.hardcodet.net/taskbar"> @@ -16,7 +17,7 @@ MenuActivation="RightClick"> - + @@ -44,10 +45,10 @@