blob: caf3970fb5b6220b8956cdbb59fbf77c228044b1 [file] [log] [blame]
Dennis Kempincee37612013-06-14 10:40:41 -07001# Copyright (c) 2013 The Chromium OS Authors. All rights reserved.
2# Use of this source code is governed by a BSD-style license that can be
3# found in the LICENSE file.
4
Harry Cutts0edf1572020-01-21 15:42:10 -08005from __future__ import print_function
6
Dennis Kempincee37612013-06-14 10:40:41 -07007import cookielib
Sean O'Brienfd204da2017-05-02 15:13:11 -07008import datetime
9import time
Dennis Kempincee37612013-06-14 10:40:41 -070010import getpass
11import imghdr
12import os
13import os.path
Dennis Kempin13d948e2014-04-18 11:23:32 -070014import re
Charlie Mooneyd30322b2014-09-04 15:02:29 -070015import subprocess
Dennis Kempincee37612013-06-14 10:40:41 -070016import sys
17import urllib
18import urllib2
Sean O'Brienfd204da2017-05-02 15:13:11 -070019import StringIO
20import multiprocessing
21import random
22import errno
Dennis Kempincee37612013-06-14 10:40:41 -070023
24# path to current script directory
25script_dir = os.path.dirname(os.path.realpath(__file__))
26cache_dir = os.path.realpath(os.path.join(script_dir, "..", "cache"))
27if not os.path.exists(cache_dir):
28 os.mkdir(cache_dir)
29
30# path to the cookies file used for storing the login cookies.
Sean O'Brienfd204da2017-05-02 15:13:11 -070031rpc_cred_file = os.path.join(cache_dir, "rpc_cred")
Dennis Kempincee37612013-06-14 10:40:41 -070032
33# path to folder where downloaded reports are cached
34log_cache_dir = os.path.join(cache_dir, "reports")
35if not os.path.exists(log_cache_dir):
36 os.mkdir(log_cache_dir)
37
38class FeedbackDownloader():
Sean O'Brienfd204da2017-05-02 15:13:11 -070039 # Stubby command to download a feedback log:
40 # field_id - determines what data to download:
41 # 1: feedback log ID #
42 # 7: screenshot
43 # 8: system log
44 # resource_id - feedback log ID #
45 STUBBY_FILE_CMD = 'stubby --rpc_creds_file=' + rpc_cred_file + \
46 ' call blade:feedback-export-api-prod ' \
47 'ReportService.Get ' \
48 '\'result_mask <field <id:{field_id}>>, ' \
49 'resource_id: "{resource_id}"\''
50 # Stubby command to download a list of feedback log IDs:
51 # product_id - 208 refers to ChromeOS
52 # submission_time_[start|end]_time_ms - feedback submission time range
53 # max_results - number of IDs to download
54 # token - page token
55 STUBBY_LIST_CMD = 'stubby --rpc_creds_file=' + rpc_cred_file + \
56 ' call blade:feedback-export-api-prod ' \
57 'ReportService.List --proto2 ' \
58 '\'result_mask <field <id:1>>, ' \
59 'product_id: "208", ' \
60 'submission_time_start_ms: 0, ' \
61 'submission_time_end_ms: {end_time}, ' \
62 'page_selection {{ max_results: {max_results} ' \
63 'token: "{token}" }}\''
64 # Refer to go/touch-feedback-download for information on getting permission
65 # to download feedback logs.
66 GAIA_CMD = '/google/data/ro/projects/gaiamint/bin/get_mint --type=loas ' \
67 '--text --scopes=40700 --endusercreds > ' + rpc_cred_file
68 SYSTEM_LOG_FIELD = 7
69 SCREENSHOT_FIELD = 8
70 sleep_sec = 4.0
71 auth_lock = multiprocessing.Lock()
Charlie Mooneyd30322b2014-09-04 15:02:29 -070072
Sean O'Brienfd204da2017-05-02 15:13:11 -070073 def __init__(self, force_authenticate=False):
74 if force_authenticate:
75 self._Authenticate()
Dennis Kempincee37612013-06-14 10:40:41 -070076
Charlie Mooneyd30322b2014-09-04 15:02:29 -070077 def _OctetStreamToBinary(self, octetStream):
78 """ The zip files are returned in an octet-stream format that must
79 be decoded back into a binary. This function scans through the stream
80 and unescapes the special characters
81 """
82 binary = ''
83 i = 0
84 while i < len(octetStream):
85 if ord(octetStream[i]) is ord('\\'):
86 if re.match('\d\d\d', octetStream[i + 1:i + 4]):
87 binary += chr(int(octetStream[i + 1:i + 4], 8))
88 i += 4
89 else:
90 binary += octetStream[i:i + 2].decode("string-escape")
91 i += 2
92 else:
93 binary += octetStream[i]
94 i += 1
95 return binary
96
Sean O'Brienfd204da2017-05-02 15:13:11 -070097 def _AuthenticateWithLock(self):
98 is_owner = self.auth_lock.acquire(False)
99 if is_owner:
100 self._Authenticate()
101 self.auth_lock.release()
102 else:
103 self.auth_lock.acquire()
104 self.auth_lock.release()
105
106 def _StubbyCall(self, cmd):
107 if not os.path.exists(rpc_cred_file):
108 try:
109 os.mkdir(cache_dir)
110 except OSError as ex:
111 if ex.errno != errno.EEXIST:
112 raise
113 pass
114 self._AuthenticateWithLock()
115 while True:
116 process = subprocess.Popen(cmd, shell=True,
117 stdout=subprocess.PIPE,
118 stderr=subprocess.PIPE)
119
120 output, errors = process.communicate()
121 errorcode = process.returncode
122
123 if ('AUTH_FAIL' in errors or
124 'CredentialsExpiredException' in output or
125 'FORBIDDEN' in output):
126 self._AuthenticateWithLock()
127 continue
128 elif ('exceeds rate limit' in errors or
129 'WaitUntilNonEmpty' in errors):
130 self._AuthenticateWithLock()
131
132 sleep_time = self.sleep_sec + random.uniform(0.0, self.sleep_sec * 2)
133 time.sleep(sleep_time)
134 continue
135 elif errorcode != 0:
Harry Cutts0edf1572020-01-21 15:42:10 -0800136 print(errors)
137 print("default error")
138 print("An error (%d) occurred while downloading" % errorcode)
Sean O'Brienfd204da2017-05-02 15:13:11 -0700139 sys.exit(errorcode)
140 return output
141
142 def _DownloadAttachedFile(self, id, field):
143 cmd = FeedbackDownloader.STUBBY_FILE_CMD.format(
144 field_id=field, resource_id=id)
145 output = self._StubbyCall(cmd)
146 return output
147
148 def _Authenticate(self):
149 cmd = FeedbackDownloader.GAIA_CMD
Charlie Mooneyd30322b2014-09-04 15:02:29 -0700150 process = subprocess.Popen(cmd, shell=True,
151 stdout=subprocess.PIPE,
152 stderr=subprocess.PIPE)
Harry Cutts0edf1572020-01-21 15:42:10 -0800153 print("Authenticating...")
Charlie Mooneyd30322b2014-09-04 15:02:29 -0700154 output, errors = process.communicate()
155 errorcode = process.returncode
Charlie Mooneyd30322b2014-09-04 15:02:29 -0700156
Sean O'Brienfd204da2017-05-02 15:13:11 -0700157 if errorcode != 0:
Harry Cutts0edf1572020-01-21 15:42:10 -0800158 print(errors)
159 print("An error (%d) occurred while authenticating" % errorcode)
Sean O'Brienfd204da2017-05-02 15:13:11 -0700160 if errorcode == 126:
Harry Cutts0edf1572020-01-21 15:42:10 -0800161 print("You may need to run prodaccess")
Sean O'Brienfd204da2017-05-02 15:13:11 -0700162 sys.exit(errorcode)
163 return None
Harry Cutts0edf1572020-01-21 15:42:10 -0800164 print("Done Authenticating")
Sean O'Brienfd204da2017-05-02 15:13:11 -0700165
166 def DownloadIDs(self, num, end_time=None, page_token=''):
167 if not end_time:
168 dt = datetime.datetime.utcnow() - datetime.datetime(1970, 1, 1)
169 end_time = (((dt.days * 24 * 60 * 60 + dt.seconds) * 1000) +
170 (dt.microseconds / 10))
171
172 cmd = FeedbackDownloader.STUBBY_LIST_CMD.format(
173 end_time=end_time, max_results=num, token=page_token)
174 output = self._StubbyCall(cmd)
175
176 page_token = filter(lambda x: 'next_page_token' in x, output.split('\n'))[0]
177 page_token = page_token.split(" ")[-1][1:-1]
178 ids = filter(lambda x: 'id' in x, output.split('\n'))
179 ids = [x[7:-1] for x in ids]
180 return page_token, ids
Dennis Kempincee37612013-06-14 10:40:41 -0700181
182 def DownloadSystemLog(self, id):
Charlie Mooneyd30322b2014-09-04 15:02:29 -0700183 report = self._DownloadAttachedFile(id,
Sean O'Brienfd204da2017-05-02 15:13:11 -0700184 FeedbackDownloader.SYSTEM_LOG_FIELD)
185 sleep_time = self.sleep_sec + random.uniform(0.0, self.sleep_sec * 2)
186 time.sleep(sleep_time)
187 data_line = None
188 system_log = None
189 for count, line in enumerate(StringIO.StringIO(report)):
190 if 'name: "system_logs.zip"' in line:
191 data_line = count + 2
192 elif data_line and data_line == count:
193 system_log = re.search('data: "(.*)"\s*', line).group(1)
Dennis Kempinc6c981d2014-04-18 11:49:16 -0700194
Sean O'Brienfd204da2017-05-02 15:13:11 -0700195 if not system_log or (system_log[0:2] != "BZ" and system_log[0:2] != "PK"):
Harry Cutts0edf1572020-01-21 15:42:10 -0800196 print("Report " + id + " does not seem to include include log files...")
Dennis Kempincee37612013-06-14 10:40:41 -0700197 return None
Charlie Mooneyd30322b2014-09-04 15:02:29 -0700198
Sean O'Brienfd204da2017-05-02 15:13:11 -0700199 return self._OctetStreamToBinary(system_log)
Dennis Kempincee37612013-06-14 10:40:41 -0700200
201 def DownloadScreenshot(self, id):
Harry Cutts0edf1572020-01-21 15:42:10 -0800202 print("Downloading screenshot from %s..." % id)
Sean O'Brienfd204da2017-05-02 15:13:11 -0700203 report = self._DownloadAttachedFile(id,
204 FeedbackDownloader.SCREENSHOT_FIELD)
205 data_line = None
206 screenshot = None
207 for count, line in enumerate(StringIO.StringIO(report)):
208 if 'screenshot <' in line:
209 data_line = count + 2
210 elif data_line and data_line == count:
211 screenshot = re.search('content: "(.*)"\s*', line).group(1)
212
213 if not screenshot:
Harry Cutts0edf1572020-01-21 15:42:10 -0800214 print("Report does not seem to include include a screenshot...")
Sean O'Brienfd204da2017-05-02 15:13:11 -0700215 return None
216
217 return self._OctetStreamToBinary(screenshot)