fix: improved mkfifo/bash detection for ZIG_PROGRESS

This commit is contained in:
FalsePattern 2025-03-19 16:52:17 +01:00
parent 7ee7e2f3d1
commit cc7d1393d6
Signed by: falsepattern
GPG key ID: E930CDEC50C50E23
5 changed files with 115 additions and 36 deletions

View file

@ -17,6 +17,11 @@ Changelog structure reference:
## [Unreleased] ## [Unreleased]
### Fixed
- Project
- mkfifo/bash for zig progress visualization is now detected more reliably (fixes error on macOS)
## [23.0.0] ## [23.0.0]
### Added ### Added

View file

@ -25,9 +25,10 @@ package com.falsepattern.zigbrains.direnv
import com.intellij.openapi.util.SystemInfo import com.intellij.openapi.util.SystemInfo
import com.intellij.openapi.util.io.toNioPathOrNull import com.intellij.openapi.util.io.toNioPathOrNull
import com.intellij.util.EnvironmentUtil import com.intellij.util.EnvironmentUtil
import kotlinx.coroutines.flow.firstOrNull
import kotlinx.coroutines.flow.flow
import org.jetbrains.annotations.NonNls import org.jetbrains.annotations.NonNls
import java.io.File import java.io.File
import java.nio.file.Path
import kotlin.io.path.* import kotlin.io.path.*
data class Env(val env: Map<String, String>) { data class Env(val env: Map<String, String>) {
@ -36,9 +37,11 @@ data class Env(val env: Map<String, String>) {
private fun getVariable(name: @NonNls String) = private fun getVariable(name: @NonNls String) =
env.getOrElse(name) { EnvironmentUtil.getValue(name) } 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 exeName = if (SystemInfo.isWindows) "$exe.exe" else exe
val paths = path ?: return null val paths = path ?: return@flow
for (dir in paths) { for (dir in paths) {
val path = dir.toNioPathOrNull()?.absolute() ?: continue val path = dir.toNioPathOrNull()?.absolute() ?: continue
if (!path.toFile().exists() || !path.isDirectory()) if (!path.toFile().exists() || !path.isDirectory())
@ -46,9 +49,8 @@ data class Env(val env: Map<String, String>) {
val exePath = path.resolve(exeName).absolute() val exePath = path.resolve(exeName).absolute()
if (!exePath.isRegularFile() || !exePath.isExecutable()) if (!exePath.isRegularFile() || !exePath.isExecutable())
continue continue
return exePath emit(exePath)
} }
return null
} }
} }

View file

@ -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 <https://www.gnu.org/licenses/>.
*/
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)

View file

@ -27,55 +27,102 @@ import com.intellij.execution.configurations.GeneralCommandLine
import com.intellij.openapi.util.SystemInfo import com.intellij.openapi.util.SystemInfo
import com.intellij.openapi.util.io.FileUtil import com.intellij.openapi.util.io.FileUtil
import com.intellij.util.io.awaitExit 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 java.nio.file.Path
import javax.swing.tree.DefaultMutableTreeNode
import kotlin.io.path.deleteIfExists import kotlin.io.path.deleteIfExists
import kotlin.io.path.inputStream
import kotlin.io.path.pathString import kotlin.io.path.pathString
/** /**
* Zig build progress node IPC glue code * Zig build progress node IPC glue code
*/ */
object IPCUtil { 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) { if (SystemInfo.isWindows) {
return false; return null
} }
val mkfifo = emptyEnv.findExecutableOnPATH("mkfifo") val mkfifo = emptyEnv
val bash = emptyEnv.findExecutableOnPATH("bash") .findAllExecutablesOnPATH("mkfifo")
return mkfifo != null && bash != null .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 selectedBash = emptyEnv
val cli = GeneralCommandLine("mkfifo", path.pathString) .findAllExecutablesOnPATH("bash")
val process = cli.createProcess() .map { it.pathString }
val exitCode = process.awaitExit() .filter {
return if (exitCode == 0) AutoCloseable { val cli = GeneralCommandLine(it)
path.deleteIfExists() val tmpFile = FileUtil.createTempFile("zigbrains-bash-detection", null, true).toPath()
} else null 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? { suspend fun wrapWithIPC(cli: GeneralCommandLine): IPC? {
if (!haveIPC) if (!haveIPC)
return null return null
val fifoFile = FileUtil.createTempFile("zigbrains-ipc-pipe", null, true).toPath() val (fifoFile, fifo) = info!!.mkfifo.createTemp() ?: return null
fifoFile.deleteIfExists()
val fifo = mkfifo(fifoFile)
if (fifo == null) {
fifoFile.deleteIfExists()
return null
}
//FIFO created, hack cli //FIFO created, hack cli
val exePath = cli.exePath val exePath = cli.exePath
val args = "exec {var}>${fifoFile.pathString}; ZIG_PROGRESS=\$var $exePath ${cli.parametersList.parametersString}; exec {var}>&-" 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.parametersList.clearAll()
cli.addParameters("-c", args) cli.addParameters("-c", args)
return IPC(cli, fifoFile, fifo) return IPC(cli, fifoFile, fifo)
} }
@JvmRecord
data class MKFifo(val exe: String) {
suspend fun createTemp(): Pair<Path, AutoCloseable>? {
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
}
}
} }

View file

@ -22,14 +22,11 @@
package com.falsepattern.zigbrains.shared.ipc package com.falsepattern.zigbrains.shared.ipc
import com.falsepattern.zigbrains.Icons
import com.falsepattern.zigbrains.project.steps.ui.BaseNodeDescriptor 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.ipc.Payload.Companion.readPayload
import com.falsepattern.zigbrains.shared.zigCoroutineScope import com.falsepattern.zigbrains.shared.zigCoroutineScope
import com.google.common.io.LittleEndianDataInputStream import com.google.common.io.LittleEndianDataInputStream
import com.intellij.icons.AllIcons 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.components.service import com.intellij.openapi.components.service
import com.intellij.openapi.project.Project import com.intellij.openapi.project.Project
@ -93,7 +90,7 @@ class ZigIPCService(val project: Project) {
return roots return roots
} }
private suspend fun watch(ipc: IPCUtil.IPC, process: Process) { private suspend fun watch(ipc: IPC, process: Process) {
val currentNode = IPCTreeNode(BaseNodeDescriptor<Any>(project, "pid: ${process.pid()}", AllIcons.Actions.InlayGear)) val currentNode = IPCTreeNode(BaseNodeDescriptor<Any>(project, "pid: ${process.pid()}", AllIcons.Actions.InlayGear))
mutex.withLock { mutex.withLock {
nodes.add(currentNode) 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 { project.zigCoroutineScope.launch {
watch(ipc, process) watch(ipc, process)
} }