Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 12 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,8 @@ Linux implementation where the measures are actually energy counters for the who
charge of computing the process attribution.

Only Linux/amd64 and macOS (amd64/apple silicon) are supported at the moment. Of note, this tool needs to be run
via `sudo` because power consumption information is considered as security sensitive (as it can
privileged code either via running it with `sudo` (preferred) or giving it access to the read the resources it needs
because power consumption information is considered as security sensitive (as it can
enable [side-channel attacks](https://en.wikipedia.org/wiki/Side-channel_attack)) See below for platform-specific
information.

Expand All @@ -45,8 +46,16 @@ information.
Power monitoring is performed using the
bundled
`[powermetrics](https://developer.apple.com/library/archive/documentation/Performance/Conceptual/power_efficiency_guidelines_osx/PrioritizeWorkAtTheTaskLevel.html#//apple_ref/doc/uid/TP40013929-CH35-SW10)`
tool, which is run with specific parameters and which
output is then parsed into a usable representation.
tool, which is run with specific parameters and which output is then parsed into a usable representation.
There are several options to give access to the tool:

- Add the user running the server to the list of `sudoers` with no-password access to `/usr/bin/powermetrics`
- Run with `sudo` (though this is impractical during development)
- Provide a secret to be able to run the `powermetrics` process (and only this process) using `sudo`. The server will
look for the secret under the `power-server.sudo.secret` property key. Please look
at https://quarkus.io/guides/config-secrets for more details on how to do this securely with Quarkus. Note that this
option is only provided to facilitate development, notably dev mode, and will only work when re-building the project
with the appropriate dependencies.

### Linux

Expand Down
84 changes: 84 additions & 0 deletions server/src/main/java/net/laprun/sustainability/power/Security.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
package net.laprun.sustainability.power;

import java.io.IOException;
import java.io.OutputStream;
import java.io.PrintStream;
import java.util.List;
import java.util.prefs.Preferences;

import jakarta.inject.Singleton;

import io.smallrye.config.SmallRyeConfig;

@Singleton
public class Security {
private final static boolean isRoot;
public static final String SECRET_PROPERTY_KEY = "power-server.sudo.secret";

static {
isRoot = isRunningAsAdministrator();
}

private final String pwd;

public Security(SmallRyeConfig config) {
pwd = config.getConfigValue(SECRET_PROPERTY_KEY).getValue();
if (pwd == null && !isRoot) {
throw new IllegalStateException(
"This application requires sudo access. Either provide a sudo secret using the 'power-server.sudo.secret' property or run using sudo.");
}
}

// figure out if we're running as admin by trying to write a system-level preference
// see: https://stackoverflow.com/a/23538961/5752008
private synchronized static boolean isRunningAsAdministrator() {
final var preferences = Preferences.systemRoot();

// avoid outputting errors
System.setErr(new PrintStream(new OutputStream() {
@Override
public void write(int b) {
}
}));

try {
preferences.put("foo", "bar"); // SecurityException on Windows
preferences.remove("foo");
preferences.flush(); // BackingStoreException on Linux and macOS
return true;
} catch (Exception exception) {
return false;
} finally {
System.setErr(System.err);
}

}

public Process execPowermetrics(String... options) throws IOException {
if (options == null || options.length == 0) {
throw new IllegalArgumentException("No powermetrics options specified");
}
final var args = new String[options.length + 2];
args[0] = "powermetrics";
args[1] = "--samplers";
System.arraycopy(options, 0, args, 2, options.length);
return sudo(args);
}

public Process sudo(String... cmd) throws IOException {
if (cmd == null || cmd.length == 0) {
throw new IllegalArgumentException("No command specified to run with sudo");
}

if (!isRoot) {
final var args = new String[cmd.length + 2];
args[0] = "sudo";
args[1] = "-S";
System.arraycopy(cmd, 0, args, 2, cmd.length);
final var runWithSudo = new ProcessBuilder(args);
return ProcessBuilder.startPipeline(List.of(new ProcessBuilder("echo", pwd), runWithSudo)).getLast();
} else {
return new ProcessBuilder().command(cmd).start();
}
}
}
Original file line number Diff line number Diff line change
@@ -1,23 +1,28 @@
package net.laprun.sustainability.power.sensors;

import jakarta.enterprise.inject.Produces;
import jakarta.inject.Inject;
import jakarta.inject.Singleton;

import net.laprun.sustainability.power.Security;
import net.laprun.sustainability.power.sensors.linux.rapl.IntelRAPLSensor;
import net.laprun.sustainability.power.sensors.macos.powermetrics.ProcessMacOSPowermetricsSensor;

@Singleton
public class PowerSensorProducer {
private static final String OS_NAME = System.getProperty("os.name").toLowerCase();

@Inject
Security security;

@Produces
public PowerSensor sensor() {
return determinePowerSensor();
return determinePowerSensor(security);
}

public static PowerSensor determinePowerSensor() {
public static PowerSensor determinePowerSensor(Security security) {
if (OS_NAME.contains("mac os x")) {
return new ProcessMacOSPowermetricsSensor();
return new ProcessMacOSPowermetricsSensor(security);
}

if (!OS_NAME.contains("linux")) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,18 @@

import java.io.InputStream;

import net.laprun.sustainability.power.Security;

public class ProcessMacOSPowermetricsSensor extends MacOSPowermetricsSensor {
private final Security security;
private Process powermetrics;

public ProcessMacOSPowermetricsSensor() {
public ProcessMacOSPowermetricsSensor(Security security) {
this.security = security;

// extract metadata
try {
final var exec = new ProcessBuilder()
.command("powermetrics", "--samplers", "cpu_power", "-i", "10", "-n", "1")
.start();
final var exec = security.execPowermetrics("cpu_power", "-i", "10", "-n", "1");

// if the process is already dead, get the error
if (!exec.isAlive()) {
Expand All @@ -35,8 +38,8 @@ public void start(long frequency) throws Exception {
if (!isStarted()) {
// it takes some time for the external process in addition to the sampling time so adjust the sampling frequency to account for this so that at most one measure occurs during the sampling time window
final var freq = Long.toString(frequency - 50);
powermetrics = new ProcessBuilder().command("powermetrics", "--samplers", "cpu_power,tasks",
"--show-process-samp-norm", "--show-process-gpu", "-i", freq).start();
powermetrics = security.execPowermetrics("cpu_power,tasks", "--show-process-samp-norm", "--show-process-gpu", "-i",
freq);
}
}

Expand Down