17
17
# along with this program; if not, write to the Free Software Foundation,
18
18
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19
19
#
20
+ from enum import Enum
20
21
import json
22
+ import logging
23
+ from operator import le
21
24
import pathlib
22
- from typing import Optional
25
+ from threading import Thread
26
+ from typing import IO , Callable , Optional
27
+
28
+ from dataclasses import dataclass
23
29
24
30
import pysonar_scanner .api as api
25
31
30
36
from subprocess import Popen , PIPE
31
37
32
38
39
+ @dataclass (frozen = True )
40
+ class LogLine :
41
+ level : str
42
+ message : str
43
+ stacktrace : Optional [str ] = None
44
+
45
+ def get_logging_level (self ) -> int :
46
+ if self .level == "ERROR" :
47
+ return logging .ERROR
48
+ if self .level == "WARN" :
49
+ return logging .WARNING
50
+ if self .level == "INFO" :
51
+ return logging .INFO
52
+ if self .level == "DEBUG" :
53
+ return logging .DEBUG
54
+ if self .level == "TRACE" :
55
+ return logging .DEBUG
56
+ return logging .INFO
57
+
58
+
59
+ def parse_log_line (line : str ) -> LogLine :
60
+ try :
61
+ line_json = json .loads (line )
62
+ level = line_json .get ("level" , "INFO" )
63
+ message = line_json .get ("message" , line )
64
+ stacktrace = line_json .get ("stacktrace" )
65
+ return LogLine (level = level , message = message , stacktrace = stacktrace )
66
+ except json .JSONDecodeError :
67
+ return LogLine (level = "INFO" , message = line , stacktrace = None )
68
+
69
+
70
+ def default_log_line_listener (log_line : LogLine ):
71
+ logging .log (log_line .get_logging_level (), log_line .message )
72
+ if log_line .stacktrace is not None :
73
+ logging .log (log_line .get_logging_level (), log_line .stacktrace )
74
+
75
+
76
+ class CmdExecutor :
77
+ def __init__ (
78
+ self ,
79
+ cmd : list [str ],
80
+ properties_str : str ,
81
+ log_line_listener : Callable [[LogLine ], None ] = default_log_line_listener ,
82
+ ):
83
+ self .cmd = cmd
84
+ self .properties_str = properties_str
85
+ self .log_line_listener = log_line_listener
86
+
87
+ def execute (self ):
88
+ process = Popen (self .cmd , stdin = PIPE , stdout = PIPE , stderr = PIPE )
89
+ process .stdin .write (self .properties_str .encode ())
90
+ process .stdin .close ()
91
+
92
+ output_thread = Thread (target = self .__log_output , args = (process .stdout ,))
93
+ error_thread = Thread (target = self .__log_output , args = (process .stderr ,))
94
+
95
+ return self .__process_output (output_thread , error_thread , process )
96
+
97
+ def __log_output (self , stream : IO [bytes ]):
98
+ for line in stream :
99
+ decoded_line = line .decode ("utf-8" ).rstrip ()
100
+ log_line = parse_log_line (decoded_line )
101
+ self .log_line_listener (log_line )
102
+
103
+ def __process_output (self , output_thread : Thread , error_thread : Thread , process : Popen ) -> int :
104
+ output_thread .start ()
105
+ error_thread .start ()
106
+ process .wait ()
107
+ output_thread .join ()
108
+ error_thread .join ()
109
+
110
+ return process .returncode
111
+
112
+
33
113
class ScannerEngineProvisioner :
34
114
def __init__ (self , api : SonarQubeApi , cache : Cache ):
35
115
self .api = api
@@ -71,7 +151,8 @@ def run(self, config: dict[str, any]):
71
151
jre_path = self .__resolve_jre (config )
72
152
scanner_engine_path = self .__fetch_scanner_engine ()
73
153
cmd = self .__build_command (jre_path , scanner_engine_path )
74
- return self .__execute_scanner_engine (config , cmd )
154
+ properties_str = self .__config_to_json (config )
155
+ return CmdExecutor (cmd , properties_str ).execute ()
75
156
76
157
def __build_command (self , jre_path : JREResolvedPath , scanner_engine_path : pathlib .Path ) -> list [str ]:
77
158
cmd = []
@@ -80,35 +161,10 @@ def __build_command(self, jre_path: JREResolvedPath, scanner_engine_path: pathli
80
161
cmd .append (scanner_engine_path )
81
162
return cmd
82
163
83
- def __execute_scanner_engine (self , config : dict [str , any ], cmd : list [str ]) -> int :
84
- popen = Popen (cmd , stdout = PIPE , stderr = PIPE , stdin = PIPE )
85
- outs , _ = popen .communicate (self .__config_to_json (config ).encode ())
86
- exitcode = popen .wait () # 0 means success
87
- self .__extract_errors_from_log (outs )
88
- if exitcode != 0 :
89
- errors = self .__extract_errors_from_log (outs )
90
- raise RuntimeError (f"Scan failed with exit code { exitcode } " , errors )
91
- return exitcode
92
-
93
164
def __config_to_json (self , config : dict [str , any ]) -> str :
94
165
scanner_properties = [{"key" : k , "value" : v } for k , v in config .items ()]
95
166
return json .dumps ({"scannerProperties" : scanner_properties })
96
167
97
- def __extract_errors_from_log (self , outs : str ) -> list [str ]:
98
- try :
99
- errors = []
100
- for line in outs .decode ("utf-8" ).split ("\n " ):
101
- if line .strip () == "" :
102
- continue
103
- out_json = json .loads (line )
104
- if out_json ["level" ] == "ERROR" :
105
- errors .append (out_json ["message" ])
106
- print (f"{ out_json ['level' ]} : { out_json ['message' ]} " )
107
- return errors
108
- except Exception as e :
109
- print (e )
110
- return []
111
-
112
168
def __version_check (self ):
113
169
if self .api .is_sonar_qube_cloud ():
114
170
return
0 commit comments