1. Operating System Access

Java developers often don’t realize how much the Java libraries abstract from the operating system. A few examples: Java automatically sets the correct path separator (/ or \) for path entries and automatically sets the operating system’s usual end-of-line character for line breaks. When formatting for console input and when parsing console input, Java automatically falls back on the set language of the operating system.

Not only does the Java library use these properties internally, but they are accessible to all. The properties are hidden in various places, such as:

  • in system properties

  • in platform MXBeans, like the OperatingSystemMXBean you get with getSystemCpuLoad()

  • in java.net.NetworkInterface for network cards and MAC address

  • in Toolkit for screen resolution

  • in GraphicsEnvironment for the installed fonts

If special information is missing, Java programs can call external native programs and interact with them, for example to ask for more details. The tasks in this chapter focus on system properties and how to call external programs.

Prerequisites

  • know how to interact with the command line

  • be able to evaluate environment variables

  • be able to start external programs

Data types used in this chapter:

1.1. Console

Java programs are not just algorithms, they interact with us and with the operating system. Two of the first statements were System.out.println() and new Scanner(System.in).next() — a screen output and console input are typical interfaces between program and user.

System.out and System.err are of type PrintStream, System.in is of type InputStream. The class System has methods to redirect the three streams, for example into files.

1.1.1. Colored console outputs ⭐

Even in the very earliest Java programs, we output text on the console. However, the text output on System.out and System.err shows the text once in black, then in red, so there are different colors.

In this task we want to deal with the question how we can write colored outputs ourselves. The solution behind this is that outputs write more than just text. There are special commands that change the color in the console, move the cursor, clear the screen, and so on. This is done by ANSI escape sequences; they start with a control sequence introducer, the string

