github.com/swaros/contxt/module/taskrun@v0.0.0-20240305083542-3dbd4436ac40/cmdflow.go (about)

     1  // Copyright (c) 2020 Thomas Ziegler <thomas.zglr@googlemail.com>. All rights reserved.
     2  //
     3  // # Licensed under the MIT License
     4  //
     5  // Permission is hereby granted, free of charge, to any person obtaining a copy
     6  // of this software and associated documentation files (the "Software"), to deal
     7  // in the Software without restriction, including without limitation the rights
     8  // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
     9  // copies of the Software, and to permit persons to whom the Software is
    10  // furnished to do so, subject to the following conditions:
    11  //
    12  // The above copyright notice and this permission notice shall be included in all
    13  // copies or substantial portions of the Software.
    14  //
    15  // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
    16  // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
    17  // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
    18  // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
    19  // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
    20  // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
    21  // SOFTWARE.
    22  package taskrun
    23  
    24  import (
    25  	"context"
    26  	"fmt"
    27  	"os"
    28  	"strconv"
    29  	"strings"
    30  	"sync"
    31  	"time"
    32  
    33  	"github.com/imdario/mergo"
    34  	"github.com/sirupsen/logrus"
    35  	"github.com/swaros/contxt/module/awaitgroup"
    36  	"github.com/swaros/contxt/module/dirhandle"
    37  	"github.com/swaros/contxt/module/trigger"
    38  	"github.com/swaros/manout"
    39  
    40  	"github.com/swaros/contxt/module/systools"
    41  
    42  	"github.com/swaros/contxt/module/configure"
    43  )
    44  
    45  const (
    46  	EventAllLines         = "ExecuteScriptLine" // EventAllLines is the event name for sending all lines
    47  	EventTaskStatusUpdate = "TaskStatus"        // EventTaskStatusUpdate is the event name for sending task status updates
    48  )
    49  
    50  type EventScriptLine struct {
    51  	Line   string
    52  	Target string
    53  	Error  error
    54  }
    55  
    56  type EventTaskStatus struct {
    57  	Target   string
    58  	Running  bool
    59  	Finished bool
    60  	RunCount int
    61  	Error    error
    62  	ExitCode int
    63  	Pid      int
    64  }
    65  
    66  // SharedFolderExecuter runs shared .contxt.yml files directly without merging them into
    67  // the current contxt file
    68  func SharedFolderExecuter(template configure.RunConfig, locationHandle func(string, string)) {
    69  	if len(template.Config.Use) > 0 {
    70  		GetLogger().WithField("uses", template.Config.Use).Info("shared executer")
    71  		for _, shared := range template.Config.Use {
    72  			externalPath := HandleUsecase(shared)
    73  			GetLogger().WithField("path", externalPath).Info("shared contxt location")
    74  			currentDir, _ := dirhandle.Current()
    75  			os.Chdir(externalPath)
    76  			locationHandle(externalPath, currentDir)
    77  			os.Chdir(currentDir)
    78  		}
    79  	}
    80  }
    81  
    82  func RunShared(targets string) {
    83  
    84  	allTargets := strings.Split(targets, ",")
    85  	template, templatePath, exists, terr := GetTemplate()
    86  	if terr != nil {
    87  		CtxOut(manout.MessageCln(manout.ForeRed, "Error ", manout.CleanTag, terr.Error()))
    88  		return
    89  	}
    90  	if !exists {
    91  		return
    92  	}
    93  
    94  	if template.Config.Loglevel != "" { // set logger level by template definition
    95  		setLogLevelByString(template.Config.Loglevel)
    96  	}
    97  
    98  	GetLogger().WithField("targets", allTargets).Info("SHARED START run targets...")
    99  
   100  	// handle all shared usages. these usages are set
   101  	// in the template by the string map named Use in the config section
   102  	// Config:
   103  	//    Use:
   104  	//      - shared_task_1
   105  	//      - shared_task_2
   106  	if len(template.Config.Use) > 0 {
   107  		GetLogger().WithField("uses", template.Config.Use).Info("found external dependecy")
   108  		CtxOut(manout.MessageCln(manout.ForeCyan, "[SHARED loop]"))
   109  		for _, shared := range template.Config.Use {
   110  			CtxOut(manout.MessageCln(manout.ForeCyan, "[SHARED CONTXT][", manout.ForeBlue, shared, manout.ForeCyan, "] "))
   111  			externalPath := HandleUsecase(shared)
   112  			GetLogger().WithField("path", externalPath).Info("shared contxt location")
   113  			currentDir, _ := dirhandle.Current()
   114  			os.Chdir(externalPath)
   115  			for _, runTarget := range allTargets {
   116  				CtxOut(manout.MessageCln(manout.ForeCyan, manout.ForeGreen, runTarget, manout.ForeYellow, " [ external:", manout.ForeWhite, externalPath, manout.ForeYellow, "] ", manout.ForeDarkGrey, templatePath))
   117  				RunTargets(runTarget, false)
   118  				CtxOut(manout.MessageCln(manout.ForeCyan, "["+manout.ForeBlue, shared+"] ", manout.ForeGreen, runTarget, " DONE"))
   119  			}
   120  			os.Chdir(currentDir)
   121  		}
   122  		CtxOut(manout.MessageCln(manout.ForeCyan, "[SHARED done]"))
   123  	}
   124  	GetLogger().WithField("targets", allTargets).Info("  SHARED DONE run targets...")
   125  }
   126  
   127  // RunTargets executes multiple targets
   128  // the targets string can have multiple targets
   129  // seperated by comma
   130  func RunTargets(targets string, sharedRun bool) {
   131  
   132  	// validate first
   133  	if err := TestTemplate(); err != nil {
   134  		CtxOut("found issues in the current template. ", err)
   135  		systools.Exit(systools.ErrorTemplate)
   136  		return
   137  	}
   138  
   139  	SetPH("CTX_TARGETS", targets)
   140  
   141  	// this flag should only true on the first execution
   142  	if sharedRun {
   143  		// do it here makes sure we are not in the shared scope
   144  		currentDir, _ := dirhandle.Current()
   145  		SetPH("CTX_PWD", currentDir)
   146  		// run shared use
   147  		RunShared(targets)
   148  	}
   149  
   150  	allTargets := strings.Split(targets, ",")
   151  	template, templatePath, exists, terr := GetTemplate()
   152  	if terr != nil {
   153  		CtxOut(manout.MessageCln(manout.ForeRed, "Error ", manout.CleanTag, terr.Error()))
   154  		systools.Exit(systools.ErrorTemplateReading)
   155  		return
   156  	}
   157  	GetLogger().WithField("targets", allTargets).Info("run targets...")
   158  	var runSequencially = false // default is async mode
   159  	if exists {                 // TODO: the exists check just for this config reading seems wrong
   160  		runSequencially = template.Config.Sequencially
   161  		if template.Config.Coloroff {
   162  			manout.ColorEnabled = false
   163  		}
   164  	}
   165  
   166  	if template.Config.Loglevel != "" { // loglevel by config
   167  		setLogLevelByString(template.Config.Loglevel)
   168  	}
   169  
   170  	var wg sync.WaitGroup // the main waitgroup
   171  
   172  	// handle all imports.
   173  	// these are yaml or json files, they can be accessed for reading by the gson doted format
   174  	if len(template.Config.Imports) > 0 {
   175  		GetLogger().WithField("Import", template.Config.Imports).Info("import variables from Files")
   176  		handleFileImportsToVars(template.Config.Imports)
   177  	} else {
   178  		GetLogger().Info("No Imports defined")
   179  	}
   180  
   181  	// experimental usage of taskrunner
   182  
   183  	if runSequencially { // non async run
   184  		for _, trgt := range allTargets {
   185  			SetPH("CTX_TARGET", trgt)
   186  			CtxOut(LabelFY("exec"), InfoMinor("execute target in sequence"), ValF(trgt), manout.ForeLightCyan, " ", templatePath)
   187  			ExecPathFile(&wg, !runSequencially, template, trgt)
   188  		}
   189  	} else {
   190  		var futuresExecs []awaitgroup.FutureStack
   191  		for _, trgt := range allTargets { // iterate all targets
   192  			CtxOut(LabelFY("exec"), InfoMinor("execute target in Async"), ValF(trgt), manout.ForeLightCyan, " ", templatePath)
   193  			futuresExecs = append(futuresExecs, awaitgroup.FutureStack{
   194  				AwaitFunc: func(ctx context.Context) interface{} {
   195  					ctxTarget := ctx.Value(awaitgroup.CtxKey{}).(string)            // get the target from context
   196  					SetPH("CTX_TARGET", ctxTarget)                                  // update global target. TODO: makes this any sense in async?
   197  					return ExecPathFile(&wg, !runSequencially, template, ctxTarget) // execute target
   198  				},
   199  				Argument: trgt,
   200  			})
   201  		}
   202  		futures := awaitgroup.ExecFutureGroup(futuresExecs)           // execute all async task
   203  		CtxOut(LabelFY("exec"), "all targets started ", len(targets)) // just info
   204  		awaitgroup.WaitAtGroup(futures)                               // wait until all task are done
   205  		CtxOut(LabelFY("exec"), "all targets done ", len(targets))    // also just info for the user
   206  	}
   207  
   208  	CtxOut(manout.MessageCln(manout.ForeBlue, "[done] ", manout.BoldTag, targets))
   209  	GetLogger().Info("target task execution done")
   210  }
   211  
   212  func setLogLevelByString(loglevel string) {
   213  	level, err := logrus.ParseLevel(loglevel)
   214  	if err != nil {
   215  		GetLogger().Error("Invalid loglevel in task defined.", err)
   216  	} else {
   217  		GetLogger().SetLevel(level)
   218  	}
   219  
   220  }
   221  
   222  func listenerWatch(script configure.Task, target, logLine string, e error, waitGroup *sync.WaitGroup, useWaitGroup bool, runCfg configure.RunConfig) {
   223  	if script.Listener != nil {
   224  
   225  		GetLogger().WithFields(logrus.Fields{
   226  			"count":    len(script.Listener),
   227  			"listener": script.Listener,
   228  			"target":   target,
   229  		}).Debug("testing Listener")
   230  
   231  		for _, listener := range script.Listener {
   232  			triggerFound, triggerMessage := checkReason(listener.Trigger, logLine, e) // check if a trigger have a match
   233  			if triggerFound {
   234  				GetLogger().WithFields(logrus.Fields{"target": target, "reason": triggerMessage}).Debug("Trigger Found")
   235  				SetPH("RUN."+target+".LOG.HIT", logLine)
   236  				if script.Options.Displaycmd {
   237  					CtxOut(manout.MessageCln(manout.ForeCyan, "[trigger]\t", manout.ForeYellow, triggerMessage, manout.Dim, " ", logLine))
   238  				}
   239  
   240  				someReactionTriggered := false                 // did this trigger something? used as flag
   241  				actionDef := configure.Action(listener.Action) // extract action
   242  
   243  				if len(actionDef.Script) > 0 { // script are directs executes without any async or other executes out of scope
   244  					someReactionTriggered = true
   245  					var dummyArgs map[string]string = make(map[string]string) // create empty arguments as scoped values
   246  					for _, triggerScript := range actionDef.Script {          // run any line of script
   247  						GetLogger().WithFields(logrus.Fields{
   248  							"cmd": triggerScript,
   249  						}).Debug("TRIGGER SCRIPT ACTION")
   250  						lineExecuter(waitGroup, useWaitGroup, script.Stopreasons, runCfg, "93", "46", triggerScript, target, dummyArgs, script)
   251  					}
   252  
   253  				}
   254  
   255  				if actionDef.Target != "" { // here we have a target defined thats needs to be started
   256  					someReactionTriggered = true
   257  					GetLogger().WithFields(logrus.Fields{
   258  						"target": actionDef.Target,
   259  					}).Debug("TRIGGER ACTION")
   260  
   261  					if script.Options.Displaycmd {
   262  						CtxOut(manout.MessageCln(manout.ForeCyan, "[trigger]\t ", manout.ForeGreen, "target:", manout.ForeLightGreen, actionDef.Target))
   263  					}
   264  
   265  					// TODO: i can't remember why i am doing this placeholder thing
   266  					hitKeyTargets := "RUN.LISTENER." + target + ".HIT.TARGETS" // compose the placeholder key
   267  					lastHitTargets := GetPH(hitKeyTargets)                     // get the last stored value if exists
   268  					if !strings.Contains(lastHitTargets, "("+actionDef.Target+")") {
   269  						lastHitTargets = lastHitTargets + "(" + actionDef.Target + ")"
   270  						SetPH(hitKeyTargets, lastHitTargets)
   271  					}
   272  
   273  					hitKeyCnt := "RUN.LISTENER." + actionDef.Target + ".HIT.CNT"
   274  					lastCnt := GetPH(hitKeyCnt)
   275  					if lastCnt == "" {
   276  						SetPH(hitKeyCnt, "1")
   277  					} else {
   278  						iCnt, err := strconv.Atoi(lastCnt)
   279  						if err != nil {
   280  							GetLogger().Fatal("fail converting trigger count")
   281  						}
   282  						iCnt++
   283  						SetPH(hitKeyCnt, strconv.Itoa(iCnt))
   284  					}
   285  
   286  					GetLogger().WithFields(logrus.Fields{
   287  						"trigger":   triggerMessage,
   288  						"target":    actionDef.Target,
   289  						"waitgroup": useWaitGroup,
   290  						"RUN.LISTENER." + target + ".HIT.TARGETS": lastHitTargets,
   291  					}).Info("TRIGGER Called")
   292  					var scopeVars map[string]string = make(map[string]string)
   293  
   294  					GetLogger().WithFields(logrus.Fields{
   295  						"target": actionDef.Target,
   296  					}).Info("RUN Triggered target (not async)")
   297  
   298  					// because we are anyway in a async scope, we should no longer
   299  					// try to run this target too async.
   300  					// also the target is triggered by an specific log entriy, it makes
   301  					// sence to stop the execution of the parent, til this target is executed
   302  					CtxOut("running target ", manout.ForeCyan, actionDef.Target, manout.ForeLightCyan, " trigger action")
   303  					executeTemplate(waitGroup, useWaitGroup, runCfg, actionDef.Target, scopeVars)
   304  
   305  				}
   306  				if !someReactionTriggered {
   307  					GetLogger().WithFields(logrus.Fields{
   308  						"trigger": triggerMessage,
   309  						"output":  logLine,
   310  					}).Warn("trigger defined without any action")
   311  				}
   312  			} else {
   313  				GetLogger().WithFields(logrus.Fields{
   314  					"output": logLine,
   315  				}).Debug("no trigger found")
   316  			}
   317  		}
   318  	}
   319  }
   320  
   321  // create the event for the trigger
   322  // this is a workaround for the trigger package
   323  // we have to create the event before we can use it
   324  // but we can't create it in the init function, because
   325  // the trigger package is not initialized at this time
   326  // so we have to create it on the first call
   327  // and we have to handle the listener here
   328  func createOrGetExecuteEvent(eventName string) *trigger.Event {
   329  	var execEvent *trigger.Event
   330  	execEvent, _ = trigger.GetEvent(eventName)
   331  	// we have to handle the listener here
   332  	if execEvent == nil {
   333  		var eventErr error
   334  		execEvent, eventErr = trigger.NewEvent(eventName)
   335  
   336  		if eventErr != nil {
   337  			GetLogger().Error("fail to create event")
   338  			return nil
   339  		}
   340  	}
   341  	return execEvent
   342  }
   343  
   344  // the main script handler
   345  func lineExecuter(
   346  	waitGroup *sync.WaitGroup, // the main waitgoup
   347  	useWaitGroup bool, // flag if we have to use the waitgroup. also means we run in async mode
   348  	stopReason configure.Trigger, // configuration for the stop reasons
   349  	runCfg configure.RunConfig, // the runtime configuration
   350  	colorCode, bgCode, // colorcodes for the left panel
   351  	codeLine, // the script that have to be processed
   352  	target string, // the actual target
   353  	arguments map[string]string, // the arguments for the current scope
   354  	script configure.Task) (int, bool) {
   355  	panelSize := 12                   // default panelsize
   356  	if script.Options.Panelsize > 0 { // overwrite panel size if set
   357  		panelSize = script.Options.Panelsize
   358  	}
   359  	var mainCommand = defaultString(script.Options.Maincmd, DefaultCommandFallBack) // get the maincommand by default first
   360  	if configure.GetOs() == "windows" {                                             // handle windows behavior depending default commands
   361  		mainCommand = defaultString(script.Options.Maincmd, DefaultCommandFallBackWindows) // setup windows default command
   362  	}
   363  	replacedLine := HandlePlaceHolderWithScope(codeLine, arguments) // placeholders
   364  	if script.Options.Displaycmd {                                  // do we show the argument?
   365  		CtxOut(LabelFY("cmd"), ValF(target), InfoF(replacedLine))
   366  	}
   367  
   368  	SetPH("RUN.SCRIPT_LINE", replacedLine) // overwrite the current scriptline. this is only reliable if we not in async mode
   369  	// composing the label for the output
   370  	var targetLabel CtxTargetOut = CtxTargetOut{
   371  		ForeCol:   defaultString(script.Options.Colorcode, colorCode),
   372  		BackCol:   defaultString(script.Options.Bgcolorcode, bgCode),
   373  		PanelSize: panelSize,
   374  	}
   375  	// here we execute the current script line
   376  	execCode, realExitCode, execErr := ExecuteScriptLine(mainCommand, script.Options.Mainparams, replacedLine,
   377  		func(logLine string, err error) bool { // callback for any logline
   378  
   379  			execEvent := createOrGetExecuteEvent(EventAllLines) // create or get the event for all lines
   380  			if execEvent != nil {
   381  				event := EventScriptLine{
   382  					Target: target,
   383  					Line:   logLine,
   384  					Error:  err,
   385  				}
   386  				execEvent.SetArguments(event)
   387  				trigger.UpdateEvents()
   388  				execEvent.Send()
   389  			}
   390  
   391  			SetPH("RUN."+target+".LOG.LAST", logLine) // set or overwrite the last script output for the target
   392  
   393  			if script.Listener != nil { // do we have listener?
   394  				GetLogger().WithFields(logrus.Fields{
   395  					"cnt":      len(script.Listener),
   396  					"listener": script.Listener,
   397  				}).Debug("CHECK Listener")
   398  				listenerWatch(script, target, logLine, err, waitGroup, useWaitGroup, runCfg) // listener handler
   399  			}
   400  			targetLabel.Target = target
   401  			// The whole output can be ignored by configuration
   402  			// if this is not enabled then we handle all these here
   403  			if !script.Options.Hideout {
   404  				// the background color
   405  				if script.Options.Format != "" { // do we have a specific format for the label, then we use them instead
   406  					format := HandlePlaceHolderWithScope(script.Options.Format, script.Variables) // handle placeholder in the label
   407  					fomatedOutStr := manout.Message(format)                                       // first just get the format as it is
   408  					if strings.Contains(format, "%s") {                                           // do we have the string placeholder?
   409  						fomatedOutStr = manout.Message(fmt.Sprintf(format, target)) // ... then format the message depending format codes
   410  					}
   411  					targetLabel.Alternative = fomatedOutStr
   412  				}
   413  
   414  				//outStr := systools.LabelPrintWithArg(logLine, colorCode, "39", 2) // hardcoded format for the logoutput iteself
   415  				outStr := manout.MessageCln(logLine)
   416  				if script.Options.Stickcursor { // optional set back the cursor to the beginning
   417  					fmt.Print("\033[G\033[K") // done by escape codes
   418  				}
   419  
   420  				CtxOut(targetLabel, outStr)     // prints the codeline
   421  				if script.Options.Stickcursor { // cursor stick handling
   422  					fmt.Print("\033[A")
   423  				}
   424  			}
   425  
   426  			stopReasonFound, message := checkReason(stopReason, logLine, err) // do we found a defined reason to stop execution
   427  			if stopReasonFound {
   428  				if script.Options.Displaycmd {
   429  					CtxOut(LabelFY("stop-reason"), ValF(message))
   430  				}
   431  				return false
   432  			}
   433  			return true
   434  		}, func(process *os.Process) { // callback if the process started and we got the process id
   435  			pidStr := fmt.Sprintf("%d", process.Pid) // we use them as info for the user only
   436  			SetPH("RUN.PID", pidStr)
   437  			SetPH("RUN."+target+".PID", pidStr)
   438  			if script.Options.Displaycmd {
   439  				CtxOut(LabelFY("pid"), ValF(process.Pid))
   440  			}
   441  			taskUpdate := createOrGetExecuteEvent(EventTaskStatusUpdate)
   442  			event := EventTaskStatus{
   443  				Target:   target,
   444  				Running:  true,
   445  				Finished: false,
   446  				Error:    nil,
   447  				Pid:      process.Pid,
   448  			}
   449  			if taskUpdate != nil {
   450  				taskUpdate.SetArguments(event)
   451  				taskUpdate.Send()
   452  			}
   453  
   454  		})
   455  	if execErr != nil {
   456  		if script.Options.Displaycmd {
   457  			CtxOut(LabelErrF("exec error"), ValF(execErr))
   458  		}
   459  
   460  		// send errors as event too
   461  		execEvent := createOrGetExecuteEvent(EventAllLines)
   462  		if execEvent != nil {
   463  			event := EventScriptLine{
   464  				Target: target,
   465  				Line:   execErr.Error(),
   466  				Error:  execErr,
   467  			}
   468  			execEvent.SetArguments(event)
   469  			trigger.UpdateEvents()
   470  			execEvent.Send()
   471  
   472  		}
   473  	}
   474  	// check execution codes
   475  	switch execCode {
   476  	case systools.ExitByStopReason:
   477  		return systools.ExitByStopReason, true
   478  	case systools.ExitCmdError:
   479  		if script.Options.IgnoreCmdError {
   480  			if script.Stopreasons.Onerror {
   481  				return systools.ExitByStopReason, true
   482  			}
   483  			CtxOut(manout.MessageCln(manout.ForeYellow, "NOTE!\t", manout.BackLightYellow, manout.ForeDarkGrey, " a script execution was failing. no stopreason is set so execution will continued "))
   484  			CtxOut(manout.MessageCln("\t", manout.BackLightYellow, manout.ForeDarkGrey, " if this is expected you can ignore this message.                                 "))
   485  			CtxOut(manout.MessageCln("\t", manout.BackLightYellow, manout.ForeDarkGrey, " but you should handle error cases                                                "))
   486  			CtxOut("\ttarget :\t", manout.MessageCln(manout.ForeBlue, target))
   487  			CtxOut("\tcommand:\t", manout.MessageCln(manout.ForeYellow, codeLine))
   488  
   489  		} else {
   490  			errMsg := " = exit code from command: "
   491  			lastMessage := manout.MessageCln(manout.BackRed, manout.ForeYellow, realExitCode, manout.CleanTag, manout.ForeLightRed, errMsg, manout.ForeWhite, codeLine)
   492  			CtxOut("\t Exit ", lastMessage)
   493  			CtxOut()
   494  			CtxOut("\t check the command. if this command can fail you may fit the execution rules. see options:")
   495  			CtxOut("\t you may disable a hard exit on error by setting ignoreCmdError: true")
   496  			CtxOut("\t if you do so, a Note will remind you, that a error is happend in this case.")
   497  			CtxOut()
   498  			//GetLogger().Error("runtime error:", execErr, "exit", realExitCode)
   499  			systools.Exit(realExitCode)
   500  			// returns the error code
   501  			return systools.ExitCmdError, true
   502  		}
   503  	case systools.ExitOk:
   504  		return systools.ExitOk, false
   505  	}
   506  	return systools.ExitNoCode, true
   507  }
   508  
   509  func generateFuturesByTargetListAndExec(RunTargets []string, waitGroup *sync.WaitGroup, runCfg configure.RunConfig) []awaitgroup.Future {
   510  	if len(RunTargets) < 1 {
   511  		return []awaitgroup.Future{}
   512  	}
   513  	var runTargetExecs []awaitgroup.FutureStack
   514  	for _, needTarget := range RunTargets {
   515  		GetLogger().Debug("runTarget name should be added " + needTarget)
   516  		runTargetExecs = append(runTargetExecs, awaitgroup.FutureStack{
   517  			AwaitFunc: func(ctx context.Context) interface{} {
   518  				argTask := ctx.Value(awaitgroup.CtxKey{}).(string)
   519  				_, argmap := StringSplitArgs(argTask, "arg")
   520  				GetLogger().Debug("add runTarget task " + argTask)
   521  				return executeTemplate(waitGroup, true, runCfg, argTask, argmap)
   522  			},
   523  			Argument: needTarget})
   524  
   525  	}
   526  	CtxOut(LabelFY("async targets"), InfoF("count"), len(runTargetExecs), InfoF(" targets"))
   527  	return awaitgroup.ExecFutureGroup(runTargetExecs)
   528  }
   529  
   530  // merge a list of task to an single task.
   531  func mergeTargets(target string, runCfg configure.RunConfig) configure.Task {
   532  	var checkTasks configure.Task
   533  	first := true
   534  	if len(runCfg.Task) > 0 {
   535  		for _, script := range runCfg.Task {
   536  			if strings.EqualFold(target, script.ID) {
   537  				canRun, failMessage := checkRequirements(script.Requires)
   538  				if canRun {
   539  					// update task variables
   540  					for keyName, variable := range script.Variables {
   541  						SetPH(keyName, HandlePlaceHolder(variable))
   542  					}
   543  					if first {
   544  						checkTasks = script
   545  						first = false
   546  					} else {
   547  						mergo.Merge(&checkTasks, script, mergo.WithOverride, mergo.WithAppendSlice)
   548  					}
   549  				} else {
   550  					GetLogger().Debug(failMessage)
   551  				}
   552  			}
   553  		}
   554  	}
   555  	return checkTasks
   556  }
   557  
   558  func executeTemplate(waitGroup *sync.WaitGroup, runAsync bool, runCfg configure.RunConfig, target string, scopeVars map[string]string) int {
   559  	if runAsync {
   560  		waitGroup.Add(1)
   561  		defer waitGroup.Done()
   562  	}
   563  	// check if task is already running
   564  	// this check depends on the target name.
   565  	if !runCfg.Config.AllowMutliRun && TaskRunning(target) {
   566  		GetLogger().WithField("task", target).Warning("task would be triggered again while is already running. IGNORED")
   567  		return systools.ExitAlreadyRunning
   568  	}
   569  
   570  	// increment task counter
   571  	targetTaskCount := incTaskCount(target)
   572  	defer incTaskDoneCount(target)
   573  
   574  	GetLogger().WithFields(logrus.Fields{
   575  		"target": target,
   576  	}).Info("executeTemplate LOOKING for target")
   577  
   578  	// create the task update event
   579  	taskUpdate := createOrGetExecuteEvent(EventTaskStatusUpdate)
   580  	event := EventTaskStatus{
   581  		Target:   target,
   582  		Running:  false,
   583  		RunCount: targetTaskCount,
   584  		Finished: false,
   585  		Error:    nil,
   586  	}
   587  	if taskUpdate != nil {
   588  		taskUpdate.SetArguments(event)
   589  		taskUpdate.Send()
   590  	}
   591  
   592  	// Checking if the Tasklist have something
   593  	// to handle
   594  	if len(runCfg.Task) > 0 {
   595  		returnCode := systools.ExitOk
   596  
   597  		// the main variables will be set at first
   598  		// but only if the they not already exists
   599  		// from other task or by start argument
   600  		for keyName, variable := range runCfg.Config.Variables {
   601  			SetIfNotExists(keyName, HandlePlaceHolder(variable))
   602  		}
   603  		// set the colorcodes for the labels on left side of screen
   604  		colorCode, bgCode := systools.CreateColorCode()
   605  
   606  		// updates global variables
   607  		SetPH("RUN.TARGET", target)
   608  
   609  		// this flag is only used
   610  		// for a "target not found" message later
   611  		targetFound := false
   612  
   613  		// oure tasklist that will use later
   614  		var taskList []configure.Task
   615  
   616  		// depending on the config
   617  		// we merge the tasks and handle them as one task,
   618  		// or we keep them as a list of tasks what would
   619  		// keep more flexibility.
   620  		// by merging task we can loose runtime definitions
   621  		if runCfg.Config.MergeTasks {
   622  			mergedScript := mergeTargets(target, runCfg)
   623  			taskList = append(taskList, mergedScript)
   624  		} else {
   625  			for _, script := range runCfg.Task {
   626  				if strings.EqualFold(target, script.ID) {
   627  					taskList = append(taskList, script)
   628  				}
   629  			}
   630  		}
   631  
   632  		// check if we have found the target
   633  		for curTIndex, script := range taskList {
   634  			if strings.EqualFold(target, script.ID) {
   635  				GetLogger().WithFields(logrus.Fields{
   636  					"target":    target,
   637  					"scopeVars": scopeVars,
   638  				}).Info("executeTemplate EXECUTE target")
   639  				targetFound = true
   640  
   641  				stopReason := script.Stopreasons
   642  
   643  				var messageCmdCtrl CtxOutCtrl = CtxOutCtrl{ // define a controll hook, depending on the display comand option
   644  					IgnoreCase: !script.Options.Displaycmd, // we ignore thie message, as long the display command is NOT set
   645  				}
   646  
   647  				// check requirements
   648  				canRun, message := checkRequirements(script.Requires)
   649  				if !canRun {
   650  					GetLogger().WithFields(logrus.Fields{
   651  						"target": target,
   652  						"reason": message,
   653  					}).Info("executeTemplate IGNORE because requirements not matching")
   654  					if script.Options.Displaycmd {
   655  						CtxOut(messageCmdCtrl, LabelFY("require"), ValF(message), InfoF("Task-Block "), curTIndex+1, " of ", len(taskList), " skipped")
   656  					}
   657  					// ---- return ExitByRequirement
   658  					continue
   659  				}
   660  
   661  				// get the task related variables
   662  				for keyName, variable := range script.Variables {
   663  					SetPH(keyName, HandlePlaceHolder(variable))
   664  					scopeVars[keyName] = variable
   665  				}
   666  				backToDir := ""
   667  				// if working dir is set change to them
   668  				if script.Options.WorkingDir != "" {
   669  					backToDir, _ = dirhandle.Current()
   670  					chDirError := os.Chdir(HandlePlaceHolderWithScope(script.Options.WorkingDir, scopeVars))
   671  					if chDirError != nil {
   672  						manout.Error("Workspace setting seems invalid ", chDirError)
   673  						systools.Exit(systools.ErrorBySystem)
   674  					}
   675  				}
   676  
   677  				// just the abort flag.
   678  				abort := false
   679  
   680  				// experimental usage of needs
   681  
   682  				// -- NEEDS
   683  				// needs are task, the have to be startet once
   684  				// before we continue.
   685  				// any need can have his own needs they needs to
   686  				// be executed
   687  				if len(script.Needs) > 0 {
   688  
   689  					CtxOut(messageCmdCtrl, LabelFY("target"), ValF(target), InfoF("require"), ValF(len(script.Needs)), InfoF("needs. async?"), ValF(runAsync))
   690  					GetLogger().WithField("needs", script.Needs).Debug("Needs for the script")
   691  					if runAsync {
   692  						var needExecs []awaitgroup.FutureStack
   693  						for _, needTarget := range script.Needs {
   694  							if TaskRunsAtLeast(needTarget, 1) {
   695  								CtxOut(messageCmdCtrl, LabelFY("need check"), ValF(target), InfoRed("already executed"), ValF(needTarget))
   696  								GetLogger().Debug("need already handled " + needTarget)
   697  							} else {
   698  								GetLogger().Debug("need name should be added " + needTarget)
   699  								CtxOut(messageCmdCtrl, LabelFY("need check"), ValF(target), InfoF("executing"), ValF(needTarget))
   700  								needExecs = append(needExecs, awaitgroup.FutureStack{
   701  									AwaitFunc: func(ctx context.Context) interface{} {
   702  										argNeed := ctx.Value(awaitgroup.CtxKey{}).(string)
   703  										_, argmap := StringSplitArgs(argNeed, "arg")
   704  										GetLogger().Debug("add need task " + argNeed)
   705  										return executeTemplate(waitGroup, true, runCfg, argNeed, argmap)
   706  									},
   707  									Argument: needTarget})
   708  							}
   709  						}
   710  						futures := awaitgroup.ExecFutureGroup(needExecs) // create the futures and start the tasks
   711  						results := awaitgroup.WaitAtGroup(futures)       // wait until any task is executed
   712  
   713  						GetLogger().WithField("result", results).Debug("needs result")
   714  					} else {
   715  						for _, needTarget := range script.Needs {
   716  							if TaskRunsAtLeast(needTarget, 1) { // do not run needs the already runs
   717  								GetLogger().Debug("need already handled " + needTarget)
   718  							} else {
   719  								_, argmap := StringSplitArgs(needTarget, "arg")
   720  								executeTemplate(waitGroup, false, runCfg, needTarget, argmap)
   721  							}
   722  						}
   723  					}
   724  					CtxOut(LabelFY("target"), ValF(target), InfoF("needs done"))
   725  				}
   726  
   727  				// targets that should be started as well
   728  				// these targets running at the same time
   729  				// so different to scope, we dont need to wait
   730  				// right now until they ends
   731  				runTargetfutures := generateFuturesByTargetListAndExec(script.RunTargets, waitGroup, runCfg)
   732  
   733  				// check if we have script lines.
   734  				// if not, we need at least to check
   735  				// 'now' listener
   736  				if len(script.Script) < 1 {
   737  					GetLogger().Debug("no script lines defined. run listener anyway")
   738  					listenerWatch(script, target, "", nil, waitGroup, runAsync, runCfg)
   739  					// workaround til the async runnig is refactored
   740  					// now we need to give the subtask time to run and update the waitgroup
   741  					duration := time.Second
   742  					time.Sleep(duration)
   743  				}
   744  
   745  				// now we update all the listeners
   746  				// and start the script
   747  				if taskUpdate != nil {
   748  					event.Target = target
   749  					event.Running = true
   750  					taskUpdate.SetArguments(event)
   751  					taskUpdate.Send()
   752  				}
   753  
   754  				// preparing codelines by execute second level commands
   755  				// that can affect the whole script
   756  				abort, returnCode, _ = TryParse(script.Script, func(codeLine string) (bool, int) {
   757  					lineAbort, lineExitCode := lineExecuter(waitGroup, runAsync, stopReason, runCfg, colorCode, bgCode, codeLine, target, scopeVars, script)
   758  					return lineExitCode, lineAbort
   759  				})
   760  				if abort {
   761  					GetLogger().Debug("abort reason found ")
   762  				}
   763  				// here we update the listeners again, after scripts was running
   764  				if taskUpdate != nil {
   765  					event.Running = false
   766  					event.ExitCode = returnCode
   767  					event.Finished = true
   768  					taskUpdate.SetArguments(event)
   769  					taskUpdate.Send()
   770  				}
   771  
   772  				// waitin until the any target that runns also is done
   773  				if len(runTargetfutures) > 0 {
   774  					CtxOut(messageCmdCtrl, LabelFY("wait targets"), "waiting until beside running targets are done")
   775  					trgtRes := awaitgroup.WaitAtGroup(runTargetfutures)
   776  					CtxOut(messageCmdCtrl, LabelFY("wait targets"), "waiting done", trgtRes)
   777  				}
   778  				// next are tarets they runs afterwards the regular
   779  				// script os done
   780  				GetLogger().WithFields(logrus.Fields{
   781  					"current-target": target,
   782  					"nexts":          script.Next,
   783  				}).Debug("executeTemplate next definition")
   784  
   785  				nextfutures := generateFuturesByTargetListAndExec(script.Next, waitGroup, runCfg)
   786  				nextRes := awaitgroup.WaitAtGroup(nextfutures)
   787  				CtxOut(messageCmdCtrl, LabelFY("wait next"), "waiting done", nextRes)
   788  
   789  				//return returnCode
   790  				// back to old dir if workpace usage was set
   791  				if backToDir != "" {
   792  					os.Chdir(backToDir)
   793  				}
   794  			}
   795  
   796  		}
   797  		// we have at least none of the possible task executed.
   798  		if !targetFound {
   799  			CtxOut(manout.MessageCln(manout.ForeYellow, "target not defined or matching any requirement: ", manout.ForeWhite, target))
   800  			GetLogger().Error("Target can not be found: ", target)
   801  			return systools.ExitByNoTargetExists
   802  		}
   803  
   804  		GetLogger().WithFields(logrus.Fields{
   805  			"target": target,
   806  		}).Info("executeTemplate. target do not contains tasks")
   807  		return returnCode
   808  	}
   809  	return systools.ExitNoCode
   810  }
   811  
   812  func defaultString(line string, defaultString string) string {
   813  	if line == "" {
   814  		return defaultString
   815  	}
   816  	return line
   817  }
   818  
   819  func handleFileImportsToVars(imports []string) {
   820  	for _, filenameFull := range imports {
   821  		var keyname string
   822  		parts := strings.Split(filenameFull, " ")
   823  		filename := parts[0]
   824  		if len(parts) > 1 {
   825  			keyname = parts[1]
   826  		}
   827  
   828  		dirhandle.FileTypeHandler(filename, func(jsonBaseName string) {
   829  			GetLogger().Debug("loading json File as second level variables:", filename)
   830  			if keyname == "" {
   831  				keyname = jsonBaseName
   832  			}
   833  			if err := ImportDataFromJSONFile(keyname, filename); err != nil {
   834  				GetLogger().Error("error while loading import: ", filename)
   835  				manout.Error("error loading json file base import:", filename, " ", err)
   836  				systools.Exit(systools.ErrorOnConfigImport)
   837  			}
   838  
   839  		}, func(yamlBaseName string) {
   840  			GetLogger().Debug("loading yaml File: as second level variables", filename)
   841  			if keyname == "" {
   842  				keyname = yamlBaseName
   843  			}
   844  			if err := ImportDataFromYAMLFile(keyname, filename); err != nil {
   845  				GetLogger().Error("error while loading import", filename)
   846  				manout.Error("error loading yaml based import:", filename, " ", err)
   847  				systools.Exit(systools.ErrorOnConfigImport)
   848  			}
   849  		}, func(filenameBase string, ext string) {
   850  			if keyname == "" {
   851  				keyname = filename
   852  			}
   853  			GetLogger().Debug("loading File: as plain named variable", filename, ext)
   854  
   855  			if str, err := ParseFileAsTemplate(filename); err != nil {
   856  				GetLogger().Error("error while loading import", filename)
   857  				manout.Error("error loading text file import:", filename, " ", err)
   858  				systools.Exit(systools.ErrorOnConfigImport)
   859  			} else {
   860  				SetPH(keyname, str)
   861  			}
   862  
   863  		}, func(path string, err error) {
   864  			GetLogger().Errorln("file not exists:", err)
   865  			manout.Error("varibales file not exists:", path, err)
   866  			systools.Exit(1)
   867  		})
   868  	}
   869  }