@@ -4,18 +4,23 @@ import jsonrpclib.CallId
4
4
import jsonrpclib .fs2 ._
5
5
import cats .effect ._
6
6
import fs2 .io ._
7
- import eu .monniot .process .Process
8
7
import com .github .plokhotnyuk .jsoniter_scala .core .JsonValueCodec
9
8
import com .github .plokhotnyuk .jsoniter_scala .macros .JsonCodecMaker
10
9
import jsonrpclib .Endpoint
11
10
import cats .syntax .all ._
12
11
import fs2 .Stream
13
12
import jsonrpclib .StubTemplate
13
+ import cats .effect .std .Dispatcher
14
+ import scala .sys .process .ProcessIO
15
+ import cats .effect .implicits ._
16
+ import scala .sys .process .{Process => SProcess }
17
+ import java .io .OutputStream
18
+ import java .io .InputStream
14
19
15
20
object ClientMain extends IOApp .Simple {
16
21
17
22
// Reserving a method for cancelation.
18
- val cancelTemplate = CancelTemplate .make[CallId ](" $/cancel" , identity, identity)
23
+ val cancelEndpoint = CancelTemplate .make[CallId ](" $/cancel" , identity, identity)
19
24
20
25
// Creating a datatype that'll serve as a request (and response) of an endpoint
21
26
case class IntWrapper (value : Int )
@@ -27,21 +32,75 @@ object ClientMain extends IOApp.Simple {
27
32
def log (str : String ): IOStream [Unit ] = Stream .eval(IO .consoleForIO.errorln(str))
28
33
29
34
def run : IO [Unit ] = {
35
+ import scala .concurrent .duration ._
30
36
// Using errorln as stdout is used by the RPC channel
31
37
val run = for {
32
38
_ <- log(" Starting client" )
33
39
serverJar <- sys.env.get(" SERVER_JAR" ).liftTo[IOStream ](new Exception (" SERVER_JAR env var does not exist" ))
34
- serverProcess <- Stream .resource(Process .spawn[IO ](" java" , " -jar" , serverJar))
35
- fs2Channel <- FS2Channel .lspCompliant[IO ](serverProcess.stdout, serverProcess.stdin)
40
+ // Starting the server
41
+ (serverStdin, serverStdout, serverStderr) <- Stream .resource(process(" java" , " -jar" , serverJar))
42
+ pipeErrors = serverStderr.through(fs2.io.stderr)
43
+ // Creating a channel that will be used to communicate to the server
44
+ fs2Channel <- FS2Channel
45
+ .lspCompliant[IO ](serverStdout, serverStdin, cancelTemplate = cancelEndpoint.some)
46
+ .concurrently(pipeErrors)
36
47
// Opening the stream to be able to send and receive data
37
48
_ <- fs2Channel.openStream
38
49
// Creating a `IntWrapper => IO[IntWrapper]` stub that can call the server
39
50
increment = fs2Channel.simpleStub[IntWrapper , IntWrapper ](" increment" )
40
- result <- Stream .eval(IO .pure (IntWrapper (0 )).flatMap(increment ))
51
+ result <- Stream .eval(increment (IntWrapper (0 )))
41
52
_ <- log(s " Client received $result" )
42
53
_ <- log(" Terminating client" )
43
54
} yield ()
44
- run.compile.drain
55
+ run.compile.drain.timeout( 2 .second)
45
56
}
46
57
58
+ /** Wraps the spawning of a subprocess into fs2 friendly semantics
59
+ */
60
+ import scala .concurrent .duration ._
61
+ def process (command : String * ) = for {
62
+ dispatcher <- Dispatcher [IO ]
63
+ stdinPromise <- IO .deferred[fs2.Pipe [IO , Byte , Unit ]].toResource
64
+ stdoutPromise <- IO .deferred[fs2.Stream [IO , Byte ]].toResource
65
+ stderrPromise <- IO .deferred[fs2.Stream [IO , Byte ]].toResource
66
+ makeProcessBuilder = IO (sys.process.stringSeqToProcess(command))
67
+ makeProcessIO = IO (
68
+ new ProcessIO (
69
+ in = { (outputStream : OutputStream ) =>
70
+ val pipe = writeOutputStreamFlushingChunks(IO (outputStream))
71
+ val fulfil = stdinPromise.complete(pipe)
72
+ dispatcher.unsafeRunSync(fulfil)
73
+ },
74
+ out = { (inputStream : InputStream ) =>
75
+ val stream = fs2.io.readInputStream(IO (inputStream), 512 )
76
+ val fulfil = stdoutPromise.complete(stream)
77
+ dispatcher.unsafeRunSync(fulfil)
78
+ },
79
+ err = { (inputStream : InputStream ) =>
80
+ val stream = fs2.io.readInputStream(IO (inputStream), 512 )
81
+ val fulfil = stderrPromise.complete(stream)
82
+ dispatcher.unsafeRunSync(fulfil)
83
+ }
84
+ )
85
+ )
86
+ makeProcess = (makeProcessBuilder, makeProcessIO).flatMapN { case (b, io) => IO .blocking(b.run(io)) }
87
+ _ <- Resource .make(makeProcess)((runningProcess) => IO .blocking(runningProcess.destroy()))
88
+ pipes <- (stdinPromise.get, stdoutPromise.get, stderrPromise.get).tupled.toResource
89
+ } yield pipes
90
+
91
+ /** Adds a flush after each chunk
92
+ */
93
+ def writeOutputStreamFlushingChunks [F [_]](
94
+ fos : F [OutputStream ],
95
+ closeAfterUse : Boolean = true
96
+ )(implicit F : Sync [F ]): fs2.Pipe [F , Byte , Nothing ] =
97
+ s => {
98
+ def useOs (os : OutputStream ): Stream [F , Nothing ] =
99
+ s.chunks.foreach(c => F .interruptible(os.write(c.toArray)) >> F .blocking(os.flush()))
100
+
101
+ val os =
102
+ if (closeAfterUse) Stream .bracket(fos)(os => F .blocking(os.close()))
103
+ else Stream .eval(fos)
104
+ os.flatMap(os => useOs(os) ++ Stream .exec(F .blocking(os.flush())))
105
+ }
47
106
}
0 commit comments