def mergeObjectsFunction(snapshotObj, updateObj, masterObj, snapshotBackend, workBackend, masterBackend): # pylint: disable=unused-argument,too-many-arguments masterVersions = sorted([ f"{p.productVersion}-{p.packageVersion}" for p in masterBackend.productOnDepot_getObjects( ["productVersion", "packageVersion"], productId=snapshotObj.productId, depotId=self._depotId) ]) snapshotVersions = sorted([ f"{p.productVersion}-{p.packageVersion}" for p in snapshotBackend.productOnDepot_getObjects( ["productVersion", "packageVersion"], productId=snapshotObj.productId, depotId=self._depotId) ]) logger.info( "Syncing ProductOnClient %s (product versions local=%s, server=%s)", updateObj, snapshotVersions, masterVersions) if snapshotVersions != masterVersions: logger.notice( "Product %s changed on server since last sync, not updating actionRequest (local=%s, server=%s)", snapshotObj.productId, snapshotVersions, masterVersions) updateObj.actionRequest = None updateObj.targetConfiguration = None return updateObj
def waitForClient(self): logger.trace("Creating pipe %s", self._pipeName) PIPE_ACCESS_DUPLEX = 0x3 PIPE_TYPE_MESSAGE = 0x4 PIPE_READMODE_MESSAGE = 0x2 PIPE_WAIT = 0 PIPE_UNLIMITED_INSTANCES = 255 NMPWAIT_USE_DEFAULT_WAIT = 0 INVALID_HANDLE_VALUE = -1 self._pipe = windll.kernel32.CreateNamedPipeA( self._pipeName.encode("ascii"), PIPE_ACCESS_DUPLEX, PIPE_TYPE_MESSAGE | PIPE_READMODE_MESSAGE | PIPE_WAIT, PIPE_UNLIMITED_INSTANCES, self.bufferSize, self.bufferSize, NMPWAIT_USE_DEFAULT_WAIT, None) if self._pipe == INVALID_HANDLE_VALUE: raise Exception( f"Failed to create named pipe: {windll.kernel32.GetLastError()}" ) logger.debug("Pipe %s created, waiting for client to connect", self._pipeName) # This call is blocking until a client connects fConnected = windll.kernel32.ConnectNamedPipe(self._pipe, None) if fConnected == 0 and windll.kernel32.GetLastError() == 535: # ERROR_PIPE_CONNECTED fConnected = 1 if fConnected == 1: logger.notice("Client connected to %s", self._pipeName) self._client_id += 1 return (self._pipe, f"#{self._client_id}") error = windll.kernel32.GetLastError() windll.kernel32.CloseHandle(self._pipe) raise RuntimeError(f"Failed to connect to pipe (error: {error})")
def createActionProcessorUser(self, recreate=True): if not config.get('action_processor', 'create_user'): return run_as_user = config.get('action_processor', 'run_as_user') if run_as_user.lower() == 'system': self._actionProcessorUserName = '' self._actionProcessorUserPassword = '' return if '\\' in run_as_user: logger.warning( "Ignoring domain part of user to run action processor '%s'", run_as_user) run_as_user = run_as_user.split('\\', -1) if not recreate and self._actionProcessorUserName and self._actionProcessorUserPassword and System.existsUser( username=run_as_user): return self._actionProcessorUserName = run_as_user logger.notice(f"Creating local user '{run_as_user}'") self._actionProcessorUserPassword = '******' + str( randomString(16)) + '!/%' secret_filter.add_secrets(self._actionProcessorUserPassword) if System.existsUser(username=run_as_user): System.deleteUser(username=run_as_user) System.createUser(username=run_as_user, password=self._actionProcessorUserPassword, groups=[System.getAdminGroupName()])
def SvcStop(self): # pylint: disable=invalid-name """ Gets called from windows to stop service """ logger.notice("Handling stop request") self.ReportServiceStatus(win32service.SERVICE_STOP_PENDING) win32event.SetEvent(self._stopEvent)
def setup_firewall_linux(): logger.notice("Configure iptables") port = config.get('control_server', 'port') cmds = [] if os.path.exists("/usr/bin/firewall-cmd"): # openSUSE Leap cmds.append([ "/usr/bin/firewall-cmd", f"--add-port={port}/tcp", "--zone", "public" ]) elif os.path.exists("/sbin/SuSEfirewall2"): # other SUSE cmds.append(["/sbin/SuSEfirewall2", "open", "EXT", "TCP" f"{port}"]) elif os.path.exists("/usr/sbin/ucr"): # UCS cmds.append([ "/usr/sbin/ucr", "set", f"security/packetfilter/package/opsiclientd/tcp/{port}/all=ACCEPT" ]) cmds.append(["/usr/sbin/service", "univention-firewall", "restart"]) elif os.path.exists("/sbin/iptables"): for iptables in ("iptables", "ip6tables"): cmds.append([ iptables, "-A", "INPUT", "-p", "tcp", "--dport", str(port), "-j", "ACCEPT" ]) else: logger.warning( "Could not configure firewall - no suitable executable found.") for cmd in cmds: logger.info("Running command: %s", str(cmd)) subprocess.call(cmd)
def restart(self, waitSeconds=0): def _restart(waitSeconds=0): time.sleep(waitSeconds) timeline.addEvent(title="opsiclientd restart", category="system") try: if not os.path.exists(config.restart_marker): logger.notice("Writing restart marker %s", config.restart_marker) with open(config.restart_marker, "w", encoding="utf-8") as file: file.write("#") except Exception as err: # pylint: disable=broad-except logger.error(err) if RUNNING_ON_WINDOWS: subprocess.Popen( # pylint: disable=consider-using-with "net stop opsiclientd & net start opsiclientd", shell=True, creationflags=subprocess.CREATE_NEW_PROCESS_GROUP | subprocess.DETACHED_PROCESS) else: logger.notice("Executing: %s", self._argv) os.chdir(os.path.dirname(self._argv[0])) os.execvp(self._argv[0], self._argv) logger.notice("Will restart in %d seconds", waitSeconds) threading.Thread(target=_restart, args=(waitSeconds, )).start()
def install_service_windows(): logger.notice("Installing windows service") from opsiclientd.windows.service import handle_commandline # pylint: disable=import-outside-toplevel handle_commandline( argv=["opsiclientd.exe", "--startup", "auto", "install"]) # pyright: reportMissingImports=false import winreg # pylint: disable=import-outside-toplevel,import-error import win32process # pylint: disable=import-outside-toplevel,import-error key_handle = winreg.CreateKey( winreg.HKEY_LOCAL_MACHINE, r"SYSTEM\CurrentControlSet\Services\opsiclientd") if win32process.IsWow64Process(): winreg.DisableReflectionKey(key_handle) winreg.SetValueEx(key_handle, 'DependOnService', 0, winreg.REG_MULTI_SZ, ["Dhcp"]) #winreg.SetValueEx(key_handle, 'DependOnService', 0, winreg.REG_MULTI_SZ, ["Dhcp", "Dnscache"]) winreg.CloseKey(key_handle) key_handle = winreg.CreateKey(winreg.HKEY_LOCAL_MACHINE, r"SYSTEM\CurrentControlSet\Control") if win32process.IsWow64Process(): winreg.DisableReflectionKey(key_handle) winreg.SetValueEx(key_handle, 'ServicesPipeTimeout', 0, winreg.REG_DWORD, 120000) winreg.CloseKey(key_handle)
def reconfigureEventGenerators(): eventConfigs = getEventConfigs() for eventGenerator in _EVENT_GENERATORS.values(): eventGenerator.setEventConfigs([]) for (eventConfigId, eventConfig) in eventConfigs.items(): mainEventConfigId = eventConfigId.split('{')[0] try: eventGenerator = _EVENT_GENERATORS[mainEventConfigId] except KeyError: logger.info( "Cannot reconfigure event generator for event '%s': not found", eventConfigId) continue try: eventType = eventConfig['type'] del eventConfig['type'] ec = EventConfigFactory(eventType, eventConfigId, **eventConfig) eventGenerator.addEventConfig(ec) logger.notice("Event config '%s' added to event generator '%s'", eventConfigId, mainEventConfigId) except Exception as err: # pylint: disable=broad-except logger.error("Failed to reconfigure event generator '%s': %s", mainEventConfigId, err)
def setup(full=False, options=None) -> None: logger.notice("Running opsiclientd setup") errors = [] if full: opsi_service_setup(options) try: install_service() except Exception as err: # pylint: disable=broad-except logger.error("Failed to install service: %s", err, exc_info=True) errors.append(str(err)) try: setup_ssl(full) except Exception as err: # pylint: disable=broad-except logger.error("Failed to setup ssl: %s", err, exc_info=True) errors.append(str(err)) try: setup_firewall() except Exception as err: # pylint: disable=broad-except logger.error("Failed to setup firewall: %s", err, exc_info=True) errors.append(str(err)) try: setup_on_shutdown() except Exception as err: # pylint: disable=broad-except logger.error("Failed to setup on_shutdown: %s", err, exc_info=True) errors.append(str(err)) logger.notice("Setup completed with %d errors", len(errors)) if errors and full: raise RuntimeError(", ".join(errors))
def getFromService(self, configService): ''' Get settings from service ''' logger.notice("Getting config from service") if not configService: raise Exception("Config service is undefined") query = { "objectId": self.get('global', 'host_id'), "configId": [ 'clientconfig.configserver.url', 'clientconfig.depot.drive', 'clientconfig.depot.id', 'clientconfig.depot.user', 'clientconfig.suspend_bitlocker_on_reboot', 'opsiclientd.*' # everything starting with opsiclientd. ] } configService.backend_setOptions({"addConfigStateDefaults": True}) for configState in configService.configState_getObjects(**query): logger.info("Got config state from service: %r", configState) if not configState.values: logger.debug("No values - skipping %s", configState.configId) continue if configState.configId == 'clientconfig.configserver.url': self.set('config_service', 'url', configState.values) elif configState.configId == 'clientconfig.depot.drive': self.set('depot_server', 'drive', configState.values[0]) elif configState.configId == 'clientconfig.depot.id': self.set('depot_server', 'depot_id', configState.values[0]) elif configState.configId == 'clientconfig.depot.user': self.set('depot_server', 'username', configState.values[0]) elif configState.configId == 'clientconfig.suspend_bitlocker_on_reboot': self.set('global', 'suspend_bitlocker_on_reboot', configState.values[0]) elif configState.configId.startswith('opsiclientd.'): try: parts = configState.configId.lower().split('.') if len(parts) < 3: logger.debug( "Expected at least 3 parts in %s - skipping.", configState.configId) continue value = configState.values if len(value) == 1: value = value[0] self.set(section=parts[1], option=parts[2], value=value) except Exception as err: # pylint: disable=broad-except logger.error("Failed to process configState '%s': %s", configState.configId, err) logger.notice("Got config from service") logger.debug("Config is now:\n %s", objectToBeautifiedText(self.getDict()))
def systemShutdownInitiated(self): if not self.isRebootTriggered() and not self.isShutdownTriggered(): # This shutdown was triggered by someone else # Reset shutdown/reboot requests to avoid reboot/shutdown on next boot logger.notice( "Someone triggered a reboot or a shutdown => clearing reboot request" ) self.clearRebootRequest()
def self_update_from_file(self, filename): logger.notice("Self-update from file %s", filename) test_file = "base_library.zip" inst_dir = os.path.dirname(os.path.dirname(os.path.realpath(__file__))) if not os.path.exists(os.path.join(inst_dir, test_file)): raise RuntimeError( f"File not found: {os.path.join(inst_dir, test_file)}") if self._selfUpdating: raise RuntimeError("Self-update already running") self._selfUpdating = True try: with tempfile.TemporaryDirectory() as tmpdir: destination = os.path.join(tmpdir, "content") shutil.unpack_archive(filename=filename, extract_dir=destination) bin_dir = destination if not os.path.exists(os.path.join(bin_dir, test_file)): bin_dir = None for fn in os.listdir(destination): if os.path.exists( os.path.join(destination, fn, test_file)): bin_dir = os.path.join(destination, fn) break if not bin_dir: raise RuntimeError("Invalid archive") try: check_signature(bin_dir) except Exception as err: # pylint: disable=broad-except logger.error("Could not verify signature!\n%s", err, exc_info=True) logger.error("Not performing self_update.") raise RuntimeError("Invalid signature") from err binary = os.path.join(bin_dir, os.path.basename(self._argv[0])) logger.info("Testing new binary: %s", binary) out = subprocess.check_output([binary, "--version"]) logger.info(out) move_dir = inst_dir + "_old" logger.info("Moving current installation dir '%s' to '%s'", inst_dir, move_dir) if os.path.exists(move_dir): shutil.rmtree(move_dir) os.rename(inst_dir, move_dir) logger.info("Installing '%s' into '%s'", bin_dir, inst_dir) shutil.copytree(bin_dir, inst_dir) self.restart(3) finally: self._selfUpdating = False
def setBlockLogin(self, blockLogin, handleNotifier=True): # pylint: disable=too-many-branches blockLogin = forceBool(blockLogin) changed = self._blockLogin != blockLogin self._blockLogin = blockLogin logger.notice("Block login now set to '%s'", self._blockLogin) if self._blockLogin: if not self._blockLoginEventId: self._blockLoginEventId = timeline.addEvent( title="Blocking login", description="User login blocked", category="block_login", durationEvent=True) if not self._blockLoginNotifierPid and config.get( 'global', 'block_login_notifier'): if handleNotifier and RUNNING_ON_WINDOWS: logger.info("Starting block login notifier app") # Start block login notifier on physical console sessionId = System.getActiveConsoleSessionId() while True: try: self._blockLoginNotifierPid = System.runCommandInSession( command=config.get('global', 'block_login_notifier'), sessionId=sessionId, desktop='winlogon', waitForProcessEnding=False)[2] break except Exception as err: # pylint: disable=broad-except logger.error( "Failed to start block login notifier app: %s", err) break else: if self._blockLoginEventId: timeline.setEventEnd(eventId=self._blockLoginEventId) self._blockLoginEventId = None if handleNotifier and self._blockLoginNotifierPid: try: logger.info( "Terminating block login notifier app (pid %s)", self._blockLoginNotifierPid) System.terminateProcess( processId=self._blockLoginNotifierPid) except Exception as err: # pylint: disable=broad-except logger.warning( "Failed to terminate block login notifier app: %s", err) self._blockLoginNotifierPid = None if changed and self._controlPipe: try: self._controlPipe.executeRpc("blockLogin", self._blockLogin) except Exception as rpc_error: # pylint: disable=broad-except logger.debug(rpc_error)
def SvcShutdown(self): # pylint: disable=invalid-name """ Gets called from windows on system shutdown """ logger.notice("Handling shutdown request") self.ReportServiceStatus(win32service.SERVICE_STOP_PENDING) if self.opsiclientd: self.opsiclientd.systemShutdownInitiated() win32event.SetEvent(self._stopEvent)
def processEvent(self, event): logger.notice("Processing event %s", event) description = f"Event {event.eventConfig.getId()} occurred\n" description += "Config:\n" _config = event.eventConfig.getConfig() configKeys = list(_config.keys()) configKeys.sort() for configKey in configKeys: description += f"{configKey}: {_config[configKey]}\n" logger.trace("check lock (ocd), currently %s -> locking if not True", self.eventLock.locked()) # if triggered by Basic.py fire_event, lock is already acquired if not self.eventLock.locked(): self.eventLock.acquire() # pylint: disable=consider-using-with try: timeline.addEvent(title=f"Event {event.eventConfig.getName()}", description=description, category="event_occurrence") # if processEvent is called through Event.fireEvent(), this check is already done #self.canProcessEvent(event) # A user login event should not cancel running non-login Event if event.eventConfig.actionType != 'login': self.cancelOthersAndWaitUntilReady() except (ValueError, RuntimeError) as err: # skipping execution if event cannot be created logger.warning("Could not start event: %s", err, exc_info=True) logger.trace("release lock (ocd cannot process event)") self.eventLock.release() return try: logger.debug("Creating new ept (ocd)") eventProcessingThread = EventProcessingThread(self, event) self.createActionProcessorUser(recreate=False) with self._eptListLock: self._eventProcessingThreads.append(eventProcessingThread) finally: logger.trace("release lock (ocd)") self.eventLock.release() try: eventProcessingThread.start() eventProcessingThread.join() logger.notice("Done processing event %s", event) finally: with self._eptListLock: self._eventProcessingThreads.remove(eventProcessingThread) if not self._eventProcessingThreads: try: self.deleteActionProcessorUser() except Exception as err: # pylint: disable=broad-except logger.warning(err)
def run(self): while True: try: System.reboot(0) logger.notice("Reboot initiated") break except Exception as err: # pylint: disable=broad-except # Device not ready? logger.info("Failed to initiate reboot: %s", err) time.sleep(1)
def start(self): db_file = config.get('global', 'timeline_db') logger.notice("Starting timeline (database location: %s)", db_file) try: self._createDatabase() except sqlite3.DatabaseError as err: logger.error( "Failed to connect to database %s: %s, recreating database", db_file, err) self._createDatabase(delete_existing=True) self._cleanupDatabase()
def waitForClient(self): logger.debug("Waiting for client to connect to %s", self._socketName) self._socket.settimeout(2.0) while True: try: connection, client_address = self._socket.accept() logger.notice("Client %s connected to %s", client_address, self._socketName) return (connection, client_address) except socket.timeout: if self._stopEvent.is_set(): return (None, None)
def isShutdownRequested(self): try: shutdownRequested = System.getRegistryValue( System.HKEY_LOCAL_MACHINE, "SOFTWARE\\opsi.org\\winst", "ShutdownRequested") except Exception as err: # pylint: disable=broad-except logger.info("Failed to get shutdownRequested from registry: %s", err) shutdownRequested = 0 logger.notice("Shutdown request in Registry: %s", shutdownRequested) return forceBool(shutdownRequested)
def initialize(self): EventGenerator.initialize(self) logger.notice("Registring ISensLogon") from opsiclientd.windows import importWmiAndPythoncom, SensLogon # pylint: disable=import-outside-toplevel (_wmi, pythoncom) = importWmiAndPythoncom(importWmi=False, importPythoncom=True) pythoncom.CoInitialize() sl = SensLogon(self.callback) sl.subscribe()
def suspendBitlocker(self): # pylint: disable=no-self-use logger.notice("Suspending bitlocker for one reboot if active") try: System.execute( "powershell.exe -ExecutionPolicy Bypass -Command \"" "foreach($v in Get-BitLockerVolume)" "{if ($v.EncryptionPercentage -gt 0)" "{$v | Suspend-BitLocker -RebootCount 1}}\"", captureStderr=True, waitForEnding=True, timeout=20) except Exception as err: # pylint: disable=broad-except logger.error("Failed to suspend bitlocker: %s", err, exc_info=True)
def setup_firewall_windows(): logger.notice("Configure Windows firewall") port = config.get('control_server', 'port') cmds = [[ "netsh", "advfirewall", "firewall", "delete", "rule", 'name="opsiclientd-control-port"' ], [ "netsh", "advfirewall", "firewall", "add", "rule", 'name="opsiclientd-control-port"', "dir=in", "action=allow", "protocol=TCP", f"localport={port}" ]] for cmd in cmds: logger.info("Running command: %s", str(cmd)) subprocess.call(cmd)
def deleteActionProcessorUser(self): if not config.get('action_processor', 'delete_user'): return if not self._actionProcessorUserName: return if not System.existsUser(username=self._actionProcessorUserName): return logger.notice("Deleting local user '%s'", self._actionProcessorUserName) System.deleteUser(username=self._actionProcessorUserName) self._actionProcessorUserName = '' self._actionProcessorUserPassword = ''
def self_update_from_url(self, url): logger.notice("Self-update from url: %s", url) filename = url.split('/')[-1] with tempfile.TemporaryDirectory() as tmpdir: filename = os.path.join(tmpdir, filename) if url.startswith("file://"): src = url[7:] if RUNNING_ON_WINDOWS: src = src.lstrip('/').replace('/', '\\') shutil.copy(src, filename) else: with urllib.request.urlopen(url) as response: with open(filename, 'wb') as file: file.write(response.read()) self.self_update_from_file(filename)
def setup_firewall_macos(): logger.notice("Configure MacOS firewall") cmds = [] for path in ("/usr/local/bin/opsiclientd", "/usr/local/lib/opsiclientd/opsiclientd"): cmds.append( ["/usr/libexec/ApplicationFirewall/socketfilterfw", "--add", path]) cmds.append([ "/usr/libexec/ApplicationFirewall/socketfilterfw", "--unblockapp", path ]) for cmd in cmds: logger.info("Running command: %s", str(cmd)) subprocess.call(cmd)
def run(self): with opsicommon.logging.log_context( {'instance': f'event generator {self._generatorConfig.getId()}'}): try: logger.info("Initializing event generator '%s'", self) self.initialize() if self._generatorConfig.activationDelay > 0: logger.debug( "Waiting %d seconds before activation of event generator '%s'", self._generatorConfig.activationDelay, self) time.sleep(self._generatorConfig.activationDelay) logger.info("Activating event generator '%s'", self) while not self._stopped and ( (self._generatorConfig.maxRepetitions < 0) or (self._eventsOccured <= self._generatorConfig.maxRepetitions)): logger.info("Getting next event...") event = self.getNextEvent() # pylint: disable=assignment-from-none,assignment-from-no-return self._eventsOccured += 1 # Count as occured, even if event is None! if event: logger.info("Got new event: %s (%d/%d)", event, self._eventsOccured, self._generatorConfig.maxRepetitions + 1) self.fireEvent(event) for _unused in range(10): if self._stopped: break time.sleep(1) if not self._stopped: logger.notice( "Event generator '%s' now deactivated after %d event occurrences", self, self._eventsOccured) except Exception as err: # pylint: disable=broad-except if not self._stopped: logger.error("Failure in event generator '%s': %s", self, err, exc_info=True) try: self.cleanup() except Exception as err: # pylint: disable=broad-except if not self._stopped: logger.error("Failed to clean up: %s", err) logger.info("Event generator '%s' exiting ", self)
def opsi_service_setup(options=None): try: config.readConfigFile() except Exception as err: # pylint: disable=broad-except logger.info(err) if os.path.exists(config.ca_cert_file): # Delete ca cert which could be invalid or expired os.remove(config.ca_cert_file) service_address = getattr(options, "service_address", None) or config.get( 'config_service', 'url')[0] service_username = getattr(options, "service_username", None) or config.get('global', 'host_id') service_password = getattr(options, "service_password", None) or config.get('global', 'opsi_host_key') if getattr(options, "client_id", None): config.set('global', 'host_id', options.client_id) if not config.get('global', 'host_id'): fqdn = get_fqdn() fqdn = config.set('global', 'host_id', fqdn) secret_filter.add_secrets(service_password) logger.notice("Connecting to '%s' as '%s'", service_address, service_username) jsonrpc_client = JSONRPCClient(address=service_address, username=service_username, password=service_password, verify_server_cert=False) try: update_ca_cert(jsonrpc_client, allow_remove=False) except Exception as err: # pylint: disable=broad-except logger.error(err, exc_info=True) try: client = jsonrpc_client.host_getObjects(id=config.get( 'global', 'host_id')) # pylint: disable=no-member if client and client[0] and client[0].opsiHostKey: config.set('global', 'opsi_host_key', client[0].opsiHostKey) config.getFromService(jsonrpc_client) config.updateConfigFile(force=True) finally: jsonrpc_client.disconnect()
def isRebootRequested(self): try: rebootRequested = System.getRegistryValue( System.HKEY_LOCAL_MACHINE, "SOFTWARE\\opsi.org\\winst", "RebootRequested") except Exception as error: # pylint: disable=broad-except logger.warning("Failed to get RebootRequested from registry: %s", error) rebootRequested = 0 logger.notice("Reboot request in Registry: %s", rebootRequested) if rebootRequested == 2: # Logout logger.info("Logout requested") self.clearRebootRequest() return False return forceBool(rebootRequested)
def getDaemonLoopingContext(): with getEventGeneratorContext(): for event_generator in getEventGenerators( generatorClass=DaemonStartupEventGenerator): try: event_generator.createAndFireEvent() except ValueError as err: logger.error( "Unable to fire DaemonStartupEvent from %s: %s", event_generator, err, exc_info=True) if getEventGenerators(generatorClass=GUIStartupEventGenerator): # Wait until gui starts up logger.notice( "Waiting for gui startup (timeout: %d seconds)", config.get('global', 'wait_for_gui_timeout')) self.waitForGUI( timeout=config.get('global', 'wait_for_gui_timeout')) if not self.is_stopping(): logger.notice("Done waiting for GUI") # Wait some more seconds for events to fire time.sleep(5) try: yield finally: for event_generator in getEventGenerators( generatorClass=DaemonShutdownEventGenerator): logger.info( "Create and fire shutdown event generator %s", event_generator) try: event_generator.createAndFireEvent() except ValueError as err: logger.error( "Unable to fire DaemonStartupEvent from %s: %s", event_generator, err, exc_info=True)
def start_pty(shell="powershell.exe", lines=30, columns=120): logger.notice("Starting %s (%d/%d)", shell, lines, columns) try: # Import of winpty may sometimes fail because of problems with the needed dll. # Therefore we do not import at toplevel from winpty import PtyProcess # pylint: disable=import-error,import-outside-toplevel except ImportError as err: logger.error("Failed to start pty: %s", err, exc_info=True) raise process = PtyProcess.spawn(shell, dimensions=(lines, columns)) def read(length: int): return process.read(length).encode("utf-8") def write(data: bytes): return process.write(data.decode("utf-8")) def stop(): process.close() return (read, write, stop)