diff --git a/kernel.go b/kernel.go index 0751c68..5fe6de1 100644 --- a/kernel.go +++ b/kernel.go @@ -9,6 +9,7 @@ import ( "io/ioutil" "log" "os" + "runtime" "strings" "github.com/cosmos72/gomacro/base" @@ -43,23 +44,44 @@ type SocketGroup struct { Key []byte } -// kernelInfo holds information about the igo kernel, for -// kernel_info_reply messages. -type kernelInfo struct { - ProtocolVersion []int `json:"protocol_version"` - Language string `json:"language"` +// KernelLanguageInfo holds information about the language that this kernel executes code in. +type kernelLanguageInfo struct { + Name string `json:"name"` + Version string `json:"version"` + MIMEType string `json:"mimetype"` + FileExtension string `json:"file_extension"` + PygmentsLexer string `json:"pygments_lexer"` + CodeMirrorMode string `json:"codemirror_mode"` + NBConvertExporter string `json:"nbconvert_exporter"` +} + +// HelpLink stores data to be displayed in the help menu of the notebook. +type helpLink struct { + Text string `json:"text"` + URL string `json:"url"` } -// kernelStatus holds a kernel state, for status broadcast messages. -type kernelStatus struct { - ExecutionState string `json:"execution_state"` +// KernelInfo holds information about the igo kernel, for kernel_info_reply messages. +type kernelInfo struct { + ProtocolVersion string `json:"protocol_version"` + Implementation string `json:"implementation"` + ImplementationVersion string `json:"implementation_version"` + LanguageInfo kernelLanguageInfo `json:"language_info"` + Banner string `json:"banner"` + HelpLinks []helpLink `json:"help_links"` } -// shutdownReply encodes a boolean indication of stutdown/restart +// shutdownReply encodes a boolean indication of stutdown/restart. type shutdownReply struct { Restart bool `json:"restart"` } +const ( + kernelStarting = "starting" + kernelBusy = "busy" + kernelIdle = "idle" +) + // runKernel is the main entry point to start the kernel. func runKernel(connectionFile string) { @@ -120,11 +142,11 @@ func runKernel(connectionFile string) { handleShellMsg(ir, msgReceipt{msg, ids, sockets}) - // TODO Handle stdin socket. + // TODO Handle stdin socket. case sockets.StdinSocket: sockets.StdinSocket.RecvMessageBytes(0) - // Handle control messages. + // Handle control messages. case sockets.ControlSocket: msgParts, err = sockets.ControlSocket.RecvMessageBytes(0) if err != nil { @@ -210,31 +232,29 @@ func handleShellMsg(ir *classic.Interp, receipt msgReceipt) { // sendKernelInfo sends a kernel_info_reply message. func sendKernelInfo(receipt msgReceipt) error { - reply, err := NewMsg("kernel_info_reply", receipt.Msg) - if err != nil { - return err - } - - reply.Content = kernelInfo{[]int{4, 0}, "go"} - if err := receipt.SendResponse(receipt.Sockets.ShellSocket, reply); err != nil { - return err - } - - return nil + return receipt.Reply("kernel_info_reply", + kernelInfo{ + ProtocolVersion: ProtocolVersion, + Implementation: "gophernotes", + ImplementationVersion: Version, + Banner: fmt.Sprintf("Go kernel: gophernotes - v%s", Version), + LanguageInfo: kernelLanguageInfo{ + Name: "go", + Version: runtime.Version(), + FileExtension: ".go", + }, + HelpLinks: []helpLink{ + {Text: "Go", URL: "https://golang.org/"}, + {Text: "gophernotes", URL: "https://github.com/gopherdata/gophernotes"}, + }, + }, + ) } // handleExecuteRequest runs code from an execute_request method, // and sends the various reply messages. func handleExecuteRequest(ir *classic.Interp, receipt msgReceipt) error { - - // Prepare the reply message. - reply, err := NewMsg("execute_reply", receipt.Msg) - if err != nil { - return err - } - - content := make(map[string]interface{}) - + // Extract the data from the request reqcontent := receipt.Msg.Content.(map[string]interface{}) code := reqcontent["code"].(string) in := bufio.NewReader(strings.NewReader(code)) @@ -244,8 +264,26 @@ func handleExecuteRequest(ir *classic.Interp, receipt msgReceipt) error { ExecCounter++ } + // Prepare the map that will hold the reply content. + content := make(map[string]interface{}) content["execution_count"] = ExecCounter + // Tell the front-end that the kernel is working and when finished notify the + // front-end that the kernel is idle again. + if err := receipt.PublishKernelStatus(kernelBusy); err != nil { + log.Printf("Error publishing kernel status 'busy': %v\n", err) + } + defer func() { + if err := receipt.PublishKernelStatus(kernelIdle); err != nil { + log.Printf("Error publishing kernel status 'idle': %v\n", err) + } + }() + + // Tell the front-end what the kernel is about to execute. + if err := receipt.PublishExecutionInput(ExecCounter, code); err != nil { + log.Printf("Error publishing execution input: %v\n", err) + } + // Redirect the standard out from the REPL. oldStdout := os.Stdout rOut, wOut, err := os.Pipe() @@ -266,7 +304,7 @@ func handleExecuteRequest(ir *classic.Interp, receipt msgReceipt) error { env.Options &^= base.OptShowPrompt env.Line = 0 - // Perform the first iteration manually, to collect comments + // Perform the first iteration manually, to collect comments. var comments string str, firstToken := env.ReadMultiline(in, base.ReadOptCollectAllComments) if firstToken >= 0 { @@ -306,25 +344,17 @@ func handleExecuteRequest(ir *classic.Interp, receipt msgReceipt) error { wErr.Close() stdErr := <-outStderr + // TODO write stdout and stderr to streams rather than publishing as results + if len(val) > 0 { content["status"] = "ok" - content["payload"] = make([]map[string]interface{}, 0) - content["user_variables"] = make(map[string]string) content["user_expressions"] = make(map[string]string) - if !silent { - var outContent OutputMsg - out, err := NewMsg("execute_result", receipt.Msg) - if err != nil { - return err + if !silent { + // Publish the result of the execution. + if err := receipt.PublishExecutionResult(ExecCounter, val); err != nil { + log.Printf("Error publishing execution result: %v\n", err) } - - outContent.Execcount = ExecCounter - outContent.Data = make(map[string]string) - outContent.Data["text/plain"] = val - outContent.Metadata = make(map[string]interface{}) - out.Content = outContent - receipt.SendResponse(receipt.Sockets.IOPubSocket, out) } } @@ -334,48 +364,25 @@ func handleExecuteRequest(ir *classic.Interp, receipt msgReceipt) error { content["evalue"] = stdErr content["traceback"] = nil - errormsg, err := NewMsg("pyerr", receipt.Msg) - if err != nil { - return err + if err := receipt.PublishExecutionError(stdErr, []string{stdErr}); err != nil { + log.Printf("Error publishing execution error: %v\n", err) } - - errormsg.Content = ErrMsg{"Error", stdErr, []string{stdErr}} - receipt.SendResponse(receipt.Sockets.IOPubSocket, errormsg) } // Send the output back to the notebook. - reply.Content = content - - if err := receipt.SendResponse(receipt.Sockets.ShellSocket, reply); err != nil { - return err - } - - idle, err := NewMsg("status", receipt.Msg) - if err != nil { - return err - } - - idle.Content = kernelStatus{"idle"} - - if err := receipt.SendResponse(receipt.Sockets.IOPubSocket, idle); err != nil { - return err - } - - return nil + return receipt.Reply("execute_reply", content) } -// handleShutdownRequest sends a "shutdown" message +// handleShutdownRequest sends a "shutdown" message. func handleShutdownRequest(receipt msgReceipt) { - reply, err := NewMsg("shutdown_reply", receipt.Msg) - if err != nil { - log.Fatal(err) - } - content := receipt.Msg.Content.(map[string]interface{}) restart := content["restart"].(bool) - reply.Content = shutdownReply{restart} - if err := receipt.SendResponse(receipt.Sockets.ShellSocket, reply); err != nil { + reply := shutdownReply{ + Restart: restart, + } + + if err := receipt.Reply("shutdown_reply", reply); err != nil { log.Fatal(err) } diff --git a/main.go b/main.go index 8606328..9ae36e2 100644 --- a/main.go +++ b/main.go @@ -5,6 +5,9 @@ import ( "log" ) +const Version string = "1.0.0" +const ProtocolVersion string = "5.0" + func main() { // Parse the connection file. diff --git a/messages.go b/messages.go index 783519e..49cbd68 100644 --- a/messages.go +++ b/messages.go @@ -5,17 +5,20 @@ import ( "crypto/sha256" "encoding/hex" "encoding/json" + "time" - uuid "github.com/nu7hatch/gouuid" + "github.com/nu7hatch/gouuid" zmq "github.com/pebbe/zmq4" ) // MsgHeader encodes header info for ZMQ messages. type MsgHeader struct { - MsgID string `json:"msg_id"` - Username string `json:"username"` - Session string `json:"session"` - MsgType string `json:"msg_type"` + MsgID string `json:"msg_id"` + Username string `json:"username"` + Session string `json:"session"` + MsgType string `json:"msg_type"` + ProtocolVersion string `json:"version"` + Timestamp string `json:"date"` } // ComposedMsg represents an entire message in a high-level structure. @@ -34,19 +37,10 @@ type msgReceipt struct { Sockets SocketGroup } -// OutputMsg holds the data for a pyout message. -type OutputMsg struct { - Execcount int `json:"execution_count"` - Data map[string]string `json:"data"` - Metadata map[string]interface{} `json:"metadata"` -} - -// ErrMsg encodes the traceback of errors output to the notebook. -type ErrMsg struct { - EName string `json:"ename"` - EValue string `json:"evalue"` - Traceback []string `json:"traceback"` -} +// bundledMIMEData holds data that can be presented in multiple formats. The keys are MIME types +// and the values are the data formatted with respect to it's MIME type. All bundles should contain +// at least a "text/plain" representation with a string value. +type bundledMIMEData map[string]interface{} // InvalidSignatureError is returned when the signature on a received message does not // validate. @@ -173,6 +167,8 @@ func NewMsg(msgType string, parent ComposedMsg) (ComposedMsg, error) { msg.Header.Session = parent.Header.Session msg.Header.Username = parent.Header.Username msg.Header.MsgType = msgType + msg.Header.ProtocolVersion = ProtocolVersion + msg.Header.Timestamp = time.Now().UTC().Format(time.RFC3339) u, err := uuid.NewV4() if err != nil { @@ -182,3 +178,93 @@ func NewMsg(msgType string, parent ComposedMsg) (ComposedMsg, error) { return msg, nil } + +// Publish creates a new ComposedMsg and sends it back to the return identities over the +// IOPub channel. +func (receipt *msgReceipt) Publish(msgType string, content interface{}) error { + msg, err := NewMsg(msgType, receipt.Msg) + + if err != nil { + return err + } + + msg.Content = content + return receipt.SendResponse(receipt.Sockets.IOPubSocket, msg) +} + +// Reply creates a new ComposedMsg and sends it back to the return identities over the +// Shell channel. +func (receipt *msgReceipt) Reply(msgType string, content interface{}) error { + msg, err := NewMsg(msgType, receipt.Msg) + + if err != nil { + return err + } + + msg.Content = content + return receipt.SendResponse(receipt.Sockets.ShellSocket, msg) +} + +// newTextMIMEDataBundle creates a bundledMIMEData that only contains a text representation described +// by the value parameter. +func newTextBundledMIMEData(value string) bundledMIMEData { + return bundledMIMEData{ + "text/plain": value, + } +} + +// PublishKernelStatus publishes a status message notifying front-ends of the state the kernel is in. Supports +// states "starting", "busy", and "idle". +func (receipt *msgReceipt) PublishKernelStatus(status string) error { + return receipt.Publish("status", + struct { + ExecutionState string `json:"execution_state"` + }{ + ExecutionState: status, + }, + ) +} + +// PublishExecutionInput publishes a status message notifying front-ends of what code is +// currently being executed. +func (receipt *msgReceipt) PublishExecutionInput(execCount int, code string) error { + return receipt.Publish("execute_input", + struct { + ExecCount int `json:"execution_count"` + Code string `json:"code"` + }{ + ExecCount: execCount, + Code: code, + }, + ) +} + +// PublishExecuteResult publishes the result of the `execCount` execution as a string. +func (receipt *msgReceipt) PublishExecutionResult(execCount int, output string) error { + return receipt.Publish("execute_result", + struct { + ExecCount int `json:"execution_count"` + Data bundledMIMEData `json:"data"` + Metadata bundledMIMEData `json:"metadata"` + }{ + ExecCount: execCount, + Data: newTextBundledMIMEData(output), + Metadata: make(bundledMIMEData), + }, + ) +} + +// PublishExecuteResult publishes a serialized error that was encountered during execution. +func (receipt *msgReceipt) PublishExecutionError(err string, trace []string) error { + return receipt.Publish("error", + struct { + Name string `json:"ename"` + Value string `json:"evalue"` + Trace []string `json:"traceback"` + }{ + Name: "ERROR", + Value: err, + Trace: trace, + }, + ) +}