github.com/olivere/camlistore@v0.0.0-20140121221811-1b7ac2da0199/pkg/client/android/android.go (about) 1 /* 2 Copyright 2013 The Camlistore Authors 3 4 Licensed under the Apache License, Version 2.0 (the "License"); 5 you may not use this file except in compliance with the License. 6 You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10 Unless required by applicable law or agreed to in writing, software 11 distributed under the License is distributed on an "AS IS" BASIS, 12 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 See the License for the specific language governing permissions and 14 limitations under the License. 15 */ 16 17 // Package android contains code specific to running the Camlistore client 18 // code as a child process on Android. This removes ugly API from the 19 // client package itself. 20 package android 21 22 import ( 23 "bufio" 24 "crypto/tls" 25 "crypto/x509" 26 "fmt" 27 "io" 28 "io/ioutil" 29 "log" 30 "net" 31 "net/http" 32 "os" 33 "os/exec" 34 "path/filepath" 35 "regexp" 36 "strconv" 37 "sync" 38 39 "camlistore.org/pkg/blob" 40 "camlistore.org/pkg/blobserver" 41 "camlistore.org/pkg/schema" 42 ) 43 44 // TODO(mpl): distinguish CAMPUT, CAMGET, etc 45 var androidOutput, _ = strconv.ParseBool(os.Getenv("CAMPUT_ANDROID_OUTPUT")) 46 47 // IsChild reports whether this process is running as an Android 48 // child process and should report its output in the form that the 49 // Android uploader app expects. 50 func IsChild() bool { 51 return androidOutput 52 } 53 54 var androidOutMu sync.Mutex 55 56 func Printf(format string, args ...interface{}) { 57 androidOutMu.Lock() 58 defer androidOutMu.Unlock() 59 fmt.Printf(format, args...) 60 } 61 62 var detectOnce sync.Once 63 var onAndroidCache bool 64 65 func dirExists(f string) bool { 66 fi, err := os.Stat(f) 67 return err == nil && fi.IsDir() 68 } 69 70 func initOnAndroid() { 71 // Good enough heuristic. Suggestions welcome. 72 onAndroidCache = dirExists("/data/data") && dirExists("/system/etc") 73 } 74 75 func OnAndroid() bool { 76 detectOnce.Do(initOnAndroid) 77 return onAndroidCache 78 } 79 80 var pingRx = regexp.MustCompile(`\((.+?)\)`) 81 82 type namedInt struct { 83 name string 84 sync.Mutex 85 val int64 86 } 87 88 func (ni *namedInt) Incr(delta int64) { 89 ni.Lock() 90 ni.val += delta 91 nv := ni.val 92 ni.Unlock() 93 Printf("STAT %s %d\n", ni.name, nv) 94 } 95 96 var ( 97 statDNSStart = &namedInt{name: "dns_start"} 98 statDNSDone = &namedInt{name: "dns_done"} 99 statTCPStart = &namedInt{name: "tcp_start"} 100 statTCPStarted = &namedInt{name: "tcp_started"} 101 statTCPFail = &namedInt{name: "tcp_fail"} 102 statTCPDone = &namedInt{name: "tcp_done"} 103 statTCPWrites = &namedInt{name: "tcp_write_byte"} 104 statTCPWrote = &namedInt{name: "tcp_wrote_byte"} 105 statTCPReads = &namedInt{name: "tcp_read_byte"} 106 statHTTPStart = &namedInt{name: "http_start"} 107 statHTTPResHeaders = &namedInt{name: "http_res_headers"} 108 statBlobUploaded = &namedInt{name: "blob_uploaded"} 109 statBlobExisted = &namedInt{name: "blob_existed"} 110 statFileUploaded = &namedInt{name: "file_uploaded"} 111 statFileExisted = &namedInt{name: "file_existed"} 112 ) 113 114 type statTrackingConn struct { 115 net.Conn 116 } 117 118 func (c statTrackingConn) Write(p []byte) (n int, err error) { 119 statTCPWrites.Incr(int64(len(p))) 120 n, err = c.Conn.Write(p) 121 statTCPWrote.Incr(int64(n)) 122 return 123 } 124 125 func (c statTrackingConn) Read(p []byte) (n int, err error) { 126 n, err = c.Conn.Read(p) 127 statTCPReads.Incr(int64(n)) 128 return 129 } 130 131 var ( 132 dnsMu sync.Mutex 133 dnsCache = make(map[string]string) 134 ) 135 136 func androidLookupHost(host string) string { 137 dnsMu.Lock() 138 v, ok := dnsCache[host] 139 dnsMu.Unlock() 140 if ok { 141 return v 142 } 143 statDNSStart.Incr(1) 144 defer statDNSDone.Incr(1) 145 146 // Android has no "dig" or "host" tool, so use "ping -c 1". Ghetto. 147 // $ ping -c 1 google.com 148 // PING google.com (74.125.224.64) 56(84) bytes of data. 149 c := make(chan string, 1) 150 go func() { 151 cmd := exec.Command("/system/bin/ping", "-c", "1", host) 152 stdout, err := cmd.StdoutPipe() 153 if err != nil { 154 panic(err) 155 } 156 defer stdout.Close() 157 if err := cmd.Start(); err != nil { 158 log.Printf("Error resolving %q with ping: %v", host, err) 159 c <- host 160 return 161 } 162 defer func() { 163 if p := cmd.Process; p != nil { 164 p.Kill() 165 } 166 }() 167 br := bufio.NewReader(stdout) 168 line, err := br.ReadString('\n') 169 if err != nil { 170 log.Printf("Failed to resolve %q with ping", host) 171 c <- host 172 return 173 } 174 if m := pingRx.FindStringSubmatch(line); m != nil { 175 ip := m[1] 176 dnsMu.Lock() 177 dnsCache[host] = ip 178 dnsMu.Unlock() 179 c <- ip 180 return 181 } 182 log.Printf("Failed to resolve %q with ping", host) 183 c <- host 184 }() 185 return <-c 186 return v 187 } 188 189 type StatsTransport struct { 190 Rt http.RoundTripper 191 } 192 193 func (t StatsTransport) RoundTrip(req *http.Request) (res *http.Response, err error) { 194 statHTTPStart.Incr(1) 195 res, err = t.Rt.RoundTrip(req) 196 statHTTPResHeaders.Incr(1) 197 // TODO: track per-response code stats, and also track when body done being read. 198 return 199 } 200 201 func Dial(network, addr string) (net.Conn, error) { 202 // Temporary laziness hack, avoiding doing a 203 // cross-compiled Android cgo build. 204 // Without cgo, package net uses 205 // /etc/resolv.conf (not available on 206 // Android). We really want a cgo binary to 207 // use Android's DNS cache, but it's kinda 208 // hard/impossible to cross-compile for now. 209 statTCPStart.Incr(1) 210 host, port, err := net.SplitHostPort(addr) 211 if err != nil { 212 statTCPFail.Incr(1) 213 return nil, fmt.Errorf("couldn't split %q", addr) 214 } 215 c, err := net.Dial(network, net.JoinHostPort(androidLookupHost(host), port)) 216 if err != nil { 217 statTCPFail.Incr(1) 218 return nil, err 219 } 220 statTCPStarted.Incr(1) 221 return statTrackingConn{Conn: c}, err 222 } 223 224 func TLSConfig() (*tls.Config, error) { 225 if !OnAndroid() { 226 return nil, nil 227 } 228 certDir := "/system/etc/security/cacerts" 229 fi, err := os.Stat(certDir) 230 if err != nil { 231 return nil, err 232 } 233 if !fi.IsDir() { 234 return nil, fmt.Errorf("%q not a dir", certDir) 235 } 236 pool := x509.NewCertPool() 237 cfg := &tls.Config{RootCAs: pool} 238 239 f, err := os.Open(certDir) 240 if err != nil { 241 return nil, err 242 } 243 defer f.Close() 244 names, _ := f.Readdirnames(-1) 245 for _, name := range names { 246 pem, err := ioutil.ReadFile(filepath.Join(certDir, name)) 247 if err != nil { 248 return nil, err 249 } 250 pool.AppendCertsFromPEM(pem) 251 } 252 return cfg, nil 253 } 254 255 // NoteFileUploaded is a hook for camput to report that a file 256 // was uploaded. TODO: move this to pkg/client/android probably. 257 func NoteFileUploaded(fullPath string, uploaded bool) { 258 if !IsChild() { 259 return 260 } 261 if uploaded { 262 statFileUploaded.Incr(1) 263 } else { 264 statFileExisted.Incr(1) 265 } 266 Printf("FILE_UPLOADED %s\n", fullPath) 267 } 268 269 // androidStatusReceiver is a blobserver.StatReceiver wrapper that 270 // reports the full filename path and size of uploaded blobs. 271 // The android app wrapping camput watches stdout for this, for progress bars. 272 type StatusReceiver struct { 273 Sr blobserver.StatReceiver 274 Path string 275 } 276 277 func (asr StatusReceiver) noteChunkOnServer(sb blob.SizedRef) { 278 Printf("CHUNK_UPLOADED %d %s %s\n", sb.Size, sb.Ref, asr.Path) 279 } 280 281 func (asr StatusReceiver) ReceiveBlob(blob blob.Ref, source io.Reader) (blob.SizedRef, error) { 282 // Sniff the first 1KB of it and don't print the stats if it looks like it was just a schema 283 // blob. We won't update the progress bar for that yet. 284 var buf [1024]byte 285 contents := buf[:0] 286 sb, err := asr.Sr.ReceiveBlob(blob, io.TeeReader(source, writeUntilSliceFull{&contents})) 287 if err == nil && !schema.LikelySchemaBlob(contents) { 288 statBlobUploaded.Incr(1) 289 asr.noteChunkOnServer(sb) 290 } 291 return sb, err 292 } 293 294 func (asr StatusReceiver) StatBlobs(dest chan<- blob.SizedRef, blobs []blob.Ref) error { 295 midc := make(chan blob.SizedRef) 296 errc := make(chan error, 1) 297 go func() { 298 err := asr.Sr.StatBlobs(midc, blobs) 299 errc <- err 300 close(midc) 301 }() 302 for sb := range midc { 303 asr.noteChunkOnServer(sb) 304 statBlobExisted.Incr(1) 305 dest <- sb 306 } 307 return <-errc 308 } 309 310 type writeUntilSliceFull struct { 311 s *[]byte 312 } 313 314 func (w writeUntilSliceFull) Write(p []byte) (n int, err error) { 315 s := *w.s 316 l := len(s) 317 growBy := cap(s) - l 318 if growBy > len(p) { 319 growBy = len(p) 320 } 321 s = s[0 : l+growBy] 322 copy(s[l:], p) 323 *w.s = s 324 return len(p), nil 325 }