Skip to content

Commit 2949802

Browse files
committed
Switch PosixPluginFrontend to sockets
1 parent 91c0519 commit 2949802

9 files changed

+121
-54
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
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.
10+
* We use a server socket instead of named pipes because named pipes are unreliable on macOS:
11+
* https://github.com/scalapb/protoc-bridge/issues/366.
12+
* Since `nc` is widely available on macOS, this is the simplest and most reliable solution for macOS.
13+
*/
14+
object MacPluginFrontend extends SocketBasedPluginFrontend {
15+
16+
protected def createShellScript(port: Int): Path = {
17+
val shell = sys.env.getOrElse("PROTOCBRIDGE_SHELL", "/bin/sh")
18+
// We use 127.0.0.1 instead of localhost for the (very unlikely) case that localhost is missing from /etc/hosts.
19+
val scriptName = PluginFrontend.createTempFile(
20+
"",
21+
s"""|#!$shell
22+
|set -e
23+
|nc 127.0.0.1 $port
24+
""".stripMargin
25+
)
26+
val perms = new ju.HashSet[PosixFilePermission]
27+
perms.add(PosixFilePermission.OWNER_EXECUTE)
28+
perms.add(PosixFilePermission.OWNER_READ)
29+
Files.setPosixFilePermissions(
30+
scriptName,
31+
perms
32+
)
33+
scriptName
34+
}
35+
}

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

+4-2
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,7 @@ package protocbridge.frontend
22

33
import java.io.{ByteArrayOutputStream, InputStream, PrintWriter, StringWriter}
44
import java.nio.file.{Files, Path}
5-
6-
import protocbridge.{ProtocCodeGenerator, ExtraEnv}
5+
import protocbridge.{ExtraEnv, ProtocCodeGenerator}
76

87
import scala.util.Try
98

@@ -133,8 +132,11 @@ object PluginFrontend {
133132

134133
def isWindows: Boolean = sys.props("os.name").startsWith("Windows")
135134

135+
def isMac: Boolean = sys.props("os.name").startsWith("Mac") || sys.props("os.name").startsWith("Darwin")
136+
136137
def newInstance: PluginFrontend = {
137138
if (isWindows) WindowsPluginFrontend
139+
else if (isMac) MacPluginFrontend
138140
else PosixPluginFrontend
139141
}
140142
}

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

+3-1
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,12 @@ 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, etc)
1616
*
1717
* Creates a pair of named pipes for input/output and a shell script that
1818
* communicates with them.
19+
* Compared with `SocketBasedPluginFrontend`,
20+
* this frontend doesn't rely on `nc` that might not be available in some distributions.
1921
*/
2022
object PosixPluginFrontend extends PluginFrontend {
2123
case class InternalState(
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
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(plugin, client.getInputStream, env)
29+
client.getOutputStream.write(response)
30+
} finally {
31+
client.close()
32+
}
33+
}
34+
}
35+
36+
(sh, InternalState(ss, sh))
37+
}
38+
39+
override def cleanup(state: InternalState): Unit = {
40+
state.serverSocket.close()
41+
if (sys.props.get("protocbridge.debug") != Some("1")) {
42+
Files.delete(state.shellScript)
43+
}
44+
}
45+
46+
protected def createShellScript(port: Int): Path
47+
}
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

+8-6
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ class OsSpecificFrontendSpec extends AnyFlatSpec with Matchers {
1616
generator: ProtocCodeGenerator,
1717
env: ExtraEnv,
1818
request: Array[Byte]
19-
): Array[Byte] = {
19+
): (frontend.InternalState, Array[Byte]) = {
2020
val (path, state) = frontend.prepare(
2121
generator,
2222
env
@@ -40,10 +40,10 @@ class OsSpecificFrontendSpec extends AnyFlatSpec with Matchers {
4040
}, _.close()))
4141
process.exitValue()
4242
frontend.cleanup(state)
43-
actualOutput.toByteArray
43+
(state, actualOutput.toByteArray)
4444
}
4545

46-
protected def testSuccess(frontend: PluginFrontend): Unit = {
46+
protected def testSuccess(frontend: PluginFrontend): frontend.InternalState = {
4747
val random = new Random()
4848
val toSend = Array.fill(123)(random.nextInt(256).toByte)
4949
val toReceive = Array.fill(456)(random.nextInt(256).toByte)
@@ -55,11 +55,12 @@ class OsSpecificFrontendSpec extends AnyFlatSpec with Matchers {
5555
toReceive
5656
}
5757
}
58-
val response = testPluginFrontend(frontend, fakeGenerator, env, toSend)
58+
val (state, response) = testPluginFrontend(frontend, fakeGenerator, env, toSend)
5959
response mustBe toReceive
60+
state
6061
}
6162

62-
protected def testFailure(frontend: PluginFrontend): Unit = {
63+
protected def testFailure(frontend: PluginFrontend): frontend.InternalState = {
6364
val random = new Random()
6465
val toSend = Array.fill(123)(random.nextInt(256).toByte)
6566
val env = new ExtraEnv(secondaryOutputDir = "tmp")
@@ -69,7 +70,8 @@ class OsSpecificFrontendSpec extends AnyFlatSpec with Matchers {
6970
throw new OutOfMemoryError("test error")
7071
}
7172
}
72-
val response = testPluginFrontend(frontend, fakeGenerator, env, toSend)
73+
val (state, response) = testPluginFrontend(frontend, fakeGenerator, env, toSend)
7374
response.length must be > 0
75+
state
7476
}
7577
}

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)