@@ -4,18 +4,23 @@ import jsonrpclib.CallId
44import jsonrpclib .fs2 ._
55import cats .effect ._
66import fs2 .io ._
7- import eu .monniot .process .Process
87import com .github .plokhotnyuk .jsoniter_scala .core .JsonValueCodec
98import com .github .plokhotnyuk .jsoniter_scala .macros .JsonCodecMaker
109import jsonrpclib .Endpoint
1110import cats .syntax .all ._
1211import fs2 .Stream
1312import 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
1419
1520object ClientMain extends IOApp .Simple {
1621
1722 // Reserving a method for cancelation.
18- val cancelTemplate = CancelTemplate .make[CallId ](" $/cancel" , identity, identity)
23+ val cancelEndpoint = CancelTemplate .make[CallId ](" $/cancel" , identity, identity)
1924
2025 // Creating a datatype that'll serve as a request (and response) of an endpoint
2126 case class IntWrapper (value : Int )
@@ -27,21 +32,75 @@ object ClientMain extends IOApp.Simple {
2732 def log (str : String ): IOStream [Unit ] = Stream .eval(IO .consoleForIO.errorln(str))
2833
2934 def run : IO [Unit ] = {
35+ import scala .concurrent .duration ._
3036 // Using errorln as stdout is used by the RPC channel
3137 val run = for {
3238 _ <- log(" Starting client" )
3339 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)
3647 // Opening the stream to be able to send and receive data
3748 _ <- fs2Channel.openStream
3849 // Creating a `IntWrapper => IO[IntWrapper]` stub that can call the server
3950 increment = fs2Channel.simpleStub[IntWrapper , IntWrapper ](" increment" )
40- result <- Stream .eval(IO .pure (IntWrapper (0 )).flatMap(increment ))
51+ result <- Stream .eval(increment (IntWrapper (0 )))
4152 _ <- log(s " Client received $result" )
4253 _ <- log(" Terminating client" )
4354 } yield ()
44- run.compile.drain
55+ run.compile.drain.timeout( 2 .second)
4556 }
4657
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+ }
47106}
0 commit comments