├── .gitignore ├── Automate.png ├── AutomateDiagnostics.ps1 ├── AutomateDiagnostics.py ├── AutomateDiagnostics.sh ├── DiagnosticsNewStyle.css ├── Initializer.js ├── Manifest.xml ├── Promote.png ├── README.md ├── Service.ashx ├── SessionEventTrigger.cs └── Web.en-US.resx /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | ## 4 | ## Get latest from https://github.com/github/gitignore/blob/main/VisualStudio.gitignore 5 | 6 | # User-specific files 7 | *.rsuser 8 | *.suo 9 | *.user 10 | *.userosscache 11 | *.sln.docstates 12 | 13 | # User-specific files (MonoDevelop/Xamarin Studio) 14 | *.userprefs 15 | 16 | # Mono auto generated files 17 | mono_crash.* 18 | 19 | # Build results 20 | [Dd]ebug/ 21 | [Dd]ebugPublic/ 22 | [Rr]elease/ 23 | [Rr]eleases/ 24 | x64/ 25 | x86/ 26 | [Ww][Ii][Nn]32/ 27 | [Aa][Rr][Mm]/ 28 | [Aa][Rr][Mm]64/ 29 | bld/ 30 | [Bb]in/ 31 | [Oo]bj/ 32 | [Ll]og/ 33 | [Ll]ogs/ 34 | 35 | # Visual Studio 2015/2017 cache/options directory 36 | .vs/ 37 | # Uncomment if you have tasks that create the project's static files in wwwroot 38 | #wwwroot/ 39 | 40 | # Visual Studio 2017 auto generated files 41 | Generated\ Files/ 42 | 43 | # MSTest test Results 44 | [Tt]est[Rr]esult*/ 45 | [Bb]uild[Ll]og.* 46 | 47 | # NUnit 48 | *.VisualState.xml 49 | TestResult.xml 50 | nunit-*.xml 51 | 52 | # Build Results of an ATL Project 53 | [Dd]ebugPS/ 54 | [Rr]eleasePS/ 55 | dlldata.c 56 | 57 | # Benchmark Results 58 | BenchmarkDotNet.Artifacts/ 59 | 60 | # .NET Core 61 | project.lock.json 62 | project.fragment.lock.json 63 | artifacts/ 64 | 65 | # ASP.NET Scaffolding 66 | ScaffoldingReadMe.txt 67 | 68 | # StyleCop 69 | StyleCopReport.xml 70 | 71 | # Files built by Visual Studio 72 | *_i.c 73 | *_p.c 74 | *_h.h 75 | *.ilk 76 | *.meta 77 | *.obj 78 | *.iobj 79 | *.pch 80 | *.pdb 81 | *.ipdb 82 | *.pgc 83 | *.pgd 84 | *.rsp 85 | *.sbr 86 | *.tlb 87 | *.tli 88 | *.tlh 89 | *.tmp 90 | *.tmp_proj 91 | *_wpftmp.csproj 92 | *.log 93 | *.tlog 94 | *.vspscc 95 | *.vssscc 96 | .builds 97 | *.pidb 98 | *.svclog 99 | *.scc 100 | 101 | # Chutzpah Test files 102 | _Chutzpah* 103 | 104 | # Visual C++ cache files 105 | ipch/ 106 | *.aps 107 | *.ncb 108 | *.opendb 109 | *.opensdf 110 | *.sdf 111 | *.cachefile 112 | *.VC.db 113 | *.VC.VC.opendb 114 | 115 | # Visual Studio profiler 116 | *.psess 117 | *.vsp 118 | *.vspx 119 | *.sap 120 | 121 | # Visual Studio Trace Files 122 | *.e2e 123 | 124 | # TFS 2012 Local Workspace 125 | $tf/ 126 | 127 | # Guidance Automation Toolkit 128 | *.gpState 129 | 130 | # ReSharper is a .NET coding add-in 131 | _ReSharper*/ 132 | *.[Rr]e[Ss]harper 133 | *.DotSettings.user 134 | 135 | # TeamCity is a build add-in 136 | _TeamCity* 137 | 138 | # DotCover is a Code Coverage Tool 139 | *.dotCover 140 | 141 | # AxoCover is a Code Coverage Tool 142 | .axoCover/* 143 | !.axoCover/settings.json 144 | 145 | # Coverlet is a free, cross platform Code Coverage Tool 146 | coverage*.json 147 | coverage*.xml 148 | coverage*.info 149 | 150 | # Visual Studio code coverage results 151 | *.coverage 152 | *.coveragexml 153 | 154 | # NCrunch 155 | _NCrunch_* 156 | .*crunch*.local.xml 157 | nCrunchTemp_* 158 | 159 | # MightyMoose 160 | *.mm.* 161 | AutoTest.Net/ 162 | 163 | # Web workbench (sass) 164 | .sass-cache/ 165 | 166 | # Installshield output folder 167 | [Ee]xpress/ 168 | 169 | # DocProject is a documentation generator add-in 170 | DocProject/buildhelp/ 171 | DocProject/Help/*.HxT 172 | DocProject/Help/*.HxC 173 | DocProject/Help/*.hhc 174 | DocProject/Help/*.hhk 175 | DocProject/Help/*.hhp 176 | DocProject/Help/Html2 177 | DocProject/Help/html 178 | 179 | # Click-Once directory 180 | publish/ 181 | 182 | # Publish Web Output 183 | *.[Pp]ublish.xml 184 | *.azurePubxml 185 | # Note: Comment the next line if you want to checkin your web deploy settings, 186 | # but database connection strings (with potential passwords) will be unencrypted 187 | *.pubxml 188 | *.publishproj 189 | 190 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 191 | # checkin your Azure Web App publish settings, but sensitive information contained 192 | # in these scripts will be unencrypted 193 | PublishScripts/ 194 | 195 | # NuGet Packages 196 | *.nupkg 197 | # NuGet Symbol Packages 198 | *.snupkg 199 | # The packages folder can be ignored because of Package Restore 200 | **/[Pp]ackages/* 201 | # except build/, which is used as an MSBuild target. 202 | !**/[Pp]ackages/build/ 203 | # Uncomment if necessary however generally it will be regenerated when needed 204 | #!**/[Pp]ackages/repositories.config 205 | # NuGet v3's project.json files produces more ignorable files 206 | *.nuget.props 207 | *.nuget.targets 208 | 209 | # Microsoft Azure Build Output 210 | csx/ 211 | *.build.csdef 212 | 213 | # Microsoft Azure Emulator 214 | ecf/ 215 | rcf/ 216 | 217 | # Windows Store app package directories and files 218 | AppPackages/ 219 | BundleArtifacts/ 220 | Package.StoreAssociation.xml 221 | _pkginfo.txt 222 | *.appx 223 | *.appxbundle 224 | *.appxupload 225 | 226 | # Visual Studio cache files 227 | # files ending in .cache can be ignored 228 | *.[Cc]ache 229 | # but keep track of directories ending in .cache 230 | !?*.[Cc]ache/ 231 | 232 | # Others 233 | ClientBin/ 234 | ~$* 235 | *~ 236 | *.dbmdl 237 | *.dbproj.schemaview 238 | *.jfm 239 | *.pfx 240 | *.publishsettings 241 | orleans.codegen.cs 242 | 243 | # Including strong name files can present a security risk 244 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 245 | #*.snk 246 | 247 | # Since there are multiple workflows, uncomment next line to ignore bower_components 248 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 249 | #bower_components/ 250 | 251 | # RIA/Silverlight projects 252 | Generated_Code/ 253 | 254 | # Backup & report files from converting an old project file 255 | # to a newer Visual Studio version. Backup files are not needed, 256 | # because we have git ;-) 257 | _UpgradeReport_Files/ 258 | Backup*/ 259 | UpgradeLog*.XML 260 | UpgradeLog*.htm 261 | ServiceFabricBackup/ 262 | *.rptproj.bak 263 | 264 | # SQL Server files 265 | *.mdf 266 | *.ldf 267 | *.ndf 268 | 269 | # Business Intelligence projects 270 | *.rdl.data 271 | *.bim.layout 272 | *.bim_*.settings 273 | *.rptproj.rsuser 274 | *- [Bb]ackup.rdl 275 | *- [Bb]ackup ([0-9]).rdl 276 | *- [Bb]ackup ([0-9][0-9]).rdl 277 | 278 | # Microsoft Fakes 279 | FakesAssemblies/ 280 | 281 | # GhostDoc plugin setting file 282 | *.GhostDoc.xml 283 | 284 | # Node.js Tools for Visual Studio 285 | .ntvs_analysis.dat 286 | node_modules/ 287 | 288 | # Visual Studio 6 build log 289 | *.plg 290 | 291 | # Visual Studio 6 workspace options file 292 | *.opt 293 | 294 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 295 | *.vbw 296 | 297 | # Visual Studio 6 auto-generated project file (contains which files were open etc.) 298 | *.vbp 299 | 300 | # Visual Studio 6 workspace and project file (working project files containing files to include in project) 301 | *.dsw 302 | *.dsp 303 | 304 | # Visual Studio 6 technical files 305 | *.ncb 306 | *.aps 307 | 308 | # Visual Studio LightSwitch build output 309 | **/*.HTMLClient/GeneratedArtifacts 310 | **/*.DesktopClient/GeneratedArtifacts 311 | **/*.DesktopClient/ModelManifest.xml 312 | **/*.Server/GeneratedArtifacts 313 | **/*.Server/ModelManifest.xml 314 | _Pvt_Extensions 315 | 316 | # Paket dependency manager 317 | .paket/paket.exe 318 | paket-files/ 319 | 320 | # FAKE - F# Make 321 | .fake/ 322 | 323 | # CodeRush personal settings 324 | .cr/personal 325 | 326 | # Python Tools for Visual Studio (PTVS) 327 | __pycache__/ 328 | *.pyc 329 | 330 | # Cake - Uncomment if you are using it 331 | # tools/** 332 | # !tools/packages.config 333 | 334 | # Tabs Studio 335 | *.tss 336 | 337 | # Telerik's JustMock configuration file 338 | *.jmconfig 339 | 340 | # BizTalk build output 341 | *.btp.cs 342 | *.btm.cs 343 | *.odx.cs 344 | *.xsd.cs 345 | 346 | # OpenCover UI analysis results 347 | OpenCover/ 348 | 349 | # Azure Stream Analytics local run output 350 | ASALocalRun/ 351 | 352 | # MSBuild Binary and Structured Log 353 | *.binlog 354 | 355 | # NVidia Nsight GPU debugger configuration file 356 | *.nvuser 357 | 358 | # MFractors (Xamarin productivity tool) working folder 359 | .mfractor/ 360 | 361 | # Local History for Visual Studio 362 | .localhistory/ 363 | 364 | # Visual Studio History (VSHistory) files 365 | .vshistory/ 366 | 367 | # BeatPulse healthcheck temp database 368 | healthchecksdb 369 | 370 | # Backup folder for Package Reference Convert tool in Visual Studio 2017 371 | MigrationBackup/ 372 | 373 | # Ionide (cross platform F# VS Code tools) working folder 374 | .ionide/ 375 | 376 | # Fody - auto-generated XML schema 377 | FodyWeavers.xsd 378 | 379 | # VS Code files for those working on multiple tools 380 | .vscode/* 381 | !.vscode/settings.json 382 | !.vscode/tasks.json 383 | !.vscode/launch.json 384 | !.vscode/extensions.json 385 | *.code-workspace 386 | 387 | # Local History for Visual Studio Code 388 | .history/ 389 | 390 | # Windows Installer files from build outputs 391 | *.cab 392 | *.msi 393 | *.msix 394 | *.msm 395 | *.msp 396 | 397 | # JetBrains Rider 398 | *.sln.iml -------------------------------------------------------------------------------- /Automate.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JohnDuprey/CWCAutomateDiagnostics/2ee86460ec10a5b6b169bba3b724cee25ea4b81b/Automate.png -------------------------------------------------------------------------------- /AutomateDiagnostics.ps1: -------------------------------------------------------------------------------- 1 | # WMI Service check and start/auto 2 | Function serviceCheck($service) { 3 | Write-Verbose "Checking $service" 4 | Try { 5 | $svc_info = Get-WmiObject win32_service | Where-Object { $_.name -eq $service } 6 | if ($null -ne $svc_info.State) { 7 | @{'Status' = $svc_info.State; 'Start Mode' = $svc_info.StartMode; 'User' = $svc_info.StartName } 8 | } else { 9 | @{'Status' = 'Not Detected'; 'Start Mode' = ''; 'User' = '' } 10 | } 11 | } Catch { 12 | Write-Verbose $Error[0].exception.GetType().fullname 13 | @{'Status' = 'WMI Error'; 'Start Mode' = ''; 'User' = '' } 14 | } 15 | } 16 | 17 | # Check PS Version 18 | Function Get-PSVersion { 19 | if (Test-Path variable:psversiontable) { 20 | $psversiontable.psversion 21 | } else { 22 | [version]'1.0.0.0' 23 | } 24 | } 25 | 26 | function extractHostname($url) { 27 | if ($url -eq '') { 28 | $false 29 | } elseif ($url -match '^http.+$') { 30 | ([System.Uri]"$url").Authority 31 | } else { 32 | Write-Verbose 'Warning, server address does not supply http(s). Modify your agent template accordingly and run Update Config.' 33 | $url 34 | } 35 | } 36 | 37 | # Author: Joakim Borger Svendsen, 2017. 38 | # JSON info: http://www.json.org 39 | # Svendsen Tech. MIT License. Copyright Joakim Borger Svendsen / Svendsen Tech. 2016-present. 40 | 41 | # Take care of special characters in JSON (see json.org), such as newlines, backslashes 42 | # carriage returns and tabs. 43 | # '\\(?!["/bfnrt]|u[0-9a-f]{4})' 44 | function EscapeJson { 45 | param( 46 | [String] $String) 47 | # removed: #-replace '/', '\/' ` 48 | # This is returned 49 | $String -replace '\\', '\\' -replace '\n', '\n' ` 50 | -replace '\u0008', '\b' -replace '\u000C', '\f' -replace '\r', '\r' ` 51 | -replace '\t', '\t' -replace '"', '\"' 52 | } 53 | 54 | # Meant to be used as the "end value". Adding coercion of strings that match numerical formats 55 | # supported by JSON as an optional, non-default feature (could actually be useful and save a lot of 56 | # calculated properties with casts before passing..). 57 | # If it's a number (or the parameter -CoerceNumberStrings is passed and it 58 | # can be "coerced" into one), it'll be returned as a string containing the number. 59 | # If it's not a number, it'll be surrounded by double quotes as is the JSON requirement. 60 | function GetNumberOrString { 61 | param( 62 | $InputObject) 63 | if ($InputObject -is [System.Byte] -or $InputObject -is [System.Int32] -or ` 64 | ($env:PROCESSOR_ARCHITECTURE -imatch '^(?:amd64|ia64)$' -and $InputObject -is [System.Int64]) -or ` 65 | $InputObject -is [System.Decimal] -or ` 66 | ($InputObject -is [System.Double] -and -not [System.Double]::IsNaN($InputObject) -and -not [System.Double]::IsInfinity($InputObject)) -or ` 67 | $InputObject -is [System.Single] -or $InputObject -is [long] -or ` 68 | ($Script:CoerceNumberStrings -and $InputObject -match $Script:NumberRegex)) { 69 | Write-Verbose -Message 'Got a number as end value.' 70 | "$InputObject" 71 | } else { 72 | Write-Verbose -Message "Got a string (or 'NaN') as end value." 73 | """$(EscapeJson -String $InputObject)""" 74 | } 75 | } 76 | 77 | function ConvertToJsonInternal { 78 | param( 79 | $InputObject, # no type for a reason 80 | [Int32] $WhiteSpacePad = 0) 81 | 82 | [String] $Json = '' 83 | 84 | $Keys = @() 85 | 86 | Write-Verbose -Message "WhiteSpacePad: $WhiteSpacePad." 87 | 88 | if ($null -eq $InputObject) { 89 | Write-Verbose -Message "Got 'null' in `$InputObject in inner function" 90 | $null 91 | } 92 | 93 | elseif ($InputObject -is [Bool] -and $InputObject -eq $true) { 94 | Write-Verbose -Message "Got 'true' in `$InputObject in inner function" 95 | $true 96 | } 97 | 98 | elseif ($InputObject -is [Bool] -and $InputObject -eq $false) { 99 | Write-Verbose -Message "Got 'false' in `$InputObject in inner function" 100 | $false 101 | } 102 | 103 | elseif ($InputObject -is [DateTime] -and $Script:DateTimeAsISO8601) { 104 | Write-Verbose -Message 'Got a DateTime and will format it as ISO 8601.' 105 | """$($InputObject.ToString('yyyy\-MM\-ddTHH\:mm\:ss'))""" 106 | } 107 | 108 | elseif ($InputObject -is [HashTable]) { 109 | $Keys = @($InputObject.Keys) 110 | Write-Verbose -Message "Input object is a hash table (keys: $($Keys -join ', '))." 111 | } 112 | 113 | elseif ($InputObject.GetType().FullName -eq 'System.Management.Automation.PSCustomObject') { 114 | $Keys = @(Get-Member -InputObject $InputObject -MemberType NoteProperty | 115 | Select-Object -ExpandProperty Name) 116 | 117 | Write-Verbose -Message "Input object is a custom PowerShell object (properties: $($Keys -join ', '))." 118 | } 119 | 120 | elseif ($InputObject.GetType().Name -match '\[\]|Array') { 121 | 122 | Write-Verbose -Message 'Input object appears to be of a collection/array type. Building JSON for array input object.' 123 | 124 | $Json += "[`n" + (($InputObject | ForEach-Object { 125 | 126 | if ($null -eq $_) { 127 | Write-Verbose -Message 'Got null inside array.' 128 | 129 | ' ' * ((4 * ($WhiteSpacePad / 4)) + 4) + 'null' 130 | } 131 | 132 | elseif ($_ -is [Bool] -and $_ -eq $true) { 133 | Write-Verbose -Message "Got 'true' inside array." 134 | 135 | ' ' * ((4 * ($WhiteSpacePad / 4)) + 4) + 'true' 136 | } 137 | 138 | elseif ($_ -is [Bool] -and $_ -eq $false) { 139 | Write-Verbose -Message "Got 'false' inside array." 140 | 141 | ' ' * ((4 * ($WhiteSpacePad / 4)) + 4) + 'false' 142 | } 143 | 144 | elseif ($_ -is [DateTime] -and $Script:DateTimeAsISO8601) { 145 | Write-Verbose -Message 'Got a DateTime and will format it as ISO 8601.' 146 | 147 | ' ' * ((4 * ($WhiteSpacePad / 4)) + 4) + """$($_.ToString('yyyy\-MM\-ddTHH\:mm\:ss'))""" 148 | } 149 | 150 | elseif ($_ -is [HashTable] -or $_.GetType().FullName -eq 'System.Management.Automation.PSCustomObject' -or $_.GetType().Name -match '\[\]|Array') { 151 | Write-Verbose -Message 'Found array, hash table or custom PowerShell object inside array.' 152 | 153 | ' ' * ((4 * ($WhiteSpacePad / 4)) + 4) + (ConvertToJsonInternal -InputObject $_ -WhiteSpacePad ($WhiteSpacePad + 4)) -replace '\s*,\s*$' 154 | } 155 | 156 | else { 157 | Write-Verbose -Message 'Got a number or string inside array.' 158 | 159 | $TempJsonString = GetNumberOrString -InputObject $_ 160 | ' ' * ((4 * ($WhiteSpacePad / 4)) + 4) + $TempJsonString 161 | } 162 | 163 | }) -join ",`n") + "`n$(' ' * (4 * ($WhiteSpacePad / 4)))],`n" 164 | 165 | } else { 166 | Write-Verbose -Message 'Input object is a single element (treated as string/number).' 167 | 168 | GetNumberOrString -InputObject $InputObject 169 | } 170 | if ($Keys.Count) { 171 | 172 | Write-Verbose -Message 'Building JSON for hash table or custom PowerShell object.' 173 | 174 | $Json += "{`n" 175 | 176 | foreach ($Key in $Keys) { 177 | 178 | # -is [PSCustomObject]) { # this was buggy with calculated properties, the value was thought to be PSCustomObject 179 | 180 | if ($null -eq $InputObject.$Key) { 181 | Write-Verbose -Message "Got null as `$InputObject.`$Key in inner hash or PS object." 182 | $Json += ' ' * ((4 * ($WhiteSpacePad / 4)) + 4) + """$Key"": null,`n" 183 | } 184 | 185 | elseif ($InputObject.$Key -is [Bool] -and $InputObject.$Key -eq $true) { 186 | Write-Verbose -Message "Got 'true' in `$InputObject.`$Key in inner hash or PS object." 187 | $Json += ' ' * ((4 * ($WhiteSpacePad / 4)) + 4) + """$Key"": true,`n" 188 | } 189 | 190 | elseif ($InputObject.$Key -is [Bool] -and $InputObject.$Key -eq $false) { 191 | Write-Verbose -Message "Got 'false' in `$InputObject.`$Key in inner hash or PS object." 192 | $Json += ' ' * ((4 * ($WhiteSpacePad / 4)) + 4) + """$Key"": false,`n" 193 | } 194 | 195 | elseif ($InputObject.$Key -is [DateTime] -and $Script:DateTimeAsISO8601) { 196 | Write-Verbose -Message 'Got a DateTime and will format it as ISO 8601.' 197 | $Json += ' ' * ((4 * ($WhiteSpacePad / 4)) + 4) + """$Key"": ""$($InputObject.$Key.ToString('yyyy\-MM\-ddTHH\:mm\:ss'))"",`n" 198 | 199 | } 200 | 201 | elseif ($InputObject.$Key -is [HashTable] -or $InputObject.$Key.GetType().FullName -eq 'System.Management.Automation.PSCustomObject') { 202 | Write-Verbose -Message "Input object's value for key '$Key' is a hash table or custom PowerShell object." 203 | $Json += ' ' * ($WhiteSpacePad + 4) + """$Key"":`n$(' ' * ($WhiteSpacePad + 4))" 204 | $Json += ConvertToJsonInternal -InputObject $InputObject.$Key -WhiteSpacePad ($WhiteSpacePad + 4) 205 | } 206 | 207 | elseif ($InputObject.$Key.GetType().Name -match '\[\]|Array') { 208 | 209 | Write-Verbose -Message "Input object's value for key '$Key' has a type that appears to be a collection/array." 210 | Write-Verbose -Message "Building JSON for ${Key}'s array value." 211 | 212 | $Json += ' ' * ($WhiteSpacePad + 4) + """$Key"":`n$(' ' * ((4 * ($WhiteSpacePad / 4)) + 4))[`n" + (($InputObject.$Key | ForEach-Object { 213 | 214 | if ($null -eq $_) { 215 | Write-Verbose -Message 'Got null inside array inside inside array.' 216 | ' ' * ((4 * ($WhiteSpacePad / 4)) + 8) + 'null' 217 | } 218 | 219 | elseif ($_ -is [Bool] -and $_ -eq $true) { 220 | Write-Verbose -Message "Got 'true' inside array inside inside array." 221 | ' ' * ((4 * ($WhiteSpacePad / 4)) + 8) + 'true' 222 | } 223 | 224 | elseif ($_ -is [Bool] -and $_ -eq $false) { 225 | Write-Verbose -Message "Got 'false' inside array inside inside array." 226 | ' ' * ((4 * ($WhiteSpacePad / 4)) + 8) + 'false' 227 | } 228 | 229 | elseif ($_ -is [DateTime] -and $Script:DateTimeAsISO8601) { 230 | Write-Verbose -Message 'Got a DateTime and will format it as ISO 8601.' 231 | ' ' * ((4 * ($WhiteSpacePad / 4)) + 8) + """$($_.ToString('yyyy\-MM\-ddTHH\:mm\:ss'))""" 232 | } 233 | 234 | elseif ($_ -is [HashTable] -or $_.GetType().FullName -eq 'System.Management.Automation.PSCustomObject' ` 235 | -or $_.GetType().Name -match '\[\]|Array') { 236 | Write-Verbose -Message 'Found array, hash table or custom PowerShell object inside inside array.' 237 | ' ' * ((4 * ($WhiteSpacePad / 4)) + 8) + (ConvertToJsonInternal -InputObject $_ -WhiteSpacePad ($WhiteSpacePad + 8)) -replace '\s*,\s*$' 238 | } 239 | 240 | else { 241 | Write-Verbose -Message 'Got a string or number inside inside array.' 242 | $TempJsonString = GetNumberOrString -InputObject $_ 243 | ' ' * ((4 * ($WhiteSpacePad / 4)) + 8) + $TempJsonString 244 | } 245 | 246 | }) -join ",`n") + "`n$(' ' * (4 * ($WhiteSpacePad / 4) + 4 ))],`n" 247 | 248 | } else { 249 | 250 | Write-Verbose -Message 'Got a string inside inside hashtable or PSObject.' 251 | # '\\(?!["/bfnrt]|u[0-9a-f]{4})' 252 | 253 | $TempJsonString = GetNumberOrString -InputObject $InputObject.$Key 254 | $Json += ' ' * ((4 * ($WhiteSpacePad / 4)) + 4) + """$Key"": $TempJsonString,`n" 255 | 256 | } 257 | 258 | } 259 | 260 | $Json = $Json -replace '\s*,$' # remove trailing comma that'll break syntax 261 | $Json += "`n" + ' ' * $WhiteSpacePad + "},`n" 262 | 263 | } 264 | 265 | $Json 266 | 267 | } 268 | 269 | function ConvertTo-STJson { 270 | [CmdletBinding()] 271 | #[OutputType([Void], [Bool], [String])] 272 | Param( 273 | [AllowNull()] 274 | [Parameter(Mandatory = $True, 275 | ValueFromPipeline = $True, 276 | ValueFromPipelineByPropertyName = $True)] 277 | $InputObject, 278 | [Switch] $Compress, 279 | [Switch] $CoerceNumberStrings = $False, 280 | [Switch] $DateTimeAsISO8601 = $False) 281 | Begin { 282 | 283 | $JsonOutput = '' 284 | $Collection = @() 285 | # Not optimal, but the easiest now. 286 | [Bool] $Script:CoerceNumberStrings = $CoerceNumberStrings 287 | [Bool] $Script:DateTimeAsISO8601 = $DateTimeAsISO8601 288 | [String] $Script:NumberRegex = '^-?\d+(?:(?:\.\d+)?(?:e[+\-]?\d+)?)?$' 289 | #$Script:NumberAndValueRegex = '^-?\d+(?:(?:\.\d+)?(?:e[+\-]?\d+)?)?$|^(?:true|false|null)$' 290 | 291 | } 292 | 293 | Process { 294 | 295 | # Hacking on pipeline support ... 296 | if ($_) { 297 | Write-Verbose -Message "Adding object to `$Collection. Type of object: $($_.GetType().FullName)." 298 | $Collection += $_ 299 | } 300 | 301 | } 302 | 303 | End { 304 | 305 | if ($Collection.Count) { 306 | Write-Verbose -Message "Collection count: $($Collection.Count), type of first object: $($Collection[0].GetType().FullName)." 307 | $JsonOutput = ConvertToJsonInternal -InputObject ($Collection | ForEach-Object { $_ }) 308 | } 309 | 310 | else { 311 | $JsonOutput = ConvertToJsonInternal -InputObject $InputObject 312 | } 313 | 314 | if ($null -eq $JsonOutput) { 315 | Write-Verbose -Message "Returning `$null." 316 | return $null # becomes an empty string :/ 317 | } 318 | 319 | elseif ($JsonOutput -is [Bool] -and $JsonOutput -eq $true) { 320 | Write-Verbose -Message "Returning `$true." 321 | [Bool] $true # doesn't preserve bool type :/ but works for comparisons against $true 322 | } 323 | 324 | elseif ($JsonOutput -is [Bool] -and $JsonOutput -eq $false) { 325 | Write-Verbose -Message "Returning `$false." 326 | [Bool] $false # doesn't preserve bool type :/ but works for comparisons against $false 327 | } 328 | 329 | elseif ($Compress) { 330 | Write-Verbose -Message 'Compress specified.' 331 | ( 332 | ($JsonOutput -split '\n' | Where-Object { $_ -match '\S' }) -join "`n" ` 333 | -replace '^\s*|\s*,\s*$' -replace '\ *\]\ *$', ']' 334 | ) -replace ( # these next lines compress ... 335 | '(?m)^\s*("(?:\\"|[^"])+"): ((?:"(?:\\"|[^"])+")|(?:null|true|false|(?:' + ` 336 | $Script:NumberRegex.Trim('^$') + ` 337 | ')))\s*(?,)?\s*$'), "`${1}:`${2}`${Comma}`n" ` 338 | -replace '(?m)^\s*|\s*\z|[\r\n]+' 339 | } 340 | 341 | else { 342 | ($JsonOutput -split '\n' | Where-Object { $_ -match '\S' }) -join "`n" ` 343 | -replace '^\s*|\s*,\s*$' -replace '\ *\]\ *$', ']' 344 | } 345 | 346 | } 347 | 348 | } 349 | 350 | Function Test-CommandExists { 351 | Param ($command) 352 | $oldPreference = $ErrorActionPreference 353 | $ErrorActionPreference = 'stop' 354 | try { 355 | if (Get-Command $command ) { 356 | $true 357 | } 358 | } Catch { 359 | $false 360 | } Finally { 361 | $ErrorActionPreference = $oldPreference 362 | } 363 | } 364 | 365 | Function Test-JanusLoaded { 366 | try { 367 | $janus = Get-Content $env:windir\ltsvc\lterrors.txt | Select-String 'Janus' | Select-Object -Last 1 368 | if ($janus -match 'Janus enabled') { 369 | $true 370 | } else { 371 | $false 372 | } 373 | } catch { 374 | $false 375 | } 376 | } 377 | 378 | Function Test-FailedSignup { 379 | try { 380 | $signup = Get-Content $env:windir\ltsvc\lterrors.txt | Select-String 'Failed Signup' | Select-Object -Last 1 381 | if ($signup -match 'Failed Signup') { 382 | $true 383 | } else { 384 | $false 385 | } 386 | } catch { 387 | $false 388 | } 389 | } 390 | Function Test-CryptoFailed { 391 | try { 392 | $signup = Get-Content $env:windir\ltsvc\lterrors.txt | Select-String 'Unable to initialize remote agent security.' | Select-Object -Last 1 393 | if ($signup -match 'Unable to initialize remote agent security.') { 394 | $true 395 | } else { 396 | $false 397 | } 398 | } catch { 399 | $false 400 | } 401 | } 402 | 403 | Function Invoke-CheckIn { 404 | $servicecmd = (Join-Path $env:windir '\system32\sc.exe') 405 | # Force check-in 406 | Try { 407 | & $servicecmd control ltservice 136 | Out-Null 408 | } catch { 409 | Write-Verbose 'Error sending checkin' 410 | } 411 | } 412 | 413 | Function Start-AutomateDiagnostics { 414 | Param( 415 | $ltposh = 'http://bit.ly/LTPoSh', 416 | $automate_server = '', 417 | [switch]$verbose, 418 | [switch]$include_lterrors, 419 | [switch]$skip_updates, 420 | [switch]$use_sockets 421 | ) 422 | 423 | if ($verbose) { 424 | $VerbosePreference = 'Continue' 425 | } 426 | 427 | $signup_failure = $false 428 | $agent_crypto_failure = $false 429 | $janus_res = $false 430 | # Initial checkin 431 | Invoke-CheckIn 432 | 433 | # Get powershell version 434 | $psver = Get-PSVersion 435 | 436 | # 2023.01.16 -- Joe McCall 437 | # Simplified the checks for the $ltposh_loaded variable 438 | 439 | Write-Verbose 'Loading LTPosh' 440 | Try { 441 | (New-Object Net.WebClient).DownloadString($ltposh) | Invoke-Expression 442 | $ltposh_loaded = [bool](Get-Command -ListImported -Name Get-LTServiceInfo) 443 | } Catch { 444 | Write-Verbose $Error[0].exception.GetType().fullname 445 | Write-Verbose $_.Exception.Message 446 | $ltposh_loaded = $false 447 | } 448 | If ($ltposh_loaded -eq $false -and $ltposh -ne 'http://bit.ly/LTPoSh') { 449 | Write-Output 'LTPosh failed to load, failing back to bit.ly link' 450 | $ltposh = 'http://bit.ly/LTPoSh' 451 | Try { 452 | (New-Object Net.WebClient).DownloadString($ltposh) | Invoke-Expression 453 | $ltposh_loaded = [bool](Get-Command -ListImported -Name Get-LTServiceInfo) 454 | } Catch { 455 | Write-Verbose $Error[0].exception.GetType().fullname 456 | Write-Verbose $_.Exception.Message 457 | $ltposh_loaded = $false 458 | } 459 | } 460 | 461 | # Check services 462 | $ltservice_check = serviceCheck('LTService') 463 | $ltsvcmon_check = serviceCheck('LTSVCMon') 464 | 465 | # Check LTSVC path and lterrors.txt 466 | $ltsvc_path_exists = Test-Path -Path (Join-Path $env:windir '\ltsvc') 467 | $lterrors_exists = Test-Path -Path (Join-Path $env:windir '\ltsvc\lterrors.txt') 468 | 469 | # 2023.01.16 -- Joe McCall 470 | # Strip the leading LTService version header from each line, and ignore the Disk Clean access denied errors to improve readability of the log output in the browser 471 | if ($include_lterrors) { 472 | $lterrors = if ($lterrors_exists) { 473 | (Get-Content -Path (Join-Path $env:windir '\ltsvc\lterrors.txt') | Select-String 'Disk Clean Dir Error: Access to the path' -NotMatch) -replace 'LTService\s*v\d+\.\d+\s*-\s*', '' -join "`n" 474 | } else { 475 | '' 476 | } 477 | $lterrors_enc = [Convert]::ToBase64String([System.Text.Encoding]::Unicode.GetBytes($lterrors)) 478 | } else { 479 | $lterrors_enc = '' 480 | } 481 | 482 | # Get reg keys in case LTPosh fails 483 | $locationid = Try { 484 | (Get-ItemProperty -Path hklm:\software\labtech\service -ErrorAction Stop).locationid 485 | } Catch { 486 | $null 487 | } 488 | $clientid = Try { 489 | (Get-ItemProperty -Path hklm:\software\labtech\service -ErrorAction Stop).clientid 490 | } Catch { 491 | $null 492 | } 493 | $id = Try { 494 | (Get-ItemProperty -Path hklm:\software\labtech\service -ErrorAction Stop).id 495 | } Catch { 496 | $null 497 | } 498 | $version = Try { 499 | (Get-ItemProperty -Path hklm:\software\labtech\service -ErrorAction Stop).version 500 | } Catch { 501 | $null 502 | } 503 | $server = Try { 504 | ((Get-ItemProperty -Path hklm:\software\labtech\service -ErrorAction Stop).'Server Address') 505 | } Catch { 506 | $null 507 | } 508 | if ($ltsvc_path_exists -and $lterrors_exists) { 509 | $janus_res = Test-JanusLoaded 510 | $signup_failure = Test-FailedSignup 511 | $agent_crypto_failure = Test-CryptoFailed 512 | } 513 | 514 | if ($ltposh_loaded) { 515 | # Get ltservice info 516 | Try { 517 | $info = Get-LTServiceInfo 518 | 519 | # Get checkin / heartbeat times to DateTime 520 | $lasthbsent = Get-Date $info.HeartbeatLastSent 521 | # This could throw exception because it is only returned if there has been a SuccessStatus 522 | if ($info.LastSuccessStatus) { 523 | $lastsuccess = Get-Date $info.LastSuccessStatus 524 | } 525 | $lasthbrcv = Get-Date $info.HeartbeatLastReceived 526 | 527 | # Check online and heartbeat statuses 528 | $online_threshold = (Get-Date).AddMinutes(-5) 529 | $heartbeat_threshold = (Get-Date).AddMinutes(-5) 530 | 531 | # Split server list 532 | $servers = ($info.'Server Address').Split('|') 533 | 534 | $online = $lastsuccess -ge $online_threshold 535 | $heartbeat_rcv = $lasthbrcv -ge $heartbeat_threshold 536 | $heartbeat_snd = $lasthbsent -ge $heartbeat_threshold 537 | $heartbeat = $heartbeat_rcv -or $heartbeat_snd 538 | $heartbeat_status = $info.HeartbeatLastSent 539 | 540 | # Check for persistent TCP connection 541 | Try { 542 | if ($psver -ge [version]'3.0.0.0' -and $heartbeat -eq $false) { 543 | # Check network sockets for established connection from ltsvc to server 544 | Write-Verbose 'Heartbeat failed, checking for TCP tunnel' 545 | $socket = Get-Process -processname 'ltsvc' | ForEach-Object { $process = $_.ID; Get-NetTCPConnection | Where-Object { $_.State -eq 'Established' -and $_.RemotePort -eq 443 -and $_.OwningProcess -eq $process } } 546 | if ($socket.State -eq 'Established') { 547 | $heartbeat = $true 548 | $heartbeat_status = 'Socket Established' 549 | } 550 | } 551 | } Catch { 552 | } 553 | 554 | # If services are stopped, use Restart-LTService to get them working again 555 | If ($ltservice_check.Status -eq 'Stopped' -or $ltsvcmon_check -eq 'Stopped' -or !($heartbeat) -or !($online)) { 556 | Write-Verbose 'Issuing Restart-LTService and sending checkin' 557 | Restart-LTService 558 | Invoke-CheckIn 559 | Start-Sleep -Seconds 10 560 | $info = Get-LTServiceInfo 561 | $ltservice_check = serviceCheck('LTService') 562 | $ltsvcmon_check = serviceCheck('LTSVCMon') 563 | # Get checkin / heartbeat times to DateTime 564 | # This could throw exception because it is only returned if there has been a SuccessStatus 565 | if ($info.LastSuccessStatus) { 566 | $lastsuccess = Get-Date $info.LastSuccessStatus 567 | } 568 | $lasthbsent = Get-Date $info.HeartbeatLastSent 569 | $lasthbrcv = Get-Date $info.HeartbeatLastReceived 570 | $online = $lastsuccess -ge $online_threshold 571 | $heartbeat_rcv = $lasthbrcv -ge $heartbeat_threshold 572 | $heartbeat_snd = $lasthbsent -ge $heartbeat_threshold 573 | $heartbeat = $heartbeat_rcv -or $heartbeat_snd 574 | $heartbeat_status = $info.HeartbeatLastSent 575 | } 576 | 577 | # Get server list 578 | $server_test = $false 579 | foreach ($server in $servers) { 580 | Write-Verbose "Server: $server" 581 | $hostname = extractHostname($server) 582 | Write-Verbose "Hostname: $hostname" 583 | if (!($hostname) -or $hostname -eq '' -or $null -eq $hostname) { 584 | Write-Verbose 'Error with hostname' 585 | continue 586 | } else { 587 | $compare_test = if (($hostname -eq $automate_server -and $automate_server -ne '') -or $automate_server -eq '') { 588 | $true 589 | } else { 590 | $false 591 | } 592 | if (Test-CommandExists -Command 'Test-NetConnection' -and $use_sockets) { 593 | Try { 594 | $conn_test = Test-NetConnection -ComputerName $hostname -Port 443 -ErrorAction Stop 595 | } Catch { 596 | $timeout = 1000 597 | $netping = New-Object System.Net.NetworkInformation.Ping 598 | $ping = $netping.Send($hostname, $timeout) 599 | if ($ping.Status -eq 'Success') { 600 | Write-Verbose 'Fallback to .net ping succeeded' 601 | $conn_test = $true 602 | } else { 603 | Write-Verbose 'Port test failed' 604 | $conn_test = $false 605 | } 606 | } 607 | } else { 608 | Try { 609 | $conn_test = Test-Connection -ComputerName $hostname -Count 1 -ErrorAction Stop 610 | } Catch { 611 | $timeout = 1000 612 | $netping = New-Object System.Net.NetworkInformation.Ping 613 | $ping = $netping.Send($hostname, $timeout) 614 | if ($ping.Status -eq 'Success') { 615 | Write-Verbose 'Fallback to .net ping succeeded' 616 | $conn_test = $true 617 | } else { 618 | Write-Verbose 'Ping test failed' 619 | $conn_test = $false 620 | } 621 | } 622 | } 623 | 624 | Try { 625 | $ver_test = (New-Object Net.WebClient).DownloadString("$($server)/labtech/agent.aspx") 626 | $target_version = $ver_test.Split('|')[6] 627 | } Catch { 628 | Write-Verbose 'Unable to obtain target version' 629 | $target_version = '' 630 | } 631 | if ($conn_test -and $target_version -ne '' -and $compare_test) { 632 | $server_test = $true 633 | $server_msg = "$server passed all checks" 634 | break 635 | } 636 | } 637 | } 638 | if ($server_test -eq $false -and $servers.Count -eq 0) { 639 | $server_msg = 'No automate servers detected' 640 | } elseif ($server_test -eq $false -and $servers.Count -gt 0) { 641 | $server_msg = 'Error' 642 | if (!($compare_test)) { 643 | $server_msg = $server_msg + " | Server address not matched ($hostname)" 644 | } 645 | if (!($conn_test)) { 646 | $server_msg = $server_msg + " | Ping failure ($hostname)" 647 | } 648 | if ($target_version -eq '') { 649 | $server_msg = $server_msg + " | Version check fail ($hostname)" 650 | } 651 | } 652 | 653 | # Check updates 654 | $current_version = $info.Version 655 | if ($target_version -eq $info.Version) { 656 | $update_text = 'Version {0} - Latest' -f $info.Version 657 | } elseif (!$skip_updates) { 658 | Write-Verbose 'Starting update' 659 | taskkill /im ltsvc.exe /f 660 | taskkill /im ltsvcmon.exe /f 661 | taskkill /im lttray.exe /f 662 | Try { 663 | Update-LTService -WarningVariable updatewarn 664 | Start-Sleep -Seconds 30 665 | Try { 666 | Restart-LTService -Confirm:$false 667 | } Catch { 668 | } 669 | Invoke-CheckIn 670 | Start-Sleep -Seconds 30 671 | $info = Get-LTServiceInfo 672 | $janus_res = Test-JanusLoaded 673 | if ([version]$info.Version -gt [version]$current_version) { 674 | $update_text = 'Updated from {1} to {0}' -f $info.Version, $current_version 675 | } else { 676 | $update_text = 'Error updating, still on {0}' -f $info.Version 677 | } 678 | } Catch { 679 | $update_text = 'Error: Update-LTService failed to run' 680 | } 681 | 682 | } else { 683 | $update_text = 'Updates needed, available version {0}' -f $target_version 684 | } 685 | # Collect diagnostic data into hashtable 686 | $diag = @{ 687 | 'id' = $info.id 688 | 'version' = $info.Version 689 | 'server_addr' = $server_msg 690 | 'server_match' = $compare_test 691 | 'online' = $online 692 | 'heartbeat' = $heartbeat 693 | 'update' = $update_text 694 | 'lastcontact' = $info.LastSuccessStatus 695 | 'heartbeat_sent' = $heartbeat_status 696 | 'heartbeat_rcv' = $info.HeartbeatLastReceived 697 | 'svc_ltservice' = $ltservice_check 698 | 'svc_ltsvcmon' = $ltsvcmon_check 699 | 'ltposh_loaded' = $ltposh_loaded 700 | 'clientid' = $info.ClientID 701 | 'locationid' = $info.LocationID 702 | 'ltsvc_path_exists' = $ltsvc_path_exists 703 | 'janus_status' = $janus_res 704 | 'signup_failure' = $signup_failure 705 | 'agent_crypto_failure' = $agent_crypto_failure 706 | 'lterrors' = $lterrors_enc 707 | } 708 | } Catch { 709 | # LTPosh loaded, issue with agent 710 | $exception = $_.Exception.Message 711 | $repair = if (-not ($ltsvc_path_exists) -or $ltsvcmon_check.Status -eq 'Not Detected' -or $ltservice_check.Status -eq 'Not Detected' -or $null -eq $id -or $janus_res -eq $false) { 712 | 'Reinstall' 713 | } else { 714 | 'Restart' 715 | } 716 | if ($null -eq $version -or $ltsvc_path_exists -eq $false) { 717 | $version = 'Agent error' 718 | } 719 | if ($janus_res -eq $false) { 720 | $version = 'Janus failure' 721 | } 722 | $diag = @{ 723 | 'id' = $id 724 | 'svc_ltservice' = $ltservice_check 725 | 'svc_ltsvcmon' = $ltsvcmon_check 726 | 'ltposh_loaded' = $ltposh_loaded 727 | 'server_addr' = $server 728 | 'version' = $version 729 | 'ltsvc_path_exists' = $ltsvc_path_exists 730 | 'locationid' = $locationid 731 | 'clientid' = $clientid 732 | 'repair' = $repair 733 | 'janus_status' = $janus_res 734 | 'signup_failure' = $signup_failure 735 | 'agent_crypto_failure' = $agent_crypto_failure 736 | 'lterrors' = $lterrors_enc 737 | 'exception' = $exception 738 | } 739 | } 740 | } else { 741 | # LTPosh Failure, show basic settings 742 | $diag = @{ 743 | 'id' = $id 744 | 'ltsvc_path_exists' = $ltsvc_path_exists 745 | 'server_addr' = $server 746 | 'locationid' = $locationid 747 | 'clientid' = $clientid 748 | 'svc_ltservice' = $ltservice_check 749 | 'svc_ltsvcmon' = $ltsvcmon_check 750 | 'ltposh_loaded' = $ltposh_loaded 751 | 'version' = $version 752 | 'janus_status' = $janus_res 753 | 'signup_failure' = $signup_failure 754 | 'agent_crypto_failure' = $agent_crypto_failure 755 | 'lterrors' = $lterrors_enc 756 | } 757 | } 758 | 759 | # Output diagnostic data in JSON format - ps2.0 compatible 760 | if ($psver -ge [version]'3.0.0.0') { 761 | $output = $diag | ConvertTo-Json -Depth 2 762 | } else { 763 | $output = $diag | ConvertTo-STJson 764 | } 765 | Write-Host '!---BEGIN JSON---!' 766 | Write-Host $output 767 | } 768 | -------------------------------------------------------------------------------- /AutomateDiagnostics.py: -------------------------------------------------------------------------------- 1 | import json 2 | import os 3 | import subprocess 4 | import platform 5 | import base64 6 | import getopt 7 | import sys 8 | import cgi 9 | 10 | def system_call(command): 11 | p = subprocess.Popen([command], stdout=subprocess.PIPE, shell=True) 12 | return p.stdout.read() 13 | 14 | # Read state file for info 15 | with open("/usr/local/ltechagent/state","r") as read_file: 16 | data = json.load(read_file) 17 | 18 | lterrors = "" 19 | try: 20 | opts, args = getopt.getopt(sys.argv[1:],"e") 21 | for opt, arg in opts: 22 | if opt == '-e': 23 | with open("/usr/local/ltechagent/agent.log","r") as log_file: 24 | lterrors = cgi.escape(log_file.read()) 25 | except getopt.GetoptError: 26 | pass 27 | 28 | 29 | lterrors_bytes = lterrors.encode("ascii") 30 | lterrors_b64_bytes = base64.b64encode(lterrors_bytes) 31 | lterrors_str = lterrors_b64_bytes.decode("ascii") 32 | 33 | # Get last contact date 34 | lc = data["last_contact"] 35 | last_contact = "{0}/{1}/{2} {3}:{4:02d}:{5:02d}".format(lc["month"],lc["day_of_month"],lc["year"],lc["hour"],lc["min"],lc["sec"]) 36 | 37 | old_version = data["version"] 38 | 39 | system_call("/usr/local/ltechagent/ltupdate") 40 | 41 | # Read state file for info 42 | with open("/usr/local/ltechagent/state","r") as read_file: 43 | data = json.load(read_file) 44 | 45 | if old_version != data["version"]: 46 | update = "Updated from "+old_version+" to "+ data["version"] 47 | else: 48 | update = "Already updated to "+ data["version"] 49 | 50 | # Check services 51 | if platform.system() == 'Darwin': 52 | if system_call("launchctl list | grep com.labtechsoftware.LTSvc") != "": 53 | statusname = "Running" 54 | else: 55 | os.system("launchctl stop com.labtechsoftware.LTSvc") 56 | os.system("launchctl start com.labtechsoftware.LTSvc") 57 | if system_call("launchctl list | grep com.labtechsoftware.LTSvc") != "": 58 | statusname = "Running" 59 | else: 60 | statusname = "Stopped" 61 | svc_ltsvc = { "Status": statusname, "User": "com.labtechsoftware.LTSvc", "Start Mode": "Auto"} 62 | elif platform.system() == 'Linux': 63 | status = os.system('service ltechagent status') 64 | if status == 0: 65 | statusname = "Running" 66 | elif status == 3: 67 | os.system('service ltechagent stop') 68 | os.system('service ltechagent start') 69 | status = os.system('service ltechagent status') 70 | if status == 0: 71 | statusname = "Running" 72 | else: 73 | statusname = "Stopped" 74 | else: 75 | statusname = "Stopped" 76 | svc_ltsvc = { "Status": statusname, "User": "ltechagent", "Start Mode": "Auto"} 77 | 78 | diag_result = { 79 | 'server_addr': data["last_good_server_url"], 80 | 'lastcontact': last_contact, 81 | 'update': update, 82 | 'version': data["version"], 83 | 'id': data['computer_id'], 84 | 'clientid': data['client_id'], 85 | 'online': data["is_signed_in"], 86 | 'svc_ltservice': svc_ltsvc, 87 | 'lterrors': lterrors_str 88 | } 89 | 90 | print("!---BEGIN JSON---!") 91 | print(json.dumps(diag_result)) 92 | -------------------------------------------------------------------------------- /AutomateDiagnostics.sh: -------------------------------------------------------------------------------- 1 | # source: https://raw.githubusercontent.com/noaht8um/CWCAutomateDiagnostics/master/AutomateDiagnostics.sh 2 | 3 | # Exit if not macOS 4 | if [ "$(uname)" != "Darwin" ]; then 5 | exit 1 6 | fi 7 | 8 | # Arcane way to parse JSON natively on Macs with AppleScript 9 | # https://paulgalow.com/how-to-work-with-json-api-data-in-macos-shell-scripts 10 | convertFromJson() { 11 | JSON="$1" osascript -l 'JavaScript' \ 12 | -e 'const env = $.NSProcessInfo.processInfo.environment.objectForKey("JSON").js' \ 13 | -e "JSON.parse(env).$2" 14 | } 15 | 16 | # Read state file for info 17 | data=$(cat /usr/local/ltechagent/state) 18 | 19 | status=$(launchctl list | grep com.labtechsoftware.LTSvc) 20 | if [ -z "$status" ]; then 21 | launchctl stop com.labtechsoftware.LTSvc 22 | launchctl start com.labtechsoftware.LTSvc 23 | status=$(launchctl list | grep com.labtechsoftware.LTSvc) 24 | if [ -z "$status" ]; then 25 | statusName="Stopped" 26 | else 27 | statusName="Running" 28 | fi 29 | else 30 | statusName="Running" 31 | fi 32 | 33 | old_version=$(convertFromJson "$data" 'version') 34 | 35 | /usr/local/ltechagent/ltupdate 36 | 37 | # Read state file for info 38 | data=$(cat /usr/local/ltechagent/state) 39 | 40 | new_version=$(convertFromJson "$data" 'version') 41 | if [ "$old_version" != "$new_version" ]; then 42 | update="Updated from $old_version to $new_version" 43 | else 44 | update="Already updated to $new_version" 45 | fi 46 | 47 | server_addr=$(convertFromJson "$data" 'last_good_server_url') 48 | version=$(convertFromJson "$data" 'version') 49 | id=$(convertFromJson "$data" 'computer_id') 50 | clientid=$(convertFromJson "$data" 'client_id') 51 | online=$(convertFromJson "$data" 'is_signed_in') 52 | 53 | # Format lastcontact time 54 | sec=$(printf "%02d\n" $(convertFromJson "$data" 'last_contact.sec')) 55 | min=$(printf "%02d\n" $(convertFromJson "$data" 'last_contact.min')) 56 | hour=$(convertFromJson "$data" 'last_contact.hour') 57 | day_of_month=$(convertFromJson "$data" 'last_contact.day_of_month') 58 | month=$(convertFromJson "$data" 'last_contact.month') 59 | year=$(convertFromJson "$data" 'last_contact.year') 60 | lastcontact=$(echo "$month/$day_of_month/$year $hour:$min:$sec") 61 | 62 | # collect agent logs 63 | lterrors="" 64 | log_file="/usr/local/ltechagent/agent.log" 65 | 66 | if [ -f "$log_file" ]; then 67 | lterrors_str=$(sed -e 's/&/\&/g' -e 's//\>/g' -e 's/"/\"/g' -e "s/'/\'/g" "$log_file") 68 | lterrors=$(echo "$lterrors_str" | base64) 69 | fi 70 | 71 | json=$( 72 | cat < firstOpen); 476 | firstOpen = str.indexOf("{", firstOpen + 1); 477 | } while (firstOpen != -1); 478 | } 479 | 480 | function parseJson(eventData) { 481 | var json = extractJSON(eventData); 482 | //console.log(json); 483 | return json; 484 | } 485 | 486 | function displayDataJson(json) { 487 | SC.ui.addElement($("dataContainer"), "h3", { 488 | id: "tableDetails", 489 | innerHTML: "Details", 490 | }); 491 | 492 | console.log(json); 493 | 494 | if ("server_addr" in json) { 495 | if (!/Error/i.test(json["server_addr"]) && json["server_addr"] != null) { 496 | var server_status = ""; 497 | } else { 498 | var server_status = ""; 499 | } 500 | if (json["server_addr"] != null) { 501 | server = json["server_addr"]; 502 | } else { 503 | server = "No Agent Installed"; 504 | } 505 | SC.ui.addElement($("dataTable"), "tr", { id: "server_row" }); 506 | SC.ui.addElement($("server_row"), "th", { 507 | id: "server_hdr", 508 | innerHTML: "Server Check", 509 | }); 510 | SC.ui.addElement($("server_row"), "td", { 511 | id: "server", 512 | innerHTML: server_status + " " + server, 513 | colspan: 2, 514 | }); 515 | } 516 | 517 | if ("id" in json) { 518 | if (json["id"] > 0) { 519 | var agentid_status = ""; 520 | } else { 521 | var agentid_status = ""; 522 | } 523 | SC.ui.addElement($("dataTable"), "tr", { id: "agent_id_row" }); 524 | SC.ui.addElement($("agent_id_row"), "th", { 525 | id: "agent_id_hdr", 526 | innerHTML: "Agent ID", 527 | }); 528 | SC.ui.addElement($("agent_id_row"), "td", { 529 | id: "agent_id", 530 | innerHTML: agentid_status + " " + json["id"], 531 | }); 532 | } 533 | 534 | if ("locationid" in json) { 535 | if (json["locationid"] > 0) { 536 | var locationid_status = ""; 537 | } else { 538 | var locationid_status = ""; 539 | } 540 | SC.ui.addElement($("dataTable"), "tr", { id: "locationid_row" }); 541 | SC.ui.addElement($("locationid_row"), "th", { 542 | id: "locationid_hdr", 543 | innerHTML: "Location ID", 544 | }); 545 | SC.ui.addElement($("locationid_row"), "td", { 546 | id: "locationid", 547 | innerHTML: 548 | locationid_status + 549 | " " + 550 | json["locationid"] + 551 | "", 552 | }); 553 | } 554 | 555 | if ("update" in json) { 556 | if (!/Error/i.test(json["update"])) { 557 | var update_status = ""; 558 | } else { 559 | var update_status = ""; 560 | } 561 | SC.ui.addElement($("dataTable"), "tr", { id: "update_row" }); 562 | SC.ui.addElement($("update_row"), "th", { 563 | id: "agent_id_hdr", 564 | innerHTML: "Update Check", 565 | }); 566 | SC.ui.addElement($("update_row"), "td", { 567 | id: "agent_id", 568 | innerHTML: update_status + " " + json["update"], 569 | colspan: 2, 570 | }); 571 | } 572 | 573 | if ("online" in json) { 574 | var online_status = json["online"] 575 | ? "" 576 | : ""; 577 | SC.ui.addElement($("dataTable"), "tr", { id: "status_row" }); 578 | SC.ui.addElement($("status_row"), "th", { 579 | id: "status_hdr", 580 | innerHTML: "Checkin Health", 581 | }); 582 | SC.ui.addElement($("status_row"), "td", { 583 | id: "status", 584 | innerHTML: online_status + " " + json["lastcontact"], 585 | }); 586 | } 587 | 588 | if ("heartbeat" in json) { 589 | var heartbeat_status = json["heartbeat"] 590 | ? "" 591 | : ""; 592 | SC.ui.addElement($("dataTable"), "tr", { id: "status_row2" }); 593 | SC.ui.addElement($("status_row2"), "th", { 594 | id: "status_hdr2", 595 | innerHTML: "Heartbeat Health", 596 | }); 597 | SC.ui.addElement($("status_row2"), "td", { 598 | id: "status2", 599 | innerHTML: heartbeat_status + " " + json["heartbeat_sent"], 600 | }); 601 | } 602 | 603 | if ("svc_ltservice" in json) { 604 | if (json["svc_ltservice"]["Status"] != "Not Detected") { 605 | var ltservice_txt = 606 | json["svc_ltservice"]["Status"] + 607 | " | " + 608 | json["svc_ltservice"]["Start Mode"] + 609 | " | " + 610 | json["svc_ltservice"]["User"]; 611 | } else { 612 | var ltservice_txt = json["svc_ltservice"]["Status"]; 613 | } 614 | if ( 615 | json["svc_ltservice"]["Status"] == "Running" && 616 | json["svc_ltservice"]["Start Mode"] == "Auto" 617 | ) { 618 | var ltservice_status = ""; 619 | } else { 620 | var ltservice_status = ""; 621 | } 622 | SC.ui.addElement($("dataTable"), "tr", { id: "ltsvc_row" }); 623 | SC.ui.addElement($("ltsvc_row"), "th", { 624 | id: "agent_id_hdr", 625 | innerHTML: "SVC - LTService", 626 | }); 627 | SC.ui.addElement($("ltsvc_row"), "td", { 628 | id: "ltsvc", 629 | innerHTML: ltservice_status + " " + ltservice_txt, 630 | }); 631 | } 632 | 633 | if ("svc_ltsvcmon" in json) { 634 | if (json["svc_ltsvcmon"]["Status"] != "Not Detected") { 635 | var ltsvcmon_txt = 636 | json["svc_ltsvcmon"]["Status"] + 637 | " | " + 638 | json["svc_ltsvcmon"]["Start Mode"] + 639 | " | " + 640 | json["svc_ltsvcmon"]["User"]; 641 | } else { 642 | var ltsvcmon_txt = json["svc_ltsvcmon"]["Status"]; 643 | } 644 | if ( 645 | json["svc_ltsvcmon"]["Status"] == "Running" && 646 | json["svc_ltsvcmon"]["Start Mode"] == "Auto" 647 | ) { 648 | var ltsvcmon_status = ""; 649 | } else { 650 | var ltsvcmon_status = ""; 651 | } 652 | SC.ui.addElement($("dataTable"), "tr", { id: "ltsvcmon_row" }); 653 | SC.ui.addElement($("ltsvcmon_row"), "th", { 654 | id: "agent_id_hdr", 655 | innerHTML: "SVC - LTSVCMon", 656 | }); 657 | SC.ui.addElement($("ltsvcmon_row"), "td", { 658 | id: "ltsvc", 659 | innerHTML: ltsvcmon_status + " " + ltsvcmon_txt, 660 | }); 661 | } 662 | 663 | if ("ltposh_loaded" in json) { 664 | SC.ui.addElement($("dataTable"), "tr", { id: "ltposh_row" }); 665 | SC.ui.addElement($("ltposh_row"), "th", { 666 | id: "ltposh_hdr", 667 | innerHTML: "LTPosh Loaded", 668 | }); 669 | SC.ui.addElement($("ltposh_row"), "td", { 670 | id: "ltposh", 671 | innerHTML: json["ltposh_loaded"] 672 | ? " PowerShell Module Loaded" 673 | : " Failed to load PowerShell Module", 674 | colspan: 2, 675 | }); 676 | } 677 | 678 | if ("repair" in json) { 679 | SC.ui.addElement($("dataTable"), "tr", { id: "repair_row" }); 680 | SC.ui.addElement($("repair_row"), "th", { 681 | id: "repair_hdr", 682 | innerHTML: "Recommended Repair", 683 | }); 684 | SC.ui.addElement($("repair_row"), "td", { 685 | id: "repair_val", 686 | innerHTML: json["repair"], 687 | colspan: 2, 688 | }); 689 | } 690 | 691 | SC.ui.addElement($("repairOptions"), "DIV", { 692 | id: "repairDiv", 693 | innerHTML: '

