From 7c8cdf7d653182de5ce85876e8a11a6a80d4262f Mon Sep 17 00:00:00 2001 From: Roman Shevchenko Date: Mon, 4 Jun 2018 20:58:48 +0300 Subject: [PATCH] [platform] power status API Replaces initial implementation with a fuller one and moves it into a more appropriate package. --- .../src/com/intellij/util/io/power.kt | 238 ++++++++++++++++++ .../com/intellij/util/system/PowerStatus.kt | 110 -------- .../com/intellij/util/io/PowerStatusTest.java | 21 ++ 3 files changed, 259 insertions(+), 110 deletions(-) create mode 100644 platform/platform-impl/src/com/intellij/util/io/power.kt delete mode 100644 platform/platform-impl/src/com/intellij/util/system/PowerStatus.kt create mode 100644 platform/platform-tests/testSrc/com/intellij/util/io/PowerStatusTest.java diff --git a/platform/platform-impl/src/com/intellij/util/io/power.kt b/platform/platform-impl/src/com/intellij/util/io/power.kt new file mode 100644 index 000000000000..e05389fc0b3f --- /dev/null +++ b/platform/platform-impl/src/com/intellij/util/io/power.kt @@ -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 + } + } +} + +// +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 +} +// + +// +@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) +} +// + +// +@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) "" + 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 Pointer?.use(block: (Pointer) -> T): T? = + if (this === null) null + else try { block(this) } finally { ioKit.CFRelease(this) } +} +// + +// +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) { "-" } +} +// \ No newline at end of file diff --git a/platform/platform-impl/src/com/intellij/util/system/PowerStatus.kt b/platform/platform-impl/src/com/intellij/util/system/PowerStatus.kt deleted file mode 100644 index 0eca2b68e2a9..000000000000 --- a/platform/platform-impl/src/com/intellij/util/system/PowerStatus.kt +++ /dev/null @@ -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 { - 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) { - val startTime = System.nanoTime() - val powerStatus = getPowerStatus() - val elapsed = (System.nanoTime() - startTime) / 1000 - println("The current power status is $powerStatus, retrieved in $elapsed useс") -} diff --git a/platform/platform-tests/testSrc/com/intellij/util/io/PowerStatusTest.java b/platform/platform-tests/testSrc/com/intellij/util/io/PowerStatusTest.java new file mode 100644 index 000000000000..3c601581c602 --- /dev/null +++ b/platform/platform-tests/testSrc/com/intellij/util/io/PowerStatusTest.java @@ -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); + } + } +} \ No newline at end of file