mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Enable Grafana extensions at build time. (#11752)
* extensions: import and build * bus: use predefined error * enterprise: build script for enterprise packages * poc: auto registering services and dependency injection (cherry picked from commit b5b1ef875f905473af41e49f8071cb9028edc845) * poc: backend services registry progress (cherry picked from commit 97be69725881241bfbf1e7adf0e66801d6b0af3d) * poc: minor update (cherry picked from commit 03d7a6888b81403f458b94305792e075568f0794) * ioc: introduce manuel ioc * enterprise: adds setting for enterprise * build: test and build specific ee commit * cleanup: test testing code * removes example hello service
This commit is contained in:
committed by
Torkel Ödegaard
parent
afce0feb05
commit
28f7b6dad1
@@ -23,7 +23,7 @@ func (hs *HTTPServer) registerRoutes() {
|
||||
// automatically set HEAD for every GET
|
||||
macaronR.SetAutoHead(true)
|
||||
|
||||
r := newRouteRegister(middleware.RequestMetrics, middleware.RequestTracing)
|
||||
r := hs.RouteRegister
|
||||
|
||||
// not logged in views
|
||||
r.Get("/", reqSignedIn, Index)
|
||||
|
||||
@@ -35,15 +35,14 @@ type HTTPServer struct {
|
||||
context context.Context
|
||||
streamManager *live.StreamManager
|
||||
cache *gocache.Cache
|
||||
RouteRegister RouteRegister `inject:""`
|
||||
|
||||
httpSrv *http.Server
|
||||
}
|
||||
|
||||
func NewHTTPServer() *HTTPServer {
|
||||
return &HTTPServer{
|
||||
log: log.New("http.server"),
|
||||
cache: gocache.New(5*time.Minute, 10*time.Minute),
|
||||
}
|
||||
func (hs *HTTPServer) Init() {
|
||||
hs.log = log.New("http.server")
|
||||
hs.cache = gocache.New(5*time.Minute, 10*time.Minute)
|
||||
}
|
||||
|
||||
func (hs *HTTPServer) Start(ctx context.Context) error {
|
||||
|
||||
@@ -289,7 +289,7 @@ func setIndexViewData(c *m.ReqContext) (*dtos.IndexViewData, error) {
|
||||
|
||||
data.NavTree = append(data.NavTree, &dtos.NavLink{
|
||||
Text: "Help",
|
||||
SubTitle: fmt.Sprintf(`Grafana v%s (%s)`, setting.BuildVersion, setting.BuildCommit),
|
||||
SubTitle: fmt.Sprintf(`%s v%s (%s)`, setting.ApplicationName, setting.BuildVersion, setting.BuildCommit),
|
||||
Id: "help",
|
||||
Url: "#",
|
||||
Icon: "gicon gicon-question",
|
||||
|
||||
@@ -11,6 +11,8 @@ type Router interface {
|
||||
Get(pattern string, handlers ...macaron.Handler) *macaron.Route
|
||||
}
|
||||
|
||||
// RouteRegister allows you to add routes and macaron.Handlers
|
||||
// that the web server should serve.
|
||||
type RouteRegister interface {
|
||||
Get(string, ...macaron.Handler)
|
||||
Post(string, ...macaron.Handler)
|
||||
@@ -26,7 +28,8 @@ type RouteRegister interface {
|
||||
|
||||
type RegisterNamedMiddleware func(name string) macaron.Handler
|
||||
|
||||
func newRouteRegister(namedMiddleware ...RegisterNamedMiddleware) RouteRegister {
|
||||
// NewRouteRegister creates a new RouteRegister with all middlewares sent as params
|
||||
func NewRouteRegister(namedMiddleware ...RegisterNamedMiddleware) RouteRegister {
|
||||
return &routeRegister{
|
||||
prefix: "",
|
||||
routes: []route{},
|
||||
|
||||
@@ -51,7 +51,7 @@ func TestRouteSimpleRegister(t *testing.T) {
|
||||
}
|
||||
|
||||
// Setup
|
||||
rr := newRouteRegister(func(name string) macaron.Handler {
|
||||
rr := NewRouteRegister(func(name string) macaron.Handler {
|
||||
return emptyHandler(name)
|
||||
})
|
||||
|
||||
@@ -96,7 +96,7 @@ func TestRouteGroupedRegister(t *testing.T) {
|
||||
}
|
||||
|
||||
// Setup
|
||||
rr := newRouteRegister()
|
||||
rr := NewRouteRegister()
|
||||
|
||||
rr.Delete("/admin", emptyHandler("1"))
|
||||
rr.Get("/down", emptyHandler("1"), emptyHandler("2"))
|
||||
@@ -150,7 +150,7 @@ func TestNamedMiddlewareRouteRegister(t *testing.T) {
|
||||
}
|
||||
|
||||
// Setup
|
||||
rr := newRouteRegister(func(name string) macaron.Handler {
|
||||
rr := NewRouteRegister(func(name string) macaron.Handler {
|
||||
return emptyHandler(name)
|
||||
})
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ package bus
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"errors"
|
||||
"reflect"
|
||||
)
|
||||
|
||||
@@ -10,6 +10,8 @@ type HandlerFunc interface{}
|
||||
type CtxHandlerFunc func()
|
||||
type Msg interface{}
|
||||
|
||||
var ErrHandlerNotFound = errors.New("handler not found")
|
||||
|
||||
type Bus interface {
|
||||
Dispatch(msg Msg) error
|
||||
DispatchCtx(ctx context.Context, msg Msg) error
|
||||
@@ -38,12 +40,17 @@ func New() Bus {
|
||||
return bus
|
||||
}
|
||||
|
||||
// Want to get rid of global bus
|
||||
func GetBus() Bus {
|
||||
return globalBus
|
||||
}
|
||||
|
||||
func (b *InProcBus) DispatchCtx(ctx context.Context, msg Msg) error {
|
||||
var msgName = reflect.TypeOf(msg).Elem().Name()
|
||||
|
||||
var handler = b.handlers[msgName]
|
||||
if handler == nil {
|
||||
return fmt.Errorf("handler not found for %s", msgName)
|
||||
return ErrHandlerNotFound
|
||||
}
|
||||
|
||||
var params = make([]reflect.Value, 2)
|
||||
@@ -64,7 +71,7 @@ func (b *InProcBus) Dispatch(msg Msg) error {
|
||||
|
||||
var handler = b.handlers[msgName]
|
||||
if handler == nil {
|
||||
return fmt.Errorf("handler not found for %s", msgName)
|
||||
return ErrHandlerNotFound
|
||||
}
|
||||
|
||||
var params = make([]reflect.Value, 1)
|
||||
|
||||
@@ -18,6 +18,7 @@ import (
|
||||
"github.com/grafana/grafana/pkg/metrics"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
|
||||
_ "github.com/grafana/grafana/pkg/extensions"
|
||||
_ "github.com/grafana/grafana/pkg/services/alerting/conditions"
|
||||
_ "github.com/grafana/grafana/pkg/services/alerting/notifiers"
|
||||
_ "github.com/grafana/grafana/pkg/tsdb/cloudwatch"
|
||||
@@ -33,6 +34,7 @@ import (
|
||||
var version = "5.0.0"
|
||||
var commit = "NA"
|
||||
var buildstamp string
|
||||
var enterprise string
|
||||
|
||||
var configFile = flag.String("config", "", "path to config file")
|
||||
var homePath = flag.String("homepath", "", "path to grafana install/home path, defaults to working directory")
|
||||
@@ -76,6 +78,7 @@ func main() {
|
||||
setting.BuildVersion = version
|
||||
setting.BuildCommit = commit
|
||||
setting.BuildStamp = buildstampInt64
|
||||
setting.Enterprise, _ = strconv.ParseBool(enterprise)
|
||||
|
||||
metrics.M_Grafana_Version.WithLabelValues(version).Set(1)
|
||||
shutdownCompleted := make(chan int)
|
||||
|
||||
@@ -8,9 +8,15 @@ import (
|
||||
"net"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/facebookgo/inject"
|
||||
"github.com/grafana/grafana/pkg/bus"
|
||||
"github.com/grafana/grafana/pkg/middleware"
|
||||
"github.com/grafana/grafana/pkg/registry"
|
||||
"github.com/grafana/grafana/pkg/services/dashboards"
|
||||
"github.com/grafana/grafana/pkg/services/provisioning"
|
||||
|
||||
"golang.org/x/sync/errgroup"
|
||||
@@ -20,15 +26,17 @@ import (
|
||||
"github.com/grafana/grafana/pkg/login"
|
||||
"github.com/grafana/grafana/pkg/metrics"
|
||||
"github.com/grafana/grafana/pkg/plugins"
|
||||
"github.com/grafana/grafana/pkg/services/alerting"
|
||||
"github.com/grafana/grafana/pkg/services/cleanup"
|
||||
"github.com/grafana/grafana/pkg/services/notifications"
|
||||
"github.com/grafana/grafana/pkg/services/search"
|
||||
"github.com/grafana/grafana/pkg/services/sqlstore"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
|
||||
"github.com/grafana/grafana/pkg/social"
|
||||
"github.com/grafana/grafana/pkg/tracing"
|
||||
|
||||
_ "github.com/grafana/grafana/pkg/extensions"
|
||||
_ "github.com/grafana/grafana/pkg/services/alerting"
|
||||
_ "github.com/grafana/grafana/pkg/services/cleanup"
|
||||
_ "github.com/grafana/grafana/pkg/services/search"
|
||||
)
|
||||
|
||||
func NewGrafanaServer() *GrafanaServerImpl {
|
||||
@@ -48,18 +56,20 @@ type GrafanaServerImpl struct {
|
||||
shutdownFn context.CancelFunc
|
||||
childRoutines *errgroup.Group
|
||||
log log.Logger
|
||||
RouteRegister api.RouteRegister `inject:""`
|
||||
|
||||
httpServer *api.HTTPServer
|
||||
HttpServer *api.HTTPServer `inject:""`
|
||||
}
|
||||
|
||||
func (g *GrafanaServerImpl) Start() error {
|
||||
g.initLogging()
|
||||
g.writePIDFile()
|
||||
|
||||
initSql()
|
||||
// initSql
|
||||
sqlstore.NewEngine() // TODO: this should return an error
|
||||
sqlstore.EnsureAdminUser()
|
||||
|
||||
metrics.Init(setting.Cfg)
|
||||
search.Init()
|
||||
login.Init()
|
||||
social.NewOAuthService()
|
||||
|
||||
@@ -79,30 +89,64 @@ func (g *GrafanaServerImpl) Start() error {
|
||||
}
|
||||
defer tracingCloser.Close()
|
||||
|
||||
// init alerting
|
||||
if setting.AlertingEnabled && setting.ExecuteAlerts {
|
||||
engine := alerting.NewEngine()
|
||||
g.childRoutines.Go(func() error { return engine.Run(g.context) })
|
||||
}
|
||||
|
||||
// cleanup service
|
||||
cleanUpService := cleanup.NewCleanUpService()
|
||||
g.childRoutines.Go(func() error { return cleanUpService.Run(g.context) })
|
||||
|
||||
if err = notifications.Init(); err != nil {
|
||||
return fmt.Errorf("Notification service failed to initialize. error: %v", err)
|
||||
}
|
||||
|
||||
serviceGraph := inject.Graph{}
|
||||
serviceGraph.Provide(&inject.Object{Value: bus.GetBus()})
|
||||
serviceGraph.Provide(&inject.Object{Value: dashboards.NewProvisioningService()})
|
||||
serviceGraph.Provide(&inject.Object{Value: api.NewRouteRegister(middleware.RequestMetrics, middleware.RequestTracing)})
|
||||
serviceGraph.Provide(&inject.Object{Value: api.HTTPServer{}})
|
||||
services := registry.GetServices()
|
||||
|
||||
// Add all services to dependency graph
|
||||
for _, service := range services {
|
||||
serviceGraph.Provide(&inject.Object{Value: service})
|
||||
}
|
||||
|
||||
serviceGraph.Provide(&inject.Object{Value: g})
|
||||
|
||||
// Inject dependencies to services
|
||||
if err := serviceGraph.Populate(); err != nil {
|
||||
return fmt.Errorf("Failed to populate service dependency: %v", err)
|
||||
}
|
||||
|
||||
// Init & start services
|
||||
for _, service := range services {
|
||||
if registry.IsDisabled(service) {
|
||||
continue
|
||||
}
|
||||
|
||||
g.log.Info("Initializing " + reflect.TypeOf(service).Elem().Name())
|
||||
|
||||
if err := service.Init(); err != nil {
|
||||
return fmt.Errorf("Service init failed %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Start background services
|
||||
for index := range services {
|
||||
service, ok := services[index].(registry.BackgroundService)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
if registry.IsDisabled(services[index]) {
|
||||
continue
|
||||
}
|
||||
|
||||
g.childRoutines.Go(func() error {
|
||||
err := service.Run(g.context)
|
||||
g.log.Info("Stopped "+reflect.TypeOf(service).Elem().Name(), "reason", err)
|
||||
return err
|
||||
})
|
||||
}
|
||||
|
||||
sendSystemdNotification("READY=1")
|
||||
|
||||
return g.startHttpServer()
|
||||
}
|
||||
|
||||
func initSql() {
|
||||
sqlstore.NewEngine()
|
||||
sqlstore.EnsureAdminUser()
|
||||
}
|
||||
|
||||
func (g *GrafanaServerImpl) initLogging() {
|
||||
err := setting.NewConfigContext(&setting.CommandLineArgs{
|
||||
Config: *configFile,
|
||||
@@ -115,14 +159,14 @@ func (g *GrafanaServerImpl) initLogging() {
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
g.log.Info("Starting Grafana", "version", version, "commit", commit, "compiled", time.Unix(setting.BuildStamp, 0))
|
||||
g.log.Info("Starting "+setting.ApplicationName, "version", version, "commit", commit, "compiled", time.Unix(setting.BuildStamp, 0))
|
||||
setting.LogConfigurationInfo()
|
||||
}
|
||||
|
||||
func (g *GrafanaServerImpl) startHttpServer() error {
|
||||
g.httpServer = api.NewHTTPServer()
|
||||
g.HttpServer.Init()
|
||||
|
||||
err := g.httpServer.Start(g.context)
|
||||
err := g.HttpServer.Start(g.context)
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("Fail to start server. error: %v", err)
|
||||
@@ -134,7 +178,7 @@ func (g *GrafanaServerImpl) startHttpServer() error {
|
||||
func (g *GrafanaServerImpl) Shutdown(code int, reason string) {
|
||||
g.log.Info("Shutdown started", "code", code, "reason", reason)
|
||||
|
||||
err := g.httpServer.Shutdown(g.context)
|
||||
err := g.HttpServer.Shutdown(g.context)
|
||||
if err != nil {
|
||||
g.log.Error("Failed to shutdown server", "error", err)
|
||||
}
|
||||
|
||||
3
pkg/extensions/main.go
Normal file
3
pkg/extensions/main.go
Normal file
@@ -0,0 +1,3 @@
|
||||
package extensions
|
||||
|
||||
import _ "github.com/pkg/errors"
|
||||
@@ -58,7 +58,7 @@ func (p *PluginManager) Run(ctx context.Context) error {
|
||||
p.Kill()
|
||||
}
|
||||
|
||||
p.log.Info("Stopped Plugins", "error", ctx.Err())
|
||||
p.log.Info("Stopped Plugins", "reason", ctx.Err())
|
||||
return ctx.Err()
|
||||
}
|
||||
|
||||
|
||||
33
pkg/registry/registry.go
Normal file
33
pkg/registry/registry.go
Normal file
@@ -0,0 +1,33 @@
|
||||
package registry
|
||||
|
||||
import (
|
||||
"context"
|
||||
)
|
||||
|
||||
var services = []Service{}
|
||||
|
||||
func RegisterService(srv Service) {
|
||||
services = append(services, srv)
|
||||
}
|
||||
|
||||
func GetServices() []Service {
|
||||
return services
|
||||
}
|
||||
|
||||
type Service interface {
|
||||
Init() error
|
||||
}
|
||||
|
||||
// Useful for alerting service
|
||||
type CanBeDisabled interface {
|
||||
IsDisabled() bool
|
||||
}
|
||||
|
||||
type BackgroundService interface {
|
||||
Run(ctx context.Context) error
|
||||
}
|
||||
|
||||
func IsDisabled(srv Service) bool {
|
||||
canBeDisabled, ok := srv.(CanBeDisabled)
|
||||
return ok && canBeDisabled.IsDisabled()
|
||||
}
|
||||
@@ -11,6 +11,8 @@ import (
|
||||
|
||||
"github.com/benbjohnson/clock"
|
||||
"github.com/grafana/grafana/pkg/log"
|
||||
"github.com/grafana/grafana/pkg/registry"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
"golang.org/x/sync/errgroup"
|
||||
)
|
||||
|
||||
@@ -25,31 +27,37 @@ type Engine struct {
|
||||
resultHandler ResultHandler
|
||||
}
|
||||
|
||||
func NewEngine() *Engine {
|
||||
e := &Engine{
|
||||
ticker: NewTicker(time.Now(), time.Second*0, clock.New()),
|
||||
execQueue: make(chan *Job, 1000),
|
||||
scheduler: NewScheduler(),
|
||||
evalHandler: NewEvalHandler(),
|
||||
ruleReader: NewRuleReader(),
|
||||
log: log.New("alerting.engine"),
|
||||
resultHandler: NewResultHandler(),
|
||||
}
|
||||
func init() {
|
||||
registry.RegisterService(&Engine{})
|
||||
}
|
||||
|
||||
func NewEngine() *Engine {
|
||||
e := &Engine{}
|
||||
e.Init()
|
||||
return e
|
||||
}
|
||||
|
||||
func (e *Engine) IsDisabled() bool {
|
||||
return !setting.AlertingEnabled || !setting.ExecuteAlerts
|
||||
}
|
||||
|
||||
func (e *Engine) Init() error {
|
||||
e.ticker = NewTicker(time.Now(), time.Second*0, clock.New())
|
||||
e.execQueue = make(chan *Job, 1000)
|
||||
e.scheduler = NewScheduler()
|
||||
e.evalHandler = NewEvalHandler()
|
||||
e.ruleReader = NewRuleReader()
|
||||
e.log = log.New("alerting.engine")
|
||||
e.resultHandler = NewResultHandler()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (e *Engine) Run(ctx context.Context) error {
|
||||
e.log.Info("Initializing Alerting")
|
||||
|
||||
alertGroup, ctx := errgroup.WithContext(ctx)
|
||||
|
||||
alertGroup.Go(func() error { return e.alertingTicker(ctx) })
|
||||
alertGroup.Go(func() error { return e.runJobDispatcher(ctx) })
|
||||
|
||||
err := alertGroup.Wait()
|
||||
|
||||
e.log.Info("Stopped Alerting", "reason", err)
|
||||
return err
|
||||
}
|
||||
|
||||
|
||||
@@ -7,11 +7,10 @@ import (
|
||||
"path"
|
||||
"time"
|
||||
|
||||
"golang.org/x/sync/errgroup"
|
||||
|
||||
"github.com/grafana/grafana/pkg/bus"
|
||||
"github.com/grafana/grafana/pkg/log"
|
||||
m "github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/registry"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
)
|
||||
|
||||
@@ -19,24 +18,16 @@ type CleanUpService struct {
|
||||
log log.Logger
|
||||
}
|
||||
|
||||
func NewCleanUpService() *CleanUpService {
|
||||
return &CleanUpService{
|
||||
log: log.New("cleanup"),
|
||||
}
|
||||
func init() {
|
||||
registry.RegisterService(&CleanUpService{})
|
||||
}
|
||||
|
||||
func (service *CleanUpService) Init() error {
|
||||
service.log = log.New("cleanup")
|
||||
return nil
|
||||
}
|
||||
|
||||
func (service *CleanUpService) Run(ctx context.Context) error {
|
||||
service.log.Info("Initializing CleanUpService")
|
||||
|
||||
g, _ := errgroup.WithContext(ctx)
|
||||
g.Go(func() error { return service.start(ctx) })
|
||||
|
||||
err := g.Wait()
|
||||
service.log.Info("Stopped CleanUpService", "reason", err)
|
||||
return err
|
||||
}
|
||||
|
||||
func (service *CleanUpService) start(ctx context.Context) error {
|
||||
service.cleanUpTmpFiles()
|
||||
|
||||
ticker := time.NewTicker(time.Minute * 10)
|
||||
|
||||
@@ -5,13 +5,23 @@ import (
|
||||
|
||||
"github.com/grafana/grafana/pkg/bus"
|
||||
m "github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/registry"
|
||||
)
|
||||
|
||||
func Init() {
|
||||
bus.AddHandler("search", searchHandler)
|
||||
func init() {
|
||||
registry.RegisterService(&SearchService{})
|
||||
}
|
||||
|
||||
func searchHandler(query *Query) error {
|
||||
type SearchService struct {
|
||||
Bus bus.Bus `inject:""`
|
||||
}
|
||||
|
||||
func (s *SearchService) Init() error {
|
||||
s.Bus.AddHandler(s.searchHandler)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *SearchService) searchHandler(query *Query) error {
|
||||
dashQuery := FindPersistedDashboardsQuery{
|
||||
Title: query.Title,
|
||||
SignedInUser: query.SignedInUser,
|
||||
|
||||
@@ -12,6 +12,7 @@ func TestSearch(t *testing.T) {
|
||||
|
||||
Convey("Given search query", t, func() {
|
||||
query := Query{Limit: 2000, SignedInUser: &m.SignedInUser{IsGrafanaAdmin: true}}
|
||||
ss := &SearchService{}
|
||||
|
||||
bus.AddHandler("test", func(query *FindPersistedDashboardsQuery) error {
|
||||
query.Result = HitList{
|
||||
@@ -35,7 +36,7 @@ func TestSearch(t *testing.T) {
|
||||
})
|
||||
|
||||
Convey("That is empty", func() {
|
||||
err := searchHandler(&query)
|
||||
err := ss.searchHandler(&query)
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
Convey("should return sorted results", func() {
|
||||
|
||||
@@ -77,7 +77,7 @@ func EnsureAdminUser() {
|
||||
log.Info("Created default admin user: %v", setting.AdminUser)
|
||||
}
|
||||
|
||||
func NewEngine() {
|
||||
func NewEngine() *xorm.Engine {
|
||||
x, err := getEngine()
|
||||
|
||||
if err != nil {
|
||||
@@ -91,6 +91,8 @@ func NewEngine() {
|
||||
sqlog.Error("Fail to initialize orm engine", "error", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
return x
|
||||
}
|
||||
|
||||
func SetEngine(engine *xorm.Engine) (err error) {
|
||||
|
||||
@@ -45,9 +45,11 @@ var (
|
||||
InstanceName string
|
||||
|
||||
// build
|
||||
BuildVersion string
|
||||
BuildCommit string
|
||||
BuildStamp int64
|
||||
BuildVersion string
|
||||
BuildCommit string
|
||||
BuildStamp int64
|
||||
Enterprise bool
|
||||
ApplicationName string
|
||||
|
||||
// Paths
|
||||
LogsPath string
|
||||
@@ -486,6 +488,11 @@ func NewConfigContext(args *CommandLineArgs) error {
|
||||
return err
|
||||
}
|
||||
|
||||
ApplicationName = "Grafana"
|
||||
if Enterprise {
|
||||
ApplicationName += " Enterprise"
|
||||
}
|
||||
|
||||
Env = Cfg.Section("").Key("app_mode").MustString("development")
|
||||
InstanceName = Cfg.Section("").Key("instance_name").MustString("unknown_instance_name")
|
||||
PluginsPath = makeAbsolute(Cfg.Section("paths").Key("plugins").String(), HomePath)
|
||||
|
||||
Reference in New Issue
Block a user