github.com/influx6/npkg@v0.8.8/ndaemon/daemon.go (about)

     1  package ndaemon
     2  
     3  import (
     4  	"context"
     5  	"strings"
     6  	"sync"
     7  	"time"
     8  
     9  	"github.com/robfig/cron"
    10  
    11  	"github.com/influx6/npkg/nerror"
    12  	"github.com/takama/daemon"
    13  
    14  	"github.com/influx6/npkg/njson"
    15  )
    16  
    17  var (
    18  	defaultLocation         = time.Now().Location()
    19  	defaultCronParserOption = cron.Second | cron.Minute | cron.Hour | cron.Dom | cron.Month | cron.DowOptional | cron.Descriptor
    20  )
    21  
    22  type Logger interface {
    23  	Log(json *njson.JSON)
    24  }
    25  
    26  type DaemonJob func(ctx context.Context, logger Logger)
    27  
    28  type ctxJob struct {
    29  	ctx    context.Context
    30  	logger Logger
    31  	job    DaemonJob
    32  }
    33  
    34  func (c *ctxJob) Run() {
    35  	c.job(c.ctx, c.logger)
    36  }
    37  
    38  // CronDaemon returns a new cron based daemon which will execute the function
    39  // based on a cron schedule.
    40  //
    41  // See for marker format: http://www.quartz-scheduler.org/documentation/quartz-2.x/tutorials/crontrigger.html
    42  // CRON Expression Format
    43  // A cron expression represents a set of times, using 5 space-separated fields.
    44  // Field name   | Mandatory? | Allowed values  | Allowed special characters
    45  // ----------   | ---------- | --------------  | --------------------------
    46  // Minutes      | Yes        | 0-59            | * / , -
    47  // Hours        | Yes        | 0-23            | * / , -
    48  // Day of month | Yes        | 1-31            | * / , - ?
    49  // Month        | Yes        | 1-12 or JAN-DEC | * / , -
    50  // Day of week  | Yes        | 0-6 or SUN-SAT  | * / , - ?
    51  // Month and Day-of-week field values are case insensitive.  "SUN", "Sun", and "sun" are equally accepted.
    52  // The specific interpretation of the format is based on the Cron Wikipedia page:
    53  // https://en.wikipedia.org/wiki/Cron
    54  //
    55  func CronDaemon(
    56  	ctx context.Context,
    57  	canceler context.CancelFunc,
    58  	cronExpression string,
    59  	location *time.Location,
    60  	overrideParser cron.ParseOption,
    61  	name string,
    62  	desc string,
    63  	logger Logger,
    64  	kind daemon.Kind,
    65  	templateOverride string,
    66  	depends []string,
    67  	do DaemonJob,
    68  ) *ServiceDaemon {
    69  	var daemonService = ServiceDaemon{
    70  		Cancel:           canceler,
    71  		Ctx:              ctx,
    72  		Name:             name,
    73  		Desc:             desc,
    74  		Logger:           logger,
    75  		Kind:             kind,
    76  		DependOnServices: depends,
    77  		DaemonTemplate:   templateOverride,
    78  	}
    79  
    80  	daemonService.Job = func(ctx context.Context, logger Logger) {
    81  		var logStack = njson.Log(logger)
    82  
    83  		var waiter sync.WaitGroup
    84  		waiter.Add(1)
    85  		go func() {
    86  			<-ctx.Done()
    87  			waiter.Done()
    88  		}()
    89  
    90  		var parser = cron.NewParser(overrideParser)
    91  		var cronManager = cron.NewWithLocation(location)
    92  
    93  		var schedule, err = parser.Parse(cronExpression)
    94  		if err != nil {
    95  			var wrapErr = nerror.WrapOnly(err)
    96  			logStack.New().
    97  				LError().
    98  				Message("failed to parse cron expression").
    99  				String("error", wrapErr.Error()).
   100  				End()
   101  			return
   102  		}
   103  
   104  		cronManager.Schedule(schedule, &ctxJob{
   105  			ctx:    ctx,
   106  			logger: logger,
   107  			job:    do,
   108  		})
   109  
   110  		cronManager.Start()
   111  
   112  		waiter.Wait()
   113  	}
   114  
   115  	return &daemonService
   116  }
   117  
   118  // Cron returns a cron ServiceDaemon which uses default location and
   119  // default parser options (e.g "* * * * * *") to define the cron expression.
   120  func Cron(
   121  	ctx context.Context,
   122  	canceler context.CancelFunc,
   123  	cronExpression string,
   124  	name string,
   125  	desc string,
   126  	logger Logger,
   127  	kind daemon.Kind,
   128  	do DaemonJob,
   129  	depends ...string,
   130  ) *ServiceDaemon {
   131  	return CronDaemon(
   132  		ctx,
   133  		canceler,
   134  		cronExpression,
   135  		defaultLocation,
   136  		defaultCronParserOption,
   137  		name,
   138  		desc,
   139  		logger,
   140  		kind,
   141  		"",
   142  		depends,
   143  		do,
   144  	)
   145  }
   146  
   147  type ServiceDaemon struct {
   148  	Name             string
   149  	Desc             string
   150  	Logger           Logger
   151  	Kind             daemon.Kind
   152  	DependOnServices []string
   153  
   154  	// Job function which will be lunched into a goroutine of it's own.
   155  	// If the job immediately returns then the context attached will be
   156  	// cancelled and the daemon killed.
   157  	Job DaemonJob
   158  
   159  	/***
   160  	DaemonTemplate is a go template string to be used to create the service.
   161  	This is optional, but can be modified to suit usage.
   162  
   163  		[Unit]
   164  		Description={{.Description}}
   165  		Requires={{.Dependencies}}
   166  		After={{.Dependencies}}
   167  
   168  		[Service]
   169  		PIDFile=/var/run/{{.Name}}.pid
   170  		ExecStartPre=/bin/rm -f /var/run/{{.Name}}.pid
   171  		ExecStart={{.Path}} {{.Args}}
   172  		Restart=on-failure
   173  
   174  		[Install]
   175  		WantedBy=multi-user.target
   176  	*/
   177  	DaemonTemplate string
   178  
   179  	// Context to use for delivering to operation.
   180  	Ctx context.Context
   181  
   182  	// Cancel function to cancel context when application stops
   183  	// or is removed/uninstalled.
   184  	Cancel context.CancelFunc
   185  }
   186  
   187  func (c *ServiceDaemon) Run(args []string) (string, error) {
   188  	var logStack = njson.Log(c.Logger)
   189  
   190  	var usageHelp = "Usage: " + c.Name + " install | remove | start | stop | status"
   191  
   192  	var serviceDaemon, err = daemon.New(c.Name, c.Desc, c.Kind, c.DependOnServices...)
   193  	if err != nil {
   194  		return "", nerror.WrapOnly(err)
   195  	}
   196  
   197  	// start service details
   198  	if len(args) == 0 {
   199  		var waiter = WaiterForCtxSignal(c.Ctx, c.Cancel)
   200  
   201  		go func() {
   202  			defer c.Cancel()
   203  
   204  			c.Job(c.Ctx, c.Logger)
   205  		}()
   206  
   207  		waiter.Wait()
   208  		return "", nil
   209  	}
   210  
   211  	if len(c.DaemonTemplate) != 0 {
   212  		if err := serviceDaemon.SetTemplate(c.DaemonTemplate); err != nil {
   213  			var wrapErr = nerror.WrapOnly(err)
   214  			logStack.New().
   215  				LError().
   216  				Message("failed to set daemon template").
   217  				String("error", wrapErr.Error()).
   218  				End()
   219  			return "", wrapErr
   220  		}
   221  	}
   222  
   223  	switch strings.ToLower(args[0]) {
   224  	case "install":
   225  		var message, opErr = serviceDaemon.Install(args[1:]...)
   226  		if opErr != nil {
   227  			var wrapErr = nerror.WrapOnly(opErr)
   228  			logStack.New().
   229  				LError().
   230  				Message("failed to complete install operation").
   231  				String("message", message).
   232  				String("error", wrapErr.Error()).
   233  				End()
   234  			return "", wrapErr
   235  		}
   236  		logStack.New().
   237  			LInfo().
   238  			Message("installed daemon").
   239  			String("message", message).
   240  			End()
   241  		return message, nil
   242  	case "remove":
   243  		var message, opErr = serviceDaemon.Remove()
   244  		if opErr != nil {
   245  			var wrapErr = nerror.WrapOnly(opErr)
   246  			logStack.New().
   247  				LError().
   248  				Message("failed to complete remove operation").
   249  				String("message", message).
   250  				String("error", wrapErr.Error()).
   251  				End()
   252  			return "", wrapErr
   253  		}
   254  		logStack.New().
   255  			LInfo().
   256  			Message("removed daemon").
   257  			String("message", message).
   258  			End()
   259  		return message, nil
   260  	case "start":
   261  		var message, opErr = serviceDaemon.Start()
   262  		if opErr != nil {
   263  			var wrapErr = nerror.WrapOnly(opErr)
   264  			logStack.New().
   265  				LError().
   266  				Message("failed to complete start operation").
   267  				String("message", message).
   268  				String("error", wrapErr.Error()).
   269  				End()
   270  			return "", wrapErr
   271  		}
   272  		logStack.New().
   273  			LInfo().
   274  			Message("started daemon").
   275  			String("message", message).
   276  			End()
   277  		return message, nil
   278  	case "stop":
   279  		var message, opErr = serviceDaemon.Stop()
   280  		if opErr != nil {
   281  			var wrapErr = nerror.WrapOnly(opErr)
   282  			logStack.New().
   283  				LError().
   284  				Message("failed to complete start operation").
   285  				String("message", message).
   286  				String("error", wrapErr.Error()).
   287  				End()
   288  			return "", wrapErr
   289  		}
   290  		logStack.New().
   291  			LInfo().
   292  			Message("started daemon").
   293  			String("message", message).
   294  			End()
   295  		return message, nil
   296  	case "status":
   297  		var message, opErr = serviceDaemon.Status()
   298  		if opErr != nil {
   299  			var wrapErr = nerror.WrapOnly(opErr)
   300  			logStack.New().
   301  				LError().
   302  				Message("failed to complete start operation").
   303  				String("message", message).
   304  				String("error", wrapErr.Error()).
   305  				End()
   306  			return "", wrapErr
   307  		}
   308  		logStack.New().
   309  			LInfo().
   310  			Message("started daemon").
   311  			String("message", message).
   312  			End()
   313  		return message, nil
   314  	}
   315  	return usageHelp, nil
   316  }