11package protocbridge .frontend
22
3+ import protocbridge .{ExtraEnv , ProtocCodeGenerator }
4+ import sun .misc .{Signal , SignalHandler }
5+
6+ import java .lang .management .ManagementFactory
37import java .nio .file .attribute .PosixFilePermission
48import java .nio .file .{Files , Path }
9+ import java .nio .{ByteBuffer , ByteOrder }
510import 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.
58+ // Output PID as int32 big-endian.
59+ // The current maximum PID on macOS is 99998 (3 bytes) but just in case it's bumped.
60+ // Use `wait` background `sleep` instead of foreground `sleep`,
61+ // so that signals are handled immediately instead of after `sleep` finishes.
62+ // Renew `sleep` if `sleep` expires before the signal (the `wait` result is 0).
63+ // Clean up `sleep` if `wait` exits due to the signal (the `wait` result is 128 + SIGUSR1 = 138).
2064 val scriptName = PluginFrontend .createTempFile(
2165 " " ,
2266 s """ |#! $shell
2367 |set -e
24- |nc 127.0.0.1 $port
68+ |printf "%08x" $$$$ | xxd -r -p > " $inputPipe"
69+ |cat /dev/stdin >> " $inputPipe"
70+ |trap 'cat " $outputPipe"' USR1
71+ |kill -USR1 " $serverPid"
72+ |sleep 1 & SLEEP_PID= $$ !
73+ |while wait " $$ SLEEP_PID"; do sleep 1 & SLEEP_PID= $$ !; done
74+ |kill $$ SLEEP_PID 2>/dev/null || true
2575 """ .stripMargin
2676 )
2777 val perms = new ju.HashSet [PosixFilePermission ]
@@ -33,4 +83,40 @@ object MacPluginFrontend extends SocketBasedPluginFrontend {
3383 )
3484 scriptName
3585 }
86+
87+ private def getCurrentPid : Int = {
88+ val jvmName = ManagementFactory .getRuntimeMXBean.getName
89+ val pid = jvmName.split(" @" )(0 )
90+ pid.toInt
91+ }
92+
93+ private class SigUsr1Handler (
94+ internalState : InternalState ,
95+ plugin : ProtocCodeGenerator ,
96+ env : ExtraEnv
97+ ) extends SignalHandler {
98+ override def handle (sig : Signal ): Unit = {
99+ val fsin = Files .newInputStream(internalState.inputFile)
100+
101+ val buffer = ByteBuffer .allocate(4 ).order(ByteOrder .BIG_ENDIAN )
102+ val shPid = if (fsin.read(buffer.array()) == 4 ) {
103+ buffer.getInt(0 )
104+ } else {
105+ fsin.close()
106+ throw new RuntimeException (
107+ s " The first 4 bytes in ' ${internalState.inputFile}' should be the PID of the shell script "
108+ )
109+ }
110+
111+ val response = PluginFrontend .runWithInputStream(plugin, fsin, env)
112+ fsin.close()
113+
114+ val fsout = Files .newOutputStream(internalState.outputFile)
115+ fsout.write(response)
116+ fsout.close()
117+
118+ // Signal the shell script to read the output file.
119+ s " kill -USR1 $shPid" .!!
120+ }
121+ }
36122}
0 commit comments