projectaria-debug-sharing 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- projectaria_debug_sharing/__init__.py +22 -0
- projectaria_debug_sharing/debug_sharing/__init__.py +15 -0
- projectaria_debug_sharing/debug_sharing/_internal/__init__.py +13 -0
- projectaria_debug_sharing/debug_sharing/_internal/delivery.py +252 -0
- projectaria_debug_sharing/debug_sharing/_internal/file_access.py +130 -0
- projectaria_debug_sharing/debug_sharing/_internal/manifest.py +133 -0
- projectaria_debug_sharing/debug_sharing/_internal/plain_files.py +208 -0
- projectaria_debug_sharing/debug_sharing/_internal/report_command.py +560 -0
- projectaria_debug_sharing/debug_sharing/_internal/spool.py +673 -0
- projectaria_debug_sharing/debug_sharing/cli.py +183 -0
- projectaria_debug_sharing/debug_sharing/collectors.py +472 -0
- projectaria_debug_sharing/debug_sharing/command.py +36 -0
- projectaria_debug_sharing/debug_sharing/facade.py +435 -0
- projectaria_debug_sharing/debug_sharing/flytrap.py +327 -0
- projectaria_debug_sharing/debug_sharing/submitter.py +103 -0
- projectaria_debug_sharing/debug_sharing/types.py +172 -0
- projectaria_debug_sharing-0.1.0.dist-info/METADATA +30 -0
- projectaria_debug_sharing-0.1.0.dist-info/RECORD +22 -0
- projectaria_debug_sharing-0.1.0.dist-info/WHEEL +5 -0
- projectaria_debug_sharing-0.1.0.dist-info/entry_points.txt +2 -0
- projectaria_debug_sharing-0.1.0.dist-info/licenses/LICENSE +201 -0
- projectaria_debug_sharing-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,560 @@
|
|
|
1
|
+
# Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
|
|
15
|
+
"""What `aria-debug-report` runs, built on `ProjectAriaDebugSharing`."""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
import asyncio
|
|
20
|
+
import getpass
|
|
21
|
+
import json
|
|
22
|
+
import logging
|
|
23
|
+
import sys
|
|
24
|
+
from collections.abc import AsyncIterator, Mapping, Sequence
|
|
25
|
+
from contextlib import asynccontextmanager
|
|
26
|
+
from dataclasses import dataclass, field
|
|
27
|
+
from pathlib import Path
|
|
28
|
+
from typing import ClassVar, Final
|
|
29
|
+
|
|
30
|
+
from projectaria_debug_sharing.debug_sharing._internal.plain_files import (
|
|
31
|
+
open_plain_file,
|
|
32
|
+
)
|
|
33
|
+
from projectaria_debug_sharing.debug_sharing.collectors import Collector, FileCollector
|
|
34
|
+
from projectaria_debug_sharing.debug_sharing.facade import (
|
|
35
|
+
CollectorFailure,
|
|
36
|
+
ConsentDeclinedError,
|
|
37
|
+
Delivery,
|
|
38
|
+
DrainResult,
|
|
39
|
+
OfferedCollector,
|
|
40
|
+
ProjectAriaDebugSharing,
|
|
41
|
+
RequestConsent,
|
|
42
|
+
RESERVED_METADATA_KEYS,
|
|
43
|
+
)
|
|
44
|
+
from projectaria_debug_sharing.debug_sharing.flytrap import (
|
|
45
|
+
DEV_SERVER_ENV,
|
|
46
|
+
FlyTrapSubmissionError,
|
|
47
|
+
FlyTrapSubmitter,
|
|
48
|
+
NotLoggedInError,
|
|
49
|
+
REPORT_BUG_URL,
|
|
50
|
+
report_bug_url,
|
|
51
|
+
)
|
|
52
|
+
from projectaria_debug_sharing.debug_sharing.submitter import Submitter
|
|
53
|
+
from projectaria_login import (
|
|
54
|
+
AuthenticationConfig,
|
|
55
|
+
FileEncryptionKeyRequest,
|
|
56
|
+
KeyRequestPurpose,
|
|
57
|
+
ProjectAriaLogin,
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
CLI_LOGGER_NAME: Final[str] = "projectaria_debug_sharing.debug_sharing.cli"
|
|
61
|
+
"""The logger whose lines reach the console without `--verbose`. Named
|
|
62
|
+
explicitly: as the binary's `__main__`, `cli`'s `__name__` falls outside the
|
|
63
|
+
package logger the console handler is attached to."""
|
|
64
|
+
|
|
65
|
+
logger: logging.Logger = logging.getLogger(CLI_LOGGER_NAME)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def debug_sharing_auth_config() -> AuthenticationConfig:
|
|
69
|
+
"""The application `aria-debug-report` logs in as: ARK Debug Sharing.
|
|
70
|
+
|
|
71
|
+
Every value is one an installed client may carry. The FB part is the app's
|
|
72
|
+
client token, never its secret: device login accepts it, and the server
|
|
73
|
+
fetches the secret itself when it mints the ID token. The FRL client token
|
|
74
|
+
is Project Aria Login's until Debug Sharing's own is issued; only the
|
|
75
|
+
password login reads it.
|
|
76
|
+
"""
|
|
77
|
+
return AuthenticationConfig(
|
|
78
|
+
fb_app_access_token="2004636393387720|867e3e56220e91334bd008e8607090f9",
|
|
79
|
+
client_token="FRL|1563499658844492|888ed02d3443510a67c96e63d4d71b7b",
|
|
80
|
+
client_application=29261831290084371,
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
class ReportInputError(Exception):
|
|
85
|
+
"""The user mistyped something. Reported as one line, not a traceback."""
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
@dataclass(frozen=True)
|
|
89
|
+
class ReportInput:
|
|
90
|
+
"""What the user asked to send. Build it with `from_command_line`."""
|
|
91
|
+
|
|
92
|
+
description: str
|
|
93
|
+
attachments: tuple[Path, ...] = ()
|
|
94
|
+
metadata: Mapping[str, str] = field(default_factory=dict)
|
|
95
|
+
|
|
96
|
+
_JSON_FIELDS: ClassVar[frozenset[str]] = frozenset(
|
|
97
|
+
{"description", "attachments", "metadata"}
|
|
98
|
+
)
|
|
99
|
+
"""The keys `--from-json` accepts. `ClassVar`, since a dataclass makes a
|
|
100
|
+
`Final` annotation a field."""
|
|
101
|
+
|
|
102
|
+
@classmethod
|
|
103
|
+
def from_command_line(
|
|
104
|
+
cls,
|
|
105
|
+
*,
|
|
106
|
+
description: str = "",
|
|
107
|
+
attachments: Sequence[Path] = (),
|
|
108
|
+
metadata: Sequence[tuple[str, str]] = (),
|
|
109
|
+
from_json: Path | None = None,
|
|
110
|
+
) -> ReportInput:
|
|
111
|
+
"""
|
|
112
|
+
Checks the `submit` flags, or reads `--from-json`, before anything logs
|
|
113
|
+
in, so a mistyped path fails before authentication starts.
|
|
114
|
+
|
|
115
|
+
Args:
|
|
116
|
+
description: `--description`.
|
|
117
|
+
attachments: Each `--attach`.
|
|
118
|
+
metadata: Each `--metadata KEY=VALUE`.
|
|
119
|
+
from_json: `--from-json`, which replaces the other three.
|
|
120
|
+
|
|
121
|
+
Returns:
|
|
122
|
+
The checked input; every attachment is a plain file that opens.
|
|
123
|
+
|
|
124
|
+
Raises:
|
|
125
|
+
ReportInputError: Anything the user mistyped, including flags that
|
|
126
|
+
`--from-json` would silently override, no description, a
|
|
127
|
+
metadata key the consent record owns, or an attachment that
|
|
128
|
+
cannot be read.
|
|
129
|
+
"""
|
|
130
|
+
if from_json is not None:
|
|
131
|
+
if description or attachments or metadata:
|
|
132
|
+
raise ReportInputError(
|
|
133
|
+
"--from-json cannot be combined with --description, --attach "
|
|
134
|
+
"or --metadata"
|
|
135
|
+
)
|
|
136
|
+
return cls._from_json(from_json)
|
|
137
|
+
if not description:
|
|
138
|
+
raise ReportInputError("--description is required: say what went wrong")
|
|
139
|
+
return cls._checked(description, attachments, dict(metadata))
|
|
140
|
+
|
|
141
|
+
@classmethod
|
|
142
|
+
def _from_json(cls, path: Path) -> ReportInput:
|
|
143
|
+
fields = cls._read_json_object(path)
|
|
144
|
+
unknown = fields.keys() - cls._JSON_FIELDS
|
|
145
|
+
if unknown:
|
|
146
|
+
raise ReportInputError(
|
|
147
|
+
f"{path}: unknown field(s) {', '.join(sorted(unknown))}"
|
|
148
|
+
)
|
|
149
|
+
description = fields.get("description")
|
|
150
|
+
if not isinstance(description, str) or not description:
|
|
151
|
+
raise ReportInputError(f"{path}: description must be a non-empty string")
|
|
152
|
+
listed = fields.get("attachments", [])
|
|
153
|
+
if not isinstance(listed, list) or not all(isinstance(p, str) for p in listed):
|
|
154
|
+
raise ReportInputError(f"{path}: attachments must be a list of paths")
|
|
155
|
+
metadata = fields.get("metadata", {})
|
|
156
|
+
if not isinstance(metadata, dict) or not all(
|
|
157
|
+
isinstance(v, str) for v in metadata.values()
|
|
158
|
+
):
|
|
159
|
+
raise ReportInputError(f"{path}: metadata must be an object of strings")
|
|
160
|
+
return cls._checked(description, [Path(p) for p in listed], metadata)
|
|
161
|
+
|
|
162
|
+
@staticmethod
|
|
163
|
+
def _read_json_object(path: Path) -> dict[str, object]:
|
|
164
|
+
try:
|
|
165
|
+
fields = json.loads(path.read_text(encoding="utf-8"))
|
|
166
|
+
except (OSError, ValueError) as e:
|
|
167
|
+
raise ReportInputError(f"{path} could not be read: {e}") from e
|
|
168
|
+
if not isinstance(fields, dict):
|
|
169
|
+
raise ReportInputError(f"{path} must contain a JSON object")
|
|
170
|
+
return fields
|
|
171
|
+
|
|
172
|
+
@classmethod
|
|
173
|
+
def _checked(
|
|
174
|
+
cls, description: str, attachments: Sequence[Path], metadata: dict[str, str]
|
|
175
|
+
) -> ReportInput:
|
|
176
|
+
reserved = metadata.keys() & RESERVED_METADATA_KEYS
|
|
177
|
+
if reserved:
|
|
178
|
+
raise ReportInputError(
|
|
179
|
+
f"metadata key(s) {', '.join(sorted(reserved))} are reserved for "
|
|
180
|
+
"the sharer"
|
|
181
|
+
)
|
|
182
|
+
for path in attachments:
|
|
183
|
+
try:
|
|
184
|
+
open_plain_file(path).close()
|
|
185
|
+
except OSError as e:
|
|
186
|
+
raise ReportInputError(
|
|
187
|
+
f"could not attach {path}: {e.strerror or type(e).__name__}"
|
|
188
|
+
) from e
|
|
189
|
+
return cls(
|
|
190
|
+
description=description, attachments=tuple(attachments), metadata=metadata
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
class DebugReportCommand:
|
|
195
|
+
"""
|
|
196
|
+
Runs one `aria-debug-report` command through `ProjectAriaDebugSharing`:
|
|
197
|
+
files a report, which the sharer keeps if sending fails, or drains what is
|
|
198
|
+
kept.
|
|
199
|
+
|
|
200
|
+
Another tool's command line can run the same commands under its own
|
|
201
|
+
identity, with collectors of its own and a consent prompt. It says what
|
|
202
|
+
happened in its own words by overriding the `report_*` methods.
|
|
203
|
+
"""
|
|
204
|
+
|
|
205
|
+
SOURCE_APP: Final[str] = "aria_debug_report"
|
|
206
|
+
"""The name `aria-debug-report` files under, and the default `source_app`.
|
|
207
|
+
A tool that runs the command passes its own; no command line can set it,
|
|
208
|
+
so a report cannot claim another app."""
|
|
209
|
+
|
|
210
|
+
_CONTENT_TYPES: Final[Mapping[str, str]] = {
|
|
211
|
+
".txt": "text/plain",
|
|
212
|
+
".log": "text/plain",
|
|
213
|
+
".json": "application/json",
|
|
214
|
+
}
|
|
215
|
+
"""The type an attachment is sent as, by suffix; anything else is binary."""
|
|
216
|
+
|
|
217
|
+
def __init__(
|
|
218
|
+
self,
|
|
219
|
+
*,
|
|
220
|
+
source_app: str = SOURCE_APP,
|
|
221
|
+
auth_config: AuthenticationConfig | None = None,
|
|
222
|
+
distribution: str | None = None,
|
|
223
|
+
request_consent: RequestConsent | None = None,
|
|
224
|
+
spool_dir: Path | None = None,
|
|
225
|
+
submitter: Submitter | None = None,
|
|
226
|
+
) -> None:
|
|
227
|
+
"""
|
|
228
|
+
Args:
|
|
229
|
+
source_app: The tool filing the report.
|
|
230
|
+
auth_config: The application the login presents. Defaults to ARK
|
|
231
|
+
Debug Sharing's.
|
|
232
|
+
distribution: The tool's installed package, described in the
|
|
233
|
+
report's environment.
|
|
234
|
+
request_consent: Asks the user what may be collected, for
|
|
235
|
+
`submit(..., ask=True)`.
|
|
236
|
+
spool_dir: Where unsent reports are kept. Defaults to the spool
|
|
237
|
+
every Project Aria tool shares; the command line deliberately
|
|
238
|
+
offers no other location.
|
|
239
|
+
submitter: Where reports are sent. Defaults to FlyTrap, over the
|
|
240
|
+
saved Project Aria login, opened for each command.
|
|
241
|
+
"""
|
|
242
|
+
self._source_app = source_app
|
|
243
|
+
self._auth_config = auth_config
|
|
244
|
+
self._distribution = distribution
|
|
245
|
+
self._request_consent = request_consent
|
|
246
|
+
self._spool_dir = spool_dir
|
|
247
|
+
self._submitter = submitter
|
|
248
|
+
|
|
249
|
+
async def submit(
|
|
250
|
+
self,
|
|
251
|
+
report_input: ReportInput,
|
|
252
|
+
*,
|
|
253
|
+
collectors: Sequence[Collector] = (),
|
|
254
|
+
report_id: str | None = None,
|
|
255
|
+
ask: bool = False,
|
|
256
|
+
) -> int:
|
|
257
|
+
"""
|
|
258
|
+
Files one report. Without `ask`, running the command is the user's
|
|
259
|
+
consent: naming a file on the command line, or a host's `--yes`, so
|
|
260
|
+
the report records everything offered as granted by the caller. The
|
|
261
|
+
console says only what happened and quotes the bug id; the error
|
|
262
|
+
itself, which can carry server detail, is logged at DEBUG for
|
|
263
|
+
`--verbose`.
|
|
264
|
+
|
|
265
|
+
Args:
|
|
266
|
+
report_input: The checked command line.
|
|
267
|
+
collectors: The calling tool's own collectors. One that fails is
|
|
268
|
+
reported with `report_collector_failure` and the report is
|
|
269
|
+
filed without it. They run with the attachments, so anything
|
|
270
|
+
one does while collecting has happened even if an attachment
|
|
271
|
+
then stops the report.
|
|
272
|
+
report_id: Correlates the report with another, such as the bug
|
|
273
|
+
report the glasses file; generated if None.
|
|
274
|
+
ask: Ask with the `request_consent` given at construction.
|
|
275
|
+
|
|
276
|
+
Returns:
|
|
277
|
+
0 when filed, else 1, including when the user declines.
|
|
278
|
+
|
|
279
|
+
Raises:
|
|
280
|
+
ReportInputError: PROJECTARIA_DEV_SERVER is malformed, checked
|
|
281
|
+
before logging in; a collector's name is already taken; or an
|
|
282
|
+
attachment could not be read after all, in which case the
|
|
283
|
+
report is not sent.
|
|
284
|
+
ValueError: `ask` without a `request_consent`.
|
|
285
|
+
"""
|
|
286
|
+
if ask and self._request_consent is None:
|
|
287
|
+
raise ValueError("ask=True needs a request_consent")
|
|
288
|
+
self._check_destination()
|
|
289
|
+
async with self._open_submitter() as submitter:
|
|
290
|
+
sharer = self._sharer(submitter)
|
|
291
|
+
paths = self._register_attachments(sharer, report_input.attachments)
|
|
292
|
+
self._register_host_collectors(sharer, collectors)
|
|
293
|
+
if not ask:
|
|
294
|
+
self.report_consent_given(self._offered(sharer))
|
|
295
|
+
try:
|
|
296
|
+
report, failures = await sharer.collect(
|
|
297
|
+
description=report_input.description,
|
|
298
|
+
metadata=report_input.metadata,
|
|
299
|
+
report_id=report_id,
|
|
300
|
+
granted=None if ask else sharer.offered_scopes,
|
|
301
|
+
)
|
|
302
|
+
except ConsentDeclinedError:
|
|
303
|
+
return self.report_cancelled()
|
|
304
|
+
self._report_failures(failures, paths)
|
|
305
|
+
delivery = await sharer.deliver(report)
|
|
306
|
+
return self.report_delivery(delivery)
|
|
307
|
+
|
|
308
|
+
@staticmethod
|
|
309
|
+
def _register_host_collectors(
|
|
310
|
+
sharer: ProjectAriaDebugSharing, collectors: Sequence[Collector]
|
|
311
|
+
) -> None:
|
|
312
|
+
for collector in collectors:
|
|
313
|
+
try:
|
|
314
|
+
sharer.register(collector)
|
|
315
|
+
except ValueError as e:
|
|
316
|
+
# A name already taken, such as by an attachment's stem.
|
|
317
|
+
raise ReportInputError(str(e)) from e
|
|
318
|
+
|
|
319
|
+
@staticmethod
|
|
320
|
+
def _offered(sharer: ProjectAriaDebugSharing) -> tuple[OfferedCollector, ...]:
|
|
321
|
+
return tuple(
|
|
322
|
+
OfferedCollector(c.name, c.scope, c.disclosure) for c in sharer.collectors
|
|
323
|
+
)
|
|
324
|
+
|
|
325
|
+
def _report_failures(
|
|
326
|
+
self, failures: Sequence[CollectorFailure], paths: Mapping[str, Path]
|
|
327
|
+
) -> None:
|
|
328
|
+
"""
|
|
329
|
+
Says each of the tool's own failed collectors, then raises for the
|
|
330
|
+
first attachment that could not be read. The tool's failures come
|
|
331
|
+
first: they are part of what the user sees even when an attachment
|
|
332
|
+
stops the report.
|
|
333
|
+
|
|
334
|
+
Raises:
|
|
335
|
+
ReportInputError: An attachment could not be read.
|
|
336
|
+
"""
|
|
337
|
+
attachment_failures = [f for f in failures if f.collector_name in paths]
|
|
338
|
+
for failure in failures:
|
|
339
|
+
if failure.collector_name not in paths:
|
|
340
|
+
self.report_collector_failure(failure)
|
|
341
|
+
if attachment_failures:
|
|
342
|
+
failure = attachment_failures[0]
|
|
343
|
+
raise ReportInputError(
|
|
344
|
+
f"could not attach {paths[failure.collector_name]}: {failure.error}"
|
|
345
|
+
)
|
|
346
|
+
|
|
347
|
+
async def drain(self) -> int:
|
|
348
|
+
"""
|
|
349
|
+
Sends every kept report. Running it by hand is the user asking, so it
|
|
350
|
+
includes reports they were offered but never asked to send.
|
|
351
|
+
|
|
352
|
+
Returns:
|
|
353
|
+
0 when nothing is left over, else 1, including when the kept
|
|
354
|
+
reports cannot be read. Never a count, which the process exit
|
|
355
|
+
status would wrap.
|
|
356
|
+
|
|
357
|
+
Raises:
|
|
358
|
+
ReportInputError: PROJECTARIA_DEV_SERVER is malformed. Checked
|
|
359
|
+
before logging in.
|
|
360
|
+
"""
|
|
361
|
+
self._check_destination()
|
|
362
|
+
async with self._open_submitter() as submitter:
|
|
363
|
+
try:
|
|
364
|
+
result = await self._sharer(submitter).drain()
|
|
365
|
+
except OSError as e:
|
|
366
|
+
logger.error(
|
|
367
|
+
"Could not read the kept reports: %s",
|
|
368
|
+
e.strerror or type(e).__name__,
|
|
369
|
+
)
|
|
370
|
+
return 1
|
|
371
|
+
return self._report_drain(result)
|
|
372
|
+
|
|
373
|
+
def _sharer(self, submitter: Submitter) -> ProjectAriaDebugSharing:
|
|
374
|
+
return ProjectAriaDebugSharing(
|
|
375
|
+
source_app=self._source_app,
|
|
376
|
+
submitter=submitter,
|
|
377
|
+
request_consent=self._request_consent,
|
|
378
|
+
distribution=self._distribution,
|
|
379
|
+
spool_dir=self._spool_dir,
|
|
380
|
+
)
|
|
381
|
+
|
|
382
|
+
@classmethod
|
|
383
|
+
def _register_attachments(
|
|
384
|
+
cls, sharer: ProjectAriaDebugSharing, attachments: Sequence[Path]
|
|
385
|
+
) -> dict[str, Path]:
|
|
386
|
+
"""
|
|
387
|
+
Registers a `FileCollector` per attachment, named after the file's stem
|
|
388
|
+
and numbered when stems repeat, since the sharer refuses two collectors
|
|
389
|
+
with one name.
|
|
390
|
+
|
|
391
|
+
Returns:
|
|
392
|
+
Each collector's name, mapped to its file.
|
|
393
|
+
"""
|
|
394
|
+
paths: dict[str, Path] = {}
|
|
395
|
+
for path in attachments:
|
|
396
|
+
name = path.stem
|
|
397
|
+
count = 1
|
|
398
|
+
while name in paths:
|
|
399
|
+
count += 1
|
|
400
|
+
name = f"{path.stem}-{count}"
|
|
401
|
+
paths[name] = path
|
|
402
|
+
sharer.register(
|
|
403
|
+
FileCollector(
|
|
404
|
+
name,
|
|
405
|
+
path,
|
|
406
|
+
disclosure=f"the file {path}",
|
|
407
|
+
content_type=cls._CONTENT_TYPES.get(
|
|
408
|
+
path.suffix.lower(), "application/octet-stream"
|
|
409
|
+
),
|
|
410
|
+
)
|
|
411
|
+
)
|
|
412
|
+
return paths
|
|
413
|
+
|
|
414
|
+
def report_consent_given(self, offered: tuple[OfferedCollector, ...]) -> None:
|
|
415
|
+
"""
|
|
416
|
+
Called when running the command was the consent, just before
|
|
417
|
+
collecting; collection can still fail after it, and then nothing is
|
|
418
|
+
sent. Says nothing by default: `aria-debug-report` sends only the files
|
|
419
|
+
its user named.
|
|
420
|
+
|
|
421
|
+
Args:
|
|
422
|
+
offered: Everything the report will collect.
|
|
423
|
+
"""
|
|
424
|
+
|
|
425
|
+
def report_cancelled(self) -> int:
|
|
426
|
+
"""
|
|
427
|
+
Says the user declined, when `submit` asked.
|
|
428
|
+
|
|
429
|
+
Returns:
|
|
430
|
+
The exit code.
|
|
431
|
+
"""
|
|
432
|
+
logger.error("Report cancelled. Nothing was sent.")
|
|
433
|
+
return 1
|
|
434
|
+
|
|
435
|
+
def report_collector_failure(self, failure: CollectorFailure) -> None:
|
|
436
|
+
"""
|
|
437
|
+
Says one of the calling tool's collectors failed. The report is filed
|
|
438
|
+
without it.
|
|
439
|
+
|
|
440
|
+
Args:
|
|
441
|
+
failure: Which collector, and why.
|
|
442
|
+
"""
|
|
443
|
+
logger.warning(
|
|
444
|
+
"Could not collect %s: %s", failure.collector_name, failure.error
|
|
445
|
+
)
|
|
446
|
+
|
|
447
|
+
def report_delivery(self, delivery: Delivery) -> int:
|
|
448
|
+
"""
|
|
449
|
+
Says what happened to the report. The error itself, which can quote the
|
|
450
|
+
server's response, goes to DEBUG only.
|
|
451
|
+
|
|
452
|
+
Args:
|
|
453
|
+
delivery: What happened.
|
|
454
|
+
|
|
455
|
+
Returns:
|
|
456
|
+
The exit code: 0 when filed, else 1.
|
|
457
|
+
"""
|
|
458
|
+
if delivery.bug_id is not None:
|
|
459
|
+
logger.info("Filed as bug %s", delivery.bug_id)
|
|
460
|
+
return 0
|
|
461
|
+
logger.debug("Not filed: %s", delivery.error)
|
|
462
|
+
logged_out = isinstance(delivery.error, NotLoggedInError)
|
|
463
|
+
if logged_out:
|
|
464
|
+
logger.error(
|
|
465
|
+
"Not logged in to Project Aria, or the saved login could not be "
|
|
466
|
+
"unlocked, so the report was not sent."
|
|
467
|
+
)
|
|
468
|
+
if delivery.will_retry:
|
|
469
|
+
logger.error(
|
|
470
|
+
"The report could not be sent. Kept at %s; the next drain sends it.",
|
|
471
|
+
delivery.spooled,
|
|
472
|
+
)
|
|
473
|
+
if logged_out:
|
|
474
|
+
logger.error(
|
|
475
|
+
"Log in to Project Aria, then run: aria-debug-report drain"
|
|
476
|
+
)
|
|
477
|
+
else:
|
|
478
|
+
logger.error(
|
|
479
|
+
"To retry it now and see why it failed, run: "
|
|
480
|
+
"aria-debug-report drain --verbose"
|
|
481
|
+
)
|
|
482
|
+
return 1
|
|
483
|
+
if delivery.error is not None and not delivery.error.retryable:
|
|
484
|
+
logger.error("The report was not accepted, so it will not be retried.")
|
|
485
|
+
else:
|
|
486
|
+
logger.error("The report could not be sent, and could not be kept either.")
|
|
487
|
+
logger.error("Run with --verbose for details.")
|
|
488
|
+
return 1
|
|
489
|
+
|
|
490
|
+
@staticmethod
|
|
491
|
+
def _report_drain(result: DrainResult) -> int:
|
|
492
|
+
if not (result.sent or result.waiting or result.rejected):
|
|
493
|
+
logger.info("Nothing pending.")
|
|
494
|
+
return 0
|
|
495
|
+
logger.info(
|
|
496
|
+
"Filed %d, %d still waiting, %d will not be sent",
|
|
497
|
+
len(result.sent),
|
|
498
|
+
result.waiting,
|
|
499
|
+
result.rejected,
|
|
500
|
+
)
|
|
501
|
+
if not (result.waiting or result.rejected):
|
|
502
|
+
return 0
|
|
503
|
+
logger.error("Run with --verbose for details.")
|
|
504
|
+
return 1
|
|
505
|
+
|
|
506
|
+
@asynccontextmanager
|
|
507
|
+
async def _open_submitter(self) -> AsyncIterator[Submitter]:
|
|
508
|
+
"""The given submitter, or FlyTrap over the saved login; there is no token flag."""
|
|
509
|
+
if self._submitter is not None:
|
|
510
|
+
yield self._submitter
|
|
511
|
+
return
|
|
512
|
+
async with ProjectAriaLogin(
|
|
513
|
+
self._auth_config or debug_sharing_auth_config(),
|
|
514
|
+
on_need_file_encryption_key=self.ask_for_login_passphrase,
|
|
515
|
+
) as login:
|
|
516
|
+
yield FlyTrapSubmitter(login=login)
|
|
517
|
+
|
|
518
|
+
@staticmethod
|
|
519
|
+
async def ask_for_login_passphrase(
|
|
520
|
+
request: FileEncryptionKeyRequest,
|
|
521
|
+
) -> str | None:
|
|
522
|
+
"""
|
|
523
|
+
The passphrase protecting a saved login, for machines with no keyring.
|
|
524
|
+
|
|
525
|
+
Args:
|
|
526
|
+
request: What the login wants the key for.
|
|
527
|
+
|
|
528
|
+
Returns:
|
|
529
|
+
The typed passphrase. None without a terminal to ask at, and for a
|
|
530
|
+
new login, which this tool never saves: `projectaria_login login`
|
|
531
|
+
does.
|
|
532
|
+
"""
|
|
533
|
+
if request.purpose is not KeyRequestPurpose.LOAD or not sys.stdin.isatty():
|
|
534
|
+
return None
|
|
535
|
+
try:
|
|
536
|
+
return await asyncio.to_thread(
|
|
537
|
+
getpass.getpass, "Project Aria login passphrase: "
|
|
538
|
+
)
|
|
539
|
+
except EOFError:
|
|
540
|
+
return None
|
|
541
|
+
|
|
542
|
+
@staticmethod
|
|
543
|
+
def _check_destination() -> None:
|
|
544
|
+
"""
|
|
545
|
+
Warns on the console when PROJECTARIA_DEV_SERVER sends reports
|
|
546
|
+
somewhere other than production; the library says so only at DEBUG.
|
|
547
|
+
|
|
548
|
+
Raises:
|
|
549
|
+
ReportInputError: PROJECTARIA_DEV_SERVER is set but malformed.
|
|
550
|
+
Checked before logging in, so it is a usage error rather than
|
|
551
|
+
a failed send.
|
|
552
|
+
"""
|
|
553
|
+
try:
|
|
554
|
+
url = report_bug_url()
|
|
555
|
+
except FlyTrapSubmissionError as e:
|
|
556
|
+
raise ReportInputError(str(e)) from e
|
|
557
|
+
if url != REPORT_BUG_URL:
|
|
558
|
+
logger.warning(
|
|
559
|
+
"Sending to %s, not production: %s is set", url, DEV_SERVER_ENV
|
|
560
|
+
)
|