# Copyright 2019 The Meson development team # 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. # This class contains the basic functionality needed to run any interpreter # or an interpreter-based tool. from .common import CMakeException, CMakeConfiguration, CMakeBuildFile from .. import mlog from contextlib import contextmanager from subprocess import Popen, PIPE, TimeoutExpired from pathlib import Path import typing as T import json if T.TYPE_CHECKING: from ..environment import Environment from .executor import CMakeExecutor CMAKE_SERVER_BEGIN_STR = '[== "CMake Server" ==[' CMAKE_SERVER_END_STR = ']== "CMake Server" ==]' CMAKE_MESSAGE_TYPES = { 'error': ['cookie', 'errorMessage'], 'hello': ['supportedProtocolVersions'], 'message': ['cookie', 'message'], 'progress': ['cookie'], 'reply': ['cookie', 'inReplyTo'], 'signal': ['cookie', 'name'], } # type: T.Dict[str, T.List[str]] CMAKE_REPLY_TYPES = { 'handshake': [], 'configure': [], 'compute': [], 'cmakeInputs': ['buildFiles', 'cmakeRootDirectory', 'sourceDirectory'], 'codemodel': ['configurations'] } # type: T.Dict[str, T.List[str]] # Base CMake server message classes class MessageBase: def __init__(self, msg_type: str, cookie: str) -> None: self.type = msg_type self.cookie = cookie def to_dict(self) -> T.Dict[str, T.Union[str, T.List[str], T.Dict[str, int]]]: return {'type': self.type, 'cookie': self.cookie} def log(self) -> None: mlog.warning('CMake server message of type', mlog.bold(type(self).__name__), 'has no log function') class RequestBase(MessageBase): cookie_counter = 0 def __init__(self, msg_type: str) -> None: super().__init__(msg_type, self.gen_cookie()) @staticmethod def gen_cookie() -> str: RequestBase.cookie_counter += 1 return 'meson_{}'.format(RequestBase.cookie_counter) class ReplyBase(MessageBase): def __init__(self, cookie: str, in_reply_to: str) -> None: super().__init__('reply', cookie) self.in_reply_to = in_reply_to class SignalBase(MessageBase): def __init__(self, cookie: str, signal_name: str) -> None: super().__init__('signal', cookie) self.signal_name = signal_name def log(self) -> None: mlog.log(mlog.bold('CMake signal:'), mlog.yellow(self.signal_name)) # Special Message classes class Error(MessageBase): def __init__(self, cookie: str, message: str) -> None: super().__init__('error', cookie) self.message = message def log(self) -> None: mlog.error(mlog.bold('CMake server error:'), mlog.red(self.message)) class Message(MessageBase): def __init__(self, cookie: str, message: str) -> None: super().__init__('message', cookie) self.message = message def log(self) -> None: #mlog.log(mlog.bold('CMake:'), self.message) pass class Progress(MessageBase): def __init__(self, cookie: str) -> None: super().__init__('progress', cookie) def log(self) -> None: pass class MessageHello(MessageBase): def __init__(self, supported_protocol_versions: T.List[T.Dict[str, int]]) -> None: super().__init__('hello', '') self.supported_protocol_versions = supported_protocol_versions def supports(self, major: int, minor: T.Optional[int] = None) -> bool: for i in self.supported_protocol_versions: assert 'major' in i assert 'minor' in i if major == i['major']: if minor is None or minor == i['minor']: return True return False # Request classes class RequestHandShake(RequestBase): def __init__(self, src_dir: Path, build_dir: Path, generator: str, vers_major: int, vers_minor: T.Optional[int] = None) -> None: super().__init__('handshake') self.src_dir = src_dir self.build_dir = build_dir self.generator = generator self.vers_major = vers_major self.vers_minor = vers_minor def to_dict(self) -> T.Dict[str, T.Union[str, T.List[str], T.Dict[str, int]]]: vers = {'major': self.vers_major} if self.vers_minor is not None: vers['minor'] = self.vers_minor # Old CMake versions (3.7) want '/' even on Windows self.src_dir = self.src_dir.resolve() self.build_dir = self.build_dir.resolve() return { **super().to_dict(), 'sourceDirectory': self.src_dir.as_posix(), 'buildDirectory': self.build_dir.as_posix(), 'generator': self.generator, 'protocolVersion': vers } class RequestConfigure(RequestBase): def __init__(self, args: T.Optional[T.List[str]] = None): super().__init__('configure') self.args = args def to_dict(self) -> T.Dict[str, T.Union[str, T.List[str], T.Dict[str, int]]]: res = super().to_dict() if self.args: res['cacheArguments'] = self.args return res class RequestCompute(RequestBase): def __init__(self) -> None: super().__init__('compute') class RequestCMakeInputs(RequestBase): def __init__(self) -> None: super().__init__('cmakeInputs') class RequestCodeModel(RequestBase): def __init__(self) -> None: super().__init__('codemodel') # Reply classes class ReplyHandShake(ReplyBase): def __init__(self, cookie: str) -> None: super().__init__(cookie, 'handshake') class ReplyConfigure(ReplyBase): def __init__(self, cookie: str) -> None: super().__init__(cookie, 'configure') class ReplyCompute(ReplyBase): def __init__(self, cookie: str) -> None: super().__init__(cookie, 'compute') class ReplyCMakeInputs(ReplyBase): def __init__(self, cookie: str, cmake_root: Path, src_dir: Path, build_files: T.List[CMakeBuildFile]) -> None: super().__init__(cookie, 'cmakeInputs') self.cmake_root = cmake_root self.src_dir = src_dir self.build_files = build_files def log(self) -> None: mlog.log('CMake root: ', mlog.bold(self.cmake_root.as_posix())) mlog.log('Source dir: ', mlog.bold(self.src_dir.as_posix())) mlog.log('Build files:', mlog.bold(str(len(self.build_files)))) with mlog.nested(): for i in self.build_files: mlog.log(str(i)) class ReplyCodeModel(ReplyBase): def __init__(self, data: T.Dict[str, T.Any]) -> None: super().__init__(data['cookie'], 'codemodel') self.configs = [] for i in data['configurations']: self.configs += [CMakeConfiguration(i)] def log(self) -> None: mlog.log('CMake code mode:') for idx, i in enumerate(self.configs): mlog.log('Configuration {}:'.format(idx)) with mlog.nested(): i.log() # Main client class class CMakeClient: def __init__(self, env: 'Environment') -> None: self.env = env self.proc = None # type: T.Optional[Popen] self.type_map = { 'error': lambda data: Error(data['cookie'], data['errorMessage']), 'hello': lambda data: MessageHello(data['supportedProtocolVersions']), 'message': lambda data: Message(data['cookie'], data['message']), 'progress': lambda data: Progress(data['cookie']), 'reply': self.resolve_type_reply, 'signal': lambda data: SignalBase(data['cookie'], data['name']) } # type: T.Dict[str, T.Callable[[T.Dict[str, T.Any]], MessageBase]] self.reply_map = { 'handshake': lambda data: ReplyHandShake(data['cookie']), 'configure': lambda data: ReplyConfigure(data['cookie']), 'compute': lambda data: ReplyCompute(data['cookie']), 'cmakeInputs': self.resolve_reply_cmakeInputs, 'codemodel': lambda data: ReplyCodeModel(data), } # type: T.Dict[str, T.Callable[[T.Dict[str, T.Any]], ReplyBase]] def readMessageRaw(self) -> T.Dict[str, T.Any]: assert self.proc is not None rawData = [] begin = False while self.proc.poll() is None: line = self.proc.stdout.readline() if not line: break line = line.decode('utf-8') line = line.strip() if begin and line == CMAKE_SERVER_END_STR: break # End of the message elif begin: rawData += [line] elif line == CMAKE_SERVER_BEGIN_STR: begin = True # Begin of the message if rawData: res = json.loads('\n'.join(rawData)) assert isinstance(res, dict) for i in res.keys(): assert isinstance(i, str) return res raise CMakeException('Failed to read data from the CMake server') def readMessage(self) -> MessageBase: raw_data = self.readMessageRaw() if 'type' not in raw_data: raise CMakeException('The "type" attribute is missing from the message') msg_type = raw_data['type'] func = self.type_map.get(msg_type, None) if not func: raise CMakeException('Recieved unknown message type "{}"'.format(msg_type)) for i in CMAKE_MESSAGE_TYPES[msg_type]: if i not in raw_data: raise CMakeException('Key "{}" is missing from CMake server message type {}'.format(i, msg_type)) return func(raw_data) def writeMessage(self, msg: MessageBase) -> None: raw_data = '\n{}\n{}\n{}\n'.format(CMAKE_SERVER_BEGIN_STR, json.dumps(msg.to_dict(), indent=2), CMAKE_SERVER_END_STR) self.proc.stdin.write(raw_data.encode('ascii')) self.proc.stdin.flush() def query(self, request: RequestBase) -> MessageBase: self.writeMessage(request) while True: reply = self.readMessage() if reply.cookie == request.cookie and reply.type in ['reply', 'error']: return reply reply.log() def query_checked(self, request: RequestBase, message: str) -> MessageBase: reply = self.query(request) h = mlog.green('SUCCEEDED') if reply.type == 'reply' else mlog.red('FAILED') mlog.log(message + ':', h) if reply.type != 'reply': reply.log() raise CMakeException('CMake server query failed') return reply def do_handshake(self, src_dir: Path, build_dir: Path, generator: str, vers_major: int, vers_minor: T.Optional[int] = None) -> None: # CMake prints the hello message on startup msg = self.readMessage() if not isinstance(msg, MessageHello): raise CMakeException('Recieved an unexpected message from the CMake server') request = RequestHandShake(src_dir, build_dir, generator, vers_major, vers_minor) self.query_checked(request, 'CMake server handshake') def resolve_type_reply(self, data: T.Dict[str, T.Any]) -> ReplyBase: reply_type = data['inReplyTo'] func = self.reply_map.get(reply_type, None) if not func: raise CMakeException('Recieved unknown reply type "{}"'.format(reply_type)) for i in ['cookie'] + CMAKE_REPLY_TYPES[reply_type]: if i not in data: raise CMakeException('Key "{}" is missing from CMake server message type {}'.format(i, type)) return func(data) def resolve_reply_cmakeInputs(self, data: T.Dict[str, T.Any]) -> ReplyCMakeInputs: files = [] for i in data['buildFiles']: for j in i['sources']: files += [CMakeBuildFile(Path(j), i['isCMake'], i['isTemporary'])] return ReplyCMakeInputs(data['cookie'], Path(data['cmakeRootDirectory']), Path(data['sourceDirectory']), files) @contextmanager def connect(self, cmake_exe: 'CMakeExecutor') -> T.Generator[None, None, None]: self.startup(cmake_exe) try: yield finally: self.shutdown() def startup(self, cmake_exe: 'CMakeExecutor') -> None: if self.proc is not None: raise CMakeException('The CMake server was already started') assert cmake_exe.found() mlog.debug('Starting CMake server with CMake', mlog.bold(' '.join(cmake_exe.get_command())), 'version', mlog.cyan(cmake_exe.version())) self.proc = Popen(cmake_exe.get_command() + ['-E', 'server', '--experimental', '--debug'], stdin=PIPE, stdout=PIPE) def shutdown(self) -> None: if self.proc is None: return mlog.debug('Shutting down the CMake server') # Close the pipes to exit self.proc.stdin.close() self.proc.stdout.close() # Wait for CMake to finish try: self.proc.wait(timeout=2) except TimeoutExpired: # Terminate CMake if there is a timeout # terminate() may throw a platform specific exception if the process has already # terminated. This may be the case if there is a race condition (CMake exited after # the timeout but before the terminate() call). Additionally, this behavior can # also be triggered on cygwin if CMake crashes. # See https://github.com/mesonbuild/meson/pull/4969#issuecomment-499413233 try: self.proc.terminate() except Exception: pass self.proc = None