pysfi 0.1.11__py3-none-any.whl → 0.1.13__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.
sfi/taskkill/taskkill.py CHANGED
@@ -6,40 +6,66 @@ import logging
6
6
  import subprocess
7
7
  import sys
8
8
  from dataclasses import dataclass
9
+ from typing import Final
9
10
 
11
+ # Configure logging
10
12
  logging.basicConfig(level=logging.INFO, format="%(message)s")
11
13
  logger = logging.getLogger(__name__)
12
14
 
15
+ # Constants
16
+ DEFAULT_MATCH_THRESHOLD: Final[int] = 10
17
+ FORCE_MATCH_THRESHOLD: Final[int] = 1000
18
+ PROCESS_LIST_TIMEOUT: Final[int] = 10
19
+ ENCODING_ATTEMPTS: Final[tuple[str, ...]] = ("gbk", "utf8")
20
+
21
+ # Global state (consider refactoring to class-based approach in future)
13
22
  _cached_process_list: list[ProcessInfo] = []
14
23
  _processes_cached: bool = False
15
- _max_match_threshold: int = 10 # Maximum number of processes to match without confirmation
24
+ _max_match_threshold: int = DEFAULT_MATCH_THRESHOLD
16
25
 
17
26
 
18
27
  @dataclass
19
28
  class ProcessInfo:
20
- """Process information class."""
29
+ """Represents information about a system process.
30
+
31
+ Attributes:
32
+ name: The name of the process executable
33
+ pid: The process identifier as string
34
+ """
21
35
 
22
36
  name: str
23
37
  pid: str
24
38
 
25
39
 
26
40
  def get_matched_process(process_name: str) -> list[ProcessInfo]:
27
- """Get the list of matching processes.
41
+ """Get processes matching the given pattern.
42
+
43
+ Performs case-insensitive wildcard matching against process names.
44
+
45
+ Args:
46
+ process_name: Pattern to match against process names (supports wildcards)
28
47
 
29
48
  Returns:
30
- list[ProcessInfo]: List of matched processes
49
+ List of ProcessInfo objects that match the pattern
31
50
  """
32
- return [p for p in _cached_process_list if fnmatch.fnmatch(p.name.lower(), process_name.lower())]
51
+ pattern = process_name.lower()
52
+ return [p for p in _cached_process_list if fnmatch.fnmatch(p.name.lower(), pattern)]
33
53
 
34
54
 
35
55
  def get_process_list(force_refresh: bool = False) -> None:
36
- """Get the list of processes.
56
+ """Retrieve the current list of system processes.
57
+
58
+ Caches the process list to avoid repeated system calls. Use force_refresh=True
59
+ to bypass the cache when fresh data is required.
37
60
 
38
61
  Args:
39
- force_refresh: Force refresh the process list even if cached.
62
+ force_refresh: If True, forces refreshing the process list even if cached.
63
+ Defaults to False.
40
64
  """
41
- global _processes_cached
65
+ global _processes_cached, _cached_process_list
66
+
42
67
  if _processes_cached and not force_refresh:
68
+ logger.debug("Using cached process list")
43
69
  return
44
70
 
45
71
  _cached_process_list.clear()
@@ -48,9 +74,18 @@ def get_process_list(force_refresh: bool = False) -> None:
48
74
  else:
49
75
  _get_process_list_unix()
50
76
  _processes_cached = True
77
+ logger.debug(f"Retrieved {len(_cached_process_list)} processes")
51
78
 
52
79
 
53
80
  def _get_process_list_windows(encoding: str = "gbk") -> None:
81
+ """Retrieve process list on Windows using tasklist command.
82
+
83
+ Attempts to decode output with specified encoding, falling back to alternatives
84
+ if decoding fails.
85
+
86
+ Args:
87
+ encoding: Text encoding to try first. Defaults to "gbk".
88
+ """
54
89
  try:
55
90
  result = subprocess.run(
56
91
  ["tasklist", "/fo", "csv", "/nh"],
@@ -58,7 +93,7 @@ def _get_process_list_windows(encoding: str = "gbk") -> None:
58
93
  text=True,
59
94
  encoding=encoding,
60
95
  check=True,
61
- timeout=10, # Add timeout to prevent infinite wait
96
+ timeout=PROCESS_LIST_TIMEOUT,
62
97
  )
63
98
  logger.debug(f"Decoded using {encoding}")
