Fix supervisord collector (#978)
* Replace supervisord xmlrpc library * Use `github.com/mattn/go-xmlrpc` that doesn't leak goroutines. * Fix uptime metric * Use Prometheus best practices for uptime metric. * Use "start time" rather than "uptime". * Don't emit a start time if the process is down. * Add changelog entry. * Add example compatibility rules. Signed-off-by: Ben Kochie <superq@gmail.com>pull/1038/head
parent
2c52b8c761
commit
5d23ad0ca7
@ -0,0 +1,5 @@
|
||||
groups:
|
||||
- name: node_exporter-17-supervisord
|
||||
rules:
|
||||
- record: node_supervisord_start_time_seconds
|
||||
expr: node_supervisord_uptime + time()
|
@ -0,0 +1,5 @@
|
||||
groups:
|
||||
- name: node_exporter-17-supervisord
|
||||
rules:
|
||||
- record: node_supervisord_uptime
|
||||
expr: time() - node_supervisord_start_time_seconds
|
@ -1,79 +0,0 @@
|
||||
## Overview
|
||||
|
||||
xmlrpc is an implementation of client side part of XMLRPC protocol in Go language.
|
||||
|
||||
## Installation
|
||||
|
||||
To install xmlrpc package run `go get github.com/kolo/xmlrpc`. To use
|
||||
it in application add `"github.com/kolo/xmlrpc"` string to `import`
|
||||
statement.
|
||||
|
||||
## Usage
|
||||
|
||||
client, _ := xmlrpc.NewClient("https://bugzilla.mozilla.org/xmlrpc.cgi", nil)
|
||||
result := struct{
|
||||
Version string `xmlrpc:"version"`
|
||||
}{}
|
||||
client.Call("Bugzilla.version", nil, &result)
|
||||
fmt.Printf("Version: %s\n", result.Version) // Version: 4.2.7+
|
||||
|
||||
Second argument of NewClient function is an object that implements
|
||||
[http.RoundTripper](http://golang.org/pkg/net/http/#RoundTripper)
|
||||
interface, it can be used to get more control over connection options.
|
||||
By default it initialized by http.DefaultTransport object.
|
||||
|
||||
### Arguments encoding
|
||||
|
||||
xmlrpc package supports encoding of native Go data types to method
|
||||
arguments.
|
||||
|
||||
Data types encoding rules:
|
||||
* int, int8, int16, int32, int64 encoded to int;
|
||||
* float32, float64 encoded to double;
|
||||
* bool encoded to boolean;
|
||||
* string encoded to string;
|
||||
* time.Time encoded to datetime.iso8601;
|
||||
* xmlrpc.Base64 encoded to base64;
|
||||
* slice decoded to array;
|
||||
|
||||
Structs decoded to struct by following rules:
|
||||
* all public field become struct members;
|
||||
* field name become member name;
|
||||
* if field has xmlrpc tag, its value become member name.
|
||||
|
||||
Server method can accept few arguments, to handle this case there is
|
||||
special approach to handle slice of empty interfaces (`[]interface{}`).
|
||||
Each value of such slice encoded as separate argument.
|
||||
|
||||
### Result decoding
|
||||
|
||||
Result of remote function is decoded to native Go data type.
|
||||
|
||||
Data types decoding rules:
|
||||
* int, i4 decoded to int, int8, int16, int32, int64;
|
||||
* double decoded to float32, float64;
|
||||
* boolean decoded to bool;
|
||||
* string decoded to string;
|
||||
* array decoded to slice;
|
||||
* structs decoded following the rules described in previous section;
|
||||
* datetime.iso8601 decoded as time.Time data type;
|
||||
* base64 decoded to string.
|
||||
|
||||
## Implementation details
|
||||
|
||||
xmlrpc package contains clientCodec type, that implements [rpc.ClientCodec](http://golang.org/pkg/net/rpc/#ClientCodec)
|
||||
interface of [net/rpc](http://golang.org/pkg/net/rpc) package.
|
||||
|
||||
xmlrpc package works over HTTP protocol, but some internal functions
|
||||
and data type were made public to make it easier to create another
|
||||
implementation of xmlrpc that works over another protocol. To encode
|
||||
request body there is EncodeMethodCall function. To decode server
|
||||
response Response data type can be used.
|
||||
|
||||
## Contribution
|
||||
|
||||
Feel free to fork the project, submit pull requests, ask questions.
|
||||
|
||||
## Authors
|
||||
|
||||
Dmitry Maksimov (dmtmax@gmail.com)
|
@ -1,144 +0,0 @@
|
||||
package xmlrpc
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/http/cookiejar"
|
||||
"net/rpc"
|
||||
"net/url"
|
||||
)
|
||||
|
||||
type Client struct {
|
||||
*rpc.Client
|
||||
}
|
||||
|
||||
// clientCodec is rpc.ClientCodec interface implementation.
|
||||
type clientCodec struct {
|
||||
// url presents url of xmlrpc service
|
||||
url *url.URL
|
||||
|
||||
// httpClient works with HTTP protocol
|
||||
httpClient *http.Client
|
||||
|
||||
// cookies stores cookies received on last request
|
||||
cookies http.CookieJar
|
||||
|
||||
// responses presents map of active requests. It is required to return request id, that
|
||||
// rpc.Client can mark them as done.
|
||||
responses map[uint64]*http.Response
|
||||
|
||||
response *Response
|
||||
|
||||
// ready presents channel, that is used to link request and it`s response.
|
||||
ready chan uint64
|
||||
}
|
||||
|
||||
func (codec *clientCodec) WriteRequest(request *rpc.Request, args interface{}) (err error) {
|
||||
httpRequest, err := NewRequest(codec.url.String(), request.ServiceMethod, args)
|
||||
|
||||
if codec.cookies != nil {
|
||||
for _, cookie := range codec.cookies.Cookies(codec.url) {
|
||||
httpRequest.AddCookie(cookie)
|
||||
}
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var httpResponse *http.Response
|
||||
httpResponse, err = codec.httpClient.Do(httpRequest)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if codec.cookies != nil {
|
||||
codec.cookies.SetCookies(codec.url, httpResponse.Cookies())
|
||||
}
|
||||
|
||||
codec.responses[request.Seq] = httpResponse
|
||||
codec.ready <- request.Seq
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (codec *clientCodec) ReadResponseHeader(response *rpc.Response) (err error) {
|
||||
seq := <-codec.ready
|
||||
httpResponse := codec.responses[seq]
|
||||
|
||||
if httpResponse.StatusCode < 200 || httpResponse.StatusCode >= 300 {
|
||||
return fmt.Errorf("request error: bad status code - %d", httpResponse.StatusCode)
|
||||
}
|
||||
|
||||
respData, err := ioutil.ReadAll(httpResponse.Body)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
httpResponse.Body.Close()
|
||||
|
||||
resp := NewResponse(respData)
|
||||
|
||||
if resp.Failed() {
|
||||
response.Error = fmt.Sprintf("%v", resp.Err())
|
||||
}
|
||||
|
||||
codec.response = resp
|
||||
|
||||
response.Seq = seq
|
||||
delete(codec.responses, seq)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (codec *clientCodec) ReadResponseBody(v interface{}) (err error) {
|
||||
if v == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if err = codec.response.Unmarshal(v); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (codec *clientCodec) Close() error {
|
||||
transport := codec.httpClient.Transport.(*http.Transport)
|
||||
transport.CloseIdleConnections()
|
||||
return nil
|
||||
}
|
||||
|
||||
// NewClient returns instance of rpc.Client object, that is used to send request to xmlrpc service.
|
||||
func NewClient(requrl string, transport http.RoundTripper) (*Client, error) {
|
||||
if transport == nil {
|
||||
transport = http.DefaultTransport
|
||||
}
|
||||
|
||||
httpClient := &http.Client{Transport: transport}
|
||||
|
||||
jar, err := cookiejar.New(nil)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
u, err := url.Parse(requrl)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
codec := clientCodec{
|
||||
url: u,
|
||||
httpClient: httpClient,
|
||||
ready: make(chan uint64),
|
||||
responses: make(map[uint64]*http.Response),
|
||||
cookies: jar,
|
||||
}
|
||||
|
||||
return &Client{rpc.NewClientWithCodec(&codec)}, nil
|
||||
}
|
@ -1,449 +0,0 @@
|
||||
package xmlrpc
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/xml"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
const iso8601 = "20060102T15:04:05"
|
||||
|
||||
var (
|
||||
// CharsetReader is a function to generate reader which converts a non UTF-8
|
||||
// charset into UTF-8.
|
||||
CharsetReader func(string, io.Reader) (io.Reader, error)
|
||||
|
||||
invalidXmlError = errors.New("invalid xml")
|
||||
)
|
||||
|
||||
type TypeMismatchError string
|
||||
|
||||
func (e TypeMismatchError) Error() string { return string(e) }
|
||||
|
||||
type decoder struct {
|
||||
*xml.Decoder
|
||||
}
|
||||
|
||||
func unmarshal(data []byte, v interface{}) (err error) {
|
||||
dec := &decoder{xml.NewDecoder(bytes.NewBuffer(data))}
|
||||
|
||||
if CharsetReader != nil {
|
||||
dec.CharsetReader = CharsetReader
|
||||
}
|
||||
|
||||
var tok xml.Token
|
||||
for {
|
||||
if tok, err = dec.Token(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if t, ok := tok.(xml.StartElement); ok {
|
||||
if t.Name.Local == "value" {
|
||||
val := reflect.ValueOf(v)
|
||||
if val.Kind() != reflect.Ptr {
|
||||
return errors.New("non-pointer value passed to unmarshal")
|
||||
}
|
||||
if err = dec.decodeValue(val.Elem()); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// read until end of document
|
||||
err = dec.Skip()
|
||||
if err != nil && err != io.EOF {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (dec *decoder) decodeValue(val reflect.Value) error {
|
||||
var tok xml.Token
|
||||
var err error
|
||||
|
||||
if val.Kind() == reflect.Ptr {
|
||||
if val.IsNil() {
|
||||
val.Set(reflect.New(val.Type().Elem()))
|
||||
}
|
||||
val = val.Elem()
|
||||
}
|
||||
|
||||
var typeName string
|
||||
for {
|
||||
if tok, err = dec.Token(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if t, ok := tok.(xml.EndElement); ok {
|
||||
if t.Name.Local == "value" {
|
||||
return nil
|
||||
} else {
|
||||
return invalidXmlError
|
||||
}
|
||||
}
|
||||
|
||||
if t, ok := tok.(xml.StartElement); ok {
|
||||
typeName = t.Name.Local
|
||||
break
|
||||
}
|
||||
|
||||
// Treat value data without type identifier as string
|
||||
if t, ok := tok.(xml.CharData); ok {
|
||||
if value := strings.TrimSpace(string(t)); value != "" {
|
||||
if err = checkType(val, reflect.String); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
val.SetString(value)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
switch typeName {
|
||||
case "struct":
|
||||
ismap := false
|
||||
pmap := val
|
||||
valType := val.Type()
|
||||
|
||||
if err = checkType(val, reflect.Struct); err != nil {
|
||||
if checkType(val, reflect.Map) == nil {
|
||||
if valType.Key().Kind() != reflect.String {
|
||||
return fmt.Errorf("only maps with string key type can be unmarshalled")
|
||||
}
|
||||
ismap = true
|
||||
} else if checkType(val, reflect.Interface) == nil && val.IsNil() {
|
||||
var dummy map[string]interface{}
|
||||
pmap = reflect.New(reflect.TypeOf(dummy)).Elem()
|
||||
valType = pmap.Type()
|
||||
ismap = true
|
||||
} else {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
var fields map[string]reflect.Value
|
||||
|
||||
if !ismap {
|
||||
fields = make(map[string]reflect.Value)
|
||||
|
||||
for i := 0; i < valType.NumField(); i++ {
|
||||
field := valType.Field(i)
|
||||
fieldVal := val.FieldByName(field.Name)
|
||||
|
||||
if fieldVal.CanSet() {
|
||||
if fn := field.Tag.Get("xmlrpc"); fn != "" {
|
||||
fields[fn] = fieldVal
|
||||
} else {
|
||||
fields[field.Name] = fieldVal
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Create initial empty map
|
||||
pmap.Set(reflect.MakeMap(valType))
|
||||
}
|
||||
|
||||
// Process struct members.
|
||||
StructLoop:
|
||||
for {
|
||||
if tok, err = dec.Token(); err != nil {
|
||||
return err
|
||||
}
|
||||
switch t := tok.(type) {
|
||||
case xml.StartElement:
|
||||
if t.Name.Local != "member" {
|
||||
return invalidXmlError
|
||||
}
|
||||
|
||||
tagName, fieldName, err := dec.readTag()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if tagName != "name" {
|
||||
return invalidXmlError
|
||||
}
|
||||
|
||||
var fv reflect.Value
|
||||
ok := true
|
||||
|
||||
if !ismap {
|
||||
fv, ok = fields[string(fieldName)]
|
||||
} else {
|
||||
fv = reflect.New(valType.Elem())
|
||||
}
|
||||
|
||||
if ok {
|
||||
for {
|
||||
if tok, err = dec.Token(); err != nil {
|
||||
return err
|
||||
}
|
||||
if t, ok := tok.(xml.StartElement); ok && t.Name.Local == "value" {
|
||||
if err = dec.decodeValue(fv); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// </value>
|
||||
if err = dec.Skip(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// </member>
|
||||
if err = dec.Skip(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if ismap {
|
||||
pmap.SetMapIndex(reflect.ValueOf(string(fieldName)), reflect.Indirect(fv))
|
||||
val.Set(pmap)
|
||||
}
|
||||
case xml.EndElement:
|
||||
break StructLoop
|
||||
}
|
||||
}
|
||||
case "array":
|
||||
pslice := val
|
||||
if checkType(val, reflect.Interface) == nil && val.IsNil() {
|
||||
var dummy []interface{}
|
||||
pslice = reflect.New(reflect.TypeOf(dummy)).Elem()
|
||||
} else if err = checkType(val, reflect.Slice); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ArrayLoop:
|
||||
for {
|
||||
if tok, err = dec.Token(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
switch t := tok.(type) {
|
||||
case xml.StartElement:
|
||||
if t.Name.Local != "data" {
|
||||
return invalidXmlError
|
||||
}
|
||||
|
||||
slice := reflect.MakeSlice(pslice.Type(), 0, 0)
|
||||
|
||||
DataLoop:
|
||||
for {
|
||||
if tok, err = dec.Token(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
switch tt := tok.(type) {
|
||||
case xml.StartElement:
|
||||
if tt.Name.Local != "value" {
|
||||
return invalidXmlError
|
||||
}
|
||||
|
||||
v := reflect.New(pslice.Type().Elem())
|
||||
if err = dec.decodeValue(v); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
slice = reflect.Append(slice, v.Elem())
|
||||
|
||||
// </value>
|
||||
if err = dec.Skip(); err != nil {
|
||||
return err
|
||||
}
|
||||
case xml.EndElement:
|
||||
pslice.Set(slice)
|
||||
val.Set(pslice)
|
||||
break DataLoop
|
||||
}
|
||||
}
|
||||
case xml.EndElement:
|
||||
break ArrayLoop
|
||||
}
|
||||
}
|
||||
default:
|
||||
if tok, err = dec.Token(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var data []byte
|
||||
|
||||
switch t := tok.(type) {
|
||||
case xml.EndElement:
|
||||
return nil
|
||||
case xml.CharData:
|
||||
data = []byte(t.Copy())
|
||||
default:
|
||||
return invalidXmlError
|
||||
}
|
||||
|
||||
switch typeName {
|
||||
case "int", "i4", "i8":
|
||||
if checkType(val, reflect.Interface) == nil && val.IsNil() {
|
||||
i, err := strconv.ParseInt(string(data), 10, 64)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
pi := reflect.New(reflect.TypeOf(i)).Elem()
|
||||
pi.SetInt(i)
|
||||
val.Set(pi)
|
||||
} else if err = checkType(val, reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64); err != nil {
|
||||
return err
|
||||
} else {
|
||||
i, err := strconv.ParseInt(string(data), 10, val.Type().Bits())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
val.SetInt(i)
|
||||
}
|
||||
case "string", "base64":
|
||||
str := string(data)
|
||||
if checkType(val, reflect.Interface) == nil && val.IsNil() {
|
||||
pstr := reflect.New(reflect.TypeOf(str)).Elem()
|
||||
pstr.SetString(str)
|
||||
val.Set(pstr)
|
||||
} else if err = checkType(val, reflect.String); err != nil {
|
||||
return err
|
||||
} else {
|
||||
val.SetString(str)
|
||||
}
|
||||
case "dateTime.iso8601":
|
||||
t, err := time.Parse(iso8601, string(data))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if checkType(val, reflect.Interface) == nil && val.IsNil() {
|
||||
ptime := reflect.New(reflect.TypeOf(t)).Elem()
|
||||
ptime.Set(reflect.ValueOf(t))
|
||||
val.Set(ptime)
|
||||
} else if _, ok := val.Interface().(time.Time); !ok {
|
||||
return TypeMismatchError(fmt.Sprintf("error: type mismatch error - can't decode %v to time", val.Kind()))
|
||||
} else {
|
||||
val.Set(reflect.ValueOf(t))
|
||||
}
|
||||
case "boolean":
|
||||
v, err := strconv.ParseBool(string(data))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if checkType(val, reflect.Interface) == nil && val.IsNil() {
|
||||
pv := reflect.New(reflect.TypeOf(v)).Elem()
|
||||
pv.SetBool(v)
|
||||
val.Set(pv)
|
||||
} else if err = checkType(val, reflect.Bool); err != nil {
|
||||
return err
|
||||
} else {
|
||||
val.SetBool(v)
|
||||
}
|
||||
case "double":
|
||||
if checkType(val, reflect.Interface) == nil && val.IsNil() {
|
||||
i, err := strconv.ParseFloat(string(data), 64)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
pdouble := reflect.New(reflect.TypeOf(i)).Elem()
|
||||
pdouble.SetFloat(i)
|
||||
val.Set(pdouble)
|
||||
} else if err = checkType(val, reflect.Float32, reflect.Float64); err != nil {
|
||||
return err
|
||||
} else {
|
||||
i, err := strconv.ParseFloat(string(data), val.Type().Bits())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
val.SetFloat(i)
|
||||
}
|
||||
default:
|
||||
return errors.New("unsupported type")
|
||||
}
|
||||
|
||||
// </type>
|
||||
if err = dec.Skip(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (dec *decoder) readTag() (string, []byte, error) {
|
||||
var tok xml.Token
|
||||
var err error
|
||||
|
||||
var name string
|
||||
for {
|
||||
if tok, err = dec.Token(); err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
|
||||
if t, ok := tok.(xml.StartElement); ok {
|
||||
name = t.Name.Local
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
value, err := dec.readCharData()
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
|
||||
return name, value, dec.Skip()
|
||||
}
|
||||
|
||||
func (dec *decoder) readCharData() ([]byte, error) {
|
||||
var tok xml.Token
|
||||
var err error
|
||||
|
||||
if tok, err = dec.Token(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if t, ok := tok.(xml.CharData); ok {
|
||||
return []byte(t.Copy()), nil
|
||||
} else {
|
||||
return nil, invalidXmlError
|
||||
}
|
||||
}
|
||||
|
||||
func checkType(val reflect.Value, kinds ...reflect.Kind) error {
|
||||
if len(kinds) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
if val.Kind() == reflect.Ptr {
|
||||
val = val.Elem()
|
||||
}
|
||||
|
||||
match := false
|
||||
|
||||
for _, kind := range kinds {
|
||||
if val.Kind() == kind {
|
||||
match = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !match {
|
||||
return TypeMismatchError(fmt.Sprintf("error: type mismatch - can't unmarshal %v to %v",
|
||||
val.Kind(), kinds[0]))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
@ -1,164 +0,0 @@
|
||||
package xmlrpc
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/xml"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
type encodeFunc func(reflect.Value) ([]byte, error)
|
||||
|
||||
func marshal(v interface{}) ([]byte, error) {
|
||||
if v == nil {
|
||||
return []byte{}, nil
|
||||
}
|
||||
|
||||
val := reflect.ValueOf(v)
|
||||
return encodeValue(val)
|
||||
}
|
||||
|
||||
func encodeValue(val reflect.Value) ([]byte, error) {
|
||||
var b []byte
|
||||
var err error
|
||||
|
||||
if val.Kind() == reflect.Ptr || val.Kind() == reflect.Interface {
|
||||
if val.IsNil() {
|
||||
return []byte("<value/>"), nil
|
||||
}
|
||||
|
||||
val = val.Elem()
|
||||
}
|
||||
|
||||
switch val.Kind() {
|
||||
case reflect.Struct:
|
||||
switch val.Interface().(type) {
|
||||
case time.Time:
|
||||
t := val.Interface().(time.Time)
|
||||
b = []byte(fmt.Sprintf("<dateTime.iso8601>%s</dateTime.iso8601>", t.Format(iso8601)))
|
||||
default:
|
||||
b, err = encodeStruct(val)
|
||||
}
|
||||
case reflect.Map:
|
||||
b, err = encodeMap(val)
|
||||
case reflect.Slice:
|
||||
b, err = encodeSlice(val)
|
||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||
b = []byte(fmt.Sprintf("<int>%s</int>", strconv.FormatInt(val.Int(), 10)))
|
||||
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
|
||||
b = []byte(fmt.Sprintf("<i4>%s</i4>", strconv.FormatUint(val.Uint(), 10)))
|
||||
case reflect.Float32, reflect.Float64:
|
||||
b = []byte(fmt.Sprintf("<double>%s</double>",
|
||||
strconv.FormatFloat(val.Float(), 'g', -1, val.Type().Bits())))
|
||||
case reflect.Bool:
|
||||
if val.Bool() {
|
||||
b = []byte("<boolean>1</boolean>")
|
||||
} else {
|
||||
b = []byte("<boolean>0</boolean>")
|
||||
}
|
||||
case reflect.String:
|
||||
var buf bytes.Buffer
|
||||
|
||||
xml.Escape(&buf, []byte(val.String()))
|
||||
|
||||
if _, ok := val.Interface().(Base64); ok {
|
||||
b = []byte(fmt.Sprintf("<base64>%s</base64>", buf.String()))
|
||||
} else {
|
||||
b = []byte(fmt.Sprintf("<string>%s</string>", buf.String()))
|
||||
}
|
||||
default:
|
||||
return nil, fmt.Errorf("xmlrpc encode error: unsupported type")
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return []byte(fmt.Sprintf("<value>%s</value>", string(b))), nil
|
||||
}
|
||||
|
||||
func encodeStruct(val reflect.Value) ([]byte, error) {
|
||||
var b bytes.Buffer
|
||||
|
||||
b.WriteString("<struct>")
|
||||
|
||||
t := val.Type()
|
||||
for i := 0; i < t.NumField(); i++ {
|
||||
b.WriteString("<member>")
|
||||
f := t.Field(i)
|
||||
|
||||
name := f.Tag.Get("xmlrpc")
|
||||
if name == "" {
|
||||
name = f.Name
|
||||
}
|
||||
b.WriteString(fmt.Sprintf("<name>%s</name>", name))
|
||||
|
||||
p, err := encodeValue(val.FieldByName(f.Name))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
b.Write(p)
|
||||
|
||||
b.WriteString("</member>")
|
||||
}
|
||||
|
||||
b.WriteString("</struct>")
|
||||
|
||||
return b.Bytes(), nil
|
||||
}
|
||||
|
||||
func encodeMap(val reflect.Value) ([]byte, error) {
|
||||
var t = val.Type()
|
||||
|
||||
if t.Key().Kind() != reflect.String {
|
||||
return nil, fmt.Errorf("xmlrpc encode error: only maps with string keys are supported")
|
||||
}
|
||||
|
||||
var b bytes.Buffer
|
||||
|
||||
b.WriteString("<struct>")
|
||||
|
||||
keys := val.MapKeys()
|
||||
|
||||
for i := 0; i < val.Len(); i++ {
|
||||
key := keys[i]
|
||||
kval := val.MapIndex(key)
|
||||
|
||||
b.WriteString("<member>")
|
||||
b.WriteString(fmt.Sprintf("<name>%s</name>", key.String()))
|
||||
|
||||
p, err := encodeValue(kval)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
b.Write(p)
|
||||
b.WriteString("</member>")
|
||||
}
|
||||
|
||||
b.WriteString("</struct>")
|
||||
|
||||
return b.Bytes(), nil
|
||||
}
|
||||
|
||||
func encodeSlice(val reflect.Value) ([]byte, error) {
|
||||
var b bytes.Buffer
|
||||
|
||||
b.WriteString("<array><data>")
|
||||
|
||||
for i := 0; i < val.Len(); i++ {
|
||||
p, err := encodeValue(val.Index(i))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
b.Write(p)
|
||||
}
|
||||
|
||||
b.WriteString("</data></array>")
|
||||
|
||||
return b.Bytes(), nil
|
||||
}
|
@ -1,57 +0,0 @@
|
||||
package xmlrpc
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
func NewRequest(url string, method string, args interface{}) (*http.Request, error) {
|
||||
var t []interface{}
|
||||
var ok bool
|
||||
if t, ok = args.([]interface{}); !ok {
|
||||
if args != nil {
|
||||
t = []interface{}{args}
|
||||
}
|
||||
}
|
||||
|
||||
body, err := EncodeMethodCall(method, t...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
request, err := http.NewRequest("POST", url, bytes.NewReader(body))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
request.Header.Set("Content-Type", "text/xml")
|
||||
request.Header.Set("Content-Length", fmt.Sprintf("%d", len(body)))
|
||||
|
||||
return request, nil
|
||||
}
|
||||
|
||||
func EncodeMethodCall(method string, args ...interface{}) ([]byte, error) {
|
||||
var b bytes.Buffer
|
||||
b.WriteString(`<?xml version="1.0" encoding="UTF-8"?>`)
|
||||
b.WriteString(fmt.Sprintf("<methodCall><methodName>%s</methodName>", method))
|
||||
|
||||
if args != nil {
|
||||
b.WriteString("<params>")
|
||||
|
||||
for _, arg := range args {
|
||||
p, err := marshal(arg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
b.WriteString(fmt.Sprintf("<param>%s</param>", string(p)))
|
||||
}
|
||||
|
||||
b.WriteString("</params>")
|
||||
}
|
||||
|
||||
b.WriteString("</methodCall>")
|
||||
|
||||
return b.Bytes(), nil
|
||||
}
|
@ -1,52 +0,0 @@
|
||||
package xmlrpc
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
)
|
||||
|
||||
var (
|
||||
faultRx = regexp.MustCompile(`<fault>(\s|\S)+</fault>`)
|
||||
)
|
||||
|
||||
type failedResponse struct {
|
||||
Code int `xmlrpc:"faultCode"`
|
||||
Error string `xmlrpc:"faultString"`
|
||||
}
|
||||
|
||||
func (r *failedResponse) err() error {
|
||||
return &xmlrpcError{
|
||||
code: r.Code,
|
||||
err: r.Error,
|
||||
}
|
||||
}
|
||||
|
||||
type Response struct {
|
||||
data []byte
|
||||
}
|
||||
|
||||
func NewResponse(data []byte) *Response {
|
||||
return &Response{
|
||||
data: data,
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Response) Failed() bool {
|
||||
return faultRx.Match(r.data)
|
||||
}
|
||||
|
||||
func (r *Response) Err() error {
|
||||
failedResp := new(failedResponse)
|
||||
if err := unmarshal(r.data, failedResp); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return failedResp.err()
|
||||
}
|
||||
|
||||
func (r *Response) Unmarshal(v interface{}) error {
|
||||
if err := unmarshal(r.data, v); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
@ -1,25 +0,0 @@
|
||||
# encoding: utf-8
|
||||
|
||||
require "xmlrpc/server"
|
||||
|
||||
class Service
|
||||
def time
|
||||
Time.now
|
||||
end
|
||||
|
||||
def upcase(s)
|
||||
s.upcase
|
||||
end
|
||||
|
||||
def sum(x, y)
|
||||
x + y
|
||||
end
|
||||
|
||||
def error
|
||||
raise XMLRPC::FaultException.new(500, "Server error")
|
||||
end
|
||||
end
|
||||
|
||||
server = XMLRPC::Server.new 5001, 'localhost'
|
||||
server.add_handler "service", Service.new
|
||||
server.serve
|
@ -1,19 +0,0 @@
|
||||
package xmlrpc
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// xmlrpcError represents errors returned on xmlrpc request.
|
||||
type xmlrpcError struct {
|
||||
code int
|
||||
err string
|
||||
}
|
||||
|
||||
// Error() method implements Error interface
|
||||
func (e *xmlrpcError) Error() string {
|
||||
return fmt.Sprintf("error: \"%s\" code: %d", e.err, e.code)
|
||||
}
|
||||
|
||||
// Base64 represents value in base64 encoding
|
||||
type Base64 string
|
12
vendor/github.com/kolo/xmlrpc/LICENSE → vendor/github.com/mattn/go-xmlrpc/LICENSE
generated
vendored
12
vendor/github.com/kolo/xmlrpc/LICENSE → vendor/github.com/mattn/go-xmlrpc/LICENSE
generated
vendored
@ -0,0 +1,48 @@
|
||||
# go-xmlrpc
|
||||
|
||||
xmlrpc interface for go
|
||||
|
||||
## Usage
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/mattn/go-xmlrpc"
|
||||
"fmt"
|
||||
"log"
|
||||
)
|
||||
|
||||
func main() {
|
||||
res, e := xmlrpc.Call(
|
||||
"http://your-blog.example.com/xmlrpc.php",
|
||||
"metaWeblog.getRecentPosts",
|
||||
"blog-id",
|
||||
"user-id",
|
||||
"password",
|
||||
10)
|
||||
if e != nil {
|
||||
log.Fatal(e)
|
||||
}
|
||||
for _, p := range res.(xmlrpc.Array) {
|
||||
for k, v := range p.(xmlrpc.Struct) {
|
||||
fmt.Printf("%s=%v\n", k, v)
|
||||
}
|
||||
fmt.Println()
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Installation
|
||||
|
||||
```
|
||||
$ go get github.com/mattn/go-xmlrpc
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
MIT
|
||||
|
||||
## Author
|
||||
|
||||
Yasuhiro Matsumoto (a.k.a. mattn)
|
@ -0,0 +1,365 @@
|
||||
package xmlrpc
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/base64"
|
||||
"encoding/xml"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Array []interface{}
|
||||
type Struct map[string]interface{}
|
||||
|
||||
var xmlSpecial = map[byte]string{
|
||||
'<': "<",
|
||||
'>': ">",
|
||||
'"': """,
|
||||
'\'': "'",
|
||||
'&': "&",
|
||||
}
|
||||
|
||||
func xmlEscape(s string) string {
|
||||
var b bytes.Buffer
|
||||
for i := 0; i < len(s); i++ {
|
||||
c := s[i]
|
||||
if s, ok := xmlSpecial[c]; ok {
|
||||
b.WriteString(s)
|
||||
} else {
|
||||
b.WriteByte(c)
|
||||
}
|
||||
}
|
||||
return b.String()
|
||||
}
|
||||
|
||||
type valueNode struct {
|
||||
Type string `xml:"attr"`
|
||||
Body string `xml:"chardata"`
|
||||
}
|
||||
|
||||
func next(p *xml.Decoder) (xml.Name, interface{}, error) {
|
||||
se, e := nextStart(p)
|
||||
if e != nil {
|
||||
return xml.Name{}, nil, e
|
||||
}
|
||||
|
||||
var nv interface{}
|
||||
switch se.Name.Local {
|
||||
case "string":
|
||||
var s string
|
||||
if e = p.DecodeElement(&s, &se); e != nil {
|
||||
return xml.Name{}, nil, e
|
||||
}
|
||||
return xml.Name{}, s, nil
|
||||
case "boolean":
|
||||
var s string
|
||||
if e = p.DecodeElement(&s, &se); e != nil {
|
||||
return xml.Name{}, nil, e
|
||||
}
|
||||
s = strings.TrimSpace(s)
|
||||
var b bool
|
||||
switch s {
|
||||
case "true", "1":
|
||||
b = true
|
||||
case "false", "0":
|
||||
b = false
|
||||
default:
|
||||
e = errors.New("invalid boolean value")
|
||||
}
|
||||
return xml.Name{}, b, e
|
||||
case "int", "i1", "i2", "i4", "i8":
|
||||
var s string
|
||||
var i int
|
||||
if e = p.DecodeElement(&s, &se); e != nil {
|
||||
return xml.Name{}, nil, e
|
||||
}
|
||||
i, e = strconv.Atoi(strings.TrimSpace(s))
|
||||
return xml.Name{}, i, e
|
||||
case "double":
|
||||
var s string
|
||||
var f float64
|
||||
if e = p.DecodeElement(&s, &se); e != nil {
|
||||
return xml.Name{}, nil, e
|
||||
}
|
||||
f, e = strconv.ParseFloat(strings.TrimSpace(s), 64)
|
||||
return xml.Name{}, f, e
|
||||
case "dateTime.iso8601":
|
||||
var s string
|
||||
if e = p.DecodeElement(&s, &se); e != nil {
|
||||
return xml.Name{}, nil, e
|
||||
}
|
||||
t, e := time.Parse("20060102T15:04:05", s)
|
||||
if e != nil {
|
||||
t, e = time.Parse("2006-01-02T15:04:05-07:00", s)
|
||||
if e != nil {
|
||||
t, e = time.Parse("2006-01-02T15:04:05", s)
|
||||
}
|
||||
}
|
||||
return xml.Name{}, t, e
|
||||
case "base64":
|
||||
var s string
|
||||
if e = p.DecodeElement(&s, &se); e != nil {
|
||||
return xml.Name{}, nil, e
|
||||
}
|
||||
if b, e := base64.StdEncoding.DecodeString(s); e != nil {
|
||||
return xml.Name{}, nil, e
|
||||
} else {
|
||||
return xml.Name{}, b, nil
|
||||
}
|
||||
case "member":
|
||||
nextStart(p)
|
||||
return next(p)
|
||||
case "value":
|
||||
nextStart(p)
|
||||
return next(p)
|
||||
case "name":
|
||||
nextStart(p)
|
||||
return next(p)
|
||||
case "struct":
|
||||
st := Struct{}
|
||||
|
||||
se, e = nextStart(p)
|
||||
for e == nil && se.Name.Local == "member" {
|
||||
// name
|
||||
se, e = nextStart(p)
|
||||
if se.Name.Local != "name" {
|
||||
return xml.Name{}, nil, errors.New("invalid response")
|
||||
}
|
||||
if e != nil {
|
||||
break
|
||||
}
|
||||
var name string
|
||||
if e = p.DecodeElement(&name, &se); e != nil {
|
||||
return xml.Name{}, nil, e
|
||||
}
|
||||
se, e = nextStart(p)
|
||||
if e != nil {
|
||||
break
|
||||
}
|
||||
|
||||
// value
|
||||
_, value, e := next(p)
|
||||
if se.Name.Local != "value" {
|
||||
return xml.Name{}, nil, errors.New("invalid response")
|
||||
}
|
||||
if e != nil {
|
||||
break
|
||||
}
|
||||
st[name] = value
|
||||
|
||||
se, e = nextStart(p)
|
||||
if e != nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
return xml.Name{}, st, nil
|
||||
case "array":
|
||||
var ar Array
|
||||
nextStart(p) // data
|
||||
for {
|
||||
nextStart(p) // top of value
|
||||
_, value, e := next(p)
|
||||
if e != nil {
|
||||
break
|
||||
}
|
||||
ar = append(ar, value)
|
||||
}
|
||||
return xml.Name{}, ar, nil
|
||||
case "nil":
|
||||
return xml.Name{}, nil, nil
|
||||
}
|
||||
|
||||
if e = p.DecodeElement(nv, &se); e != nil {
|
||||
return xml.Name{}, nil, e
|
||||
}
|
||||
return se.Name, nv, e
|
||||
}
|
||||
func nextStart(p *xml.Decoder) (xml.StartElement, error) {
|
||||
for {
|
||||
t, e := p.Token()
|
||||
if e != nil {
|
||||
return xml.StartElement{}, e
|
||||
}
|
||||
switch t := t.(type) {
|
||||
case xml.StartElement:
|
||||
return t, nil
|
||||
}
|
||||
}
|
||||
panic("unreachable")
|
||||
}
|
||||
|
||||
func toXml(v interface{}, typ bool) (s string) {
|
||||
if v == nil {
|
||||
return "<nil/>"
|
||||
}
|
||||
r := reflect.ValueOf(v)
|
||||
t := r.Type()
|
||||
k := t.Kind()
|
||||
|
||||
if b, ok := v.([]byte); ok {
|
||||
return "<base64>" + base64.StdEncoding.EncodeToString(b) + "</base64>"
|
||||
}
|
||||
|
||||
switch k {
|
||||
case reflect.Invalid:
|
||||
panic("unsupported type")
|
||||
case reflect.Bool:
|
||||
return fmt.Sprintf("<boolean>%v</boolean>", v)
|
||||
case reflect.Int,
|
||||
reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64,
|
||||
reflect.Uint,
|
||||
reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
|
||||
if typ {
|
||||
return fmt.Sprintf("<int>%v</int>", v)
|
||||
}
|
||||
return fmt.Sprintf("%v", v)
|
||||
case reflect.Uintptr:
|
||||
panic("unsupported type")
|
||||
case reflect.Float32, reflect.Float64:
|
||||
if typ {
|
||||
return fmt.Sprintf("<double>%v</double>", v)
|
||||
}
|
||||
return fmt.Sprintf("%v", v)
|
||||
case reflect.Complex64, reflect.Complex128:
|
||||
panic("unsupported type")
|
||||
case reflect.Array:
|
||||
s = "<array><data>"
|
||||
for n := 0; n < r.Len(); n++ {
|
||||
s += "<value>"
|
||||
s += toXml(r.Index(n).Interface(), typ)
|
||||
s += "</value>"
|
||||
}
|
||||
s += "</data></array>"
|
||||
return s
|
||||
case reflect.Chan:
|
||||
panic("unsupported type")
|
||||
case reflect.Func:
|
||||
panic("unsupported type")
|
||||
case reflect.Interface:
|
||||
return toXml(r.Elem(), typ)
|
||||
case reflect.Map:
|
||||
s = "<struct>"
|
||||
for _, key := range r.MapKeys() {
|
||||
s += "<member>"
|
||||
s += "<name>" + xmlEscape(key.Interface().(string)) + "</name>"
|
||||
s += "<value>" + toXml(r.MapIndex(key).Interface(), typ) + "</value>"
|
||||
s += "</member>"
|
||||
}
|
||||
s += "</struct>"
|
||||
return s
|
||||
case reflect.Ptr:
|
||||
panic("unsupported type")
|
||||
case reflect.Slice:
|
||||
s = "<array><data>"
|
||||
for n := 0; n < r.Len(); n++ {
|
||||
s += "<value>"
|
||||
s += toXml(r.Index(n).Interface(), typ)
|
||||
s += "</value>"
|
||||
}
|
||||
s += "</data></array>"
|
||||
return s
|
||||
case reflect.String:
|
||||
if typ {
|
||||
return fmt.Sprintf("<string>%v</string>", xmlEscape(v.(string)))
|
||||
}
|
||||
return xmlEscape(v.(string))
|
||||
case reflect.Struct:
|
||||
s = "<struct>"
|
||||
for n := 0; n < r.NumField(); n++ {
|
||||
s += "<member>"
|
||||
s += "<name>" + t.Field(n).Name + "</name>"
|
||||
s += "<value>" + toXml(r.FieldByIndex([]int{n}).Interface(), true) + "</value>"
|
||||
s += "</member>"
|
||||
}
|
||||
s += "</struct>"
|
||||
return s
|
||||
case reflect.UnsafePointer:
|
||||
return toXml(r.Elem(), typ)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Client is client of XMLRPC
|
||||
type Client struct {
|
||||
HttpClient *http.Client
|
||||
url string
|
||||
}
|
||||
|
||||
// NewClient create new Client
|
||||
func NewClient(url string) *Client {
|
||||
return &Client{
|
||||
HttpClient: &http.Client{Transport: http.DefaultTransport, Timeout: 10 * time.Second},
|
||||
url: url,
|
||||
}
|
||||
}
|
||||
|
||||
func makeRequest(name string, args ...interface{}) *bytes.Buffer {
|
||||
buf := new(bytes.Buffer)
|
||||
buf.WriteString(`<?xml version="1.0"?><methodCall>`)
|
||||
buf.WriteString("<methodName>" + xmlEscape(name) + "</methodName>")
|
||||
buf.WriteString("<params>")
|
||||
for _, arg := range args {
|
||||
buf.WriteString("<param><value>")
|
||||
buf.WriteString(toXml(arg, true))
|
||||
buf.WriteString("</value></param>")
|
||||
}
|
||||
buf.WriteString("</params></methodCall>")
|
||||
return buf
|
||||
}
|
||||
|
||||
func call(client *http.Client, url, name string, args ...interface{}) (v interface{}, e error) {
|
||||
r, e := httpClient.Post(url, "text/xml", makeRequest(name, args...))
|
||||
if e != nil {
|
||||
return nil, e
|
||||
}
|
||||
|
||||
// Since we do not always read the entire body, discard the rest, which
|
||||
// allows the http transport to reuse the connection.
|
||||
defer io.Copy(ioutil.Discard, r.Body)
|
||||
defer r.Body.Close()
|
||||
|
||||
if r.StatusCode/100 != 2 {
|
||||
return nil, errors.New(http.StatusText(http.StatusBadRequest))
|
||||
}
|
||||
|
||||
p := xml.NewDecoder(r.Body)
|
||||
se, e := nextStart(p) // methodResponse
|
||||
if se.Name.Local != "methodResponse" {
|
||||
return nil, errors.New("invalid response: missing methodResponse")
|
||||
}
|
||||
se, e = nextStart(p) // params
|
||||
if se.Name.Local != "params" {
|
||||
return nil, errors.New("invalid response: missing params")
|
||||
}
|
||||
se, e = nextStart(p) // param
|
||||
if se.Name.Local != "param" {
|
||||
return nil, errors.New("invalid response: missing param")
|
||||
}
|
||||
se, e = nextStart(p) // value
|
||||
if se.Name.Local != "value" {
|
||||
return nil, errors.New("invalid response: missing value")
|
||||
}
|
||||
_, v, e = next(p)
|
||||
return v, e
|
||||
}
|
||||
|
||||
// Call call remote procedures function name with args
|
||||
func (c *Client) Call(name string, args ...interface{}) (v interface{}, e error) {
|
||||
return call(c.HttpClient, c.url, name, args...)
|
||||
}
|
||||
|
||||
// Global httpClient allows us to pool/reuse connections and not wastefully
|
||||
// re-create transports for each request.
|
||||
var httpClient = &http.Client{Transport: http.DefaultTransport, Timeout: 10 * time.Second}
|
||||
|
||||
// Call call remote procedures function name with args
|
||||
func Call(url, name string, args ...interface{}) (v interface{}, e error) {
|
||||
return call(httpClient, url, name, args...)
|
||||
}
|
Loading…
Reference in new issue