diff --git a/cylc/flow/scripts/reinstall.py b/cylc/flow/scripts/reinstall.py
index d81d3e6949e..bb471cc05e9 100644
--- a/cylc/flow/scripts/reinstall.py
+++ b/cylc/flow/scripts/reinstall.py
@@ -30,18 +30,56 @@
# Or, to reinstall a specific run:
$ cylc reinstall myflow/run2
- # View the changes reinstall would make:
- $ cylc reinstall myflow --dry-run
+ # If the workflow is running:
+ $ cylc reinstall myflow # reinstall as usual
+ $ cylc reload myflow # pick up changes in the workflow config
+
+What reinstall does:
+ Reinstall synchronises files between the workflow source and the specified
+ run directory.
+
+ Any files which have been added, updated or removed in the source directory
+ will be added, updated or removed in the run directory. Cylc uses "rsync"
+ to do this (run in "--debug" mode to see the exact command used).
+
+How changes are displayed:
+ Reinstall will first perform a dry run showing the files it would change.
+ This is displayed in "rsync" format e.g:
+
+ send foo # this means the file "foo" would be added/updated
+ del. bar # this means the file "bar" would be deleted
+
+How to prevent reinstall deleting files:
+ Reinstall will delete any files which are not present in the source directory
+ (i.e. if you delete a file from the source directory, a reinstall would
+ remove the file from the run directory too). The "work/" and "share/"
+ directory are excluded from this. These are the recommended locations for any
+ files created at runtime.
+
+ You can extend the list of "excluded" paths by creating a ".cylcignore" file.
+ For example the following file would exclude "data/" and any ".csv" files
+ from being overwritten by a reinstallation:
+
+ $ cat .cylcignore
+ data
+ *.csv
+
+ Note any paths listed in ".cylcignore" will not be installed by
+ "cylc install" even if present in the source directory.
"""
from pathlib import Path
import sys
-from typing import Optional, TYPE_CHECKING
+from typing import Optional, TYPE_CHECKING, List
from ansimarkup import parse as cparse
from cylc.flow import iter_entry_points
-from cylc.flow.exceptions import PluginError, WorkflowFilesError
+from cylc.flow.exceptions import (
+ PluginError,
+ ServiceFileError,
+ WorkflowFilesError,
+)
from cylc.flow.id_cli import parse_id
from cylc.flow.option_parsers import (
WORKFLOW_ID_ARG_DOC,
@@ -50,9 +88,10 @@
from cylc.flow.pathutil import get_workflow_run_dir
from cylc.flow.workflow_files import (
get_workflow_source_dir,
+ load_contact_file,
reinstall_workflow,
)
-from cylc.flow.terminal import cli_function
+from cylc.flow.terminal import cli_function, DIM, is_terminal
if TYPE_CHECKING:
from optparse import Values
@@ -78,48 +117,19 @@ def get_option_parser() -> COP:
dest="clear_rose_install_opts"
)
- parser.add_option(
- '--dry', '--dry-run',
- action='store_true',
- help='Show the changes reinstallation would make.'
- )
-
return parser
-def format_rsync_out(out):
- """Format rsync stdout for presenting to users.
-
- Note: Output formats of different rsync implementations may differ so keep
- this code simple and robust.
-
- """
- lines = []
- for line in out.splitlines():
- if line[0:4] == 'send':
- # file added or updated
- lines.append(cparse(f'{line}'))
- elif line[0:4] == 'del.':
- # file deleted
- lines.append(cparse(f'{line}'))
- elif line == 'cannot delete non-empty directory: opt':
- # These "cannot delete non-empty directory" messages can arise
- # as a result of excluding files within sub-directories.
- # This opt dir message is likely to occur when a rose-suit.conf
- # file is present.
- continue
- else:
- # other uncategorised log line
- lines.append(line)
- return lines
-
-
@cli_function(get_option_parser)
def main(
parser: COP,
opts: 'Values',
args: Optional[str] = None
) -> None:
+ """Implement cylc reinstall.
+
+ This is the bit which contains all the CLI logic.
+ """
run_dir: Optional[Path]
workflow_id: str
workflow_id, *_ = parse_id(
@@ -141,7 +151,62 @@ def main(
' symlink to continue.'
)
- if not opts.dry:
+ try:
+ usr: str = ''
+ if is_terminal():
+ # interactive mode - perform dry-run and prompt
+ reinstall(opts, workflow_id, source, run_dir, dry_run=True)
+ print(
+ cparse(
+ f'\n<{DIM}>TIP: You can "exclude" files/dirs to prevent'
+ ' Cylc from installing or overwriting\n them by adding'
+ 'them to the .cylcignore file. See cylc reinstall --help.'
+ f'{DIM}>\n'
+ '\nReinstall would make the above changes.'
+ )
+ )
+ # prompt for permission to continue
+ while usr not in ['y', 'n']:
+ usr = input(
+ cparse('Continue [y/n]: ')
+ ).lower()
+ else:
+ # non interactive-mode - no dry-run, no prompt
+ usr = 'y'
+ except KeyboardInterrupt:
+ # ensure the "reinstall canceled" message shows for ctrl+c
+ usr = 'n' # cancel the reinstall
+ print() # clear the traceback line
+
+ if usr == 'y':
+ reinstall(opts, workflow_id, source, run_dir, dry_run=False)
+ print(cparse('Successfully reinstalled.'))
+ if is_workflow_running(workflow_id):
+ print(cparse(
+ '\n'
+ 'Run "cylc reload {workflow_id}" to pick up changes.'
+ ''
+ ))
+ else:
+ print(
+ cparse('Reinstall canceled, no changes made.')
+ )
+
+
+def reinstall(
+ opts: 'Values',
+ workflow_id: str,
+ source: str,
+ run_dir: Path,
+ dry_run: bool = False,
+) -> None:
+ """Perform reinstallation.
+
+ This is the purely functional bit without any CLI logic.
+ """
+ # run pre_configure plugins
+ if not dry_run:
+ # don't run plugins in dry-mode
for entry_point in iter_entry_points(
'cylc.pre_configure'
):
@@ -156,28 +221,36 @@ def main(
exc
) from None
- stdout = reinstall_workflow(
+ # reinstall from source
+ stdout: str = reinstall_workflow(
source=Path(source),
named_run=workflow_id,
rundir=run_dir,
- dry_run=opts.dry,
+ dry_run=dry_run,
)
- if Path(source, 'rose-suite.conf').is_file():
- print(
- cparse(
- ''
- 'NOTE: Files created by Rose file installation will show as'
- ' deleted.'
- '\n They will be re-created during the reinstall'
- ' process.'
- '',
- ),
- file=sys.stderr,
- )
- print('\n'.join(format_rsync_out(stdout)), file=sys.stderr)
+ # display changes
+ if dry_run:
+ # only write rsync output in dry-mode
+ if Path(source, 'rose-suite.conf').is_file():
+ # TODO: remove this in combination with
+ # https://github.com/cylc/cylc-rose/issues/149
+ print(
+ cparse(
+ f'\n<{DIM}>'
+ 'NOTE: Files created by Rose file installation will show'
+ ' as deleted.'
+ '\n They will be re-created during the reinstall'
+ ' process.'
+ f'{DIM}>\n',
+ ),
+ file=sys.stderr,
+ )
+ print('\n'.join(format_rsync_out(stdout)), file=sys.stderr)
- if not opts.dry:
+ # run post_install plugins
+ if not dry_run:
+ # don't run plugins in dry-mode
for entry_point in iter_entry_points(
'cylc.post_install'
):
@@ -195,3 +268,53 @@ def main(
entry_point.name,
exc
) from None
+
+
+def format_rsync_out(out: str) -> List[str]:
+ r"""Format rsync stdout for presenting to users.
+
+ Note: Output formats of different rsync implementations may differ so keep
+ this code simple and robust.
+
+ Example:
+ >>> format_rsync_out(
+ ... 'send foo\ndel. bar\nbaz'
+ ... ) == [
+ ... cparse('send foo'),
+ ... cparse('del. bar'),
+ ... 'baz',
+ ... ]
+ True
+
+ """
+ lines = []
+ for line in out.splitlines():
+ if line[0:4] == 'send':
+ # file added or updated
+ lines.append(cparse(f'{line}'))
+ elif line[0:4] == 'del.':
+ # file deleted
+ lines.append(cparse(f'{line}'))
+ elif line == 'cannot delete non-empty directory: opt':
+ # These "cannot delete non-empty directory" messages can arise
+ # as a result of excluding files within sub-directories.
+ # This opt dir message is likely to occur when a rose-suit.conf
+ # file is present.
+ continue
+ else:
+ # other uncategorised log line
+ lines.append(line)
+ return lines
+
+
+def is_workflow_running(workflow_id: str) -> bool:
+ """Quick and simple way to tell if a workflow is running.
+
+ It would be better to "ping" the workflow, however, this is sufficient
+ for our purposes.
+ """
+ try:
+ load_contact_file(workflow_id)
+ except ServiceFileError:
+ return False
+ return True
diff --git a/cylc/flow/workflow_files.py b/cylc/flow/workflow_files.py
index 26e63992faf..76e3570ea49 100644
--- a/cylc/flow/workflow_files.py
+++ b/cylc/flow/workflow_files.py
@@ -1514,7 +1514,7 @@ def reinstall_workflow(
dry_run=dry_run,
)
reinstall_log.info(cli_format(rsync_cmd))
- # print(cli_format(rsync_cmd))
+ LOG.debug(cli_format(rsync_cmd))
proc = Popen(rsync_cmd, stdout=PIPE, stderr=PIPE, text=True) # nosec
# * command is constructed via internal interface
stdout, stderr = proc.communicate()