11package backend
22
33import (
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
1623const 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.
101109func 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