plugin.go 源码阅读
package pingo
import (
"bufio"
"errors"
"fmt"
"io"
"log"
"net"
"net/rpc"
"os"
"os/exec"
"strings"
"time"
)
var (
errInvalidMessage = ErrInvalidMessage(errors.New("Invalid ready message"))
errRegistrationTimeout = ErrRegistrationTimeout(errors.New("Registration timed out"))
)
// Represents a plugin. After being created the plugin is not started or ready to run.
//
// Additional configuration (ErrorHandler and Timeout) can be set after initialization.
//
// Use Start() to make the plugin available.
type Plugin struct {
exe string
proto string
unixdir string
params []string
initTimeout time.Duration
exitTimeout time.Duration
handler ErrorHandler
running bool
meta meta
objsCh chan *objects
connCh chan *conn
killCh chan *waiter
exitCh chan struct{}
}
// NewPlugin create a new plugin ready to be started, or returns an error if the initial setup fails.
//
// The first argument specifies the protocol. It can be either set to "unix" for communication on an
// ephemeral local socket, or "tcp" for network communication on the local host (using a random
// unprivileged port.)
//
// This constructor will panic if the proto argument is neither "unix" nor "tcp".
//
// The path to the plugin executable should be absolute. Any path accepted by the "exec" package in the
// standard library is accepted and the same rules for execution are applied.
//
// Optionally some parameters might be passed to the plugin executable.
func NewPlugin(proto, path string, params ...string) *Plugin {
if proto != "unix" && proto != "tcp" {
panic("Invalid protocol. Specify 'unix' or 'tcp'.")
}
p := &Plugin{
exe: path,
proto: proto,
params: params,
initTimeout: 2 * time.Second,
exitTimeout: 2 * time.Second,
handler: NewDefaultErrorHandler(),
meta: meta("pingo" + randstr(5)),
objsCh: make(chan *objects),
connCh: make(chan *conn),
killCh: make(chan *waiter),
exitCh: make(chan struct{}),
}
return p
}
// Set the error (and output) handler implementation. Use this to set a custom implementation.
// By default, standard logging is used. See ErrorHandler.
//
// Panics if called after Start.
func (p *Plugin) SetErrorHandler(h ErrorHandler) {
if p.running {
panic("Cannot call SetErrorHandler after Start")
}
p.handler = h
}
// Set the maximum time a plugin is allowed to start up and to shut down. Empty timeout (zero)
// is not allowed, default will be used.
//
// Default is two seconds.
//
// Panics if called after Start.
func (p *Plugin) SetTimeout(t time.Duration) {
if p.running {
panic("Cannot call SetTimeout after Start")
}
if t == 0 {
return
}
p.initTimeout = t
p.exitTimeout = t
}
func (p *Plugin) SetSocketDirectory(dir string) {
if p.running {
panic("Cannot call SetSocketDirectory after Start")
}
p.unixdir = dir
}
// Default string representation
func (p *Plugin) String() string {
return fmt.Sprintf("%s %s", p.exe, strings.Join(p.params, " "))
}
// Start will execute the plugin as a subprocess. Start will return immediately. Any first call to the
// plugin will reveal eventual errors occurred at initialization.
//
// Calls subsequent to Start will hang until the plugin has been properly initialized.
func (p *Plugin) Start() {
p.running = true
go p.run()
}
// Stop attemps to stop cleanly or kill the running plugin, then will free all resources.
// Stop returns when the plugin as been shut down and related routines have exited.
func (p *Plugin) Stop() {
wr := newWaiter()
p.killCh <- wr
wr.wait()
p.exitCh <- struct{}{}
}
// Call performs an RPC call to the plugin. Prior to calling Call, the plugin must have been
// initialized by calling Start.
//
// Call will hang until a plugin has been initialized; it will return any error that happens
// either when performing the call or during plugin initialization via Start.
//
// Please refer to the "rpc" package from the standard library for more information on the
// semantics of this function.
func (p *Plugin) Call(name string, args interface{}, resp interface{}) error {
conn := &conn{wr: newWaiter()}
p.connCh <- conn
conn.wr.wait()
if conn.err != nil {
return conn.err
}
return conn.client.Call(name, args, resp)
}
// Objects returns a list of the exported objects from the plugin. Exported objects used
// internally are not reported.
//
// Like Call, Objects returns any error happened on initialization if called after Start.
func (p *Plugin) Objects() ([]string, error) {
objects := &objects{wr: newWaiter()}
p.objsCh <- objects
objects.wr.wait()
return objects.list, objects.err
}
// ErrorHandler is the interface used by Plugin to report non-fatal errors and any other
// output from the plugin.
//
// A default implementation is provided and used if none is specified on plugin creation.
type ErrorHandler interface {
// Error is called whenever a non-fatal error occurs in the plugin subprocess.
Error(error)
// Print is called for each line of output received from the plugin subprocess.
Print(interface{})
}
// Default error handler implementation. Uses the default logging facility from the
// Go standard library.
type DefaultErrorHandler struct{}
// Constructor for default error handler.
func NewDefaultErrorHandler() *DefaultErrorHandler {
return &DefaultErrorHandler{}
}
// Log via default standard library facility prepending the "error: " string.
func (e *DefaultErrorHandler) Error(err error) {
log.Print("error: ", err)
}
// Log via default standard library facility.
func (e *DefaultErrorHandler) Print(s interface{}) {
log.Print(s)
}
const internalObject = "PingoRpc"
type conn struct {
client *rpc.Client
err error
wr *waiter
}
type waiter struct {
c chan struct{}
}
func newWaiter() *waiter {
return &waiter{c: make(chan struct{})}
}
func (wr *waiter) wait() {
<-wr.c
}
func (wr *waiter) done() {
close(wr.c)
}
func (wr *waiter) reset() {
wr.c = make(chan struct{})
}
type client struct {
*rpc.Client
secret string
}
func newClient(s string, conn io.ReadWriteCloser) *client {
return &client{secret: s, Client: rpc.NewClient(conn)}
}
func (c *client) authenticate(w io.Writer) error {
_, err := io.WriteString(w, "Auth-Token: "+c.secret+"\n\n")
return err
}
func dialAuthRpc(secret, network, address string, timeout time.Duration) (*rpc.Client, error) {
conn, err := net.DialTimeout(network, address, timeout)
if err != nil {
return nil, err
}
c := newClient(secret, conn)
if err := c.authenticate(conn); err != nil {
return nil, err
}
return c.Client, nil
}
type objects struct {
list []string
err error
wr *waiter
}
type ctrl struct {
p *Plugin
objs []string
// Protocol and address for RPC
proto, addr string
// Secret needed to connect to server
secret string
// Unrecoverable error is used as response to calls after it happened.
err error
// This channel is an alias to p.connCh. It allows to
// intermittedly process calls (only when we can handle them).
connCh chan *conn
// Same as above, but for objects requests
objsCh chan *objects
// Timeout on plugin startup time
timeoutCh <-chan time.Time
// Get notification from Wait on the subprocess
waitCh chan error
// Get output lines from subprocess
linesCh chan string
// Respond to a routine waiting for this mail loop to exit.
over *waiter
// Executable
proc *os.Process
// RPC client to subprocess
client *rpc.Client
}
func newCtrl(p *Plugin, t time.Duration) *ctrl {
return &ctrl{
p: p,
timeoutCh: time.After(t),
linesCh: make(chan string),
waitCh: make(chan error),
}
}
func (c *ctrl) fatal(err error) {
c.err = err
c.open()
c.kill()
}
func (c *ctrl) isFatal() bool {
return c.err != nil
}
func (c *ctrl) close() {
c.connCh = nil
c.objsCh = nil
}
func (c *ctrl) open() {
c.connCh = c.p.connCh
c.objsCh = c.p.objsCh
}
func (c *ctrl) ready(val string) bool {
var err error
if err := c.parseReady(val); err != nil {
c.fatal(err)
return false
}
c.client, err = dialAuthRpc(c.secret, c.proto, c.addr, c.p.initTimeout)
if err != nil {
c.fatal(err)
return false
}
// Remove the temp socket now that we are connected
if c.proto == "unix" {
if err := os.Remove(c.addr); err != nil {
c.p.handler.Error(errors.New("Cannot remove temporary socket: " + err.Error()))
}
}
// Defuse the timeout on ready
c.timeoutCh = nil
return true
}
func (c *ctrl) readOutput(r io.Reader) {
scanner := bufio.NewScanner(r)
for scanner.Scan() {
c.linesCh <- scanner.Text()
}
}
func (c *ctrl) waitErr(pidCh chan<- int, err error) {
close(pidCh)
c.waitCh <- err
}
func (c *ctrl) wait(pidCh chan<- int, exe string, params ...string) {
defer close(c.waitCh)
cmd := exec.Command(exe, params...)
stdout, err := cmd.StdoutPipe()
if err != nil {
c.waitErr(pidCh, err)
return
}
stderr, err := cmd.StderrPipe()
if err != nil {
c.waitErr(pidCh, err)
return
}
if err := cmd.Start(); err != nil {
c.waitErr(pidCh, err)
return
}
pidCh <- cmd.Process.Pid
close(pidCh)
c.readOutput(stdout)
c.readOutput(stderr)
c.waitCh <- cmd.Wait()
}
func (c *ctrl) kill() {
if c.proc == nil {
return
}
// Ignore errors here because Kill might have been called after
// process has ended.
c.proc.Kill()
c.proc = nil
}
func (c *ctrl) parseReady(str string) error {
if !strings.HasPrefix(str, "proto=") {
return errInvalidMessage
}
str = str[6:]
s := strings.IndexByte(str, ' ')
if s < 0 {
return errInvalidMessage
}
proto := str[0:s]
if proto != "unix" && proto != "tcp" {
return errInvalidMessage
}
c.proto = proto
str = str[s+1:]
if !strings.HasPrefix(str, "addr=") {
return errInvalidMessage
}
c.addr = str[5:]
return nil
}
// Copy the list of objects for the requestor
func (c *ctrl) objects() []string {
list := make([]string, len(c.objs)-1)
for i, j := 0, 0; i < len(c.objs); i++ {
if c.objs[i] == internalObject {
continue
}
list[j] = c.objs[i]
j = j + 1
}
return list
}
func (p *Plugin) run() {
if p.unixdir == "" {
p.unixdir = os.TempDir()
}
params := []string{
"-pingo:prefix=" + string(p.meta),
"-pingo:proto=" + p.proto,
}
if p.proto == "unix" && p.unixdir != "" {
params = append(params, "-pingo:unixdir="+p.unixdir)
}
for i := 0; i < len(p.params); i++ {
params = append(params, p.params[i])
}
c := newCtrl(p, p.initTimeout)
pidCh := make(chan int)
go c.wait(pidCh, p.exe, params...)
pid := <-pidCh
if pid != 0 {
if proc, err := os.FindProcess(pid); err == nil {
c.proc = proc
}
}
for {
select {
case <-c.timeoutCh:
c.fatal(errRegistrationTimeout)
case r := <-c.connCh:
if c.isFatal() {
r.err = c.err
r.wr.done()
continue
}
r.client = c.client
r.wr.done()
case o := <-c.objsCh:
if c.isFatal() {
o.err = c.err
o.wr.done()
continue
}
o.list = c.objects()
o.wr.done()
case line := <-c.linesCh:
key, val := p.meta.parse(line)
switch key {
case "auth-token":
c.secret = val
case "fatal":
if err := parseError(val); err != nil {
c.fatal(err)
} else {
c.fatal(errors.New(val))
}
case "error":
if err := parseError(val); err != nil {
p.handler.Print(err)
} else {
p.handler.Print(errors.New(val))
}
case "objects":
c.objs = strings.Split(val, ", ")
case "ready":
if !c.ready(val) {
continue
}
// Start accepting calls
c.open()
default:
p.handler.Print(line)
}
case wr := <-p.killCh:
if c.waitCh == nil {
wr.done()
continue
}
// If we don't accept calls, kill immediately
if c.connCh == nil || c.client == nil {
c.kill()
} else {
// Be sure to kill the process if it doesn't obey Exit.
go func(pid int, t time.Duration) {
<-time.After(t)
if proc, err := os.FindProcess(pid); err == nil {
proc.Kill()
}
}(pid, p.exitTimeout)
c.client.Call(internalObject+".Exit", 0, nil)
}
if c.client != nil {
c.client.Close()
}
// Do not accept calls
c.close()
// When wait on the subprocess is exited, signal back via "over"
c.over = wr
case err := <-c.waitCh:
if err != nil {
if _, ok := err.(*exec.ExitError); !ok {
p.handler.Error(err)
}
c.fatal(err)
}
// Signal to whoever killed us (via killCh) that we are done
if c.over != nil {
c.over.done()
}
c.proc = nil
c.waitCh = nil
c.linesCh = nil
case <-p.exitCh:
return
}
}
}
plugin.go 源码阅读的更多相关文章
- 详细讲解Hadoop源码阅读工程(以hadoop-2.6.0-src.tar.gz和hadoop-2.6.0-cdh5.4.5-src.tar.gz为代表)
首先,说的是,本人到现在为止,已经玩过. 对于,这样的软件,博友,可以去看我博客的相关博文.在此,不一一赘述! Eclipse *版本 Eclipse *下载 Jd ...
- 内核源码阅读vim+cscope+ctags+taglist
杜斌博客:http://blog.db89.org/kernel-source-read-vim-cscope-ctags-taglist/ 武特博客:http://edsionte.com/tech ...
- Caddy源码阅读(二)启动流程与 Event 事件通知
Caddy源码阅读(二)启动流程与 Event 事件通知 Preface Caddy 是 Go 语言构建的轻量配置化服务器.https://github.com/caddyserver/caddy C ...
- react v16.12 源码阅读环境搭建
搭建后的代码(Keep updated): https://github.com/lirongfei123/read-react 欢迎将源码阅读遇到的问题提到issue 环境搭建思路: 搭建一个web ...
- 搭建 Spring 源码阅读环境
前言 有一个Spring源码阅读环境是学习Spring的基础.笔者借鉴了网上很多搭建环境的方法,也尝试了很多,接下来总结两种个人认为比较简便实用的方法.读者可根据自己的需要自行选择. 方法一:搭建基础 ...
- 【原】FMDB源码阅读(三)
[原]FMDB源码阅读(三) 本文转载请注明出处 —— polobymulberry-博客园 1. 前言 FMDB比较优秀的地方就在于对多线程的处理.所以这一篇主要是研究FMDB的多线程处理的实现.而 ...
- 【原】FMDB源码阅读(二)
[原]FMDB源码阅读(二) 本文转载请注明出处 -- polobymulberry-博客园 1. 前言 上一篇只是简单地过了一下FMDB一个简单例子的基本流程,并没有涉及到FMDB的所有方方面面,比 ...
- 【原】FMDB源码阅读(一)
[原]FMDB源码阅读(一) 本文转载请注明出处 —— polobymulberry-博客园 1. 前言 说实话,之前的SDWebImage和AFNetworking这两个组件我还是使用过的,但是对于 ...
- 【原】AFNetworking源码阅读(六)
[原]AFNetworking源码阅读(六) 本文转载请注明出处 —— polobymulberry-博客园 1. 前言 这一篇的想讲的,一个就是分析一下AFSecurityPolicy文件,看看AF ...
随机推荐
- 四种生成和解析XML文档的方法详解
众所周知,现在解析XML的方法越来越多,但主流的方法也就四种,即:DOM.SAX.JDOM和DOM4J 下面首先给出这四种方法的jar包下载地址 DOM:在现在的Java JDK里都自带了,在xml- ...
- Tomcat的缺省是多少,怎么修改
Tomcat的缺省端口号是8080. 修改Tomcat端口号: 1.找到Tomcat目录下的conf文件夹 2.进入conf文件夹里面找到server.xml文件 3.打开server.xml文件 ...
- AngularJs 隔离作用域
初学NG,有诸多的不解,今天看了一篇文章,原文地址:https://segmentfault.com/a/1190000002773689#articleHeader0 ,本文运行的代码也出处此. 里 ...
- geth常用指令
ubuntu下载: https://github.com/ethereum/go-ethereum/wiki/Installation-Instructions-for-Ubuntu sudo apt ...
- 选择Web框架的20条标准
原文观点由Matt Raible提出,关于Matt Rabile的介绍:http://www.infoq.com/cn/author/Matt-Raible 内容摘自<Java程序员修炼之道&g ...
- python flask中的代码约定
在Python社区中有许多关于代码风格的约定.如果你写过一段时间Python了,那么也许对此已经有些了解. 我会简单介绍一下,同时给你一些URL链接,从中你可以找到关于这个话题的详细信息. 让我们提出 ...
- spring中配置quartz调用两次及项目日志log4j不能每天生成日志解决方法
在quartz中配置了一个方法运行时会连续调用两次,是因为加载两次,只需在tomcat的server.xml中修改配置 <Host name="www.xx.cn" appB ...
- c# 多线程编程中AutoResetEvent和ManualResetEvent
作为等同于Java的wait,notify,notifyAll的存在,AutoResetEvent和ManualResetEvent分别实现了notify和notifyAll的功能,下面的代码简单讲解 ...
- 前端工程化(二)---webpack配置
导航 前端工程化(一)---工程基础目录搭建 前端工程化(二)---webpack配置 前端工程化(三)---Vue的开发模式 前端工程化(四)---helloWord 继续上一遍的配置,本节主要记录 ...
- 前端开发APP,从HBuilder开始~ 【转】
内容简介 介绍目前前端人员开发app的几种方法,具体介绍hbuilder开发app,一扇赞新的大门~ 无所不能的js 最开始js仅仅局限于网页上一些效果,操作网页内容等, 但是nodejs把js带入了 ...