Skip to content

Commit 0f48c2f

Browse files
committed
vscgo: add a little helper program for vscode-go
github.com/golang/vscode-go/vscgo is a small go program that the extension uses to delegate some tasks. The program will be installed in the extension path and its behavior will be tightly tied with the vscode go extension version. From v0.41.x the extension will use this to record go telemetry counters defined in the extension. The extension spawns a vsccode inc_counters process periodically and passes accumulated counters (key/value pairs) to the program. We plan to expand the functionality in the future, such as tools installation, upgrade, developer survey config check, that are not latency critical and easier to write in Go. For #3023 For #2891 Change-Id: I91427fc6a06dc2781ab21c12e0b8ed5957cba9b1 Reviewed-on: https://go-review.googlesource.com/c/vscode-go/+/549243 TryBot-Result: kokoro <[email protected]> Commit-Queue: Hyang-Ah Hana Kim <[email protected]> Reviewed-by: Alan Donovan <[email protected]>
1 parent db2b4c5 commit 0f48c2f

File tree

5 files changed

+337
-2
lines changed

5 files changed

+337
-2
lines changed

.vscodeignore

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,11 @@
1212
.vscode/
1313
.vscode-test/
1414
SECURITY.md
15+
bin/
1516
build/
1617
codereview.cfg
1718
docs/
18-
go.mod
19-
go.sum
19+
go.*
2020
node_modules/
2121
out/
2222
src/

vscgo/go.mod

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
module github.com/golang/vscode-go/vscgo
2+
3+
go 1.20
4+
5+
require golang.org/x/telemetry v0.0.0-20231127153925-9e8199583d1d
6+
7+
require golang.org/x/sys v0.15.0 // indirect

vscgo/go.sum

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc=
2+
golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
3+
golang.org/x/telemetry v0.0.0-20231127153925-9e8199583d1d h1:ogNR6QNR4EYuxIiZ41ylPQHVyK6iFcGngiOqNHfTc/M=
4+
golang.org/x/telemetry v0.0.0-20231127153925-9e8199583d1d/go.mod h1:2hdX6fx9lgVpcqv/QJBJjwYNa2E/imfPNoTyf/a6vBo=

vscgo/main.go

