@@ -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(
@@ -70,23 +81,16 @@ fun JupyterConnection.Socket.shellMessagesHandler(msg: Message, repl: ReplForJup
70
81
val res: ResponseWithMessage = if (isCommand(code.toString())) {
71
82
runCommand(code.toString(), repl)
72
83
} else {
73
- connection.evalWithIO {
84
+ connection.evalWithIO (repl?.outputConfig) {
74
85
repl?.eval(code.toString(), count.toInt())
75
86
}
76
87
}
77
88
78
- fun sendOut (stream : String , text : String ) {
79
- connection.iopub.send(makeReplyMessage(msg, header = makeHeader(" stream" , msg),
80
- content = jsonObject(
81
- " name" to stream,
82
- " text" to text)))
83
- }
84
-
85
89
if (res.hasStdOut) {
86
- sendOut(" stdout " , res.stdOut!! )
90
+ sendOut(msg, JupyterOutType . STDOUT , res.stdOut!! )
87
91
}
88
92
if (res.hasStdErr) {
89
- sendOut(" stderr " , res.stdErr!! )
93
+ sendOut(msg, JupyterOutType . STDERR , res.stdErr!! )
90
94
}
91
95
92
96
when (res.state) {
@@ -169,12 +173,50 @@ fun JupyterConnection.Socket.shellMessagesHandler(msg: Message, repl: ReplForJup
169
173
}
170
174
}
171
175
172
- class CapturingOutputStream (val stdout : PrintStream , val captureOutput : Boolean ) : OutputStream() {
176
+ class CapturingOutputStream (private val stdout : PrintStream ,
177
+ private val conf : OutputConfig ,
178
+ private val captureOutput : Boolean ,
179
+ val onCaptured : (String ) -> Unit ) : OutputStream() {
173
180
val capturedOutput = ByteArrayOutputStream ()
181
+ private var time = System .currentTimeMillis()
182
+ private var overallOutputSize = 0
183
+ private var newlineFound = false
184
+
185
+ private fun shouldSend (b : Int ): Boolean {
186
+ val c = b.toChar()
187
+ newlineFound = newlineFound || c == ' \n ' || c == ' \r '
188
+ if (newlineFound && capturedOutput.size() >= conf.captureNewlineBufferSize)
189
+ return true
190
+ if (capturedOutput.size() >= conf.captureBufferMaxSize)
191
+ return true
192
+
193
+ val currentTime = System .currentTimeMillis()
194
+ if (currentTime - time >= conf.captureBufferTimeLimitMs) {
195
+ time = currentTime
196
+ return true
197
+ }
198
+ return false
199
+ }
174
200
175
201
override fun write (b : Int ) {
202
+ ++ overallOutputSize
176
203
stdout.write(b)
177
- if (captureOutput) capturedOutput.write(b)
204
+
205
+ if (captureOutput && overallOutputSize <= conf.cellOutputMaxSize) {
206
+ capturedOutput.write(b)
207
+ if (shouldSend(b)) {
208
+ flush()
209
+ }
210
+ }
211
+ }
212
+
213
+ override fun flush () {
214
+ newlineFound = false
215
+ if (capturedOutput.size() > 0 ) {
216
+ val str = capturedOutput.toString(" UTF-8" )
217
+ capturedOutput.reset()
218
+ onCaptured(str)
219
+ }
178
220
}
179
221
}
180
222
@@ -185,17 +227,25 @@ fun Any.toMimeTypedResult(): MimeTypedResult? = when (this) {
185
227
else -> textResult(this .toString())
186
228
}
187
229
188
- fun JupyterConnection.evalWithIO (body : () -> EvalResult ? ): ResponseWithMessage {
230
+ fun JupyterConnection.evalWithIO (maybeConfig : OutputConfig ? , body : () -> EvalResult ? ): ResponseWithMessage {
189
231
val out = System .out
190
232
val err = System .err
233
+ val config = maybeConfig ? : OutputConfig ()
191
234
192
- // TODO: make configuration option of whether to pipe back stdout and stderr
193
- // TODO: make a configuration option to limit the total stdout / stderr possibly returned (in case it goes wild...)
194
- val forkedOut = CapturingOutputStream (out , true )
195
- val forkedError = CapturingOutputStream (err, false )
235
+ fun getCapturingStream (stream : PrintStream , outType : JupyterOutType , captureOutput : Boolean ): CapturingOutputStream {
236
+ return CapturingOutputStream (
237
+ stream,
238
+ config,
239
+ captureOutput) { text ->
240
+ this .iopub.sendOut(contextMessage!! , outType, text)
241
+ }
242
+ }
196
243
197
- System .setOut(PrintStream (forkedOut, true , " UTF-8" ))
198
- System .setErr(PrintStream (forkedError, true , " UTF-8" ))
244
+ val forkedOut = getCapturingStream(out , JupyterOutType .STDOUT , config.captureOutput)
245
+ val forkedError = getCapturingStream(err, JupyterOutType .STDERR , false )
246
+
247
+ System .setOut(PrintStream (forkedOut, false , " UTF-8" ))
248
+ System .setErr(PrintStream (forkedError, false , " UTF-8" ))
199
249
200
250
val `in ` = System .`in `
201
251
System .setIn(stdinIn)
@@ -205,26 +255,26 @@ fun JupyterConnection.evalWithIO(body: () -> EvalResult?): ResponseWithMessage {
205
255
if (exec == null ) {
206
256
ResponseWithMessage (ResponseState .Error , textResult(" Error!" ), emptyList(), null , " NO REPL!" )
207
257
} else {
208
- val stdOut = forkedOut.capturedOutput.toString( " UTF-8 " ).emptyWhenNull ()
209
- val stdErr = forkedError.capturedOutput.toString( " UTF-8 " ).emptyWhenNull ()
258
+ forkedOut.flush ()
259
+ forkedError.flush ()
210
260
211
261
try {
212
262
var result: MimeTypedResult ? = null
213
- var displays = exec.displayValues.mapNotNull { it.toMimeTypedResult() }
263
+ val displays = exec.displayValues.mapNotNull { it.toMimeTypedResult() }.toMutableList()
214
264
if (exec.resultValue is DisplayResult ) {
215
265
val resultDisplay = exec.resultValue.value.toMimeTypedResult()
216
266
if (resultDisplay != null )
217
267
displays + = resultDisplay
218
268
} else result = exec.resultValue?.toMimeTypedResult()
219
- ResponseWithMessage (ResponseState .Ok , result, displays, stdOut, stdErr )
269
+ ResponseWithMessage (ResponseState .Ok , result, displays, null , null )
220
270
} catch (e: Exception ) {
221
- ResponseWithMessage (ResponseState .Error , textResult(" Error!" ), emptyList(), stdOut ,
222
- joinLines(stdErr, " error: Unable to convert result to a string: ${e} " ) )
271
+ ResponseWithMessage (ResponseState .Error , textResult(" Error!" ), emptyList(), null ,
272
+ " error: Unable to convert result to a string: $e " )
223
273
}
224
274
}
225
275
} catch (ex: ReplCompilerException ) {
226
- val stdOut = forkedOut.capturedOutput.toString( " UTF-8 " ).emptyWhenNull ()
227
- val stdErr = forkedError.capturedOutput.toString( " UTF-8 " ).emptyWhenNull ()
276
+ forkedOut.flush ()
277
+ forkedError.flush ()
228
278
229
279
// handle runtime vs. compile time and send back correct format of response, now we just send text
230
280
/*
@@ -235,10 +285,10 @@ fun JupyterConnection.evalWithIO(body: () -> EvalResult?): ResponseWithMessage {
235
285
'traceback' : list(str), # traceback frames as strings
236
286
}
237
287
*/
238
- ResponseWithMessage (ResponseState .Error , textResult(" Error!" ), emptyList(), stdOut ,
239
- joinLines(stdErr, ex.errorResult.message) )
288
+ ResponseWithMessage (ResponseState .Error , textResult(" Error!" ), emptyList(), null ,
289
+ ex.errorResult.message)
240
290
} catch (ex: ReplEvalRuntimeException ) {
241
- val stdOut = forkedOut.capturedOutput.toString( " UTF-8 " ).emptyWhenNull ()
291
+ forkedOut.flush ()
242
292
243
293
// handle runtime vs. compile time and send back correct format of response, now we just send text
244
294
/*
@@ -265,7 +315,7 @@ fun JupyterConnection.evalWithIO(body: () -> EvalResult?): ResponseWithMessage {
265
315
}
266
316
}
267
317
}
268
- ResponseWithMessage (ResponseState .Error , textResult(" Error!" ), emptyList(), stdOut , stdErr.toString())
318
+ ResponseWithMessage (ResponseState .Error , textResult(" Error!" ), emptyList(), null , stdErr.toString())
269
319
}
270
320
} finally {
271
321
System .setIn(`in `)
@@ -274,7 +324,4 @@ fun JupyterConnection.evalWithIO(body: () -> EvalResult?): ResponseWithMessage {
274
324
}
275
325
}
276
326
277
- fun joinLines (vararg parts : String ): String = parts.filter(String ::isNotBlank).joinToString(" \n " )
278
327
fun String.nullWhenEmpty (): String? = if (this .isBlank()) null else this
279
- fun String?.emptyWhenNull (): String = if (this == null || this .isBlank()) " " else this
280
-
0 commit comments