\u001B[

and they end with the string

m

\u001B is the ESC character in the ASCII alphabet, decimal 27.

In Java, escape sequences can easily be written to System.out or System.err, and there are libraries such as https://github.com/fusesource/jansi that simplify this via constants and methods. The only problem is that the different consoles do not necessarily recognize all escape sequences. By default, Windows' cmd.exe, for example, is unable to recognize any of them. However, support for some colors is common on other consoles:

  • Black: \u001B[30m

  • Red: \u001B[31m

  • Green: \u001B[32m

  • Yellow: \u001B[33m

  • Blue: \u001B[34m

  • Magenta: \u001B[35m

  • Cyan: \u001B[36m

  • White: \u001B[37m

  • Reset: \u001B[0m

Suitable for decoration:

  • bold: \u001B[1m

  • underlined: \u001B[4m

  • inverted: \u001B[7m

Task:

  • Create a new class AnsiColorHexDumper, and put the following constants in the class:

    public static final String ANSI_RED    = "\u001B[31m";
    public static final String ANSI_GREEN  = "\u001B[32m";
    public static final String ANSI_BLUE   = "\u001B[34m";
    public static final String ANSI_PURPLE = "\u001B[35m";
    public static final String ANSI_CYAN   = "\u001B[36m";
    public static final String ANSI_RESET  = "\u001B[0m";
  • Write a new method printColorfulHexDump(Path) that reads the given file and prints it as a hexdump on the console. A hexdump is the output of a file in hexadecimal notation, i.e. sequences like 50 4B 03 04 14 00 06 00 08 00 written in columns.

  • Extend the program so that colors indicate the occurrence of certain bytes in the file. For example, ASCII letters can appear in one color, but digits in another.

1.2. Properties

The term properties is used multiple times in Java. It stands for the JavaBean properties and for key-value pairs that can be used for configuration. When we talk about "properties" in this chapter, we always mean key-value pairs, especially the pairs that a Properties object manages.

1.2.1. Windows or Unix or macOS? ⭐

The system properties contain a set of information, some of which is accessible via methods. The Javadoc lists the variables at https://docs.oracle.com/javase/8/docs/api/java/lang/System.html#getProperties--.

Task:

  • Create a new enumeration type OS with the constants WINDOWS, MACOS, UNIX, UNKNOWN.

  • Add a static method current() which reads the name of the operating system with System.getProperty("os.name") and returns the corresponding enumeration element as result.

1.2.2. Unify command line properties and properties from files ⭐

Properties can be set from the command line and can thus be introduced into a Java program from the outside.

Web servers run on different ports, usually 8080 in development mode.

Task:

  • Write a program that can accept port information from different sources:

    • On the command line the port can be specified with --port=8000.

    • If --port does not exist, an environment variable port should be evaluated, which can also be set on the command line with -Dport=8020.

    • If the environment variable does not exist, an assignment like port=8888 is to be evaluated in a application.properties file.

    • If no specification is made at all, the port is set to 8080 by default.

  • Finally, output the port.

Example for three call variants:

$ java com.tutego.exercise.os.PortConfiguration
$ java com.tutego.exercise.os.PortConfiguration --port=8000
$ java -Dport=8020 com.tutego.exercise.os.PortConfiguration

1.3. Execute external processes

As a platform-independent programming language, Java cannot offer everything, and so there are several ways to make requests to the host environment, to the operating system. One simple way is to call external programs, which is what is asked for in the next task.

1.3.1. Read the battery status via Windows Management Instrumentation ⭐⭐

Bonny Brain is playing a strategy game on her notebook. The next round will take 30 minutes, and since her notebook runs on battery power only, she doesn’t want to have to abort the game if the battery is about to die. The following calls can be used on the command line in Windows to determine the percentage of runtime remaining and the number of minutes remaining estimated by that of the current load:

> wmic path win32_battery get EstimatedChargeRemaining
EstimatedChargeRemaining
10

> wmic path win32_battery get EstimatedRunTime
EstimatedRunTime
37

If the computer is not a laptop with battery the output is "No instances available." If the laptop is loaded, the result for EstimatedRunTime is 71582788 (hexadecimal 4444444). Microsoft provides details at https://docs.microsoft.com/en-us/windows/win32/cimwin32prov/win32-battery.

Task:

  • In a Java program, start the Windows program wmic as an external process using ProcessBuilder, and read the result of EstimatedChargeRemaining and EstimatedRunTime.

  • Take into account in the program and in the output that a laptop can be powered from the power grid or a desktop computer has no battery.

Microsoft’s operating system provides an API named Windows Management Instrumentation (WMI) that can be used to read and, in some cases, change settings on a computer. WMI is comparable to JMX on the Java side. Because shell scripts often want to access this mapping for automation purposes, Microsoft has created a command line interface Windows Management Instrumentation Command-line (WMIC).

A WMI provider provides information about CPU usage, motherboard, network, battery status and much more. The following call gives a small overview:

> wmic /?

1.4. Suggested solutions

1.4.1. Colored console outputs

com/tutego/exercise/os/AnsiColorHexDumper.java
private static final int EOF = -1;
private static final int HEX_PER_LINE = 32;
private static void printColorfulHexDump( Path path ) throws IOException {
  try ( InputStream is = new BufferedInputStream(Files.newInputStream(path)) ) {
    for ( int i = 0, b; (b = is.read()) != EOF; i++ ) {
      String color = b == 0 ? ANSI_GREEN :
                     b == 0xFF ? ANSI_RED :
                     Character.isDigit( b ) ? ANSI_PURPLE :
                     Character.isLetter( b ) ? ANSI_BLUE :
                     b == ' ' ? ANSI_CYAN :
                     ANSI_RESET;
      System.out.printf( "%s%02X ", color, b );
      if ( i % HEX_PER_LINE == (HEX_PER_LINE - 1) )
        System.out.println();
    }
  }
}

Instead of reading the input in one go with Files.readAllBytes(…​), we open an input stream to then read byte by byte. The for loop declares two variables: A variable i controls the newline, which should always be set after 32 hexadecimal characters, and the variable b contains the read byte. The InputStream is read as long as the result is not unequal -1, i.e. bytes are still available.

In the body of the for loop the read byte is tested and depending on the assignment the variable color is initialized with the ANSI escape sequence. If the byte does not fall into the five categories, the color is reset. Finally, the escape sequence is written together with the hexadecimal code and a newline is set after a maximum of 32 hexadecimal characters in one line.

1.4.2. Windows or Unix or macOS?

com/tutego/exercise/os/OS.java
public enum OS {
  WINDOWS,
  MACOS,
  UNIX,
  UNKNOWN;

  public static OS current() {
    String osName = System.getProperty( "os.name" );
    if ( osName == null ) return UNKNOWN;
    osName = osName.toLowerCase();
    return osName.contains( "windows" ) ? OS.WINDOWS :
           osName.contains( "mac" ) ? OS.MACOS :
           osName.contains( "nix" ) || osName.contains( "nux" ) ? OS.UNIX :
           UNKNOWN;
  }
}

The method current() reads the name of the operating system from the property os.name. If it is null, the answer is obvious, otherwise the method converts the name to lowercase for the comparisons and tests for various substrings in a cascade with condition operators. If no case distinction is caught, the name of the operating system is unknown.

1.4.3. Unify command line properties and properties from files

Since all parameters, no matter how presented, are strings, there is a separate method parseInt(String) that attempts to convert a string to a number:

com/tutego/exercise/os/PortConfiguration.java
private static final String PORT = "port";
private static final int DEFAULT_PORT = 8080;

private static OptionalInt parseInt( String value ) {
  try {
    return OptionalInt.of( Integer.parseInt( value ) );
  }
  catch ( NumberFormatException e ) {
    return OptionalInt.empty();
  }
}

The parseInt(String) method accesses Integer.parseInt(…​) and catches the exceptions if the string cannot be converted to a number. Our method catches the exception and returns an OptionalInt.empty(), otherwise an OptionalInt with the parsed number.

Two more helper methods follow:

com/tutego/exercise/os/PortConfiguration.java
private static OptionalInt portFromCommandLine( String[] args ) {
  for ( String arg : args )
    if ( arg.startsWith( "--" + PORT + "=" ) )
      return parseInt( arg.substring( ("--" + PORT + "=").length() ) );
  return OptionalInt.empty();
}

private static OptionalInt portFromPropertyFile() {
  String filename = "/application.properties";
  try ( InputStream is = PortConfiguration.class.getResourceAsStream(filename) ) {
    Properties properties = new Properties();
    properties.load( is );
    return parseInt( properties.getProperty( PORT ) );
  }
  catch ( IOException e ) { /* Ignore */ }
  return OptionalInt.empty();
}

portFromCommandLine(…​) gets as parameter the command line arguments for evaluation. Since in principle multiple command line arguments can be passed to the program, the method iterates through all console passes and examines whether --port= is present. If so, the method truncates the front part and returns the result of parseInt(String). If the method finds nothing, the OptionInt is empty().

portFromPropertyFile(…​) evaluates the application.properties file in the classpath. File contents in the class path are read in via the Class method getResourceAsStream(…​), because that also supports resources packaged in a JAR archive. We first build an empty Properties object, then call the load(…​) method and pass the InputStream. If the file does not exist, the InputStream will be empty, which will throw an exception, but catch will catch it, as well as possible errors during loading. If the Properties object could be filled, we ask for the port property and convert it to an OptionalInt via our own method; if it is filled, we return the value.

The last method is port(…​) ties everything together.

com/tutego/exercise/os/PortConfiguration.java
static int port( String[] args ) {
  // Step 1
  OptionalInt maybePort = portFromCommandLine( args );
  if ( maybePort.isPresent() )
    return maybePort.getAsInt();

  // Step 2
  OptionalInt maybePortProperty = parseInt( System.getProperty( PORT ) );
  if ( maybePortProperty.isPresent() )
    return maybePortProperty.getAsInt();

  // Step 3
  OptionalInt maybePortApplicationProperty = portFromPropertyFile();
  if ( maybePortApplicationProperty.isPresent() )
    return maybePortApplicationProperty.getAsInt();

  // Step 4
  return DEFAULT_PORT;
}

First we test the command line. If there is a hit, we don’t have to consider everything else, because passing on the command line has the highest priority.

If there is no exit from the first step, we continue with the second step. System.getProperty(String) reads a system property, we pass the return in parseInt(…​). If the property does not exist, our parseInt(…​) will return an OptionalInt.empty(). If there is an assignment, port(…​) will return it.

If there was not the key with an associated value, we have to open the file and we are in step 3.

If there is no file or no port specification, comes the last step, step 4. We have no more options and return as default port 8080.

1.4.4. Read the battery status via Windows Management Instrumentation

com/tutego/exercise/os/WmicBattery.java
static OptionalInt wmicBattery( String name ) {
  try {
    String[] command = { "CMD", "/C",
                         "wmic", "path", "win32_battery", "get", name };
    Process process = new ProcessBuilder( command ).start();
    try ( InputStream is = process.getInputStream();
          Reader isr = new InputStreamReader( is );
          Stream<String> stream = new BufferedReader( isr ).lines() ) {
      return stream.map( String::trim )
                   .filter( s -> s.matches( "\\d+" ) )
                   .mapToInt( Integer::parseInt )
                   .findFirst();
    }
  }
  catch ( IOException e ) {
    Logger.getLogger( WmicBattery.class.getName() ).info( e.toString() );
    return OptionalInt.empty();
  }
}

Since our program calls WMIC twice with different parameters, the proposed solution introduces a new method wmicBattery(…​) that can be passed EstimatedChargeRemaining and EstimatedRunTime in our case. This works well, because both queries always expect a numeric value.

wmicBattery(…​) declares a new array and passes it to the constructor of ProcessBuilder so that the process can be started with start(). The return is the Process from which we get the output in the next step, which is our InputStream. We don`t have to write anything into the process ourselves, so a separate OutputStream is not necessary. We also don`t have to wait for the process to finish, that happens automatically.

There are different approaches to extract the relevant parts from the input stream. This proposed solution converts the InputStream into a BufferedReader so that the lines() method can be used, which returns a Stream of all lines. This stream first removes the white space at the beginning and end of each line, and then leaves only those lines in the stream that contain a number. This leaves either one line or no line. If a line contains a number, it is converted, resulting in an IntStream where findFirst() gives us an OptionalInt that is either empty if, for example, there is no number in the process output due to a missing battery, or otherwise contains the number. Possible errors during processing are caught and logged; the return is then an OptionalInt.empty().

In use, the method can be called as follows:

com/tutego/exercise/os/WmicBattery.java
wmicBattery( "EstimatedChargeRemaining" ).ifPresentOrElse(
    value -> System.out.printf( "Estimated charge remaining: %d %%%n", value ),
    () -> System.out.println( "No instances available." ) );

wmicBattery( "EstimatedRunTime" ).ifPresentOrElse(
    minutes -> System.out.printf(
        minutes == 0X4444444 ?
        "Charging" :
        "Estimated run time: %d:%02d h (%d minutes)%n",
        minutes / 60, minutes % 60,minutes ),
    () -> System.out.println( "No instances available." ) );

For EstimatedChargeRemaining, there is no value that needs special interpretation. If the OptionalInt is empty, a different operation should be performed than if the OptionalInt contains something. This is where the OptionalInt method ifPresentOrElse(IntConsumer action, Runnable emptyAction) is useful.

For EstimatedRunTime there is the special case that the battery is currently being charged and thus there is no estimate of the remaining time. The code tests this case, and then returns a different string. Arguments are always passed to the printf(…​) method, which are of course ignored in the charging case.

Java 8 Backport

The OptionalInt method ifPresentOrElse(IntConsumer action, Runnable emptyAction) has been around since Java 9. From the OpenJDK:

excerpt from the OpenJDK
public void ifPresentOrElse( IntConsumer action, Runnable emptyAction ) {
  if ( isPresent ) { action.accept( value ); }
  else { emptyAction.run(); }
}

Under Java 8, we need to reprogram this functionality.

The output of the properties can quickly become confusing, like

> wmic diskdrive get
Availability BytesPerSector Capabilities CapabilityDescriptions Caption CompressionMethod ConfigManagerErrorCode ConfigManagerUserConfig CreationClassName DefaultBlockSize Description DeviceID ErrorCleared ErrorDescription ErrorMethodology FirmwareRevision Index InstallDate InterfaceType LastErrorCode Manufacturer MaxBlockSize MaxMediaSize MediaLoaded MediaType MinBlockSize Model Name NeedsCleaning NumberOfMediaSupported Partitions PNPDeviceID PowerManagementCapabilities PowerManagementSupported SCSIBus SCSILogicalUnit SCSIPort SCSITargetId SectorsPerTrack SerialNumber Signature Size Status StatusInfo SystemCreationClassName SystemName TotalCylinders TotalHeads TotalSectors TotalTracks TracksPerCylinder
512 {3, 4} {"Random Access", "Supports Writing"} SAMSUNG MZVLB1T0HALR-00000 0 FALSE Win32_DiskDrive Laufwerk \\.\PHYSICALDRIVE1 EXA7201Q 1 SCSI (Standardlaufwerke) TRUE Fixed hard disk media SAMSUNG MZVLB1T0HALR-00000 \\.\PHYSICALDRIVE1 3 SCSI\DISK&VEN_NVME&PROD_SAMSUNG_MZVLB1T0\5&1E3C5E74&0&000000 0 0 1 0 63 0025_3886_81B2_A1AD. 1024203640320 OK Win32_ComputerSystem DESKTOP-0P7C7G7 124519 255 2000397735 31752345 255
512 {3, 4, 10} {"Random Access", "Supports Writing", "SMART Notification"} WDC WD40EZRX-22SPEB0 0 FALSE Win32_DiskDrive Laufwerk \\.\PHYSICALDRIVE0 80.00A80 0 IDE (Standardlaufwerke) TRUE Fixed hard disk media WDC WD40EZRX-22SPEB0 \\.\PHYSICALDRIVE0 1 SCSI\DISK&VEN_WDC&PROD_WD40EZRX-22SPEB0\4&2D010F8D&0&000000 0 0 0 0 63 WD-WCC4E2SHPE5N 4000784417280 OK Win32_ComputerSystem DESKTOP-0P7C7G7 486401 255 7814032065 124032255 255

shows. This is difficult to parse, which is why WMIC provides different output formats. At the end of the command a /format:csv can be appended, which makes the output much easier to process.

> wmic diskdrive get /format:csv
Node,Availability,BytesPerSector,Capabilities,CapabilityDescriptions,Caption,CompressionMethod,ConfigManagerErrorCode,ConfigManagerUserConfig,CreationClassName,DefaultBlockSize,Description,DeviceID,ErrorCleared,ErrorDescription,ErrorMethodology,FirmwareRevision,Index,InstallDate,InterfaceType,LastErrorCode,Manufacturer,MaxBlockSize,MaxMediaSize,MediaLoaded,MediaType,MinBlockSize,Model,Name,NeedsCleaning,NumberOfMediaSupported,Partitions,PNPDeviceID,PowerManagementCapabilities,PowerManagementSupported,SCSIBus,SCSILogicalUnit,SCSIPort,SCSITargetId,SectorsPerTrack,SerialNumber,Signature,Size,Status,StatusInfo,SystemCreationClassName,SystemName,TotalCylinders,TotalHeads,TotalSectors,TotalTracks,TracksPerCylinder
DESKTOP-0P7C7G7,,512,{3;4},{Random Access;Supports Writing},SAMSUNG MZVLB1T0HALR-00000,,0,FALSE,Win32_DiskDrive,,Laufwerk,\\.\PHYSICALDRIVE1,,,,EXA7201Q,1,,SCSI,,(Standardlaufwerke),,,TRUE,Fixed hard disk media,,SAMSUNG MZVLB1T0HALR-00000,\\.\PHYSICALDRIVE1,,,3,SCSI\DISK&VEN_NVME&PROD_SAMSUNG_MZVLB1T0\5&1E3C5E74&0&000000,,,0,0,1,0,63,0025_3886_81B2_A1AD.,,1024203640320,OK,,Win32_ComputerSystem,DESKTOP-0P7C7G7,124519,255,2000397735,31752345,255
DESKTOP-0P7C7G7,,512,{3;4;10},{Random Access;Supports Writing;SMART Notification},WDC WD40EZRX-22SPEB0,,0,FALSE,Win32_DiskDrive,,Laufwerk,\\.\PHYSICALDRIVE0,,,,80.00A80,0,,IDE,,(Standardlaufwerke),,,TRUE,Fixed hard disk media,,WDC WD40EZRX-22SPEB0,\\.\PHYSICALDRIVE0,,,1,SCSI\DISK&VEN_WDC&PROD_WD40EZRX-22SPEB0\4&2D010F8D&0&000000,,,0,0,0,0,63, WD-WCC4E2SHPE5N,,4000784417280,OK,,Win32_ComputerSystem,DESKTOP-0P7C7G7,486401,255,7814032065,124032255,255

The first line contains the column names, followed by the comma-separated values.

If only selected keys are requested, they are listed after get, for example:

> wmic diskdrive get Model,Size,SerialNumber /format:csv

Node,Model,SerialNumber,Size
DESKTOP-0P7C7G7,SAMSUNG MZVLB1T0HALR-00000,0025_3886_81B2_A1AD.,1024203640320
DESKTOP-0P7C7G7,WDC WD40EZRX-22SPEB0, WD-WCC4E2SHPE5N,4000784417280

Also /format:list is good to parse, because it produces the property format known under Java, which is handy when there is a result. There are many other formats.