├── .gitattributes
├── .github
└── FUNDING.yml
├── .gitignore
├── Arguments.cs
├── LICENSE
├── LocalSystem
├── Msi
│ ├── Api.cs
│ ├── InstallProperty.cs
│ ├── InstalledProduct.cs
│ └── MsiException.cs
├── WinApi.cs
├── WindowsUser.cs
└── WindowsUserSidNotFound.cs
├── Log.cs
├── MainForm.Designer.cs
├── MainForm.cs
├── MainForm.resx
├── Plex
├── Api.cs
├── AppNotInstalledException.cs
├── EventSource.cs
├── MediaContainer.cs
├── MediaServer.cs
├── PlexDataFolderNotFoundException.cs
├── Registry.cs
├── ServerService.cs
├── ServiceNotInstalledException.cs
├── SilentUpdate.cs
└── Update
│ ├── CurrentVersion.cs
│ ├── Package.cs
│ ├── Release.cs
│ └── SystemType.cs
├── PlexServerAutoUpdater.csproj
├── PlexServerAutoUpdater.sln
├── Program.cs
├── Properties
└── AssemblyInfo.cs
├── README.md
├── SystemExitCodes.cs
└── app.config
/.gitattributes:
--------------------------------------------------------------------------------
1 | # Auto detect text files and perform LF normalization
2 | * text=auto
3 |
4 | # Custom for Visual Studio
5 | *.cs diff=csharp
6 |
7 | # Standard to msysgit
8 | *.doc diff=astextplain
9 | *.DOC diff=astextplain
10 | *.docx diff=astextplain
11 | *.DOCX diff=astextplain
12 | *.dot diff=astextplain
13 | *.DOT diff=astextplain
14 | *.pdf diff=astextplain
15 | *.PDF diff=astextplain
16 | *.rtf diff=astextplain
17 | *.RTF diff=astextplain
18 |
--------------------------------------------------------------------------------
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | # These are supported funding model platforms
2 |
3 | github: TechieGuy12 # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
4 | patreon: # Replace with a single Patreon username
5 | open_collective: # Replace with a single Open Collective username
6 | ko_fi: # Replace with a single Ko-fi username
7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
9 | liberapay: # Replace with a single Liberapay username
10 | issuehunt: # Replace with a single IssueHunt username
11 | otechie: # Replace with a single Otechie username
12 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
13 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | ## Ignore Visual Studio temporary files, build results, and
2 | ## files generated by popular Visual Studio add-ons.
3 |
4 | # User-specific files
5 | *.suo
6 | *.user
7 | *.userosscache
8 | *.sln.docstates
9 |
10 | # User-specific files (MonoDevelop/Xamarin Studio)
11 | *.userprefs
12 |
13 | # Build results
14 | [Dd]ebug/
15 | [Dd]ebugPublic/
16 | [Rr]elease/
17 | [Rr]eleases/
18 | x64/
19 | x86/
20 | bld/
21 | [Bb]in/
22 | [Oo]bj/
23 | [Ll]og/
24 |
25 | # Visual Studio 2015 cache/options directory
26 | .vs/
27 | # Uncomment if you have tasks that create the project's static files in wwwroot
28 | #wwwroot/
29 |
30 | # MSTest test Results
31 | [Tt]est[Rr]esult*/
32 | [Bb]uild[Ll]og.*
33 |
34 | # NUNIT
35 | *.VisualState.xml
36 | TestResult.xml
37 |
38 | # Build Results of an ATL Project
39 | [Dd]ebugPS/
40 | [Rr]eleasePS/
41 | dlldata.c
42 |
43 | # DNX
44 | project.lock.json
45 | project.fragment.lock.json
46 | artifacts/
47 |
48 | *_i.c
49 | *_p.c
50 | *_i.h
51 | *.ilk
52 | *.meta
53 | *.obj
54 | *.pch
55 | *.pdb
56 | *.pgc
57 | *.pgd
58 | *.rsp
59 | *.sbr
60 | *.tlb
61 | *.tli
62 | *.tlh
63 | *.tmp
64 | *.tmp_proj
65 | *.log
66 | *.vspscc
67 | *.vssscc
68 | .builds
69 | *.pidb
70 | *.svclog
71 | *.scc
72 |
73 | # Chutzpah Test files
74 | _Chutzpah*
75 |
76 | # Visual C++ cache files
77 | ipch/
78 | *.aps
79 | *.ncb
80 | *.opendb
81 | *.opensdf
82 | *.sdf
83 | *.cachefile
84 | *.VC.db
85 | *.VC.VC.opendb
86 |
87 | # Visual Studio profiler
88 | *.psess
89 | *.vsp
90 | *.vspx
91 | *.sap
92 |
93 | # TFS 2012 Local Workspace
94 | $tf/
95 |
96 | # Guidance Automation Toolkit
97 | *.gpState
98 |
99 | # ReSharper is a .NET coding add-in
100 | _ReSharper*/
101 | *.[Rr]e[Ss]harper
102 | *.DotSettings.user
103 |
104 | # JustCode is a .NET coding add-in
105 | .JustCode
106 |
107 | # TeamCity is a build add-in
108 | _TeamCity*
109 |
110 | # DotCover is a Code Coverage Tool
111 | *.dotCover
112 |
113 | # NCrunch
114 | _NCrunch_*
115 | .*crunch*.local.xml
116 | nCrunchTemp_*
117 |
118 | # MightyMoose
119 | *.mm.*
120 | AutoTest.Net/
121 |
122 | # Web workbench (sass)
123 | .sass-cache/
124 |
125 | # Installshield output folder
126 | [Ee]xpress/
127 |
128 | # DocProject is a documentation generator add-in
129 | DocProject/buildhelp/
130 | DocProject/Help/*.HxT
131 | DocProject/Help/*.HxC
132 | DocProject/Help/*.hhc
133 | DocProject/Help/*.hhk
134 | DocProject/Help/*.hhp
135 | DocProject/Help/Html2
136 | DocProject/Help/html
137 |
138 | # Click-Once directory
139 | publish/
140 |
141 | # Publish Web Output
142 | *.[Pp]ublish.xml
143 | *.azurePubxml
144 | # TODO: Comment the next line if you want to checkin your web deploy settings
145 | # but database connection strings (with potential passwords) will be unencrypted
146 | *.pubxml
147 | *.publishproj
148 |
149 | # Microsoft Azure Web App publish settings. Comment the next line if you want to
150 | # checkin your Azure Web App publish settings, but sensitive information contained
151 | # in these scripts will be unencrypted
152 | PublishScripts/
153 |
154 | # NuGet Packages
155 | *.nupkg
156 | # The packages folder can be ignored because of Package Restore
157 | **/packages/*
158 | # except build/, which is used as an MSBuild target.
159 | !**/packages/build/
160 | # Uncomment if necessary however generally it will be regenerated when needed
161 | #!**/packages/repositories.config
162 | # NuGet v3's project.json files produces more ignoreable files
163 | *.nuget.props
164 | *.nuget.targets
165 |
166 | # Microsoft Azure Build Output
167 | csx/
168 | *.build.csdef
169 |
170 | # Microsoft Azure Emulator
171 | ecf/
172 | rcf/
173 |
174 | # Windows Store app package directories and files
175 | AppPackages/
176 | BundleArtifacts/
177 | Package.StoreAssociation.xml
178 | _pkginfo.txt
179 |
180 | # Visual Studio cache files
181 | # files ending in .cache can be ignored
182 | *.[Cc]ache
183 | # but keep track of directories ending in .cache
184 | !*.[Cc]ache/
185 |
186 | # Others
187 | ClientBin/
188 | ~$*
189 | *~
190 | *.dbmdl
191 | *.dbproj.schemaview
192 | *.pfx
193 | *.publishsettings
194 | node_modules/
195 | orleans.codegen.cs
196 |
197 | # Since there are multiple workflows, uncomment next line to ignore bower_components
198 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
199 | #bower_components/
200 |
201 | # RIA/Silverlight projects
202 | Generated_Code/
203 |
204 | # Backup & report files from converting an old project file
205 | # to a newer Visual Studio version. Backup files are not needed,
206 | # because we have git ;-)
207 | _UpgradeReport_Files/
208 | Backup*/
209 | UpgradeLog*.XML
210 | UpgradeLog*.htm
211 |
212 | # SQL Server files
213 | *.mdf
214 | *.ldf
215 |
216 | # Business Intelligence projects
217 | *.rdl.data
218 | *.bim.layout
219 | *.bim_*.settings
220 |
221 | # Microsoft Fakes
222 | FakesAssemblies/
223 |
224 | # GhostDoc plugin setting file
225 | *.GhostDoc.xml
226 |
227 | # Node.js Tools for Visual Studio
228 | .ntvs_analysis.dat
229 |
230 | # Visual Studio 6 build log
231 | *.plg
232 |
233 | # Visual Studio 6 workspace options file
234 | *.opt
235 |
236 | # Visual Studio LightSwitch build output
237 | **/*.HTMLClient/GeneratedArtifacts
238 | **/*.DesktopClient/GeneratedArtifacts
239 | **/*.DesktopClient/ModelManifest.xml
240 | **/*.Server/GeneratedArtifacts
241 | **/*.Server/ModelManifest.xml
242 | _Pvt_Extensions
243 |
244 | # Paket dependency manager
245 | .paket/paket.exe
246 | paket-files/
247 |
248 | # FAKE - F# Make
249 | .fake/
250 |
251 | # JetBrains Rider
252 | .idea/
253 | *.sln.iml
254 |
255 | # CodeRush
256 | .cr/
--------------------------------------------------------------------------------
/Arguments.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Specialized;
3 | using System.Text.RegularExpressions;
4 |
5 | namespace TE
6 | {
7 | ///
8 | /// Description of Arguments.
9 | ///
10 | ///
11 | /// Arguments class
12 | ///
13 | public class Arguments
14 | {
15 | // Variables
16 | private StringDictionary Parameters;
17 |
18 | // Constructor
19 | public Arguments(string[] Args)
20 | {
21 | Parameters = new StringDictionary();
22 | Regex Spliter = new Regex(@"^-{1,2}|^/|=|:(?!\\)",
23 | RegexOptions.IgnoreCase | RegexOptions.Compiled);
24 |
25 | Regex Remover = new Regex(@"^['""]?(.*?)['""]?$",
26 | RegexOptions.IgnoreCase | RegexOptions.Compiled);
27 |
28 | string Parameter = null;
29 | string[] Parts;
30 |
31 | // Valid parameters forms:
32 | // {-,/,--}param{ ,=,:}((",')value(",'))
33 | // Examples:
34 | // -param1 value1 --param2 /param3:"Test-:-work"
35 | // /param4=happy -param5 '--=nice=--'
36 | foreach (string Txt in Args)
37 | {
38 | // Look for new parameters (-,/ or --) and a
39 | // possible enclosed value (=,:)
40 | Parts = Spliter.Split(Txt, 3);
41 |
42 | switch (Parts.Length)
43 | {
44 | // Found a value (for the last parameter
45 | // found (space separator))
46 | case 1:
47 | if (Parameter != null)
48 | {
49 | if (!Parameters.ContainsKey(Parameter))
50 | {
51 | Parts[0] =
52 | Remover.Replace(Parts[0], "$1");
53 |
54 | Parameters.Add(Parameter, Parts[0]);
55 | }
56 | Parameter = null;
57 | }
58 | // else Error: no parameter waiting for a value (skipped)
59 | break;
60 |
61 | // Found just a parameter
62 | case 2:
63 | // The last parameter is still waiting.
64 | // With no value, set it to true.
65 | if (Parameter != null)
66 | {
67 | if (!Parameters.ContainsKey(Parameter))
68 | Parameters.Add(Parameter, "true");
69 | }
70 | Parameter = Parts[1];
71 | break;
72 |
73 | // Parameter with enclosed value
74 | case 3:
75 | // The last parameter is still waiting.
76 | // With no value, set it to true.
77 | if (Parameter != null)
78 | {
79 | if (!Parameters.ContainsKey(Parameter))
80 | Parameters.Add(Parameter, "true");
81 | }
82 |
83 | Parameter = Parts[1];
84 |
85 | // Remove possible enclosing characters (",')
86 | if (!Parameters.ContainsKey(Parameter))
87 | {
88 | Parts[2] = Remover.Replace(Parts[2], "$1");
89 | Parameters.Add(Parameter, Parts[2]);
90 | }
91 |
92 | Parameter = null;
93 | break;
94 | }
95 | }
96 | // In case a parameter is still waiting
97 | if (Parameter != null)
98 | {
99 | if (!Parameters.ContainsKey(Parameter))
100 | Parameters.Add(Parameter, "true");
101 | }
102 | }
103 |
104 | // Retrieve a parameter value if it exists
105 | // (overriding C# indexer property)
106 | public string this[string Param]
107 | {
108 | get
109 | {
110 | return (Parameters[Param]);
111 | }
112 | }
113 | }
114 | }
115 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2022 Paul Salmon
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/LocalSystem/Msi/Api.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Runtime.InteropServices;
4 | using System.Text;
5 |
6 | namespace TE.LocalSystem.Msi
7 | {
8 | #region Enumerations
9 | ///
10 | /// All avalible MSI Setup install exit codes.
11 | ///
12 | public enum MsiExitCodes
13 | {
14 | ///
15 | /// Action completed successfully.
16 | ///
17 | ///
18 | /// ERROR_SUCCESS
19 | ///
20 | Success = 0,
21 |
22 | ///
23 | /// The data is invalid.
24 | ///
25 | ///
26 | /// ERROR_INVALID_DATA
27 | ///
28 | InvalidDataError = 13,
29 |
30 | ///
31 | /// One of the parameters was invalid.
32 | ///
33 | ///
34 | /// ERROR_INVALID_PARAMETER
35 | ///
36 | InvalidParameterError = 87,
37 |
38 | ///
39 | /// This function is not available for this platform.
40 | /// It is only available on Windows 2000 and
41 | /// Windows XP with Window Installer version 2.0.
42 | ///
43 | ///
44 | /// ERROR_CALL_NOT_IMPLEMENTED
45 | ///
46 | CallNotImplementedError = 120,
47 |
48 |
49 | MoreData = 234,
50 |
51 | ///
52 | /// This error happens when there is no more items available for enumeration
53 | ///
54 | NoMoreItems = 259,
55 |
56 |
57 | ///
58 | /// This error code only occurs when using
59 | /// Windows Installer version 2.0 and Windows XP or later.
60 | /// If Windows Installer determines a product may be incompatible
61 | /// with the current operating system,
62 | /// it displays a dialog informing the user and asking whether to try to install anyway.
63 | /// This error code is returned if the user chooses not to try the installation.
64 | ///
65 | ///
66 | /// ERROR_APPHELP_BLOCK
67 | ///
68 | ApplicationHelpBlockedError = 1259,
69 |
70 | ///
71 | /// The Windows Installer service could not be accessed.
72 | /// Contact your support personnel to verify that the
73 | /// Windows Installer service is properly registered.
74 | ///
75 | ///
76 | /// ERROR_INSTALL_SERVICE_FAILURE
77 | ///
78 | InstallServiceFailureError = 1601,
79 |
80 | ///
81 | /// User cancel installation.
82 | ///
83 | ///
84 | /// ERROR_INSTALL_USEREXIT
85 | ///
86 | UserExitError = 1602,
87 |
88 | ///
89 | /// Fatal error during installation.
90 | ///
91 | ///
92 | /// ERROR_INSTALL_FAILURE
93 | ///
94 | FatalInstallFailureError = 1603,
95 |
96 | ///
97 | /// Installation suspended, incomplete.
98 | ///
99 | ///
100 | /// ERROR_INSTALL_SUSPEND
101 | ///
102 | InstallSuspendedError = 1604,
103 |
104 | ///
105 | /// This action is only valid for products that are currently installed.
106 | ///
107 | ///
108 | /// ERROR_UNKNOWN_PRODUCT
109 | ///
110 | UnknownProductError = 1605,
111 |
112 | ///
113 | /// Feature ID not registered.
114 | ///
115 | ///
116 | /// ERROR_UNKNOWN_FEATURE
117 | ///
118 | UnknownFeatureError = 1606,
119 |
120 | ///
121 | /// Component ID not registered.
122 | ///
123 | ///
124 | /// ERROR_UNKNOWN_COMPONENT
125 | ///
126 | UnknownComponentError = 1607,
127 |
128 | ///
129 | /// Unknown property.
130 | ///
131 | ///
132 | /// ERROR_UNKNOWN_PROPERTY
133 | ///
134 | UnknownPropertyError = 1608,
135 |
136 | ///
137 | /// Handle is in an invalid state.
138 | ///
139 | ///
140 | /// ERROR_INVALID_HANDLE_STATE
141 | ///
142 | InvalidHandleStateError = 1609,
143 |
144 | ///
145 | /// The configuration data for this product is corrupt.
146 | /// Contact your support personnel.
147 | ///
148 | ///
149 | /// ERROR_BAD_CONFIGURATION
150 | ///
151 | BadConfigurationError = 1610,
152 |
153 | ///
154 | /// Component qualifier not present.
155 | ///
156 | ///
157 | /// ERROR_INDEX_ABSENT
158 | ///
159 | IndexAbsentError = 1611,
160 |
161 | ///
162 | /// The installation source for this product is not available.
163 | /// Verify that the source exists and that you can access it.
164 | ///
165 | ///
166 | /// ERROR_INSTALL_SOURCE_ABSENT
167 | ///
168 | InstallSourceAbsentError = 1612,
169 |
170 | ///
171 | /// This installation package cannot be installed by the Windows Installer service.
172 | /// You must install a Windows service pack that contains
173 | /// a newer version of the Windows Installer service.
174 | ///
175 | ///
176 | /// ERROR_INSTALL_PACKAGE_VERSION
177 | ///
178 | WrongInstallPackageVersionError = 1613,
179 |
180 | ///
181 | /// Product is uninstalled.
182 | ///
183 | ///
184 | /// ERROR_PRODUCT_UNINSTALLED
185 | ///
186 | ProductUninstalledError = 1614,
187 |
188 | ///
189 | /// SQL query syntax invalid or unsupported.
190 | ///
191 | ///
192 | /// ERROR_BAD_QUERY_SYNTAX
193 | ///
194 | BadQuerySyntaxError = 1615,
195 |
196 | ///
197 | /// Record field does not exist.
198 | ///
199 | ///
200 | /// ERROR_INVALID_FIELD
201 | ///
202 | InvalidFieldError = 1616,
203 |
204 | ///
205 | /// Another installation is already in progress.
206 | /// Complete that installation before proceeding with this install.
207 | ///
208 | ///
209 | /// ERROR_INSTALL_ALREADY_RUNNING
210 | ///
211 | InstallInProgressError = 1618,
212 |
213 | ///
214 | /// This installation package could not be opened.
215 | /// Verify that the package exists and that you can access it,
216 | /// or contact the application vendor to verify that
217 | /// this is a valid Windows Installer package.
218 | ///
219 | ///
220 | /// ERROR_INSTALL_PACKAGE_OPEN_FAILED
221 | ///
222 | InstallPackageOpenError = 1619,
223 |
224 | ///
225 | /// This installation package could not be opened.
226 | /// Contact the application vendor to verify that
227 | /// this is a valid Windows Installer package.
228 | ///
229 | ///
230 | /// ERROR_INSTALL_PACKAGE_INVALID
231 | ///
232 | InstallPackageInvalidError = 1620,
233 |
234 | ///
235 | /// There was an error starting the Windows Installer service user interface.
236 | /// Contact your support personnel.
237 | ///
238 | ///
239 | /// ERROR_INSTALL_UI_FAILURE
240 | ///
241 | InstallUIError = 1621,
242 |
243 | ///
244 | /// Error opening installation log file.
245 | /// Verify that the specified log file location exists and is writable.
246 | ///
247 | ///
248 | /// ERROR_INSTALL_LOG_FAILURE
249 | ///
250 | InstallLogError = 1622,
251 |
252 | ///
253 | /// This language of this installation package is not supported by your system.
254 | ///
255 | ///
256 | /// ERROR_INSTALL_LANGUAGE_UNSUPPORTED
257 | ///
258 | InstallLanguageUnsupportedError = 1623,
259 |
260 | ///
261 | /// Error applying transforms.
262 | /// Verify that the specified transform paths are valid.
263 | ///
264 | ///
265 | /// ERROR_INSTALL_TRANSFORM_FAILURE
266 | ///
267 | InstallTransformError = 1624,
268 |
269 | ///
270 | /// This installation is forbidden by system policy.
271 | /// Contact your system administrator.
272 | ///
273 | ///
274 | /// ERROR_INSTALL_PACKAGE_REJECTED
275 | ///
276 | InstallPackageRejectedError = 1625,
277 |
278 | ///
279 | /// Function could not be executed.
280 | ///
281 | ///
282 | /// ERROR_FUNCTION_NOT_CALLED
283 | ///
284 | FunctionNotCalledError = 1626,
285 |
286 | ///
287 | /// Function failed during execution.
288 | ///
289 | ///
290 | /// ERROR_FUNCTION_FAILED
291 | ///
292 | FunctionFailedError = 1627,
293 |
294 | ///
295 | /// Invalid or unknown table specified.
296 | ///
297 | ///
298 | /// ERROR_INVALID_TABLE
299 | ///
300 | InvalidTableError = 1628,
301 |
302 | ///
303 | /// Data supplied is of wrong type.
304 | ///
305 | ///
306 | /// ERROR_DATATYPE_MISMATCH
307 | ///
308 | DatatypeMismatchError = 1629,
309 |
310 | ///
311 | /// Data of this type is not supported.
312 | ///
313 | ///
314 | /// ERROR_UNSUPPORTED_TYPE
315 | ///
316 | UnsupportedTypeError = 1630,
317 |
318 | ///
319 | /// The Windows Installer service failed to start.
320 | /// Contact your support personnel.
321 | ///
322 | ///
323 | /// ERROR_CREATE_FAILED
324 | ///
325 | CreateFailedError = 1631,
326 |
327 | ///
328 | /// The temp folder is either full or inaccessible.
329 | /// Verify that the temp folder exists and that you can write to it.
330 | ///
331 | ///
332 | /// ERROR_INSTALL_TEMP_UNWRITABLE
333 | ///
334 | InstallTempUnwritableError = 1632,
335 |
336 | ///
337 | /// This installation package is not supported on this platform.
338 | /// Contact your application vendor.
339 | ///
340 | ///
341 | /// ERROR_INSTALL_PLATFORM_UNSUPPORTED
342 | ///
343 | InstallPlatformUnsupportedError = 1633,
344 |
345 | ///
346 | /// Component not used on this machine
347 | ///
348 | ///
349 | /// ERROR_INSTALL_NOTUSED
350 | ///
351 | InstallNotusedError = 1634,
352 |
353 | ///
354 | /// This patch package could not be opened.
355 | /// Verify that the patch package exists and that you can access it,
356 | /// or contact the application vendor to verify that
357 | /// this is a valid Windows Installer patch package.
358 | ///
359 | ///
360 | /// ERROR_PATCH_PACKAGE_OPEN_FAILED
361 | ///
362 | PatchPackageOpenFailedError = 1635,
363 |
364 | ///
365 | /// This patch package could not be opened.
366 | /// Contact the application vendor to verify that
367 | /// this is a valid Windows Installer patch package.
368 | ///
369 | ///
370 | /// ERROR_PATCH_PACKAGE_INVALID
371 | ///
372 | PatchPackageInvalidError = 1636,
373 |
374 | ///
375 | /// This patch package cannot be processed by the Windows Installer service.
376 | /// You must install a Windows service pack that contains
377 | /// a newer version of the Windows Installer service.
378 | ///
379 | ///
380 | /// ERROR_PATCH_PACKAGE_UNSUPPORTED
381 | ///
382 | PatchPackageUnsupportedError = 1637,
383 |
384 | ///
385 | /// Another version of this product is already installed.
386 | /// Installation of this version cannot continue.
387 | /// To configure or remove the existing version of this product,
388 | /// use Add/Remove Programs on the Control Panel.
389 | ///
390 | ///
391 | /// ERROR_PRODUCT_VERSION
392 | ///
393 | ProductVersionError = 1638,
394 |
395 | ///
396 | /// Invalid command line argument.
397 | /// Consult the Windows Installer SDK for detailed command line help.
398 | ///
399 | ///
400 | /// ERROR_INVALID_COMMAND_LINE
401 | ///
402 | InvalidCommandLineError = 1639,
403 |
404 | ///
405 | /// Installation from a Terminal Server client session not permitted for current user.
406 | ///
407 | ///
408 | /// ERROR_INSTALL_REMOTE_DISALLOWED
409 | ///
410 | RemoteInstallDisallowedError = 1640,
411 |
412 | ///
413 | /// The installer has started a reboot.
414 | /// This error code not available on Windows Installer version 1.0.
415 | ///
416 | ///
417 | /// ERROR_SUCCESS_REBOOT_INITIATED
418 | ///
419 | RebootSuccessInitiatedError = 1641,
420 |
421 | ///
422 | /// The installer cannot install the upgrade patch because
423 | /// the program being upgraded may be missing or the upgrade
424 | /// patch updates a different version of the program.
425 | /// Verify that the program to be upgraded exists on your
426 | /// computer and that you have the correct upgrade patch.
427 | ///
428 | ///
429 | /// ERROR_PATCH_TARGET_NOT_FOUND
430 | ///
431 | PatchTargetNotFoundError = 1642,
432 |
433 | ///
434 | /// The patch package is not permitted by system policy.
435 | /// This error code is available with Windows Installer versions 2.0 or later.
436 | ///
437 | ///
438 | /// ERROR_PATCH_PACKAGE_REJECTED
439 | ///
440 | PatchPackageRejectedError = 1643,
441 |
442 | ///
443 | /// One or more customizations are not permitted by system policy.
444 | /// This error code is available with Windows Installer versions 2.0 or later.
445 | ///
446 | ///
447 | /// ERROR_INSTALL_TRANSFORM_REJECTED
448 | ///
449 | InstallTransformRejectedError = 1644,
450 |
451 | ///
452 | /// A reboot is required to complete the install.
453 | /// This does not include installs where the ForceReboot action is run.
454 | /// This error code not available on Windows Installer version 1.0.
455 | ///
456 | ///
457 | /// ERROR_SUCCESS_REBOOT_REQUIRED
458 | ///
459 | RebootRequiredSuccessError = 3010
460 |
461 | }
462 |
463 | ///
464 | /// Install context of a product.
465 | ///
466 | public enum InstallContext : int
467 | {
468 | Node = 0,
469 | UserManaged = 1,
470 | UserUnmanaged = 2,
471 | Machine = 4,
472 | All = (UserManaged | UserUnmanaged | Machine),
473 | }
474 | #endregion
475 |
476 | ///
477 | /// Wrapper for the Windows Installer API.
478 | ///
479 | public static class Api
480 | {
481 | #region API Declarations
482 | [DllImport("msi.dll", CharSet = CharSet.Unicode)]
483 | private static extern Int32 MsiGetProductInfo(
484 | string product,
485 | string property,
486 | [Out] String valueBuf,
487 | ref Int32 len);
488 |
489 | [DllImport("msi.dll",
490 | EntryPoint = "MsiEnumProductsExW",
491 | CharSet = CharSet.Unicode,
492 | ExactSpelling = true,
493 | CallingConvention = CallingConvention.StdCall)]
494 | private static extern uint MsiEnumProductsEx(
495 | string szProductCode,
496 | string szUserSid,
497 | uint dwContext,
498 | uint dwIndex,
499 | string szInstalledProductCode,
500 | out object pdwInstalledProductContext,
501 | string szSid,
502 | ref uint pccSid);
503 |
504 | [DllImport("msi.dll", CharSet = CharSet.Unicode)]
505 | private static extern uint MsiEnumComponents(
506 | uint iComponentIndex,
507 | StringBuilder lpComponentBuf);
508 |
509 | [DllImport("msi.dll", CharSet = CharSet.Unicode)]
510 | private static extern UInt32 MsiLocateComponent(
511 | string szComponent,
512 | [Out] StringBuilder lpPathBuf,
513 | ref UInt32 pcchBuf);
514 | #endregion
515 |
516 | #region Public Functions
517 | ///
518 | /// Enumerate all installed components.
519 | ///
520 | /// A List of strings containing all component GUIDs
521 | public static List EnumerateComponents()
522 | {
523 | List guidList = new List();
524 | uint ret = 0;
525 | uint i = 0;
526 |
527 | do
528 | {
529 | // Create the guid buffer
530 | StringBuilder guid = new StringBuilder(39);
531 |
532 | // Get a component GUID
533 | ret = Api.MsiEnumComponents(i, guid);
534 |
535 | // If the return code indicates a success, then add the GUID
536 | // to the list
537 | if (ret == 0)
538 | {
539 | guidList.Add(guid.ToString());
540 | }
541 |
542 | // Increment the counter
543 | i++;
544 |
545 | } while (ret != (uint)MsiExitCodes.NoMoreItems);
546 |
547 | return guidList;
548 | }
549 |
550 | ///
551 | /// Enumerate all installed products.
552 | ///
553 | /// A List of strings containing all GUIDs
554 | public static List EnumerateProducts()
555 | {
556 | var guidList = new List();
557 | uint ret = 0, i = 0, dummy2 = 0;
558 | do
559 | {
560 | string guid = new string(new char[39]);
561 | object dummy1;
562 | ret = Api.MsiEnumProductsEx(null, null, (uint)InstallContext.All, i, guid, out dummy1, null, ref dummy2);
563 | if (ret == 0)
564 | {
565 | guidList.Add(guid);
566 | }
567 | i++;
568 | } while (ret != (uint)MsiExitCodes.NoMoreItems);
569 |
570 | return guidList;
571 | }
572 |
573 | ///
574 | /// Gets the path of the component using a GUID.
575 | ///
576 | ///
577 | /// The GUID of the component.
578 | ///
579 | ///
580 | /// The full path of the component.
581 | ///
582 | public static string GetComponentPath(string componentGuid)
583 | {
584 | //string path = new string(new char[255]);
585 | UInt32 buffer = 500;
586 | StringBuilder path = new StringBuilder(255);
587 |
588 | MsiLocateComponent(
589 | componentGuid,
590 | path,
591 | ref buffer);
592 |
593 | return path.ToString();
594 | }
595 |
596 | ///
597 | /// Gets the path of a component using the key file of the component.
598 | ///
599 | ///
600 | /// The name of the file.
601 | ///
602 | ///
603 | /// The full path of the component of an empty string if the path was
604 | /// not found.
605 | ///
606 | public static string GetComponentPathByFile(string fileName)
607 | {
608 | string path = string.Empty;
609 | uint ret = 0;
610 | uint i = 0;
611 |
612 | do
613 | {
614 | // Create the guid buffer
615 | StringBuilder guid = new StringBuilder(39);
616 |
617 | // Get a component GUID
618 | ret = Api.MsiEnumComponents(i, guid);
619 |
620 | // If the return code indicates a success, then add the GUID
621 | // to the list
622 | if (ret == 0)
623 | {
624 | string componentPath =
625 | GetComponentPath(guid.ToString());
626 | if (componentPath.Contains(fileName))
627 | {
628 | path = componentPath;
629 | }
630 | }
631 |
632 | // Increment the counter
633 | i++;
634 |
635 | } while ((ret != (uint)MsiExitCodes.NoMoreItems) && (path.Length == 0));
636 |
637 | return path;
638 | }
639 |
640 | ///
641 | /// Get property of a product indicated by GUID. Throws exception if cannot read the property.
642 | ///
643 | /// Product GUID
644 | /// Property name
645 | /// Property value, if available.
646 | /// Throws MSIException if reading property was not successful
647 | public static String getProperty(string productGUID, string propertyName)
648 | {
649 | int len = 0;
650 | // Get the data len
651 | Api.MsiGetProductInfo(productGUID, propertyName, null, ref len);
652 | // increase for the terminating \0
653 | len++;
654 |
655 | String r = new string(new char[len]);
656 | int returnValue = Api.MsiGetProductInfo(productGUID, propertyName, r, ref len);
657 | if (returnValue != 0)
658 | {
659 | if (propertyName != InstallProperty.DisplayName)
660 | {
661 | throw new MSIException(returnValue);
662 | }
663 | }
664 | return r;
665 | }
666 | #endregion
667 | }
668 | }
669 |
--------------------------------------------------------------------------------
/LocalSystem/Msi/InstallProperty.cs:
--------------------------------------------------------------------------------
1 | using System;
2 |
3 | namespace TE.LocalSystem.Msi
4 | {
5 | ///
6 | /// The standard properties for a Windows Installer file.
7 | ///
8 | public static class InstallProperty
9 | {
10 | public static readonly string InstalledProductName = "InstalledProductName";
11 | public static readonly string VersionString = "VersionString";
12 | public static readonly string HelpLink = "HelpLink";
13 | public static readonly string HelpTelephone = "HelpTelephone";
14 | public static readonly string InstallLocation = "InstallLocation";
15 | public static readonly string InstallSource = "InstallSource";
16 | public static readonly string InstallDate = "InstallDate";
17 | public static readonly string Publisher = "Publisher";
18 | public static readonly string LocalPackage = "LocalPackage";
19 | public static readonly string URLInfoAbout = "URLInfoAbout";
20 | public static readonly string URLUpdateInfo = "URLUpdateInfo";
21 | public static readonly string VersionMinor = "VersionMinor";
22 | public static readonly string VersionMajor = "VersionMajor";
23 | public static readonly string ProductID = "ProductID";
24 | public static readonly string RegCompany = "RegCompany";
25 | public static readonly string RegOwner = "RegOwner";
26 | public static readonly string Uninstallable = "Uninstallable";
27 | public static readonly string State = "State";
28 | public static readonly string PatchType = "PatchType";
29 | public static readonly string LUAEnabled = "LUAEnabled";
30 | public static readonly string DisplayName = "DisplayName";
31 | public static readonly string MoreInfoURL = "MoreInfoURL";
32 | public static readonly string LastUsedSource = "LastUsedSource";
33 | public static readonly string LastUsedType = "LastUsedType";
34 | public static readonly string MediaPackagePath = "MediaPackagePath";
35 | public static readonly string DiskPrompt = "DiskPrompt";
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/LocalSystem/Msi/InstalledProduct.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using static System.Environment;
3 | using System.Collections.Generic;
4 | using System.Text;
5 |
6 | namespace TE.LocalSystem.Msi
7 | {
8 | ///
9 | /// Description of InstalledProduct.
10 | ///
11 | public class InstalledProduct
12 | {
13 | #region Public Variables
14 | public readonly string Guid;
15 | #endregion
16 |
17 | #region Properties
18 | public string InstalledProductName { get { return Api.getProperty(Guid, InstallProperty.InstalledProductName); } }
19 | public string VersionString { get { return Api.getProperty(Guid, InstallProperty.VersionString); } }
20 | public string HelpLink { get { return Api.getProperty(Guid, InstallProperty.HelpLink); } }
21 | public string HelpTelephone { get { return Api.getProperty(Guid, InstallProperty.HelpTelephone); } }
22 | public string InstallLocation { get { return Api.getProperty(Guid, InstallProperty.InstallLocation); } }
23 | public string InstallSource { get { return Api.getProperty(Guid, InstallProperty.InstallSource); } }
24 | public string InstallDate { get { return Api.getProperty(Guid, InstallProperty.InstallDate); } }
25 | public string Publisher { get { return Api.getProperty(Guid, InstallProperty.Publisher); } }
26 | public string LocalPackage { get { return Api.getProperty(Guid, InstallProperty.LocalPackage); } }
27 | public string URLInfoAbout { get { return Api.getProperty(Guid, InstallProperty.URLInfoAbout); } }
28 | public string URLUpdateInfo { get { return Api.getProperty(Guid, InstallProperty.URLUpdateInfo); } }
29 | public string VersionMinor { get { return Api.getProperty(Guid, InstallProperty.VersionMinor); } }
30 | public string VersionMajor { get { return Api.getProperty(Guid, InstallProperty.VersionMajor); } }
31 | public string ProductID { get { return Api.getProperty(Guid, InstallProperty.ProductID); } }
32 | public string RegCompany { get { return Api.getProperty(Guid, InstallProperty.RegCompany); } }
33 | public string RegOwner { get { return Api.getProperty(Guid, InstallProperty.RegOwner); } }
34 | public string Uninstallable { get { return Api.getProperty(Guid, InstallProperty.Uninstallable); } }
35 | public string State { get { return Api.getProperty(Guid, InstallProperty.State); } }
36 | public string PatchType { get { return Api.getProperty(Guid, InstallProperty.PatchType); } }
37 | public string LUAEnabled { get { return Api.getProperty(Guid, InstallProperty.LUAEnabled); } }
38 | public string DisplayName { get { return Api.getProperty(Guid, InstallProperty.DisplayName); } }
39 | public string MoreInfoURL { get { return Api.getProperty(Guid, InstallProperty.MoreInfoURL); } }
40 | public string LastUsedSource { get { return Api.getProperty(Guid, InstallProperty.LastUsedSource); } }
41 | public string LastUsedType { get { return Api.getProperty(Guid, InstallProperty.LastUsedType); } }
42 | public string MediaPackagePath { get { return Api.getProperty(Guid, InstallProperty.MediaPackagePath); } }
43 | public string DiskPrompt { get { return Api.getProperty(Guid, InstallProperty.DiskPrompt); } }
44 | #endregion
45 |
46 | #region Constructors
47 | public InstalledProduct(string guid)
48 | {
49 | this.Guid = guid;
50 | }
51 | #endregion
52 |
53 | #region Public Functions
54 | ///
55 | /// Enumerates all MSI installed products
56 | ///
57 | ///
58 | /// An enumeration containing InstalledProducts
59 | ///
60 | public static IEnumerable Enumerate()
61 | {
62 | foreach (var guid in Api.EnumerateProducts())
63 | yield return new InstalledProduct(guid);
64 | }
65 |
66 | ///
67 | /// Returns a string that contains all the property names and values.
68 | ///
69 | ///
70 | /// A string that contains all the property names and values.
71 | ///
72 | public override string ToString()
73 | {
74 | StringBuilder sb = new StringBuilder();
75 | foreach (var p in GetType().GetProperties())
76 | {
77 | try
78 | {
79 | sb.AppendFormat($"{p.Name}:{p.GetValue(this)}{NewLine}");
80 | }
81 | catch
82 | { }
83 | }
84 | return sb.ToString();
85 | }
86 | #endregion
87 | }
88 |
89 | }
90 |
--------------------------------------------------------------------------------
/LocalSystem/Msi/MsiException.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Runtime.Serialization;
3 |
4 | namespace TE.LocalSystem.Msi
5 | {
6 | ///
7 | /// An issue occurred with the Windows Installer.
8 | ///
9 | [Serializable]
10 | internal class MSIException : Exception
11 | {
12 | ///
13 | /// Gets or sets the return value.
14 | ///
15 | public int ReturnValue { get; private set; }
16 |
17 | ///
18 | /// Initializes a new instance of the class.
19 | ///
20 | public MSIException() { }
21 |
22 | ///
23 | /// Initializes a new instance of the class
24 | /// when provided with the execption message.
25 | ///
26 | ///
27 | /// The exception message.
28 | ///
29 | public MSIException(string message) : base(message) { }
30 |
31 | ///
32 | /// Initializes a new instance of the class
33 | /// when provided with the return value.
34 | ///
35 | ///
36 | /// The return value.
37 | ///
38 | public MSIException(int returnValue)
39 | : this($"MSIError : {((MsiExitCodes)returnValue).ToString()}")
40 | {
41 | ReturnValue = returnValue;
42 | }
43 |
44 | ///
45 | /// Initializes a new instance of the class
46 | /// when provided with the execption message and inner exception.
47 | ///
48 | ///
49 | /// The exception message.
50 | ///
51 | ///
52 | /// The inner exception.
53 | ///
54 | public MSIException(string message, Exception innerException)
55 | : base(message, innerException) { }
56 |
57 | ///
58 | /// Initializes a new instance of the class
59 | /// when provided with the serialization information and streaming
60 | /// context.
61 | ///
62 | ///
63 | /// The serialization information.
64 | ///
65 | ///
66 | /// The streaming context.
67 | ///
68 | protected MSIException(
69 | SerializationInfo info,
70 | StreamingContext context)
71 | : base(info, context) { }
72 | }
73 | }
74 |
--------------------------------------------------------------------------------
/LocalSystem/WinApi.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Runtime.InteropServices;
3 | using System.Text;
4 |
5 | namespace TE.LocalSystem
6 | {
7 | ///
8 | /// API and constant definitions for using the Windows API.
9 | ///
10 | public static class WinApi
11 | {
12 | #region Public Constants
13 | ///
14 | /// No error.
15 | ///
16 | public const int NO_ERROR = 0;
17 |
18 | ///
19 | /// The buffer isn't sufficient to store the result.
20 | ///
21 | public const int ERROR_INSUFFICIENT_BUFFER = 122;
22 |
23 | ///
24 | /// Invalid flags.
25 | ///
26 | ///
27 | /// On Windows Server 2003 this error is/can be returned, but processing can still continue.
28 | ///
29 | public const int ERROR_INVALID_FLAGS = 1004;
30 | #endregion
31 |
32 | #region Public Enumerations
33 | ///
34 | /// contains values that specify the type of a security identifier (SID).
35 | ///
36 | public enum SID_NAME_USE
37 | {
38 | ///
39 | /// A user SID.
40 | ///
41 | SidTypeUser = 1,
42 | ///
43 | /// A group SID.
44 | ///
45 | SidTypeGroup,
46 | ///
47 | /// A domain SID.
48 | ///
49 | SidTypeDomain,
50 | ///
51 | /// An alias SID.
52 | ///
53 | SidTypeAlias,
54 | ///
55 | /// A SID for a well-known group.
56 | ///
57 | SidTypeWellKnownGroup,
58 | ///
59 | /// A SID for a deleted account.
60 | ///
61 | SidTypeDeletedAccount,
62 | ///
63 | /// A SID That is not valid.
64 | ///
65 | SidTypeInvalid,
66 | ///
67 | /// A SID of unknown type.
68 | ///
69 | SidTypeUnknown,
70 | ///
71 | /// A SID for a computer.
72 | ///
73 | SidTypeComputer,
74 | ///
75 | /// A mandatory integrity label SID.
76 | ///
77 | SidTypeLabel
78 | }
79 | #endregion
80 |
81 | #region Public API Functions
82 | ///
83 | /// The LookupAccountName function accepts the name of a system and an
84 | /// account as input. It retrieves a security identifier (SID) for the
85 | /// account and the name of the domain on which the account was found.
86 | ///
87 | ///
88 | /// The name of the system.
89 | ///
90 | ///
91 | /// The name of the account on the system.
92 | ///
93 | ///
94 | /// The pointer to a buffer that receives the SID structure.
95 | ///
96 | ///
97 | /// The size of the SID buffer.
98 | ///
99 | ///
100 | /// A buffer that received the name of the domain associated with
101 | /// the account.
102 | ///
103 | ///
104 | /// The size of the domain name buffer.
105 | ///
106 | ///
107 | /// A pointer to a SID_NAME_USER
108 | /// enum.
109 | ///
110 | ///
111 | /// If the function succeeds, the function returns nonzero.
112 | /// If the function fails, it returns zero.
113 | ///
114 | [DllImport("advapi32.dll", CharSet = CharSet.Auto, SetLastError = true)]
115 | public static extern bool LookupAccountName(
116 | string lpSystemName,
117 | string lpAccountName,
118 | [MarshalAs(UnmanagedType.LPArray)] byte[] Sid,
119 | ref uint cbSid,
120 | StringBuilder ReferencedDomainName,
121 | ref uint cchReferencedDomainName,
122 | out SID_NAME_USE peUse);
123 |
124 | ///
125 | /// converts a security identifier (SID) to a string format suitable
126 | /// for display, storage, or transmission.
127 | ///
128 | ///
129 | /// A pointer to the SID structure to be converted.
130 | ///
131 | ///
132 | /// A pointer to a variable that receives a pointer to a null-terminated
133 | /// SID string.
134 | ///
135 | ///
136 | /// If the function succeeds, the return value is nonzero.
137 | /// If the function fails, the return value is zero.
138 | ///
139 | [DllImport("advapi32", CharSet = CharSet.Auto, SetLastError = true)]
140 | public static extern bool ConvertSidToStringSid(
141 | [MarshalAs(UnmanagedType.LPArray)] byte[] pSID,
142 | out IntPtr ptrSid);
143 |
144 | ///
145 | /// Frees the specified local memory object and invalidates its handle.
146 | ///
147 | ///
148 | /// A handle to the local memory object.
149 | ///
150 | ///
151 | /// If the function succeeds, the return value is NULL.
152 | /// If the function fails, the return value is equal to a handle to the
153 | /// local memory object.
154 | ///
155 | [DllImport("kernel32.dll")]
156 | public static extern IntPtr LocalFree(IntPtr hMem);
157 | #endregion
158 | }
159 | }
160 |
--------------------------------------------------------------------------------
/LocalSystem/WindowsUser.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.ComponentModel;
3 | using System.Runtime.InteropServices;
4 | using System.Security.Principal;
5 | using System.Text;
6 | using System.Text.RegularExpressions;
7 | using Microsoft.Win32;
8 |
9 | namespace TE.LocalSystem
10 | {
11 | ///
12 | /// Description of WindowsUser.
13 | ///
14 | public class WindowsUser
15 | {
16 | #region Constants
17 | ///
18 | /// The root key for the users registry hive.
19 | ///
20 | private const string RegistryUsersRoot = "HKEY_USERS";
21 | ///
22 | /// The registry key that stores the local application data folder for
23 | /// the Windows user.
24 | ///
25 | private const string RegistryLocalAppDataKey =
26 | @"Software\Microsoft\Windows\CurrentVersion\Explorer\Shell Folders\";
27 | ///
28 | /// The name of the local Plex data path registry value.
29 | ///
30 | private const string RegistryLocalAppDataValueName = "Local AppData";
31 | #endregion
32 |
33 | #region Private Variables
34 | ///
35 | /// The Windows identity of the user.
36 | ///
37 | private WindowsIdentity userIdentity = null;
38 | #endregion
39 |
40 | #region Properties
41 | ///
42 | /// Gets the local application data folder for the user.
43 | ///
44 | public string LocalAppDataFolder { get; private set; }
45 |
46 | ///
47 | /// Gets or sets the name of the Windows user.
48 | ///
49 | public string Name { get; set; }
50 |
51 | ///
52 | /// Gets the SID associated with the user.
53 | ///
54 | public string Sid { get; set; }
55 | #endregion
56 |
57 | #region Constructors
58 | ///
59 | /// Creates an instance of the
60 | /// class.
61 | ///
62 | ///
63 | /// Thrown when the SID for the user is null or empty.
64 | ///
65 | public WindowsUser()
66 | {
67 | try
68 | {
69 | Initialize(string.Empty);
70 | }
71 | catch
72 | {
73 | throw;
74 | }
75 | }
76 |
77 | ///
78 | /// Creates an instance of the
79 | /// class when provided with the Windows user's name.
80 | ///
81 | ///
82 | /// Thrown when the SID for the user is null or empty.
83 | ///
84 | public WindowsUser(string name)
85 | {
86 | try
87 | {
88 | Initialize(name);
89 | }
90 | catch
91 | {
92 | throw;
93 | }
94 | }
95 | #endregion
96 |
97 | #region Private Functions
98 | ///
99 | /// Gets the Windows identity for the user.
100 | ///
101 | ///
102 | /// A object
103 | /// of the Windows user, or null if the
104 | /// object could not be created.
105 | ///
106 | private WindowsIdentity GetIdentity()
107 | {
108 | WindowsIdentity identity = null;
109 |
110 | try
111 | {
112 | if (string.IsNullOrWhiteSpace(Name))
113 | {
114 | // If no Windows user name is specified, just get the identity
115 | // for the current user
116 | identity = WindowsIdentity.GetCurrent();
117 | }
118 | else
119 | {
120 | // Get the identity for the user associated with the user name
121 | identity = new WindowsIdentity(Name);
122 | }
123 | }
124 | catch (UnauthorizedAccessException)
125 | {
126 | return null;
127 | }
128 | catch (System.Security.SecurityException)
129 | {
130 | return null;
131 | }
132 |
133 | return identity;
134 | }
135 |
136 | ///
137 | /// Gets the local application data path for the Windows user.
138 | ///
139 | ///
140 | /// Thrown when the SID for the user is null or empty.
141 | ///
142 | ///
143 | /// The path of the local application data folder for the Windows user.
144 | ///
145 | private string GetLocalAppDataFolder()
146 | {
147 | // Check to ensure a SID value for the user is set
148 | if (string.IsNullOrWhiteSpace(Sid))
149 | {
150 | throw new WindowsUserSidNotFound(
151 | "The SID for the user was not specified.");
152 | }
153 |
154 | try
155 | {
156 | using (RegistryKey localAppDataRegistry =
157 | Registry.Users.OpenSubKey($"{Sid}\\{RegistryLocalAppDataKey}"))
158 | {
159 | return (string)localAppDataRegistry.GetValue(RegistryLocalAppDataValueName);
160 | }
161 | }
162 | catch
163 | {
164 | return null;
165 | }
166 |
167 | }
168 |
169 | ///
170 | /// Gets the SID for the associated Windows user.
171 | ///
172 | ///
173 | /// The SID for the Windows user.
174 | ///
175 | private string GetSid()
176 | {
177 | // Verify that the username was provided
178 | if (string.IsNullOrWhiteSpace(Name))
179 | {
180 | return string.Empty;
181 | }
182 |
183 | try
184 | {
185 | // Get the account for the username
186 | NTAccount account = new NTAccount(Name);
187 |
188 | // Try to get the security identifier for the username
189 | SecurityIdentifier identifier = (SecurityIdentifier)account.Translate(
190 | typeof(SecurityIdentifier));
191 |
192 | // Return the string value of the identifier
193 | return identifier.Value;
194 | }
195 | catch (IdentityNotMappedException)
196 | {
197 | // If the identity could not be mapped, return null
198 | return null;
199 | }
200 | }
201 |
202 | ///
203 | /// Gets the SID for the associated Windows user using the Windows API.
204 | ///
205 | ///
206 | /// The SID for the Windows user.
207 | ///
208 | private string GetSidApi()
209 | {
210 | // Verify that the username was provided
211 | if (string.IsNullOrWhiteSpace(Name))
212 | {
213 | return string.Empty;
214 | }
215 |
216 | // The byte array that will hold the SID
217 | byte[] sid = null;
218 | // The SID buffer size
219 | uint cbSid = 0;
220 | // Then domain name
221 | StringBuilder referencedDomainName = new StringBuilder();
222 | // The buffer size for the domain name
223 | uint cchReferencedDomainName = (uint)referencedDomainName.Capacity;
224 | // The type of SID
225 | WinApi.SID_NAME_USE sidUse;
226 |
227 | // Default the return value to indicate no error
228 | int err = WinApi.NO_ERROR;
229 |
230 | // Attempt to get the SID for the account name
231 | if (!WinApi.LookupAccountName(
232 | null,
233 | Name,
234 | sid,
235 | ref cbSid,
236 | referencedDomainName,
237 | ref cchReferencedDomainName,
238 | out sidUse))
239 | {
240 | // Get the error from the LookupAccountName API call
241 | err = Marshal.GetLastWin32Error();
242 |
243 | // Check to see if either the buffer wasn't sufficient or the
244 | // invalid flags error was returned
245 | if (err == WinApi.ERROR_INSUFFICIENT_BUFFER || err == WinApi.ERROR_INVALID_FLAGS)
246 | {
247 | // Create a new byte buffer with the size returned from the
248 | // first LookupAccountName request
249 | sid = new byte[cbSid];
250 |
251 | // Make sure that the capacity of the StringBuilder object
252 | // for the domain name is at the correct buffer size
253 | referencedDomainName.EnsureCapacity((int)cchReferencedDomainName);
254 |
255 | // Reset the return value to indicate no error
256 | err = WinApi.NO_ERROR;
257 |
258 | // Attempt to get the account name after the correct buffer
259 | // sizes have been set
260 | if (!WinApi.LookupAccountName(
261 | null,
262 | Name,
263 | sid,
264 | ref cbSid,
265 | referencedDomainName,
266 | ref cchReferencedDomainName,
267 | out sidUse))
268 | {
269 | // Return null if the SID could not be retrieved
270 | return null;
271 | }
272 | }
273 | }
274 | // Any other error that occured when trying to get the SID for the
275 | // account name
276 | else
277 | {
278 | // Return null if the SID could not be retrieved
279 | return null;
280 | }
281 |
282 | // No error occurred
283 | if (err == 0)
284 | {
285 | // Create the SID pointer
286 | IntPtr ptrSid;
287 |
288 | // Attempt to convert the SID byte array to a string
289 | if (!WinApi.ConvertSidToStringSid(sid, out ptrSid))
290 | {
291 | // Return an empty string if the SID could not be
292 | // retrieved
293 | return null;
294 | }
295 | else
296 | {
297 | // Copy all characters from an unmanaged memory string to
298 | // a manage string
299 | string sidString = Marshal.PtrToStringAuto(ptrSid);
300 | // Free up the memory
301 | WinApi.LocalFree(ptrSid);
302 |
303 | // Return the SID for the account name
304 | return sidString;
305 | }
306 | }
307 | else
308 | {
309 | // Return null if the SID could not be retrieved
310 | return null;
311 | }
312 | }
313 |
314 | ///
315 | /// Gets the SID from the registry for the associated Windows user.
316 | ///
317 | ///
318 | /// The SID For the Windows user.
319 | ///
320 | private string GetSidRegistry()
321 | {
322 | // Verify that the username was provided
323 | if (string.IsNullOrWhiteSpace(Name))
324 | {
325 | return null;
326 | }
327 |
328 | // Default to a blank SID
329 | string sid = null;
330 |
331 | // Remove the domain name from the Windows user name
332 | string name = RemoveDomainFromName(Name);
333 |
334 | // Open the registry key that contains the profiles
335 | RegistryKey key = Registry.LocalMachine.OpenSubKey(
336 | @"SOFTWARE\Microsoft\Windows NT\CurrentVersion\ProfileList");
337 |
338 | // Loop through each of the subkeys that contain the profiles
339 | foreach (string keyName in key.GetSubKeyNames())
340 | {
341 | // Open the key
342 | RegistryKey sidKey = key.OpenSubKey(keyName);
343 |
344 | // Check to ensure the key was opened
345 | if (sidKey != null)
346 | {
347 | // Get the profile path value from the registry
348 | string profilePath = sidKey.GetValue("ProfileImagePath").ToString();
349 |
350 | // Check to see if the account name is in the profile path
351 | if (profilePath.IndexOf(name, StringComparison.OrdinalIgnoreCase) >= 0)
352 | {
353 | // If the account name was in the profile path, store
354 | // the SID
355 | sid = System.IO.Path.GetFileName(sidKey.Name);
356 |
357 | }
358 | }
359 | }
360 |
361 | // Return the SID
362 | return sid;
363 | }
364 |
365 | ///
366 | /// Gets the SID of a well-known account. Such accounts are built into
367 | /// Windows and the SID for these accounts are the same on all
368 | /// computers.
369 | ///
370 | ///
371 | /// The SID for the Windows user.
372 | ///
373 | private string GetSidKnown()
374 | {
375 | // Verify that the username was provided
376 | if (string.IsNullOrWhiteSpace(Name))
377 | {
378 | return null;
379 | }
380 |
381 | // Remove the domain name from the Windows user name
382 | string name = RemoveDomainFromName(Name);
383 |
384 | SecurityIdentifier identifier = null;
385 |
386 | try
387 | {
388 |
389 | if (name == "LocalService")
390 | {
391 | // Get the security identifier for the LocalService
392 | identifier = new SecurityIdentifier(
393 | WellKnownSidType.LocalServiceSid,
394 | null);
395 | }
396 | if (name == "LocalSystem")
397 | {
398 | // Get the security identifier for the LocalSystem
399 | identifier = new SecurityIdentifier(
400 | WellKnownSidType.LocalSystemSid,
401 | null);
402 | }
403 | else if (name == "NetworkService")
404 | {
405 | // Get the security identifier for the NetworkServer
406 | identifier = new SecurityIdentifier(
407 | WellKnownSidType.NetworkServiceSid,
408 | null);
409 | }
410 |
411 | return identifier.Value;
412 |
413 | }
414 | catch
415 | {
416 | return null;
417 | }
418 | }
419 |
420 | ///
421 | /// Initializes the objects and properties in the class.
422 | ///
423 | ///
424 | /// Thrown when the SID for the user is null or empty.
425 | ///
426 | private void Initialize(string name)
427 | {
428 | Name = name;
429 | userIdentity = GetIdentity();
430 |
431 | if (string.IsNullOrWhiteSpace(Name))
432 | {
433 | Name = (userIdentity == null) ? WindowsIdentity.GetCurrent().Name : Name = userIdentity.Name;
434 | }
435 |
436 | // Try to see if the account is a well-known account and get the
437 | // SID for the account
438 | Sid = GetSidKnown();
439 |
440 | // If the the account name doesn't match a well-known account,
441 | // then try to find the SID for the account
442 | if (string.IsNullOrWhiteSpace(Sid))
443 | {
444 | Sid = GetSid();
445 |
446 | // If no SID was returned, try to get the SID using the
447 | // Windows API
448 | if (string.IsNullOrWhiteSpace(Sid))
449 | {
450 | Sid = GetSidApi();
451 |
452 | // If still no SID was returned, try to find it in the
453 | // registry
454 | if (string.IsNullOrWhiteSpace(Sid))
455 | {
456 | Sid = GetSidRegistry();
457 | }
458 | }
459 | }
460 |
461 | // Throw an exception is the SID was not found
462 | if (string.IsNullOrEmpty(Sid))
463 | {
464 | throw new WindowsUserSidNotFound(
465 | "The SID for the user was not specified.");
466 | }
467 |
468 | LocalAppDataFolder = GetLocalAppDataFolder();
469 | }
470 |
471 | ///
472 | /// Removes the domain name from the Windows user.
473 | ///
474 | ///
475 | /// The name of the Windows user
476 | ///
477 | ///
478 | /// The name of the Windows user without the domain name.
479 | ///
480 | private string RemoveDomainFromName(string name)
481 | {
482 | // Check to see if the name contains a slash
483 | if (name.Contains(@"\"))
484 | {
485 | // Remove the domain name
486 | name = Regex.Replace(
487 | name,
488 | @".*\\(.*)",
489 | "$1",
490 | RegexOptions.None);
491 | }
492 |
493 | // Return the name without the domain name
494 | return name;
495 | }
496 | #endregion
497 |
498 | #region Public Functions
499 | ///
500 | /// Checks to see if the user context running the application is an
501 | /// administrator.
502 | ///
503 | ///
504 | /// True if the user is an administrator, false if they are not an
505 | /// administrator.
506 | ///
507 | public bool IsAdministrator()
508 | {
509 | WindowsPrincipal principal = new WindowsPrincipal(userIdentity);
510 | return principal.IsInRole(WindowsBuiltInRole.Administrator);
511 | }
512 | #endregion
513 | }
514 | }
515 |
--------------------------------------------------------------------------------
/LocalSystem/WindowsUserSidNotFound.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Runtime.Serialization;
3 |
4 | namespace TE.LocalSystem
5 | {
6 | ///
7 | /// The SID for the Windows user could not be found.
8 | ///
9 | public class WindowsUserSidNotFound : Exception
10 | {
11 | public WindowsUserSidNotFound() { }
12 |
13 | public WindowsUserSidNotFound(string message)
14 | : base(message) { }
15 |
16 | public WindowsUserSidNotFound(
17 | string message,
18 | Exception innerException)
19 | : base(message, innerException) { }
20 |
21 | protected WindowsUserSidNotFound(
22 | SerializationInfo info,
23 | StreamingContext context)
24 | : base(info, context) { }
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/Log.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using static System.Environment;
4 | using System.IO;
5 | using System.Linq;
6 | using System.Text;
7 | using System.Threading.Tasks;
8 |
9 | namespace TE
10 | {
11 | ///
12 | /// Contains the properties and methods to write a log file.
13 | ///
14 | public static class Log
15 | {
16 | ///
17 | /// The name of the updater log file.
18 | ///
19 | private const string LogFileName = "plex-updater.txt";
20 |
21 | private static string _defaultFolder;
22 |
23 | ///
24 | /// Gets the the full path to the log file.
25 | ///
26 | public static string FilePath { get; private set; }
27 |
28 | ///
29 | /// Gets the folder to the log file.
30 | ///
31 | public static string Folder { get; private set; }
32 |
33 | ///
34 | /// Initializes an instance of the class.
35 | ///
36 | static Log()
37 | {
38 | _defaultFolder = Path.GetTempPath();
39 | Folder = _defaultFolder;
40 |
41 | // Set the full path to the log file
42 | FilePath = Path.Combine(Folder, LogFileName);
43 | }
44 |
45 | ///
46 | /// Gets the formatted timestamp value.
47 | ///
48 | ///
49 | /// A string representation of the timestamp.
50 | ///
51 | private static string GetTimeStamp()
52 | {
53 | return DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss ");
54 | }
55 |
56 | ///
57 | /// Deletes the log file.
58 | ///
59 | public static void Delete()
60 | {
61 | File.Delete(FilePath);
62 | }
63 |
64 | ///
65 | /// Sets the full path to the log file.
66 | ///
67 | ///
68 | /// The full path to the log file.
69 | ///
70 | public static void SetFolder(string path)
71 | {
72 | try
73 | {
74 | // Call this to validate the path
75 | Path.GetFullPath(path);
76 |
77 | Folder = Path.GetDirectoryName(path);
78 | if (!Directory.Exists(Folder))
79 | {
80 | Directory.CreateDirectory(Folder);
81 | }
82 |
83 | FilePath = Path.Combine(Folder, LogFileName);
84 | }
85 | catch
86 | {
87 | Folder = _defaultFolder;
88 | FilePath = Path.Combine(Folder, LogFileName);
89 | }
90 | }
91 |
92 | ///
93 | /// Writes a string value to the log file.
94 | ///
95 | public static void Write(string text, bool appendDate = true)
96 | {
97 | string timeStamp = string.Empty;
98 | if (appendDate)
99 | {
100 | timeStamp = GetTimeStamp();
101 | }
102 |
103 | File.AppendAllText(FilePath, $"{timeStamp}{text}{NewLine}");
104 | }
105 |
106 | ///
107 | /// Writes information about an exception to the log file.
108 | ///
109 | ///
110 | /// The object that contains information to write to
111 | /// the log file.
112 | ///
113 | public static void Write(Exception ex, bool appendDate = true)
114 | {
115 | if (ex == null)
116 | {
117 | return;
118 | }
119 |
120 | string timeStamp = string.Empty;
121 | if (appendDate)
122 | {
123 | timeStamp = GetTimeStamp();
124 | }
125 |
126 | File.AppendAllText(
127 | FilePath,
128 | $"{timeStamp}Message:{NewLine}{ex.Message}{NewLine}{NewLine}Inner Exception:{NewLine}{ex.InnerException}{NewLine}{NewLine}Stack Trace:{NewLine}{ex.StackTrace}{NewLine}");
129 | }
130 | }
131 | }
132 |
--------------------------------------------------------------------------------
/MainForm.Designer.cs:
--------------------------------------------------------------------------------
1 | /*
2 | * Created by SharpDevelop.
3 | * User: Paul
4 | * Date: 2/22/2016
5 | * Time: 7:50 PM
6 | *
7 | * To change this template use Tools | Options | Coding | Edit Standard Headers.
8 | */
9 | namespace TE.Plex
10 | {
11 | partial class MainForm
12 | {
13 | ///
14 | /// Designer variable used to keep track of non-visual components.
15 | ///
16 | private System.ComponentModel.IContainer components = null;
17 | private System.Windows.Forms.Button btnCancel;
18 | private System.Windows.Forms.Button btnUpdate;
19 | private System.Windows.Forms.GroupBox grpUpdateStatus;
20 | private System.Windows.Forms.TextBox txtUpdateStatus;
21 | private System.Windows.Forms.GroupBox groupBox1;
22 | private System.Windows.Forms.Label lblLatestVersion;
23 | private System.Windows.Forms.Label lblInstalledVersion;
24 | private System.Windows.Forms.Label lblLatestVersionLabel;
25 | private System.Windows.Forms.Label lblInstalledVersionLabel;
26 |
27 | ///
28 | /// Disposes resources used by the form.
29 | ///
30 | /// true if managed resources should be disposed; otherwise, false.
31 | protected override void Dispose(bool disposing)
32 | {
33 | if (disposing) {
34 | if (components != null) {
35 | components.Dispose();
36 | }
37 | }
38 | base.Dispose(disposing);
39 | }
40 |
41 | ///
42 | /// This method is required for Windows Forms designer support.
43 | /// Do not change the method contents inside the source code editor. The Forms designer might
44 | /// not be able to load this method if it was changed manually.
45 | ///
46 | private void InitializeComponent()
47 | {
48 | this.btnCancel = new System.Windows.Forms.Button();
49 | this.btnUpdate = new System.Windows.Forms.Button();
50 | this.grpUpdateStatus = new System.Windows.Forms.GroupBox();
51 | this.txtUpdateStatus = new System.Windows.Forms.TextBox();
52 | this.groupBox1 = new System.Windows.Forms.GroupBox();
53 | this.lblPlayCount = new System.Windows.Forms.Label();
54 | this.lblPlayCountLabel = new System.Windows.Forms.Label();
55 | this.lblLatestVersion = new System.Windows.Forms.Label();
56 | this.lblInstalledVersion = new System.Windows.Forms.Label();
57 | this.lblLatestVersionLabel = new System.Windows.Forms.Label();
58 | this.lblInstalledVersionLabel = new System.Windows.Forms.Label();
59 | this.btnExit = new System.Windows.Forms.Button();
60 | this.chkWait = new System.Windows.Forms.CheckBox();
61 | this.lblCheckEveryLabel = new System.Windows.Forms.Label();
62 | this.numSeconds = new System.Windows.Forms.NumericUpDown();
63 | this.lblCheckSecondsLabel = new System.Windows.Forms.Label();
64 | this.lblInProgressRecordingCount = new System.Windows.Forms.Label();
65 | this.lblInProgressRecordingCountLabel = new System.Windows.Forms.Label();
66 | this.grpUpdateStatus.SuspendLayout();
67 | this.groupBox1.SuspendLayout();
68 | ((System.ComponentModel.ISupportInitialize)(this.numSeconds)).BeginInit();
69 | this.SuspendLayout();
70 | //
71 | // btnCancel
72 | //
73 | this.btnCancel.Location = new System.Drawing.Point(476, 448);
74 | this.btnCancel.Name = "btnCancel";
75 | this.btnCancel.Size = new System.Drawing.Size(75, 23);
76 | this.btnCancel.TabIndex = 1;
77 | this.btnCancel.Text = "Cancel";
78 | this.btnCancel.UseVisualStyleBackColor = true;
79 | this.btnCancel.Visible = false;
80 | this.btnCancel.Click += new System.EventHandler(this.BtnCancelClick);
81 | //
82 | // btnUpdate
83 | //
84 | this.btnUpdate.Location = new System.Drawing.Point(394, 448);
85 | this.btnUpdate.Name = "btnUpdate";
86 | this.btnUpdate.Size = new System.Drawing.Size(75, 23);
87 | this.btnUpdate.TabIndex = 2;
88 | this.btnUpdate.Text = "Update";
89 | this.btnUpdate.UseVisualStyleBackColor = true;
90 | this.btnUpdate.Click += new System.EventHandler(this.BtnUpdateClick);
91 | //
92 | // grpUpdateStatus
93 | //
94 | this.grpUpdateStatus.Controls.Add(this.txtUpdateStatus);
95 | this.grpUpdateStatus.Location = new System.Drawing.Point(12, 136);
96 | this.grpUpdateStatus.Name = "grpUpdateStatus";
97 | this.grpUpdateStatus.Size = new System.Drawing.Size(538, 289);
98 | this.grpUpdateStatus.TabIndex = 3;
99 | this.grpUpdateStatus.TabStop = false;
100 | this.grpUpdateStatus.Text = "Update Status:";
101 | //
102 | // txtUpdateStatus
103 | //
104 | this.txtUpdateStatus.Location = new System.Drawing.Point(7, 20);
105 | this.txtUpdateStatus.Multiline = true;
106 | this.txtUpdateStatus.Name = "txtUpdateStatus";
107 | this.txtUpdateStatus.ScrollBars = System.Windows.Forms.ScrollBars.Both;
108 | this.txtUpdateStatus.Size = new System.Drawing.Size(525, 263);
109 | this.txtUpdateStatus.TabIndex = 0;
110 | //
111 | // groupBox1
112 | //
113 | this.groupBox1.Controls.Add(this.lblInProgressRecordingCount);
114 | this.groupBox1.Controls.Add(this.lblInProgressRecordingCountLabel);
115 | this.groupBox1.Controls.Add(this.lblPlayCount);
116 | this.groupBox1.Controls.Add(this.lblPlayCountLabel);
117 | this.groupBox1.Controls.Add(this.lblLatestVersion);
118 | this.groupBox1.Controls.Add(this.lblInstalledVersion);
119 | this.groupBox1.Controls.Add(this.lblLatestVersionLabel);
120 | this.groupBox1.Controls.Add(this.lblInstalledVersionLabel);
121 | this.groupBox1.Location = new System.Drawing.Point(13, 13);
122 | this.groupBox1.Name = "groupBox1";
123 | this.groupBox1.Size = new System.Drawing.Size(537, 116);
124 | this.groupBox1.TabIndex = 4;
125 | this.groupBox1.TabStop = false;
126 | this.groupBox1.Text = "Plex Media Server Information";
127 | //
128 | // lblPlayCount
129 | //
130 | this.lblPlayCount.Location = new System.Drawing.Point(178, 66);
131 | this.lblPlayCount.Name = "lblPlayCount";
132 | this.lblPlayCount.Size = new System.Drawing.Size(100, 17);
133 | this.lblPlayCount.TabIndex = 7;
134 | this.lblPlayCount.Text = "[]";
135 | //
136 | // lblPlayCountLabel
137 | //
138 | this.lblPlayCountLabel.AutoSize = true;
139 | this.lblPlayCountLabel.Location = new System.Drawing.Point(7, 66);
140 | this.lblPlayCountLabel.Name = "lblPlayCountLabel";
141 | this.lblPlayCountLabel.Size = new System.Drawing.Size(165, 13);
142 | this.lblPlayCountLabel.TabIndex = 6;
143 | this.lblPlayCountLabel.Text = "Number of items currently playing:";
144 | //
145 | // lblLatestVersion
146 | //
147 | this.lblLatestVersion.Location = new System.Drawing.Point(178, 43);
148 | this.lblLatestVersion.Name = "lblLatestVersion";
149 | this.lblLatestVersion.Size = new System.Drawing.Size(100, 13);
150 | this.lblLatestVersion.TabIndex = 3;
151 | this.lblLatestVersion.Text = "[]";
152 | //
153 | // lblInstalledVersion
154 | //
155 | this.lblInstalledVersion.Location = new System.Drawing.Point(178, 20);
156 | this.lblInstalledVersion.Name = "lblInstalledVersion";
157 | this.lblInstalledVersion.Size = new System.Drawing.Size(100, 13);
158 | this.lblInstalledVersion.TabIndex = 2;
159 | this.lblInstalledVersion.Text = "[]";
160 | //
161 | // lblLatestVersionLabel
162 | //
163 | this.lblLatestVersionLabel.Location = new System.Drawing.Point(7, 43);
164 | this.lblLatestVersionLabel.Name = "lblLatestVersionLabel";
165 | this.lblLatestVersionLabel.Size = new System.Drawing.Size(100, 13);
166 | this.lblLatestVersionLabel.TabIndex = 1;
167 | this.lblLatestVersionLabel.Text = "Latest Version:";
168 | //
169 | // lblInstalledVersionLabel
170 | //
171 | this.lblInstalledVersionLabel.Location = new System.Drawing.Point(7, 20);
172 | this.lblInstalledVersionLabel.Name = "lblInstalledVersionLabel";
173 | this.lblInstalledVersionLabel.Size = new System.Drawing.Size(99, 13);
174 | this.lblInstalledVersionLabel.TabIndex = 0;
175 | this.lblInstalledVersionLabel.Text = "Installed Version:";
176 | //
177 | // btnExit
178 | //
179 | this.btnExit.Location = new System.Drawing.Point(475, 448);
180 | this.btnExit.Name = "btnExit";
181 | this.btnExit.Size = new System.Drawing.Size(75, 23);
182 | this.btnExit.TabIndex = 5;
183 | this.btnExit.Text = "Exit";
184 | this.btnExit.UseVisualStyleBackColor = true;
185 | this.btnExit.Click += new System.EventHandler(this.btnExit_Click);
186 | //
187 | // chkWait
188 | //
189 | this.chkWait.AutoSize = true;
190 | this.chkWait.Checked = true;
191 | this.chkWait.CheckState = System.Windows.Forms.CheckState.Checked;
192 | this.chkWait.Location = new System.Drawing.Point(19, 431);
193 | this.chkWait.Name = "chkWait";
194 | this.chkWait.Size = new System.Drawing.Size(161, 17);
195 | this.chkWait.TabIndex = 6;
196 | this.chkWait.Text = "Only update when not in use";
197 | this.chkWait.UseVisualStyleBackColor = true;
198 | this.chkWait.CheckedChanged += new System.EventHandler(this.chkWait_CheckedChanged);
199 | //
200 | // lblCheckEveryLabel
201 | //
202 | this.lblCheckEveryLabel.AutoSize = true;
203 | this.lblCheckEveryLabel.Location = new System.Drawing.Point(16, 458);
204 | this.lblCheckEveryLabel.Name = "lblCheckEveryLabel";
205 | this.lblCheckEveryLabel.Size = new System.Drawing.Size(67, 13);
206 | this.lblCheckEveryLabel.TabIndex = 7;
207 | this.lblCheckEveryLabel.Text = "Check every";
208 | //
209 | // numSeconds
210 | //
211 | this.numSeconds.Location = new System.Drawing.Point(89, 456);
212 | this.numSeconds.Maximum = new decimal(new int[] {
213 | 3600,
214 | 0,
215 | 0,
216 | 0});
217 | this.numSeconds.Minimum = new decimal(new int[] {
218 | 5,
219 | 0,
220 | 0,
221 | 0});
222 | this.numSeconds.Name = "numSeconds";
223 | this.numSeconds.Size = new System.Drawing.Size(53, 20);
224 | this.numSeconds.TabIndex = 8;
225 | this.numSeconds.Value = new decimal(new int[] {
226 | 30,
227 | 0,
228 | 0,
229 | 0});
230 | //
231 | // lblCheckSecondsLabel
232 | //
233 | this.lblCheckSecondsLabel.AutoSize = true;
234 | this.lblCheckSecondsLabel.Location = new System.Drawing.Point(148, 458);
235 | this.lblCheckSecondsLabel.Name = "lblCheckSecondsLabel";
236 | this.lblCheckSecondsLabel.Size = new System.Drawing.Size(47, 13);
237 | this.lblCheckSecondsLabel.TabIndex = 9;
238 | this.lblCheckSecondsLabel.Text = "seconds";
239 | //
240 | // lblInProgressRecordingCount
241 | //
242 | this.lblInProgressRecordingCount.Location = new System.Drawing.Point(178, 89);
243 | this.lblInProgressRecordingCount.Name = "lblInProgressRecordingCount";
244 | this.lblInProgressRecordingCount.Size = new System.Drawing.Size(100, 17);
245 | this.lblInProgressRecordingCount.TabIndex = 9;
246 | this.lblInProgressRecordingCount.Text = "[]";
247 | //
248 | // lblInProgressRecordingCountLabel
249 | //
250 | this.lblInProgressRecordingCountLabel.AutoSize = true;
251 | this.lblInProgressRecordingCountLabel.Location = new System.Drawing.Point(7, 89);
252 | this.lblInProgressRecordingCountLabel.Name = "lblInProgressRecordingCountLabel";
253 | this.lblInProgressRecordingCountLabel.Size = new System.Drawing.Size(165, 13);
254 | this.lblInProgressRecordingCountLabel.TabIndex = 8;
255 | this.lblInProgressRecordingCountLabel.Text = "Number of in progress recordings:";
256 | //
257 | // MainForm
258 | //
259 | this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F);
260 | this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
261 | this.ClientSize = new System.Drawing.Size(563, 490);
262 | this.Controls.Add(this.lblCheckSecondsLabel);
263 | this.Controls.Add(this.numSeconds);
264 | this.Controls.Add(this.lblCheckEveryLabel);
265 | this.Controls.Add(this.chkWait);
266 | this.Controls.Add(this.btnExit);
267 | this.Controls.Add(this.groupBox1);
268 | this.Controls.Add(this.grpUpdateStatus);
269 | this.Controls.Add(this.btnUpdate);
270 | this.Controls.Add(this.btnCancel);
271 | this.MaximizeBox = false;
272 | this.MinimizeBox = false;
273 | this.Name = "MainForm";
274 | this.ShowIcon = false;
275 | this.ShowInTaskbar = false;
276 | this.StartPosition = System.Windows.Forms.FormStartPosition.CenterScreen;
277 | this.Text = "Plex Server Updater";
278 | this.Shown += new System.EventHandler(this.MainForm_Shown);
279 | this.grpUpdateStatus.ResumeLayout(false);
280 | this.grpUpdateStatus.PerformLayout();
281 | this.groupBox1.ResumeLayout(false);
282 | this.groupBox1.PerformLayout();
283 | ((System.ComponentModel.ISupportInitialize)(this.numSeconds)).EndInit();
284 | this.ResumeLayout(false);
285 | this.PerformLayout();
286 |
287 | }
288 |
289 | private System.Windows.Forms.Button btnExit;
290 | private System.Windows.Forms.Label lblPlayCount;
291 | private System.Windows.Forms.Label lblPlayCountLabel;
292 | private System.Windows.Forms.CheckBox chkWait;
293 | private System.Windows.Forms.Label lblCheckEveryLabel;
294 | private System.Windows.Forms.NumericUpDown numSeconds;
295 | private System.Windows.Forms.Label lblCheckSecondsLabel;
296 | private System.Windows.Forms.Label lblInProgressRecordingCount;
297 | private System.Windows.Forms.Label lblInProgressRecordingCountLabel;
298 | }
299 | }
300 |
--------------------------------------------------------------------------------
/MainForm.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Drawing;
4 | using static System.Environment;
5 | using System.Threading;
6 | using System.Threading.Tasks;
7 | using System.Timers;
8 | using System.Windows.Forms;
9 |
10 | namespace TE.Plex
11 | {
12 | ///
13 | /// The Plex Media Server Updater main form.
14 | ///
15 | public partial class MainForm : Form
16 | {
17 | #region Private Variables
18 | ///
19 | /// The media server object.
20 | ///
21 | private MediaServer _server = null;
22 |
23 | ///
24 | /// The cancellation token source.
25 | ///
26 | private CancellationTokenSource _cts = null;
27 |
28 | ///
29 | /// The wait timer.
30 | ///
31 | private System.Timers.Timer _timer = null;
32 | #endregion
33 |
34 | #region Properties
35 | ///
36 | /// Gets a flag indicating if the form is to be closed.
37 | ///
38 | public bool ToBeClosed { get; private set; }
39 | #endregion
40 |
41 | #region Constructors
42 | ///
43 | /// Initializes the form.
44 | ///
45 | public MainForm()
46 | {
47 | InitializeComponent();
48 | Initialize();
49 | }
50 | #endregion
51 |
52 | #region Events
53 | ///
54 | /// Cancels the Plex update..
55 | ///
56 | ///
57 | /// The sender.
58 | ///
59 | ///
60 | /// Event-related arguments.
61 | ///
62 | void BtnCancelClick(object sender, EventArgs e)
63 | {
64 | _cts?.Cancel();
65 | }
66 |
67 | ///
68 | /// Close the form.
69 | ///
70 | ///
71 | /// The sender.
72 | ///
73 | ///
74 | /// Event-related arguments.
75 | ///
76 | private void btnExit_Click(object sender, EventArgs e)
77 | {
78 | Log.Write("Closing the application.");
79 | Close();
80 | }
81 |
82 | ///
83 | /// Performs the Plex Media Server update.
84 | ///
85 | ///
86 | /// The sender.
87 | ///
88 | ///
89 | /// Event-related arguments.
90 | ///
91 | void BtnUpdateClick(object sender, EventArgs e)
92 | {
93 | if (CheckIfCanUpdate())
94 | {
95 | PerformUpdate();
96 | }
97 | }
98 |
99 | ///
100 | /// Enables or disables the controls and timers on the form.
101 | ///
102 | ///
103 | /// The sender.
104 | ///
105 | ///
106 | /// Event-related arguments.
107 | ///
108 | private void chkWait_CheckedChanged(object sender, EventArgs e)
109 | {
110 | lblCheckEveryLabel.Enabled = chkWait.Checked;
111 | lblCheckSecondsLabel.Enabled = chkWait.Checked;
112 | numSeconds.Enabled = chkWait.Checked;
113 | if (_timer != null)
114 | {
115 | _timer.Enabled = chkWait.Checked;
116 | }
117 | }
118 |
119 | ///
120 | /// The form is shown.
121 | ///
122 | ///
123 | /// The sender.
124 | ///
125 | ///
126 | /// Arguments associated with the event.
127 | ///
128 | private void MainForm_Shown(object sender, EventArgs e)
129 | {
130 | // If the form is to be closed, then close the form.
131 | if (ToBeClosed)
132 | {
133 | Log.Write("Closing the application.");
134 | Close();
135 | }
136 | }
137 |
138 | ///
139 | /// The messages from the update execution.
140 | ///
141 | ///
142 | /// The sender object.
143 | ///
144 | ///
145 | /// The message to display on the form.
146 | ///
147 | private void ServerUpdateMessage(object sender, string message)
148 | {
149 | txtUpdateStatus.Text += $"{message}{NewLine}";
150 | Log.Write(message);
151 | }
152 |
153 | ///
154 | /// The timer has elapsed so check the play count to see if the server
155 | /// is in use.
156 | ///
157 | ///
158 | /// The sender.
159 | ///
160 | ///
163 | private void OnTimedEvent(object sender, ElapsedEventArgs e)
164 | {
165 | if (_server == null)
166 | {
167 | _timer.Enabled = false;
168 | return;
169 | }
170 |
171 | if (CheckIfCanUpdate())
172 | {
173 | PerformUpdate();
174 | }
175 | }
176 | #endregion
177 |
178 | #region Private Functions
179 | ///
180 | /// Checks to see if the server can be updated at this time.
181 | ///
182 | private bool CheckIfCanUpdate()
183 | {
184 | if (_server == null)
185 | {
186 | txtUpdateStatus.Text += "The server was not specified. Cannot perform the update.";
187 | Log.Write("The server was not specified. Cannot perform the update.");
188 | _timer.Enabled = false;
189 | return false;
190 | }
191 |
192 | int playCount = _server.GetPlayCount();
193 | int inProgressRecordingCount = _server.GetInProgressRecordingCount();
194 |
195 | // No item is currently being played
196 | if (playCount == 0 && inProgressRecordingCount == 0)
197 | {
198 | txtUpdateStatus.Text += "The server is not in use continuing to perform the update.";
199 | Log.Write("The server is not in use continuing to perform the update.");
200 | lblPlayCount.Text = playCount.ToString();
201 | lblInProgressRecordingCount.Text = inProgressRecordingCount.ToString();
202 | btnUpdate.Enabled = true;
203 | _timer.Enabled = false;
204 | return true;
205 | }
206 | // At least one item is being played
207 | else if (playCount > 0 || inProgressRecordingCount > 0)
208 | {
209 | lblPlayCount.Text = playCount.ToString();
210 | lblInProgressRecordingCount.Text = inProgressRecordingCount.ToString();
211 | if (chkWait.Checked)
212 | {
213 | txtUpdateStatus.Text += "Waiting for the server to be free has been enabled. Server update can begin. Waiting for all media and/or in progress recordings to be stopped.";
214 | Log.Write("The server is in use. Waiting for all media and/or in progress recordings to be stopped before performing the update.");
215 | btnUpdate.Enabled = false;
216 | _timer.Interval =
217 | Convert.ToDouble(Math.Abs(numSeconds.Value) * 1000);
218 | _timer.Enabled = true;
219 | return false;
220 | }
221 | else if (!chkWait.Checked && inProgressRecordingCount > 0)
222 | {
223 | txtUpdateStatus.Text += "The wait option has been disabled, but you cannot update the server while there is a recording in progress. Waiting for all in progress recordings to be stopped.";
224 | Log.Write("The server is in use. Waiting for all media and/or in progress recordings to be stopped before performing the update.");
225 | btnUpdate.Enabled = false;
226 | _timer.Interval =
227 | Convert.ToDouble(Math.Abs(numSeconds.Value) * 1000);
228 | _timer.Enabled = true;
229 | return false;
230 | }
231 | else
232 | {
233 | txtUpdateStatus.Text += "The wait option has been disabled. You can go ahead and update the server.";
234 | Log.Write("The wait option has been disabled. You can go ahead and update the server.");
235 | btnUpdate.Enabled = true;
236 | _timer.Enabled = false;
237 | return true;
238 | }
239 | }
240 | // Could not determine how many items are being played
241 | else
242 | {
243 | txtUpdateStatus.Text += "The server in use status could not be determined. The server can be updated if you wish.";
244 | Log.Write("The server in use status could not be determined. The server can be updated if you wish.");
245 | lblPlayCount.Text = "Unknown";
246 | lblInProgressRecordingCount.Text = "Unknown";
247 | btnUpdate.Enabled = true;
248 | _timer.Enabled = false;
249 | return true;
250 | }
251 | }
252 |
253 | ///
254 | /// Initializes the values on the form.
255 | ///
256 | private void Initialize()
257 | {
258 | try
259 | {
260 | ToBeClosed = false;
261 | _cts = new CancellationTokenSource();
262 |
263 | Log.Write("Initializing the timer object.");
264 | _timer = new System.Timers.Timer();
265 | _timer.Elapsed += new ElapsedEventHandler(OnTimedEvent);
266 | _timer.Enabled = false;
267 | _timer.Interval = Convert.ToDouble(numSeconds.Value * 1000);
268 |
269 | Log.Write("Initializing the Plex media server object.");
270 | _server = new MediaServer(ServerUpdateMessage);
271 |
272 | if (_server == null)
273 | {
274 | Log.Write(
275 | "The Plex media server object could not be initialized. Setting the flag to close the application.");
276 | ToBeClosed = true;
277 | return;
278 | }
279 |
280 | lblInstalledVersion.Text = _server.CurrentVersion.ToString();
281 | lblLatestVersion.Text = _server.LatestVersion.ToString();
282 |
283 | if (_server.LatestVersion > _server.CurrentVersion)
284 | {
285 | btnUpdate.Visible = true;
286 | btnCancel.Visible = false;
287 | btnExit.Enabled = true;
288 | CheckIfCanUpdate();
289 | }
290 | else
291 | {
292 | btnUpdate.Visible = false;
293 | btnCancel.Visible = false;
294 | btnExit.Enabled = true;
295 | if (_server.GetPlayCount() >= 0 || _server.GetInProgressRecordingCount() >= 0)
296 | {
297 | lblPlayCount.Text = _server.PlayCount.ToString();
298 | lblInProgressRecordingCount.Text = _server.InProgressRecordingCount.ToString();
299 | }
300 | else
301 | {
302 | lblPlayCount.Text = "Unknown";
303 | lblInProgressRecordingCount.Text = "Unknown";
304 | }
305 | }
306 | }
307 | catch (LocalSystem.Msi.MSIException ex)
308 | {
309 | MessageBox.Show(
310 | $"MSI exception: {ex.Message}",
311 | "Plex Updater Error",
312 | MessageBoxButtons.OK,
313 | MessageBoxIcon.Error);
314 | Log.Write(ex);
315 | ToBeClosed = true;
316 |
317 | }
318 | catch (AppNotInstalledException ex)
319 | {
320 | MessageBox.Show(
321 | "The Plex Server application is not installed.",
322 | "Plex Updater Error",
323 | MessageBoxButtons.OK,
324 | MessageBoxIcon.Error);
325 | Log.Write(ex);
326 | ToBeClosed = true;
327 | }
328 | catch (ServiceNotInstalledException ex)
329 | {
330 | MessageBox.Show(
331 | "The Plex service is not installed.",
332 | "Plex Updater Error",
333 | MessageBoxButtons.OK,
334 | MessageBoxIcon.Error);
335 | Log.Write(ex);
336 | ToBeClosed = true;
337 | }
338 | catch (PlexDataFolderNotFoundException ex)
339 | {
340 | MessageBox.Show(
341 | "The Plex data folder could not be found.",
342 | "Plex Updater Error",
343 | MessageBoxButtons.OK,
344 | MessageBoxIcon.Error);
345 | Log.Write(ex);
346 | ToBeClosed = true;
347 | }
348 | catch (LocalSystem.WindowsUserSidNotFound ex)
349 | {
350 | MessageBox.Show(
351 | "The SID for the Plex service user could not be found.",
352 | "Plex Updater Error",
353 | MessageBoxButtons.OK,
354 | MessageBoxIcon.Error);
355 | Log.Write(ex);
356 | ToBeClosed = true;
357 | }
358 |
359 | catch (Exception ex)
360 | {
361 | MessageBox.Show(
362 | ex.Message,
363 | "Plex Updater Error",
364 | MessageBoxButtons.OK,
365 | MessageBoxIcon.Error);
366 | Log.Write(ex);
367 | ToBeClosed = true;
368 | }
369 | }
370 |
371 | ///
372 | /// Perform the server update.
373 | ///
374 | private void PerformUpdate()
375 | {
376 | try
377 | {
378 | btnUpdate.Enabled = false;
379 | btnCancel.Visible = false;
380 | btnExit.Enabled = false;
381 |
382 | if (_cts == null)
383 | {
384 | _cts = new CancellationTokenSource();
385 | }
386 |
387 | CancellationToken ct = _cts.Token;
388 |
389 | Task plexUpdate = Task.Factory.StartNew(() =>
390 | {
391 | // Throw an exception if already cancelled
392 | ct.ThrowIfCancellationRequested();
393 |
394 | _server.Update();
395 | }, _cts.Token);
396 |
397 | plexUpdate.Wait();
398 |
399 | if (!_server.IsRunning())
400 | {
401 | Log.Write("The Plex server was not started successfully.");
402 | }
403 | }
404 | catch (Exception ex)
405 | {
406 | txtUpdateStatus.Text += $"ERROR: {ex.Message}{NewLine}";
407 | Log.Write(ex);
408 | }
409 | finally
410 | {
411 | _cts?.Dispose();
412 | Initialize();
413 | }
414 | }
415 | #endregion
416 |
417 |
418 | }
419 | }
420 |
--------------------------------------------------------------------------------
/MainForm.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 |
--------------------------------------------------------------------------------
/Plex/Api.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.IO;
4 | using System.Net.Http;
5 | using System.Linq;
6 | using System.Text;
7 | using System.Threading.Tasks;
8 | using System.Xml;
9 | using System.Xml.Linq;
10 | using System.Xml.Serialization;
11 |
12 | namespace TE.Plex
13 | {
14 | public class Api
15 | {
16 | #region Event Delegates
17 | ///
18 | /// The delegate for the Message event handler.
19 | ///
20 | ///
21 | /// The object that triggered the event.
22 | ///
23 | ///
24 | /// The message.
25 | ///
26 | public delegate void MessageChangedEventHandler(object sender, string messagee);
27 | #endregion
28 |
29 | #region Events
30 | ///
31 | /// The MessageChanged event member.
32 | ///
33 | public event MessageChangedEventHandler MessageChanged;
34 |
35 | ///
36 | /// Triggered when the message has changed.
37 | ///
38 | protected virtual void OnMessageChanged(string message)
39 | {
40 | MessageChanged?.Invoke(this, message);
41 | }
42 | #endregion
43 |
44 | #region Public Constants
45 | ///
46 | /// A constant representing an unknown value.
47 | ///
48 | public const int Unknown = -1;
49 | #endregion
50 |
51 | #region Private Variables
52 | ///
53 | /// The Plex server.
54 | ///
55 | private string _server;
56 |
57 | ///
58 | /// The Plex user token.
59 | ///
60 | private string _token;
61 |
62 | ///
63 | /// The HTTP client used to connect to the Plex website.
64 | ///
65 | private HttpClient _client = new HttpClient();
66 | #endregion
67 |
68 | #region Constructors
69 | ///
70 | /// Creates an instance of the class when provided
71 | /// with the server name or IP address, and the Plex token.
72 | ///
73 | ///
74 | /// The name or IP address of the Plex server.
75 | ///
76 | ///
77 | /// The user's Plex token.
78 | ///
79 | public Api(string server, string token)
80 | {
81 | _server = server;
82 | _token = token;
83 | }
84 | #endregion
85 |
86 | #region Public Functions
87 | ///
88 | /// Gets the number of media currently being played on the Plex server.
89 | ///
90 | ///
91 | /// The number of items being played.
92 | ///
93 | public int GetPlayCount()
94 | {
95 | int playCount = Unknown;
96 | if (string.IsNullOrWhiteSpace(_server))
97 | {
98 | OnMessageChanged("The Plex server was not provided so the play count could not be retrieved.");
99 | return playCount;
100 | }
101 |
102 | if (string.IsNullOrWhiteSpace(_token))
103 | {
104 | OnMessageChanged("The Plex token was not provided so the play count could not be retrieved.");
105 | return playCount;
106 | }
107 |
108 | string url = $"http://{_server}:32400/status/sessions?X-Plex-Token={_token}";
109 | string content = null;
110 | try
111 | {
112 | using (HttpResponseMessage response = _client.GetAsync(url).Result)
113 | {
114 | if (response.StatusCode == System.Net.HttpStatusCode.OK)
115 | {
116 | content = response.Content.ReadAsStringAsync().Result;
117 | }
118 | else
119 | {
120 | OnMessageChanged($"The connection to the Plex server wasn't successful. Status: {response.StatusCode.ToString()}.");
121 | }
122 | }
123 | }
124 | catch (Exception ex)
125 | when (ex is ArgumentNullException || ex is HttpRequestException)
126 | {
127 | OnMessageChanged($"There was an issue sending the request to the Plex server. Reason: {ex.Message}");
128 | return playCount;
129 | }
130 | catch (AggregateException ae)
131 | {
132 | foreach (var e in ae.Flatten().InnerExceptions)
133 | {
134 | OnMessageChanged($"Could not process the response result. Message: {e.Message}.");
135 | }
136 | return playCount;
137 | }
138 |
139 | if (string.IsNullOrWhiteSpace(content))
140 | {
141 | OnMessageChanged("No content was returned from the Plex server.");
142 | return playCount;
143 | }
144 |
145 | using (StringReader sr = new StringReader(content))
146 | {
147 | XmlSerializer serializer =
148 | new XmlSerializer(typeof(MediaContainer));
149 | try
150 | {
151 | MediaContainer mediaContainer =
152 | (MediaContainer)serializer.Deserialize(sr);
153 | playCount = Convert.ToInt32(mediaContainer.Size);
154 | }
155 | catch (Exception ex)
156 | when (ex is InvalidOperationException || ex is FormatException || ex is OverflowException)
157 | {
158 | OnMessageChanged($"The content could not be parsed. Reason: {ex.Message}");
159 | return playCount;
160 | }
161 | }
162 |
163 | return playCount;
164 | }
165 |
166 | ///
167 | /// Gets the number of in progress recordings (i.e. by the DVR) on the Plex server.
168 | ///
169 | /// ///
170 | /// The number of items being currently recorded.
171 | ///
172 | public int GetInProgressRecordingCount()
173 | {
174 | int inProgressRecordingCount = Unknown;
175 | if (string.IsNullOrWhiteSpace(_server))
176 | {
177 | OnMessageChanged("The Plex server was not provided so the in progress recording count could not be retrieved.");
178 | return inProgressRecordingCount;
179 | }
180 |
181 | if (string.IsNullOrWhiteSpace(_token))
182 | {
183 | OnMessageChanged("The Plex token was not provided so the in progress recording count could not be retrieved.");
184 | return inProgressRecordingCount;
185 | }
186 |
187 | string url = $"http://{_server}:32400/media/subscriptions/scheduled?X-Plex-Token={_token}";
188 | string content = null;
189 | try
190 | {
191 | using (HttpResponseMessage response = _client.GetAsync(url).Result)
192 | {
193 | if (response.StatusCode == System.Net.HttpStatusCode.OK)
194 | {
195 | content = response.Content.ReadAsStringAsync().Result;
196 | }
197 | else
198 | {
199 | OnMessageChanged($"The connection to the Plex server wasn't successful. Status: {response.StatusCode.ToString()}.");
200 | }
201 | }
202 | }
203 | catch (Exception ex)
204 | when (ex is ArgumentNullException || ex is HttpRequestException)
205 | {
206 | OnMessageChanged($"There was an issue sending the request to the Plex server. Reason: {ex.Message}");
207 | return inProgressRecordingCount;
208 | }
209 | catch (AggregateException ae)
210 | {
211 | foreach (var e in ae.Flatten().InnerExceptions)
212 | {
213 | OnMessageChanged($"Could not process the response result. Message: {e.Message}.");
214 | }
215 | return inProgressRecordingCount;
216 | }
217 |
218 | if (string.IsNullOrWhiteSpace(content))
219 | {
220 | OnMessageChanged("No content was returned from the Plex server.");
221 | return inProgressRecordingCount;
222 | }
223 |
224 | try
225 | {
226 | var xml = XDocument.Parse(content);
227 | inProgressRecordingCount = xml.Descendants("MediaGrabOperation").Count(x => (string)x.Attribute("status") == "inprogress");
228 | }
229 | catch (Exception ex)
230 | when (ex is XmlException)
231 | {
232 | OnMessageChanged($"The content could not be parsed. Reason: {ex.Message}");
233 | return inProgressRecordingCount;
234 | }
235 |
236 | return inProgressRecordingCount;
237 | }
238 | #endregion
239 | }
240 | }
241 |
--------------------------------------------------------------------------------
/Plex/AppNotInstalledException.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Runtime.Serialization;
3 |
4 | namespace TE.Plex
5 | {
6 | ///
7 | /// An application is not installed.
8 | ///
9 | [Serializable]
10 | public class AppNotInstalledException : Exception
11 | {
12 | public AppNotInstalledException() { }
13 |
14 | public AppNotInstalledException(string message)
15 | : base(message) { }
16 |
17 | public AppNotInstalledException(
18 | string message,
19 | Exception innerException)
20 | : base(message, innerException) { }
21 |
22 | protected AppNotInstalledException(
23 | SerializationInfo info,
24 | StreamingContext context)
25 | : base(info, context) { }
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/Plex/EventSource.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using System.Text;
5 | using System.Threading.Tasks;
6 |
7 | namespace TE.Plex
8 | {
9 | public class EventSource
10 | {
11 | #region Event Delegates
12 | ///
13 | /// The delegate for the Message event handler.
14 | ///
15 | ///
16 | /// The object that triggered the event.
17 | ///
18 | ///
19 | /// The message.
20 | ///
21 | public delegate void MessageChangedEventHandler(object sender, string messagee);
22 | #endregion
23 |
24 | #region Events
25 | ///
26 | /// The MessageChanged event member.
27 | ///
28 | public event MessageChangedEventHandler MessageChanged;
29 |
30 | ///
31 | /// Triggered when the message has changed.
32 | ///
33 | protected virtual void OnMessageChanged(string message)
34 | {
35 | MessageChanged?.Invoke(this, message);
36 | }
37 | #endregion
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/Plex/MediaContainer.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using System.Text;
5 | using System.Threading.Tasks;
6 | using System.Xml;
7 | using System.Xml.Serialization;
8 |
9 | namespace TE.Plex
10 | {
11 | ///
12 | /// The MediaContaier element in the XML file.
13 | ///
14 | [XmlRoot("MediaContainer")]
15 | public class MediaContainer
16 | {
17 | ///
18 | /// The size of the current play list.
19 | ///
20 | [XmlAttribute("size")]
21 | public string Size { get; set; }
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/Plex/PlexDataFolderNotFoundException.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Runtime.Serialization;
3 |
4 | namespace TE.Plex
5 | {
6 | ///
7 | /// An application is not installed.
8 | ///
9 | [Serializable]
10 | public class PlexDataFolderNotFoundException : Exception
11 | {
12 | public PlexDataFolderNotFoundException() { }
13 |
14 | public PlexDataFolderNotFoundException(string message)
15 | : base(message) { }
16 |
17 | public PlexDataFolderNotFoundException(
18 | string message,
19 | Exception innerException)
20 | : base(message, innerException) { }
21 |
22 | protected PlexDataFolderNotFoundException(
23 | SerializationInfo info,
24 | StreamingContext context)
25 | : base(info, context) { }
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/Plex/Registry.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.IO;
4 | using System.Linq;
5 | using System.Security;
6 | using System.Text;
7 | using System.Threading.Tasks;
8 | using Win32 = Microsoft.Win32;
9 | using TE.LocalSystem;
10 |
11 | namespace TE.Plex
12 | {
13 | ///
14 | /// Contains the methods used to get the Plex data values from the Windows
15 | /// registry.
16 | ///
17 | internal class Registry : EventSource
18 | {
19 | ///
20 | /// The registry key tree for the Plex information.
21 | ///
22 | private const string RegistryPlexKey = @"SOFTWARE\Plex, Inc.\Plex Media Server\";
23 |
24 | ///
25 | /// The registry run key that starts Plex Media Server at Windows
26 | /// startup.
27 | ///
28 | private const string RegistryRunKey = @"Software\Microsoft\Windows\CurrentVersion\Run";
29 |
30 | ///
31 | /// The registry value that starts Plex Media Server at Windows
32 | /// startup.
33 | ///
34 | private const string RegistryRunValue = "Plex Media Server";
35 |
36 | ///
37 | /// The name of the local Plex data path registry value.
38 | ///
39 | private const string RegistryPlexDataPathValueName = "LocalAppDataPath";
40 |
41 | ///
42 | /// The location of the Plex installation.
43 | ///
44 | private const string RegistryInstallFolder = "InstallFolder";
45 |
46 | ///
47 | /// The user running the Plex server application.
48 | ///
49 | private WindowsUser user;
50 |
51 | ///
52 | /// The path to the Plex registry keys.
53 | ///
54 | private string plexRegistryPath;
55 |
56 | ///
57 | /// Creates an instance of the class when
58 | /// provided with the user that is running the Plex server.
59 | ///
60 | ///
61 | /// The user that is running Plex.
62 | ///
63 | ///
64 | /// The parameter is null.
65 | ///
66 | internal Registry(WindowsUser plexUser)
67 | {
68 | user = plexUser ?? throw new ArgumentNullException(nameof(plexUser));
69 | plexRegistryPath = $"{user.Sid}\\{RegistryPlexKey}";
70 | }
71 |
72 | ///
73 | /// Gets the value from the registry.
74 | ///
75 | ///
76 | /// The name of the value.
77 | ///
78 | ///
79 | /// The value associated with the , or null if
80 | /// name is not found.
81 | ///
82 | ///
83 | /// The parameter is null or not provided.
84 | ///
85 | ///
86 | /// The RegistryKey that contains the specified value is closed (closed keys cannot be accessed).
87 | ///
88 | ///
89 | /// The user does not have the permissions required to read from the registry key.
90 | ///
91 | ///
92 | /// The RegistryKey that contains the specified value has been marked for deletion.
93 | ///
94 | ///
95 | /// The user does not have the necessary registry rights.
96 | ///
97 | private object GetValue(string name)
98 | {
99 | if (string.IsNullOrWhiteSpace(name))
100 | {
101 | throw new ArgumentNullException(nameof(name));
102 | }
103 |
104 | using (Win32.RegistryKey plexRegistry =
105 | Win32.Registry.Users.OpenSubKey(plexRegistryPath))
106 | {
107 | return plexRegistry.GetValue(name);
108 | }
109 | }
110 |
111 | ///
112 | /// Delete the Plex Server run keys for both the user that performed
113 | /// the installation, and the user associated with the Plex Service.
114 | ///
115 | ///
116 | /// True if the registry value has been deleted, false if the value
117 | /// could not be deleted.
118 | ///
119 | internal bool DeleteRunValue()
120 | {
121 | try
122 | {
123 | using (Win32.RegistryKey key = Win32.Registry.CurrentUser.OpenSubKey(RegistryRunKey, true))
124 | {
125 | if (key != null)
126 | {
127 | try
128 | {
129 | if (key.GetValue(RegistryRunValue) == null)
130 | {
131 | return true;
132 | }
133 |
134 | key.DeleteValue(RegistryRunValue);
135 | return (key.GetValue(RegistryRunValue) == null);
136 | }
137 | catch (ArgumentException)
138 | {
139 | return true;
140 | }
141 | catch (Exception ex)
142 | when (ex is ObjectDisposedException || ex is IOException || ex is SecurityException || ex is UnauthorizedAccessException)
143 | {
144 | OnMessageChanged($"The Run key value could not be deleted. Reason: {ex.Message}");
145 | }
146 | }
147 | }
148 | }
149 | catch (Exception ex)
150 | when (ex is ObjectDisposedException || ex is SecurityException)
151 | {
152 | OnMessageChanged($"The Run key value could not be deleted. Reason: {ex.Message}");
153 | }
154 |
155 | return false;
156 | }
157 |
158 | ///
159 | /// Gets the location of the Plex installation folder.
160 | ///
161 | ///
162 | /// The location of the Plex installation folder, otherwise null.
163 | ///
164 | internal string GetInstallFolder()
165 | {
166 | try
167 | {
168 | // Get the Plex local data folder from the users registry hive
169 | // for the user ID associated with the Plex service
170 | return (string)GetValue(RegistryInstallFolder);
171 | }
172 | catch (Exception ex)
173 | when (ex is ArgumentNullException || ex is ObjectDisposedException || ex is SecurityException || ex is IOException || ex is UnauthorizedAccessException)
174 | {
175 | return null;
176 | }
177 | }
178 |
179 | ///
180 | /// Gets the local Plex data folder used by the Plex service.
181 | ///
182 | ///
183 | /// The full path to the local Plex data folder.
184 | ///
185 | internal string GetLocalDataFolder()
186 | {
187 | string folder;
188 |
189 | try
190 | {
191 | // Get the Plex local data folder from the users registry hive
192 | // for the user ID associated with the Plex service
193 | folder = (string)GetValue(RegistryPlexDataPathValueName);
194 | }
195 | catch (Exception ex)
196 | when (ex is ArgumentNullException || ex is ObjectDisposedException || ex is SecurityException || ex is IOException || ex is UnauthorizedAccessException)
197 | {
198 | OnMessageChanged($"The Plex local data folder could not be determined. Reason: {ex.Message}");
199 | folder = null;
200 | }
201 |
202 | if (string.IsNullOrEmpty(folder))
203 | {
204 | // Default to the standard local application data folder
205 | // for the Plex service user is the LocalAppDataPath value
206 | // is missing from the registry
207 | folder = user.LocalAppDataFolder;
208 | }
209 |
210 | return folder;
211 | }
212 |
213 | ///
214 | /// Gets the Plex token for the logged in Plex user.
215 | ///
216 | ///
217 | /// A Plex token or null if the token could not be retrieved.
218 | ///
219 | internal string GetToken()
220 | {
221 | try
222 | {
223 | return (string)GetValue("PlexOnlineToken");
224 | }
225 | catch (Exception ex)
226 | when (ex is ArgumentNullException || ex is ObjectDisposedException || ex is SecurityException || ex is IOException || ex is UnauthorizedAccessException)
227 | {
228 | OnMessageChanged($"The user token could not be determined. Reason: {ex.Message}");
229 | return null;
230 | }
231 |
232 | }
233 |
234 | ///
235 | /// Gets the update channel specified in Plex.
236 | ///
237 | ///
238 | /// An value indicating which channel to
239 | /// use to download the update.
240 | ///
241 | internal UpdateChannel GeUpdateChannel()
242 | {
243 | string value = null;
244 | try
245 | {
246 | value = (string)GetValue("ButlerUpdateChannel");
247 | }
248 | catch (Exception ex)
249 | when (ex is ArgumentNullException || ex is ObjectDisposedException || ex is SecurityException || ex is IOException || ex is UnauthorizedAccessException)
250 | {
251 | return UpdateChannel.Public;
252 | }
253 |
254 | if (value == null)
255 | {
256 | return UpdateChannel.Public;
257 | }
258 |
259 | int updateChannel;
260 | if (!int.TryParse(value, out updateChannel))
261 | {
262 | return UpdateChannel.Public;
263 | }
264 |
265 | return updateChannel == (int)UpdateChannel.PlexPass ? UpdateChannel.PlexPass : UpdateChannel.Public;
266 | }
267 | }
268 | }
269 |
--------------------------------------------------------------------------------
/Plex/ServerService.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using static System.Environment;
3 | using System.Linq;
4 | using System.Management;
5 | using System.ServiceProcess;
6 | using TE.LocalSystem;
7 | using System.Configuration;
8 |
9 | namespace TE.Plex
10 | {
11 | ///
12 | /// The properties and methods associated with the Plex Media Server
13 | /// service.
14 | ///
15 | public class ServerService
16 | {
17 | #region Constants
18 | ///
19 | /// The name of the Plex service.
20 | ///
21 | private static string ServiceName = ConfigurationManager.AppSettings["PlexServiceName"];
22 | #endregion
23 |
24 | #region Properties
25 | ///
26 | /// Gets the user ID used to run the service.
27 | ///
28 | public WindowsUser LogOnUser { get; private set; }
29 | #endregion
30 |
31 | #region Constructors
32 | ///
33 | /// Creates an instance of the
34 | /// class.
35 | ///
36 | ///
37 | /// The Plex Media Server service is not installed.
38 | ///
39 | ///
40 | /// The Plex Media Server service account SID could not be found.
41 | ///
42 | public ServerService()
43 | {
44 | try
45 | {
46 | // Get the LogOnUser for the Plex Media Server service
47 | LogOnUser = GetServiceUser();
48 | }
49 | catch (WindowsUserSidNotFound)
50 | {
51 | throw new WindowsUserSidNotFound(
52 | "The Plex Media Server service account SID could not be found.");
53 | }
54 |
55 | // If a WindowsUser object was not returned, throw an exception
56 | // indicating the service does not exist
57 | if (LogOnUser == null)
58 | {
59 | throw new ServiceNotInstalledException(
60 | "The Plex Media Server service is not installed.");
61 | }
62 | }
63 | #endregion
64 |
65 | #region Private Functions
66 | ///
67 | /// Gets the name of the Plex service log on user name.
68 | ///
69 | ///
70 | /// A object of the service
71 | /// log on user.
72 | ///
73 | private WindowsUser GetServiceUser()
74 | {
75 | Log.Write("Getting the service user.");
76 | WindowsUser user = null;
77 |
78 | if (IsInstalled())
79 | {
80 | Log.Write("The Plex service is installed. Let's get the user associated with the service.");
81 | ManagementObject service =
82 | new ManagementObject(
83 | $"Win32_Service.Name='{ServiceName}'");
84 |
85 | if (service == null)
86 | {
87 | Log.Write("The service user could not be found.");
88 | return null;
89 | }
90 |
91 | service.Get();
92 | user = new WindowsUser(service["startname"].ToString().Replace(
93 | @".\", $"{MachineName}\\"));
94 |
95 | Log.Write($"The Plex service user: {user.Name}.");
96 | }
97 | else
98 | {
99 | Log.Write("The Plex service is not installed.");
100 | }
101 |
102 | return user;
103 | }
104 | #endregion
105 |
106 | #region Public Functions
107 | ///
108 | /// Checks to see if the Plex service is installed.
109 | ///
110 | ///
111 | /// True if the service is installed, false if it isn't installed.
112 | ///
113 | public static bool IsInstalled()
114 | {
115 | return ServiceController.GetServices().Any(
116 | s => s.ServiceName == ServiceName);
117 | }
118 |
119 | ///
120 | /// Stops the Plex Media Server service.
121 | ///
122 | public void Stop()
123 | {
124 | if (IsInstalled())
125 | {
126 | using (ServiceController sc = new ServiceController(ServiceName))
127 | {
128 | if (sc.Status == ServiceControllerStatus.Running)
129 | {
130 | sc.Stop();
131 | sc.WaitForStatus(ServiceControllerStatus.Stopped);
132 | }
133 | }
134 | }
135 | }
136 |
137 | ///
138 | /// Starts the Plex Media Server service.
139 | ///
140 | public void Start()
141 | {
142 | if (IsInstalled())
143 | {
144 | using (ServiceController sc = new ServiceController(ServiceName))
145 | {
146 | sc.Start();
147 | sc.WaitForStatus(ServiceControllerStatus.Running);
148 | }
149 | }
150 | }
151 | #endregion
152 | }
153 | }
154 |
--------------------------------------------------------------------------------
/Plex/ServiceNotInstalledException.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Runtime.Serialization;
3 |
4 | namespace TE.Plex
5 | {
6 | ///
7 | /// A service is not installed.
8 | ///
9 | public class ServiceNotInstalledException : Exception
10 | {
11 | public ServiceNotInstalledException() { }
12 |
13 | public ServiceNotInstalledException(string message)
14 | : base(message) { }
15 |
16 | public ServiceNotInstalledException(
17 | string message,
18 | Exception innerException)
19 | : base(message, innerException) { }
20 |
21 | protected ServiceNotInstalledException(
22 | SerializationInfo info,
23 | StreamingContext context)
24 | : base(info, context) { }
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/Plex/SilentUpdate.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Diagnostics;
3 | using static System.Environment;
4 | using System.IO;
5 | using System.Timers;
6 | using TE.LocalSystem;
7 |
8 | namespace TE.Plex
9 | {
10 | ///
11 | /// Executes a silent Plex Media Server update.
12 | ///
13 | public class SilentUpdate
14 | {
15 | #region Constants
16 | ///
17 | /// The default wait time in seconds.
18 | ///
19 | public const int DefaultWaitTime = 30;
20 | #endregion
21 |
22 | #region Private Variables
23 | ///
24 | /// The media server object.
25 | ///
26 | private MediaServer _server = null;
27 |
28 | ///
29 | /// The wait timer.
30 | ///
31 | private Timer _timer = null;
32 | #endregion
33 |
34 | #region Properties
35 | ///
36 | /// Gets or sets the flag indicating that the update is forced to be
37 | /// installed regardless if any item is currently being played.
38 | ///
39 | public bool ForceUpdate { get; set; } = false;
40 |
41 | ///
42 | /// Gets or sets the default wait time in seconds.
43 | ///
44 | public int WaitTime { get; set; } = DefaultWaitTime;
45 | #endregion
46 |
47 | #region Constructors
48 | ///
49 | /// Initializes an instance of the class.
50 | ///
51 | ///
52 | /// Plex is not installed.
53 | ///
54 | ///
55 | /// The Plex service is not installed.
56 | ///
57 | ///
58 | /// The Windows user SID is not found.
59 | ///
60 | public SilentUpdate()
61 | {
62 | Initialize(null);
63 | }
64 |
65 | ///
66 | /// Initializes an instance of the class.
67 | ///
68 | ///
69 | /// Specifies a path to the installation log.
70 | ///
71 | ///
72 | /// Plex is not installed.
73 | ///
74 | ///
75 | /// The Plex service is not installed.
76 | ///
77 | ///
78 | /// The Windows user SID is not found.
79 | ///
80 | public SilentUpdate(string logPath)
81 | {
82 | Initialize(logPath);
83 | }
84 | #endregion
85 |
86 | #region Events
87 | ///
88 | /// Writes any messages from the Plex Media Server update to a log
89 | /// file.
90 | ///
91 | ///
92 | /// The sender object.
93 | ///
94 | ///
95 | /// The message to write to the log file.
96 | ///
97 | private void ServerUpdateMessage(object sender, string message)
98 | {
99 | Log.Write(message);
100 | }
101 |
102 | ///
103 | /// The timer has elapsed so check the play count to see if the server
104 | /// is in use.
105 | ///
106 | ///
107 | /// The sender.
108 | ///
109 | ///
112 | private void OnTimedEvent(object sender, ElapsedEventArgs e)
113 | {
114 | if (_server == null)
115 | {
116 | _timer.Enabled = false;
117 | return;
118 | }
119 |
120 | if (CheckIfCanUpdate())
121 | {
122 | PerformUpdate();
123 | }
124 | }
125 | #endregion
126 |
127 | #region Private Functions
128 | ///
129 | /// Checks to see if the server can be updated at this time.
130 | ///
131 | private bool CheckIfCanUpdate()
132 | {
133 | if (_server == null)
134 | {
135 | Log.Write("The server was not specified. Cannot perform the update.");
136 | _timer.Enabled = false;
137 | return false;
138 | }
139 |
140 | int playCount = _server.GetPlayCount();
141 | int inProgressRecordingCount = _server.GetInProgressRecordingCount();
142 |
143 | // No item is currently being played
144 | if (playCount == 0 && inProgressRecordingCount == 0)
145 | {
146 | Log.Write("The server is not in use continuing to perform the update.");
147 | _timer.Enabled = false;
148 | return true;
149 | }
150 | // At least one item is being played
151 | else if (playCount > 0 || inProgressRecordingCount > 0)
152 | {
153 | if (!ForceUpdate)
154 | {
155 | Log.Write("The server is in use. Waiting for all media and/or in progress recordings to be stopped before performing the update.");
156 | _timer.Interval =
157 | Convert.ToDouble(Math.Abs(WaitTime) * 1000);
158 | _timer.Enabled = true;
159 | return false;
160 | }
161 | else if (ForceUpdate && inProgressRecordingCount > 0)
162 | {
163 | Log.Write("The server cannot be forcefully updated while there is a recording in progress. Waiting for all in progress recordings to be stopped before performing the update.");
164 | _timer.Interval =
165 | Convert.ToDouble(Math.Abs(WaitTime) * 1000);
166 | _timer.Enabled = true;
167 | return false;
168 | }
169 | else
170 | {
171 | Log.Write("The update is set to be force. The update will continue.");
172 | _timer.Enabled = false;
173 | return true;
174 | }
175 | }
176 | // Could not determine how many items are being played
177 | else
178 | {
179 | Log.Write("The server in use status could not be determined. The server can be updated if you wish.");
180 | _timer.Enabled = false;
181 | return true;
182 | }
183 | }
184 |
185 | ///
186 | /// Initializes the properties and variables for the class.
187 | ///
188 | ///
189 | /// Plex is not installed.
190 | ///
191 | ///
192 | /// The Plex service is not installed.
193 | ///
194 | ///
195 | /// The Windows user SID is not found.
196 | ///
197 | private void Initialize(string logPath)
198 | {
199 | try
200 | {
201 | _server = new MediaServer(logPath, ServerUpdateMessage);
202 | _timer = new Timer(DefaultWaitTime * 1000);
203 | _timer.Elapsed += OnTimedEvent;
204 | _timer.Enabled = false;
205 | }
206 | catch (AppNotInstalledException)
207 | {
208 | Log.Write(
209 | "The Plex Media Server is not installed.");
210 | throw;
211 | }
212 | catch (ServiceNotInstalledException)
213 | {
214 | Log.Write(
215 | "The Plex Media Server service is not installed.");
216 | throw;
217 | }
218 | catch (WindowsUserSidNotFound ex)
219 | {
220 | Log.Write(ex.Message);
221 | throw;
222 | }
223 | catch (Exception ex)
224 | {
225 | Log.Write(ex);
226 | throw;
227 | }
228 | }
229 |
230 | ///
231 | /// Perform the server update.
232 | ///
233 | private void PerformUpdate()
234 | {
235 | try
236 | {
237 | Log.Write("Update is available");
238 | _server.Update();
239 | }
240 | catch (Exception ex)
241 | {
242 | Log.Write(ex.Message);
243 | Log.Write(ex.StackTrace);
244 | }
245 | }
246 | #endregion
247 |
248 | #region Public Functions
249 | ///
250 | /// Gets the value indicating that the Plex server is running.
251 | ///
252 | ///
253 | /// True if the Plex server is running, false if the server is not
254 | /// running.
255 | ///
256 | public bool IsPlexRunning()
257 | {
258 | return _server.IsRunning();
259 | }
260 | ///
261 | /// Runs the Plex Media Server update.
262 | ///
263 | public void Run()
264 | {
265 | try
266 | {
267 | Log.Write("Checking for server update.");
268 | if (_server.IsUpdateAvailable())
269 | {
270 | if (CheckIfCanUpdate())
271 | {
272 | Log.Write("Update is available");
273 | _server.Update();
274 | }
275 | }
276 | else
277 | {
278 | Log.Write("No update is available. Exiting.");
279 | }
280 | }
281 | catch (Exception ex)
282 | {
283 | Log.Write(ex.Message);
284 | Log.Write(ex.StackTrace);
285 | }
286 | }
287 | #endregion
288 | }
289 | }
290 |
--------------------------------------------------------------------------------
/Plex/Update/CurrentVersion.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using System.Text;
5 | using System.Threading.Tasks;
6 | using Newtonsoft.Json;
7 |
8 | namespace TE.Plex.Update
9 | {
10 | ///
11 | /// The current version of Plex Media Server.
12 | ///
13 | public class CurrentVersion
14 | {
15 | ///
16 | /// Gets or sets the computer Plex Media Server releases.
17 | ///
18 | [JsonProperty("computer")]
19 | public Dictionary Computer { get; set; }
20 |
21 | ///
22 | /// Gets or sets the NAS Plex Server releases.
23 | ///
24 | [JsonProperty("nas")]
25 | public Dictionary Nas { get; set; }
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/Plex/Update/Package.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.IO;
3 | using System.Net.Http;
4 | using System.Security.Cryptography;
5 | using System.Text.RegularExpressions;
6 | using System.Threading.Tasks;
7 | using Newtonsoft.Json;
8 |
9 | namespace TE.Plex.Update
10 | {
11 | ///
12 | /// Properties and methods for downloading the latest version of the Plex
13 | /// Media Server for Windows.
14 | ///
15 | public class Package : EventSource
16 | {
17 | #region Private Constants
18 | ///
19 | /// The public URL to the JSON data that contains information about the
20 | /// latest Plex Media Server installs.
21 | ///
22 | private const string PlexPackagePublicJsonUrl =
23 | "https://plex.tv/api/downloads/1.json";
24 |
25 | ///
26 | /// The URL to the JSON data that contains information about the
27 | /// latest Plex Media Server installs.
28 | ///
29 | private const string PlexPackageJsonUrl =
30 | "https://plex.tv/api/downloads/5.json";
31 |
32 | ///
33 | /// The additional querystring to add to the URL to request the Plex
34 | /// Pass edition of the Plex install.
35 | ///
36 | private const string PlexPackageJsonUrlBeta = "?channel=plexpass";
37 | #endregion
38 |
39 | #region Private Variables
40 | ///
41 | /// The HTTP client used to connect to the Plex website.
42 | ///
43 | private HttpClient _client = new HttpClient();
44 |
45 | ///
46 | /// The value indicating if Plex is 64-bit.
47 | ///
48 | private bool _is64Bit = false;
49 |
50 | ///
51 | /// The local application data folder for Plex.
52 | ///
53 | private string _updatesFolder;
54 |
55 | ///
56 | /// The update channel used to update Plex.
57 | ///
58 | private UpdateChannel _updateChannel;
59 |
60 | ///
61 | /// The Plex user's token.
62 | ///
63 | private string _token;
64 | #endregion
65 |
66 | #region Properties
67 | ///
68 | /// Gets the latest Windows version of the Plex Media Server.
69 | ///
70 | public SystemType LatestWindowsVersion { get; private set; }
71 |
72 | ///
73 | /// Gets the path to the installation file once it has been downloaded
74 | /// from the Plex server. This value remains null, unless the
75 | /// method is called, and the file has been downloaded successfully.
76 | ///
77 | public string FilePath { get; private set; } = null;
78 | #endregion
79 |
80 | #region Constructors
81 | ///
82 | /// Creates an instance of the class
83 | /// when provided with the Plex user's registry key and the user's token.
84 | ///
85 | ///
86 | /// The local application data folder for Plex.
87 | ///
88 | ///
89 | /// The update channel used to update Plex.
90 | ///
91 | ///
92 | /// The Plex user's token.
93 | ///
94 | ///
95 | /// An argument provided is null.
96 | ///
97 | public Package(
98 | string updatesFolder,
99 | UpdateChannel updateChannel,
100 | string token,
101 | bool is64Bit)
102 | {
103 | _updatesFolder =
104 | updatesFolder ?? throw new ArgumentNullException(nameof(updatesFolder));
105 | _updateChannel = updateChannel;
106 | _token = token ?? throw new ArgumentNullException(nameof(token));
107 | _is64Bit = is64Bit;
108 | }
109 | #endregion
110 |
111 | #region Private Methods
112 | ///
113 | /// Get the checksum for a specified file.
114 | ///
115 | ///
116 | /// The full path to the file.
117 | ///
118 | ///
119 | /// The checksum for the file or null if the checksum could not
120 | /// be determined.
121 | ///
122 | private string GetChecksum(string filePath)
123 | {
124 | if (string.IsNullOrEmpty(filePath))
125 | {
126 | return null;
127 | }
128 |
129 | if (!File.Exists(filePath))
130 | {
131 | return null;
132 | }
133 |
134 | using (SHA1 sha = SHA1.Create())
135 | {
136 | try
137 | {
138 | using (var stream = File.OpenRead(filePath))
139 | {
140 | byte[] hash = sha.ComputeHash(stream);
141 |
142 | if (hash == null || hash.Length == 0)
143 | {
144 | return null;
145 | }
146 |
147 | return BitConverter.ToString(hash).Replace("-", "").ToLower();
148 | }
149 | }
150 | catch
151 | {
152 | return null;
153 | }
154 | }
155 | }
156 |
157 | ///
158 | /// Gets the filename for the latest install file.
159 | ///
160 | ///
161 | /// The filename of the latest install file or null if the latest
162 | /// install file could not be determined.
163 | ///
164 | private string GetFileName()
165 | {
166 | if (LatestWindowsVersion == null)
167 | {
168 | return null;
169 | }
170 |
171 | if (LatestWindowsVersion.Releases.Count == 0)
172 | {
173 | OnMessageChanged("WARN: There were no releases specified from Plex for Windows.");
174 | return null;
175 | }
176 |
177 | if (LatestWindowsVersion.Releases[0] == null)
178 | {
179 | OnMessageChanged("WARN: There were no releases specified from Plex for Windows.");
180 | return null;
181 | }
182 |
183 | string url = LatestWindowsVersion.GetUrl(_is64Bit);
184 | if (string.IsNullOrEmpty(url))
185 | {
186 | OnMessageChanged("WARN: The URL for the Windows release was not specified.");
187 | return null;
188 | }
189 |
190 | return Path.GetFileName(url);
191 | }
192 |
193 | ///
194 | /// Gets the full local path for the install package.
195 | ///
196 | ///
197 | /// The full path for the install package, or null if the full
198 | /// path could not be determined.
199 | ///
200 | private string GetFullPath()
201 | {
202 | if (LatestWindowsVersion == null)
203 | {
204 | return null;
205 | }
206 |
207 | // Get the local path for the latest install and verify the folder
208 | // exists
209 | string folder = GetPath();
210 | if (string.IsNullOrEmpty(folder))
211 | {
212 | return null;
213 | }
214 |
215 | // Get the full path to the latest install and then verify the file
216 | // exists
217 | string name = GetFileName();
218 | if (string.IsNullOrEmpty(name))
219 | {
220 | return null;
221 | }
222 |
223 | return Path.Combine(folder, name);
224 | }
225 |
226 | ///
227 | /// Gets the download package local path.
228 | ///
229 | ///
230 | /// The downloaded package local path or null if the path could not
231 | /// be determined.
232 | ///
233 | private string GetPath()
234 | {
235 | try
236 | {
237 | if (string.IsNullOrWhiteSpace(_updatesFolder))
238 | {
239 | return null;
240 | }
241 |
242 | if (LatestWindowsVersion == null)
243 | {
244 | return null;
245 | }
246 |
247 | if (string.IsNullOrWhiteSpace(LatestWindowsVersion.Version))
248 | {
249 | return null;
250 | }
251 |
252 | return Path.Combine(
253 | _updatesFolder,
254 | $@"{LatestWindowsVersion.Version}\packages");
255 | }
256 | catch (Exception ex)
257 | when (ex is ArgumentException || ex is System.Security.SecurityException)
258 | {
259 | return null;
260 | }
261 | }
262 |
263 | ///
264 | /// Gets the URL for the specified update channel that is specified
265 | /// on the Plex server.
266 | ///
267 | ///
268 | /// The URL for the specified update channel package, or the public URL
269 | /// if the URL could not be determined.
270 | ///
271 | private string GetUrl()
272 | {
273 | if (_updateChannel == UpdateChannel.PlexPass)
274 | {
275 | OnMessageChanged("The update channel is set for Plex Pass.");
276 | return PlexPackageJsonUrl + PlexPackageJsonUrlBeta;
277 | }
278 | else
279 | {
280 | OnMessageChanged("The update channel is set for public.");
281 | return PlexPackagePublicJsonUrl;
282 | }
283 | }
284 |
285 | ///
286 | /// Gets the JSON string value for the latest versions of Plex from
287 | /// the Plex download site.
288 | ///
289 | ///
290 | /// The JSON string value if the request was successful, or null if
291 | /// the request was not successful.
292 | ///
293 | private string GetJson()
294 | {
295 | string content;
296 |
297 | if (_token != null)
298 | {
299 | _client.DefaultRequestHeaders.Add("X-Plex-Token", _token);
300 | }
301 |
302 | try
303 | {
304 | // Get the URL for the package specified by the update channel
305 | // set in the Plex server
306 | string url = GetUrl();
307 | if (url == null)
308 | {
309 | return null;
310 | }
311 |
312 | OnMessageChanged($"Sending request to Plex: {url}");
313 | using (HttpResponseMessage response = _client.GetAsync(url).Result)
314 | {
315 | content = response.Content.ReadAsStringAsync().Result;
316 | }
317 |
318 | return content;
319 | }
320 | catch (HttpRequestException ex)
321 | {
322 | OnMessageChanged($"Could not get Plex package information. Message: {ex.Message}");
323 | return null;
324 | }
325 | catch (AggregateException ae)
326 | {
327 | foreach (var e in ae.Flatten().InnerExceptions)
328 | {
329 | OnMessageChanged($"Could not get Plex package information. Message: {e.Message}");
330 |
331 | if (e.InnerException != null)
332 | {
333 | OnMessageChanged($"Additional information: {e.InnerException.Message}");
334 | }
335 | }
336 | return null;
337 | }
338 | }
339 |
340 | ///
341 | /// Initialize the latest available of Plex Media Server to download.
342 | ///
343 | private void Initialize()
344 | {
345 | OnMessageChanged("Checking for the latest version from Plex.");
346 | string json = GetJson();
347 |
348 | if (string.IsNullOrEmpty(json))
349 | {
350 | OnMessageChanged("Could not get the latest version information from Plex.");
351 | return;
352 | }
353 |
354 | OnMessageChanged("Parsing the information from Plex.");
355 | CurrentVersion versions =
356 | JsonConvert.DeserializeObject(json);
357 |
358 | LatestWindowsVersion = versions.Computer["Windows"];
359 | if (LatestWindowsVersion == null)
360 | {
361 | OnMessageChanged("Could not get the latest version information from Plex.");
362 | return;
363 | }
364 | }
365 | #endregion
366 |
367 | #region Public Methods
368 | ///
369 | /// Downloads the latest Plex Media Server installation file for
370 | /// Windows.
371 | ///
372 | ///
373 | /// The URL for the installation.
374 | ///
375 | ///
376 | /// The full path where the file is to be saved.
377 | ///
378 | ///
379 | /// A of the download.
380 | ///
381 | ///
382 | /// If the file has already been downloaded, and the file is valid,
383 | /// then the file won't be downloaded again.
384 | ///
385 | public async Task Download()
386 | {
387 | OnMessageChanged("Getting ready to download the latest package.");
388 |
389 | if (LatestWindowsVersion == null)
390 | {
391 | Initialize();
392 | if (LatestWindowsVersion == null)
393 | {
394 | OnMessageChanged(
395 | "The latest Windows version has not been specified.");
396 | return false;
397 | }
398 | }
399 |
400 | // Verify that the URL for the latest release has been stored
401 | string url = LatestWindowsVersion.GetUrl(_is64Bit);
402 | if (string.IsNullOrEmpty(url))
403 | {
404 | OnMessageChanged(
405 | "The URL for the latest version was not specified.");
406 | return false;
407 | }
408 |
409 | FilePath = GetFullPath();
410 | if (string.IsNullOrEmpty(FilePath))
411 | {
412 | OnMessageChanged(
413 | "The path to the local downloaded install file could not be determined.");
414 | return false;
415 | }
416 |
417 | string directory = Path.GetDirectoryName(FilePath);
418 | if (!Directory.Exists(directory))
419 | {
420 | try
421 | {
422 | OnMessageChanged($"Creating folder: {directory}.");
423 | Directory.CreateDirectory(directory);
424 | }
425 | catch (Exception ex)
426 | {
427 | OnMessageChanged($"Could not create download folder. Reason: {ex.Message}");
428 | return false;
429 | }
430 | }
431 | else
432 | {
433 | // If the directory already exists, check to see if the file
434 | // also exists
435 | if (File.Exists(FilePath))
436 | {
437 | OnMessageChanged($"The file, {FilePath}, exists. Checking to see if the package is valid.");
438 | // If the file is valid - meaning the checksum matches the
439 | // checksum of the file to be downloaded, then return true
440 | // to avoid redownloading the same file a second time
441 | if (IsValid())
442 | {
443 | OnMessageChanged("Since the package is valid - not downloading again.");
444 | return true;
445 | }
446 | }
447 | }
448 |
449 | try
450 | {
451 | OnMessageChanged("Downloading the latest installation package from Plex.");
452 | // Get the response once it is available and the headers are read
453 | using (HttpResponseMessage response =
454 | await _client.GetAsync(
455 | url,
456 | HttpCompletionOption.ResponseHeadersRead).ConfigureAwait(
457 | false))
458 | {
459 | // Get the stream content
460 | using (Stream streamToReadFrom =
461 | await response.Content.ReadAsStreamAsync())
462 | {
463 | // Write the stream to the local file path
464 | using (Stream streamToWriteTo =
465 | File.Open(FilePath, FileMode.Create))
466 | {
467 | await streamToReadFrom.CopyToAsync(streamToWriteTo);
468 | }
469 | }
470 | }
471 | }
472 | catch (Exception ex)
473 | when (ex is HttpRequestException || ex is IOException || ex is UnauthorizedAccessException || ex is NotSupportedException)
474 | {
475 | OnMessageChanged($"Could not download update. Message: {ex.Message}.");
476 | return false;
477 | }
478 |
479 | // Check to see if the downloaded file is valid
480 | return IsValid();
481 | }
482 |
483 | ///
484 | /// Converts the string value of the downloaded file version into
485 | /// a object.
486 | ///
487 | ///
488 | /// A object that represents the version of
489 | /// the downloaded file.
490 | ///
491 | public Version GetVersion()
492 | {
493 | if (LatestWindowsVersion == null)
494 | {
495 | Initialize();
496 | if (LatestWindowsVersion == null)
497 | {
498 | return default;
499 | }
500 | }
501 |
502 | string version = LatestWindowsVersion.Version;
503 | if (string.IsNullOrEmpty(version))
504 | {
505 | return default;
506 | }
507 |
508 | // The regular expression used to parse the file version
509 | Regex regEx = new Regex(
510 | @"^(?\d+)\.(?\d+)\.(?\d+)\.(?\d+)\-\S+$");
511 |
512 | try
513 | {
514 | // Ensure that a match is made
515 | if (regEx.IsMatch(version))
516 | {
517 | // Find the first match for the regular expression in the value
518 | Match match = regEx.Match(version);
519 |
520 | // Return the version object
521 | Version fileVersion = new Version(
522 | Convert.ToInt32(match.Groups["Major"].Value),
523 | Convert.ToInt32(match.Groups["Minor"].Value),
524 | Convert.ToInt32(match.Groups["Build"].Value),
525 | Convert.ToInt32(match.Groups["Revision"].Value));
526 |
527 | OnMessageChanged(
528 | $"The latest file version available for download is {fileVersion.ToString()}.");
529 |
530 | return fileVersion;
531 | }
532 | else
533 | {
534 | return default;
535 | }
536 | }
537 | catch (Exception ex)
538 | when (ex is ArgumentOutOfRangeException || ex is RegexMatchTimeoutException)
539 | {
540 | return default;
541 | }
542 | }
543 |
544 | ///
545 | /// Checks to see if the downloaded install package is a valid package.
546 | ///
547 | ///
548 | public bool IsValid()
549 | {
550 | if (LatestWindowsVersion == null)
551 | {
552 | Initialize();
553 | if (LatestWindowsVersion == null)
554 | {
555 | OnMessageChanged(
556 | "The package is not valid. Could not get the latest version information from Plex.");
557 | return false;
558 | }
559 | }
560 |
561 | // Get the local path for the latest install and verify the folder
562 | // exists
563 | string folder = GetPath();
564 | if (string.IsNullOrEmpty(folder))
565 | {
566 | OnMessageChanged("The package folder could not be determined.");
567 | return false;
568 | }
569 |
570 | if (!Directory.Exists(folder))
571 | {
572 | OnMessageChanged(
573 | $"The package is not valid. The folder, {folder}, does not exist.");
574 | return false;
575 | }
576 |
577 | // Get the full path to the latest install and then verify the file
578 | // exists
579 | string name = GetFileName();
580 | if (string.IsNullOrEmpty(name))
581 | {
582 | OnMessageChanged(
583 | $"The package is not valid. Could not get the file name {name}.");
584 | return false;
585 | }
586 |
587 | string filePath = Path.Combine(folder, name);
588 | if (!File.Exists(filePath))
589 | {
590 | OnMessageChanged(
591 | $"The package is not valid. Could not find {filePath}.");
592 | return false;
593 | }
594 |
595 |
596 | // Get the checksum for the latest install and the validate that
597 | // the checksum matches the checksum from the Plex site
598 | string checksum = GetChecksum(filePath);
599 |
600 | OnMessageChanged("Checking if the installation package is valid.");
601 | bool isValid =
602 | checksum.Equals(LatestWindowsVersion.GetCheckSum(_is64Bit));
603 |
604 | if (isValid)
605 | {
606 | OnMessageChanged("The package is valid. The checksums match.");
607 | }
608 | else
609 | {
610 | OnMessageChanged("The package is not valid. The checksums match.");
611 | }
612 | return isValid;
613 | }
614 |
615 | ///
616 | /// Returns the string representation for this object.
617 | ///
618 | ///
619 | /// The string value.
620 | ///
621 | public override string ToString()
622 | {
623 | return FilePath;
624 | }
625 | #endregion
626 | }
627 | }
628 |
--------------------------------------------------------------------------------
/Plex/Update/Release.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using System.Text;
5 | using System.Threading.Tasks;
6 | using Newtonsoft.Json;
7 |
8 | namespace TE.Plex.Update
9 | {
10 | ///
11 | /// A release of the Plex Media Server.
12 | ///
13 | public class Release
14 | {
15 | ///
16 | /// The label associated with the release.
17 | ///
18 | [JsonProperty("label")]
19 | public string Label { get; set; }
20 |
21 | ///
22 | /// The name of the release.
23 | ///
24 | [JsonProperty("build")]
25 | public string Build { get; set; }
26 |
27 | ///
28 | /// The distribution of the release.
29 | ///
30 | [JsonProperty("distro")]
31 | public string Distro { get; set; }
32 |
33 | ///
34 | /// The download URL for the build.
35 | ///
36 | [JsonProperty("url")]
37 | public string Url { get; set; }
38 |
39 | ///
40 | /// The checksum of the build.
41 | ///
42 | [JsonProperty("checksum")]
43 | public string CheckSum { get; set; }
44 |
45 |
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/Plex/Update/SystemType.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using System.Text;
5 | using System.Threading.Tasks;
6 | using Newtonsoft.Json;
7 |
8 | namespace TE.Plex.Update
9 | {
10 | ///
11 | /// Information about the release for a specified system type.
12 | ///
13 | public class SystemType
14 | {
15 | ///
16 | /// Name of the 32-bit build.
17 | ///
18 | private const string BUILD32BIT = "windows-x86";
19 | ///
20 | /// Name of the 64-bit build.
21 | ///
22 | private const string BUILD64BIT = "windows-x86_64";
23 |
24 | ///
25 | /// The ID of the system type.
26 | ///
27 | [JsonProperty("id")]
28 | public string Id { get; set; }
29 |
30 | ///
31 | /// The name of the system type.
32 | ///
33 | [JsonProperty("name")]
34 | public string Name { get; set; }
35 |
36 | ///
37 | /// The release date of the Plex Media Server.
38 | ///
39 | [JsonProperty("release_date")]
40 | public string ReleaseDate { get; set; }
41 |
42 | ///
43 | /// The version number of the Plex Media Server.
44 | ///
45 | [JsonProperty("version")]
46 | public string Version { get; set; }
47 |
48 | ///
49 | /// The URL to the requirements of the Plex Media Server.
50 | ///
51 | [JsonProperty("requirements")]
52 | public string Requirements { get; set; }
53 |
54 | ///
55 | /// Any additional information associated with this verison of the Plex
56 | /// Media Server.
57 | ///
58 | [JsonProperty("extra_info")]
59 | public string ExtraInfo { get; set; }
60 |
61 | ///
62 | /// A list of items added to this version of the Plex Media Server.
63 | ///
64 | [JsonProperty("items_added")]
65 | public string ItemsAdded { get; set; }
66 |
67 | ///
68 | /// A list of items that were fixed with this version of the Plex Media
69 | /// Server.
70 | ///
71 | [JsonProperty("items_fixed")]
72 | public string ItemsFixed { get; set; }
73 |
74 | ///
75 | /// A object of objects for
76 | /// each release of the Plex Media Server for this system type.
77 | ///
78 | [JsonProperty("releases")]
79 | public List Releases { get; set; } = new List();
80 |
81 | ///
82 | /// Gets the download URL based on whether the 32-bit or 64-bit
83 | /// version of Plex Media Server is to be downloaded.
84 | ///
85 | ///
86 | /// Flag indicating which version of Plex Media Server is installed.
87 | ///
88 | ///
89 | /// The URL for the version of Plex Media Server, otherwise null.
90 | ///
91 | public string GetUrl(bool is64Bit)
92 | {
93 | foreach (Release release in Releases)
94 | {
95 | if (release.Build.Equals(BUILD32BIT, StringComparison.OrdinalIgnoreCase)
96 | && !is64Bit)
97 | {
98 | return release.Url;
99 | }
100 |
101 | if (release.Build.Equals(BUILD64BIT, StringComparison.OrdinalIgnoreCase)
102 | && is64Bit)
103 | {
104 | return release.Url;
105 | }
106 | }
107 |
108 | return null;
109 | }
110 |
111 | ///
112 | /// Gets the checksum based on whether the 32-bit or 64-bit version of
113 | /// Plex Media Server is to be downloaded.
114 | ///
115 | ///
116 | /// Flag indicating which version of Plex Media Server is installed.
117 | ///
118 | ///
119 | /// The checksum for the installation file of Plex Media Server,
120 | /// otherwise null.
121 | ///
122 | public string GetCheckSum(bool is64Bit)
123 | {
124 | foreach (Release release in Releases)
125 | {
126 | if (release.Build.Equals(BUILD32BIT, StringComparison.OrdinalIgnoreCase)
127 | && !is64Bit)
128 | {
129 | return release.CheckSum;
130 | }
131 |
132 | if (release.Build.Equals(BUILD64BIT, StringComparison.OrdinalIgnoreCase)
133 | && is64Bit)
134 | {
135 | return release.CheckSum;
136 | }
137 | }
138 |
139 | return null;
140 | }
141 | }
142 | }
143 |
--------------------------------------------------------------------------------
/PlexServerAutoUpdater.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | {5E327EE8-620A-4945-81CE-029CE9448171}
5 | {FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}
6 | Debug
7 | AnyCPU
8 | WinExe
9 | TE
10 | psupdate
11 | v4.8
12 | Properties
13 | False
14 | False
15 | False
16 | OnBuildSuccess
17 | False
18 | False
19 | False
20 | obj\$(Configuration)\
21 | 4
22 |
23 |
24 |
25 | x86
26 | 4194304
27 | False
28 | Auto
29 | 4096
30 |
31 |
32 | bin\Debug\
33 | True
34 | Full
35 | False
36 | True
37 | DEBUG;TRACE
38 | obj\
39 | Project
40 |
41 |
42 | bin\Release\
43 | False
44 | None
45 | True
46 | False
47 | TRACE
48 | obj\
49 |
50 |
51 | false
52 |
53 |
54 | false
55 |
56 |
57 |
58 | ..\..\Visual Studio 2017\Projects\Supporting\Newtonsoft.Json\net45\Newtonsoft.Json.dll
59 |
60 |
61 |
62 |
63 | 3.5
64 |
65 |
66 |
67 | 3.5
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 | 3.5
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 | Form
104 |
105 |
106 | MainForm.cs
107 |
108 |
109 |
110 |
111 |
112 |
113 |
114 |
115 |
116 |
117 |
118 |
119 |
120 |
121 | MainForm.cs
122 |
123 |
124 |
125 |
--------------------------------------------------------------------------------
/PlexServerAutoUpdater.sln:
--------------------------------------------------------------------------------
1 |
2 | Microsoft Visual Studio Solution File, Format Version 11.00
3 | # Visual Studio 2010
4 | # SharpDevelop 5.1
5 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PlexServerAutoUpdater", "PlexServerAutoUpdater.csproj", "{5E327EE8-620A-4945-81CE-029CE9448171}"
6 | EndProject
7 | Global
8 | GlobalSection(SolutionConfigurationPlatforms) = preSolution
9 | Debug|Any CPU = Debug|Any CPU
10 | Release|Any CPU = Release|Any CPU
11 | EndGlobalSection
12 | GlobalSection(ProjectConfigurationPlatforms) = postSolution
13 | {5E327EE8-620A-4945-81CE-029CE9448171}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
14 | {5E327EE8-620A-4945-81CE-029CE9448171}.Debug|Any CPU.Build.0 = Debug|Any CPU
15 | {5E327EE8-620A-4945-81CE-029CE9448171}.Release|Any CPU.ActiveCfg = Release|Any CPU
16 | {5E327EE8-620A-4945-81CE-029CE9448171}.Release|Any CPU.Build.0 = Release|Any CPU
17 | EndGlobalSection
18 | EndGlobal
19 |
--------------------------------------------------------------------------------
/Program.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.ComponentModel;
3 | using static System.Console;
4 | using static System.Environment;
5 | using System.Runtime.InteropServices;
6 | using System.Windows.Forms;
7 | using TE.LocalSystem;
8 | using TE;
9 | using static TE.SystemExitCodes;
10 |
11 | namespace TE.Plex
12 | {
13 | ///
14 | /// Class with program entry point.
15 | ///
16 | internal sealed class Program
17 | {
18 | ///
19 | /// The parent process.
20 | ///
21 | private const int ATTACH_PARENT_PROCESS = -1;
22 |
23 | ///
24 | /// Attach to the console window.
25 | ///
26 | ///
27 | /// The ID of the process.
28 | ///
29 | ///
30 | /// True if successful, false if not successful.
31 | ///
32 | [DllImport("kernel32.dll")]
33 | static extern bool AttachConsole(int dwProcessId);
34 |
35 | ///
36 | /// Program entry point.
37 | ///
38 | [STAThread]
39 | private static int Main(string[] args)
40 | {
41 | // redirect console output to parent process;
42 | // must be before any calls to Console.WriteLine()
43 | AttachConsole(ATTACH_PARENT_PROCESS);
44 |
45 | Arguments arguments = new Arguments(args);
46 |
47 |
48 | bool isSilent = (arguments["silent"] != null);
49 | string logPath = arguments["log"];
50 |
51 | Log.SetFolder(logPath);
52 | Log.Delete();
53 |
54 | try
55 | {
56 | Log.Write("Getting windows user.");
57 | WindowsUser user = new WindowsUser();
58 |
59 | Log.Write("Checking if user is an administrator.");
60 | // Check if the user running this application is an administrator
61 | if (!user.IsAdministrator())
62 | {
63 | string message = "This application must be run from an administrative account.";
64 |
65 | if (!isSilent)
66 | {
67 | // If the user is not an administrator, then exit
68 | MessageBox.Show(
69 | message,
70 | "Plex Server Updater",
71 | MessageBoxButtons.OK,
72 | MessageBoxIcon.Stop);
73 | }
74 |
75 | Log.Write(message);
76 |
77 | ExitCode = ERROR_ACCESS_DENIED;
78 | return ERROR_ACCESS_DENIED;
79 | }
80 | }
81 | catch (Exception ex)
82 | {
83 | if (!isSilent)
84 | {
85 | MessageBox.Show(
86 | ex.Message,
87 | "Plex Server Updater",
88 | MessageBoxButtons.OK,
89 | MessageBoxIcon.Stop);
90 | }
91 |
92 | Log.Write(ex);
93 | Log.Write(ex.StackTrace);
94 |
95 | ExitCode = 1;
96 | return 1;
97 | }
98 |
99 | if (isSilent)
100 | {
101 | try
102 | {
103 | bool isForceUpdate = (arguments["force"] != null);
104 |
105 | int waitTime = SilentUpdate.DefaultWaitTime;
106 | if (arguments["wait"] != null)
107 | {
108 | if (!Int32.TryParse(arguments["wait"], out waitTime))
109 | {
110 | waitTime = SilentUpdate.DefaultWaitTime;
111 | }
112 | }
113 |
114 | // Run the update silently
115 | Log.Write("Initializing the silent update.");
116 | SilentUpdate silentUpdate = new SilentUpdate(Log.Folder);
117 | silentUpdate.ForceUpdate = isForceUpdate;
118 | silentUpdate.WaitTime = waitTime;
119 | silentUpdate.Run();
120 |
121 | if (silentUpdate.IsPlexRunning())
122 | {
123 | ExitCode = ERROR_SUCCESS;
124 | return ERROR_SUCCESS;
125 | }
126 | else
127 | {
128 | ExitCode = 1;
129 | return 1;
130 | }
131 | }
132 | catch (Exception ex)
133 | {
134 | Log.Write(ex.Message);
135 | Log.Write(ex.StackTrace);
136 |
137 | ExitCode = 1;
138 | return 1;
139 | }
140 | }
141 | else
142 | {
143 | try
144 | {
145 | // Display the main form
146 | Application.EnableVisualStyles();
147 | Application.SetCompatibleTextRenderingDefault(false);
148 |
149 | Log.Write("Initializing the update window.");
150 | MainForm mainForm = new MainForm();
151 |
152 | // Check to see if the form is disposed becase there was an
153 | // issue with initializing the form
154 | if (!mainForm.IsDisposed)
155 | {
156 | Log.Write("Displaying the update window.");
157 | Application.Run(mainForm);
158 | }
159 |
160 | ExitCode = ERROR_SUCCESS;
161 | return ERROR_SUCCESS;
162 | }
163 | catch (Exception ex)
164 | {
165 | MessageBox.Show(
166 | ex.Message,
167 | "Plex Server Updater",
168 | MessageBoxButtons.OK,
169 | MessageBoxIcon.Stop);
170 |
171 | Log.Write(ex);
172 | Log.Write(ex.StackTrace);
173 |
174 | ExitCode = 1;
175 | return 1;
176 | }
177 | }
178 | }
179 | }
180 | }
181 |
--------------------------------------------------------------------------------
/Properties/AssemblyInfo.cs:
--------------------------------------------------------------------------------
1 | #region Using directives
2 | using System;
3 | using System.Reflection;
4 | using System.Runtime.InteropServices;
5 |
6 | #endregion
7 | // General Information about an assembly is controlled through the following
8 | // set of attributes. Change these attribute values to modify the information
9 | // associated with an assembly.
10 | [assembly: AssemblyTitle ("PlexServerAutoUpdater")]
11 | [assembly: AssemblyDescription("Updates the Plex Server when it is run as a service.")]
12 | [assembly: AssemblyConfiguration ("")]
13 | [assembly: AssemblyCompany ("")]
14 | [assembly: AssemblyProduct ("PlexServerAutoUpdater")]
15 | [assembly: AssemblyCopyright("Copyright 2021")]
16 | [assembly: AssemblyTrademark ("")]
17 | [assembly: AssemblyCulture ("")]
18 | // This sets the default COM visibility of types in the assembly to invisible.
19 | // If you need to expose a type to COM, use [ComVisible(true)] on that type.
20 | [assembly: ComVisible (false)]
21 | // The assembly version has following format :
22 | //
23 | // Major.Minor.Build.Revision
24 | //
25 | // You can specify all the values or you can use the default the Revision and
26 | // Build Numbers by using the '*' as shown below:
27 | [assembly: AssemblyVersion("0.2.1.1")]
28 | [assembly: AssemblyFileVersion("0.2.1.1")]
29 |
30 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Plex Server Auto Updater
2 |
3 | The Plex Server Auto Updater application allows the Plex Media server to be updated automatically when it is [run as a Windows service].
4 |
5 | ## What does it do?
6 | When the Plex Server Auto Updater performs an update, the following tasks are done:
7 |
8 | - Downloads and verifies the latest update.
9 | - Stops the Plex service.
10 | - Stops any Plex processes that are running.
11 | - Installs the latest update.
12 | - Deletes the Run keys from the registry to prevent Plex from running outside of the service.
13 | - Stops any Plex processes that are running after the update.
14 | - Restarts the Plex service.
15 |
16 | ## Installation
17 | The auto updater is easy to install, in fact, there isn't an install. It is a portable application and can be run from anywhere on the machine that has the Plex service installed.
18 |
19 | To use the Plex Server Auto Updater, use the following steps:
20 |
21 | - Download the [latest release].
22 | - Extract the psupdate.exe from the zip file into any directory.
23 | - Double-click the executable and click the "Update" button to update the Plex Media Server.
24 |
25 | ## Scheduling a silent, automatic update
26 | The Plex Server Auto Updater can be run silently from any commandline using the following:
27 |
28 | psupdate.exe -silent
29 |
30 | The easiest way to keep Plex Media Server updated is to schedule the Plex Server Auto Updater from the Windows task scheduler. You can find information about how to do this from the [How to Update Plex Automatically When Run as a Service] post on [Technically Easy] or [Updating Plex When Plex is Running as a Windows Service] on [Plexopedia].
31 |
32 | Of course, you can use any scheduling application with Plex Server Auto Updater by running the psupdate.exe with the -silent argument.
33 |
34 | ## Waiting for streaming to complete
35 | By default, the updater will only update the Plex server if there is no client streaming media. If there is a client streaming from the Plex server, the update will wait until the server is free.
36 |
37 | You have a few options on how Plex is updated when media is streaming:
38 |
39 | 1. Leave the default and the updater will wait and then check the server every 30 seconds to see if the streaming has completed before performing the update.
40 | 2. From the GUI, uncheck the "Only update when not in use" checkbox, and then allow the update the go ahead regardless if Plex is streaming media.
41 | 3. You can specify the "-wait [seconds]" argument to specify how many seconds the updater will wait to check to see if the streaming as completed.
42 | 4. When running the update silently (using the -silent parameter), you can specify the -force parameter to force the update.
43 |
44 | ## Log File Location
45 | The default log location is: %TEMP%\plex-updater.txt. Any installation log files are also stored in %TEMP%.
46 |
47 | If the -log parameter is specified on the command line with a valid directory, then that directory will be used to store all the log files.
48 |
49 | [run as a Windows service]: https://forums.plex.tv/discussion/93994/pms-as-a-service/
50 | [latest release]: https://github.com/TechieGuy12/PlexServerAutoUpdater/releases/latest
51 | [How to Update Plex Automatically When Run as a Service]: http://technicallyeasy.net/2016/03/update-plex-automatically-running-plex-service/
52 | [Technically Easy]: http://technicallyeasy.net
53 | [Updating Plex When Plex is Running as a Windows Service]: https://www.plexopedia.com/plex-media-server/windows/updating-plex-media-server-service/
54 | [Plexopedia]: https://www.plexopedia.com
55 |
--------------------------------------------------------------------------------
/app.config:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
--------------------------------------------------------------------------------