func (fl *FlowLauncher) Exec() {
	if !fl.Prep(false) {
		return
	}
	glog.Info("workflow launcher ", fl.Name, " with ", fl.Threads, " threads")
	var waitGroup sync.WaitGroup
	for i := 0; i < fl.Threads; i++ {
		waitGroup.Add(1)
		go fl.execOneFlow(i, &waitGroup, false)
	}
	go func() {
		waitGroup.Wait()
		glog.Info("completed launcher ", fl.Name, " with ", fl.Threads, " threads")
		fl.LastRunResult.Completed = true
		close(fl.CStat)
		fl.iEnd <- fl.endParams
	}()
}
p are initial environment properties
run a workflow with this number of threads and gather the success/fail response statistics
package flow
import (
	"fmt"
	"github.com/golang/glog"
	"os"
	"sync"
	"time"
)
type GetFlowFunc func(threadId int) *Workflow
type LaunchableFlow interface {
	FlowFunc(threadId int) *Workflow
	Name() string
	Id() string
	GetProps() *Props
}
type BaseLaunchable struct {
	name string
	id   string
}
func (b *BaseLaunchable) Init(name string) {
	b.name = name
	b.id = MakeID(name)
}
func (b *BaseLaunchable) Name() string {
	return b.name
}
func (b *BaseLaunchable) Id() string {
	return b.id
}
func (b *BaseLaunchable) DefaultProps() *Props {
	props := Props{}
	props[KEY_WORKSPACE] = "workspace/" + b.id
	props["path"] = "/"
	props[KEY_TIDY_DESK] = "reset"  
	props[KEY_TRIGGERS] = "triggers/" + b.id
	return &props
}
type FlowLauncher struct {
	Name          string
	Id            string
	Order         int
	flowFunc      GetFlowFunc
	Threads       int
	Flows         []*Workflow  
	Props         *Props
	CStat         chan *Params
	iEnd          chan *Params  
	endParams     *Params
	LastRunResult *FlowLaunchResult  
	Error         string
	initial       *FlowLauncher
	trigger       *FlowLauncher
}
func MakeFlowLauncher(launchable LaunchableFlow, threads int, initial *FlowLauncher, trigger *FlowLauncher) *FlowLauncher {
	return &FlowLauncher{
		Props:    launchable.GetProps(),
		Name:     launchable.Name(),
		Id:       launchable.Id(),
		flowFunc: launchable.FlowFunc,
		Threads:  threads,
		initial:  initial,
		trigger:  trigger,
	}
}
func (fl *FlowLauncher) Trigger() chan *Params {
	return fl.iEnd
}
func (fl *FlowLauncher) Step(v int) {
	if len(fl.Flows) < fl.Threads {
		glog.Error("not enough threads", fl.Id, len(fl.Flows), "<", fl.Threads)
		return
	}
	glog.Info("<<<<<<<<<<<<<<<<<<<<<<<<<<< step")
	for i := 0; i < fl.Threads; i++ {
		fl.Flows[i].Stepper <- v
	}
}
func (fl *FlowLauncher) AutoStep(delay time.Duration, endChan chan *Params) {
	loop := true
	go func() {
		for stat := range fl.CStat {
			glog.Info("<<< status event ", stat)
		}
		glog.Info("loop stoppped")
	}()
	go func() {
		for loop {
			if loop {
				glog.V(2).Infoln("firing step evenct >>>")
				go fl.Step(1)
			}
			time.Sleep(delay)
		}
		glog.Info("stepper loop stoppped")
	}()
	res := <-fl.iEnd
	loop = false
	glog.Info("endChan <<<")
	if endChan != nil {
		endChan <- res
	}
}
func (fl *FlowLauncher) TrashLastResults() {
	fl.LastRunResult = nil
}
func (fl *FlowLauncher) MakeLaunchResults(tf *Workflow) {
	fl.LastRunResult = NewFlowLaunchResult(fl.Threads)
	for _, n := range tf.TaskNodes {
		glog.Info("add name node to run result: ", n.Name())
		ls, err := fl.LastRunResult.AddTask(MakeID(n.Name()))
		if err != nil {
			glog.Error("dodgy nodes ", err)
		} else {
			n.SetStream(ls.CommandStream)
		}
	}
}
func (fl *FlowLauncher) TidyDeskPolicy(p Props) bool {
	ws := p[KEY_WORKSPACE]
	if p[KEY_TIDY_DESK] != "keep" {
		glog.Info("removing and moving ", ws)
		err := os.RemoveAll(ws + "_old")
		if err != nil {
			if !os.IsNotExist(err) {
				glog.Error(err)
				fl.Error = err.Error()
				fl.LastRunResult.Error = fl.Error
				return false
			}
		}
		err = os.Rename(ws, ws+"_old")
		if err != nil {
			if !os.IsNotExist(err) {
				glog.Error(err)
				fl.Error = err.Error()
				fl.LastRunResult.Error = fl.Error
				return false
			}
		}
	}
	err := os.MkdirAll(ws, 0777)
	if err != nil {
		glog.Warning(err)
		if p[KEY_TIDY_DESK] == "keep" {
			return true
		}
		glog.Error(err)
		fl.Error = err.Error()
		fl.LastRunResult.Error = fl.Error
		return false
	}
	ts := p[KEY_TRIGGERS]
	err = os.MkdirAll(ts, 0777)
	if err != nil {
		glog.Error(err)
		fl.Error = err.Error()
		fl.LastRunResult.Error = fl.Error
		return false
	}
	glog.Info("removing and moving done")
	return true
}
func (fl *FlowLauncher) MakeFlow(threadId int) *Workflow {
	w := fl.flowFunc(threadId)
	w.Name = fl.Name
	return w
}
func (fl *FlowLauncher) Prep(isTrigger bool) bool {
	flow := fl.MakeFlow(0)
	fl.endParams = MakeParams()
	fl.endParams.Props = *fl.Props  
	fl.endParams.FlowName = flow.Name
	glog.Info("workflow prep ", flow.Name)
	if flow == nil {
		glog.Error(flow.Name, " has no flow")
		return false
	}
	if !isTrigger && flow.Start == nil {
		glog.Error(flow.Name, " has no flow start")
		return false
	}
	if flow.End == nil {
		glog.Error(flow.Name, " has no flow end")
		return false
	}
	fl.CStat = make(chan *Params)
	fl.iEnd = make(chan *Params)
	fl.Error = ""
	if fl.TidyDeskPolicy(*fl.Props) == false {
		fl.endParams.Status = FAIL
		fl.iEnd <- fl.endParams
		return false
	}
	fl.Flows = make([]*Workflow, fl.Threads, fl.Threads)
	return true
}