Lines changed: 231 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,231 @@
1+
// Copyright 2023 The Go Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style
3+
// license that can be found in the LICENSE file.
4+
5+
// The binary vscgo is a helper of the VS Code Go extension.
6+
// The source is distributed with the extension and compiled when
7+
// the extension is first activated.
8+
package main
9+
10+
import (
11+
"bufio"
12+
"flag"
13+
"fmt"
14+
"log"
15+
"os"
16+
"runtime/debug"
17+
"strings"
18+
"time"
19+
20+
"golang.org/x/telemetry/counter"
21+
)
22+
23+
type command struct {
24+
usage string
25+
short string
26+
flags *flag.FlagSet
27+
hasArgs bool
28+
run func(args []string) error
29+
}
30+
31+
func (c command) name() string {
32+
name, _, _ := strings.Cut(c.usage, " ")
33+
return name
34+
}
35+
36+
var allCommands []*command
37+
38+
func init() {
39+
allCommands = []*command{
40+
{
41+
usage: "inc_counters",
42+
short: "increment telemetry counters",
43+
run: runIncCounters,
44+
},
45+
{
46+
usage: "version",
47+
short: "print version information",
48+
run: runVersion,
49+
},
50+
{
51+
usage: "help <command>",
52+
short: "show help for a command",
53+
hasArgs: true,
54+
run: runHelp, // accesses allCommands.
55+
},
56+
}
57+
58+
for _, cmd := range allCommands {
59+
name := cmd.name()
60+
if cmd.flags == nil {
61+
cmd.flags = flag.NewFlagSet(name, flag.ExitOnError)
62+
}
63+
cmd.flags.Usage = func() {
64+
help(name)
65+
}
66+
}
67+
}
68+
69+
func main() {
70+
counter.Open()
71+
log.SetFlags(0)
72+
flag.Usage = usage
73+
flag.Parse()
74+
75+
args := flag.Args()
76+
var cmd *command
77+
if len(args) > 0 {
78+
cmd = findCommand(args[0])
79+
}
80+
if cmd == nil {
81+
flag.Usage()
82+
os.Exit(2)
83+
}
84+
cmd.flags.Parse(args[1:]) // will exit on error
85+
args = cmd.flags.Args()
86+
if !cmd.hasArgs && len(args) > 0 {
87+
help(cmd.name())
88+
failf("\ncommand %q does not accept any arguments.\n", cmd.name())
89+
}
90+
if err := cmd.run(args); err != nil {
91+
failf("%v\n", err)
92+
}
93+
}
94+
95+
func output(msgs ...interface{}) {
96+
fmt.Fprintln(flag.CommandLine.Output(), msgs...)
97+
}
98+
99+
func usage() {
100+
printCommand := func(cmd *command) {
101+
output(fmt.Sprintf("\t%s\t%s", cmd.name(), cmd.short))
102+
}
103+
output("vscgo is a helper tool for the VS Code Go extension, written in Go.")
104+
output()
105+
output("Usage:")
106+
output()
107+
output("\tvscgo <command> [arguments]")
108+
output()
109+
output("The commands are:")
110+
output()
111+
for _, cmd := range allCommands {
112+
printCommand(cmd)
113+
}
114+
output()
115+
output(`Use "vscgo help <command>" for details about any command.`)
116+
output()
117+
}
118+
119+
func failf(format string, args ...any) {
120+
fmt.Fprintf(os.Stderr, format, args...)
121+
os.Exit(1)
122+
}
123+
124+
func findCommand(name string) *command {
125+
for _, cmd := range allCommands {
126+
if cmd.name() == name {
127+
return cmd
128+
}
129+
}
130+
return nil
131+
}
132+
133+
func help(name string) {
134+
cmd := findCommand(name)
135+
if cmd == nil {
136+
failf("unknown command %q\n", name)
137+
}
138+
output(fmt.Sprintf("Usage: vscgo %s", cmd.usage))
139+
output()
140+
output(fmt.Sprintf("%s is used to %s.", cmd.name(), cmd.short))
141+
anyflags := false
142+
cmd.flags.VisitAll(func(*flag.Flag) {
143+
anyflags = true
144+
})
145+
if anyflags {
146+
output()
147+
output("Flags:")
148+
output()
149+
cmd.flags.PrintDefaults()
150+
}
151+
}
152+
153+
// runIncCounters increments telemetry counters read from stdin.
154+
func runIncCounters(_ []string) error {
155+
scanner := bufio.NewScanner(os.Stdin)
156+
return runIncCountersImpl(scanner, counter.Add)
157+
}
158+
159+
const (
160+
incCountersBadInput = "inc_counters_bad_input"
161+
)
162+
163+
func incCountersInputLength(n int) string {
164+
const name = "inc_counters_num_input"
165+
for i := 1; i < 8; i *= 2 {
166+
if n < i {
167+
return fmt.Sprintf("%s:<%d", name, i)
168+
}
169+
}
170+
return name + ":>=8"
171+
}
172+
173+
func incCountersDuration(duration time.Duration) string {
174+
const name = "inc_counters_duration"
175+
switch {
176+
case duration < 10*time.Millisecond:
177+
return name + ":<10ms"
178+
case duration < 100*time.Millisecond:
179+
return name + ":<100ms"
180+
case duration < 1*time.Second:
181+
return name + ":<1s"
182+
case duration < 10*time.Second:
183+
return name + ":<10s"
184+
}
185+
return name + ":>=10s"
186+
}
187+
188+
func runIncCountersImpl(scanner *bufio.Scanner, incCounter func(name string, count int64)) error {
189+
start := time.Now()
190+
linenum := 0
191+
for scanner.Scan() {
192+
line := strings.TrimSpace(scanner.Text())
193+
if line == "" {
194+
continue
195+
}
196+
var name string
197+
var count int64
198+
if _, err := fmt.Sscanf(line, "%s %d", &name, &count); err != nil || count < 0 {
199+
incCounter(incCountersBadInput, 1)
200+
return fmt.Errorf("invalid line: %q", line)
201+
}
202+
linenum++
203+
incCounter(name, int64(count))
204+
}
205+
incCounter(incCountersInputLength(linenum), 1)
206+
incCounter(incCountersDuration(time.Since(start)), 1)
207+
return nil
208+
}
209+
210+
func runVersion(_ []string) error {
211+
info, ok := debug.ReadBuildInfo()
212+
if !ok {
213+
fmt.Println("vscgo: unknown")
214+
fmt.Println("go: unknown")
215+
return nil
216+
}
217+
fmt.Println("vscgo:", info.Main.Version)
218+
fmt.Println("go:", info.GoVersion)
219+
return nil
220+
}
221+
222+
func runHelp(args []string) error {
223+
switch len(args) {
224+
case 1:
225+
help(args[0])
226+
default:
227+
flag.Usage()
228+
failf("too many arguments to \"help\"")
229+
}
230+
return nil
231+
}

