├── .gitignore ├── PhpVersionSwitcher.csproj ├── PhpVersionSwitcher.json.sample ├── PhpVersionSwitcher.sln ├── Properties ├── AssemblyInfo.cs ├── Resources.Designer.cs └── Resources.resx ├── app.manifest ├── docs └── assets │ └── screenshot.png ├── packages.config ├── readme.md ├── resources ├── Icon_main.ico ├── Icon_started.ico ├── Icon_stopped.ico ├── Restart.png ├── Start.png └── Stop.png └── src ├── app ├── IProcessManager.cs ├── ProcessManager.cs ├── Program.cs ├── ServiceManager.cs ├── Symlinks.cs ├── Version.cs └── VersionsManager.cs ├── config ├── Config.cs └── ConfigLoader.cs ├── exceptions └── ProcessException.cs └── ui ├── MainForm.Designer.cs ├── MainForm.cs ├── ProcessMenu.cs ├── ProcessMenuGroup.cs ├── WaitingForm.Designer.cs └── WaitingForm.cs /.gitignore: -------------------------------------------------------------------------------- 1 | /PhpVersionSwitcher.json 2 | 3 | ## Ignore Visual Studio temporary files, build results, and 4 | ## files generated by popular Visual Studio add-ons. 5 | 6 | # User-specific files 7 | *.suo 8 | *.user 9 | *.sln.docstates 10 | 11 | # Build results 12 | [Dd]ebug/ 13 | [Dd]ebugPublic/ 14 | [Rr]elease/ 15 | [Rr]eleases/ 16 | x64/ 17 | x86/ 18 | build/ 19 | bld/ 20 | [Bb]in/ 21 | [Oo]bj/ 22 | 23 | # Roslyn cache directories 24 | *.ide/ 25 | 26 | # MSTest test Results 27 | [Tt]est[Rr]esult*/ 28 | [Bb]uild[Ll]og.* 29 | 30 | #NUNIT 31 | *.VisualState.xml 32 | TestResult.xml 33 | 34 | # Build Results of an ATL Project 35 | [Dd]ebugPS/ 36 | [Rr]eleasePS/ 37 | dlldata.c 38 | 39 | *_i.c 40 | *_p.c 41 | *_i.h 42 | *.ilk 43 | *.meta 44 | *.obj 45 | *.pch 46 | *.pdb 47 | *.pgc 48 | *.pgd 49 | *.rsp 50 | *.sbr 51 | *.tlb 52 | *.tli 53 | *.tlh 54 | *.tmp 55 | *.tmp_proj 56 | *.log 57 | *.vspscc 58 | *.vssscc 59 | .builds 60 | *.pidb 61 | *.svclog 62 | *.scc 63 | 64 | # Chutzpah Test files 65 | _Chutzpah* 66 | 67 | # Visual C++ cache files 68 | ipch/ 69 | *.aps 70 | *.ncb 71 | *.opensdf 72 | *.sdf 73 | *.cachefile 74 | 75 | # Visual Studio profiler 76 | *.psess 77 | *.vsp 78 | *.vspx 79 | 80 | # TFS 2012 Local Workspace 81 | $tf/ 82 | 83 | # Guidance Automation Toolkit 84 | *.gpState 85 | 86 | # ReSharper is a .NET coding add-in 87 | _ReSharper*/ 88 | *.[Rr]e[Ss]harper 89 | *.DotSettings.user 90 | 91 | # JustCode is a .NET coding addin-in 92 | .JustCode 93 | 94 | # TeamCity is a build add-in 95 | _TeamCity* 96 | 97 | # DotCover is a Code Coverage Tool 98 | *.dotCover 99 | 100 | # NCrunch 101 | _NCrunch_* 102 | .*crunch*.local.xml 103 | 104 | # MightyMoose 105 | *.mm.* 106 | AutoTest.Net/ 107 | 108 | # Web workbench (sass) 109 | .sass-cache/ 110 | 111 | # Installshield output folder 112 | [Ee]xpress/ 113 | 114 | # DocProject is a documentation generator add-in 115 | DocProject/buildhelp/ 116 | DocProject/Help/*.HxT 117 | DocProject/Help/*.HxC 118 | DocProject/Help/*.hhc 119 | DocProject/Help/*.hhk 120 | DocProject/Help/*.hhp 121 | DocProject/Help/Html2 122 | DocProject/Help/html 123 | 124 | # Click-Once directory 125 | publish/ 126 | 127 | # Publish Web Output 128 | *.[Pp]ublish.xml 129 | *.azurePubxml 130 | # TODO: Comment the next line if you want to checkin your web deploy settings 131 | # but database connection strings (with potential passwords) will be unencrypted 132 | *.pubxml 133 | 134 | # NuGet Packages 135 | *.nupkg 136 | # The packages folder can be ignored because of Package Restore 137 | **/packages/* 138 | # except build/, which is used as an MSBuild target. 139 | !**/packages/build/ 140 | # If using the old MSBuild-Integrated Package Restore, uncomment this: 141 | #!**/packages/repositories.config 142 | 143 | # Windows Azure Build Output 144 | csx/ 145 | *.build.csdef 146 | 147 | # Windows Store app package directory 148 | AppPackages/ 149 | 150 | # Others 151 | sql/ 152 | *.Cache 153 | ClientBin/ 154 | [Ss]tyle[Cc]op.* 155 | ~$* 156 | *~ 157 | *.dbmdl 158 | *.dbproj.schemaview 159 | *.pfx 160 | *.publishsettings 161 | node_modules/ 162 | 163 | # RIA/Silverlight projects 164 | Generated_Code/ 165 | 166 | # Backup & report files from converting an old project file 167 | # to a newer Visual Studio version. Backup files are not needed, 168 | # because we have git ;-) 169 | _UpgradeReport_Files/ 170 | Backup*/ 171 | UpgradeLog*.XML 172 | UpgradeLog*.htm 173 | 174 | # SQL Server files 175 | *.mdf 176 | *.ldf 177 | 178 | # Business Intelligence projects 179 | *.rdl.data 180 | *.bim.layout 181 | *.bim_*.settings 182 | 183 | # Microsoft Fakes 184 | FakesAssemblies/ 185 | -------------------------------------------------------------------------------- /PhpVersionSwitcher.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | Debug 5 | x64 6 | 8.0.30703 7 | 2.0 8 | {42DB4255-BD78-4FDA-A1C8-C04096BB6685} 9 | WinExe 10 | Properties 11 | PhpVersionSwitcher 12 | PhpVersionSwitcher 13 | v4.5.2 14 | 512 15 | 16 | false 17 | publish\ 18 | true 19 | Disk 20 | false 21 | Foreground 22 | 7 23 | Days 24 | false 25 | false 26 | true 27 | 0 28 | 1.0.0.%2a 29 | false 30 | true 31 | true 32 | 33 | 34 | app.manifest 35 | 36 | 37 | resources\Icon_main.ico 38 | 39 | 40 | x64 41 | bin\x64\Debug\ 42 | false 43 | true 44 | 45 | 46 | x64 47 | bin\x64\Release\ 48 | false 49 | true 50 | false 51 | false 52 | 53 | 54 | 7483359A1513A1C7AD9C4459AB431030C9A3FB65 55 | 56 | 57 | PhpVersionSwitcher_TemporaryKey.pfx 58 | 59 | 60 | false 61 | 62 | 63 | false 64 | 65 | 66 | LocalIntranet 67 | 68 | 69 | PhpVersionSwitcher.Program 70 | 71 | 72 | false 73 | 74 | 75 | 76 | packages\Newtonsoft.Json.8.0.3\lib\net45\Newtonsoft.Json.dll 77 | True 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | True 88 | True 89 | Resources.resx 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | Component 98 | 99 | 100 | 101 | Form 102 | 103 | 104 | MainForm.cs 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | component 113 | 114 | 115 | Form 116 | 117 | 118 | WaitingForm.cs 119 | 120 | 121 | 122 | 123 | PreserveNewest 124 | 125 | 126 | PreserveNewest 127 | 128 | 129 | PreserveNewest 130 | 131 | 132 | 133 | 134 | False 135 | Microsoft .NET Framework 4 %28x86 and x64%29 136 | true 137 | 138 | 139 | False 140 | .NET Framework 3.5 SP1 Client Profile 141 | false 142 | 143 | 144 | False 145 | .NET Framework 3.5 SP1 146 | false 147 | 148 | 149 | False 150 | Windows Installer 3.1 151 | true 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | ResXFileCodeGenerator 166 | Resources.Designer.cs 167 | 168 | 169 | 170 | 171 | 172 | 173 | 180 | -------------------------------------------------------------------------------- /PhpVersionSwitcher.json.sample: -------------------------------------------------------------------------------- 1 | { 2 | "phpDir": "C:\\web\\php", 3 | "services": [ 4 | { 5 | "label": "Apache 2.4", 6 | "name": "Apache2.4" 7 | }, 8 | { 9 | "label": "MySQL 5.6", 10 | "name": "MySQL" 11 | } 12 | ], 13 | "executables": [ 14 | // { 15 | // "label": "Nginx 1.9", 16 | // "path": "C:\\web\\nginx\\nginx.exe" 17 | // }, 18 | // { 19 | // "label": "PHP FastCGI", 20 | // "path": "C:\\web\\php\\active\\php-cgi.exe", 21 | // "multiple": [ 22 | // {"args": "-b 127.0.0.1:9300", "label": "PHP FastCGI (9300)"}, 23 | // {"args": "-b 127.0.0.1:9301", "label": "PHP FastCGI (9301)"}, 24 | // {"args": "-b 127.0.0.1:9302", "label": "PHP FastCGI (9302)"}, 25 | // {"args": "-b 127.0.0.1:9303", "label": "PHP FastCGI (9303)"}, 26 | // {"args": "-b 127.0.0.1:9304", "label": "PHP FastCGI (9304)"}, 27 | // {"args": "-b 127.0.0.1:9305", "label": "PHP FastCGI (9305)"}, 28 | // {"args": "-b 127.0.0.1:9306", "label": "PHP FastCGI (9306)"}, 29 | // {"args": "-b 127.0.0.1:9307", "label": "PHP FastCGI (9307)"}, 30 | // {"args": "-b 127.0.0.1:9308", "label": "PHP FastCGI (9308)"}, 31 | // {"args": "-b 127.0.0.1:9309", "label": "PHP FastCGI (9309)"} 32 | // ] 33 | // }, 34 | // { 35 | // "label": "PHP built-in server", 36 | // "path": "C:\\web\\php\\active\\php.exe", 37 | // "args": "-S 127.0.0.1:9990 -t C:\\projects" 38 | // } 39 | ] 40 | } 41 | -------------------------------------------------------------------------------- /PhpVersionSwitcher.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio 2012 4 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PhpVersionSwitcher", "PhpVersionSwitcher.csproj", "{42DB4255-BD78-4FDA-A1C8-C04096BB6685}" 5 | EndProject 6 | Global 7 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 8 | Debug|x64 = Debug|x64 9 | Release|x64 = Release|x64 10 | EndGlobalSection 11 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 12 | {42DB4255-BD78-4FDA-A1C8-C04096BB6685}.Debug|x64.ActiveCfg = Debug|x64 13 | {42DB4255-BD78-4FDA-A1C8-C04096BB6685}.Debug|x64.Build.0 = Debug|x64 14 | {42DB4255-BD78-4FDA-A1C8-C04096BB6685}.Release|x64.ActiveCfg = Release|x64 15 | {42DB4255-BD78-4FDA-A1C8-C04096BB6685}.Release|x64.Build.0 = Release|x64 16 | EndGlobalSection 17 | GlobalSection(SolutionProperties) = preSolution 18 | HideSolutionNode = FALSE 19 | EndGlobalSection 20 | EndGlobal 21 | -------------------------------------------------------------------------------- /Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Runtime.CompilerServices; 3 | using System.Runtime.InteropServices; 4 | 5 | // General Information about an assembly is controlled through the following 6 | // set of attributes. Change these attribute values to modify the information 7 | // associated with an assembly. 8 | [assembly: AssemblyTitle("PhpVersionSwitcher")] 9 | [assembly: AssemblyDescription("Simple app for switching PHP versions")] 10 | [assembly: AssemblyConfiguration("")] 11 | [assembly: AssemblyCompany("")] 12 | [assembly: AssemblyProduct("PhpVersionSwitcher")] 13 | [assembly: AssemblyCopyright("Copyright © 2015 Jan Tvrdík")] 14 | [assembly: AssemblyTrademark("")] 15 | [assembly: AssemblyCulture("")] 16 | 17 | // Setting ComVisible to false makes the types in this assembly not visible 18 | // to COM components. If you need to access a type in this assembly from 19 | // COM, set the ComVisible attribute to true on that type. 20 | [assembly: ComVisible(false)] 21 | 22 | // The following GUID is for the ID of the typelib if this project is exposed to COM 23 | [assembly: Guid("9b9c9201-a073-477a-aa9d-8ec7285017f1")] 24 | 25 | // Version information for an assembly consists of the following four values: 26 | // 27 | // Major Version 28 | // Minor Version 29 | // Build Number 30 | // Revision 31 | // 32 | [assembly: AssemblyVersion("1.5.0.*")] 33 | -------------------------------------------------------------------------------- /Properties/Resources.Designer.cs: -------------------------------------------------------------------------------- 1 | //------------------------------------------------------------------------------ 2 | // 3 | // This code was generated by a tool. 4 | // Runtime Version:4.0.30319.0 5 | // 6 | // Changes to this file may cause incorrect behavior and will be lost if 7 | // the code is regenerated. 8 | // 9 | //------------------------------------------------------------------------------ 10 | 11 | namespace PhpVersionSwitcher.Properties { 12 | using System; 13 | 14 | 15 | /// 16 | /// A strongly-typed resource class, for looking up localized strings, etc. 17 | /// 18 | // This class was auto-generated by the StronglyTypedResourceBuilder 19 | // class via a tool like ResGen or Visual Studio. 20 | // To add or remove a member, edit your .ResX file then rerun ResGen 21 | // with the /str option, or rebuild your VS project. 22 | [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")] 23 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] 24 | [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] 25 | internal class Resources { 26 | 27 | private static global::System.Resources.ResourceManager resourceMan; 28 | 29 | private static global::System.Globalization.CultureInfo resourceCulture; 30 | 31 | [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] 32 | internal Resources() { 33 | } 34 | 35 | /// 36 | /// Returns the cached ResourceManager instance used by this class. 37 | /// 38 | [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] 39 | internal static global::System.Resources.ResourceManager ResourceManager { 40 | get { 41 | if (object.ReferenceEquals(resourceMan, null)) { 42 | global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("PhpVersionSwitcher.Properties.Resources", typeof(Resources).Assembly); 43 | resourceMan = temp; 44 | } 45 | return resourceMan; 46 | } 47 | } 48 | 49 | /// 50 | /// Overrides the current thread's CurrentUICulture property for all 51 | /// resource lookups using this strongly typed resource class. 52 | /// 53 | [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] 54 | internal static global::System.Globalization.CultureInfo Culture { 55 | get { 56 | return resourceCulture; 57 | } 58 | set { 59 | resourceCulture = value; 60 | } 61 | } 62 | 63 | /// 64 | /// Looks up a localized resource of type System.Drawing.Icon similar to (Icon). 65 | /// 66 | internal static System.Drawing.Icon Icon_started { 67 | get { 68 | object obj = ResourceManager.GetObject("Icon_started", resourceCulture); 69 | return ((System.Drawing.Icon)(obj)); 70 | } 71 | } 72 | 73 | /// 74 | /// Looks up a localized resource of type System.Drawing.Icon similar to (Icon). 75 | /// 76 | internal static System.Drawing.Icon Icon_stopped { 77 | get { 78 | object obj = ResourceManager.GetObject("Icon_stopped", resourceCulture); 79 | return ((System.Drawing.Icon)(obj)); 80 | } 81 | } 82 | 83 | /// 84 | /// Looks up a localized resource of type System.Drawing.Bitmap. 85 | /// 86 | internal static System.Drawing.Bitmap Restart { 87 | get { 88 | object obj = ResourceManager.GetObject("Restart", resourceCulture); 89 | return ((System.Drawing.Bitmap)(obj)); 90 | } 91 | } 92 | 93 | /// 94 | /// Looks up a localized resource of type System.Drawing.Bitmap. 95 | /// 96 | internal static System.Drawing.Bitmap Start { 97 | get { 98 | object obj = ResourceManager.GetObject("Start", resourceCulture); 99 | return ((System.Drawing.Bitmap)(obj)); 100 | } 101 | } 102 | 103 | /// 104 | /// Looks up a localized resource of type System.Drawing.Bitmap. 105 | /// 106 | internal static System.Drawing.Bitmap Stop { 107 | get { 108 | object obj = ResourceManager.GetObject("Stop", resourceCulture); 109 | return ((System.Drawing.Bitmap)(obj)); 110 | } 111 | } 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /Properties/Resources.resx: -------------------------------------------------------------------------------- 1 |  2 | 3 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | text/microsoft-resx 110 | 111 | 112 | 2.0 113 | 114 | 115 | System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 116 | 117 | 118 | System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 119 | 120 | 121 | 122 | ..\resources\Icon_started.ico;System.Drawing.Icon, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a 123 | 124 | 125 | ..\resources\Icon_stopped.ico;System.Drawing.Icon, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a 126 | 127 | 128 | ..\resources\Restart.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a 129 | 130 | 131 | ..\resources\Start.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a 132 | 133 | 134 | ..\resources\Stop.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a 135 | 136 | -------------------------------------------------------------------------------- /app.manifest: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 47 | -------------------------------------------------------------------------------- /docs/assets/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JanTvrdik/PhpVersionSwitcher/932a158e0ff5a3ea792f978f785173eb89449518/docs/assets/screenshot.png -------------------------------------------------------------------------------- /packages.config: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # PHP Version Switcher 2 | 3 | ![PHP Version Switcher screenshot](docs/assets/screenshot.png) 4 | 5 | 6 | ## Installation 7 | 8 | 1. Download and extract a [release archive](https://github.com/JanTvrdik/PhpVersionSwitcher/releases) to directory of your choice 9 | 10 | 2. Create a base directory for PHP with the following structure: 11 | ~~~ 12 | %phpDir%/ 13 | ├── configurations/ 14 | │ ├── 5.x.x.ini # php.ini options for all 5.x.x versions 15 | │ ├── 5.3.x.ini # php.ini options for all 5.3.x versions 16 | │ ├── 5.3.7.ini # php.ini options specific for 5.3.7 version 17 | │ └── ... 18 | └── versions/ 19 | ├── 5.3.7/ 20 | │ ├── ext/ 21 | │ ├── ... 22 | │ └── php.exe 23 | ├── 5.6.0-rc3/ 24 | │ ├── ext/ 25 | │ ├── ... 26 | │ └── php.exe 27 | └── ... 28 | ~~~ 29 | 30 | 3. Create php.ini files in the `configurations` directory. In all php.ini files you can use `%phpDir%` variable. This is especially useful for `zend_extension`, e.g. 31 | ~~~ini 32 | zend_extension = "%phpDir%\ext\php_opcache.dll" 33 | zend_extension = "%phpDir%\ext\php_xdebug.dll" 34 | ~~~ 35 | 36 | 4. Update `phpDir` option in `PhpVersionSwitcher.json` to contain path to the base PHP directory. 37 | 38 | 39 | ### Apache + PHP module 40 | 41 | 1. Add Apache service definition under `services` key: 42 | ~~~json 43 | { 44 | "services": [ 45 | { 46 | "label": "Apache 2.4", 47 | "name": "Apache2.4" 48 | } 49 | ] 50 | } 51 | ~~~ 52 | 53 | 2. Update Apache configuration to contain something like this: 54 | ~~~apache 55 | LoadModule php${PHP_VERSION_MAJOR}_module "C:/web/php/active/php${PHP_VERSION_MAJOR}apache2_4.dll" 56 | AddHandler application/x-httpd-php .php 57 | PHPIniDir "C:/web/php/active" 58 | ~~~ 59 | 60 | 61 | ### Nginx + PHP FastCGI 62 | 63 | 1. Add Nginx and PHP FastCGI definitions under `executables` key: 64 | ~~~json 65 | { 66 | "executables": [ 67 | { 68 | "label": "Nginx 1.9", 69 | "path": "C:\\web\\nginx\\nginx.exe" 70 | }, 71 | { 72 | "label": "PHP FastCGI", 73 | "path": "C:\\web\\php\\active\\php-cgi.exe", 74 | "multiple": [ 75 | {"args": "-b 127.0.0.1:9300", "label": "PHP FastCGI (9300)"}, 76 | {"args": "-b 127.0.0.1:9301", "label": "PHP FastCGI (9301)"}, 77 | {"args": "-b 127.0.0.1:9302", "label": "PHP FastCGI (9302)"}, 78 | {"args": "-b 127.0.0.1:9303", "label": "PHP FastCGI (9303)"}, 79 | {"args": "-b 127.0.0.1:9304", "label": "PHP FastCGI (9304)"}, 80 | {"args": "-b 127.0.0.1:9305", "label": "PHP FastCGI (9305)"}, 81 | {"args": "-b 127.0.0.1:9306", "label": "PHP FastCGI (9306)"}, 82 | {"args": "-b 127.0.0.1:9307", "label": "PHP FastCGI (9307)"}, 83 | {"args": "-b 127.0.0.1:9308", "label": "PHP FastCGI (9308)"}, 84 | {"args": "-b 127.0.0.1:9309", "label": "PHP FastCGI (9309)"} 85 | ] 86 | } 87 | ] 88 | } 89 | ~~~ 90 | 91 | 2. Update Nginx configuration to contain something like this: 92 | ~~~nginx 93 | upstream php_farm { 94 | server 127.0.0.1:9300 weight=1; 95 | server 127.0.0.1:9301 weight=1; 96 | server 127.0.0.1:9302 weight=1; 97 | server 127.0.0.1:9303 weight=1; 98 | server 127.0.0.1:9304 weight=1; 99 | server 127.0.0.1:9305 weight=1; 100 | server 127.0.0.1:9306 weight=1; 101 | server 127.0.0.1:9307 weight=1; 102 | server 127.0.0.1:9308 weight=1; 103 | server 127.0.0.1:9309 weight=1; 104 | } 105 | 106 | location ~ \.php$ { 107 | fastcgi_pass php_farm; 108 | fastcgi_index index.php; 109 | fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; 110 | include fastcgi_params; 111 | } 112 | ~~~ 113 | 114 | 115 | ### Caddy + PHP FastCGI 116 | 117 | 1. Add Caddy and PHP FastCGI definitions under `executables` key: 118 | ~~~json 119 | { 120 | "executables": [ 121 | { 122 | "label": "Caddy", 123 | "path": "C:\\web\\caddy\\caddy.exe" 124 | }, 125 | { 126 | "label": "PHP FastCGI (9300)", 127 | "path": "C:\\web\\php\\active\\php-cgi.exe", 128 | "args": "-b 127.0.0.1:9300", 129 | "env": { 130 | "PHP_FCGI_CHILDREN": "7", 131 | "PHP_FCGI_MAX_REQUESTS": "0" 132 | } 133 | } 134 | ] 135 | } 136 | ~~~ 137 | 138 | 2. Update Caddy configuration to contain something like this: 139 | ~~~nginx 140 | # https://caddyserver.com/docs/fastcgi 141 | fastcgi / 127.0.0.1:9300 php 142 | ~~~ 143 | 144 | 145 | ### PHP built-in server 146 | 147 | 1. Add definition under `executables` key: 148 | 149 | ~~~json 150 | { 151 | "executables": [ 152 | { 153 | "label": "PHP built-in server", 154 | "path": "C:\\web\\php\\active\\php.exe", 155 | "args": "-S 127.0.0.1:9990 -t C:\\projects" 156 | } 157 | ] 158 | } 159 | ~~~ 160 | 161 | 162 | ## License 163 | 164 | The MIT License (MIT) 165 | 166 | Copyright (c) 2014 Jan Tvrdík 167 | 168 | Permission is hereby granted, free of charge, to any person obtaining a copy 169 | of this software and associated documentation files (the "Software"), to deal 170 | in the Software without restriction, including without limitation the rights 171 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 172 | copies of the Software, and to permit persons to whom the Software is 173 | furnished to do so, subject to the following conditions: 174 | 175 | The above copyright notice and this permission notice shall be included in 176 | all copies or substantial portions of the Software. 177 | 178 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 179 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 180 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 181 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 182 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 183 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 184 | THE SOFTWARE. 185 | 186 | 187 | ### Icons 188 | 189 | Application icon made by [Picol](http://picol.org), state icons by [Freepik](http://www.freepik.com) 190 | from [www.flaticon.com](http://www.flaticon.com), all are modified by Jan Skrasek and licensed 191 | under [CC BY 3.0](http://creativecommons.org/licenses/by/3.0/). 192 | -------------------------------------------------------------------------------- /resources/Icon_main.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JanTvrdik/PhpVersionSwitcher/932a158e0ff5a3ea792f978f785173eb89449518/resources/Icon_main.ico -------------------------------------------------------------------------------- /resources/Icon_started.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JanTvrdik/PhpVersionSwitcher/932a158e0ff5a3ea792f978f785173eb89449518/resources/Icon_started.ico -------------------------------------------------------------------------------- /resources/Icon_stopped.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JanTvrdik/PhpVersionSwitcher/932a158e0ff5a3ea792f978f785173eb89449518/resources/Icon_stopped.ico -------------------------------------------------------------------------------- /resources/Restart.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JanTvrdik/PhpVersionSwitcher/932a158e0ff5a3ea792f978f785173eb89449518/resources/Restart.png -------------------------------------------------------------------------------- /resources/Start.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JanTvrdik/PhpVersionSwitcher/932a158e0ff5a3ea792f978f785173eb89449518/resources/Start.png -------------------------------------------------------------------------------- /resources/Stop.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JanTvrdik/PhpVersionSwitcher/932a158e0ff5a3ea792f978f785173eb89449518/resources/Stop.png -------------------------------------------------------------------------------- /src/app/IProcessManager.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | 3 | namespace PhpVersionSwitcher 4 | { 5 | internal interface IProcessManager 6 | { 7 | string Name { get; } 8 | bool IsRunning(); 9 | string GroupName { get; } 10 | Task Start(); 11 | Task Stop(); 12 | Task Restart(); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/app/ProcessManager.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics; 2 | using System.IO; 3 | using System.Threading.Tasks; 4 | using System.Management; 5 | using System; 6 | using System.Collections.Generic; 7 | 8 | namespace PhpVersionSwitcher 9 | { 10 | internal class ProcessManager : IProcessManager 11 | { 12 | public string Name { get; private set; } 13 | public string WorkingDirectory { get; private set; } 14 | public string FileName { get; private set; } 15 | public string Arguments { get; private set; } 16 | public IDictionary Env { get; private set; } 17 | public string GroupName { get; private set; } 18 | public Process Process { get; set; } 19 | 20 | public ProcessManager(string path, string arguments, IDictionary env, string name = null, string groupName = null) 21 | { 22 | var info = new FileInfo(path); 23 | this.WorkingDirectory = info.DirectoryName; 24 | this.FileName = info.Name; 25 | this.Arguments = arguments; 26 | this.Env = env; 27 | this.Name = name ?? info.Name; 28 | this.GroupName = groupName; 29 | } 30 | 31 | public bool IsRunning() 32 | { 33 | if (Process == null) 34 | { 35 | return false; 36 | } 37 | Process.Refresh(); 38 | return !Process.HasExited; 39 | } 40 | 41 | public Task Start() 42 | { 43 | return Task.Run(() => 44 | { 45 | try 46 | { 47 | var info = new ProcessStartInfo 48 | { 49 | WorkingDirectory = this.WorkingDirectory, 50 | FileName = this.FileName, 51 | Arguments = this.Arguments, 52 | CreateNoWindow = true, 53 | WindowStyle = ProcessWindowStyle.Hidden, 54 | UseShellExecute = (this.Env.Count == 0) 55 | }; 56 | 57 | foreach (var pair in this.Env) { 58 | info.EnvironmentVariables[pair.Key] = pair.Value; 59 | } 60 | 61 | Process = Process.Start(info); 62 | 63 | if (Process != null && Process.WaitForExit(1000)) 64 | { 65 | throw new ProcessException(this.Name, "start"); 66 | } 67 | } 68 | catch 69 | { 70 | throw new ProcessException(this.Name, "start"); 71 | } 72 | }); 73 | } 74 | 75 | public Task Stop() 76 | { 77 | return Task.Run(() => 78 | { 79 | if (Process == null) 80 | { 81 | return; 82 | } 83 | 84 | try 85 | { 86 | this.KillProcessAndChildren(Process.Id); 87 | if (!Process.WaitForExit(7000)) 88 | { 89 | throw new ProcessException(this.FileName, "stop"); 90 | } 91 | } 92 | catch 93 | { 94 | throw new ProcessException(this.FileName, "stop"); 95 | } 96 | }); 97 | } 98 | 99 | public async Task Restart() 100 | { 101 | await this.Stop(); 102 | await this.Start(); 103 | } 104 | 105 | 106 | private void KillProcessAndChildren(int pid) 107 | { 108 | try 109 | { 110 | Process proc = Process.GetProcessById(pid); 111 | proc.Kill(); 112 | } 113 | catch (ArgumentException) 114 | { 115 | } 116 | 117 | var searcher = new ManagementObjectSearcher("Select * From Win32_Process Where ParentProcessID=" + pid); 118 | var moc = searcher.Get(); 119 | foreach (var mo in moc) 120 | { 121 | this.KillProcessAndChildren(Convert.ToInt32(mo["ProcessID"])); 122 | } 123 | } 124 | 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /src/app/Program.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json.Linq; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Diagnostics; 5 | using System.Management; 6 | using System.Text.RegularExpressions; 7 | using System.Windows.Forms; 8 | using System.IO; 9 | using System.Linq; 10 | 11 | namespace PhpVersionSwitcher 12 | { 13 | internal static class Program 14 | { 15 | /// 16 | /// The main entry point for the application. 17 | /// 18 | [STAThread] 19 | private static void Main() 20 | { 21 | try 22 | { 23 | Application.EnableVisualStyles(); 24 | Application.SetCompatibleTextRenderingDefault(false); 25 | 26 | var processManagers = new List(); 27 | 28 | var exePath = System.Reflection.Assembly.GetEntryAssembly().Location; 29 | var configPath = exePath.Substring(0, exePath.Length - 3) + "json"; 30 | var config = new ConfigLoader().Load(configPath); 31 | 32 | config.Services?.ForEach(service => 33 | { 34 | processManagers.Add(new ServiceManager(service.Name, service.Label)); 35 | }); 36 | 37 | config.Executables?.ForEach(exe => 38 | { 39 | List processes; 40 | 41 | if (exe.Multiple == null) 42 | { 43 | processes = new List() { 44 | new ProcessManager(exe.Path, exe.Args, exe.Env, exe.Label) 45 | }; 46 | } 47 | else 48 | { 49 | processes = exe.Multiple 50 | .Select(child => new ProcessManager(child.Path, child.Args, child.Env, child.Label, exe.Label)) 51 | .ToList(); 52 | } 53 | 54 | processes.ForEach(process => processManagers.Add(process)); 55 | injectRunningProcesses(processes, processes[0].FileName); 56 | }); 57 | 58 | var phpVersions = new VersionsManager(config.PhpDir, processManagers); 59 | var waitingForm = new WaitingForm(); 60 | new MainForm(processManagers, phpVersions, waitingForm); 61 | Application.Run(); 62 | } 63 | catch (Exception ex) 64 | { 65 | MessageBox.Show("Something went wrong!\n" + ex.Message, "Fatal error", MessageBoxButtons.OK, MessageBoxIcon.Error); 66 | } 67 | } 68 | 69 | 70 | private static void injectRunningProcesses(List processManagers, string fileName) 71 | { 72 | var list = new Dictionary>>(); 73 | foreach (var processManager in processManagers) 74 | { 75 | list.Add(processManager, new List>()); 76 | } 77 | 78 | string wmiQuery = string.Format("select CommandLine, ProcessId, ParentProcessID from Win32_Process where Name='{0}'", fileName); 79 | ManagementObjectCollection managementObjects = (new ManagementObjectSearcher(wmiQuery)).Get(); 80 | foreach (ManagementObject managementObject in managementObjects) 81 | { 82 | string line = (string) (managementObject["CommandLine"]); 83 | foreach (var processManager in processManagers) 84 | { 85 | if (line != null && line.Contains(processManager.Arguments)) 86 | { 87 | var pId = Convert.ToInt32(managementObject["ProcessId"]); 88 | var parentPId = Convert.ToInt32(managementObject["ParentProcessId"]); 89 | list[processManager].Add(new Tuple(pId, parentPId)); 90 | } 91 | } 92 | } 93 | 94 | foreach (var processManager in processManagers) 95 | { 96 | int pId = -1; 97 | var pairs = list[processManager]; 98 | 99 | if (pairs.Count == 1) 100 | { 101 | pId = pairs.ToArray()[0].Item1; 102 | } 103 | else if (pairs.Count > 1) 104 | { 105 | var parentIds = new List(); 106 | foreach (var pair in pairs) 107 | { 108 | parentIds.Add(pair.Item1); 109 | } 110 | foreach (var pair in pairs) 111 | { 112 | if (!parentIds.Contains(pair.Item2)) 113 | { 114 | pId = pair.Item1; 115 | } 116 | } 117 | } 118 | 119 | if (pId == -1) 120 | { 121 | continue; 122 | } 123 | 124 | processManager.Process = Process.GetProcessById(pId); 125 | } 126 | } 127 | 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /src/app/ServiceManager.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.ServiceProcess; 3 | using System.Threading.Tasks; 4 | 5 | namespace PhpVersionSwitcher 6 | { 7 | internal class ServiceManager : IProcessManager, IDisposable 8 | { 9 | public const int WaitTime = 15; 10 | 11 | private ServiceController service; 12 | public string GroupName { get; private set; } 13 | public string Name { get; private set; } 14 | 15 | public ServiceManager(string serviceName, string label = null, string groupName = null) 16 | { 17 | this.service = new ServiceController(serviceName); 18 | this.GroupName = groupName; 19 | this.Name = label ?? serviceName; 20 | } 21 | 22 | public bool IsRunning() 23 | { 24 | return this.CheckStatus(ServiceControllerStatus.Running); 25 | } 26 | 27 | public async Task Start() 28 | { 29 | if (!await this.TrySetStatus(ServiceControllerStatus.Running, this.service.Start)) 30 | { 31 | throw new ProcessException(this.Name, "start"); 32 | } 33 | } 34 | 35 | public async Task Stop() 36 | { 37 | if (!await this.TrySetStatus(ServiceControllerStatus.Stopped, this.service.Stop)) 38 | { 39 | throw new ProcessException(this.Name, "stop"); 40 | } 41 | } 42 | 43 | public async Task Restart() 44 | { 45 | await this.Stop(); 46 | await this.Start(); 47 | } 48 | 49 | private bool CheckStatus(ServiceControllerStatus status) 50 | { 51 | try 52 | { 53 | this.service.Refresh(); 54 | return this.service.Status == status; 55 | } 56 | catch (InvalidOperationException) 57 | { 58 | return false; 59 | } 60 | } 61 | 62 | private Task TrySetStatus(ServiceControllerStatus status, Action method) 63 | { 64 | return Task.Run(() => 65 | { 66 | var timeSpan = TimeSpan.FromSeconds(WaitTime); 67 | 68 | try 69 | { 70 | method(); 71 | this.service.WaitForStatus(status, timeSpan); 72 | } 73 | catch (System.ServiceProcess.TimeoutException) { } 74 | catch (InvalidOperationException) 75 | { 76 | Task.Delay(timeSpan).Wait(); // force-wait 77 | } 78 | 79 | return this.CheckStatus(status); 80 | }); 81 | } 82 | 83 | public void Dispose() 84 | { 85 | this.service.Dispose(); 86 | } 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/app/Symlinks.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.ComponentModel; 3 | using System.Runtime.InteropServices; 4 | using System.Text; 5 | using Microsoft.Win32.SafeHandles; 6 | 7 | namespace PhpVersionSwitcher 8 | { 9 | internal static class Symlinks 10 | { 11 | private const int CreationDispositionOpenExisting = 3; 12 | private const int FileFlagBackupSemantics = 0x02000000; 13 | private const int SymbolicLinkFlagDirectory = 1; 14 | 15 | internal static class NativeMethods 16 | { 17 | // http://msdn.microsoft.com/en-us/library/aa364962(v=vs.85).aspx 18 | [DllImport("kernel32.dll", EntryPoint = "GetFinalPathNameByHandleW", CharSet = CharSet.Unicode, SetLastError = true)] 19 | internal static extern int GetFinalPathNameByHandle(IntPtr handle, [In, Out] StringBuilder path, int bufLen, int flags); 20 | 21 | // http://msdn.microsoft.com/en-us/library/aa363858(v=vs.85).aspx 22 | [DllImport("kernel32.dll", EntryPoint = "CreateFileW", CharSet = CharSet.Unicode, SetLastError = true)] 23 | internal static extern SafeFileHandle CreateFile(string lpFileName, int dwDesiredAccess, int dwShareMode, IntPtr securityAttributes, int dwCreationDisposition, int dwFlagsAndAttributes, IntPtr hTemplateFile); 24 | 25 | // http://msdn.microsoft.com/en-us/library/aa363866(v=vs.85).aspx 26 | [DllImport("kernel32.dll", EntryPoint = "CreateSymbolicLinkW", CharSet = CharSet.Unicode, SetLastError = true)] 27 | internal static extern bool CreateSymbolicLink(string lpSymlinkFileName, string lpTargetFileName, int dwFlags); 28 | } 29 | 30 | public static string GetTarget(string symlink) 31 | { 32 | SafeFileHandle fileHandle = NativeMethods.CreateFile(symlink, 0, 2, IntPtr.Zero, CreationDispositionOpenExisting, FileFlagBackupSemantics, IntPtr.Zero); 33 | if (fileHandle.IsInvalid) throw new Win32Exception(Marshal.GetLastWin32Error()); 34 | 35 | var path = new StringBuilder(512); 36 | int size = NativeMethods.GetFinalPathNameByHandle(fileHandle.DangerousGetHandle(), path, path.Capacity, 0); 37 | if (size < 0) throw new Win32Exception(Marshal.GetLastWin32Error()); 38 | 39 | // The remarks section of GetFinalPathNameByHandle mentions the return being prefixed with "\\?\" 40 | // More information about "\\?\" here -> http://msdn.microsoft.com/en-us/library/aa365247(v=vs.85).aspx 41 | var pathStr = path.ToString(); 42 | if (pathStr.StartsWith(@"\\?\")) pathStr = pathStr.Substring(4); 43 | return pathStr; 44 | } 45 | 46 | public static bool CreateDir(string symlink, string target) 47 | { 48 | return NativeMethods.CreateSymbolicLink(symlink, target, SymbolicLinkFlagDirectory); 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/app/Version.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Text; 3 | using System.Text.RegularExpressions; 4 | 5 | namespace PhpVersionSwitcher 6 | { 7 | internal class Version : IComparable, IEquatable 8 | { 9 | public enum VersionStability 10 | { 11 | Dev = 0, 12 | Alpha = 1, 13 | Beta = 2, 14 | RC = 3, 15 | Stable = 4 16 | }; 17 | 18 | public Version(int major, int minor, int patch, VersionStability stability, int stabilityVersion, string label) 19 | { 20 | this.Major = major; 21 | this.Minor = minor; 22 | this.Patch = patch; 23 | this.Stability = stability; 24 | this.StabilityVersion = stabilityVersion; 25 | this.Label = label; 26 | } 27 | 28 | public int Major { get; private set; } 29 | 30 | public int Minor { get; private set; } 31 | 32 | public int Patch { get; private set; } 33 | 34 | public string Full { get { return Major + "." + Minor + "." + Patch; } } 35 | 36 | public VersionStability Stability { get; private set; } 37 | 38 | public int StabilityVersion { get; private set; } 39 | 40 | public string Label { get; private set; } 41 | 42 | public int CompareTo(Version other) 43 | { 44 | if (this.Major != other.Major) return this.Major - other.Major; 45 | if (this.Minor != other.Minor) return this.Minor - other.Minor; 46 | if (this.Patch != other.Patch) return this.Patch - other.Patch; 47 | if (this.Stability != other.Stability) return this.Stability - other.Stability; 48 | if (this.StabilityVersion != other.StabilityVersion) return this.StabilityVersion - other.StabilityVersion; 49 | return this.Label.CompareTo(other.Label); 50 | } 51 | 52 | public bool Equals(Version other) 53 | { 54 | return other != null && this.CompareTo(other) == 0; 55 | } 56 | 57 | public static bool TryParse(string label, out Version version) 58 | { 59 | Match match = Regex.Match(label, @"^(\d+)\.(\d+)\.(\d+)(?:-(dev|(alpha|beta|rc)(\d+)))?", RegexOptions.IgnoreCase); 60 | if (!match.Success) 61 | { 62 | version = null; 63 | return false; 64 | } 65 | 66 | var major = Int32.Parse(match.Groups[1].Value); 67 | var minor = Int32.Parse(match.Groups[2].Value); 68 | var patch = Int32.Parse(match.Groups[3].Value); 69 | var stability = VersionStability.Stable; 70 | var stabilityVersion = 0; 71 | 72 | if (match.Groups[5].Success) 73 | { 74 | stability = (VersionStability) Enum.Parse(typeof(VersionStability), match.Groups[5].Value, true); 75 | stabilityVersion = Int32.Parse(match.Groups[6].Value); 76 | } 77 | else if (match.Groups[4].Success) 78 | { 79 | stability = VersionStability.Dev; 80 | } 81 | 82 | version = new Version(major, minor, patch, stability, stabilityVersion, label); 83 | return true; 84 | } 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/app/VersionsManager.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Linq; 5 | using System.Text; 6 | using System.Threading.Tasks; 7 | 8 | namespace PhpVersionSwitcher 9 | { 10 | internal class VersionsManager 11 | { 12 | private string phpBaseDir; 13 | 14 | private IEnumerable serverManagers; 15 | 16 | private IEnumerable running; 17 | 18 | private bool switchToSuccess; 19 | 20 | public VersionsManager(string phpBaseDir, IEnumerable serverManagers) 21 | { 22 | this.phpBaseDir = phpBaseDir; 23 | this.serverManagers = serverManagers; 24 | this.switchToSuccess = true; 25 | } 26 | 27 | public IEnumerable GetAvailable() 28 | { 29 | var versions = new SortedSet(); 30 | 31 | try 32 | { 33 | var dirs = Directory.EnumerateDirectories(this.VersionsDir); // may throw exception 34 | foreach (var dir in dirs) 35 | { 36 | var info = new DirectoryInfo(dir); 37 | Version version; 38 | if (File.Exists(dir + "\\php.exe") && Version.TryParse(info.Name, out version)) 39 | { 40 | versions.Add(version); 41 | } 42 | } 43 | } 44 | catch (SystemException) { } 45 | 46 | return versions; 47 | } 48 | 49 | public Version GetActive() 50 | { 51 | try 52 | { 53 | var target = Symlinks.GetTarget(this.ActivePhpDir); // may throw exception 54 | var name = new DirectoryInfo(target).Name; 55 | Version version; 56 | Version.TryParse(name, out version); 57 | return version; 58 | } 59 | catch (System.ComponentModel.Win32Exception) 60 | { 61 | return null; 62 | } 63 | } 64 | 65 | public Task SwitchTo(Version version) 66 | { 67 | return Task.Run(async () => 68 | { 69 | if (this.switchToSuccess) 70 | { 71 | this.running = this.serverManagers.Where(server => server.IsRunning()).ToArray(); 72 | } 73 | 74 | this.switchToSuccess = false; 75 | await Task.WhenAll(this.running.Select(server => server.Stop())); 76 | await Task.WhenAll( 77 | this.UpdateSymlink(version), 78 | this.UpdatePhpIni(version), 79 | this.UpdateEnvironmentVariables(version) 80 | ); 81 | await Task.WhenAll(this.running.Select(server => server.Start())); 82 | this.switchToSuccess = true; 83 | }); 84 | } 85 | 86 | public string ActivePhpDir 87 | { 88 | get { return this.phpBaseDir + "\\active"; } 89 | } 90 | 91 | private string ConfigurationDir 92 | { 93 | get { return this.phpBaseDir + "\\configurations"; } 94 | } 95 | 96 | private string VersionsDir 97 | { 98 | get { return this.phpBaseDir + "\\versions"; } 99 | } 100 | 101 | private string GetVersionDir(Version version) 102 | { 103 | return this.VersionsDir + "\\" + version.Label; 104 | } 105 | 106 | private Task UpdatePhpIni(Version version) 107 | { 108 | return Task.Run(() => 109 | { 110 | var files = new[] 111 | { 112 | version.Major + ".x.x.ini", 113 | version.Major + "." + version.Minor + ".x.ini", 114 | version.Major + "." + version.Minor + "." + version.Patch + ".ini", 115 | version.Label + ".ini" 116 | }; 117 | 118 | var dir = this.GetVersionDir(version); 119 | var ini = new StringBuilder(); 120 | foreach (var file in files.Distinct()) 121 | { 122 | var path = this.ConfigurationDir + "\\" + file; 123 | if (File.Exists(path)) 124 | { 125 | var content = File.ReadAllText(path); 126 | content = content.Replace("%phpDir%", dir); 127 | ini.AppendLine(); 128 | ini.AppendLine(content); 129 | } 130 | } 131 | 132 | File.WriteAllText(dir + "\\php.ini", ini.ToString()); 133 | }); 134 | } 135 | 136 | private Task UpdateSymlink(Version version) 137 | { 138 | return Task.Run(() => 139 | { 140 | var symlink = this.ActivePhpDir; 141 | var target = this.GetVersionDir(version); 142 | 143 | try 144 | { 145 | Directory.Delete(symlink, true); 146 | } 147 | catch (DirectoryNotFoundException) { } 148 | 149 | Symlinks.CreateDir(symlink, target); 150 | }); 151 | } 152 | 153 | private Task UpdateEnvironmentVariables(Version version) 154 | { 155 | return Task.Run(() => 156 | { 157 | var currentPath = Environment.GetEnvironmentVariable("PATH", EnvironmentVariableTarget.Machine); 158 | var segments = currentPath.Split(';'); 159 | var inPath = segments.Any((segment) => segment.Equals(this.ActivePhpDir, StringComparison.InvariantCultureIgnoreCase)); 160 | 161 | if (!inPath) 162 | { 163 | var newSegments = segments.ToList(); 164 | newSegments.Add(this.ActivePhpDir); 165 | var newPath = String.Join(";", newSegments); 166 | 167 | Environment.SetEnvironmentVariable("PATH", newPath, EnvironmentVariableTarget.Machine); 168 | } 169 | 170 | var currentMajorVersion = Environment.GetEnvironmentVariable("PHP_VERSION_MAJOR", EnvironmentVariableTarget.Machine); 171 | var newMajorVersion = version.Major.ToString(); 172 | 173 | if (newMajorVersion != currentMajorVersion) 174 | { 175 | Environment.SetEnvironmentVariable("PHP_VERSION_MAJOR", newMajorVersion, EnvironmentVariableTarget.Machine); 176 | } 177 | }); 178 | } 179 | } 180 | } 181 | -------------------------------------------------------------------------------- /src/config/Config.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace PhpVersionSwitcher 4 | { 5 | internal struct Config 6 | { 7 | public string PhpDir; 8 | public List Services; 9 | public List Executables; 10 | 11 | 12 | internal struct Service 13 | { 14 | public string Label; 15 | public string Name; 16 | } 17 | 18 | 19 | internal struct Executable 20 | { 21 | public string Label; 22 | public string Path; 23 | public string Args; 24 | public IDictionary Env; 25 | public List Multiple; 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/config/ConfigLoader.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Linq; 4 | using System.Text.RegularExpressions; 5 | using System.Collections.Generic; 6 | using Newtonsoft.Json; 7 | using Newtonsoft.Json.Linq; 8 | 9 | namespace PhpVersionSwitcher 10 | { 11 | class ConfigLoader 12 | { 13 | public Config Load(String path) 14 | { 15 | var content = File.ReadAllText(path); 16 | var contentJson = Regex.Replace(content, @"//.*$", "", RegexOptions.Multiline); // remove comments 17 | 18 | var config = new Config(); 19 | var root = (JObject) JToken.Parse(contentJson); 20 | 21 | config.PhpDir = (string) (root["phpDir"] ?? Missing(root, "phpDir")); 22 | config.Services = root["services"]?.Select(this.toService).ToList(); 23 | config.Executables = root["executables"]?.Select(this.toExecutable).ToList(); 24 | 25 | return config; 26 | } 27 | 28 | private Config.Service toService(JToken token) 29 | { 30 | var obj = (JObject) token; 31 | var service = new Config.Service(); 32 | 33 | service.Label = (string) (obj["label"] ?? Missing(obj, "label")); 34 | service.Name = (string) (obj["name"] ?? Missing(obj, "name")); 35 | 36 | return service; 37 | } 38 | 39 | private Config.Executable toExecutable(JToken token) 40 | { 41 | return this.toExecutable(token, null); 42 | } 43 | 44 | private Config.Executable toExecutable(JToken token, Config.Executable? parent) 45 | { 46 | var obj = (JObject) token; 47 | var exe = new Config.Executable(); 48 | 49 | exe.Label = (string) (obj["label"] ?? Missing(obj, "label")); 50 | exe.Path = (string) (obj["path"] ?? parent?.Path ?? Missing(obj, "path")); 51 | exe.Args = (string) (obj["args"] ?? parent?.Args ?? ""); 52 | 53 | exe.Env = parent?.Env ?? new Dictionary(); 54 | var env = (JObject) obj["env"] ?? new JObject(); 55 | foreach (var pair in env) { 56 | exe.Env.Add(pair.Key, (string) pair.Value); 57 | } 58 | 59 | exe.Multiple = obj["multiple"]?.Select(entry => this.toExecutable(entry, exe)).ToList(); 60 | 61 | return exe; 62 | } 63 | 64 | private string Missing(JToken token, string key) 65 | { 66 | throw new Exception(string.Format("Config: Missing key '{0}' in {1}.", key, token.Path)); 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/exceptions/ProcessException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace PhpVersionSwitcher 4 | { 5 | [Serializable] 6 | class ProcessException : Exception 7 | { 8 | public string Name { get; private set; } 9 | public string Operation { get; private set; } 10 | 11 | public ProcessException(string name, string operation) 12 | { 13 | this.Name = name; 14 | this.Operation = operation; 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/ui/MainForm.Designer.cs: -------------------------------------------------------------------------------- 1 | namespace PhpVersionSwitcher 2 | { 3 | internal partial class MainForm 4 | { 5 | /// 6 | /// Required designer variable. 7 | /// 8 | private System.ComponentModel.IContainer components = null; 9 | 10 | /// 11 | /// Clean up any resources being used. 12 | /// 13 | /// true if managed resources should be disposed; otherwise, false. 14 | protected override void Dispose(bool disposing) 15 | { 16 | if (disposing && (components != null)) 17 | { 18 | components.Dispose(); 19 | } 20 | base.Dispose(disposing); 21 | } 22 | 23 | #region Windows Form Designer generated code 24 | 25 | /// 26 | /// Required method for Designer support - do not modify 27 | /// the contents of this method with the code editor. 28 | /// 29 | private void InitializeComponent() 30 | { 31 | this.components = new System.ComponentModel.Container(); 32 | this.notifyIcon = new System.Windows.Forms.NotifyIcon(this.components); 33 | this.notifyIconMenu = new System.Windows.Forms.ContextMenuStrip(this.components); 34 | this.SuspendLayout(); 35 | // 36 | // notifyIcon 37 | // 38 | this.notifyIcon.ContextMenuStrip = this.notifyIconMenu; 39 | this.notifyIcon.Icon = global::PhpVersionSwitcher.Properties.Resources.Icon_stopped; 40 | this.notifyIcon.Text = "PHP Version Switcher"; 41 | this.notifyIcon.Visible = true; 42 | this.notifyIcon.MouseUp += new System.Windows.Forms.MouseEventHandler(this.notifyIcon_MouseUp); 43 | // 44 | // notifyIconMenu 45 | // 46 | this.notifyIconMenu.Name = "notifyIconMenu"; 47 | this.notifyIconMenu.Size = new System.Drawing.Size(61, 4); 48 | // 49 | // MainForm 50 | // 51 | this.ClientSize = new System.Drawing.Size(284, 262); 52 | this.Name = "MainForm"; 53 | this.Text = "PHP Version Switcher"; 54 | this.ResumeLayout(false); 55 | 56 | } 57 | 58 | #endregion 59 | 60 | private System.Windows.Forms.NotifyIcon notifyIcon; 61 | private System.Windows.Forms.ContextMenuStrip notifyIconMenu; 62 | 63 | } 64 | } 65 | 66 | -------------------------------------------------------------------------------- /src/ui/MainForm.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Reflection; 5 | using System.Threading.Tasks; 6 | using System.Windows.Forms; 7 | using PhpVersionSwitcher.Properties; 8 | 9 | namespace PhpVersionSwitcher 10 | { 11 | internal partial class MainForm : Form 12 | { 13 | private IList processManagers; 14 | private VersionsManager phpVersions; 15 | private WaitingForm waitingForm; 16 | 17 | public MainForm(IList processManagers, VersionsManager phpVersions, WaitingForm waitingForm) 18 | { 19 | this.processManagers = processManagers; 20 | this.phpVersions = phpVersions; 21 | this.waitingForm = waitingForm; 22 | 23 | this.InitializeComponent(); 24 | this.InitializeMainMenu(); 25 | } 26 | 27 | private void InitializeMainMenu() 28 | { 29 | this.notifyIconMenu.Items.Clear(); 30 | this.notifyIconMenu.Items.AddRange(this.CreateVersionsItems()); 31 | this.notifyIconMenu.Items.Add(new ToolStripSeparator()); 32 | var menuGroups = new Dictionary>(); 33 | 34 | var running = false; 35 | foreach (var pm in this.processManagers) 36 | { 37 | var menu = new ProcessMenu(pm); 38 | menu.StartItem.Click += (sender, args) => this.Attempt(pm.Name + " to start", pm.Start); 39 | menu.StopItem.Click += (sender, args) => this.Attempt(pm.Name + " to stop", pm.Stop); 40 | menu.RestartItem.Click += (sender, args) => this.Attempt(pm.Name + " to restart", pm.Restart); 41 | if (pm.IsRunning()) 42 | { 43 | running = true; 44 | } 45 | 46 | if (pm.GroupName != null) 47 | { 48 | if (!menuGroups.ContainsKey(pm.GroupName)) menuGroups.Add(pm.GroupName, new List()); 49 | menuGroups[pm.GroupName].Add(menu); 50 | } 51 | else 52 | { 53 | this.notifyIconMenu.Items.Add(menu); 54 | } 55 | } 56 | 57 | foreach (var pair in menuGroups) 58 | { 59 | var menu = new ProcessMenuGroup(pair.Key, pair.Value); 60 | var startTasks = new Func[pair.Value.Count]; 61 | var stopTasks = new Func[pair.Value.Count]; 62 | var restartTasks = new Func[pair.Value.Count]; 63 | 64 | var i = 0; 65 | foreach (var processMenu in pair.Value) 66 | { 67 | startTasks[i] = processMenu.ProcessManager.Start; 68 | stopTasks[i] = processMenu.ProcessManager.Stop; 69 | restartTasks[i] = processMenu.ProcessManager.Restart; 70 | i += 1; 71 | } 72 | 73 | menu.StartItem.Click += (sender, args) => this.Attempt(pair.Key, this.createMultiTask(startTasks)); 74 | menu.StopItem.Click += (sender, args) => this.Attempt(pair.Key, this.createMultiTask(stopTasks)); 75 | menu.RestartItem.Click += (sender, args) => this.Attempt(pair.Key, this.createMultiTask(restartTasks)); 76 | this.notifyIconMenu.Items.Add(menu); 77 | } 78 | 79 | this.notifyIconMenu.Items.Add(new ToolStripSeparator()); 80 | this.notifyIconMenu.Items.Add("Refresh", null, (sender, args) => this.InitializeMainMenu()); 81 | this.notifyIconMenu.Items.Add("Close", null, (sender, args) => Application.Exit()); 82 | this.notifyIcon.Icon = running ? Resources.Icon_started : Resources.Icon_stopped; 83 | } 84 | 85 | private ToolStripMenuItem[] CreateVersionsItems() 86 | { 87 | var activeVersion = this.phpVersions.GetActive(); 88 | var versions = this.phpVersions.GetAvailable(); 89 | 90 | var groups = versions.GroupBy(version => version.Full); 91 | var items = groups.Select(group => 92 | { 93 | var children = group.Select(version => CreateVersionItem(version, activeVersion)).ToArray(); 94 | 95 | if (children.Length == 1) 96 | { 97 | return children.First(); 98 | } 99 | else 100 | { 101 | var item = new ToolStripMenuItem(group.Key); 102 | item.DropDownItems.AddRange(children); 103 | return item; 104 | } 105 | }); 106 | 107 | return items.ToArray(); 108 | } 109 | 110 | private ToolStripMenuItem CreateVersionItem(Version version, Version activeVersion) 111 | { 112 | var item = new ToolStripMenuItem(version.Label); 113 | item.Checked = version.Equals(activeVersion); 114 | item.Click += (sender, args) => this.Attempt("PHP version to change", async () => 115 | { 116 | await this.phpVersions.SwitchTo(version); 117 | }); 118 | 119 | return item; 120 | } 121 | 122 | private async void Attempt(string description, Func action) 123 | { 124 | this.notifyIconMenu.Enabled = false; 125 | this.waitingForm.description.Text = @"Waiting for " + description + @"..."; 126 | this.waitingForm.Show(); 127 | 128 | while (true) 129 | { 130 | try 131 | { 132 | await action(); 133 | break; 134 | } 135 | catch (ProcessException ex) 136 | { 137 | var msg = "Unable to " + ex.Operation + " " + ex.Name + "."; 138 | var dialogResult = MessageBox.Show(msg, "Operation failed", MessageBoxButtons.RetryCancel, MessageBoxIcon.Error); 139 | if (dialogResult != DialogResult.Retry) break; 140 | } 141 | } 142 | 143 | this.InitializeMainMenu(); 144 | this.waitingForm.Hide(); 145 | this.notifyIconMenu.Enabled = true; 146 | } 147 | 148 | private Func createMultiTask(Func[] taskRunners) 149 | { 150 | return () => 151 | { 152 | var tasks = taskRunners.Select(fn => fn()); 153 | return Task.WhenAll(tasks); 154 | }; 155 | } 156 | 157 | private void notifyIcon_MouseUp(object sender, MouseEventArgs e) 158 | { 159 | if (e.Button == MouseButtons.Left) 160 | { 161 | // reflection hack, see http://stackoverflow.com/questions/2208690/invoke-notifyicons-context-menu 162 | MethodInfo method = typeof(NotifyIcon).GetMethod("ShowContextMenu", BindingFlags.Instance | BindingFlags.NonPublic); 163 | method.Invoke(sender, null); 164 | } 165 | } 166 | } 167 | } 168 | -------------------------------------------------------------------------------- /src/ui/ProcessMenu.cs: -------------------------------------------------------------------------------- 1 | using System.Windows.Forms; 2 | using PhpVersionSwitcher.Properties; 3 | 4 | namespace PhpVersionSwitcher 5 | { 6 | class ProcessMenu : ToolStripMenuItem 7 | { 8 | public IProcessManager ProcessManager { get; private set; } 9 | public ToolStripItem StartItem { get; private set; } 10 | public ToolStripItem StopItem { get; private set; } 11 | public ToolStripItem RestartItem { get; private set; } 12 | 13 | public ProcessMenu(IProcessManager processManager) : base(processManager.Name) 14 | { 15 | this.ProcessManager = processManager; 16 | this.StartItem = this.DropDownItems.Add("Start", Resources.Start); 17 | this.StopItem = this.DropDownItems.Add("Stop", Resources.Stop); 18 | this.RestartItem = this.DropDownItems.Add("Restart", Resources.Restart); 19 | this.Refresh(); 20 | } 21 | 22 | public void Refresh() 23 | { 24 | bool running = this.ProcessManager.IsRunning(); 25 | this.Image = running ? Resources.Start : Resources.Stop; 26 | this.StartItem.Enabled = !running; 27 | this.StopItem.Enabled = running; 28 | this.RestartItem.Enabled = running; 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/ui/ProcessMenuGroup.cs: -------------------------------------------------------------------------------- 1 | using System.Windows.Forms; 2 | using System.Collections.Generic; 3 | using PhpVersionSwitcher.Properties; 4 | 5 | namespace PhpVersionSwitcher 6 | { 7 | class ProcessMenuGroup : ToolStripMenuItem 8 | { 9 | public ToolStripItem StartItem { get; private set; } 10 | public ToolStripItem StopItem { get; private set; } 11 | public ToolStripItem RestartItem { get; private set; } 12 | public List ProcessMenus { get; private set; } 13 | 14 | public ProcessMenuGroup(string name, List processMenus) : base(name) 15 | { 16 | this.ProcessMenus = processMenus; 17 | 18 | foreach (var processMenu in processMenus) 19 | { 20 | this.DropDownItems.Add(processMenu); 21 | } 22 | 23 | this.DropDownItems.Add(new ToolStripSeparator()); 24 | this.StartItem = this.DropDownItems.Add("Start", Resources.Start); 25 | this.StopItem = this.DropDownItems.Add("Stop", Resources.Stop); 26 | this.RestartItem = this.DropDownItems.Add("Restart", Resources.Restart); 27 | this.Refresh(); 28 | } 29 | 30 | public void Refresh() 31 | { 32 | bool running = false; 33 | foreach (var processMenu in this.ProcessMenus) 34 | { 35 | if (processMenu.ProcessManager.IsRunning()) 36 | { 37 | running = true; 38 | break; 39 | } 40 | } 41 | 42 | this.Image = running ? Resources.Start : Resources.Stop; 43 | this.StartItem.Enabled = !running; 44 | this.StopItem.Enabled = running; 45 | this.RestartItem.Enabled = running; 46 | } 47 | 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/ui/WaitingForm.Designer.cs: -------------------------------------------------------------------------------- 1 | namespace PhpVersionSwitcher 2 | { 3 | internal partial class WaitingForm 4 | { 5 | /// 6 | /// Required designer variable. 7 | /// 8 | private System.ComponentModel.IContainer components = null; 9 | 10 | /// 11 | /// Clean up any resources being used. 12 | /// 13 | /// true if managed resources should be disposed; otherwise, false. 14 | protected override void Dispose(bool disposing) 15 | { 16 | if (disposing && (components != null)) 17 | { 18 | components.Dispose(); 19 | } 20 | base.Dispose(disposing); 21 | } 22 | 23 | #region Windows Form Designer generated code 24 | 25 | /// 26 | /// Required method for Designer support - do not modify 27 | /// the contents of this method with the code editor. 28 | /// 29 | private void InitializeComponent() 30 | { 31 | this.progressBar1 = new System.Windows.Forms.ProgressBar(); 32 | this.description = new System.Windows.Forms.Label(); 33 | this.SuspendLayout(); 34 | // 35 | // progressBar1 36 | // 37 | this.progressBar1.Location = new System.Drawing.Point(20, 50); 38 | this.progressBar1.MarqueeAnimationSpeed = 20; 39 | this.progressBar1.Name = "progressBar1"; 40 | this.progressBar1.Size = new System.Drawing.Size(360, 30); 41 | this.progressBar1.Style = System.Windows.Forms.ProgressBarStyle.Marquee; 42 | this.progressBar1.TabIndex = 0; 43 | this.progressBar1.UseWaitCursor = true; 44 | // 45 | // description 46 | // 47 | this.description.AutoSize = true; 48 | this.description.Font = new System.Drawing.Font("Microsoft Sans Serif", 12F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(0))); 49 | this.description.Location = new System.Drawing.Point(16, 20); 50 | this.description.Margin = new System.Windows.Forms.Padding(0); 51 | this.description.Name = "description"; 52 | this.description.Size = new System.Drawing.Size(101, 20); 53 | this.description.TabIndex = 1; 54 | this.description.Text = "Waiting for ..."; 55 | // 56 | // WaitingForm 57 | // 58 | this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F); 59 | this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; 60 | this.ClientSize = new System.Drawing.Size(400, 100); 61 | this.ControlBox = false; 62 | this.Controls.Add(this.description); 63 | this.Controls.Add(this.progressBar1); 64 | this.FormBorderStyle = System.Windows.Forms.FormBorderStyle.None; 65 | this.MaximizeBox = false; 66 | this.MinimizeBox = false; 67 | this.Name = "WaitingForm"; 68 | this.ShowInTaskbar = false; 69 | this.SizeGripStyle = System.Windows.Forms.SizeGripStyle.Hide; 70 | this.StartPosition = System.Windows.Forms.FormStartPosition.CenterScreen; 71 | this.Text = "Waiting…"; 72 | this.UseWaitCursor = true; 73 | this.ResumeLayout(false); 74 | this.PerformLayout(); 75 | 76 | } 77 | 78 | #endregion 79 | 80 | private System.Windows.Forms.ProgressBar progressBar1; 81 | public System.Windows.Forms.Label description; 82 | } 83 | } -------------------------------------------------------------------------------- /src/ui/WaitingForm.cs: -------------------------------------------------------------------------------- 1 | using System.Windows.Forms; 2 | 3 | namespace PhpVersionSwitcher 4 | { 5 | internal partial class WaitingForm : Form 6 | { 7 | public WaitingForm() 8 | { 9 | this.InitializeComponent(); 10 | } 11 | } 12 | } 13 | --------------------------------------------------------------------------------