Example #1
0
File: service.go Project: axw/lingo
// StartService starts up the program as a micro-service server.
func (l *service) Start() error {

	// set a fixed socket and manually start process to help with debugging.
	// l.socketAddr = "@01c67"
	// return nil

	// Start up the mirco-service
	log.Println("starting process", l.program)
	log.Println(l.args)
	cmd := exec.Command(l.program, l.args...)
	p, err := cmd.StdoutPipe()
	if err != nil {
		return errors.Trace(err)
	}
	if err := cmd.Start(); err != nil {
		return errors.Trace(err)
	}
	l.process = cmd.Process

	// Get the socket address from the server.
	b := bufio.NewReader(p)
	line, _, err := b.ReadLine()
	if err != nil {
		log.Println("unable to get socket address from server, stopping tenet")
		l.Stop()
		return errors.Trace(err)
	}

	l.socketAddr = strings.TrimSuffix(string(line), "\n")

	return nil
}
Example #2
0
func (b *Base) SendError(err error) {
	log.Println("sending error", err)
	log.Println(b.errorsc == nil)
	select {
	case b.errorsc <- err:
	default:
	}
}
Example #3
0
File: tenet.go Project: axw/lingo
// Close closes the connection and, if local, stops the backing service.
func (t *tenetService) Close() error {
	log.Println("closing conn")

	if t.Service == nil {
		return errors.New("attempted to close a nil service. Has it been started?")
	}
	err := t.conn.Close()
	log.Println("stopping service")
	if err1 := t.Service.Stop(); err1 != nil {
		err = err1
	}
	return err
}
Example #4
0
func (r *review) raiseIssue(issueName string, iRange *issueRange, opts []RaiseIssueOption) Review {
	if r.areAllContextsMatched() {
		return r
	}

	b := r.baseTenet()
	// TODO(waigani) error handle this.
	i := b.registeredIssues[issueName]
	if i == nil {
		// Yes panic, this is a developer error.
		msg := fmt.Sprintf("issue %q cannot be raised before it is registered", issueName)
		panic(msg)
	}
	issue := &Issue{}
	x := *i
	x.copyTo(issue)

	// TODO(waigani) This a Go blemish. Is there a nicer way to copy a struct with a map?
	issue.CommVars = map[string]interface{}{}
	for _, opt := range opts {
		opt(issue)
	}

	// TODO(waigani) this is a quick hack. We need to pull File out of *Issue.
	issue.file = r.File()
	issue.setSource(iRange)

	if err := r.setContextualComment(issue); err != nil {
		// If no comment has been set for the context in which this issue was
		// found, don't raise it.
		if err == errNoCommentForContext {
			return r
		}
		issue.Err = err
	}

	log.Println("sending issue")
	r.sendIssue(issue)
	log.Println("not blocked")

	if r.areAllContextsMatched() {

		// This is our last issue raised, close the issue chan. Note: if a
		// tenet reviews async, this will become a race condition.
		r.Close()
	}
	return r
}
Example #5
0
File: service.go Project: axw/lingo
// NewService allows you to run a program on the localhost as a micro-service.
func NewService(program string, args ...string) *service {
	log.Println("NewLocal service")
	return &service{
		program: program,
		args:    args,
	}
}
Example #6
0
File: tenet.go Project: axw/lingo
func (t *tenet) OpenService() (TenetService, error) {
	ds, err := t.Driver.Service()
	if err != nil {
		log.Print(err.Error()) // TODO(waigani) this logs are quick hacks. Work out the error paths and log them all at the root.
		return nil, errors.Trace(err)
	}

	cfg := &api.Config{}
	for k, v := range t.options {
		cfg.Options = append(cfg.Options,
			&api.Option{
				Name:  k,
				Value: fmt.Sprintf("%v", v),
			})
	}

	s := &tenetService{
		Service:      ds,
		cfg:          cfg,
		editFilename: t.Driver.EditFilename,
		editIssue:    t.Driver.EditIssue,
		mutex:        &sync.Mutex{},
	}

	if err := s.start(); err != nil {
		log.Println("got err opening service")
		// TODO(waigani) add retry logic here. 1. Keep retrying until service
		// is up. 2. Keep retrying until service is connected.
		log.Printf("err: %#v", errors.ErrorStack(err))

		return nil, errors.Trace(err)
	}
	log.Print("opened service, no issue")
	return s, nil
}
Example #7
0
File: service.go Project: axw/lingo
func (l *service) DialGRPC() (*grpc.ClientConn, error) {
	if l.socketAddr == "" {
		return nil, errors.New("socket address is empty. Is the service started?")
	}
	log.Println("dialing server")
	return grpc.Dial(l.socketAddr, grpc.WithDialer(dialer()), grpc.WithInsecure())
}
Example #8
0
// Closes a review. Can be called more than once.
func (r *review) Close() {
	if r.waitc != nil {
		log.Println("closing review and waitc")
		close(r.waitc)
		os.RemoveAll(r.tmpdir)
		r.waitc = nil
	}
}
Example #9
0
File: service.go Project: axw/lingo
// StopService stops the backing tenet server. If Common as a non-nil
// connection to the server, that will be closed.
func (l *service) Stop() (err error) {
	if l.process != nil {
		log.Println("killing process")
		if err = l.process.Kill(); err != nil {
			log.Fatalf("did not stop %s: %v", l.program, err)
		}
	}
	return
}
Example #10
0
func (s *TenetSuite) CheckFiles(c *gc.C, files []string, expectedIssues ...ExpectedIssue) {
	log.Println("CheckFiles")

	br := s.baseReview()
	br.StartReview()
	s.sendFiles(files...)

	s.AssertExpectedIssues(c, ReadAllIssues(c, br), expectedIssues...)
}
Example #11
0
func main() {
	err := app.New().Run(os.Args)
	if err != nil {
		if errors.Cause(err).Error() == "ui" {
			if e, ok := err.(*errors.Err); ok {
				log.Println(e.Underlying())
				fmt.Println(e.Underlying())
				os.Exit(1)
			}
		}
		panic(errors.ErrorStack(err))
	}
}
Example #12
0
func ReadAllIssues(c *gc.C, r tenet.BaseReview) []*tenet.Issue {
	log.Println("reading all issues")
	var issues []*tenet.Issue
l:
	for {
		select {
		case issue, ok := <-r.Issues():
			if !ok {
				break l
			}
			issues = append(issues, issue)
		case <-time.After(10 * time.Second):
			c.Fatal("timed out waiting for issues")
			break l
		}
	}
	return issues
}
Example #13
0
func (b *Base) NewReview() *review {
	r := &review{
		tenet:       b,
		issuesc:     make(chan *Issue),
		filesc:      make(chan *api.File),
		waitc:       make(chan struct{}),
		fileDoneMap: map[string]bool{},
	}
	go func() {
		<-r.waitc
		if r.issuesc != nil {
			log.Println("closing issuesc")
			close(r.issuesc)
		}
		r.waitc = nil
	}()

	return r
}
Example #14
0
// TODO(waigani) call this handleTenetError and make a TenetError type - only
// those can be passed in.
// posOfErr is the position of the node/line that was being parsed when the
// error occoured.
func (b *Base) addErrOnErr(err error, f File, posOfErr interface{}) bool {
	if err != nil {
		// TODO(waigani) this log is a quick hack. We should read all the errs off errorsc.
		log.Println(err.Error())
		errCtx := &errWithContext{err: err}
		switch p := posOfErr.(type) {
		case token.Pos:
			fset := f.Fset()
			pos := fset.Position(p)
			errCtx.errLine = &pos
		case int:
			line := f.(BaseFile).linePosition(p)
			errCtx.errLine = &line
		default:
			panic(fmt.Sprintf("unknown posOfErr type: %T", posOfErr))
		}

		b.errorsc <- errCtx
		return true
	}
	return false
}
Example #15
0
// StartReview listens for files sent to r.SendFile(filename) and reviews them.
func (r *review) StartReview() {
	go func() {
		defer r.Close()
		log.Println("started review")
		b := base(r.tenet)

		// check files synchronously to ensure correct ordering and that we stop
		// after the context is full.
		fset := token.NewFileSet()
		log.Println("reading off filesc")

		for {
			select {
			case file, ok := <-r.filesc:
				if !ok && file == nil {
					log.Println("all files reviewed.")
					return
				}

				f, err := buildFile(file.Name, "", fset, file.Lines)
				if err != nil {
					log.Println("could not build file")
					b.SendError(errors.Annotatef(err, "could not find file: %q", file))
					continue
				}
				log.Println("checking file", file)
				err = r.check(f)
				b.addErrOnErr(err, f, 0)
				log.Println("finished checking file", file)
			case <-time.After(3 * time.Second):
				b.errorsc <- errors.New("timed out waiting for file")
				return
			}
		}
	}()
}
Example #16
0
// Build up a config object by following directories up or down.
func buildConfigRecursive(cfgPath string, cascadeDir CascadeDirection, cfg *Config) error {
	cfgPath, err := filepath.Abs(cfgPath)
	if err != nil {
		return nil
	}

	currentCfg, err := ReadConfigFile(cfgPath)
	if err == nil {
		// Add the non-tenet properties - always when cascading down, otherwise
		// only if not already specified
		// TODO: Use reflection here to avoid forgotten values?
		if cascadeDir == CascadeDown || cfg.Include == "" {
			cfg.Include = currentCfg.Include
		}
		if cascadeDir == CascadeDown || cfg.Template == "" {
			cfg.Template = currentCfg.Template
		}

		// DEMOWARE: Need a better way to assign tenets to groups without duplication
		for _, g := range currentCfg.TenetGroups {
		DupeCheck:
			for _, t := range g.Tenets {
				for _, e := range cfg.allTenets {
					if e.Name == t.Name {
						continue DupeCheck
					}
				}
				cfg.AddTenet(t, g.Name)
				cfg.allTenets = append(cfg.allTenets, t)
			}
		}
		// Asign group properties
		for _, g := range currentCfg.TenetGroups {
			for i, cg := range cfg.TenetGroups {
				if cg.Name == g.Name {
					if g.Template != "" {
						cfg.TenetGroups[i].Template = g.Template
					}
				}
			}
		}
	} else if !os.IsNotExist(err) {
		// Just leave the current state of cfg on encountering an error
		log.Println("error reading file: %s", cfgPath)
		return nil
	}

	currentDir, filename := path.Split(cfgPath)
	switch cascadeDir {
	case CascadeUp:
		if currentDir == "/" || (currentCfg != nil && !currentCfg.Cascade) {
			return nil
		}

		parent := path.Dir(path.Dir(currentDir))

		if err := buildConfigRecursive(path.Join(parent, filename), cascadeDir, cfg); err != nil {
			return err
		}
	case CascadeDown:
		files, err := filepath.Glob(path.Join(currentDir, "*"))
		if err != nil {
			return nil
		}

		for _, f := range files {
			file, err := os.Open(f)
			if err != nil {
				log.Println("error reading file: %s", file)
				return nil
			}
			defer file.Close()
			if fi, err := file.Stat(); err == nil && fi.IsDir() {
				if err := buildConfigRecursive(path.Join(f, filename), cascadeDir, cfg); err != nil {
					return err
				}
			}
		}
	default:
		return errors.New("invalid cascade direction")
	}
	return nil
}
Example #17
0
File: tenet.go Project: axw/lingo
// Review sets up two goroutines. One to send files to the service from filesc,
// the other to recieve issues from the service on issuesc.
func (t *tenetService) Review(filesc <-chan *api.File, issuesc chan<- *api.Issue, filesTM *tomb.Tomb) error {
	stream, err := t.client.Review(context.Background())
	if err != nil {
		return err
	}

	// first setup our issues chan to read from the service.
	go func(issuesc chan<- *api.Issue) {
		for {
			log.Println("waiting for issues")
			issue, err := stream.Recv()
			if err != nil {
				if err == io.EOF ||
					grpc.ErrorDesc(err) == "transport is closing" ||
					err.Error() == "timed out waiting for issues" { // TODO(waigani) error type
					log.Println("closing issuesc")
					// Close our local issues channel.
					close(issuesc)
					return
				}

				// TODO(waigani) in what error cases should we close issuesc?
				// Any other err we keep calm and carry on.
				log.Println("ERROR receiving an issue : %s", err.Error())
				continue
			}

			issuesc <- t.editIssue(issue)
		}
	}(issuesc)

	// next, setup a goroutine to send our files to the service to review.
	go func(filesc <-chan *api.File) {
		for {
			select {
			case file, ok := <-filesc:
				if !ok && file == nil {
					log.Println("client filesc closed. Closing send stream.")
					// Close the file send stream.
					err := stream.CloseSend()
					if err != nil {
						log.Println(err.Error())
					}
					return
				}

				file.Name = t.editFilename(file.Name)
				if err := stream.Send(file); err != nil {
					log.Println("failed to send a file %q: %v", file.Name, err)
				}
				log.Printf("sent file %q\n", file.Name)

				// Each tenet has a 5 second idle time. If we don't find any
				// files to send it in that time, we close this tenet down.
			case <-time.After(5 * time.Second):
				// this will close this instance of the tenet.
				filesTM.Kill(errors.New("timed out waiting for a filename"))

				return
			}
		}
	}(filesc)

	return nil
}
Example #18
0
File: review.go Project: axw/lingo
// returns a chan of tenet reviews and a cancel chan that blocks until the user cancels.
func reviewQueue(ctx *cli.Context, mappings <-chan cfgMap, changed *map[string][]int, errc chan error) (<-chan *tenetReview, chan struct{}) {
	reviews := make(map[string]*tenetReview)
	reviewChannel := make(chan *tenetReview)
	cleanupWG := &sync.WaitGroup{}

	// setup a cancel exit path.
	cancelc := make(chan os.Signal, 1)
	signal.Notify(cancelc, os.Interrupt)
	signal.Notify(cancelc, syscall.SIGTERM)
	cancelledc := make(chan struct{})
	cancelled := func() bool {
		select {
		case _, ok := <-cancelledc:
			if ok {
				close(cancelc)
			}
			return true
		default:
			return false
		}
	}

	// Kill all open tenets on cancel.
	go func() {
		var i int
		for {
			<-cancelc
			if i > 0 {
				// on the second exit, just do it.
				fmt.Print("failed.\nSome docker containers may still be running.")
				os.Exit(1)
			}
			i++
			go func() {
				// TODO(waigani) add progress bar here
				fmt.Print("\ncleaning up tenets ... ")

				// Anything waiting on the cancelled chan will now fire.
				close(cancelledc)

				// Wait for all tenets to be cleaned up.
				cleanupWG.Wait()

				// say bye.
				fmt.Println("done.")
				os.Exit(1)
			}()
		}
	}()

	// TODO(waigani) reenable buffering to:
	// 1. Allow found tenets to keep running.
	// 2. Stop building new tenets until there is room in the buffer.
	// TODO(waigani) make cfg vars
	// buffLimit := 3
	// if ctx.Bool("keep-all") {
	// 	buffLimit = 100
	// }
	// buff := util.NewBuffer(buffLimit, cancelledc)

	go func() {
		for m := range mappings {
			// Glob all the files in the associated directories for this config, and assign to each tenet by hash
			for _, tc := range m.cfg.AllTenets() {

				if cancelled() {
					// empty files and dirs to stop feeding tenet reviews in progress.
					m.files = []string{}
					m.dirs = []string{}
					return
				}

				// Open the tenet service if we haven't seen this config before.
				configHash := tc.hash()
				r, found := reviews[configHash]
				if !found {

					// Don't build a new tenet until there is room in the buffer.
					// Found tenets will keep running until they are not fed files for 5 seconds.
					// WaitRoom will not block if we get a cancel signal.
					// buff.WaitRoom()

					tn, err := newTenet(ctx, tc)
					if err != nil {
						errc <- err
						continue
					}
					// Note: service should not be called outside this if block.
					service, err := tn.OpenService()
					if err != nil {
						errc <- err

						continue
					}

					info, err := service.Info()
					if err != nil {
						errc <- err
						continue
					}

					r = &tenetReview{
						configHash: configHash,
						filesc:     make(chan *api.File),
						issuesc:    make(chan *api.Issue),
						info:       info,
						issuesWG:   &sync.WaitGroup{},
						filesTM:    &tomb.Tomb{},
					}
					reviews[configHash] = r

					// Setup the takedown of this review.
					r.issuesWG.Add(1)
					cleanupWG.Add(1)
					// buff.Add(1)

					go func(r *tenetReview) {
						// The following fires when:
						select {
						// 1. all files have been sent or timed out
						// 2. the tenet buffer is full
						case <-r.filesTM.Dying():
							// 3. lingo has been stopped
						case <-cancelledc:
						}

						// make room for another tenet to start and ensure
						// that any configHash's matching this one will have
						// to start a new tenet instance.
						delete(reviews, configHash)
						// buff.Add(-1)

						// signal to the tenet that no more files are coming.

						close(r.filesc)

						// wait for the tenet to signal to us that it's finished it's review.
						r.issuesWG.Wait()

						// we can now safely close the backing service.
						if err := service.Close(); err != nil {
							log.Println("ERROR closing sevice:", err)
						}

						log.Println("cleanup done")
						cleanupWG.Done()
					}(r)

					// Make sure we're ready to handle results before we start
					// the review.
					reviewChannel <- r

					// Start this tenet's review.
					service.Review(r.filesc, r.issuesc, r.filesTM)
				}

				regexPattern, globPattern := fileExtFilterForLang(r.info.Language)
				for _, d := range m.dirs {
					files, err := filepath.Glob(path.Join(d, globPattern))
					if err != nil {
						// Non-fatal
						log.Printf("Error reading files in %s: %v\n", d, err)
					}

				l:
					for i, f := range files {
						select {
						case <-cancelledc:
							log.Println("user cancelled, dropping files.")
						case <-r.filesTM.Dying():
							dropped := len(files) - i
							log.Print("WARNING a tenet review timed out waiting for files to be sent. %d files dropped", dropped)
							break l
						case r.filesc <- &api.File{Name: f}:
						}
					}
				}
			z:
				for i, f := range m.files {
					if matches, err := regexp.MatchString(regexPattern, f); !matches {
						if err != nil {
							log.Println("error in regex: ", regexPattern)
						}
						continue
					}

					fileinfo := &api.File{Name: f}
					if changed != nil {
						if diffLines, ok := (*changed)[f]; ok { // This can be false if --diff and fileargs are specified
							for _, l := range diffLines {
								fileinfo.Lines = append(fileinfo.Lines, int64(l))
							}
						}
					}
					// TODO: Refactor so as not to have copy/pasted code with above dir handler
					select {
					case <-cancelledc:
						log.Println("user cancelled, dropping files.")
					case <-r.filesTM.Dying():
						dropped := len(m.files) - i
						log.Print("WARNING a tenet review timed out waiting for files to be sent. %d files dropped", dropped)
						break z
					case r.filesc <- fileinfo:
					}
				}
			}
		}

		for _, r := range reviews {

			// this says all files have been sent. For this review.
			r.filesTM.Done()
		}

		// wait for all tenets to be cleaned up.
		cleanupWG.Wait()

		// Closing this chan will start the wind down to end the lingo
		// process.
		close(reviewChannel)
	}()

	return reviewChannel, cancelledc
}
Example #19
0
// confirm returns true if the issue should be kept or false if it should be
// dropped.
func (c IssueConfirmer) Confirm(attempt int, issue *tenet.Issue) bool {
	if c.keepAll {
		return true
	}
	if attempt == 0 {
		fmt.Println(c.FormatPlainText(issue))
	}
	attempt++
	var options string
	fmt.Print("\n[o]pen")
	if c.output {
		fmt.Print(" [d]iscard [K]eep")
	}
	fmt.Print(": ")

	fmt.Scanln(&options)
	// if err != nil || n != 1 {
	// 	// TODO(waigani)  handle invalid input
	// 	fmt.Println("invalid input", n, err)
	// }

	switch options {
	case "o":
		var app string
		defaultEditor := "vi" // TODO(waigani) is vi an okay default?
		if editor != "" {
			defaultEditor = editor
		}
		fmt.Printf("application (%s):", defaultEditor)
		fmt.Scanln(&app)
		filename := issue.Position.Start.Filename
		if app == "" {
			app = defaultEditor
		}
		// c := issue.Position.Start.Column // TODO(waigani) use column
		l := issue.Position.Start.Line
		cmd, err := util.OpenFileCmd(app, filename, l)
		if err != nil {
			fmt.Println(err)
			return c.Confirm(attempt, issue)
		}

		if err = cmd.Start(); err != nil {
			log.Println(err)
		}
		if err = cmd.Wait(); err != nil {
			log.Println(err)
		}

		editor = app

		c.Confirm(attempt, issue)
	case "d":
		issue.Discard = true

		// TODO(waigani) only prompt for reason if we're sending to a service.
		fmt.Print("reason: ")
		in := bufio.NewReader(os.Stdin)
		issue.DiscardReason, _ = in.ReadString('\n')

		// TODO(waigani) we are now always returning true. Returning a bool at
		// all doesn't make sense and KeptIssues in commands/common.go should
		// be renamed to "AllIssues" or the like.
		return true
	case "", "k", "K", "\n":
		return true
	default:
		fmt.Printf("invalid input: %s\n", options)
		fmt.Println(options)
		c.Confirm(attempt, issue)
	}

	return true
}