-
Notifications
You must be signed in to change notification settings - Fork 39
/
Copy pathtroubleshooting.py
488 lines (389 loc) · 14.8 KB
/
troubleshooting.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
# Copyright 2023-2024 The MathWorks, Inc.
# Script to help users troubleshoot common errors with the environment
# This script is designed to be used in standalone manner and to maintain
# that, it doesn't use utility functions present in the parent repository.
import os
import platform
import shutil
import subprocess
from contextlib import suppress
from dataclasses import dataclass
from pathlib import Path
from typing import Final
GREEN_OK: Final = "\033[32mOK\033[0m" if os.name != "nt" else "ok"
RED_X: Final = "\033[31m X\033[0m" if os.name != "nt" else " X"
OS_TYPE: Final = platform.system()
def list_matlab():
title, key = "MATLAB", "matlab"
os_filter = OSFilter(OS_TYPE, key)
handlers = [FindExecutableHandler(key)]
status = EnvInfo(os_filter, TitleHandler(title), handlers)
return status.print()
def list_matlab_proxy_on_path():
title, key = "matlab-proxy-app", "matlab-proxy-app"
os_filter = OSFilter(OS_TYPE, key)
handlers = [FindExecutableHandler(key)]
status = EnvInfo(os_filter, TitleHandler(title), handlers)
return status.print()
def list_jupyter_executable():
title, key = "Jupyter", "jupyter"
os_filter = OSFilter(OS_TYPE, key)
handlers = [FindExecutableHandler(key)]
status = EnvInfo(os_filter, TitleHandler(title), handlers)
return status.print()
def check_python_and_pip_installed():
title, key = (
"Python and pip executables",
"python/pip",
)
os_filter = OSFilter(OS_TYPE, key)
handlers = [
CommandVersionHandler("python"),
CommandVersionHandler("pip"),
CommandVersionHandler("python3"),
]
status = EnvInfo(os_filter, TitleHandler(title), handlers)
return status.print()
def os_info():
title = "OS information"
header = generate_header(title)
os_data = [
platform.system(),
platform.release(),
platform.platform(),
platform.uname(),
"\n",
]
return header + "\n".join(f"{cmd}" for cmd in os_data)
def list_installed_packages():
title = "Installed packages"
key = "packages"
os_filter = OSFilter(OS_TYPE, key)
handlers = [CommandProviderHandler(key, OS_TYPE)]
env_info = EnvInfo(os_filter, TitleHandler(title), handlers)
return env_info.print()
def list_xvfb() -> str:
title, key = "Xvfb", "Xvfb"
os_filter = OSFilter(OS_TYPE, key)
handlers = [FindExecutableHandler(key)]
status = EnvInfo(os_filter, TitleHandler(title), handlers)
return status.print()
def list_conda_related_information() -> str:
title, key = "Conda information", "conda"
os_filter = OSFilter(OS_TYPE, key)
handlers = [CommandVersionHandler(key)]
status = EnvInfo(os_filter, TitleHandler(title), handlers)
return status.print()
def list_server_extensions():
title, key = (
"Jupyter server extensions",
"extensions",
)
os_filter = OSFilter(OS_TYPE, key)
handlers = [
CommandOutputHandler(key, "jupyter server extension list"),
CommandOutputHandler(key, "jupyter labextension list"),
]
status = EnvInfo(os_filter, TitleHandler(title), handlers)
return status.print()
def list_env_vars() -> str:
title = "Environment variables"
key = "Env"
os_filter = OSFilter(OS_TYPE, key)
handlers = [CommandProviderHandler(key, OS_TYPE)]
env_info = EnvInfo(os_filter, TitleHandler(title), handlers)
return env_info.print()
class EnvInfo:
"""Base class that contains filter to determine execution
and handlers for specific tasks"""
def __init__(self, filter, title_handler, handlers: list) -> None:
self.filter = filter
self.title_handler = title_handler
self.handlers = handlers
def print(self):
if self.filter.filter():
title = self.title_handler.execute()
output = "\n".join(h.execute() for h in self.handlers if h.execute())
return "\n".join([title, output]) if output else ""
else:
return ""
# Filters
class OSFilter:
"""Helps filter out commands on the basis of platform"""
def __init__(self, os_type: str, key: str) -> None:
self.os_type = os_type
self.key = key
self.data = {
"Xvfb": {"Linux": True, "Windows": False, "Darwin": False},
"Env": {"Linux": True, "Windows": True, "Darwin": True},
"conda": {"Linux": True, "Windows": True, "Darwin": True},
"matlab": {"Linux": True, "Windows": True, "Darwin": True},
"matlab-proxy-app": {"Linux": True, "Windows": True, "Darwin": True},
"jupyter": {"Linux": True, "Windows": True, "Darwin": True},
"python/pip": {"Linux": True, "Windows": True, "Darwin": True},
"os": {"Linux": True, "Windows": True, "Darwin": True},
"packages": {"Linux": True, "Windows": True, "Darwin": True},
"extensions": {"Linux": True, "Windows": True, "Darwin": True},
}
def filter(self):
return self.data[self.key][self.os_type]
class OptionalFilter:
"""Provides error icons and recommendation based on optionality"""
def __init__(self, key: str) -> None:
self.key: str = key
self.data = {
"Xvfb": False,
"Env": True,
"conda": True,
"matlab": False,
"matlab-proxy-app": False,
"jupyter": False,
"python": False,
"pip": False,
"python3": False,
"packages": True,
"extensions": True,
}
def filter(self) -> bool:
return self.data[self.key]
class CommandProvider:
"""Provides OS-specific command alternatives"""
def __init__(self, os_type: str, key: str) -> None:
self.os_type = os_type
self.key = key
self.data = {
"Env": {
"Linux": 'env | grep -iE "^[^=]*matlab|^[^=]*mw_|^[^=]*mwi_"',
"Windows": 'set | findstr /IR "^[^=]*matlab ^[^=]*mw_ ^[^=]*mwi_"',
"Darwin": 'env | grep -iE "^[^=]*matlab|^[^=]*mw_|^[^=]*mwi_"',
},
"packages": {
"Linux": 'python -m pip list | grep -E "jupyter|matlab-proxy|jupyter-matlab-proxy|notebook"',
"Windows": 'python -m pip list | findstr /IR "jupyter matlab-proxy jupyter-matlab-proxy notebook"',
"Darwin": 'python -m pip list | grep -E "jupyter|matlab-proxy|jupyter-matlab-proxy|notebook"',
},
}
def get_command(self):
result = self.data.get(self.key)
return result if result is None else result[self.os_type]
# handlers
class TitleHandler:
"""Returns the title for the command block"""
def __init__(self, title: str) -> None:
self.title = title
def execute(self):
return generate_header(self.title)
class CommandProviderHandler:
"""Handler to apply the command provider pattern to the desired commands"""
def __init__(self, key: str, os_type: str) -> None:
self.key = key
self.os_type = os_type
def execute(self):
is_suggestion_optional = OptionalFilter(self.key).filter()
# Get the command from the command provider
cmd_provider = CommandProvider(self.os_type, self.key)
cmd = cmd_provider.get_command()
rep: Report = process_output(exec_command, is_suggestion_optional, cmd)
return str(rep)
class CommandVersionHandler:
"""Handler to apply the command version pattern to the desired commands"""
def __init__(self, key: str) -> None:
self.key = key
def execute(self) -> str:
is_suggestion_optional = OptionalFilter(self.key).filter()
outputs = find_executable_and_version(self.key, is_suggestion_optional)
return "".join(str(op) for op in outputs)
class CommandOutputHandler:
"""Handler to apply the command output pattern to the desired commands"""
def __init__(self, key: str, cmd: str) -> None:
self.key = key
self.cmd = cmd
def execute(self):
is_suggestion_optional = OptionalFilter(self.key).filter()
rep: Report = process_output(exec_command, is_suggestion_optional, self.cmd)
return str(rep)
class FindExecutableHandler:
"""Handler to apply the find executable pattern to the desired commands"""
def __init__(self, key: str) -> None:
self.key = key
def execute(self):
is_suggestion_optional = OptionalFilter(self.key).filter()
rep = process_output(find_executable, is_suggestion_optional, self.key)
return str(rep)
@dataclass
class cmd_output:
"""A class to store the output generated by running system commands
Returns:
object: returns an object with overridden magic str method
"""
command: str
output: str
isError: bool
def __str__(self) -> str:
return f"{self.command} - {self.output} "
@dataclass
class cmd_only_output(cmd_output):
"""Subclass to cmd_output which overrides the magic str method differently"""
def __str__(self) -> str:
return f"{self.output} "
@dataclass
class Report:
body: str
recommendations: str
has_error: bool
err_icon: str
def __str__(self) -> str:
return "".join([self.body, self.err_icon, "\n", self.recommendations])
def find_executable(*args) -> list:
"""Runs which command (or OS type equivalent of which) to find the executable on path
Returns:
output: A list of outputs of the executed commands
"""
output: list = []
for name in args:
executable_path = shutil.which(name)
real_path = real_executable_path = None
# Using readlink to find the actual path of the executable, if one exists
with suppress(OSError):
if executable_path is not None:
real_path = os.readlink(Path(executable_path).parent.absolute())
real_executable_path = Path(real_path, name)
output.append(
cmd_output(
name,
(
real_executable_path
if real_executable_path is not None
else executable_path
),
True if executable_path is None else False,
)
)
return output
def process_output(func, is_suggestion_optional, *args) -> Report:
"""Higher-order helper function that calls find_executable/exec_command and primes the output for reporting
Args:
func (*args): Expects either of find_executable or exec_command functions as input
suppress_suggestions (bool): option to suppress suggestions shown to the user
Returns:
Report: An object of dataclass Report that contains information like command output,
errors, recommendations to be made to the users.
"""
body: str = ""
error: bool = False
err_recommendation: str = ""
err_icon = ""
for cmd_pair in func(*args):
body = str(cmd_pair)
if cmd_pair.output is None:
error = True
# Branch where we want to display suggestions and validation status
if not is_suggestion_optional:
if error:
err_recommendation = f"Recommendation: {cmd_pair.command} is not installed. Please install {cmd_pair.command}.\n"
err_icon = RED_X
else:
err_icon = GREEN_OK
rep = Report(
body=body,
recommendations=err_recommendation,
has_error=error,
err_icon=err_icon,
)
return rep
def generate_header(title: str) -> str:
"""Generates a prettified header block for all individual pieces of information
Args:
title (str): main headline of the block
Returns:
str: prettified string
"""
return (
str(
prettify(
boundary_filler="-",
text_arr=[f"{title}"],
)
)
+ "\n"
)
def prettify(boundary_filler=" ", text_arr=[]) -> str:
"""Prettify array of strings with borders for stdout
Args:
boundary_filler (str, optional): Upper and lower border filler for text. Defaults to " ".
text_arr (list, optional):The text array to prettify. Each element will be added to a newline. Defaults to [].
Returns:
[str]: Prettified String
"""
import sys
if not sys.stdout.isatty():
return (
"\n============================\n"
+ "\n".join(text_arr)
+ "\n============================\n"
)
size = os.get_terminal_size()
cols, _ = size.columns, size.lines
if any(len(text) > cols for text in text_arr):
result = ""
for text in text_arr:
result += text + "\n"
return result
upper = "\n" + "".ljust(cols, boundary_filler) + "\n" if len(text_arr) > 0 else ""
lower = "".ljust(cols, boundary_filler) if len(text_arr) > 0 else ""
content = ""
for text in text_arr:
content += text.center(cols) + "\n"
result = upper + content + lower
return result
def exec_command(*args) -> list:
"""A utility to run a custom command and gather output.
Returns:
list: of outputs that are returned by the function
"""
output: list = []
for cmd in args:
try:
completed_process = subprocess.run(
cmd, shell=True, capture_output=True, timeout=10, text=True
)
output.append(
cmd_only_output(
cmd,
completed_process.stdout.strip() + completed_process.stderr.strip(),
False if completed_process.stderr == "" else True,
)
)
except TimeoutError:
output.append(cmd_only_output(cmd, f"{cmd} command timed out!", True))
return output
def find_executable_and_version(cmd: str, is_suggestions_optional: bool) -> list:
"""A helper function to execute which command along with --version option
Args:
cmd (str): command to be executed
is_suggestions_optional (bool): provides an option to suppress suggestions that are
displayed to the user.
Returns:
list: containing which command output and the version information output
"""
output: list = []
version = ""
rep = process_output(find_executable, is_suggestions_optional, cmd)
output.append(rep)
if not rep.has_error:
# Suppressing suggestions/error icon for version command
version = process_output(exec_command, True, f"{cmd} --version")
output.append("\n" + str(version))
return output
output: str = ""
output += list_matlab()
output += list_matlab_proxy_on_path()
output += list_jupyter_executable()
output += check_python_and_pip_installed()
output += list_xvfb()
output += os_info()
output += list_conda_related_information()
output += list_installed_packages()
output += list_server_extensions()
output += list_env_vars()
print(output)