vscgo/main_test.go

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
// Copyright 2023 The Go Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style
3+
// license that can be found in the LICENSE file.
4+
5+
// The binary vscgo is a helper of the VS Code Go extension.
6+
// The source is distributed with the extension and compiled when
7+
// the extension is first activated.
8+
package main
9+
10+
import (
11+
"bufio"
12+
"reflect"
13+
"strings"
14+
"testing"
15+
)
16+
17+
func Test_runIncCounters(t *testing.T) {
18+
tests := []struct {
19+
name string
20+
in string
21+
want map[string]int64
22+
wantErr bool
23+
}{
24+
{
25+
name: "empty",
26+
in: "",
27+
want: map[string]int64{},
28+
},
29+
{
30+
name: "single",
31+
in: "foo 7",
32+
want: map[string]int64{"foo": 7},
33+
},
34+
{
35+
name: "single",
36+
in: "\nfoo 7",
37+
want: map[string]int64{"foo": 7},
38+
},
39+
{
40+
name: "multiple",
41+
in: "foo 7\nbar 8\n",
42+
want: map[string]int64{"foo": 7, "bar": 8},
43+
},
44+
{
45+
name: "trim_space_in_name",
46+
in: " foo 1\n bar 3\n",
47+
want: map[string]int64{"foo": 1, "bar": 3},
48+
},
49+
{
50+
name: "nongraphic_char_in_name",
51+
in: "foo\u200b 1\nfoo 3\n",
52+
want: map[string]int64{"foo\u200b": 1, "foo": 3},
53+
},
54+
{
55+
name: "invalid:missing_count",
56+
in: "\nfoo\nbar 1",
57+
want: map[string]int64{incCountersBadInput: 1},
58+
wantErr: true,
59+
},
60+
{
61+
name: "invalid:missing_count2",
62+
in: "foo 1\n1",
63+
want: map[string]int64{"foo": 1, incCountersBadInput: 1},
64+
wantErr: true,
65+
},
66+
{
67+
name: "invalid:negative_count",
68+
in: "foo 2\nbar -1\nbaz 8\n",
69+
want: map[string]int64{"foo": 2, incCountersBadInput: 1},
70+
wantErr: true,
71+
},
72+
}
73+
for _, tt := range tests {
74+
t.Run(tt.name, func(t *testing.T) {
75+
got := map[string]int64{}
76+
incCounter := func(name string, count int64) {
77+
if name != incCountersBadInput && strings.HasPrefix(name, "inc_counters_") {
78+
// ignore our own counters, except the bad input counter.
79+
return
80+
}
81+
got[name] = count
82+
}
83+
err := runIncCountersImpl(bufio.NewScanner(strings.NewReader(tt.in)), incCounter)
84+
if (err != nil) != tt.wantErr {
85+
t.Errorf("runIncCountersImpl(%q) = %v, wantErr=%v", tt.in, err, tt.wantErr)
86+
return
87+
}
88+
if !reflect.DeepEqual(got, tt.want) {
89+
t.Errorf("counters after runIncCountersImpl = %+v, want %+v", got, tt.want)
90+
}
91+
})
92+
}
93+
}

0 commit comments

Comments
 (0)