blob: f7aa77131b0a35e2be7b068b50758b0eb834929d [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
13import os
14import os.path
Dennis Kempin13d948e2014-04-18 11:23:32 -070015import re
Charlie Mooneyd30322b2014-09-04 15:02:29 -070016import subprocess
Dennis Kempincee37612013-06-14 10:40:41 -070017import sys
Harry Cutts69fc2be2020-01-22 18:03:21 -080018try:
19 from StringIO import StringIO
20except ImportError:
21 from io import StringIO
Sean O'Brienfd204da2017-05-02 15:13:11 -070022import multiprocessing
23import random
24import errno
Dennis Kempincee37612013-06-14 10:40:41 -070025
26# path to current script directory
27script_dir = os.path.dirname(os.path.realpath(__file__))
28cache_dir = os.path.realpath(os.path.join(script_dir, "..", "cache"))
29if not os.path.exists(cache_dir):
30 os.mkdir(cache_dir)
31
32# path to the cookies file used for storing the login cookies.
Sean O'Brienfd204da2017-05-02 15:13:11 -070033rpc_cred_file = os.path.join(cache_dir, "rpc_cred")
Dennis Kempincee37612013-06-14 10:40:41 -070034
35# path to folder where downloaded reports are cached
36log_cache_dir = os.path.join(cache_dir, "reports")
37if not os.path.exists(log_cache_dir):
38 os.mkdir(log_cache_dir)
39
40class FeedbackDownloader():
Sean O'Brienfd204da2017-05-02 15:13:11 -070041 # Stubby command to download a feedback log:
42 # field_id - determines what data to download:
43 # 1: feedback log ID #
44 # 7: screenshot
45 # 8: system log
46 # resource_id - feedback log ID #
47 STUBBY_FILE_CMD = 'stubby --rpc_creds_file=' + rpc_cred_file + \
48 ' call blade:feedback-export-api-prod ' \
49 'ReportService.Get ' \
50 '\'result_mask <field <id:{field_id}>>, ' \
51 'resource_id: "{resource_id}"\''
52 # Stubby command to download a list of feedback log IDs:
53 # product_id - 208 refers to ChromeOS
54 # submission_time_[start|end]_time_ms - feedback submission time range
55 # max_results - number of IDs to download
56 # token - page token
57 STUBBY_LIST_CMD = 'stubby --rpc_creds_file=' + rpc_cred_file + \
58 ' call blade:feedback-export-api-prod ' \
59 'ReportService.List --proto2 ' \
60 '\'result_mask <field <id:1>>, ' \
61 'product_id: "208", ' \
62 'submission_time_start_ms: 0, ' \
63 'submission_time_end_ms: {end_time}, ' \
64 'page_selection {{ max_results: {max_results} ' \
65 'token: "{token}" }}\''
66 # Refer to go/touch-feedback-download for information on getting permission
67 # to download feedback logs.
68 GAIA_CMD = '/google/data/ro/projects/gaiamint/bin/get_mint --type=loas ' \
69 '--text --scopes=40700 --endusercreds > ' + rpc_cred_file
70 SYSTEM_LOG_FIELD = 7
71 SCREENSHOT_FIELD = 8
72 sleep_sec = 4.0
73 auth_lock = multiprocessing.Lock()
Charlie Mooneyd30322b2014-09-04 15:02:29 -070074
Sean O'Brienfd204da2017-05-02 15:13:11 -070075 def __init__(self, force_authenticate=False):
76 if force_authenticate:
77 self._Authenticate()
Dennis Kempincee37612013-06-14 10:40:41 -070078
Charlie Mooneyd30322b2014-09-04 15:02:29 -070079 def _OctetStreamToBinary(self, octetStream):
80 """ The zip files are returned in an octet-stream format that must
81 be decoded back into a binary. This function scans through the stream
82 and unescapes the special characters
83 """
84 binary = ''
85 i = 0
86 while i < len(octetStream):
87 if ord(octetStream[i]) is ord('\\'):
88 if re.match('\d\d\d', octetStream[i + 1:i + 4]):
89 binary += chr(int(octetStream[i + 1:i + 4], 8))
90 i += 4
91 else:
92 binary += octetStream[i:i + 2].decode("string-escape")
93 i += 2
94 else:
95 binary += octetStream[i]
96 i += 1
97 return binary
98
Sean O'Brienfd204da2017-05-02 15:13:11 -070099 def _AuthenticateWithLock(self):
100 is_owner = self.auth_lock.acquire(False)
101 if is_owner:
102 self._Authenticate()
103 self.auth_lock.release()
104 else:
105 self.auth_lock.acquire()
106 self.auth_lock.release()
107
108 def _StubbyCall(self, cmd):
109 if not os.path.exists(rpc_cred_file):
110 try:
111 os.mkdir(cache_dir)
112 except OSError as ex:
113 if ex.errno != errno.EEXIST:
114 raise
115 pass
116 self._AuthenticateWithLock()
117 while True:
118 process = subprocess.Popen(cmd, shell=True,
119 stdout=subprocess.PIPE,
120 stderr=subprocess.PIPE)
121
122 output, errors = process.communicate()
123 errorcode = process.returncode
124
125 if ('AUTH_FAIL' in errors or
126 'CredentialsExpiredException' in output or
127 'FORBIDDEN' in output):
128 self._AuthenticateWithLock()
129 continue
130 elif ('exceeds rate limit' in errors or
131 'WaitUntilNonEmpty' in errors):
132 self._AuthenticateWithLock()
133
134 sleep_time = self.sleep_sec + random.uniform(0.0, self.sleep_sec * 2)
135 time.sleep(sleep_time)
136 continue
137 elif errorcode != 0:
Harry Cutts0edf1572020-01-21 15:42:10 -0800138 print(errors)
139 print("default error")
140 print("An error (%d) occurred while downloading" % errorcode)
Sean O'Brienfd204da2017-05-02 15:13:11 -0700141 sys.exit(errorcode)
142 return output
143
144 def _DownloadAttachedFile(self, id, field):
145 cmd = FeedbackDownloader.STUBBY_FILE_CMD.format(
146 field_id=field, resource_id=id)
147 output = self._StubbyCall(cmd)
148 return output
149
150 def _Authenticate(self):
151 cmd = FeedbackDownloader.GAIA_CMD
Charlie Mooneyd30322b2014-09-04 15:02:29 -0700152 process = subprocess.Popen(cmd, shell=True,
153 stdout=subprocess.PIPE,
154 stderr=subprocess.PIPE)
Harry Cutts0edf1572020-01-21 15:42:10 -0800155 print("Authenticating...")
Charlie Mooneyd30322b2014-09-04 15:02:29 -0700156 output, errors = process.communicate()
157 errorcode = process.returncode
Charlie Mooneyd30322b2014-09-04 15:02:29 -0700158
Sean O'Brienfd204da2017-05-02 15:13:11 -0700159 if errorcode != 0:
Harry Cutts0edf1572020-01-21 15:42:10 -0800160 print(errors)
161 print("An error (%d) occurred while authenticating" % errorcode)
Sean O'Brienfd204da2017-05-02 15:13:11 -0700162 if errorcode == 126:
Harry Cutts0edf1572020-01-21 15:42:10 -0800163 print("You may need to run prodaccess")
Sean O'Brienfd204da2017-05-02 15:13:11 -0700164 sys.exit(errorcode)
165 return None
Harry Cutts0edf1572020-01-21 15:42:10 -0800166 print("Done Authenticating")
Sean O'Brienfd204da2017-05-02 15:13:11 -0700167
168 def DownloadIDs(self, num, end_time=None, page_token=''):
169 if not end_time:
170 dt = datetime.datetime.utcnow() - datetime.datetime(1970, 1, 1)
171 end_time = (((dt.days * 24 * 60 * 60 + dt.seconds) * 1000) +
172 (dt.microseconds / 10))
173
174 cmd = FeedbackDownloader.STUBBY_LIST_CMD.format(
175 end_time=end_time, max_results=num, token=page_token)
176 output = self._StubbyCall(cmd)
177
178 page_token = filter(lambda x: 'next_page_token' in x, output.split('\n'))[0]
179 page_token = page_token.split(" ")[-1][1:-1]
180 ids = filter(lambda x: 'id' in x, output.split('\n'))
181 ids = [x[7:-1] for x in ids]
182 return page_token, ids
Dennis Kempincee37612013-06-14 10:40:41 -0700183
184 def DownloadSystemLog(self, id):
Charlie Mooneyd30322b2014-09-04 15:02:29 -0700185 report = self._DownloadAttachedFile(id,
Sean O'Brienfd204da2017-05-02 15:13:11 -0700186 FeedbackDownloader.SYSTEM_LOG_FIELD)
187 sleep_time = self.sleep_sec + random.uniform(0.0, self.sleep_sec * 2)
188 time.sleep(sleep_time)
189 data_line = None
190 system_log = None
Harry Cutts69fc2be2020-01-22 18:03:21 -0800191 for count, line in enumerate(StringIO(report)):
Sean O'Brienfd204da2017-05-02 15:13:11 -0700192 if 'name: "system_logs.zip"' in line:
193 data_line = count + 2
194 elif data_line and data_line == count:
195 system_log = re.search('data: "(.*)"\s*', line).group(1)
Dennis Kempinc6c981d2014-04-18 11:49:16 -0700196
Sean O'Brienfd204da2017-05-02 15:13:11 -0700197 if not system_log or (system_log[0:2] != "BZ" and system_log[0:2] != "PK"):
Harry Cutts0edf1572020-01-21 15:42:10 -0800198 print("Report " + id + " does not seem to include include log files...")
Dennis Kempincee37612013-06-14 10:40:41 -0700199 return None
Charlie Mooneyd30322b2014-09-04 15:02:29 -0700200
Sean O'Brienfd204da2017-05-02 15:13:11 -0700201 return self._OctetStreamToBinary(system_log)
Dennis Kempincee37612013-06-14 10:40:41 -0700202
203 def DownloadScreenshot(self, id):
Harry Cutts0edf1572020-01-21 15:42:10 -0800204 print("Downloading screenshot from %s..." % id)
Sean O'Brienfd204da2017-05-02 15:13:11 -0700205 report = self._DownloadAttachedFile(id,
206 FeedbackDownloader.SCREENSHOT_FIELD)
207 data_line = None
208 screenshot = None
Harry Cutts69fc2be2020-01-22 18:03:21 -0800209 for count, line in enumerate(StringIO(report)):
Sean O'Brienfd204da2017-05-02 15:13:11 -0700210 if 'screenshot <' in line:
211 data_line = count + 2
212 elif data_line and data_line == count:
213 screenshot = re.search('content: "(.*)"\s*', line).group(1)
214
215 if not screenshot:
Harry Cutts0edf1572020-01-21 15:42:10 -0800216 print("Report does not seem to include include a screenshot...")
Sean O'Brienfd204da2017-05-02 15:13:11 -0700217 return None
218
219 return self._OctetStreamToBinary(screenshot)