64
99
  for line in result.stdout.strip().split("\n"):
@@ -69,37 +104,46 @@ def _get_process_list_windows(encoding: str = "gbk") -> None:
69
104
  pid = parts[1].strip('"')
70
105
  _cached_process_list.append(ProcessInfo(name, pid))
71
106
  except UnicodeDecodeError:
72
- logger.warning(f"Failed to decode using {encoding} encoding, trying other encoding")
73
- if encoding == "utf8":
107
+ logger.warning(
108
+ f"Failed to decode using {encoding} encoding, trying other encoding"
109
+ )
110
+ if encoding == ENCODING_ATTEMPTS[-1]:
74
111
  logger.error("All encoding attempts failed, unable to get process list")
75
112
  return
76
- # If GBK encoding fails, try UTF-8
77
- try:
78
- _get_process_list_windows(encoding="utf8")
79
- logger.debug("Decoded using utf8")
80
- except (
81
- subprocess.SubprocessError,
82
- OSError,
83
- ValueError,
84
- UnicodeDecodeError,
85
- ):
86
- logger.error("Failed to get process list")
87
- return
113
+ # Try next encoding in sequence
114
+ next_encoding_idx = ENCODING_ATTEMPTS.index(encoding) + 1
115
+ if next_encoding_idx < len(ENCODING_ATTEMPTS):
116
+ next_encoding = ENCODING_ATTEMPTS[next_encoding_idx]
117
+ try:
118
+ _get_process_list_windows(encoding=next_encoding)
119
+ logger.debug(f"Decoded using {next_encoding}")
120
+ except (
121
+ subprocess.SubprocessError,
122
+ OSError,
123
+ ValueError,
124
+ UnicodeDecodeError,
125
+ ):
126
+ logger.error("Failed to get process list after encoding fallback")
127
+ return
88
128
  except (subprocess.CalledProcessError, subprocess.TimeoutExpired) as e:
89
129
  logger.error(f"Failed to execute tasklist command: {e.__class__.__name__}")
90
130
  return
91
131
  except Exception as e:
92
- logger.error(f"Unknown error occurred while getting process list: {e.__class__.__name__}")
132
+ logger.error(
133
+ f"Unknown error occurred while getting process list: {e.__class__.__name__}"
134
+ )
93
135
  return
94
136
 
95
137
 
96
138
  def _get_process_list_unix() -> None:
139
+ """Retrieve process list on Unix-like systems using ps command."""
97
140
  try:
98
141
  result = subprocess.run(
99
142
  ["ps", "-eo", "pid,comm", "--no-headers"],
100
143
  capture_output=True,
101
144
  text=True,
102
145
  check=True,
146
+ timeout=PROCESS_LIST_TIMEOUT,
103
147
  )
104
148
  for line in result.stdout.strip().split("\n"):
105
149
  if line:
@@ -108,17 +152,26 @@ def _get_process_list_unix() -> None:
108
152
  pid = parts[0]
109
153
  name = parts[1]
110
154
  _cached_process_list.append(ProcessInfo(name, pid))
111
- except (subprocess.SubprocessError, OSError, ValueError):
112
- logger.error("Failed to get process list")
155
+ except (
156
+ subprocess.SubprocessError,
157
+ OSError,
158
+ ValueError,
159
+ subprocess.TimeoutExpired,
160
+ ) as e:
161
+ logger.error(f"Failed to get process list: {e.__class__.__name__}")
113
162
  return
114
163
 
115
164
 
116
165
  def kill_process(process_name: str, force_refresh: bool = True) -> None:
117
- """Terminate process.
166
+ """Terminate processes matching the given name or pattern.
167
+
168
+ Implements safety mechanisms including match threshold limits to prevent
169
+ accidental mass termination of processes.
118
170
 
119
171
  Args:
