diff --git a/README.md b/README.md index 9c208fd..e38483f 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,7 @@ The following will build the demo website included in this repository. $ git clone https://github.com/StoneLabs/webduino-generator $ cd webduino-generator/ $ wgen generate input -$ arduino main/main.ino +$ wgen open Or use arduino-cli to compile and upload directly from the shell (linux only) $ ./uploader.sh @@ -29,7 +29,7 @@ Aside from build a random folder you can create a project. By default a simple h ``` $ wgen init $ wgen build -$ arduino output/main/main.ino +$ wgen open ``` ### Note diff --git a/setup.py b/setup.py index d686c86..efe978c 100644 --- a/setup.py +++ b/setup.py @@ -3,7 +3,7 @@ setup( name='webduino-generator', - version='0.4', + version='0.5', license='UNLICENSE', url='https://github.com/StoneLabs/webduino-generator', author='Levy Ehrstein', @@ -17,5 +17,5 @@ 'console_scripts': ['webduino-generator=webduino_generator.entrypoint:main', 'wgen=webduino_generator.entrypoint:main'], }, - install_requires=["jinja2", "rich"], + install_requires=["jinja2", "rich", "simple-term-menu"], ) \ No newline at end of file diff --git a/webduino_generator/__init__.py b/webduino_generator/__init__.py index fa89108..e13f6b2 100644 --- a/webduino_generator/__init__.py +++ b/webduino_generator/__init__.py @@ -2,4 +2,5 @@ __author__ = 'Levy Ehrstein' __email__ = 'levyehrstein@googlemail.com' -__version__ = '0.4' +__website__ = 'https://github.com/StoneLabs/webduino-generator' +__version__ = '0.5' diff --git a/webduino_generator/arduino.py b/webduino_generator/arduino.py new file mode 100644 index 0000000..22c3afd --- /dev/null +++ b/webduino_generator/arduino.py @@ -0,0 +1,159 @@ +import subprocess +import json + +from simple_term_menu import TerminalMenu +from .helper import get_tool + + +def get_ide_path(userio): + # Get arduino IDE location + ide_path = get_tool("arduino") + if ide_path is None: + userio.error("Could not locate 'arduino' command. Is the arduino IDE istalled?") + userio.print("IDE located: " + ide_path, verbose=True) + + return ide_path + + +def get_cli_path(userio): + # Get arduino IDE location + cli_path = get_tool("arduino-cli") + if cli_path is None: + userio.error("Could not locate 'arduino-cli' command. Is arduino-cli istalled?") + userio.print("CLI located: " + cli_path, verbose=True) + + return cli_path + + +def get_boards_json(userio, list_all=False): + cli_path = get_cli_path(userio) + + if list_all: + result = subprocess.run([cli_path, "board", "listall", "--format=json"], stdout=subprocess.PIPE) + else: + result = subprocess.run([cli_path, "board", "list", "--format=json"], stdout=subprocess.PIPE) + + userio.print("Called arduino-cli with:", verbose=True) + userio.print(result.args, verbose=True) + userio.print("Dumping arduino-cli response:", verbose=True) + userio.print(result.stdout.decode('utf-8'), verbose=True) + + if not result.returncode == 0: + userio.error("arduino-cli exited with code " + str(result.returncode)) + + try: + boards = json.loads(result.stdout.decode('utf-8')) + except Exception: + userio.error("arduino-cli returned invalid JSON") + + return boards + + +def get_boards(userio): + boards = get_boards_json(userio, True) + + # arduino-cli board listall packes the result in a dict + if "boards" not in boards: + userio.error("Could not parse arduino-cli output") + boards = boards["boards"] + + # Filter out invalid entries (or unwanted) + boards = [board for board in boards if "name" in board] + boards = [board for board in boards if "FQBN" in board] + + # Sort boards for the user + boards = sorted(boards, key=lambda board: board["name"]) + + userio.print("Dumping processed arduino-cli response:", verbose=True) + userio.print(boards, verbose=True) + + return boards + + +def get_boards_connected(userio): + boards = get_boards_json(userio, False) + + processed = [] + for board in boards: + if "boards" not in board: + continue + if "address" not in board: + continue + if len(board["boards"]) != 1: + continue + if "FQBN" not in board["boards"][0]: + continue + if "name" not in board["boards"][0]: + continue + processed += [{ + "name": board["boards"][0]["name"], + "FQBN": board["boards"][0]["FQBN"], + "address": board["address"] + }] + + # Sort boards for the user + processed = sorted(processed, key=lambda board: board["name"]) + + userio.print("Dumping processed arduino-cli response:", verbose=True) + userio.print(processed, verbose=True) + + return processed + + +def get_board(userio): + boards = get_boards(userio) + + if len(boards) == 0: + userio.error("No boards found!") + + # Query user to select a board + userio.print("Please select target board:") + terminal_menu = TerminalMenu([board["name"] for board in boards], menu_highlight_style=None) + + selection = terminal_menu.show() + + # Menu cancled by user + if selection is None: + exit(0) + board = boards[selection] + + userio.print("Selected board: ", verbose=True) + userio.print(board, verbose=True) + + return board["name"], board["FQBN"] + + +def get_board_connected(userio): + boards = get_boards_connected(userio) + + if len(boards) == 0: + userio.error("No boards found!") + + # Query user to select a board + userio.print("Please select target board:") + terminal_menu = TerminalMenu([board["address"] + ": " + board["name"] + for board in boards], menu_highlight_style=None) + + selection = terminal_menu.show() + + # Menu cancled by user + if selection is None: + exit(0) + board = boards[selection] + + userio.print("Selected board: ", verbose=True) + userio.print(board, verbose=True) + + return board["name"], board["FQBN"], board["address"] + + +def sketch_compile(userio, sketch_path, fqbn): + # Compile sketch + cli_path = get_cli_path(userio) + subprocess.run([cli_path, "compile", "--fqbn", fqbn, sketch_path]) + + +def sketch_upload(userio, sketch_path, fqbn, address): + # Upload sketch + cli_path = get_cli_path(userio) + subprocess.run([cli_path, "upload", "-p", address, "--fqbn", fqbn, sketch_path]) diff --git a/webduino_generator/entrypoint.py b/webduino_generator/entrypoint.py index d67064f..03aa0dd 100644 --- a/webduino_generator/entrypoint.py +++ b/webduino_generator/entrypoint.py @@ -1,14 +1,18 @@ import argparse +import subprocess +import json -from .__init__ import __version__ +from .__init__ import __version__, __website__ from .userio import UserIO, get_ssid_pass -from .helper import cpp_str_esc, cpp_img_esc, get_files_rec, shorten -from .project import project_make_new, project_generate +from .helper import cpp_str_esc, cpp_img_esc, get_files_rec, shorten, get_tool +from .project import Project +from .arduino import get_ide_path from .generator import * def command_version(userio, args): userio.print("Current version: " + __version__) + userio.print(__website__) def command_generate(userio, args): @@ -43,12 +47,45 @@ def command_generate(userio, args): def command_init(userio, args): - project_make_new(userio, args.target, args.force, - args.mode, args.ssid, args.port) + Project.create_project(userio, args.target, args.force, + args.mode, args.ssid, args.port) def command_build(userio, args): - project_generate(userio, args.target, args.quiet) + project = Project(userio, args.target) + project.generate(args.quiet) + + +def command_open(userio, args): + userio.section("Opening project output") + + # Get project output location + project = Project(userio, args.target) + sketch_path = project.get_sketch_path() + userio.print("Sketch located: " + sketch_path, verbose=True) + + # Get arduino IDE location + ide_path = get_ide_path(userio) + + # Launch IDE + if args.detach: + userio.print("Opening IDE detached...") + subprocess.Popen([ide_path, sketch_path], + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL) + else: + userio.print("Opening IDE...") + subprocess.call([ide_path, sketch_path]) + + +def command_compile(userio, args): + project = Project(userio, args.target) + project.compile(save=args.save, force_select=args.select_device) + + +def command_upload(userio, args): + project = Project(userio, args.target) + project.upload() def main(): @@ -100,19 +137,40 @@ def main(): help="Connection mode/library to be used") parser_init.add_argument("-f", "--force", action="store_true", dest='force', - help="Delete files that block project creation.") + help="Delete files that block project creation") parser_build = subparsers.add_parser("build", help="Generate Arduino code from current project") parser_build.add_argument("target", metavar="target", type=str, default=".", nargs="?", - help="Target folder where project will be created") + help="Root folder of target project") parser_build.add_argument("-q", "--quiet", action="store_true", dest='quiet', help="Hides password warning") + parser_open = subparsers.add_parser("open", help="Open generated code in arduino ide") + parser_open.add_argument("target", metavar="target", type=str, + default=".", nargs="?", + help="Root folder of target project") + parser_open.add_argument("-d", "--detach", + action="store_true", dest='detach', + help="Spawns IDE in a new thread") + parser_compile = subparsers.add_parser("compile", help="Compile Arduino code from current project") + parser_compile.add_argument("target", metavar="target", type=str, + default=".", nargs="?", + help="Root folder of target project") + parser_compile.add_argument("--select-device", + action="store_true", dest='select_device', + help="Ignore saved target device and select another one") + parser_compile.add_argument("--save", + action="store_true", dest='save', + help="Safe selected target to project files") + parser_upload = subparsers.add_parser("upload", help="Upload Arduino code from current project") - parser_open = subparsers.add_parser("open", help="Open generated code in arduino ide") + parser_upload.add_argument("target", metavar="target", type=str, + default=".", nargs="?", + help="Root folder of target project") + parser_version = subparsers.add_parser("version", help="Display current version") # Global arguments @@ -147,11 +205,11 @@ def handle(): elif args.command == "build": command_build(userio, args) elif args.command == "compile": - raise NotImplementedError + command_compile(userio, args) elif args.command == "upload": - raise NotImplementedError + command_upload(userio, args) elif args.command == "open": - raise NotImplementedError + command_open(userio, args) elif args.command == "generate": command_generate(userio, args) else: diff --git a/webduino_generator/generator.py b/webduino_generator/generator.py index 4f98aa8..c235ed2 100644 --- a/webduino_generator/generator.py +++ b/webduino_generator/generator.py @@ -40,6 +40,10 @@ def get_input_data(userio, input_path): # Get list of all files files = get_files_rec(input_path) userio.print("Processing " + str(len(files)) + " files...", verbose=True) + + # Sort files for deterministic output + files = sorted(files) + userio.quick_table("", ["Input Files"], [[_file] for _file in files], verbose=True) @@ -107,6 +111,17 @@ def inner(mime, mime_hash): # Try to handle file non-binary UTF-8 file. with open(os.path.join(input_path, file_name), 'r', encoding="UTF-8") as file: if (file_name.endswith(".cpp")): + # Warn user when using includes + line_num = 0 + for line in file.readlines(): + line_num += 1 + if line.startswith("#include "): + userio.print("") + userio.warn(file_name + " line " + str(line_num) + ":") + userio.print("| Putting includes in input files is not recommended!") + userio.print("| This might cause conflicts or large sketch sizes!") + userio.print("| Please put them int the template files instead.") + # Handle dynamic content (cpp files) file_content = file.read().replace("\n", "\n\t") file_type = 2 # Dynamic content diff --git a/webduino_generator/helper.py b/webduino_generator/helper.py index 74ee666..f02bff1 100644 --- a/webduino_generator/helper.py +++ b/webduino_generator/helper.py @@ -1,5 +1,7 @@ import os +from shutil import which + def cpp_str_esc(s, encoding='ascii'): if isinstance(s, bytes): @@ -24,7 +26,7 @@ def get_files_rec(parent): rel_dir = os.path.relpath(dir_, parent) rel_file = os.path.join(rel_dir, file_name) - rel_file = rel_file.replace("\\","/") + rel_file = rel_file.replace("\\", "/") if rel_file.startswith("./"): rel_file = rel_file[2:] @@ -33,4 +35,11 @@ def get_files_rec(parent): def shorten(text, maxLength): - return str(text)[:maxLength] + ("..." if maxLength < len(str(text)) else "") \ No newline at end of file + return str(text)[:maxLength] + ("..." if maxLength < len(str(text)) else "") + + +def get_tool(name): + """Returns absolute path of command. + Returns None if command it not found.""" + + return which(name) diff --git a/webduino_generator/project.py b/webduino_generator/project.py index 168ea17..a7eac6b 100644 --- a/webduino_generator/project.py +++ b/webduino_generator/project.py @@ -5,172 +5,278 @@ from .helper import get_files_rec from .generator import get_template_path, get_demo_path, generate +from .arduino import sketch_compile, sketch_upload, get_board, get_board_connected from .userio import get_ssid_pass -def project_check(target): - project_file = os.path.join(target, "project.wgen") - return os.path.exists(project_file) and os.path.isfile(project_file) +class Project(): + userio = None + root_path = "" + + @staticmethod + def make_config(input_path, template_path, output_path, + mode, ssid, port) -> str: + '''Returns content of default config as string''' + + # Make new config file with default content + config = configparser.ConfigParser() + config["PROJECT"] = \ + { + "input_path": input_path, + "template_path": template_path, + "output_path": output_path, + } + config["METADATA"] = \ + { + "mode": mode, + "ssid": ssid, + "port": port, + } + + # Write to buffer and return content + with io.StringIO() as buffer: + config.write(buffer) + buffer.seek(0) + return buffer.read() + + @staticmethod + def create_project(userio, project_path, delete_block, mode, ssid, port): + '''Creates new project in specified location''' + + # Check port + if port < 0 or port > 65535: + userio.error("Invalid port!") + + # Check mode + if mode.lower() != "wifinina": + userio.error("Target mode not supported!\nSupported modes: wifinina") + + # Check ssid + if ssid == "": + ssid = userio.get_user("Please enter network credentials:", "SSID: ") + + # Check if target exists + if not os.path.exists(project_path): + userio.error("Path " + project_path + " is invalid or does not exist!") + + userio.section("Generating 'hello world' project") + + # Check if target is empty + if len(get_files_rec(project_path)) > 0: + userio.warn("Target folder (%s) is not empty!" + % os.path.abspath(project_path)) + + userio.warn("Data will %sbe deleted by this action!" + % ("" if delete_block else "not ")) + + userio.print("Press Enter to continue anyway. Ctrl+C to cancel!") + try: + input() + except KeyboardInterrupt: + return + + # Open project and do not check if its a valid one + project = Project(userio, project_path, check_valid=False) + + # Get paths to target files/folders beforehand + path_input = os.path.join(project_path, "input") + path_output = os.path.join(project_path, "output") + path_template = os.path.join(project_path, "template") + + path_input_src = get_demo_path() + path_template_src = get_template_path() + + path_config = project.get_config_folder_path() + path_config_file = project.get_config_file_path() + + # Helper function to delete file or folder + def delete_file_or_folder(target): + if os.path.isfile(target): + os.remove(target) + else: + shutil.rmtree(target) + def error_or_delete(target, name): + if os.path.exists(target): + if delete_block: + delete_file_or_folder(target) + else: + userio.error(name + " exists! (" + target + ")") -def project_config_make(input_path, template_path, output_path, - mode, ssid, port) -> str: - # Make new config file with default content - config = configparser.ConfigParser() - config["PROJECT"] = \ - { - "input_path": input_path, - "template_path": template_path, - "output_path": output_path, - } - config["METADATA"] = \ - { - "mode": mode, - "ssid": ssid, - "port": port, - } + # Check target files before we start + # as not to leave a half initialized project + error_or_delete(path_config, "Config folder") + error_or_delete(path_config_file, "Config file") + error_or_delete(path_input, "Input folder") + error_or_delete(path_template, "Template folder") + error_or_delete(path_output, "Output folder") - # Write to buffer and return content - with io.StringIO() as buffer: - config.write(buffer) - buffer.seek(0) - return buffer.read() + # Eventually, create project files + userio.print("Creating project files") + # Project config folder + os.mkdir(path_config) -def project_config_readproject(userio, config_path): - config = configparser.ConfigParser() - config.read(os.path.join(config_path, "project.wgen")) + # Project config file + with open(path_config_file, "w") as config_file: + config_file.write(Project.make_config(path_input, path_template, + path_output, mode, ssid, port)) - if "PROJECT" not in config.sections() or \ - "input_path" not in config["PROJECT"] or \ - "output_path" not in config["PROJECT"] or \ - "template_path" not in config["PROJECT"]: - userio.error("Invalid project file!") + # Project output folder + userio.print("Creating output folder", verbose=True) + os.mkdir(path_output) - input_path = os.path.join(config_path, config["PROJECT"]["input_path"]) - output_path = os.path.join(config_path, config["PROJECT"]["output_path"]) - template_path = os.path.join(config_path, config["PROJECT"]["template_path"]) + # Project output folder + userio.print("Creating input files", verbose=True) + shutil.copytree(path_input_src, path_input) - return input_path, output_path, template_path + userio.print("Creating template files", verbose=True) + shutil.copytree(path_template_src, path_template) + userio.section("Project created successfully.") + userio.print("Use 'webduino-generator build' to build your project.") -def project_config_readmeta(userio, config_path): - config = configparser.ConfigParser() - config.read(os.path.join(config_path, "project.wgen")) + def __init__(self, userio, root_path, check_valid=True): + '''Open existing project. Will error if invalid project + is passed and check_valid is True''' - if "METADATA" not in config.sections(): - userio.error("Invalid project file!") + self.userio = userio + self.root_path = root_path - return config["METADATA"] + if check_valid and not self.check(): + self.userio.error("Invalid project passed.") + def check(self): + '''Checks whether current project is a valid project''' -def project_make_new(userio, project_path, delete_block, mode, ssid, port): - # Check port - if port < 0 or port > 65535: - userio.error("Invalid port!") + config_path = self.get_config_file_path() + if not os.path.exists(config_path) or \ + not os.path.isfile(config_path): + return False + return True - # Check mode - if mode.lower() != "wifinina": - userio.error("Target mode not supported!\nSupported modes: wifinina") + def get_config_file_path(self): + '''Returns path to project.wgen of current project.''' - # Check ssid - if ssid == "": - ssid = userio.get_user("Please enter network credentials:", "SSID: ") + config_path = os.path.join(self.root_path, "project.wgen") + return config_path - # Check if target exists - if not os.path.exists(project_path): - userio.error("Path " + project_path + " is invalid or does not exist!") + def get_config_folder_path(self): + '''Returns path to .wgen config folder of current project.''' - userio.section("Generating 'hello world' project") + config_path = os.path.join(self.root_path, ".wgen") + return config_path - # Check if target is empty - if len(get_files_rec(project_path)) > 0: - userio.warn("Target folder (%s) is not empty!" - % os.path.abspath(project_path)) + def get_sketch_path(self): + '''Returns location of arduino output sketch. (main.ino) + Returns None if project was not build yet.''' - userio.warn("Data will %sbe deleted by this action!" - % ("" if delete_block else "not ")) + # Read project data + input_path, output_path, template_path = self.read_config_project() - userio.print("Press Enter to continue anyway. Ctrl+C to cancel!") - try: - input() - except KeyboardInterrupt: - return + # Check if project has been build yet + sketch_path = os.path.join(output_path, "main", "main.ino") + if not os.path.exists(sketch_path) or not os.path.isfile(sketch_path): + return None - # Get paths to target files/folders beforehand - path_input = os.path.join(project_path, "input") - path_output = os.path.join(project_path, "output") - path_template = os.path.join(project_path, "template") + return sketch_path - path_input_src = get_demo_path() - path_template_src = get_template_path() + def read_config_project(self): + '''Returns input_path, output_path, template_path of current + project.''' + config = configparser.ConfigParser() + config.read(self.get_config_file_path()) - path_config = os.path.join(project_path, ".wgen") - path_config_file = os.path.join(project_path, "project.wgen") - - # Helper function to delete file or folder - def delete_file_or_folder(target): - if os.path.isfile(target): - os.remove(target) - else: - shutil.rmtree(target) - - def error_or_delete(target, name): - if os.path.exists(target): - if delete_block: - delete_file_or_folder(target) - else: - userio.error(name + " exists! (" + target + ")") + if "PROJECT" not in config.sections() or \ + "input_path" not in config["PROJECT"] or \ + "output_path" not in config["PROJECT"] or \ + "template_path" not in config["PROJECT"]: + self.userio.error("Invalid project file!") + + input_path = os.path.join(self.root_path, config["PROJECT"]["input_path"]) + output_path = os.path.join(self.root_path, config["PROJECT"]["output_path"]) + template_path = os.path.join(self.root_path, config["PROJECT"]["template_path"]) + + return input_path, output_path, template_path + + def read_config_meta(self): + config = configparser.ConfigParser() + config.read(self.get_config_file_path()) + + if "METADATA" not in config.sections(): + self.userio.error("Invalid project file!") + + return config["METADATA"] + + def read_config_fqbn(self): + config = configparser.ConfigParser() + config.read(self.get_config_file_path()) + + if "TARGET" not in config.sections(): + return None + + if "FQBN" not in config["TARGET"]: + return None + + return config["TARGET"]["FQBN"] + + def write_config_fqbn(self, fqbn): + config = configparser.ConfigParser() + config.read(self.get_config_file_path()) + + if "TARGET" not in config.sections(): + config["TARGET"] = {} + config["TARGET"]["FQBN"] = fqbn - # Check target files before we start - # as not to leave a half initialized project - error_or_delete(path_config, "Config folder") - error_or_delete(path_config_file, "Config file") - error_or_delete(path_input, "Input folder") - error_or_delete(path_template, "Template folder") - error_or_delete(path_output, "Output folder") + with open(self.get_config_file_path(), "w") as file: + config.write(file) - # Eventually, create project files - userio.print("Creating project files") + def generate(self, quiet): - # Project config folder - os.mkdir(path_config) + # Read project data + input_path, output_path, template_path = self.read_config_project() + meta_data = self.read_config_meta() - # Project config file - with open(path_config_file, "w") as config_file: - config_file.write(project_config_make(path_input, path_template, - path_output, mode, ssid, port)) + # Enter ssid is none is in config + if "ssid" not in meta_data: + meta_data["ssid"] = "" - # Project output folder - userio.print("Creating output folder", verbose=True) - os.mkdir(path_output) + # Get password (and ssid if necessary) + meta_data["ssid"], meta_data["pass"] = get_ssid_pass(self.userio, meta_data["ssid"], quiet) - # Project output folder - userio.print("Creating input files", verbose=True) - shutil.copytree(path_input_src, path_input) + # Eventually create output + generate(self.userio, input_path, output_path, template_path, meta_data) - userio.print("Creating template files", verbose=True) - shutil.copytree(path_template_src, path_template) + def compile(self, force_select=False, save=False): + self.userio.section("Compiling project output") - userio.section("Project created successfully.") - userio.print("Use 'webduino-generator build' to build your project.") + # Get project output location + sketch_path = self.get_sketch_path() + if sketch_path is None: + self.userio.error("Could not locate output files!") + self.userio.print("Sketch located: " + sketch_path, verbose=True) + # Get target FQBN + fqbn = self.read_config_fqbn() + if force_select or fqbn is None: + name, fqbn = get_board(self.userio) + if save: + self.write_config_fqbn(fqbn) -def project_generate(userio, target, quiet): - # Check project - if not project_check(target): - userio.error("Target project not found!") + # Compile sketch using arduino-cli + sketch_compile(self.userio, sketch_path, fqbn) - # Read project data - input_path, output_path, template_path = project_config_readproject(userio, target) - meta_data = project_config_readmeta(userio, target) + def upload(self): + self.userio.section("Uploading project output") - # Enter ssid is none is in config - if "ssid" not in meta_data: - meta_data["ssid"] = "" + # Get project output location + sketch_path = self.get_sketch_path() + if sketch_path is None: + self.userio.error("Could not locate output files!") + self.userio.print("Sketch located: " + sketch_path, verbose=True) - # Get password (and ssid if necessary) - meta_data["ssid"], meta_data["pass"] = get_ssid_pass(userio, meta_data["ssid"], quiet) + # Get target FQBN + name, fqbn, address = get_board_connected(self.userio) - # Eventually create output - generate(userio, input_path, output_path, template_path, meta_data) + # Compile sketch using arduino-cli + sketch_upload(self.userio, sketch_path, fqbn, address) diff --git a/webduino_generator/userio.py b/webduino_generator/userio.py index e84bba3..2cbf304 100644 --- a/webduino_generator/userio.py +++ b/webduino_generator/userio.py @@ -62,11 +62,17 @@ def get_user(self, prompt: str, userText: str) -> Tuple[str, str]: if prompt is not None: self.console.print(prompt) self.console.print(userText, end="") - userValue = input() + try: + userValue = input() + except KeyboardInterrupt: + exit(1) return userValue def get_pass(self, prompt: str, passText: str) -> Tuple[str, str]: if prompt is not None: self.console.print(prompt) - passValue = getpass.getpass(passText) + try: + passValue = getpass.getpass(passText) + except KeyboardInterrupt: + exit(1) return passValue