Repair Options

', 694 | className: "Header", 695 | }); 696 | var repairCol1 = SC.ui.addElement($("repairOptions"), "div", { 697 | id: "restartOption", 698 | className: "DiagActions", 699 | }); 700 | var repairCol2 = SC.ui.addElement($("repairOptions"), "div", { 701 | id: "reinstallOption", 702 | className: "DiagActions", 703 | }); 704 | SC.command.queryAndAddCommandButtons(repairCol1, "RestartButton"); 705 | SC.command.queryAndAddCommandButtons(repairCol2, "ReinstallButton"); 706 | 707 | SC.ui.addElement($("repairOptions"), "DIV", { 708 | id: "lterrorsDiv", 709 | innerHTML: '

Agent Log

', 710 | className: "Header", 711 | }); 712 | if ("lterrors" in json && json["lterrors"] != "") { 713 | SC.ui.addElement($("lterrors"), "pre", { 714 | id: "lterrors_file", 715 | innerHTML: atob(json["lterrors"]), 716 | }); 717 | } else { 718 | SC.ui.addElement($("lterrors"), "pre", { 719 | id: "lterrors_file", 720 | innerHTML: "Click 'Run CWA Diagnostic' to pull in the latest log file.", 721 | }); 722 | } 723 | } 724 | 725 | function isUsingInternetExplorerOrEdge() { 726 | var ua = window.navigator.userAgent; 727 | var msie = ua.indexOf("Trident"); 728 | 729 | if (ua.indexOf("Trident") > 0 || ua.indexOf("Edge") > 0) return true; 730 | else return false; 731 | } 732 | 733 | //ripped directly from http://stackoverflow.com/questions/6108819/javascript-timestamp-to-relative-time-eg-2-seconds-ago-one-week-ago-etc-best 734 | function timeDifference(current, previous) { 735 | var msPerMinute = 60 * 1000; 736 | var msPerHour = msPerMinute * 60; 737 | var msPerDay = msPerHour * 24; 738 | var msPerMonth = msPerDay * 30; 739 | var msPerYear = msPerDay * 365; 740 | 741 | var elapsed = current - previous; 742 | 743 | if (elapsed < msPerMinute) 744 | return Math.abs(Math.round(elapsed / 1000)) + " seconds ago"; 745 | else if (elapsed < msPerHour) 746 | return Math.round(elapsed / msPerMinute) + " minutes ago"; 747 | else if (elapsed < msPerDay) 748 | return Math.round(elapsed / msPerHour) + " hours ago"; 749 | else if (elapsed < msPerMonth) 750 | return "approximately " + Math.round(elapsed / msPerDay) + " days ago"; 751 | else if (elapsed < msPerYear) 752 | return "approximately " + Math.round(elapsed / msPerMonth) + " months ago"; 753 | else return "approximately " + Math.round(elapsed / msPerYear) + " years ago"; 754 | } 755 | 756 | // 2023.01.16 -- Joe McCall | Expanded cases and variables to add OS check so Mac and Linux commands are distinct (sh or python) 757 | function getAutomateCommandText(headers) { 758 | switch ( 759 | headers.Processor + 760 | "/" + 761 | headers.OperatingSystem + 762 | "/" + 763 | headers.Interface + 764 | "/" + 765 | headers.ContentType + 766 | "/" + 767 | headers.DiagnosticType 768 | ) { 769 | case "ps/Windows/powershell/json/Automate": 770 | return ( 771 | "$WarningPreference='SilentlyContinue'; IF([Net.SecurityProtocolType]::Tls) {[Net.ServicePointManager]::SecurityProtocol=[Net.ServicePointManager]::SecurityProtocol -bor [Net.SecurityProtocolType]::Tls}; IF([Net.SecurityProtocolType]::Tls11) {[Net.ServicePointManager]::SecurityProtocol=[Net.ServicePointManager]::SecurityProtocol -bor [Net.SecurityProtocolType]::Tls11}; IF([Net.SecurityProtocolType]::Tls12) {[Net.ServicePointManager]::SecurityProtocol=[Net.ServicePointManager]::SecurityProtocol -bor [Net.SecurityProtocolType]::Tls12}; Try { (new-object Net.WebClient).DownloadString('" + 772 | getAutomateDiagnosticsURL() + 773 | "') | iex; Start-AutomateDiagnostics -ltposh '" + 774 | getLTPoSh() + 775 | "' -include_lterrors -automate_server '" + 776 | getLTServer() + 777 | "' " + 778 | getVerbose() + 779 | "} Catch { $_.Exception.Message; Write-Output '!---BEGIN JSON---!'; Write-Output '{\"version\": \"Error loading AutomateDiagnostics\"}' }" 780 | ); 781 | case "ps/Windows/powershell/json/RestartAutomate": 782 | return ( 783 | "$WarningPreference='SilentlyContinue'; IF([Net.SecurityProtocolType]::Tls) {[Net.ServicePointManager]::SecurityProtocol=[Net.ServicePointManager]::SecurityProtocol -bor [Net.SecurityProtocolType]::Tls}; IF([Net.SecurityProtocolType]::Tls11) {[Net.ServicePointManager]::SecurityProtocol=[Net.ServicePointManager]::SecurityProtocol -bor [Net.SecurityProtocolType]::Tls11}; IF([Net.SecurityProtocolType]::Tls12) {[Net.ServicePointManager]::SecurityProtocol=[Net.ServicePointManager]::SecurityProtocol -bor [Net.SecurityProtocolType]::Tls12}; (new-object Net.WebClient).DownloadString('" + 784 | getLTPoSh() + 785 | "') | iex; Restart-LTService" 786 | ); 787 | case "ps/Windows/powershell/json/ReinstallAutomate": 788 | var txtlocationid = $("#locationidreinstall").value; 789 | var txtinstallertoken = $("#installertoken").value; 790 | if (isNaN(txtlocationid)) { 791 | txtlocationid = "1"; 792 | } 793 | return ( 794 | "$WarningPreference='SilentlyContinue'; IF([Net.SecurityProtocolType]::Tls) {[Net.ServicePointManager]::SecurityProtocol=[Net.ServicePointManager]::SecurityProtocol -bor [Net.SecurityProtocolType]::Tls}; IF([Net.SecurityProtocolType]::Tls11) {[Net.ServicePointManager]::SecurityProtocol=[Net.ServicePointManager]::SecurityProtocol -bor [Net.SecurityProtocolType]::Tls11}; IF([Net.SecurityProtocolType]::Tls12) {[Net.ServicePointManager]::SecurityProtocol=[Net.ServicePointManager]::SecurityProtocol -bor [Net.SecurityProtocolType]::Tls12};(new-object Net.WebClient).DownloadString('" + 795 | getLTPoSh() + 796 | "') | iex; Reinstall-LTService -SkipDotNet -Server https://" + 797 | getLTServer() + 798 | " -LocationID " + 799 | txtlocationid + 800 | " -InstallerToken " + 801 | txtinstallertoken 802 | ); 803 | return; 804 | case "sh/Linux/bash/json/Automate": 805 | return ( 806 | "url=" + 807 | getLinuxDiagnosticsURL() + 808 | "; CURL=$(command -v curl); WGET=$(command -v wget); if [ ! -z $CURL ]; then echo $($CURL -s $url | python - -e); else echo $($WGET -q -O - --no-check-certificate $url | python - -e); fi" 809 | ); 810 | case "sh/Mac/bash/json/Automate": 811 | return ( 812 | "url=" + 813 | getMacDiagnosticsURL() + 814 | "; CURL=$(command -v curl); WGET=$(command -v wget); if [ ! -z $CURL ]; then echo $($CURL -s $url | sh); else echo $($WGET -q -O - --no-check-certificate $url | sh); fi" 815 | ); 816 | default: 817 | throw "unknown os"; 818 | } 819 | } 820 | -------------------------------------------------------------------------------- /Manifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 1.0.7.4 4 | Automate Diagnostics 5 | John Duprey - Complete Network 6 | Diagnoses Automate Agents from CW Control. Visit https://github.com/johnduprey/CWCAutomateDiagnostics for the Readme. 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | The URL for launching LTPoSh. 16 | http://bit.ly/LTPoSh 17 | 18 | 19 | The URL for launching AutomateDiagnostics.ps1 20 | https://raw.githubusercontent.com/johnduprey/CWCAutomateDiagnostics/master/AutomateDiagnostics.ps1 21 | 22 | 23 | The URL for launching AutomateDiagnostics.py 24 | https://raw.githubusercontent.com/johnduprey/CWCAutomateDiagnostics/master/AutomateDiagnostics.py 25 | 26 | 27 | The URL for launching AutomateDiagnostics.sh 28 | https://raw.githubusercontent.com/johnduprey/CWCAutomateDiagnostics/master/AutomateDiagnostics.sh 29 | 30 | 31 | The hostname for your automate server (exclude https://). 32 | 33 | 34 | 35 | Generate a long lived InstallerToken - https://www.mspgeek.com/files/file/50-generate-agent-installertoken/ 36 | 37 | 38 | 39 | 1 = Enable, 0 = Disable 40 | 0 41 | 42 | 43 | Fix access session to use machine name - 1 = Enable, 0 = Disable 44 | 0 45 | 46 | 47 | Change this if you are currently using CustomProperty6 48 | 6 49 | 50 | 51 | Change this if you are currently using CustomProperty7 52 | 7 53 | 54 | 55 | Diagnostic timeout - Default is 10 minutes (600000ms) 56 | 600000 57 | 58 | 59 | 1 = Enable, 0 = Disable 60 | 0 61 | 62 | 63 | Set to 1 to create session group and rename custom properties. Delete the session group to re-create. 64 | 0 65 | 66 | 67 | -------------------------------------------------------------------------------- /Promote.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JohnDuprey/CWCAutomateDiagnostics/2ee86460ec10a5b6b169bba3b724cee25ea4b81b/Promote.png -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # CWCAutomateDiagnostics 2 | Run ConnectWise Automate agent diagnostics from ConnectWise Control. This extension utilizes the Labtech-Powershell-Module to review CWA agent settings and service statuses. The extension will also perform automatic service repairs and updates. 3 | 4 | 5 | 6 | ## Features 7 | - NEW - LTErrors.txt / agent.log file is returned in the diagnostic 8 | - NEW - /usr/local/ltechagent/ltupdate initiated from AutomateDiagnostics.py 9 | - Maintenance Mode - Disable diagnostics on GuestConnect event 10 | - Mac OS X/Linux Agent Reporting 11 | - Forces agent updates using Update-LTService 12 | - Verifies LTService and LTSVCmon services are running and set to Automatic. Will start services and set StartMode to Automatic. 13 | - Verifies checkin and heartbeat times. 14 | - Stores the CWA Agent ID as CustomProperty6 (customizable). 15 | - Stores the CWA Version as CustomProperty7 (customizable). 16 | - Provides custom Session Group that sorts endpoints by CWA version number. 17 | - Repair option now uses InstallerToken, generate a long lived one using this script https://www.mspgeek.com/files/file/50-generate-agent-installertoken/. 18 | 19 | ## Installation 20 | - Install Automate Diagnostics from the ConnectWise Control Marketplace (version 1.0.6.7) 21 | 22 | ### Manual Instructions 23 | > To install this version, 1.0.7.2: 24 | 1. Create a new directory for the extension - %programfiles(x86)%\ScreenConnect\App_Extensions\e4dd11eb-3c5e-407c-a7b8-a8ea5e6dbb76 25 | 2. Download the lastest master.zip and extract all files into the directory 26 | 3. Enable the extension in the administration page. 27 | 28 | ## Setup 29 | 1. In the settings, modify the PathToLTPoSh to a URL that you trust or one that is configured to bypass content filters 30 | 2. Additionally, find the Guid for the Control extension (manual one is listed above, cloud is 26a42e0d-6233-4a66-9575-6e05a248cd26) 31 | 3. Build the URL with the extension Guid and add that to the settings to avoid calling the script from GitHub. (e.g https://control_url:port/App_Extensions//AutomateDiagnostics.ps1) 32 | 4. Edit the Control web.config file (make a backup first): Under the `` section, add verb entries for each script file: 33 | 34 | ``` 35 | 36 | 37 | 38 | ``` 39 | 40 | ## Usage 41 | - Script is automatically executed on GuestConnect event (e.g. Service/Computer reboot). RanCommand events are parsed for JSON output and the version number is stored in CustomProperty7. Agent ID is stored as CustomProperty6. (NOTE: To rename the custom properties or reset the Session group, set the createdVersionSessionGroup setting to false, also do this if you change the custom property value number) 42 | - Script can be manually invoked from the Automate tab on the Host screen or in the drop down menu when selecting sessions. 43 | 44 | ## Sample Output 45 | 46 | | Diagnostic Details | Agent Logs | 47 | | ------------- | ------------- | 48 | | ![Details](https://user-images.githubusercontent.com/41485711/212959588-a29b5173-bf9f-427d-9ae8-47a5c2593143.png) | ![LTRrrors](https://user-images.githubusercontent.com/41485711/212806625-1f95e9a1-3c16-489b-9219-5a90a36a4f3f.png) | 49 | 50 | 51 | ## Troubleshooting 52 | Set Verbose = 1 in the Extension settings to log more data for on-demand diagnostics. This does not apply to Guest Connect events. 53 | 54 | ## Credit 55 | - CTaylor's Labtech-Powershell-Module - https://github.com/LabtechConsulting/LabTech-Powershell-Module 56 | - Noah Tatum - AutomateDiagnostics.sh - https://github.com/noaht8um/CWCAutomateDiagnostics/blob/master/AutomateDiagnostics.sh 57 | - Joe McCall - Bug fixes for CW Control 22.9+ 58 | - MSPGeek Community - https://mspgeek.com 59 | - CWC Tags Extension for CustomProperty session group 60 | - CWA Extension for CustomProperty setting via C# 61 | - Diagnostics Extension for running powershell on remote endpoints and collecting/parsing output 62 | -------------------------------------------------------------------------------- /Service.ashx: -------------------------------------------------------------------------------- 1 | <%@ WebHandler Language="C#" Class="Service" %> 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Globalization; 5 | using System.Linq; 6 | using System.Threading.Tasks; 7 | using ScreenConnect; 8 | 9 | [DemandPermission(PermissionInfo.AdministerPermission)] 10 | public class Service : WebServiceBase 11 | { 12 | public async Task NotifyCreatedVersionSessionGroup() 13 | { 14 | await ExtensionRuntime.SetExtensionSettingAsync(ExtensionContext.Current.ExtensionID, "CreateVersionSessionGroup", "0"); 15 | } 16 | 17 | public void SetVersionCustomProperties() 18 | { 19 | var resourceManager = WebResourceManager.Instance; 20 | var agentidproperty = Int32.Parse(ExtensionContext.Current.GetSettingValue("AgentIDCustomProperty")); 21 | var agentversionproperty = Int32.Parse(ExtensionContext.Current.GetSettingValue("AgentVersionCustomProperty")); 22 | 23 | new List() { "LabelText", "AccessVisible", "MeetingVisible", "SupportVisible" }.ForEach(delegate (string resource) { 24 | resourceManager.SaveResourceOverride("SessionProperty.Custom"+ agentversionproperty +"." + resource, new Dictionary() { { resource.Equals("LabelText") ? CultureInfo.CurrentCulture.Name : "InvariantCultureKey", resource.Equals("LabelText") ? WebResources.GetString("Diagnostics.Automate.VersionLabel") : "true" } } 25 | .Select(cultureKeyToOverrideValue => 26 | Extensions.CreateKeyValuePair( 27 | cultureKeyToOverrideValue.Key == "InvariantCultureKey" ? CultureInfo.InvariantCulture : CultureInfo.GetCultureInfo(cultureKeyToOverrideValue.Key), 28 | (object)cultureKeyToOverrideValue.Value 29 | ) 30 | ) 31 | ); 32 | resourceManager.SaveResourceOverride("SessionProperty.Custom"+ agentidproperty +"." + resource, new Dictionary() { { resource.Equals("LabelText") ? CultureInfo.CurrentCulture.Name : "InvariantCultureKey", 33 | resource.Equals("LabelText") ? WebResources.GetString("Diagnostics.Automate.IDLabel") : "true" } } 34 | .Select(cultureKeyToOverrideValue => 35 | Extensions.CreateKeyValuePair( 36 | cultureKeyToOverrideValue.Key == "InvariantCultureKey" ? CultureInfo.InvariantCulture : CultureInfo.GetCultureInfo(cultureKeyToOverrideValue.Key), 37 | (object)cultureKeyToOverrideValue.Value 38 | ) 39 | ) 40 | ); 41 | }); 42 | } 43 | } -------------------------------------------------------------------------------- /SessionEventTrigger.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections; 3 | using System.Collections.Generic; 4 | using System.IO; 5 | using System.Linq; 6 | using System.Reflection; 7 | using System.Runtime.Serialization; 8 | using System.Runtime.Serialization.Json; 9 | using System.Text; 10 | using System.Text.RegularExpressions; 11 | using System.Threading.Tasks; 12 | using ScreenConnect; 13 | 14 | public class 15 | SessionEventTriggerAccessor 16 | : IAsyncDynamicEventTrigger 17 | { 18 | public async Task 19 | ProcessEventAsync(SessionEventTriggerEvent sessionEventTriggerEvent) 20 | { 21 | if (sessionEventTriggerEvent.SessionEvent.EventType == SessionEventType.Connected 22 | && sessionEventTriggerEvent.SessionConnection.ProcessType == ProcessType.Guest 23 | && ExtensionContext.Current.GetSettingValue("MaintenanceMode") == "0" 24 | && sessionEventTriggerEvent.Session.ActiveConnections.Where(_ => _.ProcessType == ProcessType.Host).Count() == 0 25 | ) 26 | await RunDiagnostics(sessionEventTriggerEvent, ExtensionContext.Current); 27 | else if (sessionEventTriggerEvent.SessionEvent.EventType == SessionEventType.RanCommand && IsDiagnosticContent(sessionEventTriggerEvent.SessionEvent.Data)) 28 | { 29 | try 30 | { 31 | var sessionDetails = await SessionManagerPool.Demux.GetSessionDetailsAsync(sessionEventTriggerEvent.Session.SessionID); 32 | string output = sessionEventTriggerEvent.SessionEvent.Data; 33 | 34 | if (IsDiagResult(output)) 35 | { 36 | var data = output.Split(new string[] { "!---BEGIN JSON---!" }, StringSplitOptions.None); 37 | if (data[1] != "") 38 | { 39 | DiagOutput diag = Deserialize(data[1]); 40 | var newCustomProperties = sessionEventTriggerEvent.Session.CustomPropertyValues.ToArray(); 41 | 42 | if (diag.version != null) 43 | newCustomProperties[Int32.Parse(ExtensionContext.Current.GetSettingValue("AgentVersionCustomProperty")) - 1] = diag.version; 44 | 45 | if (diag.id != null) 46 | newCustomProperties[Int32.Parse(ExtensionContext.Current.GetSettingValue("AgentIDCustomProperty")) - 1] = diag.id; 47 | 48 | await SessionManagerPool.Demux.UpdateSessionAsync( 49 | "AutomateDiagnostics", 50 | sessionEventTriggerEvent.Session.SessionID, 51 | ExtensionContext.Current.GetSettingValue("SetUseMachineName") == "1" ? "": sessionEventTriggerEvent.Session.Name, 52 | sessionEventTriggerEvent.Session.IsPublic, 53 | sessionEventTriggerEvent.Session.Code, 54 | newCustomProperties 55 | ); 56 | } 57 | } 58 | else if (IsRepairResult(output)) 59 | await RunDiagnostics(sessionEventTriggerEvent, ExtensionContext.Current); 60 | } 61 | catch (Exception e) 62 | { 63 | WriteLog(e.Message); 64 | } 65 | } 66 | } 67 | 68 | public DiagOutput Deserialize(string json) 69 | { 70 | DataContractJsonSerializer ser = new DataContractJsonSerializer(typeof (DiagOutput)); 71 | using (var ms = new MemoryStream(Encoding.UTF8.GetBytes(json))) 72 | { 73 | return ser.ReadObject(ms) as DiagOutput; 74 | } 75 | } 76 | 77 | // 2023.01.16 -- Joe McCall | Imported the newer ASync method from DEV branch 78 | // 2023.01.19 -- swlinak | changed method prototype to return Task, can cause compiler issues if attempting to return void 79 | private async Task RunDiagnostics(SessionEventTriggerEvent sessionEventTriggerEvent, ExtensionContext extensionContext) 80 | { 81 | var sessionDetails = await SessionManagerPool.Demux.GetSessionDetailsAsync(sessionEventTriggerEvent.Session.SessionID); 82 | if (sessionDetails.Session.SessionType == SessionType.Access) 83 | { 84 | var ltposh = extensionContext.GetSettingValue("PathToLTPoSh"); 85 | var diag = extensionContext.GetSettingValue("PathToDiag"); 86 | var linuxdiag = extensionContext.GetSettingValue("PathToLinuxDiag"); 87 | var macdiag = extensionContext.GetSettingValue("PathToMacDiag"); 88 | var server = extensionContext.GetSettingValue("AutomateHostname"); 89 | var os = sessionDetails.Session.GuestInfo.OperatingSystemName; 90 | var timeout = extensionContext.GetSettingValue("Timeout"); 91 | var command = ""; 92 | 93 | // 2023.01.16 -- Joe McCall | Expanded with distinct option for MacOSX and Linux 94 | if (os.Contains("Windows")) 95 | { 96 | command = 97 | "#!ps\n#maxlength=100000\n#timeout=" + 98 | timeout + 99 | "\necho 'DIAGNOSTIC-RESPONSE/1'\necho 'DiagnosticType: Automate'\necho 'ContentType: json'\necho ''\n$WarningPreference='SilentlyContinue'; IF([Net.SecurityProtocolType]::Tls) {[Net.ServicePointManager]::SecurityProtocol=[Net.ServicePointManager]::SecurityProtocol -bor [Net.SecurityProtocolType]::Tls}; IF([Net.SecurityProtocolType]::Tls11) {[Net.ServicePointManager]::SecurityProtocol=[Net.ServicePointManager]::SecurityProtocol -bor [Net.SecurityProtocolType]::Tls11}; IF([Net.SecurityProtocolType]::Tls12) {[Net.ServicePointManager]::SecurityProtocol=[Net.ServicePointManager]::SecurityProtocol -bor [Net.SecurityProtocolType]::Tls12}; Try {(new-object Net.WebClient).DownloadString('" + 100 | diag + 101 | "') | iex; Start-AutomateDiagnostics -ltposh '" + 102 | ltposh + 103 | "' -automate_server '" + 104 | server + 105 | "'} Catch { $_.Exception.Message; Write-Output '!---BEGIN JSON---!'; Write-Output '{\"version\": \"Error loading AutomateDiagnostics\"}' }"; 106 | } 107 | else if (os.Contains("Mac")) 108 | { 109 | // 2023.01.16 -- Joe McCall | Calling the AutomateDiagnostics.sh sourced from here: https://github.com/noaht8um/CWCAutomateDiagnostics/ 110 | command = 111 | "#!sh\n#maxlength=100000\n#timeout=" + 112 | timeout + 113 | "\necho 'DIAGNOSTIC-RESPONSE/1'\necho 'DiagnosticType: Automate'\necho 'ContentType: json'\nurl=" + 114 | macdiag + 115 | "; CURL=$(command -v curl); WGET=$(command -v wget); if [ ! -z $CURL ]; then echo $($CURL -s $url | sh); else echo $($WGET -q -O - --no-check-certificate $url | sh); fi"; 116 | } 117 | else if (os.Contains("Linux")) 118 | { 119 | command = 120 | "#!sh\n#maxlength=100000\n#timeout=" + 121 | timeout + 122 | "\necho 'DIAGNOSTIC-RESPONSE/1'\necho 'DiagnosticType: Automate'\necho 'ContentType: json'\nurl=" + 123 | linuxdiag + 124 | "; CURL=$(command -v curl); WGET=$(command -v wget); if [ ! -z $CURL ]; then echo $($CURL -s $url | python); else echo $($WGET -q -O - --no-check-certificate $url | python); fi"; 125 | } 126 | else 127 | { 128 | command = 129 | "@echo off\necho No OS Detected, try running the diagnostic again"; 130 | } 131 | 132 | await SessionManagerPool.Demux.AddSessionEventAsync( 133 | sessionEventTriggerEvent.Session.SessionID, 134 | SessionEventType.QueuedCommand, 135 | SessionEventAttributes.NeedsProcessing, 136 | "AutomateDiagnostics", 137 | command 138 | ); 139 | } 140 | } 141 | 142 | private bool IsDiagnosticContent(string eventData) 143 | { 144 | if (eventData.StartsWith("DIAGNOSTIC-RESPONSE/1") || eventData.StartsWith("\ufeffDIAGNOSTIC-RESPONSE/1")) 145 | return true; 146 | else 147 | return false; 148 | } 149 | 150 | private bool IsRepairResult(string eventData) 151 | { 152 | if (eventData.Contains("DiagnosticType: ReinstallAutomate") ||eventData.Contains("DiagnosticType: RestartAutomate")) 153 | return true; 154 | else 155 | return false; 156 | } 157 | 158 | private bool IsDiagResult(string eventData) 159 | { 160 | var data = 161 | eventData 162 | .Split(new string[] { "!---BEGIN JSON---!" }, 163 | StringSplitOptions.None); 164 | if (data[1] != "") 165 | { 166 | if (data[0].Contains("DiagnosticType: Automate")) 167 | { 168 | return true; 169 | } 170 | else 171 | { 172 | return false; 173 | } 174 | } 175 | else 176 | { 177 | return false; 178 | } 179 | } 180 | 181 | private static string FormatMessage(string message) 182 | { 183 | DateTime now = DateTime.Now; 184 | return string.Format("{0}: {1}", now.ToString(), message); 185 | } 186 | 187 | public static void WriteLog(string message) 188 | { 189 | try 190 | { 191 | using ( 192 | StreamWriter streamWriter = 193 | new StreamWriter(string 194 | .Concat(Environment 195 | .ExpandEnvironmentVariables("%windir%"), 196 | "\\temp\\AutomateDiagnostics.log"), 197 | true) 198 | ) 199 | { 200 | streamWriter.WriteLine(FormatMessage(message)); 201 | } 202 | } 203 | catch 204 | { 205 | } 206 | } 207 | 208 | public static void var_dump(object obj) 209 | { 210 | WriteLog(String.Format("{0,-18} {1}", "Name", "Value")); 211 | string ln = 212 | @"-----------------------------------------------------------------"; 213 | WriteLog (ln); 214 | 215 | Type t = obj.GetType(); 216 | PropertyInfo[] props = t.GetProperties(); 217 | 218 | for (int i = 0; i < props.Length; i++) 219 | { 220 | try 221 | { 222 | WriteLog(String 223 | .Format("{0,-18} {1}", 224 | props[i].Name, 225 | props[i].GetValue(obj, null))); 226 | } 227 | catch (Exception e) 228 | { 229 | //Console.WriteLine(e); 230 | } 231 | } 232 | } 233 | } 234 | 235 | public class DiagOutput 236 | { 237 | [DataMember(Name = "id", IsRequired = false)] 238 | public String id; 239 | 240 | [DataMember(Name = "version", IsRequired = false)] 241 | public String version; 242 | 243 | [DataMember(Name = "server_addr", IsRequired = false)] 244 | public String server_addr; 245 | 246 | [DataMember(Name = "online", IsRequired = false)] 247 | public Boolean online; 248 | 249 | [DataMember(Name = "heartbeat", IsRequired = false)] 250 | public Boolean heartbeat; 251 | 252 | [DataMember(Name = "lastcontact", IsRequired = false)] 253 | public String lastcontact; 254 | 255 | [DataMember(Name = "heartbeat_sent", IsRequired = false)] 256 | public String heartbeat_sent; 257 | 258 | [DataMember(Name = "heartbeat_rcv", IsRequired = false)] 259 | public String heartbeat_rcv; 260 | 261 | [DataMember(Name = "ltposh_loaded", IsRequired = true)] 262 | public Boolean ltposh_loaded; 263 | } 264 | -------------------------------------------------------------------------------- /Web.en-US.resx: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | text/microsoft-resx 5 | 6 | 7 | 2.0 8 | 9 | 10 | System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 11 | 12 | 13 | System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 14 | 15 | 16 | Run CWA Diagnostic 17 | 18 | 19 | Restart Automate Agent 20 | 21 | 22 | Reinstall Automate Agent 23 | 24 | 25 | Automate Diagnostics 26 | 27 | 28 | Last Updated: 29 | 30 | 31 | CWA Version: 32 | 33 | 34 | CWA ID: 35 | 36 | 37 | --------------------------------------------------------------------------------