120
- process_name: Process name
121
- force_refresh: Force refresh process list before killing (default True)
172
+ process_name: Process name or pattern to match (supports wildcards)
173
+ force_refresh: Whether to refresh the process list before matching.
174
+ Defaults to True.
122
175
  """
123
176
  get_process_list(force_refresh=force_refresh)
124
177
  matched_processes = get_matched_process(process_name)
@@ -142,19 +195,27 @@ def kill_process(process_name: str, force_refresh: bool = True) -> None:
142
195
  logger.info(
143
196
  f"Found {match_count} process(es) matching '{process_name}': {[m.name for m in matched_processes]}",
144
197
  )
145
- try:
146
- success_count = 0
147
- for process in matched_processes:
148
- if _kill_process_by_pid(process.pid):
149
- logger.info(f"Successfully terminated process {process.name} (PID: {process.pid})")
150
- success_count += 1
151
- else:
152
- logger.info(f"Failed to terminate process {process.name} (PID: {process.pid})")
153
- except (subprocess.SubprocessError, OSError, ValueError):
154
- logger.error("Failed to terminate process")
155
- return
156
- else:
157
- logger.info(f"Successfully terminated {success_count} process(es) matching '{process_name}'")
198
+
199
+ success_count = 0
200
+ failure_count = 0
201
+
202
+ for process in matched_processes:
203
+ if _kill_process_by_pid(process.pid):
204
+ logger.info(
205
+ f"Successfully terminated process {process.name} (PID: {process.pid})"
206
+ )
207
+ success_count += 1
208
+ else:
209
+ logger.info(
210
+ f"Failed to terminate process {process.name} (PID: {process.pid})"
211
+ )
212
+ failure_count += 1
213
+
214
+ logger.info(
215
+ f"Successfully terminated {success_count} process(es) matching '{process_name}'"
216
+ )
217
+ if failure_count > 0:
218
+ logger.warning(f"Failed to terminate {failure_count} process(es)")
158
219
 
159
220
 
160
221
  def _kill_process_by_pid(pid: str) -> bool:
@@ -165,70 +226,118 @@ def _kill_process_by_pid(pid: str) -> bool:
165
226
 
166
227
 
167
228
  def _kill_process_by_pid_windows(pid: str) -> bool:
168
- """Terminate process by PID on Windows.
229
+ """Terminate process by PID on Windows using taskkill command.
230
+
231
+ Args:
232
+ pid: Process identifier as string
169
233
 
170
234
  Returns:
171
- bool: Whether the process was successfully terminated.
235
+ True if process was successfully terminated, False otherwise
172
236
  """
173
237
  try:
