-
Notifications
You must be signed in to change notification settings - Fork 2
/
server.go
166 lines (142 loc) · 4.17 KB
/
server.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
package adb
import (
"errors"
"fmt"
"os"
"os/exec"
"strings"
"time"
"github.com/prife/goadb/wire"
)
const (
AdbExecutableName = "adb"
// Default port the adb server listens on.
AdbPort = 5037
DialTimeoutDefault = time.Second * 3
)
type ServerConfig struct {
// Dialer used to connect to the adb server.
Dialer
// Path to the adb executable. If empty, the PATH environment variable will be searched.
PathToAdb string
AutoStart bool
DialTimeout time.Duration
// Host and port the adb server is listening on. If not specified, will use the default port on localhost.
Host string
Port int
fs *filesystem
}
// Server knows how to start the adb server and connect to it.
type server interface {
Start() error
Dial() (wire.IConn, error)
}
func roundTripSingleResponse(s server, req string) (resp []byte, err error) {
return roundTripSingleResponseTimeout(s, req, time.Second)
}
func roundTripSingleResponseTimeout(s server, req string, timeout time.Duration) (resp []byte, err error) {
conn, err := s.Dial()
if err != nil {
return
}
defer conn.Close()
if err = conn.SetReadDeadline(time.Now().Add(timeout)); err != nil {
return
}
if resp, err = conn.RoundTripSingleResponse([]byte(req)); err != nil {
return
}
if err = conn.SetReadDeadline(time.Time{}); err != nil {
return
}
return
}
type realServer struct {
config ServerConfig
// Caches Host:Port so they don't have to be concatenated for every dial.
address string
}
func newServer(config ServerConfig) (server, error) {
if config.Dialer == nil {
config.Dialer = tcpDialer{}
}
if config.Host == "" {
config.Host = "127.0.0.1"
}
if config.Port == 0 {
config.Port = AdbPort
}
if config.fs == nil {
config.fs = localFilesystem
}
if config.DialTimeout == 0 {
config.DialTimeout = DialTimeoutDefault
}
if config.PathToAdb == "" {
path, err := config.fs.LookPath(AdbExecutableName)
if err != nil {
return nil, fmt.Errorf("%w: could not find %s in PATH", wire.ErrServerNotAvailable, AdbExecutableName)
}
config.PathToAdb = path
}
if err := config.fs.IsExecutableFile(config.PathToAdb); err != nil {
return nil, fmt.Errorf("%w: invalid adb executable: %s, err: %w", wire.ErrServerNotAvailable, config.PathToAdb, err)
}
return &realServer{
config: config,
address: fmt.Sprintf("%s:%d", config.Host, config.Port),
}, nil
}
// Dial tries to connect to the server. If the first attempt fails, tries starting the server before
// retrying. If the second attempt fails, returns the error.
func (s *realServer) Dial() (wire.IConn, error) {
conn, err := s.config.Dial(s.address, s.config.DialTimeout)
if err != nil {
if !s.config.AutoStart {
return nil, err
}
// Attempt to start the server and try again.
if err = s.Start(); err != nil {
return nil, fmt.Errorf("%w: error starting server for dial, err:%w", wire.ErrServerNotAvailable, err)
}
conn, err = s.config.Dial(s.address, s.config.DialTimeout)
if err != nil {
return nil, err
}
}
return conn, nil
}
// StartServer ensures there is a server running.
func (s *realServer) Start() error {
output, err := s.config.fs.CmdCombinedOutput(s.config.PathToAdb /*"-L", fmt.Sprintf("tcp:%s", s.address),*/, "start-server")
outputStr := strings.TrimSpace(string(output))
if err != nil {
return fmt.Errorf("%w: error starting server: %w\noutput:\n%s", wire.ErrServerNotAvailable, err, outputStr)
}
return nil
}
// filesystem abstracts interactions with the local filesystem for testability.
type filesystem struct {
// Wraps exec.LookPath.
LookPath func(string) (string, error)
// Returns nil if path is a regular file and executable by the current user.
IsExecutableFile func(path string) error
// Wraps exec.Command().CombinedOutput()
CmdCombinedOutput func(name string, arg ...string) ([]byte, error)
}
var localFilesystem = &filesystem{
LookPath: exec.LookPath,
IsExecutableFile: func(path string) error {
info, err := os.Stat(path)
if err != nil {
return err
}
if !info.Mode().IsRegular() {
return errors.New("not a regular file")
}
return isExecutable(path)
},
CmdCombinedOutput: func(name string, arg ...string) ([]byte, error) {
return exec.Command(name, arg...).CombinedOutput()
},
}