Skip to content

Commit c574d50

Browse files
bell-dbthesamet
authored andcommitted
Switch PluginFrontend to sockets on macOS
1 parent 44b9062 commit c574d50

9 files changed

+135
-53
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
package protocbridge.frontend
2+
3+
import java.nio.file.attribute.PosixFilePermission
4+
import java.nio.file.{Files, Path}
5+
import java.{util => ju}
6+
7+
/** PluginFrontend for macOS.
8+
*
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.
14+
*/
15+
object MacPluginFrontend extends SocketBasedPluginFrontend {
16+
17+
protected def createShellScript(port: Int): Path = {
18+
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+
val scriptName = PluginFrontend.createTempFile(
21+
"",
22+
s"""|#!$shell
23+
|set -e
24+
|nc 127.0.0.1 $port
25+
""".stripMargin
26+
)
27+
val perms = new ju.HashSet[PosixFilePermission]
28+
perms.add(PosixFilePermission.OWNER_EXECUTE)
29+
perms.add(PosixFilePermission.OWNER_READ)
30+
Files.setPosixFilePermissions(
31+
scriptName,
32+
perms
33+
)
34+
scriptName
35+
}
36+
}

bridge/src/main/scala/protocbridge/frontend/PluginFrontend.scala

+5
Original file line numberDiff line numberDiff line change
@@ -131,8 +131,13 @@ object PluginFrontend {
131131

132132
def isWindows: Boolean = sys.props("os.name").startsWith("Windows")
133133

134+
def isMac: Boolean = sys.props("os.name").startsWith("Mac") || sys
135+
.props("os.name")
136+
.startsWith("Darwin")
137+
134138
def newInstance: PluginFrontend = {
135139
if (isWindows) WindowsPluginFrontend
140+
else if (isMac) MacPluginFrontend
136141
else PosixPluginFrontend
137142
}
138143
}

bridge/src/main/scala/protocbridge/frontend/PosixPluginFrontend.scala

+5-2
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,13 @@ import scala.concurrent.ExecutionContext.Implicits.global
1212
import scala.sys.process._
1313
import java.{util => ju}
1414

