@@ -15,14 +15,25 @@ enum class ResponseState {
15
15
Ok , Error
16
16
}
17
17
18
+ enum class JupyterOutType {
19
+ STDOUT , STDERR ;
20
+ fun optionName () = name.toLowerCase()
21
+ }
22
+
18
23
data class ResponseWithMessage (val state : ResponseState , val result : MimeTypedResult ? , val displays : List <MimeTypedResult > = emptyList(), val stdOut : String? = null , val stdErr : String? = null ) {
19
24
val hasStdOut: Boolean = stdOut != null && stdOut.isNotEmpty()
20
25
val hasStdErr: Boolean = stdErr != null && stdErr.isNotEmpty()
21
26
}
22
27
28
+ fun JupyterConnection.Socket.sendOut (msg : Message , stream : JupyterOutType , text : String ) {
29
+ connection.iopub.send(makeReplyMessage(msg, header = makeHeader(" stream" , msg),
30
+ content = jsonObject(
31
+ " name" to stream.optionName(),
32
+ " text" to text)))
33
+ }
34
+
23
35
fun JupyterConnection.Socket.shellMessagesHandler (msg : Message , repl : ReplForJupyter ? , executionCount : AtomicLong ) {
24
- val msgType = msg.header!! [" msg_type" ]
25
- when (msgType) {
36
+ when (msg.header!! [" msg_type" ]) {
26
37
" kernel_info_request" ->
27
38
sendWrapped(msg, makeReplyMessage(msg, " kernel_info_reply" ,
28
39
content = jsonObject(
@@ -72,18 +83,11 @@ fun JupyterConnection.Socket.shellMessagesHandler(msg: Message, repl: ReplForJup
72
83
}
73
84
}
74
85
75
- fun sendOut (stream : String , text : String ) {
76
- connection.iopub.send(makeReplyMessage(msg, header = makeHeader(" stream" , msg),
77
- content = jsonObject(
78
- " name" to stream,
79
- " text" to text)))
80
- }
81
-
82
86
if (res.hasStdOut) {
83
- sendOut(" stdout " , res.stdOut!! )
87
+ sendOut(msg, JupyterOutType . STDOUT , res.stdOut!! )
84
88
}
85
89
if (res.hasStdErr) {
86
- sendOut(" stderr " , res.stdErr!! )
90
+ sendOut(msg, JupyterOutType . STDERR , res.stdErr!! )
87
91
}
88
92
89
93
when (res.state) {
@@ -166,13 +170,54 @@ fun JupyterConnection.Socket.shellMessagesHandler(msg: Message, repl: ReplForJup
166
170
}
167
171
}
168
172
169
- class CapturingOutputStream (val stdout : PrintStream , val captureOutput : Boolean ) : OutputStream() {
173
+ class CapturingOutputStream (private val stdout : PrintStream ,
174
+ private val maxOutputSize : Int ,
175
+ private val captureOutput : Boolean ,
176
+ private val maxBufferSize : Int ,
177
+ private val maxBufferLifeTimeMs : Int ,
178
+ val onCaptured : (String ) -> Unit ) : OutputStream() {
170
179
val capturedOutput = ByteArrayOutputStream ()
180
+ private var time = System .currentTimeMillis()
181
+ private var overallOutputSize = 0
182
+
183
+ private fun shouldSend (b : Int ): Boolean {
184
+ val c = b.toChar()
185
+ if (c == ' \n ' || c == ' \r ' )
186
+ return true
187
+ if (capturedOutput.size() >= maxBufferSize)
188
+ return true
189
+
190
+ val currentTime = System .currentTimeMillis()
191
+ if (currentTime - time >= maxBufferLifeTimeMs) {
192
+ time = currentTime
193
+ return true
194
+ }
195
+ return false
196
+ }
171
197
172
198
override fun write (b : Int ) {
199
+ if (++ overallOutputSize > maxOutputSize) {
200
+ throw OutputLimitExceededException ()
201
+ }
202
+
173
203
stdout.write(b)
174
- if (captureOutput) capturedOutput.write(b)
204
+
205
+ if (captureOutput) {
206
+ capturedOutput.write(b)
207
+ if (shouldSend(b)) {
208
+ flush()
209
+ }
210
+ }
211
+ }
212
+
213
+ override fun flush () {
214
+ if (capturedOutput.size() > 0 ) {
215
+ onCaptured(capturedOutput.toString(" UTF-8" ))
216
+ capturedOutput.reset()
217
+ }
175
218
}
219
+
220
+ class OutputLimitExceededException (message : String = " Cell output limit exceeded" ): Exception(message)
176
221
}
177
222
178
223
fun Any.toMimeTypedResult (): MimeTypedResult ? = when (this ) {
@@ -186,10 +231,19 @@ fun JupyterConnection.evalWithIO(body: () -> EvalResult?): ResponseWithMessage {
186
231
val out = System .out
187
232
val err = System .err
188
233
189
- // TODO: make configuration option of whether to pipe back stdout and stderr
190
- // TODO: make a configuration option to limit the total stdout / stderr possibly returned (in case it goes wild...)
191
- val forkedOut = CapturingOutputStream (out , true )
192
- val forkedError = CapturingOutputStream (err, false )
234
+ fun getCapturingStream (stream : PrintStream , outType : JupyterOutType ): CapturingOutputStream {
235
+ return CapturingOutputStream (
236
+ stream,
237
+ config.cellOutputMaxSize,
238
+ config.captureOutput,
239
+ config.captureBufferMaxSize,
240
+ config.captureBufferTimeLimitMs) { text ->
241
+ this .iopub.sendOut(contextMessage!! , outType, text)
242
+ }
243
+ }
244
+
245
+ val forkedOut = getCapturingStream(out , JupyterOutType .STDOUT )
246
+ val forkedError = getCapturingStream(err, JupyterOutType .STDERR )
193
247
194
248
System .setOut(PrintStream (forkedOut, true , " UTF-8" ))
195
249
System .setErr(PrintStream (forkedError, true , " UTF-8" ))
@@ -202,26 +256,26 @@ fun JupyterConnection.evalWithIO(body: () -> EvalResult?): ResponseWithMessage {
202
256
if (exec == null ) {
203
257
ResponseWithMessage (ResponseState .Error , textResult(" Error!" ), emptyList(), null , " NO REPL!" )
204
258
} else {
205
- val stdOut = forkedOut.capturedOutput.toString( " UTF-8 " ).emptyWhenNull ()
206
- val stdErr = forkedError.capturedOutput.toString( " UTF-8 " ).emptyWhenNull ()
259
+ forkedOut.flush ()
260
+ forkedError.flush ()
207
261
208
262
try {
209
263
var result: MimeTypedResult ? = null
210
- var displays = exec.displayValues.mapNotNull { it.toMimeTypedResult() }
264
+ val displays = exec.displayValues.mapNotNull { it.toMimeTypedResult() }.toMutableList()
211
265
if (exec.resultValue is DisplayResult ) {
212
266
val resultDisplay = exec.resultValue.value.toMimeTypedResult()
213
267
if (resultDisplay != null )
214
268
displays + = resultDisplay
215
269
} else result = exec.resultValue?.toMimeTypedResult()
216
- ResponseWithMessage (ResponseState .Ok , result, displays, stdOut, stdErr )
270
+ ResponseWithMessage (ResponseState .Ok , result, displays, null , null )
217
271
} catch (e: Exception ) {
218
- ResponseWithMessage (ResponseState .Error , textResult(" Error!" ), emptyList(), stdOut ,
219
- joinLines(stdErr, " error: Unable to convert result to a string: ${e} " ) )
272
+ ResponseWithMessage (ResponseState .Error , textResult(" Error!" ), emptyList(), null ,
273
+ " error: Unable to convert result to a string: $e " )
220
274
}
221
275
}
222
276
} catch (ex: ReplCompilerException ) {
223
- val stdOut = forkedOut.capturedOutput.toString( " UTF-8 " ).emptyWhenNull ()
224
- val stdErr = forkedError.capturedOutput.toString( " UTF-8 " ).emptyWhenNull ()
277
+ forkedOut.flush ()
278
+ forkedError.flush ()
225
279
226
280
// handle runtime vs. compile time and send back correct format of response, now we just send text
227
281
/*
@@ -232,10 +286,10 @@ fun JupyterConnection.evalWithIO(body: () -> EvalResult?): ResponseWithMessage {
232
286
'traceback' : list(str), # traceback frames as strings
233
287
}
234
288
*/
235
- ResponseWithMessage (ResponseState .Error , textResult(" Error!" ), emptyList(), stdOut ,
236
- joinLines(stdErr, ex.errorResult.message) )
289
+ ResponseWithMessage (ResponseState .Error , textResult(" Error!" ), emptyList(), null ,
290
+ ex.errorResult.message)
237
291
} catch (ex: ReplEvalRuntimeException ) {
238
- val stdOut = forkedOut.capturedOutput.toString( " UTF-8 " ).emptyWhenNull ()
292
+ forkedOut.flush ()
239
293
240
294
// handle runtime vs. compile time and send back correct format of response, now we just send text
241
295
/*
@@ -262,7 +316,7 @@ fun JupyterConnection.evalWithIO(body: () -> EvalResult?): ResponseWithMessage {
262
316
}
263
317
}
264
318
}
265
- ResponseWithMessage (ResponseState .Error , textResult(" Error!" ), emptyList(), stdOut , stdErr.toString())
319
+ ResponseWithMessage (ResponseState .Error , textResult(" Error!" ), emptyList(), null , stdErr.toString())
266
320
}
267
321
} finally {
268
322
System .setIn(`in `)
@@ -271,7 +325,4 @@ fun JupyterConnection.evalWithIO(body: () -> EvalResult?): ResponseWithMessage {
271
325
}
272
326
}
273
327
274
- fun joinLines (vararg parts : String ): String = parts.filter(String ::isNotBlank).joinToString(" \n " )
275
328
fun String.nullWhenEmpty (): String? = if (this .isBlank()) null else this
276
- fun String?.emptyWhenNull (): String = if (this == null || this .isBlank()) " " else this
277
-
0 commit comments