Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Support
Keyboard shortcuts
?
Submit feedback
Contribute to GitLab
Sign in / Register
Toggle navigation
C
caddy
Project overview
Project overview
Details
Activity
Releases
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Issues
0
Issues
0
List
Boards
Labels
Milestones
Merge Requests
0
Merge Requests
0
Analytics
Analytics
Repository
Value Stream
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Create a new issue
Commits
Issue Boards
Open sidebar
nexedi
caddy
Commits
6b3c2212
Commit
6b3c2212
authored
Feb 10, 2018
by
Matthew Holt
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
diagnostics: AppendUnique(), restructure sets, add metrics, fix bugs
parent
703cf7bf
Changes
10
Show whitespace changes
Inline
Side-by-side
Showing
10 changed files
with
139 additions
and
80 deletions
+139
-80
caddy.go
caddy.go
+3
-0
caddy/caddymain/run.go
caddy/caddymain/run.go
+5
-5
caddyfile/parse.go
caddyfile/parse.go
+3
-0
caddyhttp/httpserver/mitm.go
caddyhttp/httpserver/mitm.go
+9
-0
caddyhttp/httpserver/plugin.go
caddyhttp/httpserver/plugin.go
+0
-3
caddyhttp/httpserver/server.go
caddyhttp/httpserver/server.go
+1
-1
caddytls/handshake.go
caddytls/handshake.go
+19
-0
diagnostics/collection.go
diagnostics/collection.go
+23
-51
diagnostics/diagnostics.go
diagnostics/diagnostics.go
+35
-8
plugins.go
plugins.go
+41
-12
No files found.
caddy.go
View file @
6b3c2212
...
@@ -44,6 +44,7 @@ import (
...
@@ -44,6 +44,7 @@ import (
"time"
"time"
"github.com/mholt/caddy/caddyfile"
"github.com/mholt/caddy/caddyfile"
"github.com/mholt/caddy/diagnostics"
)
)
// Configurable application parameters
// Configurable application parameters
...
@@ -573,6 +574,8 @@ func ValidateAndExecuteDirectives(cdyfile Input, inst *Instance, justValidate bo
...
@@ -573,6 +574,8 @@ func ValidateAndExecuteDirectives(cdyfile Input, inst *Instance, justValidate bo
return
err
return
err
}
}
diagnostics
.
Set
(
"num_server_blocks"
,
len
(
sblocks
))
return
executeDirectives
(
inst
,
cdyfile
.
Path
(),
stype
.
Directives
(),
sblocks
,
justValidate
)
return
executeDirectives
(
inst
,
cdyfile
.
Path
(),
stype
.
Directives
(),
sblocks
,
justValidate
)
}
}
...
...
caddy/caddymain/run.go
View file @
6b3c2212
...
@@ -152,18 +152,18 @@ func Run() {
...
@@ -152,18 +152,18 @@ func Run() {
// Begin diagnostics (these are no-ops if diagnostics disabled)
// Begin diagnostics (these are no-ops if diagnostics disabled)
diagnostics
.
Set
(
"caddy_version"
,
appVersion
)
diagnostics
.
Set
(
"caddy_version"
,
appVersion
)
// TODO: plugins
diagnostics
.
Set
(
"num_listeners"
,
len
(
instance
.
Servers
()))
diagnostics
.
Set
(
"num_listeners"
,
len
(
instance
.
Servers
()))
diagnostics
.
Set
(
"server_type"
,
serverType
)
diagnostics
.
Set
(
"os"
,
runtime
.
GOOS
)
diagnostics
.
Set
(
"os"
,
runtime
.
GOOS
)
diagnostics
.
Set
(
"arch"
,
runtime
.
GOARCH
)
diagnostics
.
Set
(
"arch"
,
runtime
.
GOARCH
)
diagnostics
.
Set
(
"cpu"
,
struct
{
diagnostics
.
Set
(
"cpu"
,
struct
{
NumLogical
int
`json:"num_logical
"`
BrandName
string
`json:"brand_name,omitempty
"`
AESNI
bool
`json:"aes_ni
"`
NumLogical
int
`json:"num_logical,omitempty
"`
BrandName
string
`json:"brand_name
"`
AESNI
bool
`json:"aes_ni,omitempty
"`
}{
}{
BrandName
:
cpuid
.
CPU
.
BrandName
,
NumLogical
:
runtime
.
NumCPU
(),
NumLogical
:
runtime
.
NumCPU
(),
AESNI
:
cpuid
.
CPU
.
AesNi
(),
AESNI
:
cpuid
.
CPU
.
AesNi
(),
BrandName
:
cpuid
.
CPU
.
BrandName
,
})
})
diagnostics
.
StartEmitting
()
diagnostics
.
StartEmitting
()
...
...
caddyfile/parse.go
View file @
6b3c2212
...
@@ -20,6 +20,8 @@ import (
...
@@ -20,6 +20,8 @@ import (
"os"
"os"
"path/filepath"
"path/filepath"
"strings"
"strings"
"github.com/mholt/caddy/diagnostics"
)
)
// Parse parses the input just enough to group tokens, in
// Parse parses the input just enough to group tokens, in
...
@@ -369,6 +371,7 @@ func (p *parser) directive() error {
...
@@ -369,6 +371,7 @@ func (p *parser) directive() error {
// The directive itself is appended as a relevant token
// The directive itself is appended as a relevant token
p
.
block
.
Tokens
[
dir
]
=
append
(
p
.
block
.
Tokens
[
dir
],
p
.
tokens
[
p
.
cursor
])
p
.
block
.
Tokens
[
dir
]
=
append
(
p
.
block
.
Tokens
[
dir
],
p
.
tokens
[
p
.
cursor
])
diagnostics
.
AppendUnique
(
"directives"
,
dir
)
for
p
.
Next
()
{
for
p
.
Next
()
{
if
p
.
Val
()
==
"{"
{
if
p
.
Val
()
==
"{"
{
...
...
caddyhttp/httpserver/mitm.go
View file @
6b3c2212
...
@@ -24,6 +24,8 @@ import (
...
@@ -24,6 +24,8 @@ import (
"strconv"
"strconv"
"strings"
"strings"
"sync"
"sync"
"github.com/mholt/caddy/diagnostics"
)
)
// tlsHandler is a http.Handler that will inject a value
// tlsHandler is a http.Handler that will inject a value
...
@@ -97,6 +99,13 @@ func (h *tlsHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
...
@@ -97,6 +99,13 @@ func (h *tlsHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if
checked
{
if
checked
{
r
=
r
.
WithContext
(
context
.
WithValue
(
r
.
Context
(),
MitmCtxKey
,
mitm
))
r
=
r
.
WithContext
(
context
.
WithValue
(
r
.
Context
(),
MitmCtxKey
,
mitm
))
if
mitm
{
go
diagnostics
.
AppendUnique
(
"mitm"
,
"likely"
)
}
else
{
go
diagnostics
.
AppendUnique
(
"mitm"
,
"unlikely"
)
}
}
else
{
go
diagnostics
.
AppendUnique
(
"mitm"
,
"unknown"
)
}
}
if
mitm
&&
h
.
closeOnMITM
{
if
mitm
&&
h
.
closeOnMITM
{
...
...
caddyhttp/httpserver/plugin.go
View file @
6b3c2212
...
@@ -29,7 +29,6 @@ import (
...
@@ -29,7 +29,6 @@ import (
"github.com/mholt/caddy/caddyfile"
"github.com/mholt/caddy/caddyfile"
"github.com/mholt/caddy/caddyhttp/staticfiles"
"github.com/mholt/caddy/caddyhttp/staticfiles"
"github.com/mholt/caddy/caddytls"
"github.com/mholt/caddy/caddytls"
"github.com/mholt/caddy/diagnostics"
)
)
const
serverType
=
"http"
const
serverType
=
"http"
...
@@ -206,8 +205,6 @@ func (h *httpContext) MakeServers() ([]caddy.Server, error) {
...
@@ -206,8 +205,6 @@ func (h *httpContext) MakeServers() ([]caddy.Server, error) {
}
}
}
}
diagnostics
.
Set
(
"num_sites"
,
len
(
h
.
siteConfigs
))
// we must map (group) each config to a bind address
// we must map (group) each config to a bind address
groups
,
err
:=
groupSiteConfigsByListenAddr
(
h
.
siteConfigs
)
groups
,
err
:=
groupSiteConfigsByListenAddr
(
h
.
siteConfigs
)
if
err
!=
nil
{
if
err
!=
nil
{
...
...
caddyhttp/httpserver/server.go
View file @
6b3c2212
...
@@ -346,7 +346,7 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
...
@@ -346,7 +346,7 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
}
}
}()
}()
go
diagnostics
.
AppendUnique
String
(
"user_agent"
,
r
.
Header
.
Get
(
"User-Agent"
))
go
diagnostics
.
AppendUnique
(
"user_agent"
,
r
.
Header
.
Get
(
"User-Agent"
))
// copy the original, unchanged URL into the context
// copy the original, unchanged URL into the context
// so it can be referenced by middlewares
// so it can be referenced by middlewares
...
...
caddytls/handshake.go
View file @
6b3c2212
...
@@ -25,6 +25,8 @@ import (
...
@@ -25,6 +25,8 @@ import (
"sync"
"sync"
"sync/atomic"
"sync/atomic"
"time"
"time"
"github.com/mholt/caddy/diagnostics"
)
)
// configGroup is a type that keys configs by their hostname
// configGroup is a type that keys configs by their hostname
...
@@ -98,6 +100,23 @@ func (cg configGroup) GetConfigForClient(clientHello *tls.ClientHelloInfo) (*tls
...
@@ -98,6 +100,23 @@ func (cg configGroup) GetConfigForClient(clientHello *tls.ClientHelloInfo) (*tls
//
//
// This method is safe for use as a tls.Config.GetCertificate callback.
// This method is safe for use as a tls.Config.GetCertificate callback.
func
(
cfg
*
Config
)
GetCertificate
(
clientHello
*
tls
.
ClientHelloInfo
)
(
*
tls
.
Certificate
,
error
)
{
func
(
cfg
*
Config
)
GetCertificate
(
clientHello
*
tls
.
ClientHelloInfo
)
(
*
tls
.
Certificate
,
error
)
{
go
diagnostics
.
Append
(
"client_hello"
,
struct
{
NoSNI
bool
`json:"no_sni,omitempty"`
CipherSuites
[]
uint16
`json:"cipher_suites,omitempty"`
SupportedCurves
[]
tls
.
CurveID
`json:"curves,omitempty"`
SupportedPoints
[]
uint8
`json:"points,omitempty"`
SignatureSchemes
[]
tls
.
SignatureScheme
`json:"sig_scheme,omitempty"`
ALPN
[]
string
`json:"alpn,omitempty"`
SupportedVersions
[]
uint16
`json:"versions,omitempty"`
}{
NoSNI
:
clientHello
.
ServerName
==
""
,
CipherSuites
:
clientHello
.
CipherSuites
,
SupportedCurves
:
clientHello
.
SupportedCurves
,
SupportedPoints
:
clientHello
.
SupportedPoints
,
SignatureSchemes
:
clientHello
.
SignatureSchemes
,
ALPN
:
clientHello
.
SupportedProtos
,
SupportedVersions
:
clientHello
.
SupportedVersions
,
})
cert
,
err
:=
cfg
.
getCertDuringHandshake
(
strings
.
ToLower
(
clientHello
.
ServerName
),
true
,
true
)
cert
,
err
:=
cfg
.
getCertDuringHandshake
(
strings
.
ToLower
(
clientHello
.
ServerName
),
true
,
true
)
return
&
cert
.
Certificate
,
err
return
&
cert
.
Certificate
,
err
}
}
...
...
diagnostics/collection.go
View file @
6b3c2212
...
@@ -113,7 +113,7 @@ func Set(key string, val interface{}) {
...
@@ -113,7 +113,7 @@ func Set(key string, val interface{}) {
// Append appends value to a list named key.
// Append appends value to a list named key.
// If key is new, a new list will be created.
// If key is new, a new list will be created.
// If key maps to a type that is not a list,
// If key maps to a type that is not a list,
// a
n error
is logged, and this is a no-op.
// a
panic
is logged, and this is a no-op.
//
//
// TODO: is this function needed/useful?
// TODO: is this function needed/useful?
func
Append
(
key
string
,
value
interface
{})
{
func
Append
(
key
string
,
value
interface
{})
{
...
@@ -142,66 +142,38 @@ func Append(key string, value interface{}) {
...
@@ -142,66 +142,38 @@ func Append(key string, value interface{}) {
bufferMu
.
Unlock
()
bufferMu
.
Unlock
()
}
}
// AppendUnique
String adds value to a set named
key.
// AppendUnique
adds value to a set named
key.
// Set items are unordered. Values in the set
// Set items are unordered. Values in the set
// are unique, but repeat values are counted.
// are unique, but how many times they are
// appended is counted.
//
//
// If key is new, a new set will be created.
// If key is new, a new set will be created for
// If key maps to a type that is not a string
// values with that key. If key maps to a type
// set, an error is logged, and this is a no-op.
// that is not a counting set, a panic is logged,
func
AppendUniqueString
(
key
,
value
string
)
{
// and this is a no-op.
func
AppendUnique
(
key
string
,
value
interface
{})
{
if
!
enabled
{
if
!
enabled
{
return
return
}
}
bufferMu
.
Lock
()
bufferMu
.
Lock
()
if
bufferItemCount
>=
maxBufferItems
{
bufferMu
.
Unlock
()
return
}
bufVal
,
inBuffer
:=
buffer
[
key
]
bufVal
,
inBuffer
:=
buffer
[
key
]
mapVal
,
mapOk
:=
bufVal
.
(
map
[
string
]
int
)
setVal
,
setOk
:=
bufVal
.
(
countingSet
)
if
inBuffer
&&
!
mapOk
{
if
inBuffer
&&
!
setOk
{
bufferMu
.
Unlock
()
log
.
Printf
(
"[PANIC] Diagnostics: key %s already used for non-map value"
,
key
)
return
}
if
mapVal
==
nil
{
buffer
[
key
]
=
map
[
string
]
int
{
value
:
1
}
bufferItemCount
++
}
else
if
mapOk
{
mapVal
[
value
]
++
}
bufferMu
.
Unlock
()
bufferMu
.
Unlock
()
}
log
.
Printf
(
"[PANIC] Diagnostics: key %s already used for non-counting-set value"
,
key
)
// AppendUniqueInt adds value to a set named key.
// Set items are unordered. Values in the set
// are unique, but repeat values are counted.
//
// If key is new, a new set will be created.
// If key maps to a type that is not an integer
// set, an error is logged, and this is a no-op.
func
AppendUniqueInt
(
key
string
,
value
int
)
{
if
!
enabled
{
return
return
}
}
bufferMu
.
Lock
()
if
setVal
==
nil
{
// ensure the buffer is not too full, then add new unique value
if
bufferItemCount
>=
maxBufferItems
{
if
bufferItemCount
>=
maxBufferItems
{
bufferMu
.
Unlock
()
bufferMu
.
Unlock
()
return
return
}
}
bufVal
,
inBuffer
:=
buffer
[
key
]
buffer
[
key
]
=
countingSet
{
value
:
1
}
mapVal
,
mapOk
:=
bufVal
.
(
map
[
int
]
int
)
if
inBuffer
&&
!
mapOk
{
bufferMu
.
Unlock
()
log
.
Printf
(
"[PANIC] Diagnostics: key %s already used for non-map value"
,
key
)
return
}
if
mapVal
==
nil
{
buffer
[
key
]
=
map
[
int
]
int
{
value
:
1
}
bufferItemCount
++
bufferItemCount
++
}
else
if
mapOk
{
}
else
if
setOk
{
mapVal
[
value
]
++
// unique value already exists, so just increment counter
setVal
[
value
]
++
}
}
bufferMu
.
Unlock
()
bufferMu
.
Unlock
()
}
}
...
@@ -209,7 +181,7 @@ func AppendUniqueInt(key string, value int) {
...
@@ -209,7 +181,7 @@ func AppendUniqueInt(key string, value int) {
// Increment adds 1 to a value named key.
// Increment adds 1 to a value named key.
// If it does not exist, it is created with
// If it does not exist, it is created with
// a value of 1. If key maps to a type that
// a value of 1. If key maps to a type that
// is not an integer, a
n error
is logged,
// is not an integer, a
panic
is logged,
// and this is a no-op.
// and this is a no-op.
func
Increment
(
key
string
)
{
func
Increment
(
key
string
)
{
incrementOrDecrement
(
key
,
true
)
incrementOrDecrement
(
key
,
true
)
...
...
diagnostics/diagnostics.go
View file @
6b3c2212
...
@@ -21,13 +21,16 @@
...
@@ -21,13 +21,16 @@
// collection/aggregation functions. Call StartEmitting() when you are
// collection/aggregation functions. Call StartEmitting() when you are
// ready to begin sending diagnostic updates.
// ready to begin sending diagnostic updates.
//
//
// When collecting metrics (functions like Set, Append
*
, or Increment),
// When collecting metrics (functions like Set, Append
Unique
, or Increment),
// it may be desirable and even recommended to
run
invoke them in a new
// it may be desirable and even recommended to invoke them in a new
// goroutine (use the go keyword) in case there is lock contention;
// goroutine (use the go keyword) in case there is lock contention;
// they are thread-safe (unless noted), and you may not want them to
// they are thread-safe (unless noted), and you may not want them to
// block the main thread of execution. However, sometimes blocking
// block the main thread of execution. However, sometimes blocking
// may be necessary too; for example, adding startup metrics to the
// may be necessary too; for example, adding startup metrics to the
// buffer before the call to StartEmitting().
// buffer before the call to StartEmitting().
//
// This package is designed to be as fast and space-efficient as reasonably
// possible, so that it does not disrupt the flow of execution.
package
diagnostics
package
diagnostics
import
(
import
(
...
@@ -122,11 +125,6 @@ func emit(final bool) error {
...
@@ -122,11 +125,6 @@ func emit(final bool) error {
continue
continue
}
}
// ensure we won't slam the diagnostics server
if
reply
.
NextUpdate
<
1
*
time
.
Second
{
reply
.
NextUpdate
=
defaultUpdateInterval
}
// make sure we didn't send the update too soon; if so,
// make sure we didn't send the update too soon; if so,
// just wait and try again -- this is a special case of
// just wait and try again -- this is a special case of
// error that we handle differently, as you can see
// error that we handle differently, as you can see
...
@@ -151,6 +149,11 @@ func emit(final bool) error {
...
@@ -151,6 +149,11 @@ func emit(final bool) error {
// schedule the next update using our default update
// schedule the next update using our default update
// interval because the server might be healthy later
// interval because the server might be healthy later
// ensure we won't slam the diagnostics server
if
reply
.
NextUpdate
<
1
*
time
.
Second
{
reply
.
NextUpdate
=
defaultUpdateInterval
}
// schedule the next update (if this wasn't the last one and
// schedule the next update (if this wasn't the last one and
// if the remote server didn't tell us to stop sending)
// if the remote server didn't tell us to stop sending)
if
!
final
&&
!
reply
.
Stop
{
if
!
final
&&
!
reply
.
Stop
{
...
@@ -216,6 +219,30 @@ type Payload struct {
...
@@ -216,6 +219,30 @@ type Payload struct {
Data
map
[
string
]
interface
{}
`json:"data,omitempty"`
Data
map
[
string
]
interface
{}
`json:"data,omitempty"`
}
}
// countingSet implements a set that counts how many
// times a key is inserted. It marshals to JSON in a
// way such that keys are converted to values next
// to their associated counts.
type
countingSet
map
[
interface
{}]
int
// MarshalJSON implements the json.Marshaler interface.
// It converts the set to an array so that the values
// are JSON object values instead of keys, since keys
// are difficult to query in databases.
func
(
s
countingSet
)
MarshalJSON
()
([]
byte
,
error
)
{
type
Item
struct
{
Value
interface
{}
`json:"value"`
Count
int
`json:"count"`
}
var
list
[]
Item
for
k
,
v
:=
range
s
{
list
=
append
(
list
,
Item
{
Value
:
k
,
Count
:
v
})
}
return
json
.
Marshal
(
list
)
}
var
(
var
(
// httpClient should be used for HTTP requests. It
// httpClient should be used for HTTP requests. It
// is configured with a timeout for reliability.
// is configured with a timeout for reliability.
...
@@ -253,7 +280,7 @@ var (
...
@@ -253,7 +280,7 @@ var (
const
(
const
(
// endpoint is the base URL to remote diagnostics server;
// endpoint is the base URL to remote diagnostics server;
// the instance ID will be appended to it.
// the instance ID will be appended to it.
endpoint
=
"https://diagnostics-staging.caddyserver.com/update/"
// TODO: make configurable, "http://localhost:808
1
/update/"
endpoint
=
"https://diagnostics-staging.caddyserver.com/update/"
// TODO: make configurable, "http://localhost:808
5
/update/"
// defaultUpdateInterval is how long to wait before emitting
// defaultUpdateInterval is how long to wait before emitting
// more diagnostic data. This value is only used if the
// more diagnostic data. This value is only used if the
...
...
plugins.go
View file @
6b3c2212
...
@@ -53,29 +53,59 @@ var (
...
@@ -53,29 +53,59 @@ var (
// DescribePlugins returns a string describing the registered plugins.
// DescribePlugins returns a string describing the registered plugins.
func
DescribePlugins
()
string
{
func
DescribePlugins
()
string
{
pl
:=
ListPlugins
()
str
:=
"Server types:
\n
"
str
:=
"Server types:
\n
"
for
name
:=
range
serverTypes
{
for
_
,
name
:=
range
pl
[
"server_types"
]
{
str
+=
" "
+
name
+
"
\n
"
str
+=
" "
+
name
+
"
\n
"
}
}
// List the loaders in registration order
str
+=
"
\n
Caddyfile loaders:
\n
"
str
+=
"
\n
Caddyfile loaders:
\n
"
for
_
,
name
:=
range
pl
[
"caddyfile_loaders"
]
{
str
+=
" "
+
name
+
"
\n
"
}
if
len
(
eventHooks
)
>
0
{
str
+=
"
\n
Event hook plugins:
\n
"
for
_
,
name
:=
range
pl
[
"event_hooks"
]
{
str
+=
" hook."
+
name
+
"
\n
"
}
}
str
+=
"
\n
Other plugins:
\n
"
for
_
,
name
:=
range
pl
[
"others"
]
{
str
+=
" "
+
name
+
"
\n
"
}
return
str
}
// ListPlugins makes a list of the registered plugins,
// keyed by plugin type.
func
ListPlugins
()
map
[
string
][]
string
{
p
:=
make
(
map
[
string
][]
string
)
// server type plugins
for
name
:=
range
serverTypes
{
p
[
"server_types"
]
=
append
(
p
[
"server_types"
],
name
)
}
// caddyfile loaders in registration order
for
_
,
loader
:=
range
caddyfileLoaders
{
for
_
,
loader
:=
range
caddyfileLoaders
{
str
+=
" "
+
loader
.
name
+
"
\n
"
p
[
"caddyfile_loaders"
]
=
append
(
p
[
"caddyfile_loaders"
],
loader
.
name
)
}
}
if
defaultCaddyfileLoader
.
name
!=
""
{
if
defaultCaddyfileLoader
.
name
!=
""
{
str
+=
" "
+
defaultCaddyfileLoader
.
name
+
"
\n
"
p
[
"caddyfile_loaders"
]
=
append
(
p
[
"caddyfile_loaders"
],
defaultCaddyfileLoader
.
name
)
}
}
// event hook plugins
if
len
(
eventHooks
)
>
0
{
if
len
(
eventHooks
)
>
0
{
// List the event hook plugins
for
name
:=
range
eventHooks
{
str
+=
"
\n
Event hook plugins:
\n
"
p
[
"event_hooks"
]
=
append
(
p
[
"event_hooks"
],
name
)
for
hookPlugin
:=
range
eventHooks
{
str
+=
" hook."
+
hookPlugin
+
"
\n
"
}
}
}
}
//
Let's alphabetize the rest of these...
//
alphabetize the rest of the plugins
var
others
[]
string
var
others
[]
string
for
stype
,
stypePlugins
:=
range
plugins
{
for
stype
,
stypePlugins
:=
range
plugins
{
for
name
:=
range
stypePlugins
{
for
name
:=
range
stypePlugins
{
...
@@ -89,12 +119,11 @@ func DescribePlugins() string {
...
@@ -89,12 +119,11 @@ func DescribePlugins() string {
}
}
sort
.
Strings
(
others
)
sort
.
Strings
(
others
)
str
+=
"
\n
Other plugins:
\n
"
for
_
,
name
:=
range
others
{
for
_
,
name
:=
range
others
{
str
+=
" "
+
name
+
"
\n
"
p
[
"others"
]
=
append
(
p
[
"others"
],
name
)
}
}
return
str
return
p
}
}
// ValidDirectives returns the list of all directives that are
// ValidDirectives returns the list of all directives that are
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment