1
1
package protocbridge .frontend
2
2
3
+ import protocbridge .{ExtraEnv , ProtocCodeGenerator }
4
+ import sun .misc .{Signal , SignalHandler }
5
+
6
+ import java .lang .management .ManagementFactory
3
7
import java .nio .file .attribute .PosixFilePermission
4
8
import java .nio .file .{Files , Path }
9
+ import java .nio .{ByteBuffer , ByteOrder }
5
10
import java .{util => ju }
11
+ import scala .sys .process ._
6
12
7
13
/** PluginFrontend for macOS.
8
14
*
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
14
16
*/
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
+ }
16
51
17
- protected def createShellScript (port : Int ): Path = {
52
+ private def createShellScript (
53
+ serverPid : Int ,
54
+ inputPipe : Path ,
55
+ outputPipe : Path
56
+ ): Path = {
18
57
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).
20
64
val scriptName = PluginFrontend .createTempFile(
21
65
" " ,
22
66
s """ |#! $shell
23
67
|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
25
75
""" .stripMargin
26
76
)
27
77
val perms = new ju.HashSet [PosixFilePermission ]
@@ -33,4 +83,40 @@ object MacPluginFrontend extends SocketBasedPluginFrontend {
33
83
)
34
84
scriptName
35
85
}
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
+ }
36
122
}
0 commit comments