main.py 12.7 KB
Newer Older
1
2
3
4
# -*- coding: utf-8 -*-
#
# This file is part of the bliss project
#
Benoit Formet's avatar
Benoit Formet committed
5
# Copyright (c) 2015-2020 Beamline Control Unit, ESRF
6
7
8
# Distributed under the GNU LGPLv3. See LICENSE for more info.

"""
9

10
Usage: bliss [-l | --log-level=<log_level>] [-s <name> | --session=<name>] [--no-tmux] [--debug]
11
       bliss [-v | --version]
12
       bliss [-c <name> | --create=<name>]
13
       bliss [-D <name> | --delete=<name>]
14
       bliss [-h | --help]
15
       bliss [-S | --show-sessions]
16
17
       bliss --show-sessions-only

18
Options:
19
    -l, --log-level=<log_level>   Log level [default: WARN] (CRITICAL ERROR INFO DEBUG NOTSET)
20
    -s, --session=<session_name>  Start with the specified session
21
    -v, --version                 Show version and exit
22
    -c, --create=<session_name>   Create a new session with the given name
23
    -D, --delete=<session_name>   Delete the given session
24
    -h, --help                    Show help screen and exit
GUILLOU Perceval's avatar
GUILLOU Perceval committed
25
    --no-tmux                     Deactivate Tmux usage
26
    --debug                       Allow debugging with full exceptions and keeping tmux alive after Bliss shell exits
27
    -S, --show-sessions           Display available sessions and tree of sub-sessions
28
    --show-sessions-only          Display available sessions names only
29
"""
30
import bliss
31

32
33
34
35
import warnings

warnings.filterwarnings("ignore", module="jinja2")

36
import os
37
import sys
38
import subprocess
39
from docopt import docopt, DocoptExit
40
import logging
41

42
from bliss import release
43
from bliss.config import get_sessions_list
44
from bliss.config import static
45
from bliss.config.static import ConfigNode
46
from bliss.config.conductor import client
47
from bliss.common import constants
48
from bliss import logging_startup
49
from bliss import current_session, global_map
50

51
52
from bliss.shell.cli.repl import embed
from bliss.shell.cli import session_files_templates as sft
53

54

55
56
57
58
59
60
61
62
63
64
65
def yes_or_no(question):
    answer = input(question + " (yes/no) ").lower().strip()
    print("")
    while not (answer in {"yes", "y"} or answer in {"no", "n"}):
        print("Input yes or no")
        answer = input(question + " (yes/no) ").lower().strip()
        print("")

    return answer[0] == "y"


66
67
def print_sessions_list(slist):
    for session in slist:
Vincent Michel's avatar
Vincent Michel committed
68
        print(session)
69
70
71


def print_sessions_and_trees(slist):
Vincent Michel's avatar
Vincent Michel committed
72
    print("Available BLISS sessions are:")
73
74
75
76
77
    config = static.get_config()
    for name in slist:
        session = config.get(name)
        session.sessions_tree.show()

78

79
def delete_session(session_name):
Vincent Michel's avatar
Vincent Michel committed
80
    print(("Removing '%s' session." % session_name))
81

82
83
    if session_name in get_sessions_list():
        config = static.get_config()
84

85
86
        # Gets config of the session by its name.
        session_config = config.get_config(session_name)
87

88
        # Gets the name of the setup file (found in YML file).
89
        setup_filename = session_config.get("setup-file")
90
91
92
93

        # Gets name of the YML file.
        session_file = session_config.filename

94
        if setup_filename is not None:
95
            # Gets the full path.
96
            if setup_filename.startswith("."):  # relative path
97
                base_path = os.path.dirname(session_file)
98
99
                setup_filename = os.path.normpath(
                    os.path.join(base_path, setup_filename)
100
                )
101
102
103
                script_filename = "scripts/%s.py" % session_name
                script_filename = os.path.normpath(
                    os.path.join(base_path, script_filename)
104
                )
105
106

            # Removes <session_name>_setup.py file.
107
108
            print(("removing .../%s" % setup_filename))
            client.remove_config_file(setup_filename)
109
110

        # Removes YML file.
Vincent Michel's avatar
Vincent Michel committed
111
        print(("removing .../%s" % session_file))
112
113
114
        client.remove_config_file(session_file)

        # Removes script file.
115
116
        print(("removing .../%s" % script_filename))
        client.remove_config_file(script_filename)
117
118
119


def create_session(session_name):
120
121
122
123
124
125
126
127
128
    """
    Creation of skeleton files for a new session:
       sessions/<session_name>.yml
       sessions/<session_name>_setup.py
       sessions/scripts/<session_name>.py

    This method is valid even if config directory is located on
    a remote computer.
    """
