2019-06-30 16:07:58 -06:00
// Copyright 2015 Matthew Holt and The Caddy Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
2019-06-28 15:39:41 -06:00
package caddycmd
import (
2020-05-15 23:49:51 +02:00
"bufio"
2019-06-28 15:39:41 -06:00
"bytes"
2024-03-05 14:26:30 -05:00
"encoding/json"
2023-10-11 11:46:18 -04:00
"errors"
2019-06-28 15:39:41 -06:00
"flag"
"fmt"
"io"
2024-01-02 08:48:55 +03:00
"io/fs"
2020-05-12 11:36:20 -06:00
"log"
2025-01-27 17:32:24 +01:00
"log/slog"
2019-06-28 15:39:41 -06:00
"net"
"os"
2019-12-31 16:47:35 -07:00
"path/filepath"
"runtime"
2020-05-12 11:36:20 -06:00
"runtime/debug"
2019-10-01 12:23:58 +09:00
"strconv"
"strings"
"time"
2019-07-12 10:07:11 -06:00
2025-01-27 17:32:24 +01:00
"github.com/KimMachineGun/automemlimit/memlimit"
2020-03-23 14:43:42 -06:00
"github.com/caddyserver/certmagic"
2022-08-31 01:38:38 +03:00
"github.com/spf13/pflag"
2024-01-18 11:02:14 +01:00
"go.uber.org/automaxprocs/maxprocs"
2019-12-31 16:47:35 -07:00
"go.uber.org/zap"
2025-01-27 17:32:24 +01:00
"go.uber.org/zap/exp/zapslog"
2023-08-14 23:41:15 +08:00
"github.com/caddyserver/caddy/v2"
"github.com/caddyserver/caddy/v2/caddyconfig"
2019-06-28 15:39:41 -06:00
)
2020-03-23 14:43:42 -06:00
func init ( ) {
// set a fitting User-Agent for ACME requests
2022-08-04 11:16:59 -06:00
version , _ := caddy . Version ( )
cleanModVersion := strings . TrimPrefix ( version , "v" )
2022-09-13 17:21:04 -06:00
ua := "Caddy/" + cleanModVersion
if uaEnv , ok := os . LookupEnv ( "USERAGENT" ) ; ok {
ua = uaEnv + " " + ua
}
certmagic . UserAgent = ua
2020-03-23 14:43:42 -06:00
// by using Caddy, user indicates agreement to CA terms
2022-09-13 17:21:04 -06:00
// (very important, as Caddy is often non-interactive
// and thus ACME account creation will fail!)
2020-03-23 14:43:42 -06:00
certmagic . DefaultACME . Agreed = true
}
2019-07-12 10:07:11 -06:00
// Main implements the main function of the caddy command.
2021-03-29 19:02:21 +02:00
// Call this if Caddy is to be the main() of your program.
2019-06-28 15:39:41 -06:00
func Main ( ) {
2022-09-01 21:41:09 -06:00
if len ( os . Args ) == 0 {
2019-10-28 14:39:37 -06:00
fmt . Printf ( "[FATAL] no arguments provided by OS; args[0] must be command\n" )
2019-10-01 12:23:58 +09:00
os . Exit ( caddy . ExitCodeFailedStartup )
2019-06-28 15:39:41 -06:00
}
2024-08-21 22:29:42 -05:00
if err := defaultFactory . Build ( ) . Execute ( ) ; err != nil {
2023-10-11 11:46:18 -04:00
var exitError * exitError
if errors . As ( err , & exitError ) {
os . Exit ( exitError . ExitCode )
}
2022-08-31 01:38:38 +03:00
os . Exit ( 1 )
2019-06-28 15:39:41 -06:00
}
}
// handlePingbackConn reads from conn and ensures it matches
// the bytes in expect, or returns an error if it doesn't.
func handlePingbackConn ( conn net . Conn , expect [ ] byte ) error {
defer conn . Close ( )
2021-09-30 01:17:48 +08:00
confirmationBytes , err := io . ReadAll ( io . LimitReader ( conn , 32 ) )
2019-06-28 15:39:41 -06:00
if err != nil {
return err
}
if ! bytes . Equal ( confirmationBytes , expect ) {
return fmt . Errorf ( "wrong confirmation: %x" , confirmationBytes )
}
return nil
}
2019-08-09 12:05:47 -06:00
2022-03-02 13:08:36 -05:00
// LoadConfig loads the config from configFile and adapts it
2019-08-09 12:05:47 -06:00
// using adapterName. If adapterName is specified, configFile
2020-01-22 10:04:58 -07:00
// must be also. If no configFile is specified, it tries
// loading a default config file. The lack of a config file is
// not treated as an error, but false will be returned if
// there is no config available. It prints any warnings to stderr,
// and returns the resulting JSON config bytes along with
2022-03-02 13:08:36 -05:00
// the name of the loaded config file (if any).
2025-09-26 12:50:15 -04:00
// The return values are:
// - config bytes (nil if no config)
// - config file used ("" if none)
// - adapter used ("" if none)
// - error, if any
func LoadConfig ( configFile , adapterName string ) ( [ ] byte , string , string , error ) {
2023-05-12 11:04:02 -06:00
return loadConfigWithLogger ( caddy . Log ( ) , configFile , adapterName )
}
2024-06-02 14:40:56 +03:00
func isCaddyfile ( configFile , adapterName string ) ( bool , error ) {
if adapterName == "caddyfile" {
return true , nil
}
// as a special case, if a config file starts with "caddyfile" or
// has a ".caddyfile" extension, and no adapter is specified, and
// no adapter module name matches the extension, assume
// caddyfile adapter for convenience
baseConfig := strings . ToLower ( filepath . Base ( configFile ) )
baseConfigExt := filepath . Ext ( baseConfig )
startsOrEndsInCaddyfile := strings . HasPrefix ( baseConfig , "caddyfile" ) || strings . HasSuffix ( baseConfig , ".caddyfile" )
if baseConfigExt == ".json" {
return false , nil
}
// If the adapter is not specified,
// the config file starts with "caddyfile",
// the config file has an extension,
// and isn't a JSON file (e.g. Caddyfile.yaml),
// then we don't know what the config format is.
2024-06-05 17:57:15 +03:00
if adapterName == "" && startsOrEndsInCaddyfile {
2024-06-02 14:40:56 +03:00
return true , nil
}
2024-06-05 17:57:15 +03:00
// adapter is not empty,
// adapter is not "caddyfile",
// extension is not ".json",
// extension is not ".caddyfile"
// file does not start with "Caddyfile"
2024-06-02 14:40:56 +03:00
return false , nil
}
2025-09-26 12:50:15 -04:00
func loadConfigWithLogger ( logger * zap . Logger , configFile , adapterName string ) ( [ ] byte , string , string , error ) {
2024-03-05 14:26:30 -05:00
// if no logger is provided, use a nop logger
// just so we don't have to check for nil
if logger == nil {
logger = zap . NewNop ( )
}
2019-08-09 12:05:47 -06:00
// specifying an adapter without a config file is ambiguous
2020-01-22 09:33:22 -07:00
if adapterName != "" && configFile == "" {
2025-09-26 12:50:15 -04:00
return nil , "" , "" , fmt . Errorf ( "cannot adapt config without config file (use --config)" )
2019-08-09 12:05:47 -06:00
}
// load initial config and adapter
var config [ ] byte
var cfgAdapter caddyconfig . Adapter
var err error
if configFile != "" {
2020-12-03 18:02:18 +01:00
if configFile == "-" {
2021-09-30 01:17:48 +08:00
config , err = io . ReadAll ( os . Stdin )
2024-03-05 14:26:30 -05:00
if err != nil {
2025-09-26 12:50:15 -04:00
return nil , "" , "" , fmt . Errorf ( "reading config from stdin: %v" , err )
2024-03-05 14:26:30 -05:00
}
logger . Info ( "using config from stdin" )
2020-12-03 18:02:18 +01:00
} else {
2021-09-30 01:17:48 +08:00
config , err = os . ReadFile ( configFile )
2024-03-05 14:26:30 -05:00
if err != nil {
2025-09-26 12:50:15 -04:00
return nil , "" , "" , fmt . Errorf ( "reading config from file: %v" , err )
2024-03-05 14:26:30 -05:00
}
logger . Info ( "using config from file" , zap . String ( "file" , configFile ) )
2023-05-12 11:04:02 -06:00
}
2019-08-09 12:05:47 -06:00
} else if adapterName == "" {
2023-08-09 19:40:37 +02:00
// if the Caddyfile adapter is plugged in, we can try using an
// adjacent Caddyfile by default
2019-08-09 12:05:47 -06:00
cfgAdapter = caddyconfig . GetAdapter ( "caddyfile" )
if cfgAdapter != nil {
2021-09-30 01:17:48 +08:00
config , err = os . ReadFile ( "Caddyfile" )
2024-01-02 08:48:55 +03:00
if errors . Is ( err , fs . ErrNotExist ) {
2019-09-05 14:58:07 -06:00
// okay, no default Caddyfile; pretend like this never happened
cfgAdapter = nil
} else if err != nil {
// default Caddyfile exists, but error reading it
2025-09-26 12:50:15 -04:00
return nil , "" , "" , fmt . Errorf ( "reading default Caddyfile: %v" , err )
2019-09-05 14:58:07 -06:00
} else {
// success reading default Caddyfile
configFile = "Caddyfile"
2024-03-05 14:26:30 -05:00
logger . Info ( "using adjacent Caddyfile" )
2019-08-09 12:05:47 -06:00
}
}
}
2024-06-02 14:40:56 +03:00
if yes , err := isCaddyfile ( configFile , adapterName ) ; yes {
2020-01-09 14:00:32 -07:00
adapterName = "caddyfile"
2024-06-02 14:40:56 +03:00
} else if err != nil {
2025-09-26 12:50:15 -04:00
return nil , "" , "" , err
2020-01-09 14:00:32 -07:00
}
2019-08-09 12:05:47 -06:00
// load config adapter
if adapterName != "" {
cfgAdapter = caddyconfig . GetAdapter ( adapterName )
if cfgAdapter == nil {
2025-09-26 12:50:15 -04:00
return nil , "" , "" , fmt . Errorf ( "unrecognized config adapter: %s" , adapterName )
2019-08-09 12:05:47 -06:00
}
}
// adapt config
if cfgAdapter != nil {
2022-08-02 16:39:09 -04:00
adaptedConfig , warnings , err := cfgAdapter . Adapt ( config , map [ string ] any {
2019-08-09 12:05:47 -06:00
"filename" : configFile ,
} )
if err != nil {
2025-09-26 12:50:15 -04:00
return nil , "" , "" , fmt . Errorf ( "adapting config using %s: %v" , adapterName , err )
2019-08-09 12:05:47 -06:00
}
2024-03-05 14:26:30 -05:00
logger . Info ( "adapted config to JSON" , zap . String ( "adapter" , adapterName ) )
2019-08-09 12:05:47 -06:00
for _ , warn := range warnings {
msg := warn . Message
if warn . Directive != "" {
msg = fmt . Sprintf ( "%s: %s" , warn . Directive , warn . Message )
}
2024-03-05 14:26:30 -05:00
logger . Warn ( msg ,
zap . String ( "adapter" , adapterName ) ,
zap . String ( "file" , warn . File ) ,
zap . Int ( "line" , warn . Line ) )
2019-08-09 12:05:47 -06:00
}
config = adaptedConfig
2024-04-19 00:40:12 +03:00
} else if len ( config ) != 0 {
2024-03-05 14:26:30 -05:00
// validate that the config is at least valid JSON
err = json . Unmarshal ( config , new ( any ) )
if err != nil {
2026-01-14 22:54:19 -05:00
if jsonErr , ok := err . ( * json . SyntaxError ) ; ok {
return nil , "" , "" , fmt . Errorf ( "config is not valid JSON: %w, at offset %d; did you mean to use a config adapter (the --adapter flag)?" , err , jsonErr . Offset )
}
return nil , "" , "" , fmt . Errorf ( "config is not valid JSON: %w; did you mean to use a config adapter (the --adapter flag)?" , err )
2024-03-05 14:26:30 -05:00
}
2019-08-09 12:05:47 -06:00
}
2025-09-26 12:50:15 -04:00
return config , configFile , adapterName , nil
2020-03-22 22:58:24 -06:00
}
// watchConfigFile watches the config file at filename for changes
// and reloads the config if the file was updated. This function
// blocks indefinitely; it only quits if the poller has errors for
// long enough time. The filename passed in must be the actual
// config file used, not one to be discovered.
2023-05-09 01:49:16 +03:00
// Each second the config files is loaded and parsed into an object
// and is compared to the last config object that was loaded
2020-03-22 22:58:24 -06:00
func watchConfigFile ( filename , adapterName string ) {
2020-05-12 11:36:20 -06:00
defer func ( ) {
if err := recover ( ) ; err != nil {
log . Printf ( "[PANIC] watching config file: %v\n%s" , err , debug . Stack ( ) )
}
} ( )
2020-03-22 22:58:24 -06:00
// make our logger; since config reloads can change the
// default logger, we need to get it dynamically each time
logger := func ( ) * zap . Logger {
return caddy . Log ( ) .
Named ( "watcher" ) .
With ( zap . String ( "config_file" , filename ) )
}
2023-05-09 01:49:16 +03:00
// get current config
2025-09-26 12:50:15 -04:00
lastCfg , _ , _ , err := loadConfigWithLogger ( nil , filename , adapterName )
2020-03-22 22:58:24 -06:00
if err != nil {
2023-05-09 01:49:16 +03:00
logger ( ) . Error ( "unable to load latest config" , zap . Error ( err ) )
2020-03-22 22:58:24 -06:00
return
}
logger ( ) . Info ( "watching config file for changes" )
// begin poller
2020-11-22 16:50:29 -05:00
//nolint:staticcheck
2020-03-22 22:58:24 -06:00
for range time . Tick ( 1 * time . Second ) {
2023-05-09 01:49:16 +03:00
// get current config
2025-09-26 12:50:15 -04:00
newCfg , _ , _ , err := loadConfigWithLogger ( nil , filename , adapterName )
2020-03-22 22:58:24 -06:00
if err != nil {
2023-05-09 01:49:16 +03:00
logger ( ) . Error ( "unable to load latest config" , zap . Error ( err ) )
return
2020-03-22 22:58:24 -06:00
}
// if it hasn't changed, nothing to do
2023-05-09 01:49:16 +03:00
if bytes . Equal ( lastCfg , newCfg ) {
2020-03-22 22:58:24 -06:00
continue
}
logger ( ) . Info ( "config file changed; reloading" )
2023-05-09 01:49:16 +03:00
// remember the current config
lastCfg = newCfg
2020-03-22 22:58:24 -06:00
// apply the updated config
2023-05-09 01:49:16 +03:00
err = caddy . Load ( lastCfg , false )
2020-03-22 22:58:24 -06:00
if err != nil {
logger ( ) . Error ( "applying latest config" , zap . Error ( err ) )
continue
}
}
2019-08-09 12:05:47 -06:00
}
2019-10-01 12:23:58 +09:00
// Flags wraps a FlagSet so that typed values
// from flags can be easily retrieved.
type Flags struct {
2022-08-31 01:38:38 +03:00
* pflag . FlagSet
2019-10-01 12:23:58 +09:00
}
// String returns the string representation of the
// flag given by name. It panics if the flag is not
// in the flag set.
func ( f Flags ) String ( name string ) string {
return f . FlagSet . Lookup ( name ) . Value . String ( )
}
// Bool returns the boolean representation of the
// flag given by name. It returns false if the flag
// is not a boolean type. It panics if the flag is
// not in the flag set.
func ( f Flags ) Bool ( name string ) bool {
val , _ := strconv . ParseBool ( f . String ( name ) )
return val
}
// Int returns the integer representation of the
// flag given by name. It returns 0 if the flag
// is not an integer type. It panics if the flag is
// not in the flag set.
func ( f Flags ) Int ( name string ) int {
val , _ := strconv . ParseInt ( f . String ( name ) , 0 , strconv . IntSize )
return int ( val )
}
// Float64 returns the float64 representation of the
// flag given by name. It returns false if the flag
2021-03-29 19:02:21 +02:00
// is not a float64 type. It panics if the flag is
2019-10-01 12:23:58 +09:00
// not in the flag set.
func ( f Flags ) Float64 ( name string ) float64 {
val , _ := strconv . ParseFloat ( f . String ( name ) , 64 )
return val
}
// Duration returns the duration representation of the
// flag given by name. It returns false if the flag
// is not a duration type. It panics if the flag is
// not in the flag set.
func ( f Flags ) Duration ( name string ) time . Duration {
2020-05-11 18:41:11 -04:00
val , _ := caddy . ParseDuration ( f . String ( name ) )
2019-10-01 12:23:58 +09:00
return val
}
2020-05-15 23:49:51 +02:00
func loadEnvFromFile ( envFile string ) error {
file , err := os . Open ( envFile )
if err != nil {
return fmt . Errorf ( "reading environment file: %v" , err )
}
defer file . Close ( )
envMap , err := parseEnvFile ( file )
if err != nil {
return fmt . Errorf ( "parsing environment file: %v" , err )
}
for k , v := range envMap {
2023-09-06 19:19:24 -07:00
// do not overwrite existing environment variables
_ , exists := os . LookupEnv ( k )
if ! exists {
if err := os . Setenv ( k , v ) ; err != nil {
return fmt . Errorf ( "setting environment variables: %v" , err )
}
2020-05-15 23:49:51 +02:00
}
}
2021-08-20 23:51:31 +02:00
// Update the storage paths to ensure they have the proper
// value after loading a specified env file.
caddy . ConfigAutosavePath = filepath . Join ( caddy . AppConfigDir ( ) , "autosave.json" )
caddy . DefaultStorage = & certmagic . FileStorage { Path : caddy . AppDataDir ( ) }
2020-05-15 23:49:51 +02:00
return nil
}
2022-04-13 11:35:28 -06:00
// parseEnvFile parses an env file from KEY=VALUE format.
// It's pretty naive. Limited value quotation is supported,
// but variable and command expansions are not supported.
2020-05-15 23:49:51 +02:00
func parseEnvFile ( envInput io . Reader ) ( map [ string ] string , error ) {
envMap := make ( map [ string ] string )
scanner := bufio . NewScanner ( envInput )
2022-04-13 11:35:28 -06:00
var lineNumber int
2020-05-15 23:49:51 +02:00
for scanner . Scan ( ) {
2022-04-13 11:35:28 -06:00
line := strings . TrimSpace ( scanner . Text ( ) )
2020-05-15 23:49:51 +02:00
lineNumber ++
2022-04-13 11:35:28 -06:00
// skip empty lines and lines starting with comment
if line == "" || strings . HasPrefix ( line , "#" ) {
2020-05-15 23:49:51 +02:00
continue
}
2022-04-13 11:35:28 -06:00
// split line into key and value
2022-08-04 19:17:35 +02:00
before , after , isCut := strings . Cut ( line , "=" )
if ! isCut {
2020-05-15 23:49:51 +02:00
return nil , fmt . Errorf ( "can't parse line %d; line should be in KEY=VALUE format" , lineNumber )
}
2022-08-04 19:17:35 +02:00
key , val := before , after
2020-05-15 23:49:51 +02:00
2022-04-13 11:35:28 -06:00
// sometimes keys are prefixed by "export " so file can be sourced in bash; ignore it here
key = strings . TrimPrefix ( key , "export " )
2020-05-15 23:49:51 +02:00
2022-04-13 11:35:28 -06:00
// validate key and value
2020-05-15 23:49:51 +02:00
if key == "" {
return nil , fmt . Errorf ( "missing or empty key on line %d" , lineNumber )
}
2022-04-13 11:35:28 -06:00
if strings . Contains ( key , " " ) {
return nil , fmt . Errorf ( "invalid key on line %d: contains whitespace: %s" , lineNumber , key )
}
if strings . HasPrefix ( val , " " ) || strings . HasPrefix ( val , "\t" ) {
return nil , fmt . Errorf ( "invalid value on line %d: whitespace before value: '%s'" , lineNumber , val )
}
// remove any trailing comment after value
2022-08-07 09:33:37 +05:30
if commentStart , _ , found := strings . Cut ( val , "#" ) ; found {
val = strings . TrimRight ( commentStart , " \t" )
2022-04-13 11:35:28 -06:00
}
// quoted value: support newlines
2023-04-10 13:55:45 -06:00
if strings . HasPrefix ( val , ` " ` ) || strings . HasPrefix ( val , "'" ) {
quote := string ( val [ 0 ] )
2025-06-03 02:24:32 +03:00
for ! strings . HasSuffix ( line , quote ) || strings . HasSuffix ( line , ` \ ` + quote ) {
2023-04-10 13:55:45 -06:00
val = strings . ReplaceAll ( val , ` \ ` + quote , quote )
2022-04-13 11:35:28 -06:00
if ! scanner . Scan ( ) {
break
}
lineNumber ++
2023-04-10 13:55:45 -06:00
line = strings . ReplaceAll ( scanner . Text ( ) , ` \ ` + quote , quote )
2022-04-13 11:35:28 -06:00
val += "\n" + line
}
2023-04-10 13:55:45 -06:00
val = strings . TrimPrefix ( val , quote )
val = strings . TrimSuffix ( val , quote )
2022-04-13 11:35:28 -06:00
}
2020-05-15 23:49:51 +02:00
envMap [ key ] = val
}
if err := scanner . Err ( ) ; err != nil {
return nil , err
}
return envMap , nil
}
2019-10-01 12:23:58 +09:00
func printEnvironment ( ) {
2022-08-04 11:16:59 -06:00
_ , version := caddy . Version ( )
2019-12-31 16:56:19 -07:00
fmt . Printf ( "caddy.HomeDir=%s\n" , caddy . HomeDir ( ) )
fmt . Printf ( "caddy.AppDataDir=%s\n" , caddy . AppDataDir ( ) )
fmt . Printf ( "caddy.AppConfigDir=%s\n" , caddy . AppConfigDir ( ) )
fmt . Printf ( "caddy.ConfigAutosavePath=%s\n" , caddy . ConfigAutosavePath )
2022-08-04 11:16:59 -06:00
fmt . Printf ( "caddy.Version=%s\n" , version )
2019-12-31 16:56:19 -07:00
fmt . Printf ( "runtime.GOOS=%s\n" , runtime . GOOS )
fmt . Printf ( "runtime.GOARCH=%s\n" , runtime . GOARCH )
fmt . Printf ( "runtime.Compiler=%s\n" , runtime . Compiler )
fmt . Printf ( "runtime.NumCPU=%d\n" , runtime . NumCPU ( ) )
fmt . Printf ( "runtime.GOMAXPROCS=%d\n" , runtime . GOMAXPROCS ( 0 ) )
fmt . Printf ( "runtime.Version=%s\n" , runtime . Version ( ) )
cwd , err := os . Getwd ( )
if err != nil {
cwd = fmt . Sprintf ( "<error: %v>" , err )
}
fmt . Printf ( "os.Getwd=%s\n\n" , cwd )
2019-10-01 12:23:58 +09:00
for _ , v := range os . Environ ( ) {
fmt . Println ( v )
}
}
2019-12-31 16:47:35 -07:00
2025-03-06 16:47:02 -07:00
func setResourceLimits ( logger * zap . Logger ) func ( ) {
2025-03-06 15:11:38 -07:00
// Configure the maximum number of CPUs to use to match the Linux container quota (if any)
// See https://pkg.go.dev/runtime#GOMAXPROCS
undo , err := maxprocs . Set ( maxprocs . Logger ( logger . Sugar ( ) . Infof ) )
if err != nil {
2025-03-06 16:47:02 -07:00
logger . Warn ( "failed to set GOMAXPROCS" , zap . Error ( err ) )
2025-03-06 15:11:38 -07:00
}
// Configure the maximum memory to use to match the Linux container quota (if any) or system memory
// See https://pkg.go.dev/runtime/debug#SetMemoryLimit
_ , _ = memlimit . SetGoMemLimitWithOpts (
memlimit . WithLogger (
2026-03-03 16:44:42 -05:00
slog . New ( zapslog . NewHandler (
logger . Core ( ) ,
zapslog . WithName ( "memlimit" ) ,
// the default enables traces at ERROR level, this disables
// them by setting it to a level higher than any other level
zapslog . AddStacktraceAt ( slog . Level ( 127 ) ) ,
) ) ,
2025-03-06 15:11:38 -07:00
) ,
memlimit . WithProvider (
memlimit . ApplyFallback (
memlimit . FromCgroup ,
memlimit . FromSystem ,
) ,
) ,
)
2025-03-06 16:47:02 -07:00
return undo
2025-03-06 15:11:38 -07:00
}
2022-08-01 13:36:22 -06:00
// StringSlice is a flag.Value that enables repeated use of a string flag.
type StringSlice [ ] string
func ( ss StringSlice ) String ( ) string { return "[" + strings . Join ( ss , ", " ) + "]" }
func ( ss * StringSlice ) Set ( value string ) error {
* ss = append ( * ss , value )
return nil
}
// Interface guard
var _ flag . Value = ( * StringSlice ) ( nil )