github.com/yunabe/lgo@v0.0.0-20190709125917-42c42d410fdf/jupyter/gojupyterscaffold/gojupyterscaffold.go (about)

     1  // Package gojupyterscaffold provides a scaffold of Jupyter kernel implemented by Go.
     2  //
     3  // References:
     4  // https://github.com/ipython/ipykernel/blob/master/ipykernel/kernelbase.py
     5  // https://github.com/jupyter/jupyter_client/blob/master/jupyter_client/session.py
     6  //
     7  // Misc:
     8  // ZMQ pubsub with inproc is broken (https://github.com/JustinTulloss/zeromq.node/issues/22) though it's not used in this code now.
     9  package gojupyterscaffold
    10  
    11  import (
    12  	"context"
    13  	"encoding/json"
    14  	"fmt"
    15  	"io/ioutil"
    16  	"os"
    17  	"os/signal"
    18  	"syscall"
    19  
    20  	zmq "github.com/pebbe/zmq4"
    21  )
    22  
    23  // ConnectionInfo stores the contents of the kernel connection file created by Jupyter.
    24  type connectionInfo struct {
    25  	StdinPort       int    `json:"stdin_port"`
    26  	IP              string `json:"ip"`
    27  	ControlPort     int    `json:"control_port"`
    28  	HBPort          int    `json:"hb_port"`
    29  	SignatureScheme string `json:"signature_scheme"`
    30  	Key             string `json:"key"`
    31  	KernelName      string `json:"kernel_name"`
    32  	ShellPort       int    `json:"shell_port"`
    33  	Transport       string `json:"transport"`
    34  	IOPubPort       int    `json:"iopub_port"`
    35  }
    36  
    37  func readConnectionInfo(connectionFile string) (*connectionInfo, error) {
    38  	b, err := ioutil.ReadFile(connectionFile)
    39  	if err != nil {
    40  		return nil, fmt.Errorf("Failed to read %s: %v", connectionFile, err)
    41  	}
    42  	logger.Infof("Connection info JSON: %v", string(b))
    43  	var cinfo connectionInfo
    44  	if err = json.Unmarshal(b, &cinfo); err != nil {
    45  		return nil, fmt.Errorf("Failed to parse %s: %v", connectionFile, err)
    46  	}
    47  	logger.Infof("Connection info: %+v", cinfo)
    48  	return &cinfo, nil
    49  }
    50  
    51  func (ci *connectionInfo) getAddr(port int) string {
    52  	return fmt.Sprintf("%s://%s:%d", ci.Transport, ci.IP, port)
    53  }
    54  
    55  // A Server is a jupyter kernel server that that handles user commands forwarded from
    56  // Jupyter frontend servers.
    57  type Server struct {
    58  	handlers RequestHandlers
    59  
    60  	// ctx of this server and a func to cancel it.
    61  	ctx       context.Context
    62  	cancelCtx func()
    63  
    64  	// ZMQ sockets
    65  	shell   *shellSocket
    66  	control *shellSocket
    67  	iopub   *iopubSocket
    68  	stdin   *zmq.Socket
    69  	hb      *zmq.Socket
    70  
    71  	// Attribute
    72  	connInfo *connectionInfo
    73  
    74  	execQueue *executeQueue
    75  }
    76  
    77  // NewServer returns a new jupyter kernel server.
    78  func NewServer(bgCtx context.Context, connectionFile string, handlers RequestHandlers) (server *Server, err error) {
    79  	serverCtx, cancelCtx := context.WithCancel(bgCtx)
    80  	defer func() {
    81  		// Avoid ctx leak
    82  		// https://golang.org/pkg/context/
    83  		if server == nil {
    84  			cancelCtx()
    85  		}
    86  	}()
    87  	cinfo, err := readConnectionInfo(connectionFile)
    88  	if err != nil {
    89  		return nil, err
    90  	}
    91  	ctx, err := zmq.NewContext()
    92  	if err != nil {
    93  		return nil, err
    94  	}
    95  
    96  	iopub, err := newIOPubSocket(serverCtx, ctx, cinfo)
    97  	if err != nil {
    98  		return nil, fmt.Errorf("Failed to create iopub socket: %v", err)
    99  	}
   100  
   101  	execQueue := newExecuteQueue(serverCtx, iopub, handlers)
   102  	shell, err := newShellSocket(serverCtx, ctx, "shell", cinfo, iopub, handlers, cancelCtx, execQueue)
   103  	if err != nil {
   104  		return nil, fmt.Errorf("Failed to create shell socket: %v", err)
   105  	}
   106  	control, err := newShellSocket(serverCtx, ctx, "control", cinfo, iopub, handlers, cancelCtx, execQueue)
   107  	if err != nil {
   108  		return nil, fmt.Errorf("Failed to create control socket: %v", err)
   109  	}
   110  
   111  	stdin, err := ctx.NewSocket(zmq.ROUTER)
   112  	if err != nil {
   113  		return nil, fmt.Errorf("Failed to open stdin socket: %v", err)
   114  	}
   115  	if err := stdin.Bind(cinfo.getAddr(cinfo.StdinPort)); err != nil {
   116  		return nil, fmt.Errorf("Failed to bind shell socket: %v", err)
   117  	}
   118  	// Ref: Python version of HeartBeat
   119  	// https://github.com/ipython/ipykernel/blob/master/ipykernel/heartbeat.py
   120  	hb, err := ctx.NewSocket(zmq.REP)
   121  	if err != nil {
   122  		return nil, fmt.Errorf("Failed to open heartbeat socket: %v", err)
   123  	}
   124  	if err := hb.Bind(cinfo.getAddr(cinfo.HBPort)); err != nil {
   125  		return nil, fmt.Errorf("Failed to bind heartbeat socket: %v", err)
   126  	}
   127  	return &Server{
   128  		handlers:  handlers,
   129  		ctx:       serverCtx,
   130  		cancelCtx: cancelCtx,
   131  		shell:     shell,
   132  		control:   control,
   133  		stdin:     stdin,
   134  		iopub:     iopub,
   135  		hb:        hb,
   136  		connInfo:  cinfo,
   137  		execQueue: execQueue,
   138  	}, nil
   139  }
   140  
   141  // Context returns the context of the server
   142  func (s *Server) Context() context.Context {
   143  	return s.ctx
   144  }
   145  
   146  func (s *Server) monitorSigint() {
   147  	ch := make(chan os.Signal, 1)
   148  	signal.Notify(ch, syscall.SIGINT)
   149  	go func() {
   150  		for range ch {
   151  			logger.Info("Received SIGINT. Cancelling an ongoing execute_request")
   152  			s.execQueue.cancelCurrent()
   153  		}
   154  	}()
   155  }
   156  
   157  func (s *Server) monitorTerminationSignals() {
   158  	ch := make(chan os.Signal, 1)
   159  	signal.Notify(ch, syscall.SIGTERM, syscall.SIGQUIT, syscall.SIGHUP)
   160  	go func() {
   161  		for sig := range ch {
   162  			logger.Infof("Received a signal (%s), terminating the kernel.", sig)
   163  			s.cancelCtx()
   164  		}
   165  	}()
   166  }
   167  
   168  func isEINTR(err error) bool {
   169  	if err == nil {
   170  		return false
   171  	}
   172  	errno, ok := err.(syscall.Errno)
   173  	return ok && errno == syscall.EINTR
   174  }
   175  
   176  // Loop starts the server main loop
   177  func (s *Server) Loop() {
   178  	go func() {
   179  		logger.Info("Forwarding heartbeat requests")
   180  		if err := zmq.Proxy(s.hb, s.hb, nil); err != nil {
   181  			logger.Fatalf("Failed to echo heartbeat request: %v", err)
   182  		}
   183  		logger.Info("Quitting goroutine for heartbeat requests")
   184  	}()
   185  	s.monitorSigint()
   186  	s.monitorTerminationSignals()
   187  
   188  	execDone := make(chan struct{})
   189  	sockDone := make(chan struct{})
   190  	go func() {
   191  		s.execQueue.loop()
   192  		close(execDone)
   193  	}()
   194  	go func() {
   195  		s.shell.loop()
   196  		sockDone <- struct{}{}
   197  	}()
   198  	go func() {
   199  		s.control.loop()
   200  		sockDone <- struct{}{}
   201  	}()
   202  	<-execDone
   203  
   204  	if err := s.shell.notifyLoopEnd(); err != nil {
   205  		logger.Errorf("Failed to notify the loop end to shell socket: %v", err)
   206  	}
   207  	if err := s.control.notifyLoopEnd(); err != nil {
   208  		logger.Errorf("Failed to notify the loop end to control socket: %v", err)
   209  	}
   210  	// Wait loop ends
   211  	<-sockDone
   212  	<-sockDone
   213  
   214  	if err := s.iopub.close(); err != nil {
   215  		logger.Errorf("Failed to close iopub socket: %v", err)
   216  	}
   217  	if err := s.shell.close(); err != nil {
   218  		logger.Errorf("Failed to close shell socket: %v", err)
   219  	}
   220  	if err := s.control.close(); err != nil {
   221  		logger.Errorf("Failed to close control socket: %v", err)
   222  	}
   223  
   224  	// TODO: Support stdin.
   225  }