Skip to content

Commit 281b79f

Browse files
xaionaro@dx.centerxaionaro@dx.center
authored andcommitted
feat: split bindercli commands into 323 independent Go packages
The 691K-line commands_gen.go in package main was killing CI build times (>31min timeout). Restructured into 323 auto-generated subpackages under cmd/bindercli/gen/ that compile independently: - cmd/bindercli/cliutil/ — shared Conn, Formatter, service discovery - cmd/bindercli/gen/<aidl_pkg>/commands.go — per-package Register() - cmd/bindercli/register_gen.go — imports and registers all packages Package main: 22.5K lines (down from 691K). Largest subpackage: android_app at 46K lines. Incremental builds <1s (Go build cache caches each package independently).
1 parent dd950ed commit 281b79f

334 files changed

Lines changed: 699803 additions & 691668 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/ci.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ jobs:
5252
needs: test
5353
if: startsWith(github.ref, 'refs/tags/v')
5454
runs-on: ubuntu-latest
55+
timeout-minutes: 45
5556
permissions:
5657
contents: write
5758
packages: write

Makefile

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,3 +156,5 @@ clean:
156156
find specs -name spec.yaml -delete 2>/dev/null; find specs -type d -empty -delete 2>/dev/null; true
157157
find . -maxdepth 1 -name '*.go' -exec grep -l 'Code generated' {} \; | xargs -r rm -f
158158
rm -f servicemanager/service_names_gen.go
159+
rm -f cmd/bindercli/commands_gen.go cmd/bindercli/commands_gen_*.go cmd/bindercli/registry_gen.go cmd/bindercli/register_gen.go
160+
rm -rf cmd/bindercli/gen/

cmd/bindercli/cliutil/conn.go

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
//go:build linux
2+
3+
package cliutil
4+
5+
import (
6+
"context"
7+
"fmt"
8+
9+
"github.com/facebookincubator/go-belt/tool/logger"
10+
"github.com/spf13/cobra"
11+
12+
"github.com/AndroidGoLab/binder/binder"
13+
"github.com/AndroidGoLab/binder/binder/versionaware"
14+
"github.com/AndroidGoLab/binder/kernelbinder"
15+
"github.com/AndroidGoLab/binder/servicemanager"
16+
)
17+
18+
// Conn wraps the binder driver and service manager into a single
19+
// connection handle for CLI subcommands.
20+
type Conn struct {
21+
Driver *kernelbinder.Driver
22+
Transport binder.Transport
23+
SM *servicemanager.ServiceManager
24+
}
25+
26+
// OpenConn opens a binder driver connection and creates a service manager client.
27+
func OpenConn(
28+
ctx context.Context,
29+
cmd *cobra.Command,
30+
) (_conn *Conn, _err error) {
31+
logger.Tracef(ctx, "OpenConn")
32+
defer func() { logger.Tracef(ctx, "/OpenConn: %v", _err) }()
33+
34+
mapSize, err := cmd.Root().PersistentFlags().GetInt("map-size")
35+
if err != nil {
36+
return nil, fmt.Errorf("reading --map-size flag: %w", err)
37+
}
38+
39+
targetAPI, err := cmd.Root().PersistentFlags().GetInt("target-api")
40+
if err != nil {
41+
return nil, fmt.Errorf("reading --target-api flag: %w", err)
42+
}
43+
44+
// Detect API level BEFORE opening /dev/binder. Detection may fork
45+
// a child process (getprop), and forking after mmap-ing the binder
46+
// driver corrupts its state.
47+
if targetAPI <= 0 {
48+
targetAPI = versionaware.DetectAPILevel()
49+
}
50+
51+
driver, err := kernelbinder.Open(ctx, binder.WithMapSize(uint32(mapSize)))
52+
if err != nil {
53+
return nil, fmt.Errorf("opening binder driver: %w", err)
54+
}
55+
56+
// Wrap with version-aware transport so that ResolveCode returns
57+
// the correct transaction codes for the target device's API level.
58+
transport, err := versionaware.NewTransport(ctx, driver, targetAPI)
59+
if err != nil {
60+
driver.Close(ctx)
61+
return nil, fmt.Errorf("initializing version-aware transport: %w", err)
62+
}
63+
64+
sm := servicemanager.New(transport)
65+
66+
return &Conn{
67+
Driver: driver,
68+
Transport: transport,
69+
SM: sm,
70+
}, nil
71+
}
72+
73+
// Close releases the binder driver resources.
74+
func (c *Conn) Close(
75+
ctx context.Context,
76+
) (_err error) {
77+
logger.Tracef(ctx, "Conn.Close")
78+
defer func() { logger.Tracef(ctx, "/Conn.Close: %v", _err) }()
79+
80+
return c.Driver.Close(ctx)
81+
}
82+
83+
// GetService looks up a registered binder service by name.
84+
// Returns an error if the service is not found.
85+
func (c *Conn) GetService(
86+
ctx context.Context,
87+
name string,
88+
) (_binder binder.IBinder, _err error) {
89+
logger.Tracef(ctx, "GetService(%q)", name)
90+
defer func() { logger.Tracef(ctx, "/GetService(%q): %v", name, _err) }()
91+
92+
svc, err := c.SM.CheckService(ctx, servicemanager.ServiceName(name))
93+
if err != nil {
94+
return nil, fmt.Errorf("checking service %q: %w", name, err)
95+
}
96+
97+
if svc == nil {
98+
return nil, fmt.Errorf("service %q not found", name)
99+
}
100+
101+
return svc, nil
102+
}

