github.com/olivere/camlistore@v0.0.0-20140121221811-1b7ac2da0199/clients/android/src/org/camlistore/UploadThread.java (about) 1 /* 2 Copyright 2011 Google Inc. 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 org.camlistore; 18 19 import java.io.BufferedReader; 20 import java.io.BufferedWriter; 21 import java.io.IOException; 22 import java.io.InputStream; 23 import java.io.InputStreamReader; 24 import java.io.OutputStream; 25 import java.io.OutputStreamWriter; 26 import java.util.HashMap; 27 import java.util.ListIterator; 28 import java.util.concurrent.LinkedBlockingQueue; 29 import java.util.concurrent.TimeUnit; 30 import java.util.concurrent.atomic.AtomicReference; 31 import java.util.regex.Matcher; 32 import java.util.regex.Pattern; 33 34 import android.util.Log; 35 36 public class UploadThread extends Thread { 37 private static final String TAG = "UploadThread"; 38 39 private final UploadService mService; 40 private final HostPort mHostPort; 41 private final String mTrustedCert; 42 private final String mUsername; 43 private final String mPassword; 44 private final LinkedBlockingQueue<UploadThreadMessage> msgCh = new LinkedBlockingQueue<UploadThreadMessage>(); 45 46 AtomicReference<Process> goProcess = new AtomicReference<Process>(); 47 AtomicReference<OutputStream> toChildRef = new AtomicReference<OutputStream>(); 48 HashMap<String, QueuedFile> mQueuedFile = new HashMap<String, QueuedFile>(); // guarded 49 // by 50 // itself 51 52 private final Object stdinLock = new Object(); // guards setting and writing 53 // to stdinWriter 54 private BufferedWriter stdinWriter; 55 56 public UploadThread(UploadService uploadService, HostPort hp, String trustedCert, String username, String password) { 57 mService = uploadService; 58 mHostPort = hp; 59 mTrustedCert = trustedCert != null ? trustedCert.toLowerCase().trim() : ""; 60 mUsername = username; 61 mPassword = password; 62 } 63 64 public void stopUploads() { 65 Process p = goProcess.get(); 66 if (p == null) { 67 return; 68 } 69 synchronized (stdinLock) { 70 if (stdinWriter == null) { 71 // force kill. confused. 72 p.destroy(); 73 goProcess.set(null); 74 return; 75 } 76 try { 77 stdinWriter.close(); 78 Log.d(TAG, "Closed camput's stdin"); 79 stdinWriter = null; 80 } catch (IOException e) { 81 p.destroy(); // force kill 82 goProcess.set(null); 83 return; 84 } 85 86 // Unnecessary paranoia, never seen in practice: 87 new Thread() { 88 @Override 89 public void run() { 90 try { 91 Thread.sleep(750, 0); 92 stopUploads(); // force kill if still alive. 93 } catch (InterruptedException e) { 94 } 95 96 } 97 }.start(); 98 } 99 } 100 101 private String binaryPath(String suffix) { 102 return mService.getBaseContext().getFilesDir().getAbsolutePath() + "/" + suffix; 103 } 104 105 private void status(String st) { 106 Log.d(TAG, st); 107 mService.setUploadStatusText(st); 108 } 109 110 // An UploadThreadMessage can be sent on msgCh and read by the run() method 111 // in 112 // until it's time to quit the thread. 113 private static class UploadThreadMessage { 114 } 115 116 private static class ProcessExitedMessage extends UploadThreadMessage { 117 public int code; 118 119 public ProcessExitedMessage(int code) { 120 this.code = code; 121 } 122 } 123 124 public boolean enqueueFile(QueuedFile qf) { 125 String diskPath = qf.getDiskPath(); 126 if (diskPath == null) { 127 Log.d(TAG, "file has no disk path: " + qf); 128 return false; 129 } 130 synchronized (stdinLock) { 131 if (stdinWriter == null) { 132 return false; 133 } 134 synchronized (mQueuedFile) { 135 mQueuedFile.put(diskPath, qf); 136 } 137 try { 138 stdinWriter.write(diskPath + "\n"); 139 stdinWriter.flush(); 140 } catch (IOException e) { 141 Log.d(TAG, "Failed to write " + diskPath + " to camput stdin: " + e); 142 return false; 143 } 144 } 145 return true; 146 } 147 148 @Override 149 public void run() { 150 Log.d(TAG, "Running"); 151 if (!mHostPort.isValid()) { 152 Log.d(TAG, "host/port is invalid"); 153 return; 154 } 155 status("Running UploadThread for " + mHostPort); 156 157 mService.onStatReceived(null, 0); 158 159 Process process = null; 160 try { 161 ProcessBuilder pb = new ProcessBuilder(); 162 pb.command(binaryPath("camput.bin"), "--server=" + mHostPort.urlPrefix(), "file", "-stdinargs", "-vivify"); 163 pb.redirectErrorStream(false); 164 pb.environment().put("CAMLI_AUTH", "userpass:" + mUsername + ":" + mPassword); 165 pb.environment().put("CAMLI_TRUSTED_CERT", mTrustedCert); 166 pb.environment().put("CAMLI_CACHE_DIR", mService.getCacheDir().getAbsolutePath()); 167 pb.environment().put("CAMPUT_ANDROID_OUTPUT", "1"); 168 process = pb.start(); 169 goProcess.set(process); 170 synchronized (stdinLock) { 171 stdinWriter = new BufferedWriter(new OutputStreamWriter(process.getOutputStream(), "UTF-8")); 172 } 173 new CopyToAndroidLogThread("stderr", process.getErrorStream()).start(); 174 new ParseCamputOutputThread(process, mService).start(); 175 new WaitForProcessThread(process).start(); 176 } catch (IOException e) { 177 throw new RuntimeException(e); 178 } 179 180 ListIterator<QueuedFile> iter = mService.uploadQueue().listIterator(); 181 while (iter.hasNext()) { 182 enqueueFile(iter.next()); 183 } 184 185 // Loop forever reading from msgCh 186 while (true) { 187 UploadThreadMessage msg = null; 188 try { 189 msg = msgCh.poll(10, TimeUnit.SECONDS); 190 } catch (InterruptedException e) { 191 continue; 192 } 193 if (msg instanceof ProcessExitedMessage) { 194 status("Upload process ended."); 195 ProcessExitedMessage pem = (ProcessExitedMessage) msg; 196 Log.d(TAG, "Loop exiting; code was = " + pem.code); 197 return; 198 } 199 } 200 } 201 202 // "CHUNK_UPLOADED %d %s %s\n", sb.Size, blob, asr.path 203 private final static Pattern chunkUploadedPattern = Pattern.compile("^CHUNK_UPLOADED (\\d+) (\\S+) (.+)"); 204 205 public class CamputChunkUploadedMessage { 206 private final String mFilename; 207 private final long mSize; 208 209 public CamputChunkUploadedMessage(String line) { 210 Matcher m = chunkUploadedPattern.matcher(line); 211 if (!m.matches()) { 212 throw new RuntimeException("bogus CamputChunkMessage: " + line); 213 } 214 mSize = Long.parseLong(m.group(1)); 215 mFilename = m.group(3); 216 } 217 218 public QueuedFile queuedFile() { 219 synchronized (mQueuedFile) { 220 return mQueuedFile.get(mFilename); 221 } 222 } 223 224 public long size() { 225 return mSize; 226 } 227 } 228 229 // STAT %s %d\n 230 private final static Pattern statPattern = Pattern.compile("^STAT (\\S+) (\\d+)\\b"); 231 232 public class CamputStatMessage { 233 private final Matcher mm; 234 235 public CamputStatMessage(String line) { 236 mm = statPattern.matcher(line); 237 if (!mm.matches()) { 238 throw new RuntimeException("bogus CamputStatMessage: " + line); 239 } 240 } 241 242 public String name() { 243 return mm.group(1); 244 } 245 246 public long value() { 247 return Long.parseLong(mm.group(2)); 248 } 249 } 250 251 // STATS nfile=%d nbyte=%d skfile=%d skbyte=%d upfile=%d upbyte=%d\n 252 private final static Pattern statsPattern = Pattern.compile("^STATS nfile=(\\d+) nbyte=(\\d+) skfile=(\\d+) skbyte=(\\d+) upfile=(\\d+) upbyte=(\\d+)"); 253 254 public class CamputStatsMessage { 255 private final Matcher mm; 256 257 public CamputStatsMessage(String line) { 258 mm = statsPattern.matcher(line); 259 if (!mm.matches()) { 260 throw new RuntimeException("bogus CamputStatsMessage: " + line); 261 } 262 } 263 264 private long field(int n) { 265 return Long.parseLong(mm.group(n)); 266 } 267 268 public long totalFiles() { 269 return field(1); 270 } 271 272 public long totalBytes() { 273 return field(2); 274 } 275 276 public long skippedFiles() { 277 return field(3); 278 } 279 280 public long skippedBytes() { 281 return field(4); 282 } 283 284 public long uploadedFiles() { 285 return field(5); 286 } 287 288 public long uploadedBytes() { 289 return field(6); 290 } 291 } 292 293 private class ParseCamputOutputThread extends Thread { 294 private final BufferedReader mBufIn; 295 private final UploadService mService; 296 private final static String TAG = UploadThread.TAG + "/camput-out"; 297 private final static boolean DEBUG_CAMPUT_ACTIVITY = false; 298 299 public ParseCamputOutputThread(Process process, UploadService service) { 300 mService = service; 301 mBufIn = new BufferedReader(new InputStreamReader(process.getInputStream())); 302 } 303 304 @Override 305 public void run() { 306 while (true) { 307 String line = null; 308 try { 309 line = mBufIn.readLine(); 310 } catch (IOException e) { 311 Log.d(TAG, "Exception reading camput's stdout: " + e.toString()); 312 return; 313 } 314 if (DEBUG_CAMPUT_ACTIVITY) { 315 Log.d(TAG, "camput: " + line); 316 } 317 if (line == null) { 318 // EOF 319 return; 320 } 321 if (line.startsWith("CHUNK_UPLOADED ")) { 322 CamputChunkUploadedMessage msg = new CamputChunkUploadedMessage(line); 323 mService.onChunkUploaded(msg); 324 continue; 325 } 326 if (line.startsWith("STATS ")) { 327 CamputStatsMessage msg = new CamputStatsMessage(line); 328 mService.onStatsReceived(msg); 329 continue; 330 } 331 if (line.startsWith("STAT ")) { 332 CamputStatMessage msg = new CamputStatMessage(line); 333 mService.onStatReceived(msg.name(), msg.value()); 334 continue; 335 } 336 if (line.startsWith("FILE_UPLOADED ")) { 337 String filename = line.substring(14).trim(); 338 QueuedFile qf = null; 339 synchronized (mQueuedFile) { 340 qf = mQueuedFile.get(filename); 341 if (qf != null) { 342 mQueuedFile.remove(filename); 343 } 344 } 345 if (qf != null) { 346 mService.onUploadComplete(qf); 347 } 348 continue; 349 } 350 Log.d(TAG, "camput said unknown line: " + line); 351 } 352 353 } 354 } 355 356 private class WaitForProcessThread extends Thread { 357 private final Process mProcess; 358 359 public WaitForProcessThread(Process p) { 360 mProcess = p; 361 } 362 363 @Override 364 public void run() { 365 Log.d(TAG, "Waiting for camput process."); 366 try { 367 mProcess.waitFor(); 368 } catch (InterruptedException e) { 369 Log.d(TAG, "Interrupted waiting for camput"); 370 msgCh.offer(new ProcessExitedMessage(-1)); 371 return; 372 } 373 Log.d(TAG, "Exit status of camput = " + mProcess.exitValue()); 374 msgCh.offer(new ProcessExitedMessage(mProcess.exitValue())); 375 } 376 } 377 378 // CopyToAndroidLogThread copies the camput child process's stderr 379 // to Android's log. 380 private static class CopyToAndroidLogThread extends Thread { 381 private final BufferedReader mBufIn; 382 private final String mStream; 383 384 public CopyToAndroidLogThread(String stream, InputStream in) { 385 mBufIn = new BufferedReader(new InputStreamReader(in)); 386 mStream = stream; 387 } 388 389 @Override 390 public void run() { 391 String tag = TAG + "/" + mStream + "-child"; 392 while (true) { 393 String line = null; 394 try { 395 line = mBufIn.readLine(); 396 } catch (IOException e) { 397 Log.d(tag, "Exception: " + e.toString()); 398 return; 399 } 400 if (line == null) { 401 // EOF 402 return; 403 } 404 Log.d(tag, line); 405 } 406 } 407 } 408 409 }