|
1 | 1 | package protocbridge.frontend
|
2 | 2 |
|
3 |
| -import java.nio.file.{Files, Path} |
| 3 | +import protocbridge.{ExtraEnv, ProtocCodeGenerator} |
4 | 4 |
|
5 |
| -import protocbridge.ProtocCodeGenerator |
6 |
| -import protocbridge.ExtraEnv |
| 5 | +import java.net.ServerSocket |
7 | 6 | import java.nio.file.attribute.PosixFilePermission
|
8 |
| - |
9 |
| -import scala.concurrent.blocking |
10 |
| -import scala.concurrent.Future |
11 |
| -import scala.concurrent.ExecutionContext.Implicits.global |
12 |
| -import scala.sys.process._ |
| 7 | +import java.nio.file.{Files, Path} |
13 | 8 | import java.{util => ju}
|
| 9 | +import scala.concurrent.ExecutionContext.Implicits.global |
| 10 | +import scala.concurrent.{Future, blocking} |
14 | 11 |
|
15 | 12 | /** PluginFrontend for Unix-like systems (Linux, Mac, etc)
|
16 | 13 | *
|
17 |
| - * Creates a pair of named pipes for input/output and a shell script that |
18 |
| - * communicates with them. |
| 14 | + * Creates a server socket and a shell script that communicates with it. |
19 | 15 | */
|
20 | 16 | object PosixPluginFrontend extends PluginFrontend {
|
21 |
| - case class InternalState( |
22 |
| - inputPipe: Path, |
23 |
| - outputPipe: Path, |
24 |
| - tempDir: Path, |
25 |
| - shellScript: Path |
26 |
| - ) |
| 17 | + case class InternalState(serverSocket: ServerSocket, shellScript: Path) |
27 | 18 |
|
28 | 19 | override def prepare(
|
29 | 20 | plugin: ProtocCodeGenerator,
|
30 | 21 | env: ExtraEnv
|
31 | 22 | ): (Path, InternalState) = {
|
32 |
| - val tempDirPath = Files.createTempDirectory("protopipe-") |
33 |
| - val inputPipe = createPipe(tempDirPath, "input") |
34 |
| - val outputPipe = createPipe(tempDirPath, "output") |
35 |
| - val sh = createShellScript(inputPipe, outputPipe) |
| 23 | + // We use socket instead of named pipes. |
| 24 | + // This is because named pipes are unreliable on macOS: https://github.com/scalapb/protoc-bridge/issues/366. |
| 25 | + val ss = new ServerSocket(0) // Bind to any available port. |
| 26 | + val sh = createShellScript(ss.getLocalPort) |
36 | 27 |
|
37 | 28 | Future {
|
38 | 29 | blocking {
|
| 30 | + // Accept a single client connection from the shell script. |
| 31 | + val client = ss.accept() |
| 32 | + |
39 | 33 | try {
|
40 |
| - val fsin = Files.newInputStream(inputPipe) |
41 |
| - val response = PluginFrontend.runWithInputStream(plugin, fsin, env) |
42 |
| - fsin.close() |
| 34 | + val cis = client.getInputStream |
| 35 | + val response = PluginFrontend.runWithInputStream(plugin, cis, env) |
43 | 36 |
|
44 |
| - val fsout = Files.newOutputStream(outputPipe) |
45 |
| - fsout.write(response) |
46 |
| - fsout.close() |
| 37 | + val cos = client.getOutputStream |
| 38 | + cos.write(response) |
47 | 39 | } catch {
|
48 | 40 | case e: Throwable =>
|
49 | 41 | // Handles rare exceptions not already gracefully handled in `runWithBytes`.
|
50 | 42 | // Such exceptions aren't converted to `CodeGeneratorResponse`
|
51 |
| - // because `fsin` might not be fully consumed, |
52 |
| - // therefore the plugin shell script might hang on `inputPipe`, |
53 |
| - // and never consume `CodeGeneratorResponse`. |
| 43 | + // because `cis` might not be fully consumed, |
| 44 | + // therefore the plugin shell script might hang on `nc` write, |
| 45 | + // and never get to `nc` read and consume `CodeGeneratorResponse`. |
| 46 | + // |
| 47 | + // Instead, we simply force close the client connection, |
| 48 | + // so that the plugin shell script can exit. |
54 | 49 | System.err.println("Exception occurred in PluginFrontend outside runWithBytes")
|
55 | 50 | e.printStackTrace(System.err)
|
56 |
| - // Force an exit of the program. |
57 |
| - // This is because the plugin shell script might hang on `inputPipe`, |
58 |
| - // due to `fsin` not fully consumed. |
59 |
| - // Or it might hang on `outputPipe`, due to `fsout` not closed. |
60 |
| - // Therefore, the program might be stuck waiting for protoc, |
61 |
| - // which in turn is waiting for the plugin shell script. |
62 |
| - // |
63 |
| - // We can't simply close `fsout` here either, |
64 |
| - // because `Files.newOutputStream(outputPipe)` will hang |
65 |
| - // if `outputPipe` is not yet opened by the plugin shell script for reading. |
66 |
| - sys.exit(1) |
| 51 | + } finally { |
| 52 | + client.close() |
67 | 53 | }
|
68 | 54 | }
|
69 | 55 | }
|
70 |
| - (sh, InternalState(inputPipe, outputPipe, tempDirPath, sh)) |
| 56 | + (sh, InternalState(ss, sh)) |
71 | 57 | }
|
72 | 58 |
|
73 | 59 | override def cleanup(state: InternalState): Unit = {
|
| 60 | + state.serverSocket.close() |
74 | 61 | if (sys.props.get("protocbridge.debug") != Some("1")) {
|
75 |
| - Files.delete(state.inputPipe) |
76 |
| - Files.delete(state.outputPipe) |
77 |
| - Files.delete(state.tempDir) |
78 | 62 | Files.delete(state.shellScript)
|
79 | 63 | }
|
80 | 64 | }
|
81 | 65 |
|
82 |
| - private def createPipe(tempDirPath: Path, name: String): Path = { |
83 |
| - val pipeName = tempDirPath.resolve(name) |
84 |
| - Seq("mkfifo", "-m", "600", pipeName.toAbsolutePath.toString).!! |
85 |
| - pipeName |
86 |
| - } |
87 |
| - |
88 |
| - private def createShellScript(inputPipe: Path, outputPipe: Path): Path = { |
| 66 | + private def createShellScript(port: Int): Path = { |
89 | 67 | val shell = sys.env.getOrElse("PROTOCBRIDGE_SHELL", "/bin/sh")
|
90 | 68 | val scriptName = PluginFrontend.createTempFile(
|
91 | 69 | "",
|
92 | 70 | s"""|#!$shell
|
93 | 71 | |set -e
|
94 |
| - |cat /dev/stdin > "$inputPipe" |
95 |
| - |cat "$outputPipe" |
| 72 | + |nc localhost $port |
96 | 73 | """.stripMargin
|
97 | 74 | )
|
98 | 75 | val perms = new ju.HashSet[PosixFilePermission]
|
|
0 commit comments