mirror of
https://github.com/Ultimaker/Cura.git
synced 2025-07-11 08:47:50 -06:00
Setup the authentication stuff for LegacyUM3
CL-541
This commit is contained in:
parent
f987e6d977
commit
7465a6551a
3 changed files with 284 additions and 6 deletions
|
@ -8,9 +8,19 @@ from PyQt5.QtCore import pyqtProperty, pyqtSignal, pyqtSlot, QObject, QTimer, py
|
||||||
|
|
||||||
from time import time
|
from time import time
|
||||||
from typing import Callable, Any
|
from typing import Callable, Any
|
||||||
|
from enum import IntEnum
|
||||||
|
|
||||||
|
|
||||||
|
class AuthState(IntEnum):
|
||||||
|
NotAuthenticated = 1
|
||||||
|
AuthenticationRequested = 2
|
||||||
|
Authenticated = 3
|
||||||
|
AuthenticationDenied = 4
|
||||||
|
AuthenticationReceived = 5
|
||||||
|
|
||||||
|
|
||||||
class NetworkedPrinterOutputDevice(PrinterOutputDevice):
|
class NetworkedPrinterOutputDevice(PrinterOutputDevice):
|
||||||
|
authenticationStateChanged = pyqtSignal()
|
||||||
def __init__(self, device_id, address: str, properties, parent = None):
|
def __init__(self, device_id, address: str, properties, parent = None):
|
||||||
super().__init__(device_id = device_id, parent = parent)
|
super().__init__(device_id = device_id, parent = parent)
|
||||||
self._manager = None
|
self._manager = None
|
||||||
|
@ -27,6 +37,16 @@ class NetworkedPrinterOutputDevice(PrinterOutputDevice):
|
||||||
self._user_agent = "%s/%s " % (Application.getInstance().getApplicationName(), Application.getInstance().getVersion())
|
self._user_agent = "%s/%s " % (Application.getInstance().getApplicationName(), Application.getInstance().getVersion())
|
||||||
|
|
||||||
self._onFinishedCallbacks = {}
|
self._onFinishedCallbacks = {}
|
||||||
|
self._authentication_state = AuthState.NotAuthenticated
|
||||||
|
|
||||||
|
def setAuthenticationState(self, authentication_state):
|
||||||
|
if self._authentication_state != authentication_state:
|
||||||
|
self._authentication_state = authentication_state
|
||||||
|
self.authenticationStateChanged.emit()
|
||||||
|
|
||||||
|
@pyqtProperty(int, notify=authenticationStateChanged)
|
||||||
|
def authenticationState(self):
|
||||||
|
return self._authentication_state
|
||||||
|
|
||||||
def _update(self):
|
def _update(self):
|
||||||
if self._last_response_time:
|
if self._last_response_time:
|
||||||
|
@ -81,23 +101,30 @@ class NetworkedPrinterOutputDevice(PrinterOutputDevice):
|
||||||
self._last_request_time = time()
|
self._last_request_time = time()
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def _post(self, target: str, data: str, onFinished: Callable[[Any, QNetworkReply], None], onProgress: Callable):
|
def _post(self, target: str, data: str, onFinished: Callable[[Any, QNetworkReply], None], onProgress: Callable = None):
|
||||||
if self._manager is None:
|
if self._manager is None:
|
||||||
self._createNetworkManager()
|
self._createNetworkManager()
|
||||||
|
request = self._createEmptyRequest(target)
|
||||||
self._last_request_time = time()
|
self._last_request_time = time()
|
||||||
pass
|
reply = self._manager.post(request, data)
|
||||||
|
if onProgress is not None:
|
||||||
|
reply.uploadProgress.connect(onProgress)
|
||||||
|
self._onFinishedCallbacks[reply.url().toString() + str(reply.operation())] = onFinished
|
||||||
|
|
||||||
|
def _onAuthenticationRequired(self, reply, authenticator):
|
||||||
|
Logger.log("w", "Request to {url} required authentication, which was not implemented".format(url = reply.url().toString()))
|
||||||
|
|
||||||
def _createNetworkManager(self):
|
def _createNetworkManager(self):
|
||||||
Logger.log("d", "Creating network manager")
|
Logger.log("d", "Creating network manager")
|
||||||
if self._manager:
|
if self._manager:
|
||||||
self._manager.finished.disconnect(self.__handleOnFinished)
|
self._manager.finished.disconnect(self.__handleOnFinished)
|
||||||
#self._manager.networkAccessibleChanged.disconnect(self._onNetworkAccesibleChanged)
|
#self._manager.networkAccessibleChanged.disconnect(self._onNetworkAccesibleChanged)
|
||||||
#self._manager.authenticationRequired.disconnect(self._onAuthenticationRequired)
|
self._manager.authenticationRequired.disconnect(self._onAuthenticationRequired)
|
||||||
|
|
||||||
self._manager = QNetworkAccessManager()
|
self._manager = QNetworkAccessManager()
|
||||||
self._manager.finished.connect(self.__handleOnFinished)
|
self._manager.finished.connect(self.__handleOnFinished)
|
||||||
self._last_manager_create_time = time()
|
self._last_manager_create_time = time()
|
||||||
#self._manager.authenticationRequired.connect(self._onAuthenticationRequired)
|
self._manager.authenticationRequired.connect(self._onAuthenticationRequired)
|
||||||
#self._manager.networkAccessibleChanged.connect(self._onNetworkAccesibleChanged) # for debug purposes
|
#self._manager.networkAccessibleChanged.connect(self._onNetworkAccesibleChanged) # for debug purposes
|
||||||
|
|
||||||
def __handleOnFinished(self, reply: QNetworkReply):
|
def __handleOnFinished(self, reply: QNetworkReply):
|
||||||
|
@ -107,7 +134,9 @@ class NetworkedPrinterOutputDevice(PrinterOutputDevice):
|
||||||
|
|
||||||
self._last_response_time = time()
|
self._last_response_time = time()
|
||||||
|
|
||||||
|
if self._connection_state == ConnectionState.connecting:
|
||||||
self.setConnectionState(ConnectionState.connected)
|
self.setConnectionState(ConnectionState.connected)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
self._onFinishedCallbacks[reply.url().toString() + str(reply.operation())](reply)
|
self._onFinishedCallbacks[reply.url().toString() + str(reply.operation())](reply)
|
||||||
except Exception:
|
except Exception:
|
||||||
|
|
|
@ -135,11 +135,13 @@ class PrinterOutputDevice(QObject, OutputDevice):
|
||||||
|
|
||||||
## Attempt to establish connection
|
## Attempt to establish connection
|
||||||
def connect(self):
|
def connect(self):
|
||||||
|
self.setConnectionState(ConnectionState.connecting)
|
||||||
self._update_timer.start()
|
self._update_timer.start()
|
||||||
|
|
||||||
## Attempt to close the connection
|
## Attempt to close the connection
|
||||||
def close(self):
|
def close(self):
|
||||||
self._update_timer.stop()
|
self._update_timer.stop()
|
||||||
|
self.setConnectionState(ConnectionState.closed)
|
||||||
|
|
||||||
## Ensure that close gets called when object is destroyed
|
## Ensure that close gets called when object is destroyed
|
||||||
def __del__(self):
|
def __del__(self):
|
||||||
|
|
|
@ -1,28 +1,256 @@
|
||||||
from cura.PrinterOutput.NetworkedPrinterOutputDevice import NetworkedPrinterOutputDevice
|
from cura.PrinterOutput.NetworkedPrinterOutputDevice import NetworkedPrinterOutputDevice, AuthState
|
||||||
from cura.PrinterOutput.PrinterOutputModel import PrinterOutputModel
|
from cura.PrinterOutput.PrinterOutputModel import PrinterOutputModel
|
||||||
from cura.PrinterOutput.PrintJobOutputModel import PrintJobOutputModel
|
from cura.PrinterOutput.PrintJobOutputModel import PrintJobOutputModel
|
||||||
from cura.PrinterOutput.MaterialOutputModel import MaterialOutputModel
|
from cura.PrinterOutput.MaterialOutputModel import MaterialOutputModel
|
||||||
|
|
||||||
from UM.Logger import Logger
|
from UM.Logger import Logger
|
||||||
from UM.Settings.ContainerRegistry import ContainerRegistry
|
from UM.Settings.ContainerRegistry import ContainerRegistry
|
||||||
|
from UM.Application import Application
|
||||||
|
from UM.i18n import i18nCatalog
|
||||||
|
from UM.Message import Message
|
||||||
|
|
||||||
from PyQt5.QtNetwork import QNetworkRequest
|
from PyQt5.QtNetwork import QNetworkRequest
|
||||||
|
from PyQt5.QtCore import QTimer
|
||||||
|
|
||||||
import json
|
import json
|
||||||
|
import os # To get the username
|
||||||
|
|
||||||
|
i18n_catalog = i18nCatalog("cura")
|
||||||
|
|
||||||
|
|
||||||
|
## This is the output device for the "Legacy" API of the UM3. All firmware before 4.0.1 uses this API.
|
||||||
|
# Everything after that firmware uses the ClusterUM3Output.
|
||||||
|
# The Legacy output device can only have one printer (whereas the cluster can have 0 to n).
|
||||||
|
#
|
||||||
|
# Authentication is done in a number of steps;
|
||||||
|
# 1. Request an id / key pair by sending the application & user name. (state = authRequested)
|
||||||
|
# 2. Machine sends this back and will display an approve / deny message on screen. (state = AuthReceived)
|
||||||
|
# 3. OutputDevice will poll if the button was pressed.
|
||||||
|
# 4. At this point the machine either has the state Authenticated or AuthenticationDenied.
|
||||||
|
# 5. As a final step, we verify the authentication, as this forces the QT manager to setup the authenticator.
|
||||||
class LegacyUM3OutputDevice(NetworkedPrinterOutputDevice):
|
class LegacyUM3OutputDevice(NetworkedPrinterOutputDevice):
|
||||||
def __init__(self, device_id, address: str, properties, parent = None):
|
def __init__(self, device_id, address: str, properties, parent = None):
|
||||||
super().__init__(device_id = device_id, address = address, properties = properties, parent = parent)
|
super().__init__(device_id = device_id, address = address, properties = properties, parent = parent)
|
||||||
self._api_prefix = "/api/v1/"
|
self._api_prefix = "/api/v1/"
|
||||||
self._number_of_extruders = 2
|
self._number_of_extruders = 2
|
||||||
|
|
||||||
|
self._authentication_id = None
|
||||||
|
self._authentication_key = None
|
||||||
|
|
||||||
|
self._authentication_counter = 0
|
||||||
|
self._max_authentication_counter = 5 * 60 # Number of attempts before authentication timed out (5 min)
|
||||||
|
|
||||||
|
self._authentication_timer = QTimer()
|
||||||
|
self._authentication_timer.setInterval(1000) # TODO; Add preference for update interval
|
||||||
|
self._authentication_timer.setSingleShot(False)
|
||||||
|
|
||||||
|
self._authentication_timer.timeout.connect(self._onAuthenticationTimer)
|
||||||
|
|
||||||
|
# The messages are created when connect is called the first time.
|
||||||
|
# This ensures that the messages are only created for devices that actually want to connect.
|
||||||
|
self._authentication_requested_message = None
|
||||||
|
self._authentication_failed_message = None
|
||||||
|
self._not_authenticated_message = None
|
||||||
|
|
||||||
|
def _setupMessages(self):
|
||||||
|
self._authentication_requested_message = Message(i18n_catalog.i18nc("@info:status",
|
||||||
|
"Access to the printer requested. Please approve the request on the printer"),
|
||||||
|
lifetime=0, dismissable=False, progress=0,
|
||||||
|
title=i18n_catalog.i18nc("@info:title",
|
||||||
|
"Authentication status"))
|
||||||
|
|
||||||
|
self._authentication_failed_message = Message(i18n_catalog.i18nc("@info:status", ""),
|
||||||
|
title=i18n_catalog.i18nc("@info:title", "Authentication Status"))
|
||||||
|
self._authentication_failed_message.addAction("Retry", i18n_catalog.i18nc("@action:button", "Retry"), None,
|
||||||
|
i18n_catalog.i18nc("@info:tooltip", "Re-send the access request"))
|
||||||
|
self._authentication_failed_message.actionTriggered.connect(self._requestAuthentication)
|
||||||
|
self._authentication_succeeded_message = Message(
|
||||||
|
i18n_catalog.i18nc("@info:status", "Access to the printer accepted"),
|
||||||
|
title=i18n_catalog.i18nc("@info:title", "Authentication Status"))
|
||||||
|
|
||||||
|
self._not_authenticated_message = Message(
|
||||||
|
i18n_catalog.i18nc("@info:status", "No access to print with this printer. Unable to send print job."),
|
||||||
|
title=i18n_catalog.i18nc("@info:title", "Authentication Status"))
|
||||||
|
self._not_authenticated_message.addAction("Request", i18n_catalog.i18nc("@action:button", "Request Access"),
|
||||||
|
None, i18n_catalog.i18nc("@info:tooltip",
|
||||||
|
"Send access request to the printer"))
|
||||||
|
self._not_authenticated_message.actionTriggered.connect(self._requestAuthentication)
|
||||||
|
|
||||||
|
def connect(self):
|
||||||
|
super().connect()
|
||||||
|
self._setupMessages()
|
||||||
|
global_container = Application.getInstance().getGlobalContainerStack()
|
||||||
|
if global_container:
|
||||||
|
self._authentication_id = global_container.getMetaDataEntry("network_authentication_id", None)
|
||||||
|
self._authentication_key = global_container.getMetaDataEntry("network_authentication_key", None)
|
||||||
|
|
||||||
|
def close(self):
|
||||||
|
super().close()
|
||||||
|
if self._authentication_requested_message:
|
||||||
|
self._authentication_requested_message.hide()
|
||||||
|
if self._authentication_failed_message:
|
||||||
|
self._authentication_failed_message.hide()
|
||||||
|
if self._authentication_succeeded_message:
|
||||||
|
self._authentication_succeeded_message.hide()
|
||||||
|
|
||||||
|
self._authentication_timer.stop()
|
||||||
|
|
||||||
|
## Send all material profiles to the printer.
|
||||||
|
def sendMaterialProfiles(self):
|
||||||
|
# TODO
|
||||||
|
pass
|
||||||
|
|
||||||
def _update(self):
|
def _update(self):
|
||||||
if not super()._update():
|
if not super()._update():
|
||||||
return
|
return
|
||||||
|
if self._authentication_state == AuthState.NotAuthenticated:
|
||||||
|
if self._authentication_id is None and self._authentication_key is None:
|
||||||
|
# This machine doesn't have any authentication, so request it.
|
||||||
|
self._requestAuthentication()
|
||||||
|
elif self._authentication_id is not None and self._authentication_key is not None:
|
||||||
|
# We have authentication info, but we haven't checked it out yet. Do so now.
|
||||||
|
self._verifyAuthentication()
|
||||||
|
elif self._authentication_state == AuthState.AuthenticationReceived:
|
||||||
|
# We have an authentication, but it's not confirmed yet.
|
||||||
|
self._checkAuthentication()
|
||||||
|
|
||||||
|
# We don't need authentication for requesting info, so we can go right ahead with requesting this.
|
||||||
self._get("printer", onFinished=self._onGetPrinterDataFinished)
|
self._get("printer", onFinished=self._onGetPrinterDataFinished)
|
||||||
self._get("print_job", onFinished=self._onGetPrintJobFinished)
|
self._get("print_job", onFinished=self._onGetPrintJobFinished)
|
||||||
|
|
||||||
|
def _resetAuthenticationRequestedMessage(self):
|
||||||
|
if self._authentication_requested_message:
|
||||||
|
self._authentication_requested_message.hide()
|
||||||
|
self._authentication_timer.stop()
|
||||||
|
self._authentication_counter = 0
|
||||||
|
|
||||||
|
def _onAuthenticationTimer(self):
|
||||||
|
self._authentication_counter += 1
|
||||||
|
self._authentication_requested_message.setProgress(
|
||||||
|
self._authentication_counter / self._max_authentication_counter * 100)
|
||||||
|
if self._authentication_counter > self._max_authentication_counter:
|
||||||
|
self._authentication_timer.stop()
|
||||||
|
Logger.log("i", "Authentication timer ended. Setting authentication to denied for printer: %s" % self._key)
|
||||||
|
self.setAuthenticationState(AuthState.AuthenticationDenied)
|
||||||
|
self._resetAuthenticationRequestedMessage()
|
||||||
|
self._authentication_failed_message.show()
|
||||||
|
|
||||||
|
def _verifyAuthentication(self):
|
||||||
|
Logger.log("d", "Attempting to verify authentication")
|
||||||
|
# This will ensure that the "_onAuthenticationRequired" is triggered, which will setup the authenticator.
|
||||||
|
self._get("auth/verify", onFinished=self._onVerifyAuthenticationCompleted)
|
||||||
|
|
||||||
|
def _onVerifyAuthenticationCompleted(self, reply):
|
||||||
|
status_code = reply.attribute(QNetworkRequest.HttpStatusCodeAttribute)
|
||||||
|
if status_code == 401:
|
||||||
|
# Something went wrong; We somehow tried to verify authentication without having one.
|
||||||
|
Logger.log("d", "Attempted to verify auth without having one.")
|
||||||
|
self._authentication_id = None
|
||||||
|
self._authentication_key = None
|
||||||
|
self.setAuthenticationState(AuthState.NotAuthenticated)
|
||||||
|
elif status_code == 403:
|
||||||
|
Logger.log("d",
|
||||||
|
"While trying to verify the authentication state, we got a forbidden response. Our own auth state was %s",
|
||||||
|
self._authentication_state)
|
||||||
|
self.setAuthenticationState(AuthState.AuthenticationDenied)
|
||||||
|
self._authentication_failed_message.show()
|
||||||
|
elif status_code == 200:
|
||||||
|
self.setAuthenticationState(AuthState.Authenticated)
|
||||||
|
# Now we know for sure that we are authenticated, send the material profiles to the machine.
|
||||||
|
self.sendMaterialProfiles()
|
||||||
|
|
||||||
|
def _checkAuthentication(self):
|
||||||
|
Logger.log("d", "Checking if authentication is correct for id %s and key %s", self._authentication_id, self._getSafeAuthKey())
|
||||||
|
self._get("auth/check/" + str(self._authentication_id), onFinished=self._onCheckAuthenticationFinished)
|
||||||
|
|
||||||
|
def _onCheckAuthenticationFinished(self, reply):
|
||||||
|
if str(self._authentication_id) not in reply.url().toString():
|
||||||
|
Logger.log("w", "Got an old id response.")
|
||||||
|
# Got response for old authentication ID.
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
data = json.loads(bytes(reply.readAll()).decode("utf-8"))
|
||||||
|
except json.decoder.JSONDecodeError:
|
||||||
|
Logger.log("w", "Received an invalid authentication check from printer: Not valid JSON.")
|
||||||
|
return
|
||||||
|
|
||||||
|
if data.get("message", "") == "authorized":
|
||||||
|
Logger.log("i", "Authentication was approved")
|
||||||
|
self.setAuthenticationState(AuthState.Authenticated)
|
||||||
|
self._saveAuthentication()
|
||||||
|
|
||||||
|
# Double check that everything went well.
|
||||||
|
self._verifyAuthentication()
|
||||||
|
|
||||||
|
# Notify the user.
|
||||||
|
self._resetAuthenticationRequestedMessage()
|
||||||
|
self._authentication_succeeded_message.show()
|
||||||
|
elif data.get("message", "") == "unauthorized":
|
||||||
|
Logger.log("i", "Authentication was denied.")
|
||||||
|
self.setAuthenticationState(AuthState.AuthenticationDenied)
|
||||||
|
self._authentication_failed_message.show()
|
||||||
|
|
||||||
|
def _saveAuthentication(self):
|
||||||
|
global_container_stack = Application.getInstance().getGlobalContainerStack()
|
||||||
|
if global_container_stack:
|
||||||
|
if "network_authentication_key" in global_container_stack.getMetaData():
|
||||||
|
global_container_stack.setMetaDataEntry("network_authentication_key", self._authentication_key)
|
||||||
|
else:
|
||||||
|
global_container_stack.addMetaDataEntry("network_authentication_key", self._authentication_key)
|
||||||
|
|
||||||
|
if "network_authentication_id" in global_container_stack.getMetaData():
|
||||||
|
global_container_stack.setMetaDataEntry("network_authentication_id", self._authentication_id)
|
||||||
|
else:
|
||||||
|
global_container_stack.addMetaDataEntry("network_authentication_id", self._authentication_id)
|
||||||
|
|
||||||
|
# Force save so we are sure the data is not lost.
|
||||||
|
Application.getInstance().saveStack(global_container_stack)
|
||||||
|
Logger.log("i", "Authentication succeeded for id %s and key %s", self._authentication_id,
|
||||||
|
self._getSafeAuthKey())
|
||||||
|
else:
|
||||||
|
Logger.log("e", "Unable to save authentication for id %s and key %s", self._authentication_id,
|
||||||
|
self._getSafeAuthKey())
|
||||||
|
|
||||||
|
def _onRequestAuthenticationFinished(self, reply):
|
||||||
|
try:
|
||||||
|
data = json.loads(bytes(reply.readAll()).decode("utf-8"))
|
||||||
|
except json.decoder.JSONDecodeError:
|
||||||
|
Logger.log("w", "Received an invalid authentication request reply from printer: Not valid JSON.")
|
||||||
|
self.setAuthenticationState(AuthState.NotAuthenticated)
|
||||||
|
return
|
||||||
|
|
||||||
|
self.setAuthenticationState(AuthState.AuthenticationReceived)
|
||||||
|
self._authentication_id = data["id"]
|
||||||
|
self._authentication_key = data["key"]
|
||||||
|
Logger.log("i", "Got a new authentication ID (%s) and KEY (%s). Waiting for authorization.",
|
||||||
|
self._authentication_id, self._getSafeAuthKey())
|
||||||
|
|
||||||
|
def _requestAuthentication(self):
|
||||||
|
self._authentication_requested_message.show()
|
||||||
|
self._authentication_timer.start()
|
||||||
|
|
||||||
|
# Reset any previous authentication info. If this isn't done, the "Retry" action on the failed message might
|
||||||
|
# give issues.
|
||||||
|
self._authentication_key = None
|
||||||
|
self._authentication_id = None
|
||||||
|
|
||||||
|
self._post("auth/request",
|
||||||
|
json.dumps({"application": "Cura-" + Application.getInstance().getVersion(),
|
||||||
|
"user": self._getUserName()}).encode(),
|
||||||
|
onFinished=self._onRequestAuthenticationFinished)
|
||||||
|
|
||||||
|
self.setAuthenticationState(AuthState.AuthenticationRequested)
|
||||||
|
|
||||||
|
def _onAuthenticationRequired(self, reply, authenticator):
|
||||||
|
if self._authentication_id is not None and self._authentication_key is not None:
|
||||||
|
Logger.log("d",
|
||||||
|
"Authentication was required for printer: %s. Setting up authenticator with ID %s and key %s",
|
||||||
|
self._id, self._authentication_id, self._getSafeAuthKey())
|
||||||
|
authenticator.setUser(self._authentication_id)
|
||||||
|
authenticator.setPassword(self._authentication_key)
|
||||||
|
else:
|
||||||
|
Logger.log("d", "No authentication is available to use for %s, but we did got a request for it.", self._key)
|
||||||
|
|
||||||
def _onGetPrintJobFinished(self, reply):
|
def _onGetPrintJobFinished(self, reply):
|
||||||
status_code = reply.attribute(QNetworkRequest.HttpStatusCodeAttribute)
|
status_code = reply.attribute(QNetworkRequest.HttpStatusCodeAttribute)
|
||||||
|
|
||||||
|
@ -104,3 +332,22 @@ class LegacyUM3OutputDevice(NetworkedPrinterOutputDevice):
|
||||||
else:
|
else:
|
||||||
Logger.log("w",
|
Logger.log("w",
|
||||||
"Got status code {status_code} while trying to get printer data".format(status_code = status_code))
|
"Got status code {status_code} while trying to get printer data".format(status_code = status_code))
|
||||||
|
|
||||||
|
## Convenience function to "blur" out all but the last 5 characters of the auth key.
|
||||||
|
# This can be used to debug print the key, without it compromising the security.
|
||||||
|
def _getSafeAuthKey(self):
|
||||||
|
if self._authentication_key is not None:
|
||||||
|
result = self._authentication_key[-5:]
|
||||||
|
result = "********" + result
|
||||||
|
return result
|
||||||
|
|
||||||
|
return self._authentication_key
|
||||||
|
|
||||||
|
## Convenience function to get the username from the OS.
|
||||||
|
# The code was copied from the getpass module, as we try to use as little dependencies as possible.
|
||||||
|
def _getUserName(self):
|
||||||
|
for name in ("LOGNAME", "USER", "LNAME", "USERNAME"):
|
||||||
|
user = os.environ.get(name)
|
||||||
|
if user:
|
||||||
|
return user
|
||||||
|
return "Unknown User" # Couldn't find out username.
|
Loading…
Add table
Add a link
Reference in a new issue