forked from ctrl_packages/epics_launcher
@@ -40,8 +40,39 @@ optional arguments:
|
||||
|
||||
_Note:_ `--position` - 0 0 is on the top left, -1 -1 is on the lower right.
|
||||
|
||||
Password can be added to JSON configuration file(s) as follows:
|
||||
```bash
|
||||
pylauncher-protect <configuration>
|
||||
```
|
||||
Password can be added in the same call using the option
|
||||
|
||||
* `-p (--password) <password>`
|
||||
|
||||
If not, the user will be prompted to enter it.
|
||||
Password can be added to all the configuration file referenced within configuration file and any files referenced in the referenced file and so on using the option
|
||||
|
||||
* `-r (--recursive)`
|
||||
|
||||
For all available options and detailed help run
|
||||
|
||||
```bash
|
||||
~$ pylauncher-protect -h
|
||||
usage: pylauncher-protect [-h] [--password PASSWORD] [--recursive] configuration
|
||||
Example: pylauncher-protect -r menus/menu.json -p *****
|
||||
|
||||
positional arguments:
|
||||
configuration menu/configuration file
|
||||
|
||||
optional arguments:
|
||||
-h, --help show this help message and exit
|
||||
--password PASSWORD, -p PASSWORD
|
||||
password to be added to json file, if not provided
|
||||
user is prompted to enter it
|
||||
--recursive, -r add recursively to all files referenced in json
|
||||
```
|
||||
|
||||
## Configuration
|
||||
Launcher menus are defined via JSON configuration file(s). On top level, the configuration of the menu is divided in the following 3 sections:
|
||||
Launcher menus are defined via JSON configuration file(s). On top level, the configuration of the menu is divided in the following 4 sections:
|
||||
|
||||
* __menu-title__ - An optional section to set the menu title. If no title is specified a file name is used instead.
|
||||
|
||||
@@ -78,8 +109,13 @@ Launcher menus are defined via JSON configuration file(s). On top level, the con
|
||||
}
|
||||
]
|
||||
```
|
||||
* __password__ - Hash of a password guarding a section of menu and all submenus. Main section to define launcher items. The type of each item is defined with the `type` property. All supported types with available parameters are described in the next section.
|
||||
|
||||
An detailed example can be found at [examples/menus/menu_example.json](examples/menus/menu_example.json).
|
||||
```json
|
||||
"password": "5f4dcc3b5aa765d61d8327deb882cf99"
|
||||
```
|
||||
|
||||
A detailed example can be found at [examples/menus/menu_example.json](examples/menus/menu_example.json).
|
||||
|
||||
### Menu Items
|
||||
|
||||
@@ -91,7 +127,7 @@ For any menu item the two optional parameters __help_link__ and __tip__ can be s
|
||||
|
||||
|
||||
#### Styles
|
||||
The appearance of a menu item can be customized via styles and themes. Therefore each menu item has following 2 optional paramters:
|
||||
The appearance of a menu item can be customized via styles and themes. Therefore each menu item has following 2 optional parameters:
|
||||
|
||||
* __style__ - Enables very flexible customization with [QSS](http://doc.qt.io/qt-4.8/stylesheet-syntax.html) syntax
|
||||
* __theme__ - Enables customization using one of the predefined themes. How to define a theme is described in section [Stylesheet](#stylesheet). However we strongly discourage the use of theme on a per menu item basis.
|
||||
@@ -295,6 +331,7 @@ LauncherDetachButton{
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
# Installation
|
||||
## Anaconda
|
||||
Anaconda comes with all required packages for __pylauncher__. To install the package use
|
||||
@@ -343,3 +380,4 @@ _Note:_ To be able to build the Anaconda package you need to have the `patchelf`
|
||||
conda create -n build_environment python patchelf
|
||||
source activate build_environment
|
||||
```
|
||||
|
||||
|
||||
@@ -24,6 +24,7 @@ build:
|
||||
entry_points:
|
||||
- pylauncher = pylauncher.launcher:main
|
||||
- pylauncher-convert = pylauncher.convert.convert:main
|
||||
- pylauncher-protect = pylauncher.protect:main
|
||||
|
||||
about:
|
||||
home: https://github.psi.ch/projects/COS/repos/pylauncher/browse
|
||||
|
||||
+89
-18
@@ -1,24 +1,95 @@
|
||||
{
|
||||
"menu-title":
|
||||
{"text": "F_L1","theme": "green", "style": "color: #FF3388"},
|
||||
"password": "5f4dcc3b5aa765d61d8327deb882cf99",
|
||||
"menu-title": {
|
||||
"text": "F_L1",
|
||||
"theme": "green",
|
||||
"style": "color: #FF3388"
|
||||
},
|
||||
"file-choice": [
|
||||
{"text": "Load F_L2", "file": "../menus/menu_2.json"}
|
||||
{
|
||||
"text": "Load F_L2",
|
||||
"file": "../menus/menu_2.json"
|
||||
}
|
||||
],
|
||||
"menu": [
|
||||
{"type": "title", "text": "General"},
|
||||
{"type": "cmd", "text": "Who is online", "command": "xeyes", "tip": "List of currently connected users.", "help-link": "http://www.google.com"},
|
||||
{"type": "title", "text": "HighLevel Applications", "theme": "green"},
|
||||
{"type": "cmd", "text": "ScreenMonitorTool", "command": "ls", "theme": "green", "style": "color: #FF3388"},
|
||||
{"type": "cmd", "text": "QE Measurement", "command": "ls -l", "theme": "green"},
|
||||
{"type": "separator"},
|
||||
{"type": "title", "text": "Utilities", "theme": "green"},
|
||||
{"type": "cmd", "text": "Remote network access control by control room", "command": "ls"},
|
||||
{"type": "menu", "text": "Strip-tool", "file": "menu_10.json"},
|
||||
{"type": "caqtdm", "text": "CA-screen", "macros": "SYS=TEST", "panel": "screen.ui" },
|
||||
{"type": "medm", "text": "MEDM-screen", "macros": "SYS=TEST", "panel": "screen.adl" },
|
||||
{"type": "cmd", "text": "Test", "command": "pep -mc S10BC02-MBND100:I-SET"},
|
||||
{"type": "cmd", "text": "Test 2", "command": "pylauncher -h"},
|
||||
{"type": "pep", "text": "Pep type test (.prc)", "panel": "S_TEST_REMOTECTRL_GUDE_SF-CTRL-TEST01.prc"},
|
||||
{"type": "pep", "text": "Pep type test (.prc)", "param": "-mc S10BC02-MBND100:I-SET"}
|
||||
{
|
||||
"type": "title",
|
||||
"text": "General"
|
||||
},
|
||||
{
|
||||
"type": "cmd",
|
||||
"text": "Who is online",
|
||||
"command": "xeyes",
|
||||
"tip": "List of currently connected users.",
|
||||
"help-link": "http://www.google.com"
|
||||
},
|
||||
{
|
||||
"type": "title",
|
||||
"text": "HighLevel Applications",
|
||||
"theme": "green"
|
||||
},
|
||||
{
|
||||
"type": "cmd",
|
||||
"text": "ScreenMonitorTool",
|
||||
"command": "ls",
|
||||
"theme": "green",
|
||||
"style": "color: #FF3388"
|
||||
},
|
||||
{
|
||||
"type": "cmd",
|
||||
"text": "QE Measurement",
|
||||
"command": "ls -l",
|
||||
"theme": "green"
|
||||
},
|
||||
{
|
||||
"type": "separator"
|
||||
},
|
||||
{
|
||||
"type": "title",
|
||||
"text": "Utilities",
|
||||
"theme": "green"
|
||||
},
|
||||
{
|
||||
"type": "cmd",
|
||||
"text": "Remote network access control by control room",
|
||||
"command": "ls"
|
||||
},
|
||||
{
|
||||
"type": "menu",
|
||||
"text": "Strip-tool",
|
||||
"file": "menu_10.json"
|
||||
},
|
||||
{
|
||||
"type": "caqtdm",
|
||||
"text": "CA-screen",
|
||||
"macros": "SYS=TEST",
|
||||
"panel": "screen.ui"
|
||||
},
|
||||
{
|
||||
"type": "medm",
|
||||
"text": "MEDM-screen",
|
||||
"macros": "SYS=TEST",
|
||||
"panel": "screen.adl"
|
||||
},
|
||||
{
|
||||
"type": "cmd",
|
||||
"text": "Test",
|
||||
"command": "pep -mc S10BC02-MBND100:I-SET"
|
||||
},
|
||||
{
|
||||
"type": "cmd",
|
||||
"text": "Test 2",
|
||||
"command": "pylauncher -h"
|
||||
},
|
||||
{
|
||||
"type": "pep",
|
||||
"text": "Pep type test (.prc)",
|
||||
"panel": "S_TEST_REMOTECTRL_GUDE_SF-CTRL-TEST01.prc"
|
||||
},
|
||||
{
|
||||
"type": "pep",
|
||||
"text": "Pep type test (.prc)",
|
||||
"param": "-mc S10BC02-MBND100:I-SET"
|
||||
}
|
||||
]
|
||||
}
|
||||
+51
-5
@@ -5,19 +5,19 @@
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
|
||||
|
||||
# ---------python 2/3 compatibility imports---------
|
||||
|
||||
import platform
|
||||
import argparse
|
||||
import copy
|
||||
import enum
|
||||
import shlex
|
||||
import subprocess
|
||||
import hashlib
|
||||
import sys
|
||||
|
||||
from PyQt5 import QtCore
|
||||
from PyQt5.QtCore import Qt
|
||||
from PyQt5.QtGui import QDesktopServices, QIcon, QCursor, QKeySequence
|
||||
from PyQt5.QtWidgets import QMainWindow, QMenu, QWidgetAction, QLineEdit, QWidget, QHBoxLayout, QToolButton, QVBoxLayout, QCheckBox, QAction, QLabel, QPushButton, QApplication
|
||||
from PyQt5.QtWidgets import QMainWindow, QMenu, QWidgetAction, QLineEdit, QWidget, QHBoxLayout, QToolButton, QVBoxLayout, QCheckBox, QAction, QLabel, QPushButton, QApplication, QInputDialog, QMessageBox
|
||||
|
||||
from .launcher_model import *
|
||||
|
||||
@@ -30,6 +30,35 @@ def stringContains(string, substring, caseSensitive):
|
||||
else:
|
||||
return substring.lower() in string.lower()
|
||||
|
||||
def convertPwdToHash(password):
|
||||
m = hashlib.md5()
|
||||
m.update(password.encode())
|
||||
return m.hexdigest()
|
||||
|
||||
def verifyPassword(object, password):
|
||||
passwordInput = showPasswordDialog(object)
|
||||
if passwordInput is not None:
|
||||
convertedPwd = convertPwdToHash(passwordInput)
|
||||
if convertedPwd == password:
|
||||
return True
|
||||
else:
|
||||
showWrongPasswordDialog(object)
|
||||
return False
|
||||
def showPasswordDialog(object):
|
||||
|
||||
password, ok = QInputDialog.getText(object.window(),
|
||||
'Password',
|
||||
'Enter password:')
|
||||
if ok:
|
||||
return password
|
||||
return None
|
||||
|
||||
def showWrongPasswordDialog(parent):
|
||||
messageBox = QMessageBox(parent.window())
|
||||
messageBox.setText("Wrong password")
|
||||
messageBox.setStandardButtons(QMessageBox.Ok)
|
||||
returnValue = messageBox.exec()
|
||||
|
||||
|
||||
class SearchOptions(enum.Enum):
|
||||
|
||||
@@ -78,6 +107,7 @@ class LauncherWindow(QMainWindow):
|
||||
theme_base)
|
||||
|
||||
self.menuModel = self.buildMenuModel(rootFilePath)
|
||||
|
||||
self.setWindowTitle(self.menuModel.main_title.text)
|
||||
# QMainWindow has predefined layout. Content should be in the central
|
||||
# widget. Create widget with a QVBoxLayout and set it as central.
|
||||
@@ -117,6 +147,10 @@ class LauncherWindow(QMainWindow):
|
||||
if self.use_sbox:
|
||||
self.searchInput.setMouseTracking(True)
|
||||
|
||||
if self.menuModel.password is not None:
|
||||
if not verifyPassword(self, self.menuModel.password):
|
||||
sys.exit(-1)
|
||||
|
||||
def setNewView(self, rootMenuFile, text=None):
|
||||
"""Rebuild launcher from new config file.
|
||||
|
||||
@@ -128,6 +162,7 @@ class LauncherWindow(QMainWindow):
|
||||
del self.menuModel
|
||||
|
||||
self.menuModel = self.buildMenuModel(rootMenuFile)
|
||||
|
||||
if text:
|
||||
self.setWindowTitle(text)
|
||||
else:
|
||||
@@ -246,6 +281,7 @@ class LauncherMenu(QMenu):
|
||||
candidate = self
|
||||
while type(candidate) != LauncherWindow:
|
||||
candidate = candidate.parent()
|
||||
print(candidate)
|
||||
|
||||
return candidate
|
||||
|
||||
@@ -948,6 +984,7 @@ class LauncherCmdButton(LauncherNamedButton):
|
||||
def __init__(self, itemModel, sectionTitle=None, parent=None):
|
||||
LauncherNamedButton.__init__(self, itemModel, sectionTitle, parent)
|
||||
self.cmd = itemModel.cmd
|
||||
self.pwd = itemModel.pwd
|
||||
self.clicked.connect(self.executeCmd)
|
||||
|
||||
toolTip = ""
|
||||
@@ -971,16 +1008,25 @@ class LauncherCmdButton(LauncherNamedButton):
|
||||
cb.setText(self.cmd, mode=cb.Clipboard)
|
||||
|
||||
|
||||
def executeCmd(self):
|
||||
def executeCmd(self, itemModel):
|
||||
""" Run specified command as a separate process
|
||||
|
||||
Runs commands from the same environment ($PATH) as the launcher was
|
||||
started. Apart from "bash" it aboarts scripts without shebang on
|
||||
first line (strictly).
|
||||
"""
|
||||
|
||||
self.parent().hideAll() # When done hide all popuped menus
|
||||
try:
|
||||
subprocess.Popen(shlex.split(self.cmd))
|
||||
if self.pwd is not None:
|
||||
"""passwordInput = showPasswordDialog(self)
|
||||
if passwordInput is not None:
|
||||
convertedPwd = convertPwdToHash(passwordInput)
|
||||
if convertedPwd != self.pwd:
|
||||
showWrongPasswordDialog()
|
||||
return"""
|
||||
if verifyPassword(self, self.pwd):
|
||||
subprocess.Popen(shlex.split(self.cmd))
|
||||
except OSError:
|
||||
warn_msg = "Command \"" + self.cmd + "\" cannot be executed. " + \
|
||||
"Wrong path or bad/no interpreter."
|
||||
|
||||
@@ -4,17 +4,17 @@
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
|
||||
|
||||
# ---------python 2/3 compatibility imports---------
|
||||
import sys
|
||||
from urllib.request import urlopen
|
||||
from urllib.parse import urljoin
|
||||
from urllib.error import URLError
|
||||
# ------end of python 2/3 compatibility imports-----
|
||||
|
||||
import os
|
||||
import re
|
||||
import json
|
||||
import logging
|
||||
import hashlib
|
||||
import sys
|
||||
|
||||
def join_launcher_path(base, file):
|
||||
# In case file is absolute path ora full url, base will be ignored
|
||||
@@ -63,6 +63,7 @@ class launcher_menu_model(object):
|
||||
"""
|
||||
|
||||
def __init__(self, parent, menu_file_path, level, launcher_cfg):
|
||||
self.password = None
|
||||
self.menu_items = list()
|
||||
self.parent = parent
|
||||
self.level = level
|
||||
@@ -87,6 +88,8 @@ class launcher_menu_model(object):
|
||||
if 0 == self.level:
|
||||
self.flags = menu.get("flags", dict())
|
||||
|
||||
self.password = menu.get("password", None)
|
||||
|
||||
main_title_item = menu.get("menu-title", dict())
|
||||
self.main_title = launcher_main_title_item(
|
||||
main_title_item,
|
||||
@@ -182,7 +185,7 @@ class launcher_menu_model(object):
|
||||
sys.exit()
|
||||
|
||||
def __repr__(self):
|
||||
s = "{} (nelm: {})\n".format(self.main_title, len(self.menu_items))
|
||||
s = "{} (nelm: {}) {}\n".format(self.main_title, len(self.menu_items), self.password)
|
||||
tabs = "\t" *self.level
|
||||
strings = list(map(repr,self.menu_items))
|
||||
strings = [tabs + str for str in strings]
|
||||
@@ -258,6 +261,7 @@ class launcher_cmd_item(launcher_menu_model_item):
|
||||
|
||||
self.cmd = self.cmd.format(**params)
|
||||
|
||||
self.pwd = parent.password
|
||||
|
||||
class launcher_sub_menu_item(launcher_menu_model_item):
|
||||
|
||||
@@ -302,4 +306,4 @@ class launcher_title_item(launcher_menu_model_item):
|
||||
"""Text menu separator."""
|
||||
|
||||
def __init__(self, parent, item):
|
||||
launcher_menu_model_item.__init__(self, parent, item)
|
||||
launcher_menu_model_item.__init__(self, parent, item)
|
||||
Executable
+100
@@ -0,0 +1,100 @@
|
||||
#!/usr/bin/env python
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
|
||||
|
||||
import os
|
||||
import re
|
||||
import json
|
||||
import logging
|
||||
import hashlib
|
||||
import argparse
|
||||
import collections
|
||||
import sys
|
||||
|
||||
def loadJson(filePath):
|
||||
try:
|
||||
with open(filePath) as json_file:
|
||||
data = json.load(json_file, object_pairs_hook=collections.OrderedDict)
|
||||
return data
|
||||
except json.decoder.JSONDecodeError as error:
|
||||
error_msg = "File \"" + filePath + "\" is empty. " \
|
||||
"Json cannot be loaded."
|
||||
logging.error(error_msg)
|
||||
except IOError as error:
|
||||
error_msg = "Json cannot be loaded from file \"" + filePath + "\". " \
|
||||
"Wrong path."
|
||||
logging.error(error_msg)
|
||||
|
||||
sys.exit(-1)
|
||||
|
||||
def processFile(filePath, password, recursive):
|
||||
data = loadJson(filePath)
|
||||
dirname = os.path.dirname(filePath)
|
||||
protected = addPassword(data, hashPassword(password))
|
||||
saveFile(protected, filePath)
|
||||
if recursive:
|
||||
files = findAllFiles(data)
|
||||
for file in files:
|
||||
processFile(os.path.join(dirname, file), password, recursive)
|
||||
|
||||
def findAllFiles(root):
|
||||
fileList = []
|
||||
for key in root:
|
||||
if key == 'file':
|
||||
fileList.append(root[key])
|
||||
elif isinstance(root[key], list):
|
||||
for element in root[key]:
|
||||
if isinstance(element, dict):
|
||||
fileList += findAllFiles(element)
|
||||
elif isinstance(root[key], dict):
|
||||
fileList += findAllFiles(root[key])
|
||||
return fileList
|
||||
|
||||
def addPassword(root, password):
|
||||
|
||||
# Append password to json at the beginning
|
||||
root["password"]=password
|
||||
root.move_to_end('password', last=False)
|
||||
return root
|
||||
|
||||
def hashPassword(password):
|
||||
m = hashlib.md5()
|
||||
m.update(password.encode())
|
||||
return m.hexdigest()
|
||||
|
||||
def saveFile(jsonWithPwd, filename):
|
||||
with open(filename, 'w') as outfile:
|
||||
json.dump(jsonWithPwd, outfile, indent=4)
|
||||
|
||||
def main():
|
||||
""" Main logic """
|
||||
|
||||
# Parse input arguments
|
||||
argsParse = argparse.ArgumentParser(description='Example: pylauncher-protect -r menus/menu.json -p *****')
|
||||
argsParse.add_argument('configuration',
|
||||
help="menu/configuration file")
|
||||
argsParse.add_argument('--password', '-p',
|
||||
help="password to be added to json file, if not provided user is prompted to enter it")
|
||||
argsParse.add_argument('--recursive', '-r',
|
||||
help="add recursively to all files referenced in json", action='store_true')
|
||||
args = argsParse.parse_args()
|
||||
|
||||
# Add password to json structure
|
||||
if args.password == None:
|
||||
# Ask for password
|
||||
password = input("Enter password: ")
|
||||
else:
|
||||
password = args.password
|
||||
|
||||
# Get current dir and create path to json file
|
||||
cwd = os.getcwd()
|
||||
jsonFilePath = os.path.join(cwd, args.configuration)
|
||||
|
||||
# Add password to file and, if recursive, to all the files within
|
||||
processFile(jsonFilePath, password, args.recursive)
|
||||
|
||||
# Start program here
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
Reference in New Issue
Block a user