From cc7d1393d6fe88c3b42dfddd50622e2f844d0d7d Mon Sep 17 00:00:00 2001 From: FalsePattern Date: Wed, 19 Mar 2025 16:52:17 +0100 Subject: [PATCH] fix: improved mkfifo/bash detection for ZIG_PROGRESS --- CHANGELOG.md | 5 + .../com/falsepattern/zigbrains/direnv/Env.kt | 12 ++- .../falsepattern/zigbrains/shared/ipc/IPC.kt | 28 ++++++ .../zigbrains/shared/ipc/IPCUtil.kt | 99 ++++++++++++++----- .../zigbrains/shared/ipc/ZigIPCService.kt | 7 +- 5 files changed, 115 insertions(+), 36 deletions(-) create mode 100644 core/src/main/kotlin/com/falsepattern/zigbrains/shared/ipc/IPC.kt diff --git a/CHANGELOG.md b/CHANGELOG.md index ca3a1e3f..40fb68bc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,11 @@ Changelog structure reference: ## [Unreleased] +### Fixed + +- Project + - mkfifo/bash for zig progress visualization is now detected more reliably (fixes error on macOS) + ## [23.0.0] ### Added diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/direnv/Env.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/direnv/Env.kt index bb108cb1..df168117 100644 --- a/core/src/main/kotlin/com/falsepattern/zigbrains/direnv/Env.kt +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/direnv/Env.kt @@ -25,9 +25,10 @@ package com.falsepattern.zigbrains.direnv import com.intellij.openapi.util.SystemInfo import com.intellij.openapi.util.io.toNioPathOrNull import com.intellij.util.EnvironmentUtil +import kotlinx.coroutines.flow.firstOrNull +import kotlinx.coroutines.flow.flow import org.jetbrains.annotations.NonNls import java.io.File -import java.nio.file.Path import kotlin.io.path.* data class Env(val env: Map) { @@ -36,9 +37,11 @@ data class Env(val env: Map) { private fun getVariable(name: @NonNls String) = env.getOrElse(name) { EnvironmentUtil.getValue(name) } - fun findExecutableOnPATH(exe: @NonNls String): Path? { + suspend fun findExecutableOnPATH(exe: @NonNls String) = findAllExecutablesOnPATH(exe).firstOrNull() + + fun findAllExecutablesOnPATH(exe: @NonNls String) = flow { val exeName = if (SystemInfo.isWindows) "$exe.exe" else exe - val paths = path ?: return null + val paths = path ?: return@flow for (dir in paths) { val path = dir.toNioPathOrNull()?.absolute() ?: continue if (!path.toFile().exists() || !path.isDirectory()) @@ -46,9 +49,8 @@ data class Env(val env: Map) { val exePath = path.resolve(exeName).absolute() if (!exePath.isRegularFile() || !exePath.isExecutable()) continue - return exePath + emit(exePath) } - return null } } diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/shared/ipc/IPC.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/shared/ipc/IPC.kt new file mode 100644 index 00000000..963f6f59 --- /dev/null +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/shared/ipc/IPC.kt @@ -0,0 +1,28 @@ +/* + * This file is part of ZigBrains. + * + * Copyright (C) 2023-2025 FalsePattern + * All Rights Reserved + * + * The above copyright notice and this permission notice shall be included + * in all copies or substantial portions of the Software. + * + * ZigBrains is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, only version 3 of the License. + * + * ZigBrains is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with ZigBrains. If not, see . + */ + +package com.falsepattern.zigbrains.shared.ipc + +import com.intellij.execution.configurations.GeneralCommandLine +import java.nio.file.Path + +data class IPC(val cli: GeneralCommandLine, val fifoPath: Path, val fifoClose: AutoCloseable) \ No newline at end of file diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/shared/ipc/IPCUtil.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/shared/ipc/IPCUtil.kt index 218f6ec8..7a77eb7e 100644 --- a/core/src/main/kotlin/com/falsepattern/zigbrains/shared/ipc/IPCUtil.kt +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/shared/ipc/IPCUtil.kt @@ -27,55 +27,102 @@ import com.intellij.execution.configurations.GeneralCommandLine import com.intellij.openapi.util.SystemInfo import com.intellij.openapi.util.io.FileUtil import com.intellij.util.io.awaitExit -import java.io.File +import kotlinx.coroutines.flow.* +import kotlinx.coroutines.runBlocking +import java.nio.charset.Charset import java.nio.file.Path -import javax.swing.tree.DefaultMutableTreeNode import kotlin.io.path.deleteIfExists +import kotlin.io.path.inputStream import kotlin.io.path.pathString /** * Zig build progress node IPC glue code */ object IPCUtil { - val haveIPC = checkHaveIPC() - private fun checkHaveIPC(): Boolean { + val haveIPC: Boolean get() = info != null + + @JvmRecord + data class IPCInfo(val mkfifo: MKFifo, val bash: String) + + private val info: IPCInfo? by lazy { runBlocking { + createInfo() + } } + + private suspend fun createInfo(): IPCInfo? { if (SystemInfo.isWindows) { - return false; + return null } - val mkfifo = emptyEnv.findExecutableOnPATH("mkfifo") - val bash = emptyEnv.findExecutableOnPATH("bash") - return mkfifo != null && bash != null - } + val mkfifo = emptyEnv + .findAllExecutablesOnPATH("mkfifo") + .map { it.pathString } + .map(::MKFifo) + .toList() + .find { mkfifo -> + val fifo = mkfifo.createTemp() ?: return@find false + fifo.second.close() + true + } ?: return null - private suspend fun mkfifo(path: Path): AutoCloseable? { - val cli = GeneralCommandLine("mkfifo", path.pathString) - val process = cli.createProcess() - val exitCode = process.awaitExit() - return if (exitCode == 0) AutoCloseable { - path.deleteIfExists() - } else null - } + val selectedBash = emptyEnv + .findAllExecutablesOnPATH("bash") + .map { it.pathString } + .filter { + val cli = GeneralCommandLine(it) + val tmpFile = FileUtil.createTempFile("zigbrains-bash-detection", null, true).toPath() + try { + cli.addParameters("-c", "exec {var}>${tmpFile.pathString}; echo foo >&\$var; exec {var}>&-") + val process = cli.createProcess() + val exitCode = process.awaitExit() + if (exitCode != 0) { + return@filter false + } + val text = tmpFile.inputStream().use { it.readAllBytes().toString(Charset.defaultCharset()).trim() } + if (text != "foo") { + return@filter false + } + true + } finally { + tmpFile.deleteIfExists() + } + } + .firstOrNull() ?: return null - data class IPC(val cli: GeneralCommandLine, val fifoPath: Path, val fifoClose: AutoCloseable) + return IPCInfo(mkfifo, selectedBash) + } suspend fun wrapWithIPC(cli: GeneralCommandLine): IPC? { if (!haveIPC) return null - val fifoFile = FileUtil.createTempFile("zigbrains-ipc-pipe", null, true).toPath() - fifoFile.deleteIfExists() - val fifo = mkfifo(fifoFile) - if (fifo == null) { - fifoFile.deleteIfExists() - return null - } + val (fifoFile, fifo) = info!!.mkfifo.createTemp() ?: return null //FIFO created, hack cli val exePath = cli.exePath val args = "exec {var}>${fifoFile.pathString}; ZIG_PROGRESS=\$var $exePath ${cli.parametersList.parametersString}; exec {var}>&-" - cli.withExePath("bash") + cli.withExePath(info!!.bash) cli.parametersList.clearAll() cli.addParameters("-c", args) return IPC(cli, fifoFile, fifo) } + @JvmRecord + data class MKFifo(val exe: String) { + suspend fun createTemp(): Pair? { + val fifoFile = FileUtil.createTempFile("zigbrains-ipc-pipe", null, true).toPath() + fifoFile.deleteIfExists() + val fifo = create(fifoFile) + if (fifo == null) { + fifoFile.deleteIfExists() + return null + } + return Pair(fifoFile, fifo) + } + suspend fun create(path: Path): AutoCloseable? { + val cli = GeneralCommandLine(exe, path.pathString) + val process = cli.createProcess() + val exitCode = process.awaitExit() + return if (exitCode == 0) AutoCloseable { + path.deleteIfExists() + } else null + } + } } \ No newline at end of file diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/shared/ipc/ZigIPCService.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/shared/ipc/ZigIPCService.kt index e06440fc..205387f0 100644 --- a/core/src/main/kotlin/com/falsepattern/zigbrains/shared/ipc/ZigIPCService.kt +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/shared/ipc/ZigIPCService.kt @@ -22,14 +22,11 @@ package com.falsepattern.zigbrains.shared.ipc -import com.falsepattern.zigbrains.Icons import com.falsepattern.zigbrains.project.steps.ui.BaseNodeDescriptor -import com.falsepattern.zigbrains.shared.coroutine.withEDTContext import com.falsepattern.zigbrains.shared.ipc.Payload.Companion.readPayload import com.falsepattern.zigbrains.shared.zigCoroutineScope import com.google.common.io.LittleEndianDataInputStream import com.intellij.icons.AllIcons -import com.intellij.openapi.application.ModalityState import com.intellij.openapi.components.Service import com.intellij.openapi.components.service import com.intellij.openapi.project.Project @@ -93,7 +90,7 @@ class ZigIPCService(val project: Project) { return roots } - private suspend fun watch(ipc: IPCUtil.IPC, process: Process) { + private suspend fun watch(ipc: IPC, process: Process) { val currentNode = IPCTreeNode(BaseNodeDescriptor(project, "pid: ${process.pid()}", AllIcons.Actions.InlayGear)) mutex.withLock { nodes.add(currentNode) @@ -125,7 +122,7 @@ class ZigIPCService(val project: Project) { } } - fun launchWatcher(ipc: IPCUtil.IPC, process: Process) { + fun launchWatcher(ipc: IPC, process: Process) { project.zigCoroutineScope.launch { watch(ipc, process) }