apsbits 1.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- apsbits/__init__.py +18 -0
- apsbits/_version.py +21 -0
- apsbits/core/__init__.py +11 -0
- apsbits/core/best_effort_init.py +51 -0
- apsbits/core/catalog_init.py +36 -0
- apsbits/core/run_engine_init.py +118 -0
- apsbits/demo_instrument/README.md +1 -0
- apsbits/demo_instrument/__init__.py +22 -0
- apsbits/demo_instrument/callbacks/__init__.py +1 -0
- apsbits/demo_instrument/callbacks/nexus_data_file_writer.py +58 -0
- apsbits/demo_instrument/callbacks/spec_data_file_writer.py +97 -0
- apsbits/demo_instrument/configs/__init__.py +1 -0
- apsbits/demo_instrument/configs/devices.yml +52 -0
- apsbits/demo_instrument/configs/devices_aps_only.yml +6 -0
- apsbits/demo_instrument/configs/iconfig.yml +82 -0
- apsbits/demo_instrument/configs/logging.yml +41 -0
- apsbits/demo_instrument/devices/__init__.py +1 -0
- apsbits/demo_instrument/plans/__init__.py +8 -0
- apsbits/demo_instrument/plans/dm_plans.py +111 -0
- apsbits/demo_instrument/plans/sim_plans.py +69 -0
- apsbits/demo_instrument/startup.py +76 -0
- apsbits/demo_qserver/qs-config.yml +51 -0
- apsbits/demo_qserver/qs_host.sh +231 -0
- apsbits/demo_qserver/user_group_permissions.yaml +46 -0
- apsbits/tests/__init__.py +1 -0
- apsbits/tests/conftest.py +39 -0
- apsbits/tests/test_config.py +113 -0
- apsbits/tests/test_device_factories.py +44 -0
- apsbits/tests/test_general.py +98 -0
- apsbits/tests/test_stored_dict.py +139 -0
- apsbits/utils/__init__.py +1 -0
- apsbits/utils/aps_functions.py +67 -0
- apsbits/utils/config_loaders.py +169 -0
- apsbits/utils/controls_setup.py +107 -0
- apsbits/utils/create_new_instrument.py +129 -0
- apsbits/utils/helper_functions.py +123 -0
- apsbits/utils/logging_setup.py +211 -0
- apsbits/utils/make_devices.py +162 -0
- apsbits/utils/metadata.py +96 -0
- apsbits/utils/sim_creator.py +202 -0
- apsbits/utils/stored_dict.py +174 -0
- apsbits-1.0.0.dist-info/METADATA +195 -0
- apsbits-1.0.0.dist-info/RECORD +47 -0
- apsbits-1.0.0.dist-info/WHEEL +5 -0
- apsbits-1.0.0.dist-info/entry_points.txt +2 -0
- apsbits-1.0.0.dist-info/licenses/LICENSE +48 -0
- apsbits-1.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Plans in support of APS Data Management
|
|
3
|
+
=======================================
|
|
4
|
+
|
|
5
|
+
.. autosummary::
|
|
6
|
+
|
|
7
|
+
~dm_kickoff_workflow
|
|
8
|
+
~dm_list_processing_jobs
|
|
9
|
+
~dm_submit_workflow_job
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import logging
|
|
13
|
+
|
|
14
|
+
from apstools.devices import DM_WorkflowConnector
|
|
15
|
+
from apstools.utils import dm_api_proc
|
|
16
|
+
from apstools.utils import share_bluesky_metadata_with_dm
|
|
17
|
+
from bluesky import plan_stubs as bps
|
|
18
|
+
|
|
19
|
+
logger = logging.getLogger(__name__)
|
|
20
|
+
logger.bsdev(__file__)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def dm_kickoff_workflow(run, argsDict, timeout=None, wait=False):
|
|
24
|
+
"""
|
|
25
|
+
Start a DM workflow for this bluesky run and share run's metadata with DM.
|
|
26
|
+
|
|
27
|
+
PARAMETERS:
|
|
28
|
+
|
|
29
|
+
run (*obj*): Bluesky run object (such as 'run = cat[uid]').
|
|
30
|
+
|
|
31
|
+
argsDict (*dict*): Dictionary of parameters needed by 'workflowName'.
|
|
32
|
+
At minimum, most workflows expect these keys: 'filePath' and
|
|
33
|
+
'experimentName'. Consult the workflow for the expected
|
|
34
|
+
content of 'argsDict'.
|
|
35
|
+
|
|
36
|
+
timeout (*number*): When should bluesky stop reporting on this
|
|
37
|
+
DM workflow job (if it has not ended). Units are seconds.
|
|
38
|
+
Default is forever.
|
|
39
|
+
|
|
40
|
+
wait (*bool*): Should this plan stub wait for the job to end?
|
|
41
|
+
Default is 'False'.
|
|
42
|
+
"""
|
|
43
|
+
dm_workflow = DM_WorkflowConnector(name="dm_workflow")
|
|
44
|
+
|
|
45
|
+
if timeout is None:
|
|
46
|
+
# Disable periodic reports, use a long time (s).
|
|
47
|
+
timeout = 999_999_999_999
|
|
48
|
+
|
|
49
|
+
yield from bps.mv(dm_workflow.concise_reporting, True)
|
|
50
|
+
yield from bps.mv(dm_workflow.reporting_period, timeout)
|
|
51
|
+
|
|
52
|
+
workflow_name = argsDict.pop["workflowName"]
|
|
53
|
+
yield from dm_workflow.run_as_plan(
|
|
54
|
+
workflow=workflow_name,
|
|
55
|
+
wait=wait,
|
|
56
|
+
timeout=timeout,
|
|
57
|
+
**argsDict,
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
# Upload bluesky run metadata to APS DM.
|
|
61
|
+
share_bluesky_metadata_with_dm(argsDict["experimentName"], workflow_name, run)
|
|
62
|
+
|
|
63
|
+
# Users requested the DM workflow job ID be printed to the console.
|
|
64
|
+
dm_workflow._update_processing_data()
|
|
65
|
+
job_id = dm_workflow.job_id.get()
|
|
66
|
+
job_stage = dm_workflow.stage_id.get()
|
|
67
|
+
job_status = dm_workflow.status.get()
|
|
68
|
+
print(f"DM workflow id: {job_id!r} status: {job_status} stage: {job_stage}")
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def dm_list_processing_jobs(exclude=None):
|
|
72
|
+
"""
|
|
73
|
+
Show all the DM jobs with status not excluded.
|
|
74
|
+
|
|
75
|
+
Excluded status (default): 'done', 'failed'
|
|
76
|
+
"""
|
|
77
|
+
yield from bps.null() # make this a plan stub
|
|
78
|
+
api = dm_api_proc()
|
|
79
|
+
if exclude is None:
|
|
80
|
+
exclude = ("done", "failed")
|
|
81
|
+
|
|
82
|
+
for j in api.listProcessingJobs():
|
|
83
|
+
if j["status"] not in exclude:
|
|
84
|
+
print(
|
|
85
|
+
f"id={j['id']!r}"
|
|
86
|
+
f" submitted={j.get('submissionTimestamp')}"
|
|
87
|
+
f" status={j['status']!r}"
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def dm_submit_workflow_job(workflowName, argsDict):
|
|
92
|
+
"""
|
|
93
|
+
Low-level plan stub to submit a job to a DM workflow.
|
|
94
|
+
|
|
95
|
+
It is recommended to use dm_kickoff_workflow() instead.
|
|
96
|
+
This plan does not share run metadata with DM.
|
|
97
|
+
|
|
98
|
+
PARAMETERS:
|
|
99
|
+
|
|
100
|
+
workflowName (*str*): Name of the DM workflow to be run.
|
|
101
|
+
|
|
102
|
+
argsDict (*dict*): Dictionary of parameters needed by 'workflowName'.
|
|
103
|
+
At minimum, most workflows expect these keys: 'filePath' and
|
|
104
|
+
'experimentName'. Consult the workflow for the expected
|
|
105
|
+
content of 'argsDict'.
|
|
106
|
+
"""
|
|
107
|
+
yield from bps.null() # make this a plan stub
|
|
108
|
+
api = dm_api_proc()
|
|
109
|
+
|
|
110
|
+
job = api.startProcessingJob(api.username, workflowName, argsDict)
|
|
111
|
+
print(f"workflow={workflowName!r} id={job['id']!r}")
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Simulators from ophyd
|
|
3
|
+
=====================
|
|
4
|
+
|
|
5
|
+
For development and testing only, provides plans.
|
|
6
|
+
|
|
7
|
+
.. autosummary::
|
|
8
|
+
~sim_count_plan
|
|
9
|
+
~sim_print_plan
|
|
10
|
+
~sim_rel_scan_plan
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
import logging
|
|
14
|
+
|
|
15
|
+
from bluesky import plan_stubs as bps
|
|
16
|
+
from bluesky import plans as bp
|
|
17
|
+
|
|
18
|
+
from apsbits.utils.controls_setup import oregistry
|
|
19
|
+
|
|
20
|
+
logger = logging.getLogger(__name__)
|
|
21
|
+
logger.bsdev(__file__)
|
|
22
|
+
|
|
23
|
+
DEFAULT_MD = {"title": "test run with simulator(s)"}
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def sim_count_plan(num: int = 1, imax: float = 10_000, md: dict = DEFAULT_MD):
|
|
27
|
+
"""Demonstrate the ``count()`` plan."""
|
|
28
|
+
logger.debug("sim_count_plan()")
|
|
29
|
+
sim_det = oregistry["sim_det"]
|
|
30
|
+
yield from bps.mv(sim_det.Imax, imax)
|
|
31
|
+
yield from bp.count([sim_det], num=num, md=md)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def sim_print_plan():
|
|
35
|
+
"""Demonstrate a ``print()`` plan stub (no data streams)."""
|
|
36
|
+
logger.debug("sim_print_plan()")
|
|
37
|
+
yield from bps.null()
|
|
38
|
+
sim_det = oregistry["sim_det"]
|
|
39
|
+
sim_motor = oregistry["sim_motor"]
|
|
40
|
+
print("sim_print_plan(): This is a test.")
|
|
41
|
+
print(f"sim_print_plan(): {sim_motor.position=} {sim_det.read()=}.")
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def sim_rel_scan_plan(
|
|
45
|
+
span: float = 5,
|
|
46
|
+
num: int = 11,
|
|
47
|
+
imax: float = 10_000,
|
|
48
|
+
center: float = 0,
|
|
49
|
+
sigma: float = 1,
|
|
50
|
+
noise: str = "uniform", # none poisson uniform
|
|
51
|
+
md: dict = DEFAULT_MD,
|
|
52
|
+
):
|
|
53
|
+
"""Demonstrate the ``rel_scan()`` plan."""
|
|
54
|
+
logger.debug("sim_rel_scan_plan()")
|
|
55
|
+
sim_det = oregistry["sim_det"]
|
|
56
|
+
sim_motor = oregistry["sim_motor"]
|
|
57
|
+
# fmt: off
|
|
58
|
+
yield from bps.mv(
|
|
59
|
+
sim_det.Imax, imax,
|
|
60
|
+
sim_det.center, center,
|
|
61
|
+
sim_det.sigma, sigma,
|
|
62
|
+
sim_det.noise, noise,
|
|
63
|
+
)
|
|
64
|
+
# fmt: on
|
|
65
|
+
print(f"sim_rel_scan_plan(): {sim_motor.position=}.")
|
|
66
|
+
print(f"sim_rel_scan_plan(): {sim_det.read()=}.")
|
|
67
|
+
print(f"sim_rel_scan_plan(): {sim_det.read_configuration()=}.")
|
|
68
|
+
print(f"sim_rel_scan_plan(): {sim_det.noise._enum_strs=}.")
|
|
69
|
+
yield from bp.rel_scan([sim_det], sim_motor, -span / 2, span / 2, num=num, md=md)
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Start Bluesky Data Acquisition sessions of all kinds.
|
|
3
|
+
|
|
4
|
+
Includes:
|
|
5
|
+
|
|
6
|
+
* Python script
|
|
7
|
+
* IPython console
|
|
8
|
+
* Jupyter notebook
|
|
9
|
+
* Bluesky queueserver
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import logging
|
|
13
|
+
|
|
14
|
+
from apsbits.core.best_effort_init import init_bec_peaks
|
|
15
|
+
from apsbits.core.catalog_init import init_catalog
|
|
16
|
+
from apsbits.core.run_engine_init import init_RE
|
|
17
|
+
from apsbits.utils.aps_functions import aps_dm_setup
|
|
18
|
+
from apsbits.utils.config_loaders import get_config
|
|
19
|
+
from apsbits.utils.helper_functions import register_bluesky_magics
|
|
20
|
+
from apsbits.utils.helper_functions import running_in_queueserver
|
|
21
|
+
from apsbits.utils.make_devices import make_devices
|
|
22
|
+
|
|
23
|
+
logger = logging.getLogger(__name__)
|
|
24
|
+
logger.bsdev(__file__)
|
|
25
|
+
|
|
26
|
+
# Get the configuration
|
|
27
|
+
iconfig = get_config()
|
|
28
|
+
|
|
29
|
+
# Configure the session with callbacks, devices, and plans.
|
|
30
|
+
aps_dm_setup(iconfig.get("DM_SETUP_FILE"))
|
|
31
|
+
|
|
32
|
+
if iconfig.get("USE_BLUESKY_MAGICS", False):
|
|
33
|
+
register_bluesky_magics()
|
|
34
|
+
|
|
35
|
+
# Initialize core components
|
|
36
|
+
bec, peaks = init_bec_peaks(iconfig)
|
|
37
|
+
cat = init_catalog(iconfig)
|
|
38
|
+
RE, sd = init_RE(iconfig, bec_instance=bec, cat_instance=cat)
|
|
39
|
+
|
|
40
|
+
# Import optional components based on configuration
|
|
41
|
+
if iconfig.get("NEXUS_DATA_FILES", {}).get("ENABLE", False):
|
|
42
|
+
from .callbacks.nexus_data_file_writer import nxwriter_init
|
|
43
|
+
|
|
44
|
+
nxwriter = nxwriter_init(RE)
|
|
45
|
+
|
|
46
|
+
if iconfig.get("SPEC_DATA_FILES", {}).get("ENABLE", False):
|
|
47
|
+
from .callbacks.spec_data_file_writer import init_specwriter_with_RE
|
|
48
|
+
from .callbacks.spec_data_file_writer import newSpecFile # noqa: F401
|
|
49
|
+
from .callbacks.spec_data_file_writer import spec_comment # noqa: F401
|
|
50
|
+
from .callbacks.spec_data_file_writer import specwriter # noqa: F401
|
|
51
|
+
|
|
52
|
+
init_specwriter_with_RE(RE)
|
|
53
|
+
|
|
54
|
+
# Import all plans
|
|
55
|
+
from .plans import * # noqa
|
|
56
|
+
|
|
57
|
+
# These imports must come after the above setup.
|
|
58
|
+
if running_in_queueserver():
|
|
59
|
+
### To make all the standard plans available in QS, import by '*', otherwise import
|
|
60
|
+
### plan by plan.
|
|
61
|
+
from apstools.plans import lineup2 # noqa: F401
|
|
62
|
+
from bluesky.plans import * # noqa: F403
|
|
63
|
+
|
|
64
|
+
else:
|
|
65
|
+
# Import bluesky plans and stubs with prefixes set by common conventions.
|
|
66
|
+
# The apstools plans and utils are imported by '*'.
|
|
67
|
+
from apstools.plans import * # noqa: F403
|
|
68
|
+
from apstools.utils import * # noqa: F403
|
|
69
|
+
from bluesky import plan_stubs as bps # noqa: F401
|
|
70
|
+
from bluesky import plans as bp # noqa: F401
|
|
71
|
+
|
|
72
|
+
from apsbits.utils.controls_setup import oregistry
|
|
73
|
+
|
|
74
|
+
oregistry.clear()
|
|
75
|
+
|
|
76
|
+
RE(make_devices())
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# Bluesky-Queueserver start-re-manager configuration
|
|
2
|
+
# use:
|
|
3
|
+
# queue-monitor &
|
|
4
|
+
# ./qserver/qs_host.sh start
|
|
5
|
+
#
|
|
6
|
+
# or
|
|
7
|
+
# cd ./qserver
|
|
8
|
+
# start-re-manager --config=./qs-config.yml
|
|
9
|
+
#
|
|
10
|
+
# https://blueskyproject.io/bluesky-queueserver/manager_config.html
|
|
11
|
+
|
|
12
|
+
network:
|
|
13
|
+
redis_addr: localhost:6379
|
|
14
|
+
redis_name_prefix: qs_default
|
|
15
|
+
zmq_control_addr: tcp://*:60615
|
|
16
|
+
zmq_info_addr: tcp://*:60625
|
|
17
|
+
zmq_publish_console: true
|
|
18
|
+
|
|
19
|
+
operation:
|
|
20
|
+
# choices: SILENT QUIET NORMAL VERBOSE
|
|
21
|
+
console_logging_level: NORMAL
|
|
22
|
+
|
|
23
|
+
# emergency_lock_key: custom_lock_key
|
|
24
|
+
|
|
25
|
+
print_console_output: true
|
|
26
|
+
|
|
27
|
+
# choices: NEVER ENVIRONMENT_OPEN ALWAYS
|
|
28
|
+
update_existing_plans_and_devices: ENVIRONMENT_OPEN
|
|
29
|
+
|
|
30
|
+
# choices: NEVER ON_REQUEST ON_STARTUP
|
|
31
|
+
user_group_permissions_reload: ON_STARTUP
|
|
32
|
+
|
|
33
|
+
run_engine:
|
|
34
|
+
# databroker_config: name_of_databroker_config_file
|
|
35
|
+
|
|
36
|
+
# kafka_server: 127.0.0.1:9092
|
|
37
|
+
# kafka_topic: custom_topic_name
|
|
38
|
+
|
|
39
|
+
use_persistent_metadata: true
|
|
40
|
+
zmq_data_proxy_addr: localhost:5567
|
|
41
|
+
|
|
42
|
+
startup:
|
|
43
|
+
keep_re: true
|
|
44
|
+
startup_module: demo_instrument.startup
|
|
45
|
+
existing_plans_and_devices_path: ./
|
|
46
|
+
user_group_permissions_path: ./
|
|
47
|
+
|
|
48
|
+
worker:
|
|
49
|
+
use_ipython_kernel: true
|
|
50
|
+
# ipython_kernel_ip: auto
|
|
51
|
+
ipython_matplotlib: qt5
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# file: qs_host.sh
|
|
3
|
+
# Manage the bluesky queueserver host process.
|
|
4
|
+
# Could be in a screen session or run as a direct process.
|
|
5
|
+
|
|
6
|
+
SHELL_SCRIPT_NAME=${BASH_SOURCE:-${0}}
|
|
7
|
+
SCRIPT_DIR="$(dirname $(readlink -f "${SHELL_SCRIPT_NAME}"))"
|
|
8
|
+
CONFIGS_DIR=$(readlink -f "${SCRIPT_DIR}/../demo_instrument/configs")
|
|
9
|
+
|
|
10
|
+
###-----------------------------
|
|
11
|
+
### Change program defaults here
|
|
12
|
+
|
|
13
|
+
# Instrument configuration YAML file with databroker catalog name.
|
|
14
|
+
ICONFIG_YML="${CONFIGS_DIR}"/iconfig.yml
|
|
15
|
+
|
|
16
|
+
# Bluesky queueserver configuration YAML file.
|
|
17
|
+
# This file contains the definition of 'redis_addr'. (default: localhost:6379)
|
|
18
|
+
QS_CONFIG_YML="${SCRIPT_DIR}/qs-config.yml"
|
|
19
|
+
|
|
20
|
+
# Host name (from $hostname) where the queueserver host process runs.
|
|
21
|
+
# QS_HOSTNAME=amber.xray.aps.anl.gov # if a specific host is required
|
|
22
|
+
QS_HOSTNAME="$(hostname)"
|
|
23
|
+
|
|
24
|
+
PROCESS=start-re-manager # from the conda environment
|
|
25
|
+
STARTUP_COMMAND="${PROCESS} --config=${QS_CONFIG_YML}"
|
|
26
|
+
|
|
27
|
+
#--------------------
|
|
28
|
+
# internal configuration below
|
|
29
|
+
|
|
30
|
+
# echo "PROCESS=${PROCESS}"
|
|
31
|
+
if [ ! -f $(which "${PROCESS}") ]; then
|
|
32
|
+
echo "PROCESS '${PROCESS}': file not found. CONDA_PREFIX='${CONDA_PREFIX}'"
|
|
33
|
+
exit 1
|
|
34
|
+
fi
|
|
35
|
+
|
|
36
|
+
if [ -z "$STARTUP_DIR" ] ; then
|
|
37
|
+
# If no startup dir is specified, use the directory with this script
|
|
38
|
+
STARTUP_DIR="${SCRIPT_DIR}"
|
|
39
|
+
fi
|
|
40
|
+
|
|
41
|
+
if [ "${DATABROKER_CATALOG}" == "" ]; then
|
|
42
|
+
if [ -f "${ICONFIG_YML}" ]; then
|
|
43
|
+
DATABROKER_CATALOG=$(grep DATABROKER_CATALOG "${ICONFIG_YML}" | awk '{print $NF}')
|
|
44
|
+
# echo "Using catalog ${DATABROKER_CATALOG}"
|
|
45
|
+
fi
|
|
46
|
+
fi
|
|
47
|
+
DEFAULT_SESSION_NAME="bluesky_queueserver-${DATABROKER_CATALOG}"
|
|
48
|
+
|
|
49
|
+
#--------------------
|
|
50
|
+
|
|
51
|
+
SELECTION=${1:-usage}
|
|
52
|
+
SESSION_NAME=${2:-"${DEFAULT_SESSION_NAME}"}
|
|
53
|
+
|
|
54
|
+
# But other management commands will fail if mismatch
|
|
55
|
+
if [ "$(hostname)" != "${QS_HOSTNAME}" ]; then
|
|
56
|
+
echo "Must manage queueserver process on ${QS_HOSTNAME}. This is $(hostname)."
|
|
57
|
+
exit 1
|
|
58
|
+
fi
|
|
59
|
+
|
|
60
|
+
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
|
61
|
+
|
|
62
|
+
# echo "SESSION_NAME = ${SESSION_NAME}"
|
|
63
|
+
# echo "SHELL_SCRIPT_NAME = ${SHELL_SCRIPT_NAME}"
|
|
64
|
+
# echo "STARTUP_COMMAND = ${STARTUP_COMMAND}"
|
|
65
|
+
# echo "STARTUP_DIR = ${STARTUP_DIR}"
|
|
66
|
+
|
|
67
|
+
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
|
68
|
+
|
|
69
|
+
function checkpid() {
|
|
70
|
+
# Assume the process is down until proven otherwise
|
|
71
|
+
PROCESS_DOWN=1
|
|
72
|
+
|
|
73
|
+
MY_UID=$(id -u)
|
|
74
|
+
# The '\$' is needed in the pgrep pattern to select vm7, but not vm7.sh
|
|
75
|
+
MY_PID=$(ps -u | grep "${PROCESS}")
|
|
76
|
+
#!echo "MY_PID=${MY_PID}"
|
|
77
|
+
SCREEN_SESSION="${MY_PID}.${SESSION_NAME}"
|
|
78
|
+
|
|
79
|
+
if [ "${MY_PID}" != "" ] ; then
|
|
80
|
+
SCREEN_PID="${MY_PID}"
|
|
81
|
+
|
|
82
|
+
# At least one instance of the process is running;
|
|
83
|
+
# Find the binary that is associated with this process
|
|
84
|
+
for pid in ${MY_PID}; do
|
|
85
|
+
# compare directories
|
|
86
|
+
BIN_CWD=$(readlink "/proc/${pid}/cwd")
|
|
87
|
+
START_CWD=$(readlink -f "${STARTUP_DIR}")
|
|
88
|
+
|
|
89
|
+
if [ "$BIN_CWD" = "$START_CWD" ] ; then
|
|
90
|
+
# The process is running with PID=$pid from $STARTUP_DIR
|
|
91
|
+
P_PID=$(ps -p "${pid}" -o ppid=)
|
|
92
|
+
# strip leading (and trailing) whitespace
|
|
93
|
+
arr=($P_PID)
|
|
94
|
+
P_PID=${arr[0]}
|
|
95
|
+
SCREEN_SESSION="${P_PID}.${SESSION_NAME}"
|
|
96
|
+
SCREEN_MATCH=$(screen -ls "${SCREEN_SESSION}" | grep "${SESSION_NAME}")
|
|
97
|
+
if [ "${SCREEN_MATCH}" != "" ] ; then
|
|
98
|
+
# process is running in screen
|
|
99
|
+
PROCESS_DOWN=0
|
|
100
|
+
MY_PID=${pid}
|
|
101
|
+
SCREEN_PID=${P_PID}
|
|
102
|
+
break
|
|
103
|
+
fi
|
|
104
|
+
fi
|
|
105
|
+
done
|
|
106
|
+
else
|
|
107
|
+
# process is not running
|
|
108
|
+
PROCESS_DOWN=1
|
|
109
|
+
fi
|
|
110
|
+
|
|
111
|
+
return ${PROCESS_DOWN}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function checkup () {
|
|
115
|
+
if ! checkpid; then
|
|
116
|
+
restart
|
|
117
|
+
fi
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function console () {
|
|
121
|
+
if checkpid; then
|
|
122
|
+
echo "Connecting to ${SCREEN_SESSION}'s screen session"
|
|
123
|
+
# The -r flag will only connect if no one is attached to the session
|
|
124
|
+
#!screen -r "${SESSION_NAME}"
|
|
125
|
+
# The -x flag will connect even if someone is attached to the session
|
|
126
|
+
screen -x "${SCREEN_SESSION}"
|
|
127
|
+
else
|
|
128
|
+
echo "${SCREEN_NAME} is not running"
|
|
129
|
+
fi
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function exit_if_running() {
|
|
133
|
+
# ensure that multiple, simultaneous processes are not started by this user ID
|
|
134
|
+
MY_UID=$(id -u)
|
|
135
|
+
MY_PID=$(pgrep "${SESSION_NAME}"\$ -u "${MY_UID}")
|
|
136
|
+
|
|
137
|
+
if [ "" != "${MY_PID}" ] ; then
|
|
138
|
+
echo "${SESSION_NAME} is already running (PID=${MY_PID}), won't start a new one"
|
|
139
|
+
exit 1
|
|
140
|
+
fi
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function restart() {
|
|
144
|
+
stop
|
|
145
|
+
start
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function run_process() {
|
|
149
|
+
# only use this for diagnostic purposes
|
|
150
|
+
exit_if_running
|
|
151
|
+
cd "${STARTUP_DIR}"
|
|
152
|
+
${STARTUP_COMMAND}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function screenpid() {
|
|
156
|
+
if [ -z "${SCREEN_PID}" ] ; then
|
|
157
|
+
echo
|
|
158
|
+
else
|
|
159
|
+
echo " in a screen session (pid=${SCREEN_PID})"
|
|
160
|
+
fi
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function start() {
|
|
164
|
+
if checkpid; then
|
|
165
|
+
echo -n "${SCREEN_SESSION} is already running (pid=${MY_PID})"
|
|
166
|
+
screenpid
|
|
167
|
+
else
|
|
168
|
+
if [ ! -f "${CONDA_EXE}" ]; then
|
|
169
|
+
echo "No 'conda' command available."
|
|
170
|
+
exit 1
|
|
171
|
+
fi
|
|
172
|
+
echo "Starting ${SESSION_NAME}"
|
|
173
|
+
cd "${STARTUP_DIR}"
|
|
174
|
+
# Run SESSION_NAME inside a screen session
|
|
175
|
+
CMD="screen -DmS ${SESSION_NAME} -h 5000 ${STARTUP_COMMAND}"
|
|
176
|
+
${CMD} &
|
|
177
|
+
fi
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function status() {
|
|
181
|
+
if checkpid; then
|
|
182
|
+
echo -n "${SCREEN_SESSION} is running (pid=${MY_PID})"
|
|
183
|
+
screenpid
|
|
184
|
+
else
|
|
185
|
+
echo "${SESSION_NAME} is not running"
|
|
186
|
+
fi
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function stop() {
|
|
190
|
+
if checkpid; then
|
|
191
|
+
echo "Stopping ${SCREEN_SESSION} (pid=${MY_PID})"
|
|
192
|
+
kill "${MY_PID}"
|
|
193
|
+
else
|
|
194
|
+
echo "${SESSION_NAME} is not running"
|
|
195
|
+
fi
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function usage() {
|
|
199
|
+
echo "Usage: $(basename "${SHELL_SCRIPT_NAME}") {start|stop|restart|status|checkup|console|run} [NAME]"
|
|
200
|
+
echo ""
|
|
201
|
+
echo " COMMANDS"
|
|
202
|
+
echo " console attach to process console if process is running in screen"
|
|
203
|
+
echo " checkup check that process is running, restart if not"
|
|
204
|
+
echo " restart restart process"
|
|
205
|
+
echo " run run process in console (not screen)"
|
|
206
|
+
echo " start start process"
|
|
207
|
+
echo " status report if process is running"
|
|
208
|
+
echo " stop stop process"
|
|
209
|
+
echo ""
|
|
210
|
+
echo " OPTIONAL TERMS"
|
|
211
|
+
echo " NAME name of process (default: ${DEFAULT_SESSION_NAME})"
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
|
215
|
+
|
|
216
|
+
case ${SELECTION} in
|
|
217
|
+
start) start ;;
|
|
218
|
+
stop | kill) stop ;;
|
|
219
|
+
restart) restart ;;
|
|
220
|
+
status) status ;;
|
|
221
|
+
checkup) checkup ;;
|
|
222
|
+
console) console ;;
|
|
223
|
+
run) run_process ;;
|
|
224
|
+
*) usage ;;
|
|
225
|
+
esac
|
|
226
|
+
|
|
227
|
+
# -----------------------------------------------------------------------------
|
|
228
|
+
# :author: BCDA
|
|
229
|
+
# :copyright: (c) 2017-2025, UChicago Argonne, LLC
|
|
230
|
+
# The full license is in the file LICENSE, distributed with this software.
|
|
231
|
+
# -----------------------------------------------------------------------------
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
user_groups:
|
|
2
|
+
root: # The group includes all available plan and devices
|
|
3
|
+
allowed_plans:
|
|
4
|
+
- null # Allow all
|
|
5
|
+
forbidden_plans:
|
|
6
|
+
- ":^_" # All plans with names starting with '_'
|
|
7
|
+
allowed_devices:
|
|
8
|
+
- null # Allow all
|
|
9
|
+
forbidden_devices:
|
|
10
|
+
- ":^_:?.*" # All devices with names starting with '_'
|
|
11
|
+
allowed_functions:
|
|
12
|
+
- null # Allow all
|
|
13
|
+
forbidden_functions:
|
|
14
|
+
- ":^_" # All functions with names starting with '_'
|
|
15
|
+
primary: # The group includes beamline staff, includes all or most of the plans and devices
|
|
16
|
+
allowed_plans:
|
|
17
|
+
- ":.*" # Different way to allow all plans.
|
|
18
|
+
forbidden_plans:
|
|
19
|
+
- null # Nothing is forbidden
|
|
20
|
+
allowed_devices:
|
|
21
|
+
- ":?.*:depth=5" # Allow all device and subdevices. Maximum deepth for subdevices is 5.
|
|
22
|
+
forbidden_devices:
|
|
23
|
+
- null # Nothing is forbidden
|
|
24
|
+
allowed_functions:
|
|
25
|
+
- "function_sleep" # Explicitly listed name
|
|
26
|
+
test_user: # Users with limited access capabilities
|
|
27
|
+
allowed_plans:
|
|
28
|
+
- ":^count" # Use regular expression patterns
|
|
29
|
+
- ":scan$"
|
|
30
|
+
forbidden_plans:
|
|
31
|
+
- ":^adaptive_scan$" # Use regular expression patterns
|
|
32
|
+
- ":^inner_product"
|
|
33
|
+
allowed_devices:
|
|
34
|
+
- ":^det:?.*" # Use regular expression patterns
|
|
35
|
+
- ":^motor:?.*"
|
|
36
|
+
- ":^sim_bundle_A:?.*"
|
|
37
|
+
forbidden_devices:
|
|
38
|
+
- ":^det[3-5]$:?.*" # Use regular expression patterns
|
|
39
|
+
- ":^motor\\d+$:?.*"
|
|
40
|
+
allowed_functions:
|
|
41
|
+
- ":element$"
|
|
42
|
+
- ":elements$"
|
|
43
|
+
- "function_sleep"
|
|
44
|
+
- "clear_buffer"
|
|
45
|
+
forbidden_functions:
|
|
46
|
+
- ":^_" # All functions with names starting with '_'
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Test code for minimal instrument package."""
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Pytest fixtures for instrument tests.
|
|
3
|
+
|
|
4
|
+
This module provides fixtures for initializing the RunEngine with devices,
|
|
5
|
+
allowing tests to operate with device-dependent configurations without relying
|
|
6
|
+
on the production startup logic.
|
|
7
|
+
|
|
8
|
+
Fixtures:
|
|
9
|
+
runengine_with_devices: A RunEngine object in a session with devices configured.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from typing import Any
|
|
14
|
+
|
|
15
|
+
import pytest
|
|
16
|
+
|
|
17
|
+
from apsbits.demo_instrument.startup import RE
|
|
18
|
+
from apsbits.demo_instrument.startup import make_devices
|
|
19
|
+
from apsbits.utils.config_loaders import load_config
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@pytest.fixture(scope="session")
|
|
23
|
+
def runengine_with_devices() -> Any:
|
|
24
|
+
"""
|
|
25
|
+
Initialize the RunEngine with devices for testing.
|
|
26
|
+
|
|
27
|
+
This fixture calls RE with the `make_devices()` plan stub to mimic
|
|
28
|
+
the behavior previously performed in the startup module.
|
|
29
|
+
|
|
30
|
+
Returns:
|
|
31
|
+
Any: An instance of the RunEngine with devices configured.
|
|
32
|
+
"""
|
|
33
|
+
# Load the configuration before testing
|
|
34
|
+
instrument_path = Path(__file__).parent.parent / "demo_instrument"
|
|
35
|
+
iconfig_path = instrument_path / "configs" / "iconfig.yml"
|
|
36
|
+
load_config(iconfig_path)
|
|
37
|
+
|
|
38
|
+
RE(make_devices())
|
|
39
|
+
return RE
|