
The isAlive() method of threading.Thread has been removed in Python 3.9. The is_alive() method is available on Python 2.6+. See https://bugs.python.org/issue37804 Change-Id: I951b1ae331c3101722fe34babf81d6f82d838380
189 lines
5.9 KiB
Python
189 lines
5.9 KiB
Python
# Copyright (c) 2016 IBM Corp.
|
|
# Copyright 2017 Red Hat, Inc.
|
|
#
|
|
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
|
# not use this file except in compliance with the License. You may obtain
|
|
# a copy of the License at
|
|
#
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
#
|
|
# Unless required by applicable law or agreed to in writing, software
|
|
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
# License for the specific language governing permissions and limitations
|
|
# under the License.
|
|
|
|
import logging
|
|
import os
|
|
import os.path
|
|
import re
|
|
import select
|
|
import threading
|
|
import time
|
|
|
|
from zuul.lib import streamer_utils
|
|
|
|
|
|
class Log(object):
|
|
|
|
def __init__(self, path):
|
|
self.path = path
|
|
# The logs are written as binary encoded utf-8, which is what we
|
|
# send over the wire.
|
|
self.file = open(path, 'rb')
|
|
self.stat = os.stat(path)
|
|
self.size = self.stat.st_size
|
|
|
|
|
|
class RequestHandler(streamer_utils.BaseFingerRequestHandler):
|
|
'''
|
|
Class to handle a single log streaming request.
|
|
|
|
The log streaming code was blatantly stolen from zuul_console.py. Only
|
|
the (class/method/attribute) names were changed to protect the innocent.
|
|
'''
|
|
|
|
log = logging.getLogger("zuul.log_streamer")
|
|
|
|
def handle(self):
|
|
try:
|
|
build_uuid = self.getCommand()
|
|
except Exception:
|
|
self.log.exception("Failure during getCommand:")
|
|
msg = 'Internal streaming error'
|
|
self.request.sendall(msg.encode("utf-8"))
|
|
return
|
|
|
|
# validate build ID
|
|
if not re.match("[0-9A-Fa-f]+$", build_uuid):
|
|
msg = 'Build ID %s is not valid' % build_uuid
|
|
self.request.sendall(msg.encode("utf-8"))
|
|
return
|
|
|
|
job_dir = os.path.join(self.server.jobdir_root, build_uuid)
|
|
if not os.path.exists(job_dir):
|
|
msg = 'Build ID %s not found' % build_uuid
|
|
self.request.sendall(msg.encode("utf-8"))
|
|
return
|
|
|
|
# check if log file exists
|
|
log_file = os.path.join(job_dir, 'work', 'logs', 'job-output.txt')
|
|
if not os.path.exists(log_file):
|
|
msg = 'Log not found for build ID %s' % build_uuid
|
|
self.request.sendall(msg.encode("utf-8"))
|
|
return
|
|
|
|
try:
|
|
self.stream_log(log_file)
|
|
except Exception:
|
|
self.log.exception("Streaming failure for build UUID %s:",
|
|
build_uuid)
|
|
msg = 'Internal streaming error'
|
|
self.request.sendall(msg.encode("utf-8"))
|
|
|
|
def stream_log(self, log_file):
|
|
log = None
|
|
while True:
|
|
if log is not None:
|
|
try:
|
|
log.file.close()
|
|
except Exception:
|
|
pass
|
|
while True:
|
|
log = self.chunk_log(log_file)
|
|
if log:
|
|
break
|
|
time.sleep(0.5)
|
|
while True:
|
|
if self.follow_log(log):
|
|
break
|
|
else:
|
|
if log is not None:
|
|
try:
|
|
log.file.close()
|
|
except Exception:
|
|
pass
|
|
return
|
|
|
|
def chunk_log(self, log_file):
|
|
try:
|
|
log = Log(log_file)
|
|
except Exception:
|
|
return
|
|
while True:
|
|
chunk = log.file.read(4096)
|
|
if not chunk:
|
|
break
|
|
self.request.send(chunk)
|
|
return log
|
|
|
|
def follow_log(self, log):
|
|
while True:
|
|
# As long as we have unread data, keep reading/sending
|
|
while True:
|
|
chunk = log.file.read(4096)
|
|
if chunk:
|
|
self.request.send(chunk)
|
|
else:
|
|
break
|
|
|
|
# See if the file has been removed, meaning we should stop
|
|
# streaming it.
|
|
if not os.path.exists(log.path):
|
|
return False
|
|
|
|
# At this point, we are waiting for more data to be written
|
|
time.sleep(0.5)
|
|
|
|
# Check to see if the remote end has sent any data, if so,
|
|
# discard
|
|
r, w, e = select.select([self.request], [], [self.request], 0)
|
|
if self.request in e:
|
|
return False
|
|
if self.request in r:
|
|
ret = self.request.recv(1024)
|
|
# Discard anything read, if input is eof, it has
|
|
# disconnected.
|
|
if not ret:
|
|
return False
|
|
|
|
|
|
class LogStreamerServer(streamer_utils.CustomThreadingTCPServer):
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
self.jobdir_root = kwargs.pop('jobdir_root')
|
|
super(LogStreamerServer, self).__init__(*args, **kwargs)
|
|
|
|
|
|
class LogStreamer(object):
|
|
'''
|
|
Class implementing log streaming over the finger daemon port.
|
|
'''
|
|
|
|
def __init__(self, host, port, jobdir_root):
|
|
self.log = logging.getLogger('zuul.log_streamer')
|
|
self.log.debug("LogStreamer starting on port %s", port)
|
|
self.server = LogStreamerServer((host, port),
|
|
RequestHandler,
|
|
jobdir_root=jobdir_root)
|
|
|
|
# We start the actual serving within a thread so we can return to
|
|
# the owner.
|
|
self.thd = threading.Thread(target=self._run)
|
|
self.thd.daemon = True
|
|
self.thd.start()
|
|
|
|
def _run(self):
|
|
try:
|
|
self.server.serve_forever()
|
|
except Exception:
|
|
self.log.exception("Abnormal termination:")
|
|
raise
|
|
|
|
def stop(self):
|
|
if self.thd.is_alive():
|
|
self.server.shutdown()
|
|
self.server.server_close()
|
|
self.thd.join()
|
|
self.log.debug("LogStreamer stopped")
|