Skip to content

Commit cdc5808

Browse files
authored
Backend plugin debug mode improvements (#621)
* Add support for debug mode when running from GoLand * Add support for -debug flag to explicitly run the plugin in debug mode * Fix plugin killing itself when running with -debug flag * Update comments as per PR review * Update comment * Improve debug mode plugin management - Store PID in standalone file, to allow Grafana to detect if a debug plugin is running or not. This allows Grafana to start the plugin with the default Hashicorp mode if the debug plugin isn't running anymore. - Graceful shutdown of GRPC server, with standalone file cleanup Note: needs to be tested on Mac OS X and Windows * Fix comment indentation * Split standalone address and pid in two different files Fixes the dummy locator server not reading the address properly if it's been compiled with the old version SDK, but the standalone.txt comes from the new version of the SDK * Fix standalone.txt not being cleaned * Kill dummy plugin locator when stopping debug plugin * Ensure plugin is running before starting dummy plugin locator * Fix GoLand debug mode when using non-standard config name, add some tests * Fix goimports
1 parent 05f97c9 commit cdc5808

File tree

8 files changed

+352
-104
lines changed

8 files changed

+352
-104
lines changed

backend/app/manage.go

Lines changed: 2 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ package app
33
import (
44
"github.com/grafana/grafana-plugin-sdk-go/backend"
55
"github.com/grafana/grafana-plugin-sdk-go/internal/automanagement"
6-
"github.com/grafana/grafana-plugin-sdk-go/internal/standalone"
76
)
87

98
// ManageOpts can modify Manage behaviour.
@@ -16,29 +15,12 @@ type ManageOpts struct {
1615
// pluginID should match the one from plugin.json.
1716
func Manage(pluginID string, instanceFactory InstanceFactoryFunc, opts ManageOpts) error {
1817
backend.SetupPluginEnvironment(pluginID) // Enable profiler.
19-
2018
handler := automanagement.NewManager(NewInstanceManager(instanceFactory))
21-
22-
serveOpts := backend.ServeOpts{
19+
return backend.Manage(pluginID, backend.ServeOpts{
2320
CheckHealthHandler: handler,
2421
CallResourceHandler: handler,
2522
QueryDataHandler: handler,
2623
StreamHandler: handler,
2724
GRPCSettings: opts.GRPCSettings,
28-
}
29-
30-
info, err := standalone.GetInfo(pluginID)
31-
if err != nil {
32-
return err
33-
}
34-
35-
if info.Standalone {
36-
return backend.StandaloneServe(serveOpts, info.Address)
37-
} else if info.Address != "" {
38-
standalone.RunDummyPluginLocator(info.Address)
39-
return nil
40-
}
41-
42-
// The default/normal hashicorp path.
43-
return backend.Serve(serveOpts)
25+
})
4426
}

backend/datasource/manage.go

Lines changed: 2 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ package datasource
33
import (
44
"github.com/grafana/grafana-plugin-sdk-go/backend"
55
"github.com/grafana/grafana-plugin-sdk-go/internal/automanagement"
6-
"github.com/grafana/grafana-plugin-sdk-go/internal/standalone"
76
)
87

98
// ManageOpts can modify Manage behaviour.
@@ -16,29 +15,12 @@ type ManageOpts struct {
1615
// pluginID should match the one from plugin.json.
1716
func Manage(pluginID string, instanceFactory InstanceFactoryFunc, opts ManageOpts) error {
1817
backend.SetupPluginEnvironment(pluginID) // Enable profiler.
19-
2018
handler := automanagement.NewManager(NewInstanceManager(instanceFactory))
21-
22-
serveOpts := backend.ServeOpts{
19+
return backend.Manage(pluginID, backend.ServeOpts{
2320
CheckHealthHandler: handler,
2421
CallResourceHandler: handler,
2522
QueryDataHandler: handler,
2623
StreamHandler: handler,
2724
GRPCSettings: opts.GRPCSettings,
28-
}
29-
30-
info, err := standalone.GetInfo(pluginID)
31-
if err != nil {
32-
return err
33-
}
34-
35-
if info.Standalone {
36-
return backend.StandaloneServe(serveOpts, info.Address)
37-
} else if info.Address != "" {
38-
standalone.RunDummyPluginLocator(info.Address)
39-
return nil
40-
}
41-
42-
// The default/normal hashicorp path.
43-
return backend.Serve(serveOpts)
25+
})
4426
}

backend/serve.go

Lines changed: 111 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,23 @@
11
package backend
22

33
import (
4+
"fmt"
45
"net"
6+
"os"
7+
"os/signal"
8+
"strconv"
9+
"syscall"
510

6-
"github.com/grafana/grafana-plugin-sdk-go/backend/grpcplugin"
7-
"github.com/grafana/grafana-plugin-sdk-go/backend/log"
8-
"github.com/grafana/grafana-plugin-sdk-go/genproto/pluginv2"
911
grpc_middleware "github.com/grpc-ecosystem/go-grpc-middleware"
1012
grpc_prometheus "github.com/grpc-ecosystem/go-grpc-prometheus"
1113
"github.com/hashicorp/go-plugin"
1214
"github.com/prometheus/client_golang/prometheus"
1315
"google.golang.org/grpc"
16+
17+
"github.com/grafana/grafana-plugin-sdk-go/backend/grpcplugin"
18+
"github.com/grafana/grafana-plugin-sdk-go/backend/log"
19+
"github.com/grafana/grafana-plugin-sdk-go/genproto/pluginv2"
20+
"github.com/grafana/grafana-plugin-sdk-go/internal/standalone"
1421
)
1522

1623
const defaultServerMaxReceiveMessageSize = 1024 * 1024 * 16
@@ -97,17 +104,65 @@ func Serve(opts ServeOpts) error {
97104
return grpcplugin.Serve(pluginOpts)
98105
}
99106

100-
// StandaloneServe starts a gRPC server that is not managed by hashicorp
107+
// StandaloneServe starts a gRPC server that is not managed by hashicorp.
108+
// Deprecated: use GracefulStandaloneServe instead.
101109
func StandaloneServe(dsopts ServeOpts, address string) error {
102-
opts := asGRPCServeOpts(dsopts)
110+
// GracefulStandaloneServe has a new signature, this function keeps the old
111+
// signature for existing plugins for backwards compatibility.
112+
// Create a new standalone.Args and disable all the standalone-file-related features.
113+
return GracefulStandaloneServe(dsopts, standalone.Args{Address: address})
114+
}
115+
116+
// GracefulStandaloneServe starts a gRPC server that is not managed by hashicorp.
117+
// The provided standalone.Args must have an Address set, or the function returns an error.
118+
// The function handles creating/cleaning up the standalone address file, and graceful GRPC server termination.
119+
// The function returns after the GRPC server has been terminated.
120+
func GracefulStandaloneServe(dsopts ServeOpts, info standalone.Args) error {
121+
// We must have an address if we want to run the plugin in standalone mode
122+
if info.Address == "" {
123+
return fmt.Errorf("standalone address must be specified")
124+
}
103125

126+
// Write the address to the local file
127+
if info.Debugger {
128+
log.DefaultLogger.Info("Creating standalone address and pid files")
129+
if err := standalone.CreateStandaloneAddressFile(info); err != nil {
130+
return fmt.Errorf("create standalone address file: %w", err)
131+
}
132+
if err := standalone.CreateStandalonePIDFile(info); err != nil {
133+
return fmt.Errorf("create standalone pid file: %w", err)
134+
}
135+
136+
// sadly vs-code can not listen to shutdown events
137+
// https://github.com/golang/vscode-go/issues/120
138+
139+
// Cleanup function that deletes standalone.txt and pid.txt, if it exists. Fails silently.
140+
// This is so the address file is deleted when the plugin shuts down gracefully, if possible.
141+
defer func() {
142+
log.DefaultLogger.Info("Cleaning up standalone address and pid files")
143+
if err := standalone.CleanupStandaloneAddressFile(info); err != nil {
144+
log.DefaultLogger.Error("Error while cleaning up standalone address file", "error", err)
145+
}
146+
if err := standalone.CleanupStandalonePIDFile(info); err != nil {
147+
log.DefaultLogger.Error("Error while cleaning up standalone pid file", "error", err)
148+
}
149+
// Kill the dummy locator so Grafana reloads the plugin
150+
standalone.FindAndKillCurrentPlugin(info.Dir)
151+
}()
152+
153+
// When debugging, be sure to kill the running instances, so we reconnect
154+
standalone.FindAndKillCurrentPlugin(info.Dir)
155+
}
156+
157+
// Start GRPC server
158+
opts := asGRPCServeOpts(dsopts)
104159
if opts.GRPCServer == nil {
105160
opts.GRPCServer = plugin.DefaultGRPCServer
106161
}
107162

108163
server := opts.GRPCServer(nil)
109164

110-
plugKeys := []string{}
165+
var plugKeys []string
111166
if opts.DiagnosticsServer != nil {
112167
pluginv2.RegisterDiagnosticsServer(server, opts.DiagnosticsServer)
113168
plugKeys = append(plugKeys, "diagnostics")
@@ -128,18 +183,63 @@ func StandaloneServe(dsopts ServeOpts, address string) error {
128183
plugKeys = append(plugKeys, "stream")
129184
}
130185

186+
// Start the GRPC server and handle graceful shutdown to ensure we execute deferred functions correctly
131187
log.DefaultLogger.Debug("Standalone plugin server", "capabilities", plugKeys)
132-
133-
listener, err := net.Listen("tcp", address)
188+
listener, err := net.Listen("tcp", info.Address)
134189
if err != nil {
135190
return err
136191
}
137192

138-
err = server.Serve(listener)
139-
if err != nil {
193+
signalChan := make(chan os.Signal, 1)
194+
serverErrChan := make(chan error, 1)
195+
signal.Notify(signalChan, syscall.SIGINT, syscall.SIGTERM)
196+
197+
// Unregister signal handlers before returning
198+
defer signal.Stop(signalChan)
199+
200+
// Start GRPC server in a separate goroutine
201+
go func() {
202+
serverErrChan <- server.Serve(listener)
203+
}()
204+
205+
// Block until signal or GRPC server termination
206+
select {
207+
case <-signalChan:
208+
// Signal received, stop the server
209+
server.Stop()
210+
if err := <-serverErrChan; err != nil {
211+
// Bubble up error
212+
return err
213+
}
214+
case err := <-serverErrChan:
215+
// Server stopped prematurely, bubble up the error
140216
return err
141217
}
142-
log.DefaultLogger.Debug("Plugin server exited")
143218

219+
log.DefaultLogger.Debug("Plugin server exited")
144220
return nil
145221
}
222+
223+
// Manage runs the plugin in either standalone mode, dummy locator or normal (hashicorp) mode.
224+
func Manage(pluginID string, serveOpts ServeOpts) error {
225+
info, err := standalone.GetInfo(pluginID)
226+
if err != nil {
227+
return err
228+
}
229+
230+
if info.Standalone {
231+
// Run the standalone GRPC server
232+
return GracefulStandaloneServe(serveOpts, info)
233+
}
234+
235+
if info.Address != "" && standalone.CheckPIDIsRunning(info.PID) {
236+
// Grafana is trying to run the dummy plugin locator to connect to the standalone
237+
// GRPC server (separate process)
238+
Logger.Debug("Running dummy plugin locator", "addr", info.Address, "pid", strconv.Itoa(info.PID))
239+
standalone.RunDummyPluginLocator(info.Address)
240+
return nil
241+
}
242+
243+
// The default/normal hashicorp path.
244+
return Serve(serveOpts)
245+
}

0 commit comments

Comments
 (0)