fork of https://github.com/sourcegraph/zoekt
1// Copyright 2017 Google Inc. All rights reserved.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7// http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15package ctags
16
17import (
18 "bytes"
19 "fmt"
20 "log"
21 "os"
22 "os/exec"
23 "strings"
24 "sync"
25 "time"
26
27 goctags "github.com/sourcegraph/go-ctags"
28)
29
30const debug = false
31
32type Parser = goctags.Parser
33type Entry = goctags.Entry
34
35type parseReq struct {
36 Name string
37 Content []byte
38}
39
40type parseResp struct {
41 Entries []*Entry
42 Err error
43}
44
45type lockedParser struct {
46 mu sync.Mutex
47 opts goctags.Options
48 p Parser
49 send chan<- parseReq
50 recv <-chan parseResp
51}
52
53// parseTimeout is how long we wait for a response for parsing a single file
54// in ctags. 1 minute is a very conservative timeout which we should only hit
55// if ctags hangs.
56const parseTimeout = time.Minute
57
58// Parse wraps go-ctags Parse. It lazily starts the process and adds a timeout
59// around parse requests. Additionally it serializes access to the parsing
60// process. The timeout is important since we occasionally come across
61// documents which hang universal-ctags.
62func (lp *lockedParser) Parse(name string, content []byte) ([]*Entry, error) {
63 lp.mu.Lock()
64 defer lp.mu.Unlock()
65
66 if lp.p == nil {
67 p, err := goctags.New(lp.opts)
68 if err != nil {
69 return nil, err
70 }
71 send := make(chan parseReq)
72 // buf of 1 so we avoid blocking sends in the parser if we exit early.
73 recv := make(chan parseResp, 1)
74
75 go func() {
76 defer close(recv)
77 for req := range send {
78 entries, err := p.Parse(req.Name, req.Content)
79 recv <- parseResp{Entries: entries, Err: err}
80 }
81 }()
82
83 lp.p = p
84 lp.send = send
85 lp.recv = recv
86 }
87
88 lp.send <- parseReq{Name: name, Content: content}
89
90 deadline := time.NewTimer(parseTimeout)
91 defer deadline.Stop()
92
93 select {
94 case resp := <-lp.recv:
95 return resp.Entries, resp.Err
96 case <-deadline.C:
97 // Error out since ctags hanging is a sign something bad is happening.
98 lp.close()
99 return nil, fmt.Errorf("ctags timedout after %s parsing %s", parseTimeout, name)
100 }
101}
102
103func (lp *lockedParser) Close() {
104 lp.mu.Lock()
105 defer lp.mu.Unlock()
106 lp.close()
107}
108
109// close assumes lp.mu is held.
110func (lp *lockedParser) close() {
111 if lp.p == nil {
112 return
113 }
114
115 lp.p.Close()
116 lp.p = nil
117 close(lp.send)
118 lp.send = nil
119 lp.recv = nil
120}
121
122// NewParser creates a parser that is implemented by the given
123// universal-ctags binary. The parser is safe for concurrent use.
124func NewParser(parserType CTagsParserType, bin string) (Parser, error) {
125 if err := checkBinary(parserType, bin); err != nil {
126 return nil, err
127 }
128
129 opts := goctags.Options{
130 Bin: bin,
131 }
132 if debug {
133 opts.Info = log.New(os.Stderr, "CTAGS INF: ", log.LstdFlags)
134 opts.Debug = log.New(os.Stderr, "CTAGS DBG: ", log.LstdFlags)
135 }
136 return &lockedParser{
137 opts: opts,
138 }, nil
139}
140
141// checkBinary does checks on bin to ensure we can correctly use the binary
142// for symbols. It is more user friendly to fail early in this case.
143func checkBinary(typ CTagsParserType, bin string) error {
144 switch typ {
145 case UniversalCTags:
146 helpOutput, err := exec.Command(bin, "--help").CombinedOutput()
147 if err != nil {
148 return fmt.Errorf("failed to check if %s is universal-ctags: %w\n--help output:\n%s", bin, err, string(helpOutput))
149 }
150 if !bytes.Contains(helpOutput, []byte("+interactive")) {
151 return fmt.Errorf("ctags binary is not universal-ctags or is not compiled with +interactive feature: bin=%s", bin)
152 }
153
154 case ScipCTags:
155 if !strings.Contains(bin, "scip-ctags") {
156 return fmt.Errorf("only supports scip-ctags, not %s", bin)
157 }
158 }
159
160 return nil
161}