blob: 63fd741fc95729cde2abb35cc69af581b1e7221b [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 Cutts69fc2be2020-01-22 18:03:21 -08005from __future__ import absolute_import
6from __future__ import division
Harry Cutts0edf1572020-01-21 15:42:10 -08007from __future__ import print_function
8
Sean O'Brienfd204da2017-05-02 15:13:11 -07009import datetime
10import time
Dennis Kempincee37612013-06-14 10:40:41 -070011import getpass
12import imghdr
Harry Cutts85378ee2020-02-07 15:53:46 -080013import math
Dennis Kempincee37612013-06-14 10:40:41 -070014import os
15import os.path
Dennis Kempin13d948e2014-04-18 11:23:32 -070016import re
Charlie Mooneyd30322b2014-09-04 15:02:29 -070017import subprocess
Dennis Kempincee37612013-06-14 10:40:41 -070018import sys
Harry Cutts69fc2be2020-01-22 18:03:21 -080019try:
20 from StringIO import StringIO
21except ImportError:
22 from io import StringIO
Sean O'Brienfd204da2017-05-02 15:13:11 -070023import multiprocessing
24import random
25import errno
Dennis Kempincee37612013-06-14 10:40:41 -070026
27# path to current script directory
28script_dir = os.path.dirname(os.path.realpath(__file__))
29cache_dir = os.path.realpath(os.path.join(script_dir, "..", "cache"))
30if not os.path.exists(cache_dir):
31 os.mkdir(cache_dir)
32
33# path to the cookies file used for storing the login cookies.
Sean O'Brienfd204da2017-05-02 15:13:11 -070034rpc_cred_file = os.path.join(cache_dir, "rpc_cred")
Dennis Kempincee37612013-06-14 10:40:41 -070035
36# path to folder where downloaded reports are cached
37log_cache_dir = os.path.join(cache_dir, "reports")
38if not os.path.exists(log_cache_dir):
39 os.mkdir(log_cache_dir)
40
41class FeedbackDownloader():
Sean O'Brienfd204da2017-05-02 15:13:11 -070042 # Stubby command to download a feedback log:
43 # field_id - determines what data to download:
44 # 1: feedback log ID #
45 # 7: screenshot
46 # 8: system log
47 # resource_id - feedback log ID #
48 STUBBY_FILE_CMD = 'stubby --rpc_creds_file=' + rpc_cred_file + \
49 ' call blade:feedback-export-api-prod ' \
50 'ReportService.Get ' \
51 '\'result_mask <field <id:{field_id}>>, ' \
52 'resource_id: "{resource_id}"\''
53 # Stubby command to download a list of feedback log IDs:
54 # product_id - 208 refers to ChromeOS
55 # submission_time_[start|end]_time_ms - feedback submission time range
56 # max_results - number of IDs to download
57 # token - page token
58 STUBBY_LIST_CMD = 'stubby --rpc_creds_file=' + rpc_cred_file + \
59 ' call blade:feedback-export-api-prod ' \
60 'ReportService.List --proto2 ' \
61 '\'result_mask <field <id:1>>, ' \
62 'product_id: "208", ' \
63 'submission_time_start_ms: 0, ' \
64 'submission_time_end_ms: {end_time}, ' \
65 'page_selection {{ max_results: {max_results} ' \
66 'token: "{token}" }}\''
67 # Refer to go/touch-feedback-download for information on getting permission
68 # to download feedback logs.
69 GAIA_CMD = '/google/data/ro/projects/gaiamint/bin/get_mint --type=loas ' \
70 '--text --scopes=40700 --endusercreds > ' + rpc_cred_file
71 SYSTEM_LOG_FIELD = 7
72 SCREENSHOT_FIELD = 8
73 sleep_sec = 4.0
74 auth_lock = multiprocessing.Lock()
Charlie Mooneyd30322b2014-09-04 15:02:29 -070075
Sean O'Brienfd204da2017-05-02 15:13:11 -070076 def __init__(self, force_authenticate=False):
77 if force_authenticate:
78 self._Authenticate()
Dennis Kempincee37612013-06-14 10:40:41 -070079
Charlie Mooneyd30322b2014-09-04 15:02:29 -070080 def _OctetStreamToBinary(self, octetStream):
81 """ The zip files are returned in an octet-stream format that must
82 be decoded back into a binary. This function scans through the stream
83 and unescapes the special characters
84 """
85 binary = ''
86 i = 0
87 while i < len(octetStream):
88 if ord(octetStream[i]) is ord('\\'):
89 if re.match('\d\d\d', octetStream[i + 1:i + 4]):
90 binary += chr(int(octetStream[i + 1:i + 4], 8))
91 i += 4
92 else:
93 binary += octetStream[i:i + 2].decode("string-escape")
94 i += 2
95 else:
96 binary += octetStream[i]
97 i += 1
98 return binary
99
Sean O'Brienfd204da2017-05-02 15:13:11 -0700100 def _AuthenticateWithLock(self):
101 is_owner = self.auth_lock.acquire(False)
102 if is_owner:
103 self._Authenticate()
104 self.auth_lock.release()
105 else:
106 self.auth_lock.acquire()
107 self.auth_lock.release()
108
109 def _StubbyCall(self, cmd):
110 if not os.path.exists(rpc_cred_file):
111 try:
112 os.mkdir(cache_dir)
113 except OSError as ex:
114 if ex.errno != errno.EEXIST:
115 raise
116 pass
117 self._AuthenticateWithLock()
118 while True:
119 process = subprocess.Popen(cmd, shell=True,
120 stdout=subprocess.PIPE,
Harry Cutts85378ee2020-02-07 15:53:46 -0800121 stderr=subprocess.PIPE,
122 universal_newlines=True)
Sean O'Brienfd204da2017-05-02 15:13:11 -0700123
124 output, errors = process.communicate()
125 errorcode = process.returncode
126
127 if ('AUTH_FAIL' in errors or
128 'CredentialsExpiredException' in output or
129 'FORBIDDEN' in output):
130 self._AuthenticateWithLock()
131 continue
132 elif ('exceeds rate limit' in errors or
133 'WaitUntilNonEmpty' in errors):
134 self._AuthenticateWithLock()
135
136 sleep_time = self.sleep_sec + random.uniform(0.0, self.sleep_sec * 2)
137 time.sleep(sleep_time)
138 continue
139 elif errorcode != 0:
Harry Cutts0edf1572020-01-21 15:42:10 -0800140 print(errors)
141 print("default error")
142 print("An error (%d) occurred while downloading" % errorcode)
Sean O'Brienfd204da2017-05-02 15:13:11 -0700143 sys.exit(errorcode)
144 return output
145
146 def _DownloadAttachedFile(self, id, field):
147 cmd = FeedbackDownloader.STUBBY_FILE_CMD.format(
148 field_id=field, resource_id=id)
149 output = self._StubbyCall(cmd)
150 return output
151
152 def _Authenticate(self):
153 cmd = FeedbackDownloader.GAIA_CMD
Charlie Mooneyd30322b2014-09-04 15:02:29 -0700154 process = subprocess.Popen(cmd, shell=True,
155 stdout=subprocess.PIPE,
Harry Cutts85378ee2020-02-07 15:53:46 -0800156 stderr=subprocess.PIPE,
157 universal_newlines=True)
Harry Cutts0edf1572020-01-21 15:42:10 -0800158 print("Authenticating...")
Charlie Mooneyd30322b2014-09-04 15:02:29 -0700159 output, errors = process.communicate()
160 errorcode = process.returncode
Charlie Mooneyd30322b2014-09-04 15:02:29 -0700161
Sean O'Brienfd204da2017-05-02 15:13:11 -0700162 if errorcode != 0:
Harry Cutts0edf1572020-01-21 15:42:10 -0800163 print(errors)
164 print("An error (%d) occurred while authenticating" % errorcode)
Sean O'Brienfd204da2017-05-02 15:13:11 -0700165 if errorcode == 126:
Harry Cutts0edf1572020-01-21 15:42:10 -0800166 print("You may need to run prodaccess")
Sean O'Brienfd204da2017-05-02 15:13:11 -0700167 sys.exit(errorcode)
168 return None
Harry Cutts0edf1572020-01-21 15:42:10 -0800169 print("Done Authenticating")
Sean O'Brienfd204da2017-05-02 15:13:11 -0700170
Harry Cutts85378ee2020-02-07 15:53:46 -0800171 def DownloadIDs(self, num, end_time_ms=None, page_token=''):
172 if not end_time_ms:
Sean O'Brienfd204da2017-05-02 15:13:11 -0700173 dt = datetime.datetime.utcnow() - datetime.datetime(1970, 1, 1)
Harry Cutts85378ee2020-02-07 15:53:46 -0800174 end_time_ms = (((dt.days * 24 * 60 * 60 + dt.seconds) * 1000) +
175 math.ceil(dt.microseconds / 10))
Sean O'Brienfd204da2017-05-02 15:13:11 -0700176
177 cmd = FeedbackDownloader.STUBBY_LIST_CMD.format(
Harry Cutts85378ee2020-02-07 15:53:46 -0800178 end_time=int(end_time_ms), max_results=num, token=page_token)
Sean O'Brienfd204da2017-05-02 15:13:11 -0700179 output = self._StubbyCall(cmd)
180
Harry Cutts85378ee2020-02-07 15:53:46 -0800181 lines = output.split('\n')
182 page_token = list(filter(lambda x: 'next_page_token' in x, lines))[0]
Sean O'Brienfd204da2017-05-02 15:13:11 -0700183 page_token = page_token.split(" ")[-1][1:-1]
Harry Cutts85378ee2020-02-07 15:53:46 -0800184 ids = filter(lambda x: 'id' in x, lines)
Sean O'Brienfd204da2017-05-02 15:13:11 -0700185 ids = [x[7:-1] for x in ids]
186 return page_token, ids
Dennis Kempincee37612013-06-14 10:40:41 -0700187
188 def DownloadSystemLog(self, id):
Charlie Mooneyd30322b2014-09-04 15:02:29 -0700189 report = self._DownloadAttachedFile(id,
Sean O'Brienfd204da2017-05-02 15:13:11 -0700190 FeedbackDownloader.SYSTEM_LOG_FIELD)
191 sleep_time = self.sleep_sec + random.uniform(0.0, self.sleep_sec * 2)
192 time.sleep(sleep_time)
193 data_line = None
194 system_log = None
Harry Cutts69fc2be2020-01-22 18:03:21 -0800195 for count, line in enumerate(StringIO(report)):
Sean O'Brienfd204da2017-05-02 15:13:11 -0700196 if 'name: "system_logs.zip"' in line:
197 data_line = count + 2
198 elif data_line and data_line == count:
199 system_log = re.search('data: "(.*)"\s*', line).group(1)
Dennis Kempinc6c981d2014-04-18 11:49:16 -0700200
Sean O'Brienfd204da2017-05-02 15:13:11 -0700201 if not system_log or (system_log[0:2] != "BZ" and system_log[0:2] != "PK"):
Harry Cutts0edf1572020-01-21 15:42:10 -0800202 print("Report " + id + " does not seem to include include log files...")
Dennis Kempincee37612013-06-14 10:40:41 -0700203 return None
Charlie Mooneyd30322b2014-09-04 15:02:29 -0700204
Sean O'Brienfd204da2017-05-02 15:13:11 -0700205 return self._OctetStreamToBinary(system_log)
Dennis Kempincee37612013-06-14 10:40:41 -0700206
207 def DownloadScreenshot(self, id):
Harry Cutts0edf1572020-01-21 15:42:10 -0800208 print("Downloading screenshot from %s..." % id)
Sean O'Brienfd204da2017-05-02 15:13:11 -0700209 report = self._DownloadAttachedFile(id,
210 FeedbackDownloader.SCREENSHOT_FIELD)
211 data_line = None
212 screenshot = None
Harry Cutts69fc2be2020-01-22 18:03:21 -0800213 for count, line in enumerate(StringIO(report)):
Sean O'Brienfd204da2017-05-02 15:13:11 -0700214 if 'screenshot <' in line:
215 data_line = count + 2
216 elif data_line and data_line == count:
217 screenshot = re.search('content: "(.*)"\s*', line).group(1)
218
219 if not screenshot:
Harry Cutts0edf1572020-01-21 15:42:10 -0800220 print("Report does not seem to include include a screenshot...")
Sean O'Brienfd204da2017-05-02 15:13:11 -0700221 return None
222
223 return self._OctetStreamToBinary(screenshot)