129
130
131
132
133
    beacon = client.get_default_connection()
    print(
        f"Creating '{session_name}' BLISS session on"
        f"BEACON_HOST={beacon._host}:{beacon._port_number}"
    )
134

135
136
    config = static.get_config()
    config.set_config_db_file("sessions/__init__.yml", "plugin: session\n")
137

138
    # <session_name>.yml: config file created as a config Node.
139
140
141
    filename = "sessions/%s.yml" % session_name
    new_session_node = ConfigNode(config.root, filename=filename)
    print(("Creating %s" % filename))
142
143
144
145
146
147
148
149
    new_session_node.update(
        {
            "class": "Session",
            "name": session_name,
            "setup-file": "./%s_setup.py" % session_name,
            "config-objects": [],
        }
    )
150
151
    new_session_node.save()

152
    # <session_name>_setup.py: setup file of the session.
153
    skeleton = sft.xxx_setup_py_template.render(name=session_name)
154
155
156
    filename = "sessions/%s_setup.py" % session_name
    print(("Creating %s" % filename))
    config.set_config_db_file(filename, skeleton)
157
158
159

    # scripts/<session_name>.py: additional python script file.
    skeleton = sft.xxx_py_template.render(name=session_name)
160
161
162
    filename = "sessions/scripts/%s.py" % session_name
    print(("Creating %s" % filename))
    config.set_config_db_file(filename, skeleton)
163
164


165
def main():
166
167
    # Parse arguments wit docopt : it uses this file (main.py) docstring
    # to define parameters to handle.
168
    try:
169
170
        arguments = docopt(__doc__)
    except DocoptExit:
Vincent Michel's avatar
Vincent Michel committed
171
172
173
        print("")
        print("Available BLISS sessions:")
        print("-------------------------")
174
        print_sessions_list(get_sessions_list())
Vincent Michel's avatar
Vincent Michel committed
175
        print("")
176
177
178
        arguments = docopt(__doc__)

    # Print version
179
    if arguments["--version"]:
Vincent Michel's avatar
Vincent Michel committed
180
        print(("BLISS version %s" % release.short_version))
181
182
183
        sys.exit()

    # Display session names and trees
184
    if arguments["--show-sessions"]:
185
        print_sessions_and_trees(get_sessions_list())
186
        sys.exit(0)
187
188

    # Display session names only
189
    if arguments["--show-sessions-only"]:
190
        print_sessions_list(get_sessions_list())
191
        sys.exit(0)
192
193

    # Create session
194
195
    if arguments["--create"]:
        session_name = arguments["--create"]
196
        if session_name in get_sessions_list():
Vincent Michel's avatar
Vincent Michel committed
197
            print(("Session '%s' cannot be created: it already exists." % session_name))
198
            sys.exit(0)
199
200
        elif session_name[0].isdigit():
            print(f"Invalid session name ({session_name}). Must start with [a-zA-Z_]")
201
            sys.exit(0)
202
203
204
        else:
            create_session(session_name)
            # exit ( or launch new session ? )
205
            sys.exit(0)
206
207

    # Delete session
208
209
    if arguments["--delete"]:
        session_name = arguments["--delete"]
210
        if session_name in get_sessions_list():
211
212
            if yes_or_no("Do you want to delete '%s' session?" % session_name):
                delete_session(session_name)
213
            sys.exit(0)
214
        else:
Vincent Michel's avatar
Vincent Michel committed
215
216
217
218
219
            print(
                (
                    "Session '%s' cannot be deleted: it seems it does not exist."
                    % session_name
                )
220
            )
221
            sys.exit(0)
222

223
    # check beacon connection
224
    static.get_config()
225

226
    # Start a specific session
227
228
    if arguments["--session"]:
        session_name = arguments["--session"]
229
        if session_name not in get_sessions_list():
Vincent Michel's avatar
Vincent Michel committed
230
            print(("'%s' does not seem to be a valid session, exiting." % session_name))
231
            print_sessions_list(get_sessions_list())
232
            sys.exit(0)
233
    else:
234
        session_name = None
235

Linus Pithan's avatar
Linus Pithan committed
236
237
238
239
240
241
    # no tmux for DEFAULT session!
    if (
        arguments["--no-tmux"]
        or sys.platform in ["win32", "cygwin"]
        or session_name is None
    ):
242

Perceval Guillou's avatar
Perceval Guillou committed
243
244
245
246
247
248
249
        # disable those ugly loggers from jedi
        logging.getLogger("parso.python.diff").disabled = True
        logging.getLogger("parso.cache").disabled = True
        # initialize logging
        log_level = getattr(logging, arguments["--log-level"][0].upper())
        logging_startup(log_level)

250
        # If session_name is None, an empty session is started.
251
        embed(session_name=session_name, expert_error_report=bool(arguments["--debug"]))
