2626import org .slf4j .LoggerFactory ;
2727
2828import java .io .BufferedReader ;
29+ import java .io .File ;
2930import java .io .IOException ;
3031import java .io .InputStreamReader ;
3132import java .nio .charset .Charset ;
3233import java .util .ArrayList ;
34+ import java .util .Collections ;
3335import java .util .HashMap ;
3436import java .util .HashSet ;
3537import java .util .List ;
3638import java .util .Map ;
3739import java .util .Set ;
40+ import java .util .concurrent .TimeUnit ;
3841
3942/**
4043 * Disk metrics manager for Windows system.
@@ -48,9 +51,13 @@ public class WindowsDiskMetricsManager implements IDiskMetricsManager {
4851
4952 private static final double BYTES_PER_KB = 1024.0 ;
5053 private static final long UPDATE_SMALLEST_INTERVAL = 10000L ;
54+ private static final long POWERSHELL_RETRY_INTERVAL = TimeUnit .MINUTES .toMillis (5 );
55+ private static final long FAILURE_LOG_INTERVAL = TimeUnit .MINUTES .toMillis (5 );
5156 private static final String POWER_SHELL = "powershell" ;
5257 private static final String POWER_SHELL_NO_PROFILE = "-NoProfile" ;
5358 private static final String POWER_SHELL_COMMAND = "-Command" ;
59+ private static final String WINDOWS_POWER_SHELL_RELATIVE_PATH =
60+ "System32\\ WindowsPowerShell\\ v1.0\\ powershell.exe" ;
5461 private static final String TOTAL_DISK_INSTANCE = "_Total" ;
5562 private static final Charset WINDOWS_SHELL_CHARSET = getWindowsShellCharset ();
5663 private static final String DISK_QUERY =
@@ -78,10 +85,14 @@ public class WindowsDiskMetricsManager implements IDiskMetricsManager {
7885 + "$_.IOWriteBytesPerSec) }" ;
7986
8087 private final String processId ;
88+ private final PowerShellExecutor powerShellExecutor ;
8189 private final Set <String > diskIdSet = new HashSet <>();
8290
8391 private long lastUpdateTime = 0L ;
8492 private long updateInterval = 1L ;
93+ private long nextPowerShellRetryTime = 0L ;
94+ private long nextFailureLogTime = 0L ;
95+ private String lastPowerShellFailure = "" ;
8596
8697 private final Map <String , Long > lastReadOperationCountForDisk = new HashMap <>();
8798 private final Map <String , Long > lastWriteOperationCountForDisk = new HashMap <>();
@@ -106,7 +117,14 @@ public class WindowsDiskMetricsManager implements IDiskMetricsManager {
106117 private long lastWriteOpsCountForProcess = 0L ;
107118
108119 public WindowsDiskMetricsManager () {
109- processId = String .valueOf (MetricConfigDescriptor .getInstance ().getMetricConfig ().getPid ());
120+ this (
121+ String .valueOf (MetricConfigDescriptor .getInstance ().getMetricConfig ().getPid ()),
122+ new ProcessBuilderPowerShellExecutor (resolvePowerShellExecutable ()));
123+ }
124+
125+ WindowsDiskMetricsManager (String processId , PowerShellExecutor powerShellExecutor ) {
126+ this .processId = processId ;
127+ this .powerShellExecutor = powerShellExecutor ;
110128 }
111129
112130 @ Override
@@ -447,46 +465,105 @@ private double clampPercentage(double value) {
447465 }
448466
449467 private List <String > executePowerShell (String command ) {
450- List <String > result = new ArrayList <>();
451- List <String > rawOutput = new ArrayList <>();
452- Process process = null ;
468+ if (System .currentTimeMillis () < nextPowerShellRetryTime ) {
469+ return Collections .emptyList ();
470+ }
471+
453472 try {
454- process =
455- new ProcessBuilder (POWER_SHELL , POWER_SHELL_NO_PROFILE , POWER_SHELL_COMMAND , command )
456- .redirectErrorStream (true )
457- .start ();
458- try (BufferedReader reader =
459- new BufferedReader (
460- new InputStreamReader (process .getInputStream (), WINDOWS_SHELL_CHARSET ))) {
461- String line ;
462- while ((line = reader .readLine ()) != null ) {
463- String trimmedLine = line .trim ();
464- if (!trimmedLine .isEmpty ()) {
465- rawOutput .add (trimmedLine );
466- }
467- }
468- }
469- int exitCode = process .waitFor ();
470- if (exitCode != 0 ) {
471- LOGGER .warn (
472- MetricsMessages .FAILED_TO_COLLECT_WINDOWS_DISK_METRICS ,
473- exitCode ,
474- command ,
475- String .join (" | " , rawOutput ));
476- } else {
477- result .addAll (rawOutput );
473+ CommandResult commandResult = powerShellExecutor .execute (command );
474+ List <String > rawOutput =
475+ commandResult .getOutput () == null ? Collections .emptyList () : commandResult .getOutput ();
476+ if (commandResult .getExitCode () != 0 ) {
477+ handlePowerShellFailure (buildPowerShellFailure (commandResult ), null , command , rawOutput );
478+ return Collections .emptyList ();
478479 }
480+ clearPowerShellFailure ();
481+ return rawOutput ;
479482 } catch (IOException e ) {
480- LOGGER . warn (MetricsMessages .FAILED_TO_EXECUTE_POWERSHELL , e );
483+ handlePowerShellFailure (MetricsMessages .FAILED_TO_EXECUTE_POWERSHELL , e , command , null );
481484 } catch (InterruptedException e ) {
482485 Thread .currentThread ().interrupt ();
483- LOGGER .warn (MetricsMessages .INTERRUPTED_COLLECTING_WINDOWS_DISK , e );
484- } finally {
485- if (process != null ) {
486- process .destroy ();
486+ handlePowerShellFailure (
487+ MetricsMessages .INTERRUPTED_COLLECTING_WINDOWS_DISK , e , command , null );
488+ }
489+ return Collections .emptyList ();
490+ }
491+
492+ private String buildPowerShellFailure (CommandResult commandResult ) {
493+ if (isAccessDeniedOutput (commandResult .getOutput ())) {
494+ return "Access denied while collecting windows disk metrics through PowerShell/CIM" ;
495+ }
496+ return String .format (
497+ "Failed to collect windows disk metrics, powershell exit code: %s" ,
498+ commandResult .getExitCode ());
499+ }
500+
501+ private boolean isAccessDeniedOutput (List <String > output ) {
502+ if (output == null ) {
503+ return false ;
504+ }
505+ for (String line : output ) {
506+ if (line == null ) {
507+ continue ;
508+ }
509+ String lowerCaseLine = line .toLowerCase ();
510+ if (lowerCaseLine .contains ("access is denied" )
511+ || lowerCaseLine .contains ("access denied" )
512+ || lowerCaseLine .contains ("permissiondenied" )
513+ || lowerCaseLine .contains ("unauthorized" )
514+ || lowerCaseLine .contains ("0x80041003" )) {
515+ return true ;
487516 }
488517 }
489- return result ;
518+ return false ;
519+ }
520+
521+ private void handlePowerShellFailure (
522+ String failureMessage , Exception exception , String command , List <String > output ) {
523+ long currentTime = System .currentTimeMillis ();
524+ nextPowerShellRetryTime = currentTime + POWERSHELL_RETRY_INTERVAL ;
525+ if (shouldLogFailure (currentTime , failureMessage )) {
526+ if (exception == null ) {
527+ LOGGER .warn (
528+ "{}. Windows disk metrics will be skipped for {} ms before retrying." ,
529+ failureMessage ,
530+ POWERSHELL_RETRY_INTERVAL );
531+ } else {
532+ LOGGER .warn (
533+ "{}: {}. Windows disk metrics will be skipped for {} ms before retrying." ,
534+ failureMessage ,
535+ exception .toString (),
536+ POWERSHELL_RETRY_INTERVAL );
537+ }
538+ LOGGER .debug (
539+ "Failed windows disk metrics powershell command: {}, output: {}" ,
540+ command ,
541+ output == null ? "" : String .join (" | " , output ),
542+ exception );
543+ } else {
544+ LOGGER .debug (
545+ "{}. Windows disk metrics collection is still in retry backoff." ,
546+ failureMessage ,
547+ exception );
548+ }
549+ }
550+
551+ private boolean shouldLogFailure (long currentTime , String failureMessage ) {
552+ if (!failureMessage .equals (lastPowerShellFailure ) || currentTime >= nextFailureLogTime ) {
553+ lastPowerShellFailure = failureMessage ;
554+ nextFailureLogTime = currentTime + FAILURE_LOG_INTERVAL ;
555+ return true ;
556+ }
557+ return false ;
558+ }
559+
560+ private void clearPowerShellFailure () {
561+ if (!lastPowerShellFailure .isEmpty () || nextPowerShellRetryTime > 0L ) {
562+ LOGGER .info ("Recovered windows disk metrics collection through PowerShell/CIM." );
563+ }
564+ lastPowerShellFailure = "" ;
565+ nextFailureLogTime = 0L ;
566+ nextPowerShellRetryTime = 0L ;
490567 }
491568
492569 private static Charset getWindowsShellCharset () {
@@ -506,9 +583,82 @@ private static Charset getWindowsShellCharset() {
506583 return Charset .defaultCharset ();
507584 }
508585
509- private void checkUpdate () {
586+ private static String resolvePowerShellExecutable () {
587+ String systemRoot = System .getenv ("SystemRoot" );
588+ if (systemRoot == null || systemRoot .isEmpty ()) {
589+ systemRoot = System .getenv ("windir" );
590+ }
591+ if (systemRoot != null && !systemRoot .isEmpty ()) {
592+ File systemPowerShell = new File (systemRoot , WINDOWS_POWER_SHELL_RELATIVE_PATH );
593+ if (systemPowerShell .isFile ()) {
594+ return systemPowerShell .getAbsolutePath ();
595+ }
596+ }
597+ return POWER_SHELL ;
598+ }
599+
600+ private synchronized void checkUpdate () {
510601 if (System .currentTimeMillis () - lastUpdateTime > UPDATE_SMALLEST_INTERVAL ) {
511602 updateInfo ();
512603 }
513604 }
605+
606+ interface PowerShellExecutor {
607+ CommandResult execute (String command ) throws IOException , InterruptedException ;
608+ }
609+
610+ static class CommandResult {
611+ private final int exitCode ;
612+ private final List <String > output ;
613+
614+ CommandResult (int exitCode , List <String > output ) {
615+ this .exitCode = exitCode ;
616+ this .output = output ;
617+ }
618+
619+ int getExitCode () {
620+ return exitCode ;
621+ }
622+
623+ List <String > getOutput () {
624+ return output ;
625+ }
626+ }
627+
628+ private static class ProcessBuilderPowerShellExecutor implements PowerShellExecutor {
629+ private final String powerShellExecutable ;
630+
631+ private ProcessBuilderPowerShellExecutor (String powerShellExecutable ) {
632+ this .powerShellExecutable = powerShellExecutable ;
633+ }
634+
635+ @ Override
636+ public CommandResult execute (String command ) throws IOException , InterruptedException {
637+ List <String > rawOutput = new ArrayList <>();
638+ Process process = null ;
639+ try {
640+ process =
641+ new ProcessBuilder (
642+ powerShellExecutable , POWER_SHELL_NO_PROFILE , POWER_SHELL_COMMAND , command )
643+ .redirectErrorStream (true )
644+ .start ();
645+ try (BufferedReader reader =
646+ new BufferedReader (
647+ new InputStreamReader (process .getInputStream (), WINDOWS_SHELL_CHARSET ))) {
648+ String line ;
649+ while ((line = reader .readLine ()) != null ) {
650+ String trimmedLine = line .trim ();
651+ if (!trimmedLine .isEmpty ()) {
652+ rawOutput .add (trimmedLine );
653+ }
654+ }
655+ }
656+ return new CommandResult (process .waitFor (), rawOutput );
657+ } finally {
658+ if (process != null ) {
659+ process .destroy ();
660+ }
661+ }
662+ }
663+ }
514664}
0 commit comments