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]
### Fixed
- Project
- mkfifo/bash for zig progress visualization is now detected more reliably (fixes error on macOS)
## [23.0.0]
### Added

View file

@ -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<String, String>) {
@ -36,9 +37,11 @@ data class Env(val env: Map<String, String>) {
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<String, String>) {
val exePath = path.resolve(exeName).absolute()
if (!exePath.isRegularFile() || !exePath.isExecutable())
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.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<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
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<Any>(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)
}