Skip to content

Commit 6b92ff4

Browse files
committed
feat: check if app is running as admin, run processes with sudo
1 parent d5b4fda commit 6b92ff4

File tree

4 files changed

+113
-12
lines changed

4 files changed

+113
-12
lines changed

README.md

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,8 @@ Linux implementation where the measures are actually energy counters for the who
3636
charge of computing the process attribution.
3737

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

@@ -45,8 +46,16 @@ information.
4546
Power monitoring is performed using the
4647
bundled
4748
`[powermetrics](https://developer.apple.com/library/archive/documentation/Performance/Conceptual/power_efficiency_guidelines_osx/PrioritizeWorkAtTheTaskLevel.html#//apple_ref/doc/uid/TP40013929-CH35-SW10)`
48-
tool, which is run with specific parameters and which
49-
output is then parsed into a usable representation.
49+
tool, which is run with specific parameters and which output is then parsed into a usable representation.
50+
There are several options to give access to the tool:
51+
52+
- Add the user running the server to the list of `sudoers` with no-password access to `/usr/bin/powermetrics`
53+
- Run with `sudo` (though this is impractical during development)
54+
- Provide a secret to be able to run the `powermetrics` process (and only this process) using `sudo`. The server will
55+
look for the secret under the `power-server.sudo.secret` property key. Please look
56+
at https://quarkus.io/guides/config-secrets for more details on how to do this securely with Quarkus. Note that this
57+
option is only provided to facilitate development, notably dev mode, and will only work when re-building the project
58+
with the appropriate dependencies.
5059

5160
### Linux
5261

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
package net.laprun.sustainability.power;
2+
3+
import java.io.IOException;
4+
import java.io.OutputStream;
5+
import java.io.PrintStream;
6+
import java.util.List;
7+
import java.util.prefs.Preferences;
8+
9+
import jakarta.inject.Singleton;
10+
11+
import io.smallrye.config.SmallRyeConfig;
12+
13+
@Singleton
14+
public class Security {
15+
private final static boolean isRoot;
16+
public static final String SECRET_PROPERTY_KEY = "power-server.sudo.secret";
17+
18+
static {
19+
isRoot = isRunningAsAdministrator();
20+
}
21+
22+
private final String pwd;
23+
24+
public Security(SmallRyeConfig config) {
25+
pwd = config.getConfigValue(SECRET_PROPERTY_KEY).getValue();
26+
if (pwd == null && !isRoot) {
27+
throw new IllegalStateException(
28+
"This application requires sudo access. Either provide a sudo secret using the 'power-server.sudo.secret' property or run using sudo.");
29+
}
30+
}
31+
32+
// figure out if we're running as admin by trying to write a system-level preference
33+
// see: https://stackoverflow.com/a/23538961/5752008
34+
private synchronized static boolean isRunningAsAdministrator() {
35+
final var preferences = Preferences.systemRoot();
36+
37+
// avoid outputting errors
38+
System.setErr(new PrintStream(new OutputStream() {
39+
@Override
40+
public void write(int b) {
41+
}
42+
}));
43+
44+
try {
45+
preferences.put("foo", "bar"); // SecurityException on Windows
46+
preferences.remove("foo");
47+
preferences.flush(); // BackingStoreException on Linux and macOS
48+
return true;
49+
} catch (Exception exception) {
50+
return false;
51+
} finally {
52+
System.setErr(System.err);
53+
}
54+
55+
}
56+
57+
public Process execPowermetrics(String... options) throws IOException {
58+
if (options == null || options.length == 0) {
59+
throw new IllegalArgumentException("No powermetrics options specified");
60+
}
61+
final var args = new String[options.length + 2];
62+
args[0] = "powermetrics";
63+
args[1] = "--samplers";
64+
System.arraycopy(options, 0, args, 2, options.length);
65+
return sudo(args);
66+
}
67+
68+
public Process sudo(String... cmd) throws IOException {
69+
if (cmd == null || cmd.length == 0) {
70+
throw new IllegalArgumentException("No command specified to run with sudo");
71+
}
72+
73+
if (!isRoot) {
74+
final var args = new String[cmd.length + 2];
75+
args[0] = "sudo";
76+
args[1] = "-S";
77+
System.arraycopy(cmd, 0, args, 2, cmd.length);
78+
final var runWithSudo = new ProcessBuilder(args);
79+
return ProcessBuilder.startPipeline(List.of(new ProcessBuilder("echo", pwd), runWithSudo)).getLast();
80+
} else {
81+
return new ProcessBuilder().command(cmd).start();
82+
}
83+
}
84+
}

server/src/main/java/net/laprun/sustainability/power/sensors/PowerSensorProducer.java

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,28 @@
11
package net.laprun.sustainability.power.sensors;
22

33
import jakarta.enterprise.inject.Produces;
4+
import jakarta.inject.Inject;
45
import jakarta.inject.Singleton;
56

7+
import net.laprun.sustainability.power.Security;
68
import net.laprun.sustainability.power.sensors.linux.rapl.IntelRAPLSensor;
79
import net.laprun.sustainability.power.sensors.macos.powermetrics.ProcessMacOSPowermetricsSensor;
810

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

15+
@Inject
16+
Security security;
17+
1318
@Produces
1419
public PowerSensor sensor() {
15-
return determinePowerSensor();
20+
return determinePowerSensor(security);
1621
}
1722

18-
public static PowerSensor determinePowerSensor() {
23+
public static PowerSensor determinePowerSensor(Security security) {
1924
if (OS_NAME.contains("mac os x")) {
20-
return new ProcessMacOSPowermetricsSensor();
25+
return new ProcessMacOSPowermetricsSensor(security);
2126
}
2227

2328
if (!OS_NAME.contains("linux")) {

server/src/main/java/net/laprun/sustainability/power/sensors/macos/powermetrics/ProcessMacOSPowermetricsSensor.java

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,18 @@
22

33
import java.io.InputStream;
44

5+
import net.laprun.sustainability.power.Security;
6+
57
public class ProcessMacOSPowermetricsSensor extends MacOSPowermetricsSensor {
8+
private final Security security;
69
private Process powermetrics;
710

8-
public ProcessMacOSPowermetricsSensor() {
11+
public ProcessMacOSPowermetricsSensor(Security security) {
12+
this.security = security;
13+
914
// extract metadata
1015
try {
11-
final var exec = new ProcessBuilder()
12-
.command("powermetrics", "--samplers", "cpu_power", "-i", "10", "-n", "1")
13-
.start();
16+
final var exec = security.execPowermetrics("cpu_power", "-i", "10", "-n", "1");
1417

1518
// if the process is already dead, get the error
1619
if (!exec.isAlive()) {
@@ -35,8 +38,8 @@ public void start(long frequency) throws Exception {
3538
if (!isStarted()) {
3639
// 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
3740
final var freq = Long.toString(frequency - 50);
38-
powermetrics = new ProcessBuilder().command("powermetrics", "--samplers", "cpu_power,tasks",
39-
"--show-process-samp-norm", "--show-process-gpu", "-i", freq).start();
41+
powermetrics = security.execPowermetrics("cpu_power,tasks", "--show-process-samp-norm", "--show-process-gpu", "-i",
42+
freq);
4043
}
4144
}
4245

0 commit comments

Comments
 (0)