* ci: shard server Postgres tests into 4 parallel runners (#35739) * ci: add test sharding plumbing to server CI Add infrastructure for upcoming test sharding without changing behavior: - Add shard-index and shard-total inputs to server-test-template.yml (defaults preserve existing single-runner behavior) - Add timing cache restore step (activates only when shard-total > 1) - Add merge-postgres-test-results job to server-ci.yml that: - Merges JUnit XML reports from shard artifacts - Saves timing data cache for future shard balancing - Handles both single-artifact and multi-shard scenarios - Add .gitignore entries for timing cache and shard work files Co-authored-by: Claude <claude@anthropic.com> * ci: shard server Postgres tests into 4 parallel runners Extract sharding logic into standalone, tested scripts and enable 4-shard parallel test execution for server Postgres CI: Scripts: - server/scripts/shard-split.js: Node.js bin-packing solver that assigns test packages to shards using timing data from previous runs. Two-tier strategy: light packages (<2min) whole, heavy packages (api4, app) split at individual test level. - server/scripts/run-shard-tests.sh: Multi-run wrapper that calls gotestsum directly for each package group with -run regex filters. - server/scripts/shard-split.test.js: 8 test cases covering round-robin fallback, timing-based balancing, heavy package splitting, JUnit XML fallback, and enterprise package separation. Workflow changes: - server-test-template.yml: Add shard splitting step that discovers test packages and runs the solver. Modified Run Tests step to use wrapper script when sharding is active. - server-ci.yml: Add 4-shard matrix to test-postgres-normal. Update merge job artifact patterns for shard-specific names. Performance: 7.2 min with timing cache vs 62.5 min baseline = 88% wall-time improvement. First run without cache uses JUnit XML fallback or round-robin, then populates the cache for subsequent runs. Co-authored-by: Claude <claude@anthropic.com> * fix: raise heavy package threshold to 5 min to preserve test isolation sqlstore integrity tests scan the entire database and fail when other packages' test data is present. At 182s, sqlstore was just over the 120s threshold and getting split at test level. Raising to 300s keeps only api4 (~38 min) and app (~15 min) as heavy — where the real sharding gains are — while sqlstore, elasticsearch, etc. stay whole and maintain their test isolation guarantees. Co-authored-by: Claude <claude@anthropic.com> * ci: only save test timing cache on default branch PR branches always restore from master's timing cache via restore-keys prefix matching. Timing data is stable day-to-day so this eliminates cache misses on first PR runs and reduces cache storage. Co-authored-by: Claude <claude@anthropic.com> * ci: skip FIPS tests on PRs (enterprise CI handles compile check) Per review feedback: the enterprise CI already runs a FIPS compile check on every PR. Running the full FIPS test suite on PRs is redundant since it uses the identical test suite as non-FIPS — the only FIPS-specific failure mode is a build failure from non-approved crypto imports, which the enterprise compile check catches. Full FIPS tests continue to run on every push to master. Co-authored-by: Claude <claude@anthropic.com> * fix: address review feedback on run-shard-tests.sh - Remove set -e so all test runs execute even if earlier ones fail; track failures and exit with error at the end (wiggin77) - Remove unused top-level COVERAGE_FLAG variable (wiggin77) - Fix RUN_IDX increment position so report, json, and coverage files share the same index (wiggin77) - Update workflow comment: heavy threshold is 5 min, not 2 min (wiggin77) Co-authored-by: Claude <claude@anthropic.com> * style: use node: prefix for built-in fs module in shard-split.js Co-authored-by: Claude <claude@anthropic.com> * fix: avoid interpolating file paths into generated shell script Read shard package lists from files at runtime instead of interpolating them into the generated script via printf. This prevents theoretical shell metacharacter injection from directory names, as flagged by DryRun Security. Co-authored-by: Claude <claude@anthropic.com> * fix(ci): rename merged artifact to match server-ci-report glob The merged artifact was named postgres-server-test-logs-merged which does not match the *-test-logs pattern in server-ci-report.yml, causing Postgres test results to be missing from PR/commit reports. Also pins junit-report-merger to exact version 7.0.0 for supply chain safety. Co-authored-by: Claude <claude@anthropic.com> * fix(ci): pass RACE_MODE env into Docker container RACE_MODE was set on the host runner but never included in the docker run --env list. The light-package path worked because the heredoc expanded on the host, but run-shard-tests.sh reads RACE_MODE at runtime inside the container where it was unset. This caused heavy packages (api4, app) to silently lose -race detection. Co-authored-by: Claude <claude@anthropic.com> * fix(ci): discover new tests in heavy packages not in timing cache Tests not present in the timing cache (newly added or renamed) would not appear in any shard -run regex, causing them to silently skip. After building items from the cache, run go test -list to discover current test names and assign any cache-missing tests to shards via the normal bin-packing algorithm with a small default duration. Co-authored-by: Claude <claude@anthropic.com> * fix(ci): add missing line continuation backslash in docker run The previous --env FIPS_ENABLED line was missing a trailing backslash after adding --env RACE_MODE, causing docker run to see a truncated command and fail with "requires at least 1 argument". Co-authored-by: Claude <claude@anthropic.com> * fix(ci): add setup-go step for shard test discovery go test -list in shard-split.js runs on the host runner via execSync, but Go is only available inside the Docker container. Without this step, every invocation fails silently and new-test discovery is a no-op. Adding actions/setup-go before the shard split step ensures the Go toolchain is available on the host. Co-authored-by: Claude <claude@anthropic.com> --------- Co-authored-by: Claude <claude@anthropic.com> * updated go to version 1.25.8 (#35817) * updated go to version 1.25.8 * updated gotestsum version to work with go 1.25.8 go 1.25 does not work with indirect tools 0.11 dependency pulled by gotestsum. * Use sync.WaitGroup.Go to simplify goroutine creation Replace the wg.Add(1) + go func() { defer wg.Done() }() pattern with wg.Go(), which was introduced in Go 1.25. * pushes fips image on workflow dispatch to allow fips test to run on go version update * fix new requirements for FIPS compliance imposed on updating to go 1.25.8 * updates openssl symbol check for library shipped with FIPS new versions go-openssl v2 shipped with FIPS versions starting from 1.25 uses mkcgo to generate bindings causing symbol names to be different. * removes temp workflow-dispatch condition * keep versions out of agents md file * upgrade golangci-lint (#35845) * test: clean up channel store data after TestChannelStore (#36066) TestChannelStore sub-tests create channels, members, and team members using fake TeamIds and UserIds (model.NewId() for non-existent rows). These records are left in the database and cause integrity tests (TestCheck*) running in the same binary to fail their full-table scans. Register a t.Cleanup on TestChannelStore that purges the affected tables entirely. A blanket purge is safe: the schema enforces no FK constraints, and every test suite creates its own data independently. * Fix command injection in server-test-template workflow (#36080) Replace the unquoted heredoc (which embedded GITHUB_HEAD_REF into a generated script) with a cp of the existing run-shard-tests.sh, which already handles the light-only case. Pass BUILD_NUMBER and TEST_TARGET as explicit docker env vars instead of interpolating them into script content. * fix(ci): restore testname format in sharded gotestsum runs (#36078) run-shard-tests.sh called gotestsum directly without --format, so it fell back to gotestsum's default (pkgname) instead of the testname format set by the Makefile. Pass --format "${GOTESTSUM_FORMAT:-testname}" to match the Makefile default. Co-authored-by: Mattermost Build <build@mattermost.com> * fix(lint): fix pre-existing golangci-lint v2.11.4 issues Fix misspelling in comment and redundant nil check flagged by the upgraded linter. * ci: use golang image for test runner on release-10.11 mattermost-build-server images are not built for release branches. Use the official golang image which is always available for any Go version. * ci: use mattermost/mattermost-build-server for release-10.11 The mattermostdevelopment/ images are only built for master. The production mattermost/ images are built for release branches. * ci: use mattermost/mattermost-build-server in mmctl test template The mattermostdevelopment/ images are only built for master. The production mattermost/ images are built for release branches. --------- Co-authored-by: Pavel Zeman <pavel.zeman@mattermost.com> Co-authored-by: Claude <claude@anthropic.com> Co-authored-by: Carlos Garcia <carlos.garcia@mattermost.com> Co-authored-by: Mattermost Build <build@mattermost.com>
1146 строки
32 KiB
Go
1146 строки
32 KiB
Go
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
// See LICENSE.txt for license information.
|
|
|
|
//go:generate go run interface_generator/main.go
|
|
|
|
package plugin
|
|
|
|
import (
|
|
"bytes"
|
|
"database/sql"
|
|
"database/sql/driver"
|
|
"encoding/gob"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"log"
|
|
"net/http"
|
|
"net/rpc"
|
|
"net/url"
|
|
"os"
|
|
"reflect"
|
|
"sync"
|
|
|
|
"github.com/go-sql-driver/mysql"
|
|
"github.com/hashicorp/go-plugin"
|
|
"github.com/lib/pq"
|
|
|
|
"github.com/mattermost/mattermost/server/public/model"
|
|
"github.com/mattermost/mattermost/server/public/shared/mlog"
|
|
)
|
|
|
|
var hookNameToId = make(map[string]int)
|
|
|
|
type hooksRPCClient struct {
|
|
client *rpc.Client
|
|
log *mlog.Logger
|
|
muxBroker *plugin.MuxBroker
|
|
apiImpl API
|
|
driver Driver
|
|
implemented [TotalHooksID]bool
|
|
doneWg sync.WaitGroup
|
|
}
|
|
|
|
type hooksRPCServer struct {
|
|
impl any
|
|
muxBroker *plugin.MuxBroker
|
|
apiRPCClient *apiRPCClient
|
|
}
|
|
|
|
// Implements hashicorp/go-plugin/plugin.Plugin interface to connect the hooks of a plugin
|
|
type hooksPlugin struct {
|
|
hooks any
|
|
apiImpl API
|
|
driverImpl Driver
|
|
log *mlog.Logger
|
|
}
|
|
|
|
func (p *hooksPlugin) Server(b *plugin.MuxBroker) (any, error) {
|
|
return &hooksRPCServer{impl: p.hooks, muxBroker: b}, nil
|
|
}
|
|
|
|
func (p *hooksPlugin) Client(b *plugin.MuxBroker, client *rpc.Client) (any, error) {
|
|
return &hooksRPCClient{
|
|
client: client,
|
|
log: p.log,
|
|
muxBroker: b,
|
|
apiImpl: p.apiImpl,
|
|
driver: p.driverImpl,
|
|
}, nil
|
|
}
|
|
|
|
type apiRPCClient struct {
|
|
client *rpc.Client
|
|
muxBroker *plugin.MuxBroker
|
|
}
|
|
|
|
type apiRPCServer struct {
|
|
impl API
|
|
muxBroker *plugin.MuxBroker
|
|
}
|
|
|
|
// ErrorString is a fallback for sending unregistered implementations of the error interface across
|
|
// rpc. For example, the errorString type from the github.com/pkg/errors package cannot be
|
|
// registered since it is not exported, but this precludes common error handling paradigms.
|
|
// ErrorString merely preserves the string description of the error, while satisfying the error
|
|
// interface itself to allow other registered types (such as model.AppError) to be sent unmodified.
|
|
type ErrorString struct {
|
|
Code int // Code to map to various error variables
|
|
Err string
|
|
}
|
|
|
|
func (e ErrorString) Error() string {
|
|
return e.Err
|
|
}
|
|
|
|
func encodableError(err error) error {
|
|
if err == nil {
|
|
return nil
|
|
}
|
|
if _, ok := err.(*model.AppError); ok {
|
|
return err
|
|
}
|
|
|
|
if _, ok := err.(*pq.Error); ok {
|
|
return err
|
|
}
|
|
|
|
if _, ok := err.(*mysql.MySQLError); ok {
|
|
return err
|
|
}
|
|
|
|
ret := &ErrorString{
|
|
Err: err.Error(),
|
|
}
|
|
|
|
switch err {
|
|
case io.EOF:
|
|
ret.Code = 1
|
|
case sql.ErrNoRows:
|
|
ret.Code = 2
|
|
case sql.ErrConnDone:
|
|
ret.Code = 3
|
|
case sql.ErrTxDone:
|
|
ret.Code = 4
|
|
case driver.ErrSkip:
|
|
ret.Code = 5
|
|
case driver.ErrBadConn:
|
|
ret.Code = 6
|
|
case driver.ErrRemoveArgument:
|
|
ret.Code = 7
|
|
}
|
|
|
|
return ret
|
|
}
|
|
|
|
func decodableError(err error) error {
|
|
if encErr, ok := err.(*ErrorString); ok {
|
|
switch encErr.Code {
|
|
case 1:
|
|
return io.EOF
|
|
case 2:
|
|
return sql.ErrNoRows
|
|
case 3:
|
|
return sql.ErrConnDone
|
|
case 4:
|
|
return sql.ErrTxDone
|
|
case 5:
|
|
return driver.ErrSkip
|
|
case 6:
|
|
return driver.ErrBadConn
|
|
case 7:
|
|
return driver.ErrRemoveArgument
|
|
}
|
|
}
|
|
return err
|
|
}
|
|
|
|
// Registering some types used by MM for encoding/gob used by rpc
|
|
func init() {
|
|
gob.Register([]*model.SlackAttachment{})
|
|
gob.Register([]any{})
|
|
gob.Register(map[string]any{})
|
|
gob.Register(&model.AppError{})
|
|
gob.Register(&pq.Error{})
|
|
gob.Register(&mysql.MySQLError{})
|
|
gob.Register(&ErrorString{})
|
|
gob.Register(&model.AutocompleteDynamicListArg{})
|
|
gob.Register(&model.AutocompleteStaticListArg{})
|
|
gob.Register(&model.AutocompleteTextArg{})
|
|
gob.Register(&model.PreviewPost{})
|
|
gob.Register(model.PropertyOptions[*model.PluginPropertyOption]{})
|
|
}
|
|
|
|
// These enforce compile time checks to make sure types implement the interface
|
|
// If you are getting an error here, you probably need to run `make pluginapi` to
|
|
// autogenerate RPC glue code
|
|
var (
|
|
_ plugin.Plugin = &hooksPlugin{}
|
|
_ Hooks = &hooksRPCClient{}
|
|
)
|
|
|
|
//
|
|
// Below are special cases for hooks or APIs that can not be auto generated
|
|
//
|
|
|
|
func (g *hooksRPCClient) Implemented() (impl []string, err error) {
|
|
err = g.client.Call("Plugin.Implemented", struct{}{}, &impl)
|
|
for _, hookName := range impl {
|
|
if hookId, ok := hookNameToId[hookName]; ok {
|
|
g.implemented[hookId] = true
|
|
}
|
|
}
|
|
return
|
|
}
|
|
|
|
// Implemented replies with the names of the hooks that are implemented.
|
|
func (s *hooksRPCServer) Implemented(args struct{}, reply *[]string) error {
|
|
ifaceType := reflect.TypeOf((*Hooks)(nil)).Elem()
|
|
implType := reflect.TypeOf(s.impl)
|
|
selfType := reflect.TypeOf(s)
|
|
var methods []string
|
|
for i := 0; i < ifaceType.NumMethod(); i++ {
|
|
method := ifaceType.Method(i)
|
|
m, ok := implType.MethodByName(method.Name)
|
|
if !ok {
|
|
continue
|
|
} else if m.Type.NumIn() != method.Type.NumIn()+1 {
|
|
continue
|
|
} else if m.Type.NumOut() != method.Type.NumOut() {
|
|
continue
|
|
}
|
|
match := true
|
|
for j := 0; j < method.Type.NumIn(); j++ {
|
|
if m.Type.In(j+1) != method.Type.In(j) {
|
|
match = false
|
|
break
|
|
}
|
|
}
|
|
for j := 0; j < method.Type.NumOut(); j++ {
|
|
if m.Type.Out(j) != method.Type.Out(j) {
|
|
match = false
|
|
break
|
|
}
|
|
}
|
|
if !match {
|
|
continue
|
|
}
|
|
if _, ok := selfType.MethodByName(method.Name); !ok {
|
|
continue
|
|
}
|
|
methods = append(methods, method.Name)
|
|
}
|
|
*reply = methods
|
|
return encodableError(nil)
|
|
}
|
|
|
|
type Z_OnActivateArgs struct {
|
|
APIMuxId uint32
|
|
DriverMuxId uint32
|
|
}
|
|
|
|
type Z_OnActivateReturns struct {
|
|
A error
|
|
}
|
|
|
|
func (g *hooksRPCClient) OnActivate() error {
|
|
muxId := g.muxBroker.NextId()
|
|
g.doneWg.Go(func() {
|
|
g.muxBroker.AcceptAndServe(muxId, &apiRPCServer{
|
|
impl: g.apiImpl,
|
|
muxBroker: g.muxBroker,
|
|
})
|
|
})
|
|
|
|
nextID := g.muxBroker.NextId()
|
|
g.doneWg.Go(func() {
|
|
g.muxBroker.AcceptAndServe(nextID, &dbRPCServer{
|
|
dbImpl: g.driver,
|
|
})
|
|
})
|
|
|
|
_args := &Z_OnActivateArgs{
|
|
APIMuxId: muxId,
|
|
DriverMuxId: nextID,
|
|
}
|
|
_returns := &Z_OnActivateReturns{}
|
|
|
|
if err := g.client.Call("Plugin.OnActivate", _args, _returns); err != nil {
|
|
g.log.Error("RPC call to OnActivate plugin failed.", mlog.Err(err))
|
|
}
|
|
return _returns.A
|
|
}
|
|
|
|
func (s *hooksRPCServer) OnActivate(args *Z_OnActivateArgs, returns *Z_OnActivateReturns) error {
|
|
connection, err := s.muxBroker.Dial(args.APIMuxId)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
conn2, err := s.muxBroker.Dial(args.DriverMuxId)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
s.apiRPCClient = &apiRPCClient{
|
|
client: rpc.NewClient(connection),
|
|
muxBroker: s.muxBroker,
|
|
}
|
|
|
|
dbClient := &dbRPCClient{
|
|
client: rpc.NewClient(conn2),
|
|
}
|
|
|
|
if mmplugin, ok := s.impl.(interface {
|
|
SetAPI(api API)
|
|
SetDriver(driver Driver)
|
|
}); ok {
|
|
mmplugin.SetAPI(s.apiRPCClient)
|
|
mmplugin.SetDriver(dbClient)
|
|
}
|
|
|
|
if mmplugin, ok := s.impl.(interface {
|
|
OnConfigurationChange() error
|
|
}); ok {
|
|
if err := mmplugin.OnConfigurationChange(); err != nil {
|
|
fmt.Fprintf(os.Stderr, "[ERROR] call to OnConfigurationChange failed, error: %v", err.Error())
|
|
}
|
|
}
|
|
|
|
// Capture output of standard logger because go-plugin
|
|
// redirects it.
|
|
log.SetOutput(os.Stderr)
|
|
|
|
if hook, ok := s.impl.(interface {
|
|
OnActivate() error
|
|
}); ok {
|
|
returns.A = encodableError(hook.OnActivate())
|
|
}
|
|
return nil
|
|
}
|
|
|
|
type Z_LoadPluginConfigurationArgsArgs struct{}
|
|
|
|
type Z_LoadPluginConfigurationArgsReturns struct {
|
|
A []byte
|
|
}
|
|
|
|
func (g *apiRPCClient) LoadPluginConfiguration(dest any) error {
|
|
_args := &Z_LoadPluginConfigurationArgsArgs{}
|
|
_returns := &Z_LoadPluginConfigurationArgsReturns{}
|
|
if err := g.client.Call("Plugin.LoadPluginConfiguration", _args, _returns); err != nil {
|
|
log.Printf("RPC call to LoadPluginConfiguration API failed: %s", err.Error())
|
|
}
|
|
if err := json.Unmarshal(_returns.A, dest); err != nil {
|
|
log.Printf("LoadPluginConfiguration API failed to unmarshal: %s", err.Error())
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (s *apiRPCServer) LoadPluginConfiguration(args *Z_LoadPluginConfigurationArgsArgs, returns *Z_LoadPluginConfigurationArgsReturns) error {
|
|
var config any
|
|
if hook, ok := s.impl.(interface {
|
|
LoadPluginConfiguration(dest any) error
|
|
}); ok {
|
|
if err := hook.LoadPluginConfiguration(&config); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
b, err := json.Marshal(config)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
returns.A = b
|
|
return nil
|
|
}
|
|
|
|
func init() {
|
|
hookNameToId["ServeHTTP"] = ServeHTTPID
|
|
}
|
|
|
|
// Using a subset of http.Request prevents a known incompatibility when decoding Go v1.23+ gob-encoded x509.Certificate
|
|
// structs from Go v1.22 compiled plugins. These come from http.Request.TLS field (*tls.ConnectionState).
|
|
type HTTPRequestSubset struct {
|
|
Method string
|
|
URL *url.URL
|
|
Proto string
|
|
ProtoMajor int
|
|
ProtoMinor int
|
|
Header http.Header
|
|
Host string
|
|
RemoteAddr string
|
|
RequestURI string
|
|
Body io.ReadCloser
|
|
}
|
|
|
|
func (r *HTTPRequestSubset) GetHTTPRequest() *http.Request {
|
|
return &http.Request{
|
|
Method: r.Method,
|
|
URL: r.URL,
|
|
Proto: r.Proto,
|
|
ProtoMajor: r.ProtoMajor,
|
|
ProtoMinor: r.ProtoMinor,
|
|
Header: r.Header,
|
|
Host: r.Host,
|
|
RemoteAddr: r.RemoteAddr,
|
|
RequestURI: r.RequestURI,
|
|
Body: r.Body,
|
|
}
|
|
}
|
|
|
|
type Z_ServeHTTPArgs struct {
|
|
ResponseWriterStream uint32
|
|
Request *HTTPRequestSubset
|
|
Context *Context
|
|
RequestBodyStream uint32
|
|
}
|
|
|
|
func (g *hooksRPCClient) ServeHTTP(c *Context, w http.ResponseWriter, r *http.Request) {
|
|
if !g.implemented[ServeHTTPID] {
|
|
http.NotFound(w, r)
|
|
return
|
|
}
|
|
|
|
serveHTTPStreamId := g.muxBroker.NextId()
|
|
go func() {
|
|
connection, err := g.muxBroker.Accept(serveHTTPStreamId)
|
|
if err != nil {
|
|
g.log.Error("Plugin failed to ServeHTTP, muxBroker couldn't accept connection", mlog.Uint("serve_http_stream_id", serveHTTPStreamId), mlog.Err(err))
|
|
return
|
|
}
|
|
defer connection.Close()
|
|
|
|
rpcServer := rpc.NewServer()
|
|
if err := rpcServer.RegisterName("Plugin", &httpResponseWriterRPCServer{w: w, log: g.log}); err != nil {
|
|
g.log.Error("Plugin failed to ServeHTTP, couldn't register RPC name", mlog.Err(err))
|
|
return
|
|
}
|
|
rpcServer.ServeConn(connection)
|
|
}()
|
|
|
|
requestBodyStreamId := uint32(0)
|
|
if r.Body != nil {
|
|
requestBodyStreamId = g.muxBroker.NextId()
|
|
go func() {
|
|
bodyConnection, err := g.muxBroker.Accept(requestBodyStreamId)
|
|
if err != nil {
|
|
g.log.Error("Plugin failed to ServeHTTP, muxBroker couldn't Accept request body connection", mlog.Err(err))
|
|
return
|
|
}
|
|
defer bodyConnection.Close()
|
|
serveIOReader(r.Body, bodyConnection)
|
|
}()
|
|
}
|
|
|
|
forwardedRequest := &HTTPRequestSubset{
|
|
Method: r.Method,
|
|
URL: r.URL,
|
|
Proto: r.Proto,
|
|
ProtoMajor: r.ProtoMajor,
|
|
ProtoMinor: r.ProtoMinor,
|
|
Header: r.Header,
|
|
Host: r.Host,
|
|
RemoteAddr: r.RemoteAddr,
|
|
RequestURI: r.RequestURI,
|
|
}
|
|
|
|
if err := g.client.Call("Plugin.ServeHTTP", Z_ServeHTTPArgs{
|
|
Context: c,
|
|
ResponseWriterStream: serveHTTPStreamId,
|
|
Request: forwardedRequest,
|
|
RequestBodyStream: requestBodyStreamId,
|
|
}, nil); err != nil {
|
|
g.log.Error("Plugin failed to ServeHTTP, RPC call failed", mlog.Err(err))
|
|
http.Error(w, "500 internal server error", http.StatusInternalServerError)
|
|
}
|
|
}
|
|
|
|
func (s *hooksRPCServer) ServeHTTP(args *Z_ServeHTTPArgs, returns *struct{}) error {
|
|
connection, err := s.muxBroker.Dial(args.ResponseWriterStream)
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "[ERROR] Can't connect to remote response writer stream, error: %v", err.Error())
|
|
return err
|
|
}
|
|
w := connectHTTPResponseWriter(connection)
|
|
defer w.Close()
|
|
|
|
r := args.Request
|
|
if args.RequestBodyStream != 0 {
|
|
connection, err := s.muxBroker.Dial(args.RequestBodyStream)
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "[ERROR] Can't connect to remote request body stream, error: %v", err.Error())
|
|
return err
|
|
}
|
|
r.Body = connectIOReader(connection)
|
|
} else {
|
|
r.Body = io.NopCloser(&bytes.Buffer{})
|
|
}
|
|
defer r.Body.Close()
|
|
|
|
httpReq := r.GetHTTPRequest()
|
|
|
|
if hook, ok := s.impl.(interface {
|
|
ServeHTTP(c *Context, w http.ResponseWriter, r *http.Request)
|
|
}); ok {
|
|
hook.ServeHTTP(args.Context, w, httpReq)
|
|
} else {
|
|
http.NotFound(w, httpReq)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
type Z_PluginHTTPArgs struct {
|
|
Request *HTTPRequestSubset
|
|
RequestBody []byte
|
|
}
|
|
|
|
type Z_PluginHTTPReturns struct {
|
|
Response *http.Response
|
|
ResponseBody []byte
|
|
}
|
|
|
|
func (g *apiRPCClient) PluginHTTP(request *http.Request) *http.Response {
|
|
forwardedRequest := &HTTPRequestSubset{
|
|
Method: request.Method,
|
|
URL: request.URL,
|
|
Proto: request.Proto,
|
|
ProtoMajor: request.ProtoMajor,
|
|
ProtoMinor: request.ProtoMinor,
|
|
Header: request.Header,
|
|
Host: request.Host,
|
|
RemoteAddr: request.RemoteAddr,
|
|
RequestURI: request.RequestURI,
|
|
}
|
|
|
|
_args := &Z_PluginHTTPArgs{
|
|
Request: forwardedRequest,
|
|
}
|
|
|
|
if request.Body != nil {
|
|
requestBody, err := io.ReadAll(request.Body)
|
|
if err != nil {
|
|
log.Printf("RPC call to PluginHTTP API failed: %s", err.Error())
|
|
return nil
|
|
}
|
|
request.Body.Close()
|
|
request.Body = nil
|
|
|
|
_args.RequestBody = requestBody
|
|
}
|
|
|
|
_returns := &Z_PluginHTTPReturns{}
|
|
if err := g.client.Call("Plugin.PluginHTTP", _args, _returns); err != nil {
|
|
log.Printf("RPC call to PluginHTTP API failed: %s", err.Error())
|
|
return nil
|
|
}
|
|
|
|
_returns.Response.Body = io.NopCloser(bytes.NewBuffer(_returns.ResponseBody))
|
|
|
|
return _returns.Response
|
|
}
|
|
|
|
func (s *apiRPCServer) PluginHTTP(args *Z_PluginHTTPArgs, returns *Z_PluginHTTPReturns) error {
|
|
args.Request.Body = io.NopCloser(bytes.NewBuffer(args.RequestBody))
|
|
|
|
if hook, ok := s.impl.(interface {
|
|
PluginHTTP(request *http.Request) *http.Response
|
|
}); ok {
|
|
response := hook.PluginHTTP(args.Request.GetHTTPRequest())
|
|
|
|
responseBody, err := io.ReadAll(response.Body)
|
|
if err != nil {
|
|
return encodableError(fmt.Errorf("RPC call to PluginHTTP API failed: %s", err.Error()))
|
|
}
|
|
response.Body.Close()
|
|
response.Body = nil
|
|
|
|
returns.Response = response
|
|
returns.ResponseBody = responseBody
|
|
} else {
|
|
return encodableError(fmt.Errorf("API PluginHTTP called but not implemented"))
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func init() {
|
|
hookNameToId["FileWillBeUploaded"] = FileWillBeUploadedID
|
|
}
|
|
|
|
type Z_FileWillBeUploadedArgs struct {
|
|
A *Context
|
|
B *model.FileInfo
|
|
UploadedFileStream uint32
|
|
ReplacementFileStream uint32
|
|
}
|
|
|
|
type Z_FileWillBeUploadedReturns struct {
|
|
A *model.FileInfo
|
|
B string
|
|
}
|
|
|
|
func (g *hooksRPCClient) FileWillBeUploaded(c *Context, info *model.FileInfo, file io.Reader, output io.Writer) (*model.FileInfo, string) {
|
|
if !g.implemented[FileWillBeUploadedID] {
|
|
return info, ""
|
|
}
|
|
|
|
uploadedFileStreamId := g.muxBroker.NextId()
|
|
go func() {
|
|
uploadedFileConnection, err := g.muxBroker.Accept(uploadedFileStreamId)
|
|
if err != nil {
|
|
g.log.Error("Plugin failed to serve upload file stream. MuxBroker could not Accept connection", mlog.Err(err))
|
|
return
|
|
}
|
|
defer uploadedFileConnection.Close()
|
|
serveIOReader(file, uploadedFileConnection)
|
|
}()
|
|
|
|
replacementDone := make(chan bool)
|
|
replacementFileStreamId := g.muxBroker.NextId()
|
|
go func() {
|
|
defer close(replacementDone)
|
|
|
|
replacementFileConnection, err := g.muxBroker.Accept(replacementFileStreamId)
|
|
if err != nil {
|
|
g.log.Error("Plugin failed to serve replacement file stream. MuxBroker could not Accept connection", mlog.Err(err))
|
|
return
|
|
}
|
|
defer replacementFileConnection.Close()
|
|
if _, err := io.Copy(output, replacementFileConnection); err != nil {
|
|
g.log.Error("Error reading replacement file.", mlog.Err(err))
|
|
}
|
|
}()
|
|
|
|
_args := &Z_FileWillBeUploadedArgs{c, info, uploadedFileStreamId, replacementFileStreamId}
|
|
_returns := &Z_FileWillBeUploadedReturns{A: _args.B}
|
|
if err := g.client.Call("Plugin.FileWillBeUploaded", _args, _returns); err != nil {
|
|
g.log.Error("RPC call FileWillBeUploaded to plugin failed.", mlog.Err(err))
|
|
}
|
|
|
|
// Ensure the io.Copy from the replacementFileConnection above completes.
|
|
<-replacementDone
|
|
|
|
return _returns.A, _returns.B
|
|
}
|
|
|
|
func (s *hooksRPCServer) FileWillBeUploaded(args *Z_FileWillBeUploadedArgs, returns *Z_FileWillBeUploadedReturns) error {
|
|
uploadFileConnection, err := s.muxBroker.Dial(args.UploadedFileStream)
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "[ERROR] Can't connect to remote upload file stream, error: %v", err.Error())
|
|
return err
|
|
}
|
|
defer uploadFileConnection.Close()
|
|
fileReader := connectIOReader(uploadFileConnection)
|
|
defer fileReader.Close()
|
|
|
|
replacementFileConnection, err := s.muxBroker.Dial(args.ReplacementFileStream)
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "[ERROR] Can't connect to remote replacement file stream, error: %v", err.Error())
|
|
return err
|
|
}
|
|
defer replacementFileConnection.Close()
|
|
returnFileWriter := replacementFileConnection
|
|
|
|
if hook, ok := s.impl.(interface {
|
|
FileWillBeUploaded(c *Context, info *model.FileInfo, file io.Reader, output io.Writer) (*model.FileInfo, string)
|
|
}); ok {
|
|
returns.A, returns.B = hook.FileWillBeUploaded(args.A, args.B, fileReader, returnFileWriter)
|
|
} else {
|
|
return fmt.Errorf("hook FileWillBeUploaded called but not implemented")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// MessageWillBePosted is in this file because of the difficulty of identifying which fields need special behaviour.
|
|
// The special behaviour needed is decoding the returned post into the original one to avoid the unintentional removal
|
|
// of fields by older plugins.
|
|
func init() {
|
|
hookNameToId["MessageWillBePosted"] = MessageWillBePostedID
|
|
}
|
|
|
|
type Z_MessageWillBePostedArgs struct {
|
|
A *Context
|
|
B *model.Post
|
|
}
|
|
|
|
type Z_MessageWillBePostedReturns struct {
|
|
A *model.Post
|
|
B string
|
|
}
|
|
|
|
func (g *hooksRPCClient) MessageWillBePosted(c *Context, post *model.Post) (*model.Post, string) {
|
|
_args := &Z_MessageWillBePostedArgs{c, post}
|
|
_returns := &Z_MessageWillBePostedReturns{A: _args.B}
|
|
if g.implemented[MessageWillBePostedID] {
|
|
if err := g.client.Call("Plugin.MessageWillBePosted", _args, _returns); err != nil {
|
|
g.log.Error("RPC call MessageWillBePosted to plugin failed.", mlog.Err(err))
|
|
}
|
|
}
|
|
return _returns.A, _returns.B
|
|
}
|
|
|
|
func (s *hooksRPCServer) MessageWillBePosted(args *Z_MessageWillBePostedArgs, returns *Z_MessageWillBePostedReturns) error {
|
|
if hook, ok := s.impl.(interface {
|
|
MessageWillBePosted(c *Context, post *model.Post) (*model.Post, string)
|
|
}); ok {
|
|
returns.A, returns.B = hook.MessageWillBePosted(args.A, args.B)
|
|
} else {
|
|
return encodableError(fmt.Errorf("hook MessageWillBePosted called but not implemented"))
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// MessageWillBeUpdated is in this file because of the difficulty of identifying which fields need special behaviour.
|
|
// The special behaviour needed is decoding the returned post into the original one to avoid the unintentional removal
|
|
// of fields by older plugins.
|
|
func init() {
|
|
hookNameToId["MessageWillBeUpdated"] = MessageWillBeUpdatedID
|
|
}
|
|
|
|
type Z_MessageWillBeUpdatedArgs struct {
|
|
A *Context
|
|
B *model.Post
|
|
C *model.Post
|
|
}
|
|
|
|
type Z_MessageWillBeUpdatedReturns struct {
|
|
A *model.Post
|
|
B string
|
|
}
|
|
|
|
func (g *hooksRPCClient) MessageWillBeUpdated(c *Context, newPost, oldPost *model.Post) (*model.Post, string) {
|
|
_args := &Z_MessageWillBeUpdatedArgs{c, newPost, oldPost}
|
|
_default_returns := &Z_MessageWillBeUpdatedReturns{A: _args.B}
|
|
if g.implemented[MessageWillBeUpdatedID] {
|
|
_returns := &Z_MessageWillBeUpdatedReturns{}
|
|
if err := g.client.Call("Plugin.MessageWillBeUpdated", _args, _returns); err != nil {
|
|
g.log.Error("RPC call MessageWillBeUpdated to plugin failed.", mlog.Err(err))
|
|
return _default_returns.A, _default_returns.B
|
|
}
|
|
return _returns.A, _returns.B
|
|
}
|
|
return _default_returns.A, _default_returns.B
|
|
}
|
|
|
|
func (s *hooksRPCServer) MessageWillBeUpdated(args *Z_MessageWillBeUpdatedArgs, returns *Z_MessageWillBeUpdatedReturns) error {
|
|
if hook, ok := s.impl.(interface {
|
|
MessageWillBeUpdated(c *Context, newPost, oldPost *model.Post) (*model.Post, string)
|
|
}); ok {
|
|
returns.A, returns.B = hook.MessageWillBeUpdated(args.A, args.B, args.C)
|
|
} else {
|
|
return encodableError(fmt.Errorf("hook MessageWillBeUpdated called but not implemented"))
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// MessagesWillBeConsumed is in this file because of the difficulty of identifying which fields need special behaviour.
|
|
// The special behaviour needed is decoding the returned post into the original one to avoid the unintentional removal
|
|
// of fields by older plugins.
|
|
func init() {
|
|
hookNameToId["MessagesWillBeConsumed"] = MessagesWillBeConsumedID
|
|
}
|
|
|
|
type Z_MessagesWillBeConsumedArgs struct {
|
|
A []*model.Post
|
|
}
|
|
|
|
type Z_MessagesWillBeConsumedReturns struct {
|
|
A []*model.Post
|
|
}
|
|
|
|
func (g *hooksRPCClient) MessagesWillBeConsumed(posts []*model.Post) []*model.Post {
|
|
_args := &Z_MessagesWillBeConsumedArgs{posts}
|
|
_returns := &Z_MessagesWillBeConsumedReturns{}
|
|
if g.implemented[MessagesWillBeConsumedID] {
|
|
if err := g.client.Call("Plugin.MessagesWillBeConsumed", _args, _returns); err != nil {
|
|
g.log.Error("RPC call MessagesWillBeConsumed to plugin failed.", mlog.Err(err))
|
|
}
|
|
}
|
|
return _returns.A
|
|
}
|
|
|
|
func (s *hooksRPCServer) MessagesWillBeConsumed(args *Z_MessagesWillBeConsumedArgs, returns *Z_MessagesWillBeConsumedReturns) error {
|
|
if hook, ok := s.impl.(interface {
|
|
MessagesWillBeConsumed(posts []*model.Post) []*model.Post
|
|
}); ok {
|
|
returns.A = hook.MessagesWillBeConsumed(args.A)
|
|
} else {
|
|
return encodableError(fmt.Errorf("hook MessagesWillBeConsumed called but not implemented"))
|
|
}
|
|
return nil
|
|
}
|
|
|
|
type Z_LogDebugArgs struct {
|
|
A string
|
|
B []any
|
|
}
|
|
|
|
type Z_LogDebugReturns struct{}
|
|
|
|
func (g *apiRPCClient) LogDebug(msg string, keyValuePairs ...any) {
|
|
stringifiedPairs := stringifyToObjects(keyValuePairs)
|
|
_args := &Z_LogDebugArgs{msg, stringifiedPairs}
|
|
_returns := &Z_LogDebugReturns{}
|
|
if err := g.client.Call("Plugin.LogDebug", _args, _returns); err != nil {
|
|
log.Printf("RPC call to LogDebug API failed: %s", err.Error())
|
|
}
|
|
}
|
|
|
|
func (s *apiRPCServer) LogDebug(args *Z_LogDebugArgs, returns *Z_LogDebugReturns) error {
|
|
if hook, ok := s.impl.(interface {
|
|
LogDebug(msg string, keyValuePairs ...any)
|
|
}); ok {
|
|
hook.LogDebug(args.A, args.B...)
|
|
} else {
|
|
return encodableError(fmt.Errorf("API LogDebug called but not implemented"))
|
|
}
|
|
return nil
|
|
}
|
|
|
|
type Z_LogInfoArgs struct {
|
|
A string
|
|
B []any
|
|
}
|
|
|
|
type Z_LogInfoReturns struct{}
|
|
|
|
func (g *apiRPCClient) LogInfo(msg string, keyValuePairs ...any) {
|
|
stringifiedPairs := stringifyToObjects(keyValuePairs)
|
|
_args := &Z_LogInfoArgs{msg, stringifiedPairs}
|
|
_returns := &Z_LogInfoReturns{}
|
|
if err := g.client.Call("Plugin.LogInfo", _args, _returns); err != nil {
|
|
log.Printf("RPC call to LogInfo API failed: %s", err.Error())
|
|
}
|
|
}
|
|
|
|
func (s *apiRPCServer) LogInfo(args *Z_LogInfoArgs, returns *Z_LogInfoReturns) error {
|
|
if hook, ok := s.impl.(interface {
|
|
LogInfo(msg string, keyValuePairs ...any)
|
|
}); ok {
|
|
hook.LogInfo(args.A, args.B...)
|
|
} else {
|
|
return encodableError(fmt.Errorf("API LogInfo called but not implemented"))
|
|
}
|
|
return nil
|
|
}
|
|
|
|
type Z_LogWarnArgs struct {
|
|
A string
|
|
B []any
|
|
}
|
|
|
|
type Z_LogWarnReturns struct{}
|
|
|
|
func (g *apiRPCClient) LogWarn(msg string, keyValuePairs ...any) {
|
|
stringifiedPairs := stringifyToObjects(keyValuePairs)
|
|
_args := &Z_LogWarnArgs{msg, stringifiedPairs}
|
|
_returns := &Z_LogWarnReturns{}
|
|
if err := g.client.Call("Plugin.LogWarn", _args, _returns); err != nil {
|
|
log.Printf("RPC call to LogWarn API failed: %s", err.Error())
|
|
}
|
|
}
|
|
|
|
func (s *apiRPCServer) LogWarn(args *Z_LogWarnArgs, returns *Z_LogWarnReturns) error {
|
|
if hook, ok := s.impl.(interface {
|
|
LogWarn(msg string, keyValuePairs ...any)
|
|
}); ok {
|
|
hook.LogWarn(args.A, args.B...)
|
|
} else {
|
|
return encodableError(fmt.Errorf("API LogWarn called but not implemented"))
|
|
}
|
|
return nil
|
|
}
|
|
|
|
type Z_LogErrorArgs struct {
|
|
A string
|
|
B []any
|
|
}
|
|
|
|
type Z_LogErrorReturns struct{}
|
|
|
|
func (g *apiRPCClient) LogError(msg string, keyValuePairs ...any) {
|
|
stringifiedPairs := stringifyToObjects(keyValuePairs)
|
|
_args := &Z_LogErrorArgs{msg, stringifiedPairs}
|
|
_returns := &Z_LogErrorReturns{}
|
|
if err := g.client.Call("Plugin.LogError", _args, _returns); err != nil {
|
|
log.Printf("RPC call to LogError API failed: %s", err.Error())
|
|
}
|
|
}
|
|
|
|
func (s *apiRPCServer) LogError(args *Z_LogErrorArgs, returns *Z_LogErrorReturns) error {
|
|
if hook, ok := s.impl.(interface {
|
|
LogError(msg string, keyValuePairs ...any)
|
|
}); ok {
|
|
hook.LogError(args.A, args.B...)
|
|
} else {
|
|
return encodableError(fmt.Errorf("API LogError called but not implemented"))
|
|
}
|
|
return nil
|
|
}
|
|
|
|
type Z_LogAuditRecArgs struct {
|
|
A *model.AuditRecord
|
|
}
|
|
|
|
type Z_LogAuditRecReturns struct {
|
|
}
|
|
|
|
// Custom audit logging methods with gob safety checks
|
|
func (g *apiRPCClient) LogAuditRec(rec *model.AuditRecord) {
|
|
gobSafeRec := makeAuditRecordGobSafe(*rec)
|
|
_args := &Z_LogAuditRecArgs{&gobSafeRec}
|
|
_returns := &Z_LogAuditRecReturns{}
|
|
if err := g.client.Call("Plugin.LogAuditRec", _args, _returns); err != nil {
|
|
log.Printf("RPC call to LogAuditRec API failed: %s", err.Error())
|
|
}
|
|
}
|
|
|
|
func (s *apiRPCServer) LogAuditRec(args *Z_LogAuditRecArgs, returns *Z_LogAuditRecReturns) error {
|
|
if hook, ok := s.impl.(interface {
|
|
LogAuditRec(rec *model.AuditRecord)
|
|
}); ok {
|
|
hook.LogAuditRec(args.A)
|
|
} else {
|
|
return encodableError(fmt.Errorf("API LogAuditRec called but not implemented"))
|
|
}
|
|
return nil
|
|
}
|
|
|
|
type Z_LogAuditRecWithLevelArgs struct {
|
|
A *model.AuditRecord
|
|
B mlog.Level
|
|
}
|
|
|
|
type Z_LogAuditRecWithLevelReturns struct {
|
|
}
|
|
|
|
func (g *apiRPCClient) LogAuditRecWithLevel(rec *model.AuditRecord, level mlog.Level) {
|
|
gobSafeRec := makeAuditRecordGobSafe(*rec)
|
|
_args := &Z_LogAuditRecWithLevelArgs{&gobSafeRec, level}
|
|
_returns := &Z_LogAuditRecWithLevelReturns{}
|
|
if err := g.client.Call("Plugin.LogAuditRecWithLevel", _args, _returns); err != nil {
|
|
log.Printf("RPC call to LogAuditRecWithLevel API failed: %s", err.Error())
|
|
}
|
|
}
|
|
|
|
func (s *apiRPCServer) LogAuditRecWithLevel(args *Z_LogAuditRecWithLevelArgs, returns *Z_LogAuditRecWithLevelReturns) error {
|
|
if hook, ok := s.impl.(interface {
|
|
LogAuditRecWithLevel(rec *model.AuditRecord, level mlog.Level)
|
|
}); ok {
|
|
hook.LogAuditRecWithLevel(args.A, args.B)
|
|
} else {
|
|
return encodableError(fmt.Errorf("API LogAuditRecWithLevel called but not implemented"))
|
|
}
|
|
return nil
|
|
}
|
|
|
|
type Z_InstallPluginArgs struct {
|
|
PluginStreamID uint32
|
|
B bool
|
|
}
|
|
|
|
type Z_InstallPluginReturns struct {
|
|
A *model.Manifest
|
|
B *model.AppError
|
|
}
|
|
|
|
func (g *apiRPCClient) InstallPlugin(file io.Reader, replace bool) (*model.Manifest, *model.AppError) {
|
|
pluginStreamID := g.muxBroker.NextId()
|
|
|
|
go func() {
|
|
uploadPluginConnection, err := g.muxBroker.Accept(pluginStreamID)
|
|
if err != nil {
|
|
log.Print("Plugin failed to upload plugin. MuxBroker could not Accept connection", mlog.Err(err))
|
|
return
|
|
}
|
|
defer uploadPluginConnection.Close()
|
|
serveIOReader(file, uploadPluginConnection)
|
|
}()
|
|
|
|
_args := &Z_InstallPluginArgs{pluginStreamID, replace}
|
|
_returns := &Z_InstallPluginReturns{}
|
|
if err := g.client.Call("Plugin.InstallPlugin", _args, _returns); err != nil {
|
|
log.Print("RPC call InstallPlugin to plugin failed.", mlog.Err(err))
|
|
}
|
|
|
|
return _returns.A, _returns.B
|
|
}
|
|
|
|
func (s *apiRPCServer) InstallPlugin(args *Z_InstallPluginArgs, returns *Z_InstallPluginReturns) error {
|
|
hook, ok := s.impl.(interface {
|
|
InstallPlugin(file io.Reader, replace bool) (*model.Manifest, *model.AppError)
|
|
})
|
|
if !ok {
|
|
return encodableError(fmt.Errorf("API InstallPlugin called but not implemented"))
|
|
}
|
|
|
|
receivePluginConnection, err := s.muxBroker.Dial(args.PluginStreamID)
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "[ERROR] Can't connect to remote plugin stream, error: %v", err.Error())
|
|
return err
|
|
}
|
|
pluginReader := connectIOReader(receivePluginConnection)
|
|
defer pluginReader.Close()
|
|
|
|
returns.A, returns.B = hook.InstallPlugin(pluginReader, args.B)
|
|
return nil
|
|
}
|
|
|
|
type Z_UploadDataArgs struct {
|
|
A *model.UploadSession
|
|
PluginStreamID uint32
|
|
}
|
|
|
|
type Z_UploadDataReturns struct {
|
|
A *model.FileInfo
|
|
B error
|
|
}
|
|
|
|
func (g *apiRPCClient) UploadData(us *model.UploadSession, rd io.Reader) (*model.FileInfo, error) {
|
|
pluginStreamID := g.muxBroker.NextId()
|
|
|
|
go func() {
|
|
pluginConnection, err := g.muxBroker.Accept(pluginStreamID)
|
|
if err != nil {
|
|
log.Print("Failed to upload data. MuxBroker could not Accept connection", mlog.Err(err))
|
|
return
|
|
}
|
|
defer pluginConnection.Close()
|
|
serveIOReader(rd, pluginConnection)
|
|
}()
|
|
|
|
_args := &Z_UploadDataArgs{us, pluginStreamID}
|
|
_returns := &Z_UploadDataReturns{}
|
|
if err := g.client.Call("Plugin.UploadData", _args, _returns); err != nil {
|
|
log.Print("RPC call UploadData to plugin failed.", mlog.Err(err))
|
|
}
|
|
|
|
return _returns.A, _returns.B
|
|
}
|
|
|
|
func (s *apiRPCServer) UploadData(args *Z_UploadDataArgs, returns *Z_UploadDataReturns) error {
|
|
hook, ok := s.impl.(interface {
|
|
UploadData(us *model.UploadSession, rd io.Reader) (*model.FileInfo, error)
|
|
})
|
|
if !ok {
|
|
return encodableError(fmt.Errorf("API UploadData called but not implemented"))
|
|
}
|
|
|
|
receivePluginConnection, err := s.muxBroker.Dial(args.PluginStreamID)
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "[ERROR] Can't connect to remote plugin stream, error: %v", err.Error())
|
|
return err
|
|
}
|
|
pluginReader := connectIOReader(receivePluginConnection)
|
|
defer pluginReader.Close()
|
|
|
|
returns.A, returns.B = hook.UploadData(args.A, pluginReader)
|
|
return nil
|
|
}
|
|
|
|
func init() {
|
|
hookNameToId["ServeMetrics"] = ServeMetricsID
|
|
}
|
|
|
|
type Z_ServeMetricsArgs struct {
|
|
ResponseWriterStream uint32
|
|
Request *HTTPRequestSubset
|
|
Context *Context
|
|
RequestBodyStream uint32
|
|
}
|
|
|
|
func (g *hooksRPCClient) ServeMetrics(c *Context, w http.ResponseWriter, r *http.Request) {
|
|
if !g.implemented[ServeMetricsID] {
|
|
http.NotFound(w, r)
|
|
return
|
|
}
|
|
|
|
serveMetricsStreamId := g.muxBroker.NextId()
|
|
go func() {
|
|
connection, err := g.muxBroker.Accept(serveMetricsStreamId)
|
|
if err != nil {
|
|
g.log.Error("Plugin failed to ServeMetrics, muxBroker couldn't accept connection", mlog.Uint("serve_http_stream_id", serveMetricsStreamId), mlog.Err(err))
|
|
return
|
|
}
|
|
defer connection.Close()
|
|
|
|
rpcServer := rpc.NewServer()
|
|
if err := rpcServer.RegisterName("Plugin", &httpResponseWriterRPCServer{w: w, log: g.log}); err != nil {
|
|
g.log.Error("Plugin failed to ServeMetrics, couldn't register RPC name", mlog.Err(err))
|
|
return
|
|
}
|
|
rpcServer.ServeConn(connection)
|
|
}()
|
|
|
|
requestBodyStreamId := uint32(0)
|
|
if r.Body != nil {
|
|
requestBodyStreamId = g.muxBroker.NextId()
|
|
go func() {
|
|
bodyConnection, err := g.muxBroker.Accept(requestBodyStreamId)
|
|
if err != nil {
|
|
g.log.Error("Plugin failed to ServeMetrics, muxBroker couldn't Accept request body connection", mlog.Err(err))
|
|
return
|
|
}
|
|
defer bodyConnection.Close()
|
|
serveIOReader(r.Body, bodyConnection)
|
|
}()
|
|
}
|
|
|
|
forwardedRequest := &HTTPRequestSubset{
|
|
Method: r.Method,
|
|
URL: r.URL,
|
|
Proto: r.Proto,
|
|
ProtoMajor: r.ProtoMajor,
|
|
ProtoMinor: r.ProtoMinor,
|
|
Header: r.Header,
|
|
Host: r.Host,
|
|
RemoteAddr: r.RemoteAddr,
|
|
RequestURI: r.RequestURI,
|
|
}
|
|
|
|
if err := g.client.Call("Plugin.ServeMetrics", Z_ServeMetricsArgs{
|
|
Context: c,
|
|
ResponseWriterStream: serveMetricsStreamId,
|
|
Request: forwardedRequest,
|
|
RequestBodyStream: requestBodyStreamId,
|
|
}, nil); err != nil {
|
|
g.log.Error("Plugin failed to ServeMetrics, RPC call failed", mlog.Err(err))
|
|
http.Error(w, "500 internal server error", http.StatusInternalServerError)
|
|
}
|
|
}
|
|
|
|
func (s *hooksRPCServer) ServeMetrics(args *Z_ServeMetricsArgs, returns *struct{}) error {
|
|
connection, err := s.muxBroker.Dial(args.ResponseWriterStream)
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "[ERROR] Can't connect to remote response writer stream, error: %v", err.Error())
|
|
return err
|
|
}
|
|
w := connectHTTPResponseWriter(connection)
|
|
defer w.Close()
|
|
|
|
r := args.Request
|
|
if args.RequestBodyStream != 0 {
|
|
connection, err := s.muxBroker.Dial(args.RequestBodyStream)
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "[ERROR] Can't connect to remote request body stream, error: %v", err.Error())
|
|
return err
|
|
}
|
|
r.Body = connectIOReader(connection)
|
|
} else {
|
|
r.Body = io.NopCloser(&bytes.Buffer{})
|
|
}
|
|
defer r.Body.Close()
|
|
|
|
httpReq := r.GetHTTPRequest()
|
|
|
|
if hook, ok := s.impl.(interface {
|
|
ServeMetrics(c *Context, w http.ResponseWriter, r *http.Request)
|
|
}); ok {
|
|
hook.ServeMetrics(args.Context, w, httpReq)
|
|
} else {
|
|
http.NotFound(w, httpReq)
|
|
}
|
|
|
|
return nil
|
|
}
|