252
        current_session.close()
253
        global_map.clear()
254
255
256

    else:

GUILLOU Perceval's avatar
GUILLOU Perceval committed
257
        if session_name is None:
258
            session = constants.DEFAULT_SESSION_NAME
259
260
261
262
263
264
265
266
267
268
        else:
            session = session_name

        from bliss import config

        config_path = os.path.join(os.path.dirname(config.__file__), "tmux.conf")

        win1 = "bliss"
        win2 = "scan"

269
270
271
272
273
274
275
276
277
278
279
280
281
        uid = os.geteuid()

        # not sure if we should use the user id instead => os.getuid()
        # euid (effective user id) can be different from uid.
        # The difference between the regular UID and the Effective UID is that
        # only the EUID is checked when you do something that requires special access
        #  (such as reading or writing a file, or making certain system calls).
        # The UID indicates the actual user who is performing the action,
        # but it is (usually) not considered when examining permissions.
        # In normal programs they will be the same.
        # Some programs change their EUID to add or subtract from the actions they are allowed to take.
        # A smaller number also change their UID, to effectively "become" another user.

282
        # to use different tmux servers per session the session name is included in the sock name
Linus Pithan's avatar
Linus Pithan committed
283
        # for the default session there is no tmux at all
284

Linus Pithan's avatar
Linus Pithan committed
285
        tsock = f"/tmp/bliss_tmux_{session_name}_{uid}.sock"
286

287
        ans = subprocess.run(
288
            ["tmux", "-S", tsock, "has-session", "-t", "=%s" % session],
289
290
291
292
293
294
295
296
297
            capture_output=True,
            text=True,
        )
        # print("stdout = ",ans.stdout,", stderr = ", ans.stderr,", returncode = ", ans.returncode)

        if ans.returncode == 0:
            print(f"Tmux session {session} already exist, joining session...")
        else:
            print(f"Starting new tmux session {session}...")
298
            ans = subprocess.run(["tmux", "-S", tsock, "start-server"])
299

300
            if arguments["--debug"]:
301
302
303
                ans = subprocess.run(
                    [
                        "tmux",
304
305
                        "-S",
                        tsock,
306
307
308
309
310
311
312
313
314
315
316
                        "-f",
                        config_path,
                        "new-session",
                        "-d",
                        "-s",
                        session,
                        "-n",
                        win1,
                    ]
                )

317
                sub_cmd = f"{sys.executable} -m bliss.shell.cli.start_bliss_repl {session} {arguments['--log-level'][0]} 1"
318
                ans = subprocess.run(
319
                    ["tmux", "-S", tsock, "send-keys", "-t", win1, sub_cmd, "Enter"]
320
321
                )

GUILLOU Perceval's avatar
GUILLOU Perceval committed
322
323
324
325
                ans = subprocess.run(
                    ["tmux", "-S", tsock, "new-window", "-d", "-n", win2]
                )

326
327
328
                sub_cmd = (
                    f"{sys.executable} -m bliss.shell.data.start_listener {session}"
                )
GUILLOU Perceval's avatar
GUILLOU Perceval committed
329
330
331
332
                ans = subprocess.run(
                    ["tmux", "-S", tsock, "send-keys", "-t", win2, sub_cmd, "Enter"]
                )

333
334
335
336
            else:
                ans = subprocess.run(
                    [
                        "tmux",
337
338
                        "-S",
                        tsock,
339
340
341
342
343
344
345
346
                        "-f",
                        config_path,
                        "new-session",
                        "-d",
                        "-s",
                        session,
                        "-n",
                        win1,
347
                        sys.executable,
348
349
350
351
352
353
354
                        "-m",
                        "bliss.shell.cli.start_bliss_repl",
                        session,
                        arguments["--log-level"][0],
                    ]
                )

GUILLOU Perceval's avatar
GUILLOU Perceval committed
355
356
357
358
359
360
361
362
363
                ans = subprocess.run(
                    [
                        "tmux",
                        "-S",
                        tsock,
                        "new-window",
                        "-d",
                        "-n",
                        win2,
364
                        sys.executable,
GUILLOU Perceval's avatar
GUILLOU Perceval committed
365
                        "-m",
366
                        "bliss.shell.data.start_listener",
GUILLOU Perceval's avatar
GUILLOU Perceval committed
367
368
369
                        session,
                    ]
                )
370
371
372
373

        ans = subprocess.run(
            [
                "tmux",
374
375
                "-S",
                tsock,
376
377
378
379
380
381
382
                "set-hook",
                "-t",
                session,
                "pane-exited",
                "kill-session -t %s" % session,
            ]
        )
383
        ans = subprocess.run(["tmux", "-S", tsock, "attach-session", "-t", session])
384

385
386

if __name__ == "__main__":
387

388
    main()