Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature: User Macros #147

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions docs/macros.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
---
title: User Macros
---

Users may define macros for `zti`. When defined, user macros appear in the list of available commands for `zti`. User macros are created by placing Python files in the ZTI macros directory, which defaults to the path `~/.zti-mac`. This path may be overridden with the `ZTI_MACROS_DIR` environment variables.

User macro file names must take the form `my_macro.py`, and must contain a function with the signature,

```python
def do_my_macro(zti, arg):
...
```

This function is invoked when the macro command is run from `zti`. It is passed the current `zti` instance as the first argument and any arguments to the command as the second argument.

The macro file may optionally also contain a function with the signature,

```python
def help_my_macro(zti):
...
```

This function will be registered as the help command for the main macro command.

See `examples/macros/logon.py` for an example of a user macro.

A macro file name may not contain more than one period character. User macros which conflict with existing ZTI commands are ignored.
31 changes: 31 additions & 0 deletions examples/macros/logon.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
from tnz.ati import ati

def logon(zti, arg):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

now need to rename to do_logon

logon_setup()

ati.wait(lambda: ati.scrhas("Enter: "))
ati.send("app1 userid[enter]")
ati.wait(lambda: ati.scrhas("Password ===> "))
ati.send("password[enter]")

# maybe look for a status message
ati.wait(lambda: ati.scrhas("ICH70001I"))

# do something useful
ati.wait(lambda: ati.scrhas("***"))
ati.send("[enter]")
ati.wait(lambda: ati.scrhas("READY"))
ati.send("logon[enter]")
ati.wait(lambda: ati.scrhas("LOGGED OFF"))

# disconnect from host
ati.drop("SESSION")

def logon_helper():
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

unused function?

ati.set("TRACE", "ALL")
ati.set("LOGDEST", "example.log")

ati.set("ONERROR", "1")
ati.set("DISPLAY", "HOST")
ati.set("SESSION_HOST", "mvs1")
ati.set("SESSION", "A")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

missing newline

57 changes: 57 additions & 0 deletions tnz/zti.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
ZTI_AUTOSIZE
ZTI_SECLEVEL (see tnz.py)
ZTI_TITLE
ZTI_MACROS_DIR (~/.zti-mac is default)
_BPX_TERMPATH (see _termlib.py)

Copyright 2021, 2024 IBM Inc. All Rights Reserved.
Expand Down Expand Up @@ -115,6 +116,11 @@ def __init__(self, stdin=None, stdout=None):
if os.getenv("ZTI_AUTOSIZE"):
self.autosize = True

self.macros_dir = os.getenv("ZTI_MACROS_DIR")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There seems to be no reason to make macros_dir a property of Zti. I suggest just doing the environment variable inspection in __register_macros(). If you would rather do the environment variable inspection here... then pass macros_dir into __register_macros().

if self.macros_dir is None or not os.path.isdir(
self.macros_dir):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't hide the error if the user has ZTI_MACROS_DIR=not_a_dir. I suggest just removing that part of the check.

self.macros_dir = os.path.expanduser("~/.zti-mac")

self.pend_intro = None
self.__dirty_ranges = []
self.single_session = False
Expand Down Expand Up @@ -169,6 +175,8 @@ def __init__(self, stdin=None, stdout=None):

self.__install_plugins()

self.__register_macros()

# Methods

def atexit(self):
Expand Down Expand Up @@ -2694,6 +2702,55 @@ def __refresh(self):

self.stdscr.refresh(_win_callback=self.__set_event_fn())

def __register_macros(self):
import importlib.util
import sys
import types

if not os.path.isdir(self.macros_dir):
_logger.exception(f"{self.macros_dir} is not a directory")
return

for macro_file in os.listdir(self.macros_dir):
if not macro_file.endswith(".py"):
continue

if len(macro_file.split('.')) > 1:
continue

macro_name = macro_file.split('.')[0]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since precmd() converts commands to lowercase, any command with a capital letter will not work. Along those lines... also consider ignoring/rejecting macros that have a space in the name.


# Ignore macros which already exist
if f"do_{macro_name}" in self.get_names():
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

may want to consider a warning message

continue

macro_path = os.path.join(self.macros_dir, macro_file)

# Import the user macro as a module
macro_spec = importlib.util.spec_from_file_location(
f"module.{macro_name}", macro_path)
macro_module = importlib.util.module_from_spec(macro_spec)
sys.modules[f"module.{macro_name}"] = macro_module
macro_spec.loader.exec_module(macro_module)

# Find the function
if hasattr(macro_module, f"do_{macro_name}"):
# Create a new bound method for the `Zti` class for this
# function
setattr(Zti, f"do_{macro_name}",
types.MethodType(
getattr(macro_module, f"do_{macro_name}"),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You may have noticed that many of the do_<command> methods call self.__bg_wait_end(). That is because a background thread runs while the user is at the prompt and that call shuts down that background thread so that the session(s) can be used in a thread-safe manner. That is something that is definitely needed here. Without it. both the thread running the do_<command> method and the background thread could be using tnz methods that are not thread-safe. I suggest you define a method that wraps the macro method in order to accomplish this. See do_plugin() as an example.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Another thing to model after do_plugin() ...

                    with ati.ati.new_program(share_sessions=True):
                        plugin(arg, **kwargs)

That helps ensure that the ati environment provided to the plugin is clean and that any variables set by the plugin do not leak into the current environment. Unless your use case requires non-shared/global ati variables as input to or output from the macro, I suggest being consistent with do_plugin().

self))

# Check if a corresponding help function exists
if hasattr(macro_module, f"help_{macro_name}"):
# Create a new bound method for the `Zti` class for this
# function
setattr(Zti, f"help_{macro_name}",
types.MethodType(
getattr(macro_module, f"help_{macro_name}"),
self))

def __scale_size(self, maxrow, maxcol):
arows, acols = self.autosize
aspect1 = arows / acols
Expand Down