15-
/** PluginFrontend for Unix-like systems (Linux, Mac, etc)
15+
/** PluginFrontend for Unix-like systems <b>except macOS</b> (Linux, FreeBSD,
16+
* etc)
1617
*
1718
* Creates a pair of named pipes for input/output and a shell script that
18-
* communicates with them.
19+
* communicates with them. Compared with `SocketBasedPluginFrontend`, this
20+
* frontend doesn't rely on `nc` that might not be available in some
21+
* distributions.
1922
*/
2023
object PosixPluginFrontend extends PluginFrontend {
2124
case class InternalState(
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
package protocbridge.frontend
2+
3+
import protocbridge.{ExtraEnv, ProtocCodeGenerator}
4+
5+
import java.net.ServerSocket
6+
import java.nio.file.{Files, Path}
7+
import scala.concurrent.ExecutionContext.Implicits.global
8+
import scala.concurrent.{Future, blocking}
9+
10+
/** PluginFrontend for Windows and macOS where a server socket is used.
11+
*/
12+
abstract class SocketBasedPluginFrontend extends PluginFrontend {
13+
case class InternalState(serverSocket: ServerSocket, shellScript: Path)
14+
15+
override def prepare(
16+
plugin: ProtocCodeGenerator,
17+
env: ExtraEnv
18+
): (Path, InternalState) = {
19+
val ss = new ServerSocket(0) // Bind to any available port.
20+
val sh = createShellScript(ss.getLocalPort)
21+
22+
Future {
23+
blocking {
24+
// Accept a single client connection from the shell script.
25+
val client = ss.accept()
26+
try {
27+
val response =
28+
PluginFrontend.runWithInputStream(
29+
plugin,
30+
client.getInputStream,
31+
env
32+
)
33+
client.getOutputStream.write(response)
34+
} finally {
35+
client.close()
36+
}
37+
}
38+
}
39+
40+
(sh, InternalState(ss, sh))
41+
}
42+
43+
override def cleanup(state: InternalState): Unit = {
44+
state.serverSocket.close()
45+
if (sys.props.get("protocbridge.debug") != Some("1")) {
46+
Files.delete(state.shellScript)
47+
}
48+
}
49+
50+
protected def createShellScript(port: Int): Path
51+
}
Original file line numberDiff line numberDiff line change
@@ -1,53 +1,15 @@
11
package protocbridge.frontend
22

3-
import java.net.ServerSocket
4-
import java.nio.file.{Files, Path, Paths}
5-
6-
import protocbridge.ExtraEnv
7-
import protocbridge.ProtocCodeGenerator
8-
9-
import scala.concurrent.blocking
10-
11-
import scala.concurrent.ExecutionContext.Implicits.global
12-
import scala.concurrent.Future
3+
import java.nio.file.{Path, Paths}
134

145
/** A PluginFrontend that binds a server socket to a local interface. The plugin
156
* is a batch script that invokes BridgeApp.main() method, in a new JVM with
167
* the same parameters as the currently running JVM. The plugin will
178
* communicate its stdin and stdout to this socket.
189
*/
19-
object WindowsPluginFrontend extends PluginFrontend {
20-
21-
case class InternalState(batFile: Path)
22-
23-
override def prepare(
24-
plugin: ProtocCodeGenerator,
25-
env: ExtraEnv
26-
): (Path, InternalState) = {
27-
val ss = new ServerSocket(0)
28-
val state = createWindowsScript(ss.getLocalPort)
29-
30-
Future {
31-
blocking {
32-
val client = ss.accept()
33-
val response =
34-
PluginFrontend.runWithInputStream(plugin, client.getInputStream, env)
35-
client.getOutputStream.write(response)
36-
client.close()
37-
ss.close()
38-
}
39-
}
40-
41-
(state.batFile, state)
42-
}
43-
44-
override def cleanup(state: InternalState): Unit = {
45-
if (sys.props.get("protocbridge.debug") != Some("1")) {
46-
Files.delete(state.batFile)
47-
}
48-
}
10+
object WindowsPluginFrontend extends SocketBasedPluginFrontend {
4911

50-
private def createWindowsScript(port: Int): InternalState = {
12+
protected def createShellScript(port: Int): Path = {
5113
val classPath =
5214
Paths.get(getClass.getProtectionDomain.getCodeSource.getLocation.toURI)
5315
val classPathBatchString = classPath.toString.replace("%", "%%")
@@ -62,6 +24,6 @@ object WindowsPluginFrontend extends PluginFrontend {
6224
].getName} $port
6325
""".stripMargin
6426
)
65-
InternalState(batchFile)
27+
batchFile
6628
}
6729
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
package protocbridge.frontend
2+
3+
class MacPluginFrontendSpec extends OsSpecificFrontendSpec {
4+
if (PluginFrontend.isMac) {
5+
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
8+
}
9+
10+
it must "not hang if there is an error in generator" in {
11+
val state = testFailure(MacPluginFrontend)
12+
state.serverSocket.isClosed mustBe true
13+
}
14+
}
15+
}

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

+14-6
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ class OsSpecificFrontendSpec extends AnyFlatSpec with Matchers {
1515
generator: ProtocCodeGenerator,
1616
env: ExtraEnv,
1717
request: Array[Byte]
18-
): Array[Byte] = {
18+
): (frontend.InternalState, Array[Byte]) = {
1919
val (path, state) = frontend.prepare(
2020
generator,
2121
env
@@ -45,10 +45,12 @@ class OsSpecificFrontendSpec extends AnyFlatSpec with Matchers {
4545
)
4646
process.exitValue()
4747
frontend.cleanup(state)
48-
actualOutput.toByteArray
48+
(state, actualOutput.toByteArray)
4949
}
5050

51-
protected def testSuccess(frontend: PluginFrontend): Unit = {
51+
protected def testSuccess(
52+
frontend: PluginFrontend
53+
): frontend.InternalState = {
5254
val random = new Random()
5355
val toSend = Array.fill(123)(random.nextInt(256).toByte)
5456
val toReceive = Array.fill(456)(random.nextInt(256).toByte)
@@ -60,11 +62,15 @@ class OsSpecificFrontendSpec extends AnyFlatSpec with Matchers {
6062
toReceive
6163
}
6264
}
63-
val response = testPluginFrontend(frontend, fakeGenerator, env, toSend)
65+
val (state, response) =
66+
testPluginFrontend(frontend, fakeGenerator, env, toSend)
6467
response mustBe toReceive
68+
state
6569
}
6670

67-
protected def testFailure(frontend: PluginFrontend): Unit = {
71+
protected def testFailure(
72+
frontend: PluginFrontend
73+
): frontend.InternalState = {
6874
val random = new Random()
6975
val toSend = Array.fill(123)(random.nextInt(256).toByte)
7076
val env = new ExtraEnv(secondaryOutputDir = "tmp")
@@ -74,7 +80,9 @@ class OsSpecificFrontendSpec extends AnyFlatSpec with Matchers {
7480
throw new OutOfMemoryError("test error")
7581
}
7682
}
77-
val response = testPluginFrontend(frontend, fakeGenerator, env, toSend)
83+
val (state, response) =
84+
testPluginFrontend(frontend, fakeGenerator, env, toSend)
7885
response.length must be > 0
86+
state
7987
}
8088
}

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

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
package protocbridge.frontend
22

33
class PosixPluginFrontendSpec extends OsSpecificFrontendSpec {
4-
if (!PluginFrontend.isWindows) {
4+
if (!PluginFrontend.isWindows && !PluginFrontend.isMac) {
55
it must "execute a program that forwards input and output to given stream" in {
66
testSuccess(PosixPluginFrontend)
77
}

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

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

910
it must "not hang if there is an OOM in generator" in {
10-
testFailure(WindowsPluginFrontend)
11+
val state = testFailure(WindowsPluginFrontend)
12+
state.serverSocket.isClosed mustBe true
1113
}
1214
}
1315
}

0 commit comments

Comments
 (0)