2929import jinja2
3030from jinja2 import select_autoescape
3131import json
32+ import markupsafe
3233import mimetypes
3334import os
3435import pwd
4142import traceback
4243from urllib .parse import quote
4344
45+ from typing import TYPE_CHECKING , Union
46+
4447from cylc .flow import __version__ as CYLC_VERSION
4548from cylc .flow .hostuserutil import get_host
4649from cylc .flow .task_state import TASK_STATUSES_ORDERED
4952from cylc .uiserver .ws import get_util_home
5053
5154
55+ if TYPE_CHECKING :
56+ from io import TextIOWrapper
57+
58+
5259# Cylc 7 Task states.
5360CYLC7_TASK_STATUSES_ORDERED = [
5461 'runahead' ,
@@ -517,7 +524,7 @@ def suites(
517524 continue
518525 with contextlib .suppress (OSError ):
519526 data ["entries" ].append ({
520- "name" : str ( item ),
527+ "name" : item . encode (). decode ( 'UTF-8' , errors = 'replace' ),
521528 "info" : {},
522529 "last_activity_time" : (
523530 self .get_last_activity_time (user , item ))})
@@ -548,8 +555,9 @@ def suites(
548555 rosie_info = {}
549556 with contextlib .suppress (IOError ):
550557 for line in open ( # noqa: SIM115
551- rosie_suite_info , 'r '
558+ rosie_suite_info , 'rb '
552559 ).readlines ():
560+ line = line .decode ('utf-8' , errors = 'replace' )
553561 if not line .strip ().startswith ('#' ) and '=' in line :
554562 rosie_key , rosie_val = line .strip ().split ("=" , 1 )
555563 if rosie_key in ("project" , "title" ):
@@ -576,8 +584,8 @@ def get_file(self, user, suite, path, path_in_tar=None, mode=None):
576584 except KeyError :
577585 raise cherrypy .HTTPError (404 ) from None
578586 f_size = tar_info .size
579- handle = tar_f .extractfile (path_in_tar )
580- if handle . read ( 2 ) == "#!" :
587+ handle = tar_f .extractfile (path_in_tar , 'rb' )
588+ if self . _is_shebang ( handle ) :
581589 mime = self .MIME_TEXT_PLAIN
582590 else :
583591 mime = mimetypes .guess_type (
@@ -601,10 +609,10 @@ def get_file(self, user, suite, path, path_in_tar=None, mode=None):
601609 return cherrypy .lib .static .serve_file (temp_f .name , mime )
602610 finally :
603611 temp_f .close ()
604- text = handle . read ( )
612+ text = self . _get_file_text ( handle )
605613 else :
606614 f_size = os .stat (f_name ).st_size
607- if open (f_name ). read ( 2 ) == "#!" : # noqa: SIM115
615+ if self . _is_shebang (f_name ):
608616 mime = self .MIME_TEXT_PLAIN
609617 else :
610618 mime = mimetypes .guess_type (quote (f_name ))[0 ]
@@ -618,7 +626,7 @@ def get_file(self, user, suite, path, path_in_tar=None, mode=None):
618626 ):
619627 cherrypy .response .headers ["Content-Type" ] = mime
620628 return cherrypy .lib .static .serve_file (f_name , mime )
621- text = open (f_name ). read () # noqa: SIM115
629+ text = self . _get_file_text (f_name )
622630 try :
623631 text = str (text )
624632 if mode in [None , "text" ]:
@@ -627,7 +635,7 @@ def get_file(self, user, suite, path, path_in_tar=None, mode=None):
627635 # escape future modifications to this string. In order to
628636 # allow log file syntax highlighting (DEBUG, INFO, etc) we
629637 # must cast this back to a str to remove this functionality.
630- text = str (jinja2 .escape (text ))
638+ text = str (markupsafe .escape (text ))
631639 lines = text .splitlines ()
632640 except ValueError :
633641 if path_in_tar :
@@ -1097,3 +1105,28 @@ def _get_user_suite_dir(self, user, suite, *paths):
10971105 def _sort_summary_entries (suite1 ):
10981106 """Sort suites by last_activity_time."""
10991107 return suite1 .get ("last_activity_time" ) or suite1 ["name" ]
1108+
1109+ @staticmethod
1110+ def _has_shebang (file : Union [str , TextIOWrapper ]) -> bool :
1111+ """File (or handle) has shebang"""
1112+ # It's a filepath
1113+ if isinstance (file , str ):
1114+ with open (file , 'rb' ) as handle :
1115+ return handle .read (2 ) == b'!#'
1116+
1117+ # It's a file handle: Make sure that you close it later!
1118+ first_chars = file .read (2 )
1119+ return first_chars in [b'!#' , '!#' ]
1120+
1121+ @staticmethod
1122+ def _get_file_text (file : Union [str , TextIOWrapper ]) -> str :
1123+ """File (or handle) has shebang"""
1124+ if isinstance (file , str ):
1125+ with open (file , 'rb' ) as handle :
1126+ return handle .read ().decode ('utf-8' , errors = 'replace' )
1127+
1128+ # It's a file handle: Make sure that you close it later!
1129+ filecontent = file .read ()
1130+ if isinstance (filecontent , bytes ):
1131+ return filecontent .decode ('utf-8' , errors = 'replace' )
1132+ return filecontent
0 commit comments