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.
20
58
val scriptName = PluginFrontend .createTempFile(
21
59
" " ,
22
60
s """ |#! $shell
23
61
|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
25
74
""" .stripMargin
26
75
)
27
76
val perms = new ju.HashSet [PosixFilePermission ]
@@ -33,4 +82,40 @@ object MacPluginFrontend extends SocketBasedPluginFrontend {
33
82
)
34
83
scriptName
35
84
}
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
+ }
36
121
}
0 commit comments