[platform] power status API

Replaces initial implementation with a fuller one and moves it into a more appropriate package.
This commit is contained in:
Roman Shevchenko
2018-06-04 20:59:29 +03:00
parent a114fd4f24
commit 7c8cdf7d65
3 changed files with 259 additions and 110 deletions
@@ -0,0 +1,238 @@
// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
package com.intellij.util.io
import com.intellij.jna.JnaLoader
import com.intellij.openapi.diagnostic.Logger
import com.intellij.openapi.util.SystemInfo
import com.sun.jna.Library
import com.sun.jna.Native
import com.sun.jna.Pointer
import com.sun.jna.Structure
import com.sun.jna.win32.StdCallLibrary
import java.io.BufferedReader
import java.io.File
import java.io.FileReader
import java.io.IOException
private val LOG = Logger.getInstance(PowerStatus::class.java)
enum class PowerStatus {
UNKNOWN, AC, BATTERY;
companion object {
@JvmStatic fun getPowerStatus(): PowerStatus =
try { service.status() }
catch (t: Throwable) {
LOG.warn(t)
UNKNOWN
}
}
}
//<editor-fold desc="Implementation loader">
private interface PowerService {
fun status(): PowerStatus
}
private val service: PowerService by lazy {
try {
when {
SystemInfo.isWindows && JnaLoader.isLoaded() -> WinPowerService()
SystemInfo.isMac && JnaLoader.isLoaded() -> MacPowerService()
SystemInfo.isLinux -> LinuxPowerService()
else -> NullPowerService()
}
}
catch (t: Throwable) {
LOG.warn(t)
NullPowerService()
}
}
private class NullPowerService : PowerService {
override fun status() = PowerStatus.UNKNOWN
}
//</editor-fold>
//<editor-fold desc="Windows implementation">
@Suppress("ClassName", "PropertyName", "FunctionName", "unused")
private class WinPowerService : PowerService {
override fun status(): PowerStatus {
val status = SYSTEM_POWER_STATUS()
if (!kernel32.GetSystemPowerStatus(status)) {
throw IOException("GetSystemPowerStatus(): ${kernel32.GetLastError()}")
}
else {
if (LOG.isDebugEnabled) LOG.debug("ACLineStatus=${status.ACLineStatus}")
return when (status.ACLineStatus.toInt()) {
0 -> PowerStatus.BATTERY
1 -> PowerStatus.AC
else -> PowerStatus.UNKNOWN
}
}
}
class SYSTEM_POWER_STATUS : Structure() {
override fun getFieldOrder() =
listOf("ACLineStatus", "BatteryFlag", "BatteryLifePercent", "SystemStatusFlag", "BatteryLifeTime", "BatteryFullLifeTime")
@JvmField var ACLineStatus: Byte = 0
@JvmField var BatteryFlag: Byte = 0
@JvmField var BatteryLifePercent: Byte = 0
@JvmField var SystemStatusFlag: Byte = 0
@JvmField var BatteryLifeTime: Int = 0
@JvmField var BatteryFullLifeTime: Int = 0
}
private interface Kernel32 : StdCallLibrary {
fun GetSystemPowerStatus(result: SYSTEM_POWER_STATUS): Boolean
fun GetLastError(): Int
}
private val kernel32 = Native.loadLibrary("kernel32", Kernel32::class.java)
}
//</editor-fold>
//<editor-fold desc="macOS implementation">
@Suppress("FunctionName", "unused")
private class MacPowerService : PowerService {
/**
* In IOKit, "power sources" include batteries and UPS devices.
* The method returns "BATTERY" in following cases:
* - when a system has a battery which is discharging
* - when a system has no discharging batteries but has a discharging UPS
*/
override fun status() =
ioKit.IOPSCopyPowerSourcesInfo().use { psBlob ->
ioKit.IOPSCopyPowerSourcesList(psBlob).use { psList ->
var batteryState: PowerStatus? = null
var upsState: PowerStatus? = null
val count = ioKit.CFArrayGetCount(psList)
if (LOG.isDebugEnabled) LOG.debug("count=${count}")
for (i in 0 until count) {
val ps = ioKit.IOPSGetPowerSourceDescription(psBlob, ioKit.CFArrayGetValueAtIndex(psList, i))
if (isTrue(ioKit.CFDictionaryGetValue(ps, kIOPSIsPresentKey))) {
val type = ioKit.CFDictionaryGetValue(ps, kIOPSTypeKey)
val state = ioKit.CFDictionaryGetValue(ps, kIOPSPowerSourceStateKey)
if (LOG.isDebugEnabled) LOG.debug("${i}: type='${str(type)}' state='${str(state)}'")
if (strEquals(type, kIOPSInternalBatteryType)) {
batteryState = if (strEquals(state, kIOPSBatteryPowerValue)) PowerStatus.BATTERY else PowerStatus.AC
break
}
else if (strEquals(type, kIOPSUPSType) && upsState === null) {
upsState = if (strEquals(state, kIOPSBatteryPowerValue)) PowerStatus.BATTERY else PowerStatus.AC
}
}
}
when {
batteryState === PowerStatus.BATTERY -> PowerStatus.BATTERY
upsState !== null -> upsState
else -> PowerStatus.AC
}
}
} ?: PowerStatus.UNKNOWN
private interface IOKit : Library {
fun IOPSCopyPowerSourcesInfo(): Pointer?
fun IOPSCopyPowerSourcesList(psBlob: Pointer): Pointer?
fun IOPSGetPowerSourceDescription(psBlob: Pointer, psName:Pointer): Pointer
fun CFRelease(p: Pointer)
fun CFArrayGetCount(array: Pointer): Long
fun CFArrayGetValueAtIndex(array: Pointer, idx: Long): Pointer
fun CFDictionaryGetValue(dict: Pointer, key: Pointer): Pointer?
fun CFStringCreateWithCharacters(alloc: Pointer?, chars: CharArray, numChars: Long): Pointer
fun CFStringCompare(str1: Pointer, str2: Pointer, flags: Long): Long
fun CFStringGetLength(str: Pointer): Long
fun CFStringGetCharacters(str: Pointer, range: CFRange, buffer: CharArray)
fun CFBooleanGetValue(ref: Pointer): Short
}
class CFRange : Structure(), Structure.ByValue {
override fun getFieldOrder() = listOf("location", "length")
@JvmField var location: Long = 0
@JvmField var length: Long = 0
}
private val ioKit: IOKit = Native.loadLibrary("IOKit", IOKit::class.java)
private val kIOPSIsPresentKey = CFSTR("Is Present")
private val kIOPSTypeKey = CFSTR("Type")
private val kIOPSInternalBatteryType = CFSTR("InternalBattery")
private val kIOPSUPSType = CFSTR("UPS")
private val kIOPSPowerSourceStateKey = CFSTR("Power Source State")
private val kIOPSBatteryPowerValue = CFSTR("Battery Power")
private val kCFCompareEqualTo = 0L
private fun CFSTR(str: String) = ioKit.CFStringCreateWithCharacters(null, str.toCharArray(), str.length.toLong())
private fun isTrue(p: Pointer?) = p !== null && ioKit.CFBooleanGetValue(p).toInt() != 0
private fun strEquals(p: Pointer?, str: Pointer) = p !== null && ioKit.CFStringCompare(p, str, 0L) == kCFCompareEqualTo
private fun str(str: Pointer?): String =
if (str === null) "<null>"
else {
val range = CFRange()
range.length = ioKit.CFStringGetLength(str)
val buffer = CharArray(range.length.toInt())
ioKit.CFStringGetCharacters(str, range, buffer)
String(buffer)
}
private inline fun <T> Pointer?.use(block: (Pointer) -> T): T? =
if (this === null) null
else try { block(this) } finally { ioKit.CFRelease(this) }
}
//</editor-fold>
//<editor-fold desc="Linux implementation">
private class LinuxPowerService : PowerService {
/**
* Returns "AC" if there is at least one online source of type "Mains".
* Returns "BATTERY" if there is at least one source of type "Battery" in a discharging state.
* UPSes doesn't seem to be represented via sysfs.
* See [https://github.com/torvalds/linux/blob/master/drivers/power/supply/power_supply_sysfs.c].
*/
override fun status(): PowerStatus {
val devices = classDirectory.listFiles() ?: throw IOException("can't enumerate devices")
if (LOG.isDebugEnabled) LOG.debug("devices=${devices.size}")
var online = false
var discharging = false
for (device in devices) {
val type = read(device, "type")
if (LOG.isDebugEnabled) LOG.debug("${device.name} type=${type}")
if (type == "Mains") {
val state = read(device, "online")
if (LOG.isDebugEnabled) LOG.debug(" online=${state}")
if (state == "1") online = true
}
else if (type == "Battery") {
val state = read(device, "status")
if (LOG.isDebugEnabled) LOG.debug(" status=${state}")
if (state == "Discharging") discharging = true
}
}
return when {
online -> PowerStatus.AC
discharging -> PowerStatus.BATTERY
else -> PowerStatus.UNKNOWN
}
}
private val classDirectory = File("/sys/class/power_supply")
init {
if (!classDirectory.isDirectory) throw IOException("not a directory: ${classDirectory}")
}
private fun read(device: File, key: String) =
try { BufferedReader(FileReader(File(device, key))).use { it.readLine() } }
catch (e: IOException) { "-" }
}
//</editor-fold>
@@ -1,110 +0,0 @@
// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
package com.intellij.util.system
import com.intellij.execution.ExecutionException
import com.intellij.execution.configurations.GeneralCommandLine
import com.intellij.execution.util.ExecUtil
import com.intellij.jna.JnaLoader
import com.intellij.openapi.util.SystemInfo
import com.intellij.openapi.util.io.FileUtil
import com.intellij.openapi.util.registry.Registry
import com.sun.jna.Native
import com.sun.jna.Structure
import com.sun.jna.win32.StdCallLibrary
import java.io.IOException
enum class PowerStatus {
UNKNOWN, AC_POWER, BATTERY
}
fun getPowerStatus(): PowerStatus {
return when {
SystemInfo.isWindows -> getWindowsPowerStatus()
SystemInfo.isLinux -> getLinuxPowerStatus()
SystemInfo.isMac -> getMacPowerStatus()
else -> PowerStatus.UNKNOWN
}
}
private fun getWindowsPowerStatus(): PowerStatus {
if (!JnaLoader.isLoaded()) return PowerStatus.UNKNOWN
val systemPowerStatus = SYSTEM_POWER_STATUS()
if (!kernel32.GetSystemPowerStatus(systemPowerStatus)) return PowerStatus.UNKNOWN
return when(systemPowerStatus.ACLineStatus.toInt()) {
1 -> PowerStatus.AC_POWER
0 -> PowerStatus.BATTERY
else -> PowerStatus.UNKNOWN
}
}
class SYSTEM_POWER_STATUS : Structure() {
override fun getFieldOrder(): List<String> {
return listOf("ACLineStatus", "BatteryFlag", "BatteryLifePercent", "SystemStatusFlag", "BatteryLifeTime", "BatteryFullLifeTime")
}
@JvmField var ACLineStatus: Byte = 0
@JvmField var BatteryFlag: Byte = 0
@JvmField var BatteryLifePercent: Byte = 0
@JvmField var SystemStatusFlag: Byte = 0
@JvmField var BatteryLifeTime: Int = 0
@JvmField var BatteryFullLifeTime: Int = 0
}
private interface Kernel32 : StdCallLibrary {
fun GetSystemPowerStatus(result: SYSTEM_POWER_STATUS): Boolean
}
private val kernel32 by lazy { Native.loadLibrary("kernel32", Kernel32::class.java) }
// https://github.com/Goles/Battery/blob/master/battery
private fun getLinuxPowerStatus(): PowerStatus {
val file = batteryFilePath ?: return PowerStatus.UNKNOWN
try {
return if (FileUtil.loadFile(file).trim() == "Discharging") PowerStatus.BATTERY else PowerStatus.AC_POWER
}
catch (e: IOException) {
return PowerStatus.UNKNOWN;
}
}
private val batteryFilePath by lazy {
FileUtil.findFirstThatExist("/sys/class/power_supply/BAT0/status")
}
fun getMacPowerStatus(): PowerStatus {
val currentTime = System.currentTimeMillis()
lastMacPowerStatus?.let {
if (currentTime - lastMacPowerStatusUpdateTimestamp > Registry.intValue("ide.mac.power.status.refresh")) {
return it
}
}
val status = updateMacPowerStatus()
lastMacPowerStatusUpdateTimestamp = currentTime
lastMacPowerStatus = status
return status
}
private var lastMacPowerStatus: PowerStatus? = null
private var lastMacPowerStatusUpdateTimestamp: Long = 0
private fun updateMacPowerStatus(): PowerStatus {
try {
val output = ExecUtil.execAndGetOutput(GeneralCommandLine("/usr/bin/pmset", "-g", "batt"))
if (output.exitCode != 0 || output.stderr.isNotEmpty()) return PowerStatus.UNKNOWN
return when {
"'AC Power'" in output.stdout -> PowerStatus.AC_POWER
"'Battery Power'" in output.stdout -> PowerStatus.BATTERY
else -> PowerStatus.UNKNOWN
}
}
catch (e: ExecutionException) {
return PowerStatus.UNKNOWN
}
}
fun main(args: Array<String>) {
val startTime = System.nanoTime()
val powerStatus = getPowerStatus()
val elapsed = (System.nanoTime() - startTime) / 1000
println("The current power status is $powerStatus, retrieved in $elapsed useс")
}
@@ -0,0 +1,21 @@
// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
package com.intellij.util.io;
import com.intellij.testFramework.UsefulTestCase;
import org.junit.Test;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotEquals;
public class PowerStatusTest {
@Test
public void test() {
PowerStatus status = PowerStatus.getPowerStatus();
if (UsefulTestCase.IS_UNDER_TEAMCITY) {
assertEquals(PowerStatus.AC, status);
}
else {
assertNotEquals(PowerStatus.UNKNOWN, status);
}
}
}