├── .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 | 
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 |
--------------------------------------------------------------------------------