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