1717# along with this program; if not, write to the Free Software Foundation,
1818# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
1919#
20+ from enum import Enum
2021import json
22+ import logging
23+ from operator import le
2124import pathlib
22- from typing import Optional
25+ from threading import Thread
26+ from typing import IO , Callable , Optional
27+
28+ from dataclasses import dataclass
2329
2430import pysonar_scanner .api as api
2531
3036from subprocess import Popen , PIPE
3137
3238
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+
33113class ScannerEngineProvisioner :
34114 def __init__ (self , api : SonarQubeApi , cache : Cache ):
35115 self .api = api
@@ -71,7 +151,8 @@ def run(self, config: dict[str, any]):
71151 jre_path = self .__resolve_jre (config )
72152 scanner_engine_path = self .__fetch_scanner_engine ()
73153 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 ()
75156
76157 def __build_command (self , jre_path : JREResolvedPath , scanner_engine_path : pathlib .Path ) -> list [str ]:
77158 cmd = []
@@ -80,35 +161,10 @@ def __build_command(self, jre_path: JREResolvedPath, scanner_engine_path: pathli
80161 cmd .append (scanner_engine_path )
81162 return cmd
82163
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-
93164 def __config_to_json (self , config : dict [str , any ]) -> str :
94165 scanner_properties = [{"key" : k , "value" : v } for k , v in config .items ()]
95166 return json .dumps ({"scannerProperties" : scanner_properties })
96167
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-
112168 def __version_check (self ):
113169 if self .api .is_sonar_qube_cloud ():
114170 return
0 commit comments