Skip to content

Commit 2f4aeb0

Browse files
committed
Switch PluginFrontend to file + signal on macOS
1 parent 0494378 commit 2f4aeb0

File tree

2 files changed

+96
-13
lines changed

2 files changed

+96
-13
lines changed
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,76 @@
11
package protocbridge.frontend
22

3+
import protocbridge.{ExtraEnv, ProtocCodeGenerator}
4+
import sun.misc.{Signal, SignalHandler}
5+
6+
import java.lang.management.ManagementFactory
37
import java.nio.file.attribute.PosixFilePermission
48
import java.nio.file.{Files, Path}
9+
import java.nio.{ByteBuffer, ByteOrder}
510
import java.{util => ju}
11+
import scala.sys.process._
612

713
/** PluginFrontend for macOS.
814
*
9-
* Creates a server socket and uses `nc` to communicate with the socket. We use
10-
* a server socket instead of named pipes because named pipes are unreliable on
11-
* macOS: https://github.com/scalapb/protoc-bridge/issues/366. Since `nc` is
12-
* widely available on macOS, this is the simplest and most reliable solution
13-
* for macOS.
15+
* TODO
1416
*/
15-
object MacPluginFrontend extends SocketBasedPluginFrontend {
17+
object MacPluginFrontend extends PluginFrontend {
18+
case class InternalState(
19+
inputFile: Path,
20+
outputFile: Path,
21+
tempDir: Path,
22+
shellScript: Path
23+
)
24+
25+
override def prepare(
26+
plugin: ProtocCodeGenerator,
27+
env: ExtraEnv
28+
): (Path, InternalState) = {
29+
val tempDirPath = Files.createTempDirectory("protopipe-")
30+
val inputFile = tempDirPath.resolve("input")
31+
val outputFile = tempDirPath.resolve("output")
32+
val sh = createShellScript(getCurrentPid, inputFile, outputFile)
33+
val internalState = InternalState(inputFile, outputFile, tempDirPath, sh)
34+
35+
Signal.handle(
36+
new Signal("USR1"),
37+
new SigUsr1Handler(internalState, plugin, env)
38+
)
39+
40+
(sh, internalState)
41+
}
42+
43+
override def cleanup(state: InternalState): Unit = {
44+
if (sys.props.get("protocbridge.debug") != Some("1")) {
45+
Files.delete(state.inputFile)
46+
Files.delete(state.outputFile)
47+
Files.delete(state.tempDir)
48+
Files.delete(state.shellScript)
49+
}
50+
}
1651

17-
protected def createShellScript(port: Int): Path = {
52+
private def createShellScript(
53+
serverPid: Int,
54+
inputPipe: Path,
55+
outputPipe: Path
56+
): Path = {
1857
val shell = sys.env.getOrElse("PROTOCBRIDGE_SHELL", "/bin/sh")
19-
// We use 127.0.0.1 instead of localhost for the (very unlikely) case that localhost is missing from /etc/hosts.
2058
val scriptName = PluginFrontend.createTempFile(
2159
"",
2260
s"""|#!$shell
2361
|set -e
24-
|nc 127.0.0.1 $port
62+
|# Output PID as 4-byte big-endian.
63+
|printf "%08x" $$$$ | xxd -r -p > "$inputPipe"
64+
|cat /dev/stdin >> "$inputPipe"
65+
|trap 'cat "$outputPipe"' USR1
66+
|kill -USR1 "$serverPid"
67+
|# Use `wait` background `sleep` instead of foreground `sleep`,
68+
|# so that signals are handled immediately instead of after `sleep` finishes.
69+
|sleep 1 & SLEEP_PID=$$!
70+
|# Renew `sleep` if `sleep` expires before the signal (the `wait` result is 0).
71+
|while wait "$$SLEEP_PID"; do sleep 1 & SLEEP_PID=$$!; done
72+
|# Clean up `sleep` if `wait` exits due to the signal (the `wait` result is 128 + SIGUSR1 = 138).
73+
|kill $$SLEEP_PID 2>/dev/null || true
2574
""".stripMargin
2675
)
2776
val perms = new ju.HashSet[PosixFilePermission]
@@ -33,4 +82,40 @@ object MacPluginFrontend extends SocketBasedPluginFrontend {
3382
)
3483
scriptName
3584
}
85+
86+
private def getCurrentPid: Int = {
87+
val jvmName = ManagementFactory.getRuntimeMXBean.getName
88+
val pid = jvmName.split("@")(0)
89+
pid.toInt
90+
}
91+
92+
private class SigUsr1Handler(
93+
internalState: InternalState,
94+
plugin: ProtocCodeGenerator,
95+
env: ExtraEnv
96+
) extends SignalHandler {
97+
override def handle(sig: Signal): Unit = {
98+
val fsin = Files.newInputStream(internalState.inputFile)
99+
100+
val buffer = ByteBuffer.allocate(4).order(ByteOrder.BIG_ENDIAN)
101+
val shPid = if (fsin.read(buffer.array()) == 4) {
102+
buffer.getInt(0)
103+
} else {
104+
fsin.close()
105+
throw new RuntimeException(
106+
s"The first 4 bytes in '${internalState.inputFile}' should be the PID of the shell script"
107+
)
108+
}
109+
110+
val response = PluginFrontend.runWithInputStream(plugin, fsin, env)
111+
fsin.close()
112+
113+
val fsout = Files.newOutputStream(internalState.outputFile)
114+
fsout.write(response)
115+
fsout.close()
116+
117+
// Signal the shell script to read the output file.
118+
s"kill -USR1 $shPid".!!
119+
}
120+
}
36121
}

bridge/src/test/scala/protocbridge/frontend/MacPluginFrontendSpec.scala

+2-4
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,11 @@ package protocbridge.frontend
33
class MacPluginFrontendSpec extends OsSpecificFrontendSpec {
44
if (PluginFrontend.isMac) {
55
it must "execute a program that forwards input and output to given stream" in {
6-
val state = testSuccess(MacPluginFrontend)
7-
state.serverSocket.isClosed mustBe true
6+
testSuccess(MacPluginFrontend)
87
}
98

109
it must "not hang if there is an error in generator" in {
11-
val state = testFailure(MacPluginFrontend)
12-
state.serverSocket.isClosed mustBe true
10+
testFailure(MacPluginFrontend)
1311
}
1412
}
1513
}

0 commit comments

Comments
 (0)