github.com/metacurrency/holochain@v0.1.0-alpha-26.0.20200915073418-5c83169c9b5b/cmd/hcdev/hcdev.go (about)

     1  // Copyright (C) 2013-2018, The MetaCurrency Project (Eric Harris-Braun, Arthur Brock, et. al.)
     2  // Use of this source code is governed by GPLv3 found in the LICENSE file
     3  //---------------------------------------------------------------------------------------
     4  // command line interface to developing and testing holochain applications
     5  
     6  package main
     7  
     8  import (
     9  	"bytes"
    10  	"fmt"
    11  	"io/ioutil"
    12  	"os"
    13  	"os/exec"
    14  	"os/user"
    15  	"path/filepath"
    16  	"regexp"
    17  	"strconv"
    18  	"time"
    19  
    20  	holo "github.com/holochain/holochain-proto"
    21  	. "github.com/holochain/holochain-proto/apptest"
    22  	"github.com/holochain/holochain-proto/cmd"
    23  	"github.com/holochain/holochain-proto/ui"
    24  	"github.com/urfave/cli"
    25  	// fsnotify	"github.com/fsnotify/fsnotify"
    26  	//spew "github.com/davecgh/go-spew/spew"
    27  )
    28  
    29  const (
    30  	defaultUIPort      = "4141"
    31  	scenarioStartDelay = 1
    32  
    33  	defaultSpecsFile = "bridge_specs.json"
    34  )
    35  
    36  var debug, appInitialized, verbose, keepalive bool
    37  var keepaliveCleanup func()
    38  var rootPath, devPath, name string
    39  var bridgeSpecsFile string
    40  var scenarioConfig *holo.TestConfig
    41  
    42  // flags for holochain config generation
    43  var dhtPort, logPrefix, bootstrapServer string
    44  var mdns bool = true
    45  var upnp bool
    46  
    47  // meta flags for program flow control
    48  var syncPausePath string
    49  var syncPauseUntil int
    50  
    51  type MutableContext struct {
    52  	str map[string]string
    53  	obj map[string]interface{}
    54  }
    55  
    56  var mutableContext MutableContext
    57  
    58  var lastRunContext *cli.Context
    59  
    60  var sysUser *user.User
    61  
    62  // TODO: move these into cmd module
    63  
    64  func appCheck(devPath string) error {
    65  	if !appInitialized {
    66  		return cmd.MakeErr(nil, fmt.Sprintf("%s doesn't look like a holochain app (missing dna).  See 'hcdev init -h' for help on initializing an app.", devPath))
    67  	}
    68  	return nil
    69  }
    70  func setupApp() (app *cli.App) {
    71  
    72  	// set default values so we can call this multiple time for testing
    73  	debug = false
    74  	appInitialized = false
    75  	rootPath = ""
    76  	devPath = ""
    77  	name = ""
    78  	mutableContext = MutableContext{map[string]string{}, map[string]interface{}{}}
    79  
    80  	var err error
    81  	sysUser, err = user.Current()
    82  	if err != nil {
    83  		panic(err)
    84  	}
    85  
    86  	app = cli.NewApp()
    87  	app.Name = "hcdev"
    88  	app.Usage = "holochain dev command line tool"
    89  	app.Version = fmt.Sprintf("0.0.6 (holochain %s)", holo.VersionStr)
    90  
    91  	var service *holo.Service
    92  	var serverID, agentID, identity string
    93  
    94  	var scenarioTmpDir = "hcdev_scenario_test_nodes_" + sysUser.Username
    95  
    96  	var dumpScenario string
    97  	var dumpTest bool
    98  	var start int
    99  
   100  	var bridgeAppTmpFilePath string
   101  
   102  	app.Flags = []cli.Flag{
   103  		cli.BoolFlag{
   104  			Name:        "debug",
   105  			Usage:       "debugging output",
   106  			Destination: &debug,
   107  		},
   108  		cli.BoolFlag{
   109  			Name:        "verbose",
   110  			Usage:       "verbose output",
   111  			Destination: &verbose,
   112  		},
   113  		cli.BoolFlag{
   114  			Name:        "keepalive",
   115  			Usage:       "don't end hcdev process upon completion of work",
   116  			Destination: &keepalive,
   117  		},
   118  		cli.StringFlag{
   119  			Name:        "execpath",
   120  			Usage:       "path to holochain dev execution directory (default: ~/.holochaindev)",
   121  			Destination: &rootPath,
   122  		},
   123  		cli.StringFlag{
   124  			Name:        "path",
   125  			Usage:       "path to chain source definition directory (default: current working dir)",
   126  			Destination: &devPath,
   127  		},
   128  		cli.StringFlag{
   129  			Name:        "DHTport",
   130  			Usage:       fmt.Sprintf("port to use for the holochain DHT and node-to-node communication (defaut: %d)", holo.DefaultDHTPort),
   131  			Destination: &dhtPort,
   132  		},
   133  		cli.BoolTFlag{
   134  			Name:        "mdns",
   135  			Usage:       "whether to use mdns for local peer discovery (default: true)",
   136  			Destination: &mdns,
   137  		},
   138  		cli.BoolFlag{
   139  			Name:        "upnp",
   140  			Usage:       "whether to use UPnP for creating a NAT port mapping (default: false)",
   141  			Destination: &upnp,
   142  		},
   143  		cli.StringFlag{
   144  			Name:        "logPrefix",
   145  			Usage:       "the prefix to put at the front of log messages",
   146  			Destination: &logPrefix,
   147  		},
   148  		cli.StringFlag{
   149  			Name:        "bootstrapServer",
   150  			Usage:       "url of bootstrap server or '_' for none",
   151  			Destination: &bootstrapServer,
   152  		},
   153  		cli.StringFlag{
   154  			Name:        "bridgeSpecs",
   155  			Usage:       fmt.Sprintf("path to bridge specs file (default: %s)", defaultSpecsFile),
   156  			Destination: &bridgeSpecsFile,
   157  		},
   158  		cli.StringFlag{
   159  			Name:        "serverID",
   160  			Usage:       "server identifier for multi-server scenario testing",
   161  			Destination: &serverID,
   162  		},
   163  		cli.StringFlag{
   164  			Name:        "agentID",
   165  			Usage:       "value to use for the agent identity (automatically set in scenario testing)",
   166  			Destination: &agentID,
   167  		},
   168  	}
   169  
   170  	var dumpChain, dumpDHT, initTest, fromDevelop, benchmarks, json bool
   171  	var clonePath, appPackagePath, cloneExample, outputDir, fromBranch, dumpFormat string
   172  
   173  	app.Commands = []cli.Command{
   174  		{
   175  			Name:    "init",
   176  			Aliases: []string{"i"},
   177  			Usage:   "initialize a holochain app directory: use default, from an appPackage file or clone from another app",
   178  			Flags: []cli.Flag{
   179  				cli.BoolFlag{
   180  					Name:        "test",
   181  					Usage:       "initialize built-in testing app",
   182  					Destination: &initTest,
   183  				},
   184  				cli.StringFlag{
   185  					Name:        "clone",
   186  					Usage:       "path from which to clone the app",
   187  					Destination: &clonePath,
   188  				},
   189  				cli.StringFlag{
   190  					Name:        "package",
   191  					Usage:       "path to an app package file from which to initialize the app",
   192  					Destination: &appPackagePath,
   193  				},
   194  				cli.StringFlag{
   195  					Name:        "cloneExample",
   196  					Usage:       "example from github.com/holochain to clone from",
   197  					Destination: &cloneExample,
   198  				},
   199  				cli.StringFlag{
   200  					Name:        "fromBranch",
   201  					Usage:       "specify branch to use with cloneExample",
   202  					Destination: &fromBranch,
   203  				},
   204  				cli.BoolFlag{
   205  					Name:        "fromDevelop",
   206  					Usage:       "specify that cloneExample should use the 'develop' branch",
   207  					Destination: &fromDevelop,
   208  				},
   209  			},
   210  			ArgsUsage: "<name>",
   211  			Action: func(c *cli.Context) error {
   212  				var name string
   213  				args := c.Args()
   214  				if len(args) != 1 {
   215  					if cloneExample != "" {
   216  						name = cloneExample
   217  					} else {
   218  						return cmd.MakeErr(c, "expecting app name as single argument")
   219  					}
   220  				}
   221  				flags := 0
   222  				if clonePath != "" {
   223  					flags += 1
   224  				}
   225  				if appPackagePath != "" {
   226  					flags += 1
   227  				}
   228  				if initTest {
   229  					flags += 1
   230  				}
   231  				if flags > 1 {
   232  					return cmd.MakeErr(c, " options are mutually exclusive, please choose just one.")
   233  				}
   234  				if name == "" {
   235  					name = args[0]
   236  				}
   237  				if filepath.IsAbs(name) {
   238  					devPath = name
   239  					name = filepath.Base(name)
   240  				} else {
   241  					devPath = filepath.Join(devPath, name)
   242  				}
   243  
   244  				info, err := os.Stat(devPath)
   245  				if err == nil && info.Mode().IsDir() {
   246  					return cmd.MakeErr(c, fmt.Sprintf("%s already exists", devPath))
   247  				}
   248  
   249  				encodingFormat := "json"
   250  				if initTest {
   251  					fmt.Printf("initializing test app as %s\n", name)
   252  					format := "json"
   253  					if len(c.Args()) == 2 {
   254  						format = c.Args()[1]
   255  						if !(format == "json" || format == "yaml" || format == "toml") {
   256  							return cmd.MakeErr(c, "format must be one of yaml,toml,json")
   257  
   258  						}
   259  					}
   260  					_, err := service.MakeTestingApp(devPath, "json", holo.SkipInitializeDB, holo.CloneWithNewUUID, nil)
   261  					if err != nil {
   262  						return cmd.MakeErrFromErr(c, err)
   263  					}
   264  				} else if clonePath != "" {
   265  
   266  					// build the app by cloning from another app
   267  					info, err := os.Stat(clonePath)
   268  					if err != nil {
   269  						dir, _ := cmd.GetCurrentDirectory()
   270  						return cmd.MakeErr(c, fmt.Sprintf("ClonePath:%s/'%s' %s", dir, clonePath, err.Error()))
   271  					}
   272  
   273  					if !info.Mode().IsDir() {
   274  						return cmd.MakeErr(c, "-clone flag expects a directory to clone from")
   275  					}
   276  					fmt.Printf("cloning %s from %s\n", name, clonePath)
   277  					err = doClone(service, clonePath, devPath)
   278  					if err != nil {
   279  						return cmd.MakeErrFromErr(c, err)
   280  					}
   281  				} else if cloneExample != "" {
   282  					tmpCopyDir, err := ioutil.TempDir("", fmt.Sprintf("holochain.example.%s", cloneExample))
   283  					if err != nil {
   284  						return cmd.MakeErrFromErr(c, err)
   285  					}
   286  					defer os.RemoveAll(tmpCopyDir)
   287  					err = os.Chdir(tmpCopyDir)
   288  					if err != nil {
   289  						return cmd.MakeErrFromErr(c, err)
   290  					}
   291  					if fromDevelop {
   292  						fromBranch = "develop"
   293  					}
   294  					command := exec.Command("git", "clone", fmt.Sprintf("git://github.com/holochain/%s.git", cloneExample))
   295  					out, err := command.CombinedOutput()
   296  					fmt.Printf("git: %s\n", string(out))
   297  					if err != nil {
   298  						return cmd.MakeErrFromErr(c, err)
   299  					}
   300  
   301  					if fromBranch != "" {
   302  						err = os.Chdir(filepath.Join(tmpCopyDir, cloneExample))
   303  						if err != nil {
   304  							return cmd.MakeErrFromErr(c, err)
   305  						}
   306  						command := exec.Command("git", "checkout", fromBranch)
   307  						out, err := command.CombinedOutput()
   308  						fmt.Printf("git: %s\n", string(out))
   309  						if err != nil {
   310  							return cmd.MakeErrFromErr(c, err)
   311  						}
   312  					}
   313  
   314  					clonePath := filepath.Join(tmpCopyDir, cloneExample)
   315  					fmt.Printf("cloning %s from github.com/holochain/%s\n", name, cloneExample)
   316  					err = doClone(service, clonePath, devPath)
   317  					if err != nil {
   318  						return cmd.MakeErrFromErr(c, err)
   319  					}
   320  
   321  				} else if appPackagePath != "" {
   322  					// build the app from the appPackage
   323  					_, err := cmd.UpackageAppPackage(service, appPackagePath, devPath, name, encodingFormat)
   324  					if err != nil {
   325  						return cmd.MakeErrFromErr(c, err)
   326  					}
   327  
   328  					fmt.Printf("initialized %s from appPackage:%s\n", devPath, appPackagePath)
   329  				} else {
   330  
   331  					// build empty app template
   332  					err := holo.MakeDirs(devPath)
   333  					if err != nil {
   334  						return cmd.MakeErrFromErr(c, err)
   335  					}
   336  					appPackageReader := bytes.NewBuffer([]byte(holo.BasicTemplateAppPackage))
   337  
   338  					var agent holo.Agent
   339  					agent, err = holo.LoadAgent(rootPath)
   340  					if err != nil {
   341  						return cmd.MakeErrFromErr(c, err)
   342  					}
   343  
   344  					var appPackage *holo.AppPackage
   345  					appPackage, err = service.SaveFromAppPackage(appPackageReader, devPath, name, agent, holo.BasicTemplateAppPackageFormat, encodingFormat, true)
   346  
   347  					if err != nil {
   348  						return cmd.MakeErrFromErr(c, err)
   349  					}
   350  					fmt.Printf("initialized empty application to %s with new UUID:%v\n", devPath, appPackage.DNA.UUID)
   351  				}
   352  
   353  				err = os.Chdir(devPath)
   354  				if err != nil {
   355  					return cmd.MakeErrFromErr(c, err)
   356  				}
   357  
   358  				return nil
   359  			},
   360  		},
   361  		{
   362  			Name:      "test",
   363  			Aliases:   []string{"t"},
   364  			ArgsUsage: "no args run's all stand-alone | [test file prefix] | [scenario] [role]",
   365  			Usage:     "run chain's stand-alone or scenario tests",
   366  			Flags: []cli.Flag{
   367  				cli.StringFlag{
   368  					Name:        "syncPausePath",
   369  					Usage:       "path to wait for multinode test sync",
   370  					Destination: &syncPausePath,
   371  				},
   372  				cli.IntFlag{
   373  					Name:        "syncPauseUntil",
   374  					Usage:       "unix timestamp - sync tests to run at this time",
   375  					Destination: &syncPauseUntil,
   376  				},
   377  				cli.BoolFlag{
   378  					Name:        "benchmarks",
   379  					Usage:       "calculate benchmarks during test",
   380  					Destination: &benchmarks,
   381  				},
   382  				cli.StringFlag{
   383  					Name:        "bridgeAppsFile",
   384  					Usage:       "path to live bridging Apps (used internally when scenario testing)",
   385  					Destination: &bridgeAppTmpFilePath,
   386  				},
   387  			},
   388  			Action: func(c *cli.Context) error {
   389  				holo.Debug("test: start")
   390  
   391  				var err error
   392  				if err = appCheck(devPath); err != nil {
   393  					return cmd.MakeErrFromErr(c, err)
   394  				}
   395  
   396  				args := c.Args()
   397  				var errs []error
   398  
   399  				var h *holo.Holochain
   400  				h, err = getHolochain(c, service, identity)
   401  				if err != nil {
   402  					return cmd.MakeErrFromErr(c, err)
   403  				}
   404  				holo.Debug("test: initialised holochain\n")
   405  
   406  				if len(args) < 2 {
   407  					var bridgeApps []BridgeAppForTests
   408  
   409  					bridgeApps, err = getBridgeAppForTests(service, h.Agent())
   410  					if err != nil {
   411  						return cmd.MakeErrFromErr(c, err)
   412  					}
   413  
   414  					if len(args) == 1 {
   415  						errs = TestOne(h, args[0], bridgeApps, benchmarks)
   416  					} else if len(args) == 0 {
   417  						errs = Test(h, bridgeApps, benchmarks)
   418  					} else {
   419  						return cmd.MakeErr(c, "expected 0 args (run all stand-alone tests), 1 arg (a single stand-alone test) or 2 args (scenario and role)")
   420  					}
   421  				} else {
   422  					var bridgeApps []holo.BridgeApp
   423  					if bridgeAppTmpFilePath != "" {
   424  						bridgeApps, err = getBridgeAppsFromTmpFile(bridgeAppTmpFilePath)
   425  						if err != nil {
   426  							return cmd.MakeErrFromErr(c, err)
   427  						}
   428  					}
   429  
   430  					holo.Debug("test: scenario")
   431  
   432  					scenario := args[0]
   433  					role := args[1]
   434  					holo.Debugf("test: scenario(%v, %v)\n", scenario, role)
   435  
   436  					holo.Debugf("test: scenario(%v, %v): paused at: %v\n", scenario, role, time.Now())
   437  
   438  					if syncPauseUntil != 0 {
   439  						// IntFlag converts the string into int64 anyway. This explicit conversion is valid
   440  						time.Sleep(cmd.GetDuration_fromUnixTimestamp(int64(syncPauseUntil)))
   441  					}
   442  					holo.Debugf("test: scenario(%v, %v): continuing at: %v\n", scenario, role, time.Now())
   443  					pairs := map[string]string{"%server%": serverID}
   444  
   445  					// The clone id is put into the identity by scenario call so we get
   446  					// out with this regex
   447  					re := regexp.MustCompile(`.*.([0-9]+)@.*`)
   448  					x := re.FindStringSubmatch(string(h.Agent().Identity()))
   449  					var clone string
   450  					if len(x) > 0 {
   451  						clone = x[1]
   452  						pairs["%clone%"] = clone
   453  					}
   454  
   455  					host := getHostName(serverID)
   456  					err = addRolesToPairs(h, scenario, host, pairs)
   457  					if err != nil {
   458  						return cmd.MakeErrFromErr(c, err)
   459  					}
   460  
   461  					err, errs = TestScenario(h, scenario, role, pairs, benchmarks, bridgeApps)
   462  					if err != nil {
   463  						return cmd.MakeErrFromErr(c, err)
   464  					}
   465  					//holo.Debugf("testScenario: h: %v\n", spew.Sdump(h))
   466  
   467  				}
   468  
   469  				var s string
   470  				for _, e := range errs {
   471  					s += e.Error()
   472  				}
   473  				if s != "" {
   474  					return cmd.MakeErr(c, s)
   475  				}
   476  				return nil
   477  			},
   478  		},
   479  		{
   480  			Name:      "scenario",
   481  			Aliases:   []string{"s"},
   482  			Usage:     "run a scenario test",
   483  			ArgsUsage: "scenario-name",
   484  			Flags: []cli.Flag{
   485  				cli.StringFlag{
   486  					Name:        "outputDir",
   487  					Usage:       "directory to send output",
   488  					Destination: &outputDir,
   489  				},
   490  				cli.BoolFlag{
   491  					Name:        "benchmarks",
   492  					Usage:       "calculate benchmarks during scenario test",
   493  					Destination: &benchmarks,
   494  				},
   495  			},
   496  			Action: func(c *cli.Context) error {
   497  				mutableContext.str["command"] = "scenario"
   498  
   499  				if err := appCheck(devPath); err != nil {
   500  					return err
   501  				}
   502  
   503  				args := c.Args()
   504  				if len(args) != 1 {
   505  					return cmd.MakeErr(c, "missing scenario name argument")
   506  				}
   507  				scenarioName := args[0]
   508  
   509  				// get the holochain from the source that we are supposed to be testing
   510  				h, err := getHolochain(c, service, identity)
   511  				if err != nil {
   512  					return cmd.MakeErrFromErr(c, err)
   513  				}
   514  
   515  				// get the bridgeApps
   516  				var bridgeApps []BridgeAppForTests
   517  				bridgeApps, err = getBridgeAppForTests(service, h.Agent())
   518  				if err != nil {
   519  					return cmd.MakeErrFromErr(c, err)
   520  				}
   521  
   522  				var bridgeAppsTmpfileName string
   523  				if len(bridgeApps) > 0 {
   524  					bridgeAppsTmpfileName, err = saveBridgeAppsToTmpFile(bridgeApps)
   525  					if err != nil {
   526  						return cmd.MakeErrFromErr(c, err)
   527  					}
   528  				}
   529  
   530  				//Spin up the bridgeApps
   531  				var bridgeAppServers []*ui.WebServer
   532  				for _, app := range bridgeApps {
   533  					var bridgeAppServer *ui.WebServer
   534  					bridgeAppServer, err = StartBridgeApp(app.H, app.BridgeApp.Port)
   535  					if err != nil {
   536  						return cmd.MakeErrFromErr(c, err)
   537  					}
   538  					bridgeAppServers = append(bridgeAppServers, bridgeAppServer)
   539  				}
   540  
   541  				// mutableContext.obj["initialHolochain"] = h
   542  				testScenarioList, err := holo.GetTestScenarios(h)
   543  				if err != nil {
   544  					return cmd.MakeErrFromErr(c, err)
   545  				}
   546  				mutableContext.obj["testScenarioList"] = &testScenarioList
   547  
   548  				// confirm the user chosen scenario name
   549  				//   TODO add this to code completion
   550  				if _, ok := testScenarioList[scenarioName]; !ok {
   551  					return cmd.MakeErr(c, "source argument is not directory in /test. scenario name must match directory name")
   552  				}
   553  				mutableContext.str["testScenarioName"] = scenarioName
   554  
   555  				// get list of roles
   556  				roleList, err := holo.GetTestScenarioRoles(h, scenarioName)
   557  				if err != nil {
   558  					return cmd.MakeErrFromErr(c, err)
   559  				}
   560  				mutableContext.obj["testScenarioRoleList"] = &roleList
   561  
   562  				// run a bunch of hcdev test processes. Separate temp folder by username in case
   563  				// multiple users on the same machine are running tests
   564  				rootExecDir, err := cmd.MakeTmpDir(scenarioTmpDir)
   565  				if err != nil {
   566  					return cmd.MakeErrFromErr(c, err)
   567  				}
   568  				secondsFromNowPlusDelay := cmd.GetUnixTimestamp_secondsFromNow(scenarioStartDelay)
   569  
   570  				scenarioPath := filepath.Join(h.TestPath(), scenarioName)
   571  
   572  				scenarioConfig, err = holo.LoadTestConfig(scenarioPath)
   573  				if err != nil {
   574  					return cmd.MakeErrFromErr(c, err)
   575  				}
   576  
   577  				if outputDir != "" {
   578  					err = os.MkdirAll(outputDir, os.ModePerm)
   579  					if err != nil {
   580  						return cmd.MakeErrFromErr(c, err)
   581  					}
   582  				}
   583  
   584  				for roleIndex, roleName := range roleList {
   585  					holo.Debugf("scenario: forRole(%v): start\n\n", roleName)
   586  
   587  					// HOLOCHAINCONFIG_DHTPORT       = FindSomeAvailablePort
   588  					// HOLOCHAINCONFIG_ENABLEMDNS = "true" or HOLOCHAINCONFIG_BOOTSTRAP = "ip[localhost]:port[3142]
   589  					// HCLOG_PREFIX  = role
   590  
   591  					clones := 1
   592  
   593  					for _, clone := range scenarioConfig.Clone {
   594  						if clone.Role == roleName {
   595  							clones = clone.Number
   596  							break
   597  						}
   598  					}
   599  
   600  					// if the bootstrapServer flag isn't set we assume this is a local scenario
   601  					// test so we set the flag "_" the use no bootstrap
   602  					if bootstrapServer == "" {
   603  						bootstrapServer = "_"
   604  					}
   605  
   606  					// check to see if there's a bridge config for the role
   607  					scenarioBridgeSpecs := filepath.Join(scenarioPath, "_"+roleName+"_"+defaultSpecsFile)
   608  					holo.Debugf("scenario: looking for bridgeSpecs:%v", scenarioBridgeSpecs)
   609  					if !holo.FileExists(scenarioBridgeSpecs) {
   610  						scenarioBridgeSpecs = bridgeSpecsFile
   611  					}
   612  
   613  					originalRoleName := roleName
   614  					for count := 0; count < clones; count++ {
   615  						freePort, err := cmd.GetFreePort()
   616  						if err != nil {
   617  							return cmd.MakeErrFromErr(c, err)
   618  						}
   619  
   620  						if clones > 1 {
   621  							roleName = fmt.Sprintf("%s.%d", originalRoleName, count)
   622  
   623  						}
   624  						agentID = roleName
   625  						if serverID != "" {
   626  							roleName = serverID + "." + roleName
   627  						}
   628  						holo.Debugf("scenario: forRole(%v): port: %v\n\n", roleName, freePort)
   629  
   630  						colorByNumbers := []string{"green", "blue", "yellow", "cyan", "magenta", "red"}
   631  
   632  						logPrefix := "%{color:" + colorByNumbers[roleIndex%6] + "}" + roleName + ": "
   633  						/* time doesn't work in prefix yet
   634  						if outputDir != "" {
   635  							logPrefix = "%{time}" + logPrefix
   636  						}*/
   637  
   638  						var upnpnat string
   639  						if bootstrapServer == "_" {
   640  							upnpnat = "false"
   641  						} else {
   642  							upnpnat = "true"
   643  						}
   644  						testCommand := exec.Command(
   645  							"hcdev",
   646  							"-path="+devPath,
   647  							"-execpath="+filepath.Join(rootExecDir, roleName),
   648  							"-DHTport="+strconv.Itoa(freePort),
   649  							fmt.Sprintf("-mdns=%v", mdns),
   650  							"-upnp="+upnpnat,
   651  							"-logPrefix="+logPrefix,
   652  							"-serverID="+serverID,
   653  							"-agentID="+agentID,
   654  							fmt.Sprintf("-bootstrapServer=%v", bootstrapServer),
   655  							fmt.Sprintf("-keepalive=%v", keepalive),
   656  
   657  							"test",
   658  							fmt.Sprintf("-bridgeAppsFile=%v", bridgeAppsTmpfileName),
   659  							fmt.Sprintf("-benchmarks=%v", benchmarks),
   660  							fmt.Sprintf("-syncPauseUntil=%v", secondsFromNowPlusDelay),
   661  							scenarioName,
   662  							originalRoleName,
   663  						)
   664  
   665  						mutableContext.obj["testCommand."+roleName] = &testCommand
   666  
   667  						holo.Debugf("scenario: forRole(%v): testCommandPrepared: %v\n", roleName, testCommand)
   668  
   669  						if outputDir != "" {
   670  							f := filepath.Join(outputDir, roleName)
   671  							df, err := os.Create(f)
   672  							if err != nil {
   673  								return cmd.MakeErrFromErr(c, err)
   674  							}
   675  							defer df.Close()
   676  							testCommand.Stdout = df
   677  							testCommand.Stderr = df
   678  						} else {
   679  
   680  							testCommand.Stdout = os.Stdout
   681  							testCommand.Stderr = os.Stderr
   682  						}
   683  						testCommand.Start()
   684  
   685  						holo.Debugf("scenario: forRole(%v): testCommandStarted\n", roleName)
   686  					}
   687  				}
   688  				keepalive = true
   689  				if len(bridgeApps) > 0 {
   690  					keepaliveCleanup = func() {
   691  						StopBridgeApps(bridgeAppServers)
   692  						os.Remove(bridgeAppsTmpfileName)
   693  					}
   694  				}
   695  				return nil
   696  			},
   697  		},
   698  		{
   699  			Name:      "web",
   700  			Aliases:   []string{"serve", "w"},
   701  			ArgsUsage: "[ui-port]",
   702  			Usage:     fmt.Sprintf("serve a chain to the web on localhost:<ui-port> (default: %s)", defaultUIPort),
   703  			Action: func(c *cli.Context) error {
   704  				if err := appCheck(devPath); err != nil {
   705  					return cmd.MakeErrFromErr(c, err)
   706  				}
   707  
   708  				h, err := getHolochain(c, service, agentID)
   709  				if err != nil {
   710  					return cmd.MakeErrFromErr(c, err)
   711  				}
   712  
   713  				bridgeApps, err := getBridgeAppForTests(service, h.Agent())
   714  				if err != nil {
   715  					return cmd.MakeErrFromErr(c, err)
   716  				}
   717  
   718  				h.Close()
   719  				h, err = service.GenChain(name)
   720  				if err != nil {
   721  					return cmd.MakeErrFromErr(c, err)
   722  				}
   723  
   724  				var port string
   725  				if len(c.Args()) == 0 {
   726  					port = defaultUIPort
   727  				} else {
   728  					port = c.Args()[0]
   729  				}
   730  
   731  				var ws *ui.WebServer
   732  				ws, err = activate(h, port)
   733  				if err != nil {
   734  					return cmd.MakeErrFromErr(c, err)
   735  				}
   736  
   737  				var bridgeAppServers []*ui.WebServer
   738  				bridgeAppServers, err = BuildBridges(h, port, bridgeApps)
   739  				if err != nil {
   740  					return cmd.MakeErrFromErr(c, err)
   741  				}
   742  				ws.Wait()
   743  				// TODO call StopBridgeApps instead????
   744  				for _, server := range bridgeAppServers {
   745  					server.Stop()
   746  				}
   747  
   748  				return nil
   749  			},
   750  		},
   751  
   752  		{
   753  			Name:      "package",
   754  			Aliases:   []string{"p"},
   755  			ArgsUsage: "[output file]",
   756  			Usage:     fmt.Sprintf("writes a package file of the dev path to file or stdout"),
   757  			Action: func(c *cli.Context) error {
   758  
   759  				var old *os.File
   760  				if len(c.Args()) == 0 {
   761  					old = os.Stdout // keep backup of the real stdout
   762  					_, w, _ := os.Pipe()
   763  					os.Stdout = w
   764  				}
   765  
   766  				if err := appCheck(devPath); err != nil {
   767  					return err
   768  				}
   769  				h, err := getHolochain(c, service, identity)
   770  				if err != nil {
   771  					return cmd.MakeErrFromErr(c, err)
   772  				}
   773  				appPackage, err := service.MakeAppPackage(h)
   774  				if err != nil {
   775  					return cmd.MakeErrFromErr(c, err)
   776  				}
   777  
   778  				if len(c.Args()) == 0 {
   779  					os.Stdout = old
   780  					fmt.Print(string(appPackage))
   781  				} else {
   782  					err = holo.WriteFile(appPackage, c.Args().First())
   783  				}
   784  				if err != nil {
   785  					return cmd.MakeErrFromErr(c, err)
   786  				}
   787  				return nil
   788  			},
   789  		},
   790  
   791  		{
   792  			Name:      "dump",
   793  			Aliases:   []string{"d"},
   794  			ArgsUsage: "holochain-name",
   795  			Usage:     "display a text dump of a chain after last 'web', 'test', or 'scenario'",
   796  			Flags: []cli.Flag{
   797  				cli.BoolFlag{
   798  					Name:        "chain",
   799  					Destination: &dumpChain,
   800  				},
   801  				cli.BoolFlag{
   802  					Name:        "dht",
   803  					Destination: &dumpDHT,
   804  				},
   805  				cli.BoolFlag{
   806  					Name:        "json",
   807  					Destination: &json,
   808  					Usage:       "Dump chain or dht as JSON string",
   809  				},
   810  				cli.IntFlag{
   811  					Name:        "index",
   812  					Destination: &start,
   813  					Usage:       "starting index for dump (zero based)",
   814  				},
   815  				cli.BoolFlag{
   816  					Name:        "test",
   817  					Destination: &dumpTest,
   818  				},
   819  				cli.StringFlag{
   820  					Name:        "scenario",
   821  					Destination: &dumpScenario,
   822  				},
   823  				cli.StringFlag{
   824  					Name:        "format",
   825  					Destination: &dumpFormat,
   826  					Usage:       "Dump format (string, json, dot)",
   827  					Value:       "string",
   828  				},
   829  			},
   830  			Action: func(c *cli.Context) error {
   831  
   832  				if !dumpChain && !dumpDHT {
   833  					dumpChain = true
   834  				}
   835  
   836  				var h *holo.Holochain
   837  				var s *holo.Service
   838  				var err error
   839  				if dumpTest {
   840  					panic("not implemented")
   841  				} else if dumpScenario != "" {
   842  					// the value is the role name which has it's own service for that role
   843  					var d string
   844  					d, err = cmd.GetTmpDir(scenarioTmpDir)
   845  					if err == nil {
   846  						s, err = holo.LoadService(filepath.Join(d, dumpScenario))
   847  					}
   848  				} else {
   849  					// use default service
   850  					s = service
   851  				}
   852  				if err == nil {
   853  					h, err = s.Load(name)
   854  					if err == nil {
   855  						err = h.Prepare()
   856  					}
   857  				}
   858  				if err != nil {
   859  					return cmd.MakeErrFromErr(c, err)
   860  				}
   861  
   862  				if !h.Started() {
   863  					return cmd.MakeErr(c, "No data to dump, chain not yet initialized.")
   864  				}
   865  
   866  				dnaHash := h.DNAHash()
   867  				if dumpChain {
   868  					if json {
   869  						dump, _ := h.Chain().JSON(start)
   870  						fmt.Println(dump)
   871  					} else if dumpFormat != "" {
   872  						switch dumpFormat {
   873  						case "string":
   874  							fmt.Printf("Chain for: %s\n%v", dnaHash, h.Chain().Dump(start))
   875  						case "dot":
   876  							dump, _ := h.Chain().Dot(start)
   877  							fmt.Println(dump)
   878  						case "json":
   879  							dump, _ := h.Chain().JSON(start)
   880  							fmt.Println(dump)
   881  						default:
   882  							return cmd.MakeErr(c, "format must be one of dot,json,string")
   883  						}
   884  					} else {
   885  						fmt.Printf("Chain for: %s\n%v", dnaHash, h.Chain().Dump(start))
   886  					}
   887  				}
   888  				if dumpDHT {
   889  					if json {
   890  						dump, _ := h.DHT().JSON()
   891  						fmt.Println(dump)
   892  					} else {
   893  						fmt.Printf("DHT for: %s\n%v", dnaHash, h.DHT().String())
   894  					}
   895  				}
   896  
   897  				return nil
   898  			},
   899  		},
   900  	}
   901  
   902  	app.Before = func(c *cli.Context) error {
   903  		lastRunContext = c
   904  
   905  		var err error
   906  
   907  		if dhtPort != "" {
   908  			err = os.Setenv("HOLOCHAINCONFIG_DHTPORT", dhtPort)
   909  			if err != nil {
   910  				return err
   911  			}
   912  		}
   913  		if mdns {
   914  			err = os.Setenv("HOLOCHAINCONFIG_ENABLEMDNS", "true")
   915  			if err != nil {
   916  				return err
   917  			}
   918  		}
   919  		if upnp != true {
   920  			err = os.Setenv("HOLOCHAINCONFIG_ENABLENATUPNP", "false")
   921  			if err != nil {
   922  				return err
   923  			}
   924  		}
   925  		if logPrefix != "" {
   926  			os.Setenv("HCLOG_PREFIX", logPrefix)
   927  			if err != nil {
   928  				return err
   929  			}
   930  		}
   931  		if bootstrapServer != "" {
   932  			os.Setenv("HOLOCHAINCONFIG_BOOTSTRAP", bootstrapServer)
   933  			if err != nil {
   934  				return err
   935  			}
   936  		}
   937  
   938  		holo.Debugf("args:%v\n", c.Args())
   939  
   940  		// hcdev always enables the app debugging, and the -debug flag enables the holochain debugging
   941  		os.Setenv("HCLOG_APP_ENABLE", "1")
   942  		if debug {
   943  			os.Setenv("HCLOG_DHT_ENABLE", "1")
   944  			os.Setenv("HCLOG_GOSSIP_ENABLE", "1")
   945  			os.Setenv("HCLOG_DEBUG_ENABLE", "1")
   946  		}
   947  		holo.InitializeHolochain()
   948  
   949  		if devPath == "" {
   950  			devPath, err = os.Getwd()
   951  			if err != nil {
   952  				return err
   953  			}
   954  		}
   955  		name = filepath.Base(devPath)
   956  
   957  		if cmd.IsAppDir(devPath) == nil {
   958  			appInitialized = true
   959  		}
   960  
   961  		if rootPath == "" {
   962  			rootPath = os.Getenv("HOLOPATHDEV")
   963  			if rootPath == "" {
   964  				userPath := sysUser.HomeDir
   965  				rootPath = filepath.Join(userPath, holo.DefaultDirectoryName+"dev")
   966  			}
   967  		}
   968  		identity = getIdentity(agentID, serverID)
   969  		if !holo.IsInitialized(rootPath) {
   970  			service, err = holo.Init(rootPath, holo.AgentIdentity(identity), holo.MakeTestSeed(identity))
   971  			if err != nil {
   972  				return err
   973  			}
   974  			fmt.Println("Holochain dev service initialized:")
   975  			fmt.Printf("    %s directory created\n", rootPath)
   976  			fmt.Printf("    defaults stored to %s\n", holo.SysFileName)
   977  			fmt.Println("    key-pair generated")
   978  			fmt.Printf("    using %s as default agent identity (stored to %s)\n", identity, holo.AgentFileName)
   979  
   980  		} else {
   981  			service, err = holo.LoadService(rootPath)
   982  		}
   983  		return err
   984  	}
   985  
   986  	app.Action = func(c *cli.Context) error {
   987  		cli.ShowAppHelp(c)
   988  
   989  		return nil
   990  	}
   991  	return
   992  
   993  }
   994  
   995  func main() {
   996  	app := setupApp()
   997  	err := app.Run(os.Args)
   998  	var stop chan bool
   999  	if keepalive {
  1000  		stop = make(chan bool, 1)
  1001  	}
  1002  	if keepalive && scenarioConfig != nil {
  1003  		go func() {
  1004  			time.Sleep(time.Second*(scenarioStartDelay+time.Duration(scenarioConfig.Duration)) + time.Second*scenarioStartDelay)
  1005  			stop <- true
  1006  		}()
  1007  	}
  1008  	if keepalive {
  1009  		<-stop
  1010  	}
  1011  	if keepaliveCleanup != nil {
  1012  		keepaliveCleanup()
  1013  	}
  1014  	if verbose {
  1015  		fmt.Printf("hcdev complete!\n")
  1016  	}
  1017  	if err != nil {
  1018  		fmt.Printf("Error: %v\n", err)
  1019  		os.Exit(1)
  1020  	}
  1021  }
  1022  
  1023  func getHolochain(c *cli.Context, service *holo.Service, identity string) (h *holo.Holochain, err error) {
  1024  	// clear out the previous chain data that was copied from the last test/run
  1025  	err = os.RemoveAll(filepath.Join(rootPath, name))
  1026  	if err != nil {
  1027  		return
  1028  	}
  1029  	var agent holo.Agent
  1030  	agent, err = holo.LoadAgent(rootPath)
  1031  	if err != nil {
  1032  		return
  1033  	}
  1034  
  1035  	if identity != "" {
  1036  		holo.SetAgentIdentity(agent, holo.AgentIdentity(identity))
  1037  	}
  1038  
  1039  	fmt.Printf("Copying chain to: %s\n", rootPath)
  1040  	h, err = service.Clone(devPath, filepath.Join(rootPath, name), agent, holo.CloneWithSameUUID, holo.InitializeDB)
  1041  	if err != nil {
  1042  		return
  1043  	}
  1044  	h.Close()
  1045  
  1046  	h, err = service.Load(name)
  1047  	if err != nil {
  1048  		return
  1049  	}
  1050  	if verbose {
  1051  		fmt.Printf("Identity: %s\n", h.Agent().Identity())
  1052  		fmt.Printf("NodeID: %s\n", h.NodeIDStr())
  1053  	}
  1054  	return
  1055  }
  1056  
  1057  // BridgeSpec describes an app to be bridged for dev
  1058  type BridgeSpec struct {
  1059  	Path                    string // path to the app to bridge to/from
  1060  	Side                    int    // what side of the bridge the dev app is (Bridge.Caller or Bridge.Callee)
  1061  	BridgeGenesisCallerData string // genesis data for the caller side
  1062  	BridgeGenesisCalleeData string // genesis data for the callee side
  1063  	Port                    string // only used if side == BridgeCallee
  1064  	BridgeZome              string // only used if side == BridgeCaller
  1065  }
  1066  
  1067  // getBridgeAppsForTests builds up an array of bridged apps based on the dev values for bridging
  1068  func getBridgeAppForTests(service *holo.Service, agent holo.Agent) (bridgedApps []BridgeAppForTests, err error) {
  1069  	if bridgeSpecsFile == "_" {
  1070  		return
  1071  	}
  1072  	var specs []BridgeSpec
  1073  	specs, err = loadBridgeSpecs()
  1074  	if err != nil {
  1075  		return
  1076  	}
  1077  	for _, spec := range specs {
  1078  		var h *holo.Holochain
  1079  		h, err = setupBridgeApp(service, agent, spec.Path)
  1080  		if err != nil {
  1081  			return
  1082  		}
  1083  		if spec.Port == "" {
  1084  			var port int
  1085  			port, err = cmd.GetFreePort()
  1086  			if err != nil {
  1087  				return
  1088  			}
  1089  			spec.Port = fmt.Sprintf("%d", port)
  1090  		}
  1091  		bridgedApps = append(bridgedApps,
  1092  			BridgeAppForTests{
  1093  				H: h,
  1094  				BridgeApp: holo.BridgeApp{
  1095  					Name: h.Name(),
  1096  					DNA:  h.DNAHash(),
  1097  					Side: spec.Side,
  1098  					BridgeGenesisCallerData: spec.BridgeGenesisCallerData,
  1099  					BridgeGenesisCalleeData: spec.BridgeGenesisCalleeData,
  1100  					Port:       spec.Port,
  1101  					BridgeZome: spec.BridgeZome,
  1102  				},
  1103  			})
  1104  	}
  1105  	return
  1106  }
  1107  
  1108  // setupBridgeApp clones the bridge app from source and loads it in preparation for actual bridging
  1109  func setupBridgeApp(service *holo.Service, agent holo.Agent, path string) (bridgeH *holo.Holochain, err error) {
  1110  
  1111  	bridgeName := filepath.Base(path)
  1112  
  1113  	os.Setenv("HOLOCHAINCONFIG_ENABLEMDNS", "true")
  1114  	os.Setenv("HOLOCHAINCONFIG_BOOTSTRAP", "_")
  1115  	os.Setenv("HCLOG_PREFIX", bridgeName+":")
  1116  
  1117  	var freePort int
  1118  	freePort, err = cmd.GetFreePort()
  1119  	if err != nil {
  1120  		return
  1121  	}
  1122  
  1123  	os.Setenv("HOLOCHAINCONFIG_DHTPORT", fmt.Sprintf("%d", freePort))
  1124  
  1125  	fmt.Printf("Copying bridge chain %s to: %s\n", bridgeName, rootPath)
  1126  	// cleanup from previous time
  1127  	err = os.RemoveAll(filepath.Join(rootPath, bridgeName))
  1128  	if err != nil {
  1129  		return
  1130  	}
  1131  	_, err = service.Clone(path, filepath.Join(rootPath, bridgeName), agent, holo.CloneWithSameUUID, holo.InitializeDB)
  1132  	if err != nil {
  1133  		return
  1134  	}
  1135  
  1136  	bridgeH, err = service.Load(bridgeName)
  1137  	if err != nil {
  1138  		return
  1139  	}
  1140  
  1141  	_, err = bridgeH.GenChain()
  1142  	if err != nil {
  1143  		return
  1144  	}
  1145  
  1146  	// clear the log prefix for the next load.
  1147  	os.Unsetenv("HCLOG_PREFIX")
  1148  	return
  1149  }
  1150  
  1151  func activate(h *holo.Holochain, port string) (ws *ui.WebServer, err error) {
  1152  	fmt.Printf("Serving holochain with DNA hash:%v on port:%s\n", h.DNAHash(), port)
  1153  	err = h.Activate()
  1154  	if err != nil {
  1155  		return
  1156  	}
  1157  	h.StartBackgroundTasks()
  1158  	ws = ui.NewWebServer(h, port)
  1159  	ws.Start()
  1160  	return
  1161  }
  1162  
  1163  func GetLastRunContext() (MutableContext, *cli.Context) {
  1164  	return mutableContext, lastRunContext
  1165  }
  1166  
  1167  func doClone(s *holo.Service, clonePath, devPath string) (err error) {
  1168  
  1169  	// TODO this is the bogus dev agent, really it should probably be someone else
  1170  	agent, err := holo.LoadAgent(rootPath)
  1171  	if err != nil {
  1172  		return
  1173  	}
  1174  
  1175  	_, err = s.Clone(clonePath, devPath, agent, holo.CloneWithSameUUID, holo.SkipInitializeDB)
  1176  	if err != nil {
  1177  		return
  1178  	}
  1179  	return
  1180  }
  1181  
  1182  func loadBridgeSpecs() (specs []BridgeSpec, err error) {
  1183  	if bridgeSpecsFile == "" {
  1184  		holo.Debug("no bridgeSpecs checking for default")
  1185  		if holo.FileExists(defaultSpecsFile) {
  1186  			bridgeSpecsFile = defaultSpecsFile
  1187  		}
  1188  	}
  1189  	if bridgeSpecsFile != "" {
  1190  		holo.Debugf("load bridgeSpecs:%s", bridgeSpecsFile)
  1191  		err = holo.DecodeFile(&specs, bridgeSpecsFile)
  1192  	}
  1193  	return
  1194  }
  1195  
  1196  func getHostName(serverID string) (host string) {
  1197  	if serverID != "" {
  1198  		host = serverID
  1199  	} else {
  1200  		host, _ = os.Hostname()
  1201  	}
  1202  	if host == "" {
  1203  		host = "example.com"
  1204  	}
  1205  	return
  1206  }
  1207  
  1208  func getIdentity(agentID, serverID string) (identity string) {
  1209  	var host, username string
  1210  	host = getHostName(serverID)
  1211  
  1212  	if agentID != "" {
  1213  		username = agentID
  1214  	} else {
  1215  		username = sysUser.Username
  1216  	}
  1217  	if username == "" {
  1218  		username = "test"
  1219  	}
  1220  
  1221  	identity = username + "@" + host
  1222  	return
  1223  }
  1224  
  1225  func addRolesToPairs(h *holo.Holochain, scenario string, host string, pairs map[string]string) (err error) {
  1226  
  1227  	var roles []string
  1228  	roles, err = holo.GetTestScenarioRoles(h, scenario)
  1229  	if err != nil {
  1230  		return
  1231  	}
  1232  
  1233  	dir := filepath.Join(h.TestPath(), scenario)
  1234  	var config *holo.TestConfig
  1235  	config, err = holo.LoadTestConfig(dir)
  1236  	if err != nil {
  1237  		return
  1238  	}
  1239  
  1240  	cloneRoles := make(map[string]holo.CloneSpec)
  1241  	for _, spec := range config.Clone {
  1242  		cloneRoles[spec.Role] = spec
  1243  	}
  1244  
  1245  	for _, role := range roles {
  1246  
  1247  		var testSet holo.TestSet
  1248  		testSet, err = holo.LoadTestFile(dir, role+".json")
  1249  		if err != nil {
  1250  			return
  1251  		}
  1252  		spec, isClone := cloneRoles[role]
  1253  
  1254  		if testSet.Identity != "" {
  1255  			if isClone {
  1256  				err = fmt.Errorf("can't both clone and specify an identity: role %s", role)
  1257  				return
  1258  			}
  1259  			err = addRoleToPairs(h, role, testSet.Identity, pairs)
  1260  		} else {
  1261  			if isClone {
  1262  				origRole := role
  1263  				for i := 0; i < spec.Number; i++ {
  1264  					role = fmt.Sprintf("%s.%d", origRole, i)
  1265  					err = addRoleToPairs(h, role, fmt.Sprintf("%s@%s", role, host), pairs)
  1266  				}
  1267  			} else {
  1268  				err = addRoleToPairs(h, role, role+"@"+host, pairs)
  1269  			}
  1270  		}
  1271  
  1272  	}
  1273  	return
  1274  }
  1275  func addRoleToPairs(h *holo.Holochain, role string, id string, pairs map[string]string) (err error) {
  1276  	var agent holo.Agent
  1277  	agent, err = holo.NewAgent(holo.LibP2P, holo.AgentIdentity(id), holo.MakeTestSeed(id))
  1278  	if err != nil {
  1279  		return
  1280  	}
  1281  	var hash string
  1282  	_, hash, err = agent.NodeID()
  1283  	if err != nil {
  1284  		return
  1285  	}
  1286  	pairs["%"+role+"_str%"] = id
  1287  	pairs["%"+role+"_key%"] = hash
  1288  	return
  1289  }
  1290  
  1291  func saveBridgeAppsToTmpFile(bridgeAppsForTests []BridgeAppForTests) (bridgeAppsTmpfileName string, err error) {
  1292  	var bridgeApps []holo.BridgeApp
  1293  	for _, app := range bridgeAppsForTests {
  1294  		bridgeApps = append(bridgeApps, app.BridgeApp)
  1295  	}
  1296  	var tmpfile *os.File
  1297  	tmpfile, err = ioutil.TempFile("", "bridgeApp")
  1298  	if err != nil {
  1299  		return
  1300  	}
  1301  	bridgeAppsTmpfileName = tmpfile.Name()
  1302  
  1303  	var data []byte
  1304  	data, err = holo.ByteEncoder(bridgeApps)
  1305  	if err != nil {
  1306  		return
  1307  	}
  1308  	_, err = tmpfile.Write(data)
  1309  	tmpfile.Close()
  1310  
  1311  	return
  1312  }
  1313  
  1314  func getBridgeAppsFromTmpFile(path string) (bridgeApps []holo.BridgeApp, err error) {
  1315  
  1316  	data, err := holo.ReadFile(path)
  1317  	if err != nil {
  1318  		return
  1319  	}
  1320  	err = holo.ByteDecoder(data, &bridgeApps)
  1321  
  1322  	return
  1323  }