Improved singbox cleanup process. And added support for killing wireguard profiles inside end_singbox.
This commit is contained in:
parent
ba3ce97e34
commit
6107a355f9
7 changed files with 155 additions and 198 deletions
|
|
@ -1,5 +1,5 @@
|
|||
from core.models.system.SystemState import SystemState
|
||||
|
||||
from core.models.enum_models import ProtocolChoice
|
||||
|
||||
class SystemStateController:
|
||||
|
||||
|
|
@ -12,11 +12,11 @@ class SystemStateController:
|
|||
return SystemState.exists()
|
||||
|
||||
@staticmethod
|
||||
def create(profile_id: int, firewalled: bool, dns_set: bool, process_id: int = None) -> SystemState:
|
||||
def create(profile_id: int, firewalled: bool, dns_set: bool, protocol: ProtocolChoice, process_id: int = None) -> SystemState:
|
||||
if process_id:
|
||||
current_state = SystemState(profile_id, firewalled, dns_set, process_id)
|
||||
current_state = SystemState(profile_id, firewalled, dns_set, protocol, process_id)
|
||||
else:
|
||||
current_state = SystemState(profile_id, firewalled, dns_set)
|
||||
current_state = SystemState(profile_id, firewalled, dns_set, protocol)
|
||||
current_state.save()
|
||||
return current_state
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
from core.models.enum_models import ProtocolChoice
|
||||
from core.Constants import Constants
|
||||
from core.utils.basic_operations.enum_tools import serialize_enums
|
||||
|
||||
# generic
|
||||
from dataclasses import dataclass
|
||||
|
|
@ -22,7 +23,12 @@ class SystemState:
|
|||
|
||||
def save(self: Self):
|
||||
|
||||
system_state_file_contents = f'{self.to_json(indent=4)}\n'
|
||||
data_as_dict = self.to_json(indent=4)
|
||||
print(f"data_as_dict is {data_as_dict}")
|
||||
data_without_enums = serialize_enums(data_as_dict)
|
||||
print(f"data_without_enums is {data_without_enums}")
|
||||
system_state_file_contents = f'{data_without_enums}\n'
|
||||
|
||||
os.makedirs(SystemState.__get_state_path(), exist_ok=True, mode=0o700)
|
||||
|
||||
system_state_file_path = f'{SystemState.__get_state_path()}/system.json'
|
||||
|
|
@ -31,6 +37,8 @@ class SystemState:
|
|||
with open(system_state_file_path, 'w') as system_state_file:
|
||||
system_state_file.write(system_state_file_contents)
|
||||
|
||||
print("saved system state")
|
||||
|
||||
@staticmethod
|
||||
def get():
|
||||
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ from core.errors.logger import logger
|
|||
from core.services.networking.systemwide.encrypted_proxy import singbox
|
||||
from core.utils.basic_operations import process_tools
|
||||
from core.utils.run_commands import run_generic_command
|
||||
from core.utils.type_checker import type_checker
|
||||
|
||||
from typing import Callable, cast, Optional
|
||||
import time
|
||||
|
|
@ -14,37 +15,8 @@ import time
|
|||
KILL_WAIT_TIME = 1.3
|
||||
APP_NAME = 'sing-box'
|
||||
|
||||
def _try_with_permission_fallback(operation: Callable, interface: str) -> Result:
|
||||
"""Execute operation; if denied, check if tunnel is still active."""
|
||||
try:
|
||||
return operation()
|
||||
except SudoScript as e:
|
||||
logger.error(f"The Sudo Scripts giving power to kill this are being denied permission, before we flag this, let's see if the proxy is active, which does NOT need permission to check, {e}")
|
||||
existance = interface_tools.exists(interface)
|
||||
if not existance.valid and existance.error_type == ResultError.INTERFACE:
|
||||
not_existing = "Permission denied, but the proxy interface is down, so this is acceptable"
|
||||
logger.info(not_existing)
|
||||
return Result(valid=True, message=not_existing)
|
||||
error_msg = "Critical Issue, the tunnel is up, but we lack permission to take it down."
|
||||
logger.error(error_msg)
|
||||
return Result(valid=False, error_type=ResultError.PERMISSION, message=error_msg)
|
||||
|
||||
|
||||
def _escalate_kill(pid: int) -> Result:
|
||||
"""Graceful → wait → force kill escalation."""
|
||||
if not process_tools.is_running(pid):
|
||||
return Result(valid=True, message=f"PID {pid} already down")
|
||||
|
||||
graceful = singbox.turn_off(pid)
|
||||
time.sleep(KILL_WAIT_TIME)
|
||||
|
||||
if not process_tools.is_running(pid):
|
||||
return Result(valid=True, message=f"PID {pid} gracefully closed")
|
||||
|
||||
force = singbox.force_kill(pid)
|
||||
return Result(valid=force.valid, message=f"Force kill {'succeeded' if force.valid else 'failed'}")
|
||||
|
||||
|
||||
@type_checker
|
||||
def get_process_id_from_state(current_state: Optional[SystemState]) -> Result:
|
||||
if current_state is None:
|
||||
return Result(valid=False, error_type=ResultError.MISSING_DATA, message="Missing the State itself.")
|
||||
|
|
@ -64,110 +36,102 @@ def get_process_id_from_state(current_state: Optional[SystemState]) -> Result:
|
|||
return Result(valid=False, error_type=ResultError.INVALID_INPUT, data=e, message="The process id is not a valid integer.")
|
||||
|
||||
|
||||
# Function is private because it leverages singbox only functionality.
|
||||
def _shut_down_by_known_process_id(process_id: int) -> Result:
|
||||
graceful_close = singbox.stop(process_id)
|
||||
if not graceful_close.valid:
|
||||
logger.error(f"Could not gracefully close it. So we'll {KILL_WAIT_TIME} seconds and kill it.")
|
||||
@type_checker
|
||||
def terminate_pid(pid: int) -> Result:
|
||||
"""
|
||||
The single source of truth for killing a process.
|
||||
Logic: Graceful Stop -> Wait -> Force Kill.
|
||||
"""
|
||||
if not process_tools.is_running(pid):
|
||||
return Result(valid=True, message=f"PID {pid} already down")
|
||||
|
||||
still_active = pid_tools.is_running(pid=process_id, process_name="sing-box")
|
||||
|
||||
if not still_active:
|
||||
return Result(valid=True, message=f"The process ID {process_id} gracefully closed or never was running to begin with.")
|
||||
# 1. Attempt Graceful Stop
|
||||
logger.info(f"Attempting graceful stop of PID {pid}...")
|
||||
singbox.stop(pid) # We don't strictly need the result here, we check is_running
|
||||
|
||||
time.sleep(KILL_WAIT_TIME)
|
||||
logger.info(f"Graceful closed failed, as {process_id} is still running. Escalating to forced killing..")
|
||||
|
||||
force_kill = singbox.force_kill(process_id)
|
||||
if not process_tools.is_running(pid):
|
||||
return Result(valid=True, message=f"PID {pid} closed gracefully")
|
||||
|
||||
if force_kill.valid:
|
||||
return Result(valid=True, message=f"The process ID {process_id} was forcefully closed.")
|
||||
else:
|
||||
return Result(valid=False, message=f"Even a force kill couldn't stop the process ID {process_id}.")
|
||||
# 2. Attempt Force Kill
|
||||
logger.warning(f"PID {pid} resisted graceful stop. Escalating to force kill...")
|
||||
force_result = singbox.force_kill(pid)
|
||||
|
||||
if force_result.valid and not process_tools.is_running(pid):
|
||||
return Result(valid=True, message=f"PID {pid} force killed successfully")
|
||||
|
||||
return Result(valid=False, message=f"PID {pid} failed to terminate even after force kill")
|
||||
|
||||
|
||||
@type_checker
|
||||
def _try_with_permission_fallback(operation: Callable, interface: str) -> Result:
|
||||
"""Wraps sudo operations; if permission is denied, check if the tunnel is already gone."""
|
||||
try:
|
||||
return operation()
|
||||
except SudoScript as e:
|
||||
logger.error(f"Sudo permission denied: {e}. Checking if interface {interface} is already down...")
|
||||
interface_output = interface_tools.get_output(interface)
|
||||
|
||||
# If the interface is gone, the permission error is irrelevant because the goal is achieved
|
||||
if not interface_output.valid and interface_output.error_type == ResultError.INTERFACE:
|
||||
return Result(valid=True, message="Permission denied, but interface is already down. Success.")
|
||||
|
||||
return Result(valid=False, error_type=ResultError.PERMISSION, message="Tunnel is UP, but sudo permissions are missing.")
|
||||
|
||||
|
||||
def _hunt_and_kill(interface_name: str) -> Result:
|
||||
"""
|
||||
Purpose:
|
||||
Kill all pids with the phrase
|
||||
|
||||
Flow:
|
||||
Gets a list of all pids with the phrase in it,
|
||||
Goes through each and tries to kill it with escalating force.
|
||||
Finally evaluates for each pid if it killed the tunnel.
|
||||
"""
|
||||
function_name = "_hunt_and_kill"
|
||||
|
||||
"""Finds all potential sing-box PIDs and kills them until the interface disappears."""
|
||||
id_results = pid_tools.get_pids_by_phrase(phase=APP_NAME)
|
||||
if id_results.valid:
|
||||
pid_data_list = id_results.data
|
||||
else:
|
||||
if not id_results.valid:
|
||||
return id_results
|
||||
|
||||
for idx, pid_info in enumerate(pid_data_list):
|
||||
pids = id_results.data
|
||||
for pid_info in pids:
|
||||
pid = pid_info['pid']
|
||||
command = pid_info['command']
|
||||
logger.info(f"Hunting: Attempting to kill PID {pid}")
|
||||
|
||||
logger.info(f"[{function_name}] Attempting to kill PID {pid}: {command}")
|
||||
graceful_or_force = _escalate_kill(pid)
|
||||
logger.info(f"Escalation of killing of pid {pid} resulted in {graceful_or_force.valid} and {graceful_or_force.message}")
|
||||
if not graceful_or_force.valid:
|
||||
logger.info("Skipping to next pid..")
|
||||
continue
|
||||
terminate_pid(int(pid))
|
||||
|
||||
if not process_tools.is_running(pid):
|
||||
logger.info(f"[{function_name}] PID {pid} force killed successfully")
|
||||
# Check if the interface finally dropped
|
||||
if not interface_tools.exists(interface=interface_name):
|
||||
return Result(valid=True, message=f"Tunnel stopped after killing PID {pid}")
|
||||
|
||||
# EVALUATION: Did killing this PID stop the app? is the interface still running?
|
||||
existance = interface_tools.exists(interface=interface_name)
|
||||
if not existance.valid:
|
||||
logger.info(f"[{function_name}] App/Tunnel is fully stopped after killing PID {pid}")
|
||||
return Result(valid=True, message=f"App stopped after killing PID {pid}")
|
||||
else:
|
||||
logger.warning(f"[{function_name}] App still running, continuing to next PID")
|
||||
continue
|
||||
|
||||
else:
|
||||
logger.error(f"[{function_name}] Could not kill PID {pid}, moving to next")
|
||||
continue
|
||||
|
||||
return Result(valid=False, error_type=ResultError.UNKNOWN, message="Could not kill any PIDs")
|
||||
return Result(valid=False, message="Hunted all matching PIDs, but interface remains active")
|
||||
|
||||
|
||||
def _try_shutdown_by_exact_match() -> Result:
|
||||
exact_match = pid_tools.get_pid_by_app_name(exact_app_name=APP_NAME)
|
||||
if exact_match.valid:
|
||||
logger.info(f"Attempting exact match PID {exact_match.data}")
|
||||
return _shut_down_by_known_process_id(cast(int, exact_match.data))
|
||||
else:
|
||||
return exact_match
|
||||
def orchestrate_closing(current_state: Optional[SystemState], interface: str) -> Result:
|
||||
"""
|
||||
Tries increasingly aggressive strategies to bring down the proxy.
|
||||
"""
|
||||
|
||||
# Strategy 1: PID from JSON state
|
||||
def try_state_pid():
|
||||
pid_res = get_process_id_from_state(current_state)
|
||||
if not pid_res.valid:
|
||||
return pid_res
|
||||
return terminate_pid(pid_res.data)
|
||||
|
||||
def _try_known_pid_from_state(current_state: Optional[SystemState]) -> Result:
|
||||
pid_query = get_process_id_from_state(current_state)
|
||||
if not pid_query.valid:
|
||||
return pid_query
|
||||
return _shut_down_by_known_process_id(cast(int, pid_query.data))
|
||||
# Strategy 2: Exact name match (pgrep -x)
|
||||
def try_exact_match():
|
||||
match = pid_tools.get_pid_by_app_name(exact_app_name=APP_NAME)
|
||||
if not match.valid:
|
||||
return match
|
||||
return terminate_pid(int(match.data))
|
||||
|
||||
# Strategy 3: Hunt all related processes
|
||||
def try_hunt():
|
||||
return _hunt_and_kill(interface)
|
||||
|
||||
def orchestrate_closing(current_state: SystemState, interface: str) -> Result:
|
||||
strategies = [
|
||||
lambda: _try_known_pid_from_state(current_state),
|
||||
lambda: _try_shutdown_by_exact_match(),
|
||||
lambda: _hunt_and_kill(interface),
|
||||
]
|
||||
quantity_of_strategies = len(strategies)
|
||||
strategies = [try_state_pid, try_exact_match, try_hunt]
|
||||
|
||||
for strategy in strategies:
|
||||
result = _try_with_permission_fallback(strategy, interface)
|
||||
if result.valid:
|
||||
# remove state json:
|
||||
SystemState.dissolve()
|
||||
return result
|
||||
|
||||
# Final sanity check: Did the strategy fail, but the interface is gone anyway?
|
||||
if not interface_tools.exists(interface):
|
||||
# remove state json:
|
||||
SystemState.dissolve()
|
||||
return Result(valid=True, message="Tunnel inactive despite strategy failure")
|
||||
|
||||
return Result(valid=False, message=f"All {quantity_of_strategies} strategies tried, we simply can not bring down the proxy, which is still up.")
|
||||
return Result(valid=True, message="Interface disappeared despite strategy reporting failure")
|
||||
|
||||
return Result(valid=False, message="All shutdown strategies exhausted. Proxy is still active.")
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
from core.controllers.profile_state.get_profile_data import get_protocol_enum
|
||||
from core.models.enum_models import ProtocolChoice
|
||||
from core.services.networking.general_connection_tools.cross_protocol import kill_any_protocol
|
||||
# from core.services.networking.general_connection_tools.cross_protocol import kill_any_protocol
|
||||
from core.services.networking.systemwide.encrypted_proxy import singbox
|
||||
from core.services.networking.systemwide.encrypted_proxy.close_singbox import orchestrate_closing
|
||||
from core.services.networking.systemwide.encrypted_proxy.close_singbox import orchestrate_closing, terminate_pid
|
||||
from core.services.networking.systemwide.general_tools import interface_tools
|
||||
from core.services.networking.systemwide import killswitch
|
||||
from core.services.networking.systemwide.wireguard.wg_firewall_dns import revert_dns
|
||||
|
|
@ -135,79 +135,58 @@ def _attempt_start_with_retry(profile_id: int, quantity_of_attempts: int = 2) ->
|
|||
return Result(valid=False, error_type=ResultError.PROCESS_WONT_START, message=error_msg)
|
||||
|
||||
|
||||
|
||||
def end_singbox() -> Result:
|
||||
"""Orchestrate closing singbox's process, firewall, dns, and json state."""
|
||||
|
||||
# get by systemstate:
|
||||
current_state = SystemState.get()
|
||||
|
||||
# ------------------
|
||||
# KILL ZOMBIE SINGBOX NOT TIED TO PROFILES
|
||||
# ------------------
|
||||
if not current_state:
|
||||
# Make sure there's no singbox id:
|
||||
singbox_pid = pid_tools.get_pid_by_app_name(exact_app_name="sing-box")
|
||||
|
||||
if singbox_pid is None:
|
||||
return Result(valid=True, message="Already was disabled.")
|
||||
|
||||
if not singbox_pid.valid:
|
||||
return Result(valid=True, message="Already was disabled.")
|
||||
|
||||
singbox_pid = singbox_pid.data
|
||||
|
||||
if not singbox_pid or singbox_pid is None:
|
||||
return Result(valid=True, message="Already was disabled or singbox showing no pid")
|
||||
|
||||
logger.error(f"Critical Issue! State JSON is blank, but Singbox is still running. {singbox_pid}")
|
||||
|
||||
|
||||
# ------------------
|
||||
# KILL NON-SINGBOX SUCH AS WIREGUARD
|
||||
# ------------------
|
||||
active_protocol = get_protocol_enum(current_state.profile_id)
|
||||
logger.info(f"State JSON shows there is already an active profile with id {profile_id} and protocol {active_protocol}")
|
||||
if active_protocol not in SINGBOX_PROTOCOLS:
|
||||
return kill_external_protocol(current_state=current_state, active_protocol=active_protocol)
|
||||
|
||||
# ------------------
|
||||
# KILL THE "REAL" KNOWN ACTIVE SINGBOX PROFILE
|
||||
# ------------------
|
||||
try:
|
||||
killed_existing = orchestrate_closing(
|
||||
current_state=current_state,
|
||||
interface=Constants.SINGBOX_TUN_IF
|
||||
)
|
||||
except RuntimeError as e: # Interface error raised by process_closure_tool's interface_exists
|
||||
return Result(valid=False, error_type=ResultError.INTERFACE, message=str(e))
|
||||
|
||||
if not killed_existing.valid:
|
||||
logger.error(f"Could not kill singbox because: {killed_existing.error_type}")
|
||||
return killed_existing
|
||||
|
||||
# ======== FIREWALL ===========
|
||||
disable_result_object = killswitch.disarm()
|
||||
if not disable_result_object.valid:
|
||||
error_msg = "We could not disable the firewall for singbox"
|
||||
logger.error(error_msg)
|
||||
raise FirewallError(error_msg) # bubbles to GUI
|
||||
|
||||
# ======== DNS =============
|
||||
reverting_dns_worked = revert_dns()
|
||||
if not disable_result_object.valid:
|
||||
error_msg = "We could not disable the DNS for singbox, but they might not use systemD"
|
||||
logger.error(error_msg)
|
||||
logger.info("Starting end_singbox")
|
||||
|
||||
# ======== JSON STATE ========
|
||||
# Wipe the JSON to reflect reality,
|
||||
if killed_existing.valid:
|
||||
logger.info("Successfully took down Singbox tunnel")
|
||||
SystemState.dissolve()
|
||||
return Result(valid=True)
|
||||
else:
|
||||
error_msg = "Could not delete the singbox state"
|
||||
logger.error(error_msg)
|
||||
return Result(valid=False, error_type=ResultError.SINGBOX, message=error_msg)
|
||||
current_state = SystemState.get()
|
||||
|
||||
# 1. WireGuard Cleanup. In case, it was a different protocol.
|
||||
if current_state and get_protocol_enum(current_state.profile_id) == ProtocolChoice.WIREGUARD:
|
||||
from core.services.networking.systemwide_wireguard import terminate_system_connection
|
||||
terminate_system_connection(
|
||||
firewall_setting=current_state.firewalled,
|
||||
dns_setting=current_state.dns_set
|
||||
)
|
||||
return Result(valid=True, message="Killed a wireguard process from within singbox.")
|
||||
|
||||
# 2. The Kill Attempt
|
||||
close_result = orchestrate_closing(
|
||||
current_state=current_state,
|
||||
interface=Constants.SINGBOX_TUN_IF
|
||||
)
|
||||
|
||||
# 3. System Restoration (Always do this, even if close_result is invalid)
|
||||
# Otherwise, it's possible to lock the user out of the internet
|
||||
|
||||
# ======== FIREWALL ========
|
||||
try:
|
||||
if not killswitch.disarm().valid:
|
||||
raise FirewallError("Could not disable the firewall killswitch")
|
||||
except Exception as e:
|
||||
logger.error(str(e))
|
||||
raise FirewallError(str(e))
|
||||
logger.info("Finished with killswitch")
|
||||
|
||||
# ======== DNS ========
|
||||
dns_result = revert_dns()
|
||||
if not dns_result.valid:
|
||||
logger.warning(f"DNS reversion failed: {dns_result.message}")
|
||||
|
||||
# 4. The "Gatekeeper" Check
|
||||
# Now that the system is safe (DNS/Firewall restored), we check if the process actually died.
|
||||
if not close_result.valid:
|
||||
logger.error("The process is still running; keeping JSON state for diagnostic/retry.")
|
||||
return close_result
|
||||
|
||||
# 5. Bookkeeping
|
||||
# Only dissolve the state if the process is confirmed dead.
|
||||
SystemState.dissolve()
|
||||
logger.info("Sing-box cleanup sequence complete.")
|
||||
return Result(valid=True)
|
||||
|
||||
|
||||
def start_singbox(
|
||||
|
|
@ -236,7 +215,7 @@ def start_singbox(
|
|||
requirements = [profile_id, server_ip]
|
||||
for each_requirement in requirements:
|
||||
if each_requirement is None:
|
||||
return Result(valid=False, error_type=ResultError.INVALID_INPUT, message=f"Invalid inputs into singbox runner")
|
||||
return Result(valid=False, error_type=ResultError.INVALID_INPUT, message="Invalid inputs into singbox runner")
|
||||
logger.info("All requirements met.")
|
||||
|
||||
# ========== KILL IT IF ALREADY UP ==========
|
||||
|
|
@ -306,9 +285,10 @@ def start_singbox(
|
|||
profile_id=profile_id,
|
||||
firewalled=True, # intended setting, not result yet
|
||||
dns_set=True, # intended setting, not result yet
|
||||
protocol=get_protocol_enum(profile_id),
|
||||
process_id=process_id
|
||||
)
|
||||
logger.info(f"State is created.")
|
||||
logger.info("State is created.")
|
||||
|
||||
# ============= CONFIRM IP ADDRESS =============
|
||||
ip_result = ip.test(connection_observer)
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@ def exists(interface: str) -> bool:
|
|||
def get_output(interface: str) -> Result:
|
||||
command = ['ip', 'link', 'show', interface]
|
||||
human_readable_goal = "Checking if the interface exists"
|
||||
result = run_generic_command(command, human_readable_goal, timeout=5)
|
||||
result = run_generic_command(command, human_readable_goal, timeout=5, allow_blank=True)
|
||||
|
||||
if not result.valid:
|
||||
return Result(valid=False, error_type=ResultError.NOT_SUPPORTED, message=f"We could not run the command to even check the interface. {result.message}")
|
||||
|
|
|
|||
|
|
@ -27,17 +27,20 @@ def is_running(pid: int, process_name: str) -> bool:
|
|||
return False
|
||||
|
||||
# Function is public because it works for any app
|
||||
def get_pid_by_app_name(exact_app_name: str) -> Result:
|
||||
def get_pid_by_app_name(exact_app_name: str, allow_blank: bool = False) -> Result:
|
||||
command = ['pgrep', '-x', exact_app_name]
|
||||
human_readable_goal = "Get a pid by an exact match get_pid_by_app_name"
|
||||
result = run_generic_command(command, human_readable_goal, timeout=7)
|
||||
result = run_generic_command(command, human_readable_goal, timeout=7, allow_blank=allow_blank)
|
||||
print(f"pgrep result: {result}")
|
||||
|
||||
if result.valid: # equivalent: (returncode == 0):
|
||||
print("pid command ran")
|
||||
try:
|
||||
process_id = int(result.data.strip()) # Strip newline, convert to int
|
||||
process_id = result.data.strip() # Strip newline, convert to int
|
||||
print(f"process_id is {process_id}, returning true regardless..")
|
||||
return Result(valid=True, data=process_id)
|
||||
except ValueError:
|
||||
return Result(valid=False, error_type=ResultError.INVALID_INPUT, message="Could not parse PID")
|
||||
except ValueError as e:
|
||||
return Result(valid=False, error_type=ResultError.INVALID_INPUT, message=f"Could not parse PID: {str(e)}")
|
||||
else:
|
||||
return Result(valid=False, error_type=ResultError.NOT_SUPPORTED, message="pgrep failed")
|
||||
|
||||
|
|
|
|||
|
|
@ -134,7 +134,8 @@ def _run_command_via_terminal(
|
|||
def run_generic_command(
|
||||
command: list,
|
||||
human_readable_goal: str,
|
||||
timeout: float | None = None
|
||||
timeout: float | None = None,
|
||||
allow_blank: bool = False
|
||||
) -> Result:
|
||||
|
||||
output_data = None
|
||||
|
|
@ -149,7 +150,7 @@ def run_generic_command(
|
|||
|
||||
# output_data = stdout
|
||||
|
||||
if returncode == 0:
|
||||
if returncode == 0 or allow_blank:
|
||||
logger.info(f"{human_readable_goal} was successful")
|
||||
return Result(valid=True, data=stdout)
|
||||
else:
|
||||
|
|
@ -189,6 +190,7 @@ def parse_errors(error_output: str) -> ResultError:
|
|||
return ResultError.MTU
|
||||
|
||||
else:
|
||||
print(f"error_output is {error_output}")
|
||||
return ResultError.UNKNOWN
|
||||
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue