[platform] more stable machine ID detection methods and saner API

(cherry-picked from commit 5e1a84a75324da0e123a443132aa38ab70c2fdea)

IJ-CR-147524

GitOrigin-RevId: 4d8a0428aa62ab6503940be1e74c47071707e81b
This commit is contained in:
Roman Shevchenko
2024-10-22 19:59:29 +02:00
committed by intellij-monorepo-bot
parent d4672f0d2e
commit cf2c0980d5
9 changed files with 89 additions and 80 deletions

View File

@@ -50,11 +50,9 @@ class ABExperiment {
companion object {
private val AB_EXPERIMENTAL_OPTION_EP = ExtensionPointName<ABExperimentOptionBean>("com.intellij.experiment.abExperimentOption")
private val LOG = logger<ABExperiment>()
private const val DEVICE_ID_PURPOSE = "A/B Experiment"
private val DEVICE_ID_PURPOSE = "A/B Experiment" + ApplicationInfo.getInstance().shortVersion
private const val TOTAL_NUMBER_OF_BUCKETS = 1024
internal const val TOTAL_NUMBER_OF_GROUPS = 256
private val DEVICE_ID_SALT = ApplicationInfo.getInstance().shortVersion
internal val OPTION_ID_FREE_GROUP = ABExperimentOptionId("free.option")
@@ -129,11 +127,11 @@ class ABExperiment {
private fun getUserBucketNumber(): Int {
val deviceId = LOG.runAndLogException {
MachineIdManager.getAnonymizedMachineId(DEVICE_ID_PURPOSE, DEVICE_ID_SALT)
MachineIdManager.getAnonymizedMachineId(DEVICE_ID_PURPOSE)
}
val bucketNumber = MathUtil.nonNegativeAbs(deviceId.hashCode()) % TOTAL_NUMBER_OF_BUCKETS
LOG.debug { "User bucket number is: $bucketNumber." }
return bucketNumber
}
}
}

View File

@@ -1,4 +1,4 @@
// Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.ide.plugins
import com.intellij.ide.IdeBundle
@@ -191,7 +191,7 @@ open class StandalonePluginUpdateChecker(
"https://plugins.jetbrains.com/plugins/list?pluginId=$pluginId&build=$buildNumber&pluginVersion=$currentVersion&os=$os&uuid=$uid"
if (!PropertiesComponent.getInstance().getBoolean(UpdateChecker.MACHINE_ID_DISABLED_PROPERTY, false)) {
val machineId = MachineIdManager.getAnonymizedMachineId("JetBrainsUpdates", "")
val machineId = MachineIdManager.getAnonymizedMachineId("JetBrainsUpdates")
if (machineId != null) {
url += "&${UpdateChecker.MACHINE_ID_PARAMETER}=$machineId"
}

View File

@@ -20,7 +20,7 @@ import static com.intellij.openapi.util.NullableLazyValue.lazyNullable;
@ApiStatus.Internal
public final class UpdateRequestParameters {
private static final NullableLazyValue<String> ourMachineId =
lazyNullable(() -> MachineIdManager.INSTANCE.getAnonymizedMachineId("JetBrainsUpdates", ""));
lazyNullable(() -> MachineIdManager.INSTANCE.getAnonymizedMachineId("JetBrainsUpdates"));
public static @NotNull Url amendUpdateRequest(@NotNull Url url) {
var parameters = new LinkedHashMap<String, String>();

View File

@@ -31,5 +31,7 @@
<orderEntry type="library" name="jackson-module-kotlin" level="project" />
<orderEntry type="library" name="kotlin-reflect" level="project" />
<orderEntry type="library" name="kotlinx-coroutines-core" level="project" />
<orderEntry type="library" scope="TEST" name="JUnit5" level="project" />
<orderEntry type="library" scope="TEST" name="assertJ" level="project" />
</component>
</module>

View File

@@ -195,7 +195,7 @@ class EventLogRecorderConfiguration internal constructor(private val recorderId:
return MachineId.DISABLED
}
val revision = if (value >= 0) value else DEFAULT_ID_REVISION
val machineId = MachineIdManager.getAnonymizedMachineId("JetBrains${alternativeRecorderId ?: recorderId}", salt) ?: return MachineId.UNKNOWN
val machineId = MachineIdManager.getAnonymizedMachineId("JetBrains${alternativeRecorderId ?: recorderId}${salt}") ?: return MachineId.UNKNOWN
return MachineId(machineId, revision)
}
@@ -279,4 +279,4 @@ private class AnonymizedIdsCache {
fun computeIfAbsent(data: String, mappingFunction: (String) -> String): String {
return cache.get(data, mappingFunction)
}
}
}

View File

@@ -27,8 +27,8 @@ class IJFUSMapper: ApplicationUsagesCollector() {
val mlConfig = EventLogConfigOptionsService.getInstance().getOptions(ML_RECORDER)
val fusConfig = EventLogConfigOptionsService.getInstance().getOptions(FUS_RECORDER)
return setOf(report.metric(
MachineIdManager.getAnonymizedMachineId("JetBrains$ML_RECORDER", mlConfig.machineIdSalt ?: ""),
MachineIdManager.getAnonymizedMachineId("JetBrains$FUS_RECORDER", fusConfig.machineIdSalt ?: ""),
MachineIdManager.getAnonymizedMachineId("JetBrains${ML_RECORDER}${mlConfig.machineIdSalt}" ?: ""),
MachineIdManager.getAnonymizedMachineId("JetBrains${FUS_RECORDER}${fusConfig.machineIdSalt}" ?: ""),
))
}
}
}

View File

@@ -1,88 +1,77 @@
// Copyright 2000-2020 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.
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.internal.statistic.eventLog.fus
import com.intellij.execution.configurations.GeneralCommandLine
import com.intellij.execution.util.ExecUtil
import com.intellij.internal.statistic.eventLog.EventLogConfiguration
import com.intellij.openapi.diagnostic.logger
import com.intellij.openapi.util.SystemInfo
import com.intellij.openapi.util.text.StringUtil
import com.sun.jna.platform.mac.IOKitUtil
import com.sun.jna.platform.win32.Advapi32Util
import com.sun.jna.platform.win32.COM.WbemcliUtil
import com.sun.jna.platform.win32.Ole32
import com.sun.jna.platform.win32.WinReg
import java.io.IOException
import java.nio.file.Files
import java.nio.file.Paths
import java.util.regex.Pattern
import java.nio.file.Path
import kotlin.io.path.readText
object MachineIdManager {
private const val IOREG_COMMAND_TIMEOUT_MS = 2000
private val macMachineIdPattern = Pattern.compile("\"IOPlatformUUID\"\\s*=\\s*\"(?<machineId>.*)\"")
private val linuxMachineIdPaths = listOf("/etc/machine-id", "/var/lib/dbus/machine-id")
private val LOG = logger<MachineIdManager>()
@Deprecated("Use `getAnonymizedMachineId(String)`", level = DeprecationLevel.ERROR)
fun getAnonymizedMachineId(purpose: String, salt: String): String? = getAnonymizedMachineId(purpose + salt)
/**
* @param purpose What id will be used for, shouldn't be empty.
* @return Anonymized machine id or null If getting machine id was failed.
* @param purpose what the ID will be used for; must not be empty.
* @return anonymized machine ID, or `null` if getting a machine ID has failed.
*/
fun getAnonymizedMachineId(purpose: String, salt: String): String? {
if (purpose.isEmpty()) {
throw IllegalArgumentException("Argument [purpose] should not be empty.")
fun getAnonymizedMachineId(purpose: String): String? {
require (purpose.isNotEmpty()) { "`purpose` should not be empty" }
return machineId.value?.let { machineId ->
EventLogConfiguration.hashSha256((System.getProperty("user.name") + purpose).toByteArray(), machineId)
}
val machineId = getMachineId() ?: return null
return EventLogConfiguration.hashSha256((System.getProperty("user.name") + purpose + salt).toByteArray(), machineId)
}
private fun getMachineId(): String? {
return try {
private val machineId: Lazy<String?> = lazy {
runCatching {
when {
SystemInfo.isWindows -> {
Advapi32Util.registryGetStringValue(WinReg.HKEY_LOCAL_MACHINE, "SOFTWARE\\Microsoft\\Cryptography", "MachineGuid")
}
SystemInfo.isMac -> {
getMacOsMachineId()
}
SystemInfo.isLinux -> {
getLinuxMachineId()
}
SystemInfo.isWindows -> getWindowsMachineId()
SystemInfo.isMac -> getMacOsMachineId()
SystemInfo.isLinux -> getLinuxMachineId()
else -> null
}
}
catch (e: Throwable) {
null
}
}.onFailure { LOG.debug(it) }.getOrNull()
}
/**
* Reads machineId from /etc/machine-id or if not found from /var/lib/dbus/machine-id
*
* See https://manpages.debian.org/testing/systemd/machine-id.5.en.html for more details
*/
private fun getLinuxMachineId(): String? {
for (machineIdPath in linuxMachineIdPaths) {
try {
val machineId = Files.readString(Paths.get(machineIdPath))
if (!machineId.isNullOrEmpty()) {
return machineId.trim()
/** See [Win32_ComputerSystemProduct](https://learn.microsoft.com/en-us/windows/win32/cimwin32prov/win32-computersystemproduct). */
private fun getWindowsMachineId(): String? =
runCatching {
Ole32.INSTANCE.CoInitializeEx(null, Ole32.COINIT_APARTMENTTHREADED)
WbemcliUtil.WmiQuery("Win32_ComputerSystemProduct", ComputerSystemProductProperty::class.java)
.execute(2000)
.let { result ->
if (result.resultCount > 0) result.getValue(ComputerSystemProductProperty.UUID, 0).toString()
else null
}
}
catch (ignore: IOException) {
}
}
return null
}
}.recover {
LOG.debug(it)
Advapi32Util.registryGetStringValue(WinReg.HKEY_LOCAL_MACHINE, "SOFTWARE\\Microsoft\\Cryptography", "MachineGuid")
}.getOrThrow()
enum class ComputerSystemProductProperty { UUID }
/**
* Invokes `ioreg -rd1 -c IOPlatformExpertDevice` to get IOPlatformUUID
*/
private fun getMacOsMachineId(): String? {
val commandLine = GeneralCommandLine("ioreg", "-rd1", "-c", "IOPlatformExpertDevice")
val processOutput = ExecUtil.execAndGetOutput(commandLine, IOREG_COMMAND_TIMEOUT_MS)
if (processOutput.exitCode == 0) {
val matcher = macMachineIdPattern.matcher(StringUtil.newBombedCharSequence(processOutput.stdout, 1000))
if (matcher.find()) {
return matcher.group("machineId")
}
/** See [IOPlatformExpertDevice](https://developer.apple.com/documentation/kernel/ioplatformexpertdevice). */
private fun getMacOsMachineId(): String? =
IOKitUtil.getMatchingService("IOPlatformExpertDevice")?.let { device ->
val property = device.getStringProperty("IOPlatformUUID")
device.release()
property
}
return null
}
}
/** See [MACHINE-ID(5)](https://manpages.debian.org/testing/systemd/machine-id.5.en.html). */
private fun getLinuxMachineId(): String? =
sequenceOf("/etc/machine-id", "/var/lib/dbus/machine-id")
.map {
runCatching { Path.of(it).readText().trim().takeIf(String::isNotEmpty) }
.onFailure { LOG.debug(it) }
.getOrNull()
}
.firstOrNull()
}

View File

@@ -0,0 +1,20 @@
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.internal.statistic
import com.intellij.internal.statistic.eventLog.fus.MachineIdManager
import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.Assertions.assertThrows
import org.junit.jupiter.api.Test
class MachineIdManagerTest {
@Test fun smoke() {
assertThat(MachineIdManager.getAnonymizedMachineId("test"))
.isNotNull
}
@Test fun contract() {
assertThrows(IllegalArgumentException::class.java) {
MachineIdManager.getAnonymizedMachineId("")
}
}
}

View File

@@ -28,7 +28,7 @@ class IdService : PersistentStateComponentWithModificationTracker<IdService.Stat
"undefined"
}
val machineId by lazy { MachineIdManager.getAnonymizedMachineId("com.intellij.platform.ae.database", "salty") ?: "undefined" }
val machineId by lazy { MachineIdManager.getAnonymizedMachineId("com.intellij.platform.ae.database") ?: "undefined" }
fun getDatabaseId(metadata: SqliteDatabaseMetadata) = metadata.ideId
@@ -54,4 +54,4 @@ class IdService : PersistentStateComponentWithModificationTracker<IdService.Stat
override fun noStateLoaded() {
myState.id = UUID.randomUUID().toString()
}
}
}