ruyi 0.44.0a20251118__py3-none-any.whl → 0.45.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.
- ruyi/__main__.py +16 -4
- ruyi/cli/cmd.py +6 -5
- ruyi/cli/config_cli.py +14 -11
- ruyi/cli/main.py +34 -17
- ruyi/cli/oobe.py +10 -10
- ruyi/cli/self_cli.py +49 -36
- ruyi/cli/user_input.py +42 -12
- ruyi/cli/version_cli.py +11 -5
- ruyi/config/__init__.py +30 -10
- ruyi/config/errors.py +19 -7
- ruyi/device/provision.py +116 -55
- ruyi/device/provision_cli.py +6 -3
- ruyi/i18n/__init__.py +129 -0
- ruyi/log/__init__.py +6 -5
- ruyi/mux/runtime.py +19 -6
- ruyi/mux/venv/maker.py +93 -35
- ruyi/mux/venv/venv_cli.py +13 -10
- ruyi/pluginhost/plugin_cli.py +4 -3
- ruyi/resource_bundle/__init__.py +22 -8
- ruyi/resource_bundle/__main__.py +6 -5
- ruyi/resource_bundle/data.py +13 -9
- ruyi/ruyipkg/admin_checksum.py +4 -1
- ruyi/ruyipkg/admin_cli.py +9 -6
- ruyi/ruyipkg/augmented_pkg.py +15 -14
- ruyi/ruyipkg/checksum.py +8 -2
- ruyi/ruyipkg/distfile.py +33 -9
- ruyi/ruyipkg/entity.py +12 -2
- ruyi/ruyipkg/entity_cli.py +20 -12
- ruyi/ruyipkg/entity_provider.py +11 -2
- ruyi/ruyipkg/fetcher.py +38 -9
- ruyi/ruyipkg/install.py +163 -64
- ruyi/ruyipkg/install_cli.py +18 -15
- ruyi/ruyipkg/list.py +27 -20
- ruyi/ruyipkg/list_cli.py +12 -7
- ruyi/ruyipkg/news.py +23 -11
- ruyi/ruyipkg/news_cli.py +10 -7
- ruyi/ruyipkg/profile_cli.py +8 -2
- ruyi/ruyipkg/repo.py +22 -8
- ruyi/ruyipkg/unpack.py +42 -8
- ruyi/ruyipkg/unpack_method.py +5 -1
- ruyi/ruyipkg/update_cli.py +8 -3
- ruyi/telemetry/aggregate.py +5 -0
- ruyi/telemetry/provider.py +292 -105
- ruyi/telemetry/store.py +68 -15
- ruyi/telemetry/telemetry_cli.py +32 -13
- ruyi/utils/git.py +18 -11
- ruyi/utils/prereqs.py +10 -5
- ruyi/utils/ssl_patch.py +2 -1
- ruyi/version.py +9 -3
- {ruyi-0.44.0a20251118.dist-info → ruyi-0.45.0.dist-info}/METADATA +4 -2
- ruyi-0.45.0.dist-info/RECORD +103 -0
- {ruyi-0.44.0a20251118.dist-info → ruyi-0.45.0.dist-info}/WHEEL +1 -1
- ruyi-0.44.0a20251118.dist-info/RECORD +0 -102
- {ruyi-0.44.0a20251118.dist-info → ruyi-0.45.0.dist-info}/entry_points.txt +0 -0
- {ruyi-0.44.0a20251118.dist-info → ruyi-0.45.0.dist-info}/licenses/LICENSE-Apache.txt +0 -0
ruyi/telemetry/provider.py
CHANGED
|
@@ -7,6 +7,7 @@ import time
|
|
|
7
7
|
from typing import Callable, TYPE_CHECKING, cast
|
|
8
8
|
import uuid
|
|
9
9
|
|
|
10
|
+
from ..i18n import _, d_
|
|
10
11
|
from ..log import RuyiLogger
|
|
11
12
|
from ..utils.node_info import NodeInfo, gather_node_info
|
|
12
13
|
from .scope import TelemetryScope
|
|
@@ -18,23 +19,32 @@ if TYPE_CHECKING:
|
|
|
18
19
|
|
|
19
20
|
FALLBACK_PM_TELEMETRY_ENDPOINT = "https://api.ruyisdk.cn/telemetry/pm/"
|
|
20
21
|
|
|
21
|
-
TELEMETRY_CONSENT_AND_UPLOAD_DESC =
|
|
22
|
-
|
|
22
|
+
TELEMETRY_CONSENT_AND_UPLOAD_DESC = d_(
|
|
23
|
+
"""
|
|
24
|
+
RuyiSDK collects minimal usage data in the form of just a version number of
|
|
25
|
+
the running [yellow]ruyi[/], to help us improve the product. With your consent,
|
|
26
|
+
RuyiSDK may also collect additional non-tracking usage data to be sent
|
|
27
|
+
periodically. The data will be recorded and processed by RuyiSDK team-managed
|
|
28
|
+
servers located in the Chinese mainland.
|
|
23
29
|
|
|
24
30
|
[green]By default, nothing leaves your machine[/], and you can also turn off usage data
|
|
25
|
-
collection completely. Only with your explicit permission can [yellow]ruyi[/]
|
|
26
|
-
|
|
27
|
-
the Chinese mainland. You can change this setting at any time by running
|
|
31
|
+
collection completely. Only with your explicit permission can [yellow]ruyi[/] collect and
|
|
32
|
+
upload more usage data. You can change this setting at any time by running
|
|
28
33
|
[yellow]ruyi telemetry consent[/], [yellow]ruyi telemetry local[/], or [yellow]ruyi telemetry optout[/].
|
|
29
34
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
35
|
+
We'll also send a one-time report from this [yellow]ruyi[/] installation so the RuyiSDK
|
|
36
|
+
team can better understand adoption. If you choose to opt out, this will be the
|
|
37
|
+
only data to be ever uploaded, without any tracking ID being generated or kept.
|
|
38
|
+
Thank you for helping us build a better experience!
|
|
33
39
|
"""
|
|
34
|
-
TELEMETRY_CONSENT_AND_UPLOAD_PROMPT = (
|
|
35
|
-
"Enable telemetry uploads and send a one-time report now?"
|
|
36
40
|
)
|
|
37
|
-
|
|
41
|
+
TELEMETRY_CONSENT_AND_UPLOAD_PROMPT = d_(
|
|
42
|
+
"Do you agree to have usage data periodically uploaded?"
|
|
43
|
+
)
|
|
44
|
+
TELEMETRY_OPTOUT_PROMPT = d_("\nDo you want to opt out of telemetry entirely?")
|
|
45
|
+
MALFORMED_TELEMETRY_STATE_MSG = d_(
|
|
46
|
+
"malformed telemetry state: unable to determine upload weekday, nothing will be uploaded"
|
|
47
|
+
)
|
|
38
48
|
|
|
39
49
|
|
|
40
50
|
def next_utc_weekday(wday: int, now: float | None = None) -> int:
|
|
@@ -108,29 +118,35 @@ def set_telemetry_mode(
|
|
|
108
118
|
return
|
|
109
119
|
match mode:
|
|
110
120
|
case "on":
|
|
111
|
-
logger.I("telemetry data uploading is now enabled")
|
|
121
|
+
logger.I(_("telemetry data uploading is now enabled"))
|
|
112
122
|
logger.I(
|
|
113
|
-
|
|
123
|
+
_(
|
|
124
|
+
"you can opt out at any time by running [yellow]ruyi telemetry optout[/]"
|
|
125
|
+
)
|
|
114
126
|
)
|
|
115
127
|
case "local":
|
|
116
|
-
logger.I("telemetry mode is now set to local collection only")
|
|
128
|
+
logger.I(_("telemetry mode is now set to local collection only"))
|
|
117
129
|
logger.I(
|
|
118
|
-
|
|
130
|
+
_(
|
|
131
|
+
"you can re-enable telemetry data uploading at any time by running [yellow]ruyi telemetry consent[/]"
|
|
132
|
+
)
|
|
119
133
|
)
|
|
120
134
|
logger.I(
|
|
121
|
-
"or opt out at any time by running [yellow]ruyi telemetry optout[/]"
|
|
135
|
+
_("or opt out at any time by running [yellow]ruyi telemetry optout[/]")
|
|
122
136
|
)
|
|
123
137
|
case "off":
|
|
124
|
-
logger.I("telemetry data collection is now disabled")
|
|
138
|
+
logger.I(_("telemetry data collection is now disabled"))
|
|
125
139
|
logger.I(
|
|
126
|
-
|
|
140
|
+
_(
|
|
141
|
+
"you can re-enable telemetry data uploads at any time by running [yellow]ruyi telemetry consent[/]"
|
|
142
|
+
)
|
|
127
143
|
)
|
|
128
144
|
case _:
|
|
129
145
|
raise ValueError(f"invalid telemetry mode: {mode}")
|
|
130
146
|
|
|
131
147
|
|
|
132
148
|
class TelemetryProvider:
|
|
133
|
-
def __init__(self, gc: "GlobalConfig") -> None:
|
|
149
|
+
def __init__(self, gc: "GlobalConfig", minimal: bool) -> None:
|
|
134
150
|
self.state_root = pathlib.Path(gc.telemetry_root)
|
|
135
151
|
|
|
136
152
|
self._discard_events = False
|
|
@@ -138,6 +154,7 @@ class TelemetryProvider:
|
|
|
138
154
|
self._is_first_run = False
|
|
139
155
|
self._stores: dict[TelemetryScope, TelemetryStore] = {}
|
|
140
156
|
self._upload_on_exit = False
|
|
157
|
+
self.minimal = minimal
|
|
141
158
|
|
|
142
159
|
# create the PM store
|
|
143
160
|
self.init_store(TelemetryScope(None))
|
|
@@ -154,6 +171,8 @@ class TelemetryProvider:
|
|
|
154
171
|
|
|
155
172
|
@property
|
|
156
173
|
def upload_consent_time(self) -> datetime.datetime | None:
|
|
174
|
+
if self.minimal or self.local_mode:
|
|
175
|
+
return None
|
|
157
176
|
return self._gc.telemetry_upload_consent_time
|
|
158
177
|
|
|
159
178
|
def store(self, scope: TelemetryScope) -> TelemetryStore | None:
|
|
@@ -203,11 +222,18 @@ class TelemetryProvider:
|
|
|
203
222
|
def installation_file(self) -> pathlib.Path:
|
|
204
223
|
return self.state_root / "installation.json"
|
|
205
224
|
|
|
225
|
+
@property
|
|
226
|
+
def minimal_installation_marker_file(self) -> pathlib.Path:
|
|
227
|
+
return self.state_root / "minimal-installation-marker"
|
|
228
|
+
|
|
206
229
|
def check_first_run_status(self) -> None:
|
|
207
230
|
"""Check if this is the first run of the application by checking if installation file exists.
|
|
208
231
|
This must be done before init_installation() is potentially called.
|
|
209
232
|
"""
|
|
210
|
-
self._is_first_run =
|
|
233
|
+
self._is_first_run = (
|
|
234
|
+
not self.installation_file.exists()
|
|
235
|
+
and not self.minimal_installation_marker_file.exists()
|
|
236
|
+
)
|
|
211
237
|
|
|
212
238
|
@property
|
|
213
239
|
def is_first_run(self) -> bool:
|
|
@@ -215,9 +241,15 @@ class TelemetryProvider:
|
|
|
215
241
|
return self._is_first_run
|
|
216
242
|
|
|
217
243
|
def init_installation(self, force_reinit: bool) -> NodeInfo | None:
|
|
244
|
+
if self.minimal:
|
|
245
|
+
# be extra safe by not reading or writing installation data at all
|
|
246
|
+
# in minimal mode
|
|
247
|
+
self._init_minimal_installation_marker(force_reinit)
|
|
248
|
+
return None
|
|
249
|
+
|
|
218
250
|
installation_file = self.installation_file
|
|
219
251
|
if installation_file.exists() and not force_reinit:
|
|
220
|
-
return self.
|
|
252
|
+
return self._read_installation_data()
|
|
221
253
|
|
|
222
254
|
# either this is a fresh installation or we're forcing a refresh
|
|
223
255
|
installation_id = uuid.uuid4()
|
|
@@ -232,13 +264,26 @@ class TelemetryProvider:
|
|
|
232
264
|
fp.write(json.dumps(installation_data).encode("utf-8"))
|
|
233
265
|
return installation_data
|
|
234
266
|
|
|
235
|
-
def
|
|
267
|
+
def _init_minimal_installation_marker(self, force_reinit: bool) -> None:
|
|
268
|
+
if self.minimal_installation_marker_file.exists() and not force_reinit:
|
|
269
|
+
return
|
|
270
|
+
|
|
271
|
+
self.logger.D("initializing minimal installation marker file")
|
|
272
|
+
self.state_root.mkdir(parents=True, exist_ok=True)
|
|
273
|
+
|
|
274
|
+
# just touch the file
|
|
275
|
+
self.minimal_installation_marker_file.touch()
|
|
276
|
+
|
|
277
|
+
def _read_installation_data(self) -> NodeInfo | None:
|
|
236
278
|
with open(self.installation_file, "rb") as fp:
|
|
237
279
|
return cast(NodeInfo, json.load(fp))
|
|
238
280
|
|
|
239
|
-
def
|
|
281
|
+
def _upload_weekday(self) -> int | None:
|
|
282
|
+
if self.minimal:
|
|
283
|
+
return None
|
|
284
|
+
|
|
240
285
|
try:
|
|
241
|
-
installation_data = self.
|
|
286
|
+
installation_data = self._read_installation_data()
|
|
242
287
|
except FileNotFoundError:
|
|
243
288
|
# init the node info if it's gone
|
|
244
289
|
installation_data = self.init_installation(False)
|
|
@@ -253,120 +298,240 @@ class TelemetryProvider:
|
|
|
253
298
|
|
|
254
299
|
return report_uuid_prefix % 7 # 0 is Monday
|
|
255
300
|
|
|
256
|
-
def
|
|
301
|
+
def _has_upload_consent(self, time_now: float | None = None) -> bool:
|
|
257
302
|
if self.upload_consent_time is None:
|
|
258
303
|
return False
|
|
259
304
|
if time_now is None:
|
|
260
305
|
time_now = time.time()
|
|
261
306
|
return self.upload_consent_time.timestamp() <= time_now
|
|
262
307
|
|
|
263
|
-
def
|
|
264
|
-
if self.local_mode:
|
|
265
|
-
if for_cli_verbose_output:
|
|
266
|
-
self.logger.I(
|
|
267
|
-
"telemetry mode is [green]local[/]: local data collection only, no uploads"
|
|
268
|
-
)
|
|
269
|
-
return
|
|
270
|
-
|
|
271
|
-
now = time.time()
|
|
272
|
-
if self.has_upload_consent(now) and not for_cli_verbose_output:
|
|
273
|
-
self.logger.D("user has consented to telemetry upload")
|
|
274
|
-
return
|
|
275
|
-
|
|
276
|
-
upload_wday = self.upload_weekday()
|
|
277
|
-
if upload_wday is None:
|
|
278
|
-
return
|
|
279
|
-
upload_wday_name = calendar.day_name[upload_wday]
|
|
280
|
-
|
|
308
|
+
def _print_upload_schedule_notice(self, upload_wday: int, now: float) -> None:
|
|
281
309
|
next_upload_day_ts = next_utc_weekday(upload_wday, now)
|
|
282
310
|
next_upload_day = time.localtime(next_upload_day_ts)
|
|
283
311
|
next_upload_day_end = time.localtime(next_upload_day_ts + 86400)
|
|
284
312
|
next_upload_day_str = time.strftime("%Y-%m-%d %H:%M:%S %z", next_upload_day)
|
|
285
313
|
next_upload_day_end_str = time.strftime(
|
|
286
|
-
"%Y-%m-%d %H:%M:%S %z",
|
|
314
|
+
"%Y-%m-%d %H:%M:%S %z",
|
|
315
|
+
next_upload_day_end,
|
|
287
316
|
)
|
|
288
317
|
|
|
289
|
-
|
|
290
|
-
if for_cli_verbose_output:
|
|
291
|
-
self.logger.I(
|
|
292
|
-
"telemetry mode is [green]on[/]: data is collected and periodically uploaded"
|
|
293
|
-
)
|
|
294
|
-
self.logger.I(
|
|
295
|
-
f"non-tracking usage information will be uploaded to RuyiSDK-managed servers [bold green]every {upload_wday_name}[/]"
|
|
296
|
-
)
|
|
297
|
-
else:
|
|
298
|
-
self.logger.W(
|
|
299
|
-
f"this [yellow]ruyi[/] installation has telemetry mode set to [yellow]on[/], and [bold]will upload non-tracking usage information to RuyiSDK-managed servers[/] [bold green]every {upload_wday_name}[/]"
|
|
300
|
-
)
|
|
301
|
-
|
|
302
|
-
if today_is_upload_day:
|
|
318
|
+
if self._is_upload_day(now):
|
|
303
319
|
for scope, store in self._stores.items():
|
|
304
|
-
has_uploaded_today = self.
|
|
320
|
+
has_uploaded_today = self._has_uploaded_today(
|
|
321
|
+
store.last_upload_timestamp,
|
|
322
|
+
now,
|
|
323
|
+
)
|
|
305
324
|
if has_uploaded_today:
|
|
306
325
|
if last_upload_time := store.last_upload_timestamp:
|
|
307
326
|
last_upload_time_str = time.strftime(
|
|
308
327
|
"%Y-%m-%d %H:%M:%S %z", time.localtime(last_upload_time)
|
|
309
328
|
)
|
|
310
329
|
self.logger.I(
|
|
311
|
-
|
|
330
|
+
_(
|
|
331
|
+
"scope {scope}: usage information has already been uploaded today at {last_upload_time_str}"
|
|
332
|
+
).format(
|
|
333
|
+
scope=scope,
|
|
334
|
+
last_upload_time_str=last_upload_time_str,
|
|
335
|
+
)
|
|
312
336
|
)
|
|
313
337
|
else:
|
|
314
338
|
self.logger.I(
|
|
315
|
-
|
|
339
|
+
_(
|
|
340
|
+
"scope {scope}: usage information has already been uploaded sometime today"
|
|
341
|
+
).format(
|
|
342
|
+
scope=scope,
|
|
343
|
+
)
|
|
316
344
|
)
|
|
317
345
|
else:
|
|
318
346
|
self.logger.I(
|
|
319
|
-
|
|
347
|
+
_(
|
|
348
|
+
"scope {scope}: the next upload will happen [bold green]today[/] if not already"
|
|
349
|
+
).format(
|
|
350
|
+
scope=scope,
|
|
351
|
+
)
|
|
320
352
|
)
|
|
321
353
|
else:
|
|
322
354
|
self.logger.I(
|
|
323
|
-
|
|
355
|
+
_("the next upload will happen anytime [yellow]ruyi[/] is executed:")
|
|
356
|
+
)
|
|
357
|
+
self.logger.I(
|
|
358
|
+
_(
|
|
359
|
+
" - between [bold green]{time_start}[/] and [bold green]{time_end}[/]"
|
|
360
|
+
).format(
|
|
361
|
+
time_start=next_upload_day_str,
|
|
362
|
+
time_end=next_upload_day_end_str,
|
|
363
|
+
)
|
|
324
364
|
)
|
|
365
|
+
self.logger.I(_(" - or if the last upload is more than a week ago"))
|
|
366
|
+
|
|
367
|
+
def print_telemetry_notice(self, for_cli_verbose_output: bool = False) -> None:
|
|
368
|
+
if self.minimal:
|
|
369
|
+
if for_cli_verbose_output:
|
|
370
|
+
self.logger.I(
|
|
371
|
+
_(
|
|
372
|
+
"telemetry mode is [green]off[/]: nothing is collected or uploaded after the first run"
|
|
373
|
+
)
|
|
374
|
+
)
|
|
375
|
+
return
|
|
376
|
+
|
|
377
|
+
now = time.time()
|
|
378
|
+
upload_wday = self._upload_weekday()
|
|
379
|
+
if upload_wday is None:
|
|
380
|
+
if for_cli_verbose_output:
|
|
381
|
+
self.logger.W(MALFORMED_TELEMETRY_STATE_MSG)
|
|
382
|
+
else:
|
|
383
|
+
self.logger.D(MALFORMED_TELEMETRY_STATE_MSG)
|
|
384
|
+
return
|
|
385
|
+
|
|
386
|
+
upload_wday_name = self._gc.babel_locale.days["format"]["wide"][upload_wday]
|
|
387
|
+
|
|
388
|
+
if self.local_mode:
|
|
389
|
+
if for_cli_verbose_output:
|
|
390
|
+
self.logger.I(
|
|
391
|
+
_(
|
|
392
|
+
"telemetry mode is [green]local[/]: local usage collection only, no usage uploads except if requested"
|
|
393
|
+
)
|
|
394
|
+
)
|
|
395
|
+
return
|
|
396
|
+
|
|
397
|
+
if self._has_upload_consent(now) and not for_cli_verbose_output:
|
|
398
|
+
self.logger.D("user has consented to telemetry upload")
|
|
399
|
+
return
|
|
400
|
+
|
|
401
|
+
if for_cli_verbose_output:
|
|
402
|
+
self.logger.I(
|
|
403
|
+
_(
|
|
404
|
+
"telemetry mode is [green]on[/]: usage data is collected and periodically uploaded"
|
|
405
|
+
)
|
|
406
|
+
)
|
|
407
|
+
self.logger.I(
|
|
408
|
+
_(
|
|
409
|
+
"non-tracking usage information will be uploaded to RuyiSDK-managed servers [bold green]every {weekday}[/]"
|
|
410
|
+
).format(
|
|
411
|
+
weekday=upload_wday_name,
|
|
412
|
+
)
|
|
413
|
+
)
|
|
414
|
+
else:
|
|
415
|
+
self.logger.W(
|
|
416
|
+
_(
|
|
417
|
+
"this [yellow]ruyi[/] installation has telemetry mode set to [yellow]on[/], and [bold]will upload non-tracking usage information to RuyiSDK-managed servers[/] [bold green]every {weekday}[/]"
|
|
418
|
+
).format(
|
|
419
|
+
weekday=upload_wday_name,
|
|
420
|
+
)
|
|
421
|
+
)
|
|
422
|
+
|
|
423
|
+
self._print_upload_schedule_notice(upload_wday, now)
|
|
325
424
|
|
|
326
425
|
if not for_cli_verbose_output:
|
|
327
|
-
self.logger.I("in order to hide this banner:")
|
|
328
|
-
self.logger.I("- opt out with [yellow]ruyi telemetry optout[/]")
|
|
329
|
-
self.logger.I(
|
|
426
|
+
self.logger.I(_("in order to hide this banner:"))
|
|
427
|
+
self.logger.I(_(" - opt out with [yellow]ruyi telemetry optout[/]"))
|
|
428
|
+
self.logger.I(
|
|
429
|
+
_(" - or give consent with [yellow]ruyi telemetry consent[/]")
|
|
430
|
+
)
|
|
330
431
|
|
|
331
|
-
def
|
|
332
|
-
upload_wday = self.
|
|
432
|
+
def _next_upload_day(self, time_now: float | None = None) -> int | None:
|
|
433
|
+
upload_wday = self._upload_weekday()
|
|
333
434
|
if upload_wday is None:
|
|
334
435
|
return None
|
|
335
436
|
return next_utc_weekday(upload_wday, time_now)
|
|
336
437
|
|
|
337
|
-
def
|
|
438
|
+
def _is_upload_day(self, time_now: float | None = None) -> bool:
|
|
338
439
|
if time_now is None:
|
|
339
440
|
time_now = time.time()
|
|
340
|
-
if upload_day := self.
|
|
441
|
+
if upload_day := self._next_upload_day(time_now):
|
|
341
442
|
return upload_day <= time_now
|
|
342
443
|
return False
|
|
343
444
|
|
|
344
|
-
def
|
|
445
|
+
def _has_uploaded_today(
|
|
345
446
|
self,
|
|
346
|
-
|
|
447
|
+
last_upload_time: float | None,
|
|
347
448
|
time_now: float | None = None,
|
|
348
449
|
) -> bool:
|
|
349
450
|
if time_now is None:
|
|
350
451
|
time_now = time.time()
|
|
351
|
-
if upload_day := self.
|
|
452
|
+
if upload_day := self._next_upload_day(time_now):
|
|
352
453
|
upload_day_end = upload_day + 86400
|
|
353
|
-
|
|
354
|
-
if store is None:
|
|
355
|
-
return False
|
|
356
|
-
if last_upload_time := store.last_upload_timestamp:
|
|
454
|
+
if last_upload_time is not None:
|
|
357
455
|
return upload_day <= last_upload_time < upload_day_end
|
|
358
456
|
return False
|
|
359
457
|
|
|
360
458
|
def record(self, scope: TelemetryScope, kind: str, **params: object) -> None:
|
|
459
|
+
if self.minimal:
|
|
460
|
+
self.logger.D(
|
|
461
|
+
f"minimal telemetry mode enabled, discarding event '{kind}' for scope {scope}"
|
|
462
|
+
)
|
|
463
|
+
return
|
|
464
|
+
|
|
361
465
|
if store := self.store(scope):
|
|
362
466
|
return store.record(kind, **params)
|
|
363
|
-
self.logger.D(
|
|
467
|
+
self.logger.D(
|
|
468
|
+
f"no telemetry store for scope {scope}, discarding event '{kind}'"
|
|
469
|
+
)
|
|
364
470
|
|
|
365
471
|
def discard_events(self, v: bool = True) -> None:
|
|
366
472
|
self._discard_events = v
|
|
367
473
|
|
|
368
|
-
def
|
|
369
|
-
|
|
474
|
+
def _should_proceed_with_upload(
|
|
475
|
+
self,
|
|
476
|
+
scope: TelemetryScope,
|
|
477
|
+
explicit_request: bool,
|
|
478
|
+
cron_mode: bool,
|
|
479
|
+
now: float,
|
|
480
|
+
) -> tuple[bool, str]:
|
|
481
|
+
# proceed to uploading if forced (explicit requested or _upload_on_exit)
|
|
482
|
+
# regardless of schedule
|
|
483
|
+
if explicit_request:
|
|
484
|
+
return True, "explicit request"
|
|
485
|
+
if self._upload_on_exit:
|
|
486
|
+
return True, "first-run upload on exit"
|
|
487
|
+
|
|
488
|
+
# this is not an explicitly requested upload, so only proceed if today
|
|
489
|
+
# is the day, or if the last upload is more than a week ago
|
|
490
|
+
#
|
|
491
|
+
# the last-upload-more-than-a-week-ago check is to avoid situations
|
|
492
|
+
# where the user has not run ruyi for a long time, thus missing
|
|
493
|
+
# the scheduled upload day.
|
|
494
|
+
#
|
|
495
|
+
# cron jobs are a mitigation, but we cannot rely on them either, because:
|
|
496
|
+
#
|
|
497
|
+
# * ruyi is more likely installed user-locally than system-wide, so
|
|
498
|
+
# users may not set up cron jobs for themselves;
|
|
499
|
+
# * telemetry data is always recorded per user so system-wide cron jobs
|
|
500
|
+
# cannot easily access this data.
|
|
501
|
+
last_upload_time: float | None = None
|
|
502
|
+
if store := self.store(scope):
|
|
503
|
+
last_upload_time = store.last_upload_timestamp
|
|
504
|
+
|
|
505
|
+
if not self._is_upload_day(now):
|
|
506
|
+
if last_upload_time is not None and now - last_upload_time >= 7 * 86400:
|
|
507
|
+
return True, "last upload more than a week ago"
|
|
508
|
+
return False, "not upload day"
|
|
509
|
+
# now we're sure today is the day
|
|
510
|
+
|
|
511
|
+
# if we're in cron mode, proceed as if it's an explicit request;
|
|
512
|
+
# otherwise, only proceed if mode is "on" and we haven't uploaded yet today
|
|
513
|
+
# for this scope
|
|
514
|
+
if cron_mode:
|
|
515
|
+
return True, "cron mode upload on upload day"
|
|
516
|
+
|
|
517
|
+
if self._gc.telemetry_mode != "on":
|
|
518
|
+
return False, "telemetry mode not 'on'"
|
|
519
|
+
|
|
520
|
+
if not self._has_uploaded_today(last_upload_time, now):
|
|
521
|
+
return True, "upload day, not yet uploaded today"
|
|
522
|
+
return False, "upload day, already uploaded today"
|
|
523
|
+
|
|
524
|
+
def flush(self, *, upload_now: bool = False, cron_mode: bool = False) -> None:
|
|
525
|
+
"""
|
|
526
|
+
Flush collected telemetry data to persistent store, and upload if needed.
|
|
527
|
+
|
|
528
|
+
:param upload_now: Upload data right now regardless of schedule.
|
|
529
|
+
:type upload_now: bool
|
|
530
|
+
:param cron_mode: Whether this flush is called from a cron job. If true,
|
|
531
|
+
non-upload-day uploads will be skipped, otherwise acts just like
|
|
532
|
+
explicit uploads via `ruyi telemetry upload`.
|
|
533
|
+
:type cron_mode: bool
|
|
534
|
+
"""
|
|
370
535
|
|
|
371
536
|
# We may be self-uninstalling and purging all state data, and in this
|
|
372
537
|
# case we don't want to record anything (thus re-creating directories).
|
|
@@ -374,31 +539,50 @@ class TelemetryProvider:
|
|
|
374
539
|
self.logger.D("discarding collected telemetry data")
|
|
375
540
|
return
|
|
376
541
|
|
|
377
|
-
|
|
542
|
+
now = time.time()
|
|
543
|
+
|
|
544
|
+
def should_proceed(scope: TelemetryScope) -> tuple[bool, str]:
|
|
545
|
+
return self._should_proceed_with_upload(
|
|
546
|
+
scope,
|
|
547
|
+
explicit_request=upload_now,
|
|
548
|
+
cron_mode=cron_mode,
|
|
549
|
+
now=now,
|
|
550
|
+
)
|
|
551
|
+
|
|
552
|
+
if self.minimal:
|
|
553
|
+
if not self._upload_on_exit:
|
|
554
|
+
self.logger.D("skipping upload for non-first-run in minimal mode")
|
|
555
|
+
return
|
|
556
|
+
|
|
557
|
+
for scope, store in self._stores.items():
|
|
558
|
+
go_ahead, reason = should_proceed(scope)
|
|
559
|
+
self.logger.D(
|
|
560
|
+
f"minimal telemetry upload check for scope {scope}: go_ahead={go_ahead}, reason={reason}"
|
|
561
|
+
)
|
|
562
|
+
if not go_ahead:
|
|
563
|
+
continue
|
|
564
|
+
store.upload_minimal()
|
|
565
|
+
return
|
|
378
566
|
|
|
379
567
|
for scope, store in self._stores.items():
|
|
568
|
+
self.logger.D(f"flushing telemetry to persistent store for scope {scope}")
|
|
380
569
|
store.persist(now)
|
|
381
570
|
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
if not (upload_now or self._upload_on_exit) and (
|
|
388
|
-
self.local_mode
|
|
389
|
-
or not self.is_upload_day(now)
|
|
390
|
-
or self.has_uploaded_today(scope, now)
|
|
391
|
-
):
|
|
571
|
+
go_ahead, reason = should_proceed(scope)
|
|
572
|
+
self.logger.D(
|
|
573
|
+
f"regular telemetry upload check for scope {scope}: go_ahead={go_ahead}, reason={reason}"
|
|
574
|
+
)
|
|
575
|
+
if not go_ahead:
|
|
392
576
|
continue
|
|
393
577
|
|
|
394
|
-
self.
|
|
395
|
-
|
|
578
|
+
self._prepare_data_for_upload(store)
|
|
579
|
+
store.upload_staged_payloads()
|
|
396
580
|
|
|
397
|
-
def
|
|
581
|
+
def _prepare_data_for_upload(self, store: TelemetryStore) -> None:
|
|
398
582
|
installation_data: NodeInfo | None = None
|
|
399
583
|
if store.scope.is_pm:
|
|
400
584
|
try:
|
|
401
|
-
installation_data = self.
|
|
585
|
+
installation_data = self._read_installation_data()
|
|
402
586
|
except FileNotFoundError:
|
|
403
587
|
# should not happen due to is_upload_day() initializing it for us
|
|
404
588
|
# beforehand, but proceed without node info nonetheless
|
|
@@ -406,28 +590,32 @@ class TelemetryProvider:
|
|
|
406
590
|
|
|
407
591
|
return store.prepare_data_for_upload(installation_data)
|
|
408
592
|
|
|
409
|
-
def upload_staged_payloads(self, store: TelemetryStore) -> None:
|
|
410
|
-
if self.local_mode:
|
|
411
|
-
return
|
|
412
|
-
|
|
413
|
-
return store.upload_staged_payloads()
|
|
414
|
-
|
|
415
593
|
def oobe_prompt(self) -> None:
|
|
416
594
|
"""Ask whether the user consents to a first-run telemetry upload, and
|
|
417
595
|
persist the user's exact telemetry choice."""
|
|
418
596
|
|
|
597
|
+
if self._gc.is_telemetry_optout:
|
|
598
|
+
# user has already explicitly opted out via the environment variable,
|
|
599
|
+
# don't bother asking
|
|
600
|
+
return
|
|
601
|
+
|
|
602
|
+
# We always report installation info on first run, regardless of
|
|
603
|
+
# user's telemetry choice. In case the user opts out, only do a one-time
|
|
604
|
+
# upload now, and never upload anything again.
|
|
605
|
+
self._upload_on_exit = True
|
|
606
|
+
|
|
419
607
|
from ..cli import user_input
|
|
420
608
|
|
|
421
|
-
self.logger.stdout(TELEMETRY_CONSENT_AND_UPLOAD_DESC)
|
|
609
|
+
self.logger.stdout(_(TELEMETRY_CONSENT_AND_UPLOAD_DESC))
|
|
422
610
|
if not user_input.ask_for_yesno_confirmation(
|
|
423
611
|
self.logger,
|
|
424
|
-
TELEMETRY_CONSENT_AND_UPLOAD_PROMPT,
|
|
612
|
+
_(TELEMETRY_CONSENT_AND_UPLOAD_PROMPT),
|
|
425
613
|
False,
|
|
426
614
|
):
|
|
427
615
|
# ask if the user wants to opt out entirely
|
|
428
616
|
if user_input.ask_for_yesno_confirmation(
|
|
429
617
|
self.logger,
|
|
430
|
-
TELEMETRY_OPTOUT_PROMPT,
|
|
618
|
+
_(TELEMETRY_OPTOUT_PROMPT),
|
|
431
619
|
False,
|
|
432
620
|
):
|
|
433
621
|
set_telemetry_mode(self._gc, "off")
|
|
@@ -441,4 +629,3 @@ class TelemetryProvider:
|
|
|
441
629
|
|
|
442
630
|
consent_time = datetime.datetime.now().astimezone()
|
|
443
631
|
set_telemetry_mode(self._gc, "on", consent_time)
|
|
444
|
-
self._upload_on_exit = True
|