cmd/bindercli/cliutil/formatter.go

Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
package cliutil
2+
3+
import (
4+
"encoding/json"
5+
"fmt"
6+
"io"
7+
"os"
8+
"sort"
9+
"text/tabwriter"
10+
11+
"golang.org/x/term"
12+
)
13+
14+
// Formatter handles text and JSON output with auto-detection of terminal vs pipe.
15+
type Formatter struct {
16+
Mode string
17+
W io.Writer
18+
}
19+
20+
// NewFormatter creates a Formatter that resolves "auto" mode by checking
21+
// whether the writer is a terminal.
22+
func NewFormatter(
23+
mode string,
24+
w io.Writer,
25+
) *Formatter {
26+
var isTTY bool
27+
if f, ok := w.(interface{ Fd() uintptr }); ok {
28+
isTTY = term.IsTerminal(int(f.Fd()))
29+
} else {
30+
isTTY = term.IsTerminal(int(os.Stdout.Fd()))
31+
}
32+
return &Formatter{
33+
Mode: ResolveMode(mode, isTTY),
34+
W: w,
35+
}
36+
}
37+
38+
// ResolveMode maps the mode string to a concrete output format.
39+
// When mode is "auto", it returns "text" for terminals and "json" for pipes.
40+
func ResolveMode(
41+
mode string,
42+
isTTY bool,
43+
) string {
44+
switch mode {
45+
case "text", "json":
46+
return mode
47+
default:
48+
if isTTY {
49+
return "text"
50+
}
51+
return "json"
52+
}
53+
}
54+
55+
// Value writes a single key-value pair.
56+
// Text: "key: val\n". JSON: {"key": val}.
57+
func (f *Formatter) Value(
58+
key string,
59+
val any,
60+
) {
61+
switch f.Mode {
62+
case "json":
63+
f.WriteJSON(map[string]any{key: val})
64+
default:
65+
fmt.Fprintf(f.W, "%s: %v\n", key, val)
66+
}
67+
}
68+
69+
// Result writes a map of key-value pairs.
70+
// Text: sorted k/v lines. JSON: object.
71+
func (f *Formatter) Result(
72+
m map[string]any,
73+
) {
74+
switch f.Mode {
75+
case "json":
76+
f.WriteJSON(m)
77+
default:
78+
keys := make([]string, 0, len(m))
79+
for k := range m {
80+
keys = append(keys, k)
81+
}
82+
sort.Strings(keys)
83+
for _, k := range keys {
84+
fmt.Fprintf(f.W, "%s: %v\n", k, m[k])
85+
}
86+
}
87+
}
88+
89+
// Table writes tabular data.
90+
// Text: aligned columns via tabwriter. JSON: array of objects keyed by headers.
91+
func (f *Formatter) Table(
92+
headers []string,
93+
rows [][]string,
94+
) {
95+
switch f.Mode {
96+
case "json":
97+
objects := make([]map[string]string, 0, len(rows))
98+
for _, row := range rows {
99+
obj := make(map[string]string, len(headers))
100+
for i, h := range headers {
101+
if i < len(row) {
102+
obj[h] = row[i]
103+
}
104+
}
105+
objects = append(objects, obj)
106+
}
107+
f.WriteJSON(objects)
108+
default:
109+
tw := tabwriter.NewWriter(f.W, 0, 4, 2, ' ', 0)
110+
for i, h := range headers {
111+
if i > 0 {
112+
fmt.Fprint(tw, "\t")
113+
}
114+
fmt.Fprint(tw, h)
115+
}
116+
fmt.Fprintln(tw)
117+
for _, row := range rows {
118+
for i, cell := range row {
119+
if i > 0 {
120+
fmt.Fprint(tw, "\t")
121+
}
122+
fmt.Fprint(tw, cell)
123+
}
124+
fmt.Fprintln(tw)
125+
}
126+
tw.Flush()
127+
}
128+
}
129+
130+
// Error writes an error message.
131+
// Text: "error: msg\n". JSON: {"error": "msg"}.
132+
func (f *Formatter) Error(
133+
err error,
134+
) {
135+
switch f.Mode {
136+
case "json":
137+
f.WriteJSON(map[string]string{"error": err.Error()})
138+
default:
139+
fmt.Fprintf(f.W, "error: %s\n", err.Error())
140+
}
141+
}
142+
143+
// WriteJSON encodes v as JSON to the formatter's writer.
144+
func (f *Formatter) WriteJSON(
145+
v any,
146+
) {
147+
enc := json.NewEncoder(f.W)
148+
enc.SetEscapeHTML(false)
149+
_ = enc.Encode(v)
150+
}
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
//go:build linux
2+
3+
package cliutil
4+
5+
import (
6+
"context"
7+
"fmt"
8+
9+
"github.com/AndroidGoLab/binder/binder"
10+
"github.com/AndroidGoLab/binder/parcel"
11+
"github.com/AndroidGoLab/binder/servicemanager"
12+
)
13+
14+
// KnownServiceNames maps AIDL descriptors to well-known Android
15+
// ServiceManager names, allowing fast lookup without enumeration.
16+
// Populated by generated code via SetKnownServiceNames.
17+
var KnownServiceNames map[string]string
18+
19+
// FindServiceByDescriptor locates a binder service by its AIDL descriptor.
20+
// It first tries the static map of well-known service names to avoid
21+
// slow enumeration, then falls back to listing all services.
22+
func FindServiceByDescriptor(
23+
ctx context.Context,
24+
conn *Conn,
25+
descriptor string,
26+
) (binder.IBinder, error) {
27+
// Try the static map of well-known service names first to avoid
28+
// slow enumeration of all registered services.
29+
if name, ok := KnownServiceNames[descriptor]; ok {
30+
svc, err := conn.SM.CheckService(ctx, servicemanager.ServiceName(name))
31+
if err == nil && svc != nil {
32+
return svc, nil
33+
}
34+
}
35+
36+
// Fall back to enumeration.
37+
services, err := conn.SM.ListServices(ctx)
38+
if err != nil {
39+
return nil, fmt.Errorf("listing services: %w", err)
40+
}
41+
42+
for _, name := range services {
43+
svc, err := conn.SM.CheckService(ctx, name)
44+
if err != nil || svc == nil {
45+
continue
46+
}
47+
desc := QueryDescriptor(ctx, svc)
48+
if desc == descriptor {
49+
return svc, nil
50+
}
51+
}
52+
53+
return nil, fmt.Errorf("no service with descriptor %q found", descriptor)
54+
}
55+
56+
// QueryDescriptor sends an InterfaceTransaction to the binder service
57+
// and reads back the interface descriptor string.
58+
// Returns "(unknown)" if the query fails.
59+
func QueryDescriptor(
60+
ctx context.Context,
61+
svc binder.IBinder,
62+
) string {
63+
reply, err := svc.Transact(ctx, binder.InterfaceTransaction, 0, parcel.New())
64+
if err != nil {
65+
return "(unknown)"
66+
}
67+
68+
desc, err := reply.ReadString16()
69+
if err != nil {
70+
return "(unknown)"
71+
}
72+
73+
return desc
74+
}

0 commit comments

Comments
 (0)