174
- subprocess.run(
238
+ result = subprocess.run(
175
239
  ["taskkill", "/F", "/PID", pid],
176
240
  capture_output=True,
177
241
  text=True,
178
242
  check=True,
243
+ timeout=PROCESS_LIST_TIMEOUT,
179
244
  )
245
+ logger.debug(f"taskkill output: {result.stdout.strip()}")
180
246
  return True
181
- except subprocess.CalledProcessError:
182
- logger.error(f"Failed to terminate process PID {pid}")
247
+ except subprocess.CalledProcessError as e:
248
+ logger.error(f"Failed to terminate process PID {pid}: {e}")
249
+ if e.stdout:
250
+ logger.debug(f"taskkill stdout: {e.stdout.strip()}")
251
+ if e.stderr:
252
+ logger.debug(f"taskkill stderr: {e.stderr.strip()}")
253
+ return False
254
+ except subprocess.TimeoutExpired:
255
+ logger.error(f"Timeout while terminating process PID {pid}")
183
256
  return False
184
257
 
185
258
 
186
259
  def _kill_process_by_pid_unix(pid: str) -> bool:
187
- """Terminate process by PID on Unix/Linux.
260
+ """Terminate process by PID on Unix/Linux using kill command.
261
+
262
+ Args:
263
+ pid: Process identifier as string
188
264
 
189
265
  Returns:
190
- bool: Whether the process was successfully terminated.
266
+ True if process was successfully terminated, False otherwise
191
267
  """
192
268
  try:
193
- subprocess.run(
269
+ result = subprocess.run(
194
270
  ["kill", "-9", pid],
195
271
  capture_output=True,
196
272
  text=True,
197
273
  check=True,
274
+ timeout=PROCESS_LIST_TIMEOUT,
198
275
  )
276
+ logger.debug(f"kill output: {result.stdout.strip()}")
199
277
  return True
200
- except subprocess.CalledProcessError:
201
- logger.error(f"Failed to terminate process PID {pid}")
278
+ except subprocess.CalledProcessError as e:
279
+ logger.error(f"Failed to terminate process PID {pid}: {e}")
280
+ if e.stdout:
281
+ logger.debug(f"kill stdout: {e.stdout.strip()}")
282
+ if e.stderr:
283
+ logger.debug(f"kill stderr: {e.stderr.strip()}")
284
+ return False
285
+ except subprocess.TimeoutExpired:
286
+ logger.error(f"Timeout while terminating process PID {pid}")
202
287
  return False
203
288
 
204
289
 
205
290
  def main() -> None:
206
- """Terminate process."""
207
- parser = argparse.ArgumentParser()
208
- parser.add_argument("processes", type=str, nargs="+", help="Process to terminate.")
209
- parser.add_argument("--debug", "-d", action="store_true", help="Enable debug mode.")
291
+ """Main entry point for the taskkill command-line interface.
292
+
293
+ Terminates processes by name or pattern with cross-platform support.
294
+ """
295
+ parser = argparse.ArgumentParser(
296
+ description="Cross-platform process termination utility",
297
+ epilog="Examples:\n taskk notepad.exe\n taskk python*\n taskk chrome.exe --debug\n taskk temp* --force",
298
+ )
299
+ parser.add_argument(
300
+ "processes",
301
+ type=str,
302
+ nargs="+",
303
+ help="Process name or pattern to terminate (supports wildcards)",
304
+ )
305
+ parser.add_argument(
306
+ "--debug",
307
+ "-d",
308
+ action="store_true",
309
+ help="Enable debug logging for detailed output",
310
+ )
210
311
  parser.add_argument(
211
312
  "--force",
212
313
  "-f",
213
314
  action="store_true",
214
- help="Force termination even if match count exceeds threshold.",
315
+ help="Force termination even if match count exceeds safety threshold",
215
316
  )
216
317
 
217
318
  args = parser.parse_args()
218
319
 
219
320
  if args.debug:
220
321
  logger.setLevel(logging.DEBUG)
322
+ logger.debug("Debug mode enabled")
221
323
 
222
324
  # Temporarily increase threshold if force flag is set
223
325
  global _max_match_threshold
224
326
  original_threshold = _max_match_threshold
225
327
  if args.force:
226
- _max_match_threshold = 1000 # Large number to effectively disable the limit
328
+ _max_match_threshold = FORCE_MATCH_THRESHOLD
329
+ logger.debug(
330
+ f"Force mode enabled, threshold increased to {_max_match_threshold}"
331
+ )
227
332
 
228
- # Fetch process list once for the first process, then reuse for remaining
333
+ # Process each target
229
334
  for i, process_name in enumerate(args.processes):
335
+ # Refresh process list only for the first process to avoid redundant calls
230
336
  force_refresh = i == 0
337
+ logger.info(f"Terminating processes matching: {process_name}")
231
338
  kill_process(process_name, force_refresh=force_refresh)
232
339
 
233
340
  # Restore original threshold
234
341
  _max_match_threshold = original_threshold
342
+ if args.force:
343
+ logger.debug("Restored original match threshold")
sfi/which/which.py CHANGED
@@ -18,8 +18,12 @@ logger = logging.getLogger(__name__)
18
18
  def find_executable(name: str, *, fuzzy: bool) -> tuple[str, str | None]:
19
19
  """Find executable path for commands cross-platform.
20
20
 
21
+ Args:
22
+ name: Name of the command to find
23
+ fuzzy: Whether to use fuzzy matching
24
+
21
25
  Returns:
22
- Tuple[str, Optional[str]]: Command name and path.
26
+ Tuple containing the command name and its path if found, None otherwise.
23
27
  """
24
28
  try:
25
29
  # Select command based on system
@@ -49,8 +53,12 @@ def find_executable(name: str, *, fuzzy: bool) -> tuple[str, str | None]:
49
53
  def main() -> None:
50
54
  parser = argparse.ArgumentParser(description="Find executable path for commands.")
51
55
  parser.add_argument("commands", type=str, nargs="+", help="Commands to query")
52
- parser.add_argument("-f", "--fuzzy", action="store_true", help="Enable fuzzy matching")
53
- parser.add_argument("-q", "--quiet", action="store_true", help="Only print paths, no status symbols")
56
+ parser.add_argument(
57
+ "-f", "--fuzzy", action="store_true", help="Enable fuzzy matching"
58
+ )
59
+ parser.add_argument(
60
+ "-q", "--quiet", action="store_true", help="Only print paths, no status symbols"
61
+ )
54
62
 
55
63
  args = parser.parse_args()
56
64