• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1#!/usr/bin/env python3
2# coding=utf-8
3
4#
5# Copyright (c) 2022 Huawei Device Co., Ltd.
6# Licensed under the Apache License, Version 2.0 (the "License");
7# you may not use this file except in compliance with the License.
8# You may obtain a copy of the License at
9#
10#     http://www.apache.org/licenses/LICENSE-2.0
11#
12# Unless required by applicable law or agreed to in writing, software
13# distributed under the License is distributed on an "AS IS" BASIS,
14# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15# See the License for the specific language governing permissions and
16# limitations under the License.
17#
18
19import os
20import platform
21import socket
22import struct
23import threading
24import time
25import shutil
26import stat
27from dataclasses import dataclass
28
29from xdevice import DeviceOsType
30from xdevice import ReportException
31from xdevice import ExecuteTerminate
32from xdevice import platform_logger
33from xdevice import Plugin
34from xdevice import get_plugin
35from xdevice import IShellReceiver
36from xdevice import exec_cmd
37from xdevice import get_file_absolute_path
38from xdevice import FilePermission
39from xdevice import DeviceError
40from xdevice import HdcError
41from xdevice import HdcCommandRejectedException
42from xdevice import ShellCommandUnresponsiveException
43from xdevice import DeviceState
44from xdevice import convert_serial
45from xdevice import is_proc_running
46from xdevice import convert_ip
47from xdevice import create_dir
48
49ID_OKAY = b'OKAY'
50ID_FAIL = b'FAIL'
51ID_STAT = b'STAT'
52ID_RECV = b'RECV'
53ID_DATA = b'DATA'
54ID_DONE = b'DONE'
55ID_SEND = b'SEND'
56ID_LIST = b'LIST'
57ID_DENT = b'DENT'
58
59DEFAULT_ENCODING = "ISO-8859-1"
60SYNC_DATA_MAX = 64 * 1024
61REMOTE_PATH_MAX_LENGTH = 1024
62SOCK_DATA_MAX = 256
63
64INSTALL_TIMEOUT = 2 * 60 * 1000
65DEFAULT_TIMEOUT = 40 * 1000
66
67MAX_CONNECT_ATTEMPT_COUNT = 10
68DATA_UNIT_LENGTH = 4
69HEXADECIMAL_NUMBER = 16
70SPECIAL_FILE_MODE = 41471
71FORMAT_BYTES_LENGTH = 4
72DEFAULT_OFFSET_OF_INT = 4
73
74INVALID_MODE_CODE = -1
75DEFAULT_STD_PORT = 8710
76HDC_NAME = "hdc"
77HDC_STD_NAME = "hdc_std"
78LOG = platform_logger("Hdc")
79
80
81class HdcMonitor:
82    """
83    A Device monitor.
84    This monitor connects to the Device Connector, gets device and
85    debuggable process information from it.
86    """
87    MONITOR_MAP = {}
88
89    def __init__(self, host="127.0.0.1", port=None, device_connector=None):
90        self.channel = dict()
91        self.channel.setdefault("host", host)
92        self.channel.setdefault("port", port)
93        self.main_hdc_connection = None
94        self.connection_attempt = 0
95        self.is_stop = False
96        self.monitoring = False
97        self.server = device_connector
98        self.devices = []
99        self.last_msg_len = 0
100        self.changed = True
101
102    @staticmethod
103    def get_instance(host, port=None, device_connector=None):
104        if host not in HdcMonitor.MONITOR_MAP:
105            monitor = HdcMonitor(host, port, device_connector)
106            HdcMonitor.MONITOR_MAP[host] = monitor
107            LOG.debug("HdcMonitor map add host %s, map is %s" %
108                      (host, HdcMonitor.MONITOR_MAP))
109
110        return HdcMonitor.MONITOR_MAP[host]
111
112    def start(self):
113        """
114        Starts the monitoring.
115        """
116        try:
117            server_thread = threading.Thread(target=self.loop_monitor,
118                                             name="HdcMonitor", args=())
119            server_thread.setDaemon(True)
120            server_thread.start()
121        except FileNotFoundError as _:
122            LOG.error("HdcMonitor can't find connector, init device "
123                      "environment failed!")
124
125    def init_hdc(self, connector_name=HDC_NAME):
126        env_hdc = shutil.which(connector_name)
127        # if not, add xdevice's own hdc path to environ path.
128        # tell if hdc has already been in the environ path.
129        if env_hdc is None:
130            LOG.error("Can not find {} or {} environment variable, "
131                      "please set it first!".format(HDC_NAME, HDC_STD_NAME))
132        if not is_proc_running(connector_name):
133            port = DEFAULT_STD_PORT
134            self.start_hdc(
135                connector=connector_name,
136                local_port=self.channel.setdefault(
137                    "port", port))
138            time.sleep(1)
139
140    def _init_hdc_connection(self):
141        if self.main_hdc_connection is not None:
142            return
143        # set all devices disconnect
144        devices = [item for item in self.devices]
145        devices.reverse()
146        for local_device1 in devices:
147            local_device1.device_state = DeviceState.OFFLINE
148            self.server.device_changed(local_device1)
149
150        connector_name = HDC_STD_NAME if HdcHelper.is_hdc_std() else HDC_NAME
151        self.init_hdc(connector_name)
152        self.connection_attempt = 0
153        self.monitoring = False
154        while self.main_hdc_connection is None:
155            self.main_hdc_connection = self.open_hdc_connection()
156            if self.main_hdc_connection is None:
157                self.connection_attempt += 1
158                if self.connection_attempt > MAX_CONNECT_ATTEMPT_COUNT:
159                    self.is_stop = True
160                    LOG.error(
161                        "HdcMonitor attempt %s, can't connect to hdc "
162                        "for Device List Monitoring" %
163                        str(self.connection_attempt))
164                    raise HdcError(
165                        "HdcMonitor cannot connect hdc server(%s %s),"
166                        " please check!" %
167                        (self.channel.get("host"),
168                         str(self.channel.get("post"))))
169
170                LOG.debug(
171                    "HdcMonitor Connection attempts: %s" %
172                    str(self.connection_attempt))
173                time.sleep(2)
174
175    def stop(self):
176        """
177        Stops the monitoring.
178        """
179        for host in HdcMonitor.MONITOR_MAP:
180            LOG.debug("HdcMonitor stop host %s" % host)
181            monitor = HdcMonitor.MONITOR_MAP[host]
182            try:
183                monitor.is_stop = True
184                if monitor.main_hdc_connection is not None:
185                    monitor.main_hdc_connection.shutdown(2)
186                    monitor.main_hdc_connection.close()
187                    monitor.main_hdc_connection = None
188            except (socket.error, socket.gaierror, socket.timeout) as _:
189                LOG.error("HdcMonitor close socket exception")
190        HdcMonitor.MONITOR_MAP.clear()
191        LOG.debug("HdcMonitor {} monitor stop!".format(HdcHelper.CONNECTOR_NAME))
192        LOG.debug("HdcMonitor map is %s" % HdcMonitor.MONITOR_MAP)
193
194    def loop_monitor(self):
195        """
196        Monitors the devices. This connects to the Debug Bridge
197        """
198        LOG.debug("current connector name is %s" % HdcHelper.CONNECTOR_NAME)
199        while not self.is_stop:
200            try:
201                if self.main_hdc_connection is None:
202                    self._init_hdc_connection()
203                    if self.main_hdc_connection is not None:
204                        LOG.debug(
205                            "HdcMonitor Connected to hdc for device "
206                            "monitoring, main_hdc_connection is %s" %
207                            self.main_hdc_connection)
208
209                self.list_targets()
210                time.sleep(1)
211            except (HdcError, Exception) as _:
212                self.handle_exception_monitor_loop()
213                time.sleep(2)
214
215    def handle_exception_monitor_loop(self):
216        LOG.debug("Handle exception monitor loop: %s" %
217                  self.main_hdc_connection)
218        if self.main_hdc_connection is None:
219            return
220        LOG.debug("Handle exception monitor loop, main hdc connection closed, "
221                  "main hdc connection: %s" % self.main_hdc_connection)
222        self.main_hdc_connection.close()
223        self.main_hdc_connection = None
224
225    def _get_device_instance(self, items, os_type):
226        device = get_plugin(plugin_type=Plugin.DEVICE, plugin_id=os_type)[0]
227        device_instance = device.__class__()
228        device_instance.__set_serial__(items[0])
229        device_instance.host = self.channel.get("host")
230        device_instance.port = self.channel.get("port")
231        if self.changed:
232            LOG.debug("Dmlib get device instance %s %s %s" %
233                      (device_instance.device_sn,
234                       device_instance.host, device_instance.port))
235        device_instance.device_state = DeviceState.get_state(items[3])
236        return device_instance
237
238    def update_devices(self, param_array_list):
239        devices = [item for item in self.devices]
240        devices.reverse()
241        for local_device1 in devices:
242            k = 0
243            for local_device2 in param_array_list:
244                if local_device1.device_sn == local_device2.device_sn and \
245                        local_device1.device_os_type == \
246                        local_device2.device_os_type:
247                    k = 1
248                    if local_device1.device_state != \
249                            local_device2.device_state:
250                        local_device1.device_state = local_device2.device_state
251                        self.server.device_changed(local_device1)
252                    param_array_list.remove(local_device2)
253                    break
254
255            if k == 0:
256                self.devices.remove(local_device1)
257                self.server.device_disconnected(local_device1)
258        for local_device in param_array_list:
259            self.devices.append(local_device)
260            self.server.device_connected(local_device)
261
262    def open_hdc_connection(self):
263        """
264        Attempts to connect to the debug bridge server. Return a connect socket
265        if success, null otherwise.
266        """
267        sock = None
268        try:
269            LOG.debug(
270                "HdcMonitor connecting to hdc for Device List Monitoring")
271            LOG.debug("HdcMonitor socket connection host: %s, port: %s" %
272                      (str(convert_ip(self.channel.get("host"))),
273                       str(int(self.channel.get("port")))))
274
275            sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
276            sock.connect((self.channel.get("host"),
277                          int(self.channel.get("port"))))
278            return sock
279
280        except (socket.error, socket.gaierror, socket.timeout) as exception:
281            LOG.error("HdcMonitor hdc socket connection Error: %s, "
282                      "host is %s, port is %s" % (str(exception),
283                                                  self.channel.get("host"),
284                                                  self.channel.get("port")))
285            return None
286
287    def start_hdc(self, connector=HDC_NAME, kill=False, local_port=None):
288        """Starts the hdc host side server.
289
290        Args:
291            connector: connector type, like "hdc"
292            kill: if True, kill exist host side server
293            local_port: local port to start host side server
294
295        Returns:
296            None
297        """
298        if kill:
299            LOG.debug("HdcMonitor {} kill".format(connector))
300            exec_cmd([connector, "kill"])
301        LOG.debug("HdcMonitor {} start".format(connector))
302        exec_cmd(
303            [connector, "-l5", "start"],
304            error_print=False, redirect=True)
305
306    def list_targets(self):
307        if self.main_hdc_connection:
308            self.server.monitor_lock.acquire(timeout=1)
309            try:
310                self.monitoring_list_targets()
311                len_buf = HdcHelper.read(self.main_hdc_connection,
312                                         DATA_UNIT_LENGTH)
313                length = struct.unpack("!I", len_buf)[0]
314                if length >= 0:
315                    if self.last_msg_len != length:
316                        LOG.debug("had received length is: %s" % length)
317                        self.last_msg_len = length
318                        self.changed = True
319                    else:
320                        self.changed = False
321                    self.connection_attempt = 0
322                    self.process_incoming_target_data(length)
323            except Exception as e:
324                LOG.error(e)
325                raise e
326            finally:
327                self.server.monitor_lock.release()
328
329    def monitoring_list_targets(self):
330        if not self.monitoring:
331            HdcHelper.handle_shake(self.main_hdc_connection)
332            request = HdcHelper.form_hdc_request("alive")
333            HdcHelper.write(self.main_hdc_connection, request)
334            self.monitoring = True
335        request = HdcHelper.form_hdc_request('list targets -v')
336        HdcHelper.write(self.main_hdc_connection, request)
337
338    def process_incoming_target_data(self, length):
339        local_array_list = []
340        data_buf = HdcHelper.read(self.main_hdc_connection, length)
341        data_str = HdcHelper.reply_to_string(data_buf)
342        if 'Empty' not in data_str:
343            lines = data_str.split('\n')
344            for line in lines:
345                items = line.strip().split('\t')
346                # Example: sn    USB     Offline localhost       hdc
347                if not items[0] or len(items) < 5:
348                    continue
349                device_instance = self._get_device_instance(
350                    items, DeviceOsType.default)
351                local_array_list.append(device_instance)
352        else:
353            if self.changed:
354                LOG.debug("please check device actually.[%s]" % data_str.strip())
355        self.update_devices(local_array_list)
356
357    @staticmethod
358    def peek_hdc():
359        LOG.debug("Peek running process to check expect connector.")
360        # if not find hdc_std, try find hdc
361        connector_name = HDC_STD_NAME
362        env_hdc = shutil.which(connector_name)
363        # if not, add xdevice's own hdc path to environ path.
364        # tell if hdc has already been in the environ path.
365        if env_hdc is None:
366            connector_name = HDC_NAME
367        LOG.debug("Peak end")
368        return connector_name
369
370
371@dataclass
372class HdcResponse:
373    """Response from HDC."""
374    okay = ID_OKAY  # first 4 bytes in response were "OKAY"?
375    message = ""  # diagnostic string if okay is false
376
377
378class SyncService:
379    """
380    Sync service class to push/pull to/from devices/emulators,
381    through the debug bridge.
382    """
383
384    def __init__(self, device, host=None, port=None):
385        self.device = device
386        self.host = host
387        self.port = port
388        self.sock = None
389
390    def open_sync(self, timeout=DEFAULT_TIMEOUT):
391        """
392        Opens the sync connection. This must be called before any calls to
393        push[File] / pull[File].
394        Return true if the connection opened, false if hdc refuse the
395        connection. This can happen device is invalid.
396        """
397        LOG.debug("Open sync, timeout=%s" % int(timeout / 1000))
398        self.sock = HdcHelper.socket(host=self.host, port=self.port,
399                                     timeout=timeout)
400        HdcHelper.set_device(self.device, self.sock)
401
402        request = HdcHelper.form_hdc_request("sync:")
403        HdcHelper.write(self.sock, request)
404
405        resp = HdcHelper.read_hdc_response(self.sock)
406        if not resp.okay:
407            self.device.log.error(
408                "Got unhappy response from HDC sync req: %s" % resp.message)
409            raise HdcError(
410                "Got unhappy response from HDC sync req: %s" % resp.message)
411
412    def close(self):
413        """
414        Closes the connection.
415        """
416        if self.sock is not None:
417            try:
418                self.sock.close()
419            except socket.error as error:
420                LOG.error("Socket close error: %s" % error, error_no="00420")
421            finally:
422                self.sock = None
423
424    def pull_file(self, remote, local, is_create=False):
425        """
426        Pulls a file.
427        The top directory won't be created if is_create is False (by default)
428        and vice versa
429        """
430        mode = self.read_mode(remote)
431        self.device.log.debug("Remote file %s mode is %d" % (remote, mode))
432        if mode == 0:
433            raise HdcError("Remote object doesn't exist!")
434
435        if str(mode).startswith("168"):
436            if is_create:
437                remote_file_split = os.path.split(remote)[-1] \
438                    if os.path.split(remote)[-1] else os.path.split(remote)[-2]
439                remote_file_basename = os.path.basename(remote_file_split)
440                new_local = os.path.join(local, remote_file_basename)
441                create_dir(new_local)
442            else:
443                new_local = local
444
445            collect_receiver = CollectingOutputReceiver()
446            HdcHelper.execute_shell_command(self.device, "ls %s" % remote,
447                                            receiver=collect_receiver)
448            files = collect_receiver.output.split()
449            for file_name in files:
450                self.pull_file("%s/%s" % (remote, file_name),
451                               new_local, is_create=True)
452        elif mode == SPECIAL_FILE_MODE:
453            self.device.log.info("skipping special file '%s'" % remote)
454        else:
455            if os.path.isdir(local):
456                local = os.path.join(local, os.path.basename(remote))
457
458            self.do_pull_file(remote, local)
459
460    def do_pull_file(self, remote, local):
461        """
462        Pulls a remote file
463        """
464        self.device.log.info(
465            "%s pull %s to %s" % (convert_serial(self.device.device_sn),
466                                  remote, local))
467        remote_path_content = remote.encode(DEFAULT_ENCODING)
468        if len(remote_path_content) > REMOTE_PATH_MAX_LENGTH:
469            raise HdcError("Remote path is too long.")
470
471        msg = self.create_file_req(ID_RECV, remote_path_content)
472        HdcHelper.write(self.sock, msg)
473        pull_result = HdcHelper.read(self.sock, DATA_UNIT_LENGTH * 2)
474        if not self.check_result(pull_result, ID_DATA) and \
475                not self.check_result(pull_result, ID_DONE):
476            raise HdcError(self.read_error_message(pull_result))
477        if platform.system() == "Windows":
478            flags = os.O_WRONLY | os.O_CREAT | os.O_APPEND | os.O_BINARY
479        else:
480            flags = os.O_WRONLY | os.O_CREAT | os.O_APPEND
481        pulled_file_open = os.open(local, flags, FilePermission.mode_755)
482        with os.fdopen(pulled_file_open, "wb") as pulled_file:
483            while True:
484                if self.check_result(pull_result, ID_DONE):
485                    break
486
487                if not self.check_result(pull_result, ID_DATA):
488                    raise HdcError(self.read_error_message(pull_result))
489
490                try:
491                    length = self.swap32bit_from_array(
492                        pull_result, DEFAULT_OFFSET_OF_INT)
493                except IndexError as index_error:
494                    self.device.log.debug("do_pull_file: %s" %
495                                          str(pull_result))
496                    if pull_result == ID_DATA:
497                        pull_result = self.sock.recv(DATA_UNIT_LENGTH)
498                        self.device.log.debug(
499                            "do_pull_file: %s" % str(pull_result))
500                        length = self.swap32bit_from_array(pull_result, 0)
501                        self.device.log.debug("do_pull_file: %s" % str(length))
502                    else:
503                        raise IndexError(str(index_error)) from index_error
504
505                if length > SYNC_DATA_MAX:
506                    raise HdcError("Receiving too much data.")
507
508                pulled_file.write(HdcHelper.read(self.sock, length))
509                pulled_file.flush()
510                pull_result = self.sock.recv(DATA_UNIT_LENGTH * 2)
511
512    def push_file(self, local, remote, is_create=False):
513        """
514        Push a single file.
515        The top directory won't be created if is_create is False (by default)
516        and vice versa
517        """
518        if not os.path.exists(local):
519            raise HdcError("Local path doesn't exist.")
520
521        if os.path.isdir(local):
522            if is_create:
523                local_file_split = os.path.split(local)[-1] \
524                    if os.path.split(local)[-1] else os.path.split(local)[-2]
525                local_file_basename = os.path.basename(local_file_split)
526                remote = "{}/{}".format(
527                    remote, local_file_basename)
528                HdcHelper.execute_shell_command(
529                    self.device, "mkdir -p %s" % remote)
530
531            for child in os.listdir(local):
532                file_path = os.path.join(local, child)
533                if os.path.isdir(file_path):
534                    self.push_file(
535                        file_path,  "%s/%s" % (remote, child),
536                        is_create=False)
537                else:
538                    self.do_push_file(file_path, "%s/%s" % (remote, child))
539        else:
540            self.do_push_file(local, remote)
541
542    def do_push_file(self, local, remote):
543        """
544        Push a single file
545
546        Args:
547        ------------
548        local : string
549            the local file to push
550        remote : string
551            the remote file (length max is 1024)
552        """
553        mode = self.read_mode(remote)
554        self.device.log.debug("Remote file %s mode is %d" % (remote, mode))
555        self.device.log.debug("%s execute command: hdc push %s %s" % (
556            convert_serial(self.device.device_sn), local, remote))
557        if str(mode).startswith("168"):
558            remote = "%s/%s" % (remote, os.path.basename(local))
559
560        try:
561            try:
562                remote_path_content = remote.encode(DEFAULT_ENCODING)
563            except UnicodeEncodeError as _:
564                remote_path_content = remote.encode("UTF-8")
565            if len(remote_path_content) > REMOTE_PATH_MAX_LENGTH:
566                raise HdcError("Remote path is too long.")
567
568            # create the header for the action
569            # and send it. We use a custom try/catch block to make the
570            # difference between file and network IO exceptions.
571            msg = self.create_send_file_req(ID_SEND, remote_path_content,
572                                            FilePermission.mode_644)
573
574            HdcHelper.write(self.sock, msg)
575            flags = os.O_RDONLY
576            modes = stat.S_IWUSR | stat.S_IRUSR
577            with os.fdopen(os.open(local, flags, modes), "rb") as test_file:
578                while True:
579                    if platform.system() == "Linux":
580                        data = test_file.read(1024 * 4)
581                    else:
582                        data = test_file.read(SYNC_DATA_MAX)
583
584                    if not data:
585                        break
586
587                    buf = struct.pack(
588                        "%ds%ds%ds" % (len(ID_DATA), FORMAT_BYTES_LENGTH,
589                                       len(data)), ID_DATA,
590                        self.swap32bits_to_bytes(len(data)), data)
591                    self.sock.send(buf)
592        except Exception as exception:
593            self.device.log.error("exception %s" % exception)
594            raise exception
595
596        msg = self.create_req(ID_DONE, int(time.time()))
597        HdcHelper.write(self.sock, msg)
598        result = HdcHelper.read(self.sock, DATA_UNIT_LENGTH * 2)
599        if not self.check_result(result, ID_OKAY):
600            self.device.log.error("exception %s" % result)
601            raise HdcError(self.read_error_message(result))
602
603    def read_mode(self, path):
604        """
605        Returns the mode of the remote file.
606        Return an Integer containing the mode if all went well or null
607        """
608        msg = self.create_file_req(ID_STAT, path)
609        HdcHelper.write(self.sock, msg)
610
611        # read the result, in a byte array containing 4 ints
612        stat_result = HdcHelper.read(self.sock, DATA_UNIT_LENGTH * 4)
613        if not self.check_result(stat_result, ID_STAT):
614            return INVALID_MODE_CODE
615
616        return self.swap32bit_from_array(stat_result, DEFAULT_OFFSET_OF_INT)
617
618    def create_file_req(self, command, path):
619        """
620        Creates the data array for a file request. This creates an array with a
621        4 byte command + the remote file name.
622
623        Args:
624        ------------
625        command :
626            the 4 byte command (ID_STAT, ID_RECV, ...)
627        path : string
628            The path, as a byte array, of the remote file on which to execute
629            the command.
630
631        return:
632        ------------
633            return the byte[] to send to the device through hdc
634        """
635        if isinstance(path, str):
636            try:
637                path = path.encode(DEFAULT_ENCODING)
638            except UnicodeEncodeError as _:
639                path = path.encode("UTF-8")
640
641        return struct.pack(
642            "%ds%ds%ds" % (len(command), FORMAT_BYTES_LENGTH, len(path)),
643            command, self.swap32bits_to_bytes(len(path)), path)
644
645    def create_send_file_req(self, command, path, mode=0o644):
646        # make the mode into a string
647        mode_str = ",%s" % str(mode & FilePermission.mode_777)
648        mode_content = mode_str.encode(DEFAULT_ENCODING)
649        return struct.pack(
650            "%ds%ds%ds%ds" % (len(command), FORMAT_BYTES_LENGTH, len(path),
651                              len(mode_content)),
652            command, self.swap32bits_to_bytes(len(path) + len(mode_content)),
653            path, mode_content)
654
655    def create_req(self, command, value):
656        """
657        Create a command with a code and an int values
658        """
659        return struct.pack("%ds%ds" % (len(command), FORMAT_BYTES_LENGTH),
660                           command, self.swap32bits_to_bytes(value))
661
662    @staticmethod
663    def check_result(result, code):
664        """
665        Checks the result array starts with the provided code
666
667        Args:
668        ------------
669        result :
670            the result array to check
671        path : string
672            the 4 byte code
673
674        return:
675        ------------
676        bool
677            return true if the code matches
678        """
679        return result[0:4] == code[0:4]
680
681    def read_error_message(self, result):
682        """
683        Reads an error message from the opened Socket.
684
685        Args:
686        ------------
687        result :
688            the current hdc result. Must contain both FAIL and the length of
689            the message.
690        """
691        if self.check_result(result, ID_FAIL):
692            length = self.swap32bit_from_array(result, 4)
693            if length > 0:
694                return str(HdcHelper.read(self.sock, length))
695
696        return None
697
698    @staticmethod
699    def swap32bits_to_bytes(value):
700        """
701        Swaps an unsigned value around, and puts the result in an bytes that
702        can be sent to a device.
703
704        Args:
705        ------------
706        value :
707            the value to swap.
708        """
709        return bytes([value & 0x000000FF,
710                      (value & 0x0000FF00) >> 8,
711                      (value & 0x00FF0000) >> 16,
712                      (value & 0xFF000000) >> 24])
713
714    @staticmethod
715    def swap32bit_from_array(value, offset):
716        """
717        Reads a signed 32 bit integer from an array coming from a device.
718
719        Args:
720        ------------
721        value :
722            the array containing the int
723        offset:
724            the offset in the array at which the int starts
725
726        Return:
727        ------------
728        int
729            the integer read from the array
730        """
731        result = 0
732        result |= (int(value[offset])) & 0x000000FF
733        result |= (int(value[offset + 1]) & 0x000000FF) << 8
734        result |= (int(value[offset + 2]) & 0x000000FF) << 16
735        result |= (int(value[offset + 3]) & 0x000000FF) << 24
736
737        return result
738
739
740class HdcHelper:
741    CONNECTOR_NAME = ""
742
743    @staticmethod
744    def check_if_hdc_running(timeout=30):
745        LOG.debug("Check if {} is running, timeout is {}s".format(
746            HdcHelper.CONNECTOR_NAME, timeout))
747        index = 1
748        while index < timeout:
749            if is_proc_running(HdcHelper.CONNECTOR_NAME):
750                return True
751            index = index + 1
752            time.sleep(1)
753        return False
754
755    @staticmethod
756    def push_file(device, local, remote, is_create=False,
757                  timeout=DEFAULT_TIMEOUT):
758        device.log.info("{} execute command: {} file send {} to {}".format(
759            convert_serial(device.device_sn), HdcHelper.CONNECTOR_NAME, local, remote))
760        HdcHelper._operator_file("file send", device, local, remote, timeout)
761
762    @staticmethod
763    def pull_file(device, remote, local, is_create=False,
764                  timeout=DEFAULT_TIMEOUT):
765        device.log.info("{} execute command: {} file recv {} to {}".format(
766            convert_serial(device.device_sn), HdcHelper.CONNECTOR_NAME, remote, local))
767        HdcHelper._operator_file("file recv", device, remote, local, timeout)
768
769    @staticmethod
770    def _install_remote_package(device, remote_file_path, command):
771        receiver = CollectingOutputReceiver()
772        cmd = "bm install -p %s %s" % (command.strip(), remote_file_path)
773        HdcHelper.execute_shell_command(device, cmd, INSTALL_TIMEOUT, receiver)
774        return receiver.output
775
776    @staticmethod
777    def install_package(device, package_file_path, command):
778        device.log.info("%s install %s" % (convert_serial(device.device_sn),
779                                           package_file_path))
780        remote_file_path = "/data/local/tmp/%s" % os.path.basename(
781            package_file_path)
782
783        HdcHelper.push_file(device, package_file_path, remote_file_path)
784
785        result = HdcHelper._install_remote_package(device, remote_file_path,
786                                                   command)
787        HdcHelper.execute_shell_command(device, "rm %s " % remote_file_path)
788        return result
789
790    @staticmethod
791    def uninstall_package(device, package_name):
792        receiver = CollectingOutputReceiver()
793        command = "bm uninstall -n %s " % package_name
794        device.log.info("%s %s" % (convert_serial(device.device_sn), command))
795        HdcHelper.execute_shell_command(device, command, INSTALL_TIMEOUT,
796                                        receiver)
797        return receiver.output
798
799    @staticmethod
800    def reboot(device, into=None):
801        device.log.info("{} execute command: {} target boot".format(convert_serial(device.device_sn),
802                                                                    HdcHelper.CONNECTOR_NAME))
803        with HdcHelper.socket(host=device.host, port=device.port) as sock:
804            HdcHelper.handle_shake(sock, device.device_sn)
805            request = HdcHelper.form_hdc_request("target boot")
806            HdcHelper.write(sock, request)
807
808    @staticmethod
809    def execute_shell_command(device, command, timeout=DEFAULT_TIMEOUT,
810                              receiver=None, **kwargs):
811        """
812        Executes a shell command on the device and retrieve the output.
813
814        Args:
815        ------------
816        device : IDevice
817            on which to execute the command.
818        command : string
819            the shell command to execute
820        timeout : int
821            max time between command output. If more time passes between
822            command output, the method will throw
823            ShellCommandUnresponsiveException (ms).
824        """
825        try:
826            if not timeout:
827                timeout = DEFAULT_TIMEOUT
828
829            with HdcHelper.socket(host=device.host, port=device.port,
830                                  timeout=timeout) as sock:
831                output_flag = kwargs.get("output_flag", True)
832                timeout_msg = " with timeout %ss" % str(timeout / 1000)
833                message = "{} execute command: {} shell {}{}".format(convert_serial(device.device_sn),
834                                                                     HdcHelper.CONNECTOR_NAME,
835                                                                     command, timeout_msg)
836                if output_flag:
837                    LOG.info(message)
838                else:
839                    LOG.debug(message)
840                from xdevice import Scheduler
841                HdcHelper.handle_shake(sock, device.device_sn)
842                request = HdcHelper.form_hdc_request("shell {}".format(command))
843                HdcHelper.write(sock, request)
844                resp = HdcResponse()
845                resp.okay = True
846                while True:
847                    len_buf = sock.recv(DATA_UNIT_LENGTH)
848                    if len_buf:
849                        length = struct.unpack("!I", len_buf)[0]
850                    else:
851                        break
852                    data = sock.recv(length)
853                    ret = HdcHelper.reply_to_string(data)
854                    if ret:
855                        if receiver:
856                            receiver.__read__(ret)
857                        else:
858                            LOG.debug(ret)
859                    if not Scheduler.is_execute:
860                        raise ExecuteTerminate()
861                return resp
862        except socket.timeout as error:
863            device.log.error("ShellCommandUnresponsiveException: {} shell {} timeout[{}S]".format(
864                convert_serial(device.device_sn), command, str(timeout / 1000)))
865            raise ShellCommandUnresponsiveException() from error
866        finally:
867            if receiver:
868                receiver.__done__()
869
870    @staticmethod
871    def set_device(device, sock):
872        """
873        Tells hdc to talk to a specific device
874        if the device is not -1, then we first tell hdc we're looking to talk
875        to a specific device
876        """
877        msg = "host:transport:%s" % device.device_sn
878        device_query = HdcHelper.form_hdc_request(msg)
879        HdcHelper.write(sock, device_query)
880        resp = HdcHelper.read_hdc_response(sock)
881        if not resp.okay:
882            raise HdcCommandRejectedException(resp.message)
883
884    @staticmethod
885    def form_hdc_request(req):
886        """
887        Create an ASCII string preceded by four hex digits.
888        """
889        try:
890            if not req.endswith('\0'):
891                req = "%s\0" % req
892            req = req.encode("utf-8")
893            fmt = "!I%ss" % len(req)
894            result = struct.pack(fmt, len(req), req)
895        except UnicodeEncodeError as ex:
896            LOG.error(ex)
897            raise ex
898        return result
899
900    @staticmethod
901    def read_hdc_response(sock, read_diag_string=False):
902        """
903        Reads the response from HDC after a command.
904
905        Args:
906        ------------
907        read_diag_string :
908            If true, we're expecting an OKAY response to be followed by a
909            diagnostic string. Otherwise, we only expect the diagnostic string
910            to follow a FAIL.
911        """
912        resp = HdcResponse()
913        reply = HdcHelper.read(sock, DATA_UNIT_LENGTH)
914        if HdcHelper.is_okay(reply):
915            resp.okay = True
916        else:
917            read_diag_string = True
918            resp.okay = False
919
920        while read_diag_string:
921            len_buf = HdcHelper.read(sock, DATA_UNIT_LENGTH)
922            len_str = HdcHelper.reply_to_string(len_buf)
923            msg = HdcHelper.read(sock, int(len_str, HEXADECIMAL_NUMBER))
924            resp.message = HdcHelper.reply_to_string(msg)
925            break
926
927        return resp
928
929    @staticmethod
930    def write(sock, req, timeout=10):
931        if isinstance(req, str):
932            req = req.encode(DEFAULT_ENCODING)
933        elif isinstance(req, list):
934            req = bytes(req)
935
936        start_time = time.time()
937        while req:
938            if time.time() - start_time > timeout:
939                LOG.debug("Socket write timeout, timeout:%ss" % timeout)
940                break
941
942            size = sock.send(req)
943            if size < 0:
944                raise DeviceError("channel EOF")
945
946            req = req[size:]
947            time.sleep(5 / 1000)
948
949    @staticmethod
950    def read(sock, length, timeout=10):
951        data = b''
952        recv_len = 0
953        start_time = time.time()
954        exc_num = 3
955        while length - recv_len > 0:
956            if time.time() - start_time > timeout:
957                LOG.debug("Socket read timeout, timout:%ss" % timeout)
958                break
959            try:
960                recv = sock.recv(length - recv_len)
961                if len(recv) > 0:
962                    time.sleep(5 / 1000)
963                else:
964                    break
965            except ConnectionResetError as error:
966                if exc_num <= 0:
967                    raise error
968                exc_num = exc_num - 1
969                recv = b''
970                time.sleep(1)
971                LOG.debug("ConnectionResetError occurs")
972
973            data += recv
974            recv_len += len(recv)
975
976        return data
977
978    @staticmethod
979    def is_okay(reply):
980        """
981        Checks to see if the first four bytes in "reply" are OKAY.
982        """
983        return reply[0:4] == ID_OKAY
984
985    @staticmethod
986    def reply_to_string(reply):
987        """
988        Converts an HDC reply to a string.
989        """
990        try:
991            return str(reply, encoding=DEFAULT_ENCODING)
992        except (ValueError, TypeError) as _:
993            return ""
994
995    @staticmethod
996    def socket(host=None, port=None, timeout=None):
997        end = time.time() + 10 * 60
998        sock = None
999        hdc_connection = HdcMonitor.MONITOR_MAP.get(host, "127.0.0.1")
1000        while host not in HdcMonitor.MONITOR_MAP or \
1001                hdc_connection.main_hdc_connection is None:
1002            LOG.debug("Host: %s, port: %s, HdcMonitor map is %s" % (
1003                host, port, HdcMonitor.MONITOR_MAP))
1004            if host in HdcMonitor.MONITOR_MAP:
1005                LOG.debug("Monitor main hdc connection is %s" %
1006                          hdc_connection.main_hdc_connection)
1007            if time.time() > end:
1008                raise HdcError("Cannot detect HDC monitor!")
1009            time.sleep(2)
1010
1011        try:
1012            sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
1013            sock.connect((host, int(port)))
1014        except socket.error as exception:
1015            LOG.exception("Connect hdc server error: %s" % str(exception),
1016                          exc_info=False)
1017            raise exception
1018
1019        if sock is None:
1020            raise HdcError("Cannot connect hdc server!")
1021
1022        if timeout is not None:
1023            sock.setblocking(False)
1024            sock.settimeout(timeout / 1000)
1025
1026        return sock
1027
1028    @staticmethod
1029    def handle_shake(connection, connect_key=""):
1030        reply = HdcHelper.read(connection, 48)
1031        struct.unpack(">I12s32s", reply)
1032        banner_str = b'OHOS HDC'
1033        connect_key = connect_key.encode("utf-8")
1034        size = struct.calcsize('12s256s')
1035        fmt = "!I12s256s"
1036        pack_cmd = struct.pack(fmt, size, banner_str, connect_key)
1037        HdcHelper.write(connection, pack_cmd)
1038        return True
1039
1040    @staticmethod
1041    def _operator_file(command, device, local, remote, timeout):
1042        sock = HdcHelper.socket(host=device.host, port=device.port,
1043                                timeout=timeout)
1044        HdcHelper.handle_shake(sock, device.device_sn)
1045        request = HdcHelper.form_hdc_request(
1046            "%s %s %s" % (command, local, remote))
1047        HdcHelper.write(sock, request)
1048        reply = HdcHelper.read(sock, DATA_UNIT_LENGTH)
1049        length = struct.unpack("!I", reply)[0]
1050        data_buf = HdcHelper.read(sock, length)
1051        HdcHelper.reply_to_string(data_buf)
1052        LOG.debug("result %s" % data_buf)
1053
1054    @staticmethod
1055    def is_hdc_std():
1056        return HDC_STD_NAME in HdcHelper.CONNECTOR_NAME
1057
1058
1059class DeviceConnector(object):
1060    __instance = None
1061    __init_flag = False
1062
1063    def __init__(self, host=None, port=None, usb_type=None):
1064        if DeviceConnector.__init_flag:
1065            return
1066        self.device_listeners = []
1067        self.device_monitor = None
1068        self.monitor_lock = threading.Condition()
1069        self.host = host if host else "127.0.0.1"
1070        self.usb_type = usb_type
1071        connector_name = HdcMonitor.peek_hdc()
1072        HdcHelper.CONNECTOR_NAME = connector_name
1073        if port:
1074            self.port = int(port)
1075        else:
1076            self.port = int(os.getenv("OHOS_HDC_SERVER_PORT", DEFAULT_STD_PORT))
1077
1078    def start(self):
1079        self.device_monitor = HdcMonitor.get_instance(
1080            self.host, self.port, device_connector=self)
1081        self.device_monitor.start()
1082
1083    def terminate(self):
1084        if self.device_monitor:
1085            self.device_monitor.stop()
1086        self.device_monitor = None
1087
1088    def add_device_change_listener(self, device_change_listener):
1089        self.device_listeners.append(device_change_listener)
1090
1091    def remove_device_change_listener(self, device_change_listener):
1092        if device_change_listener in self.device_listeners:
1093            self.device_listeners.remove(device_change_listener)
1094
1095    def device_connected(self, device):
1096        LOG.debug("DeviceConnector device connected:host %s, port %s, "
1097                  "device sn %s " % (self.host, self.port, device.device_sn))
1098        if device.host != self.host or device.port != self.port:
1099            LOG.debug("DeviceConnector device error")
1100        for listener in self.device_listeners:
1101            listener.device_connected(device)
1102
1103    def device_disconnected(self, device):
1104        LOG.debug("DeviceConnector device disconnected:host %s, port %s, "
1105                  "device sn %s" % (self.host, self.port, device.device_sn))
1106        if device.host != self.host or device.port != self.port:
1107            LOG.debug("DeviceConnector device error")
1108        for listener in self.device_listeners:
1109            listener.device_disconnected(device)
1110
1111    def device_changed(self, device):
1112        LOG.debug("DeviceConnector device changed:host %s, port %s, "
1113                  "device sn %s" % (self.host, self.port, device.device_sn))
1114        if device.host != self.host or device.port != self.port:
1115            LOG.debug("DeviceConnector device error")
1116        for listener in self.device_listeners:
1117            listener.device_changed(device)
1118
1119
1120class CollectingOutputReceiver(IShellReceiver):
1121    def __init__(self):
1122        self.output = ""
1123
1124    def __read__(self, output):
1125        self.output = "%s%s" % (self.output, output)
1126
1127    def __error__(self, message):
1128        pass
1129
1130    def __done__(self, result_code="", message=""):
1131        pass
1132
1133
1134class DisplayOutputReceiver(IShellReceiver):
1135    def __init__(self):
1136        self.output = ""
1137        self.unfinished_line = ""
1138
1139    def _process_output(self, output, end_mark="\n"):
1140        content = output
1141        if self.unfinished_line:
1142            content = "".join((self.unfinished_line, content))
1143            self.unfinished_line = ""
1144        lines = content.split(end_mark)
1145        if content.endswith(end_mark):
1146            # get rid of the tail element of this list contains empty str
1147            return lines[:-1]
1148        else:
1149            self.unfinished_line = lines[-1]
1150            # not return the tail element of this list contains unfinished str,
1151            # so we set position -1
1152            return lines[:-1]
1153
1154    def __read__(self, output):
1155        self.output = "%s%s" % (self.output, output)
1156        lines = self._process_output(output)
1157        for line in lines:
1158            line = line.strip()
1159            if line:
1160                LOG.info(line)
1161
1162    def __error__(self, message):
1163        pass
1164
1165    def __done__(self, result_code="", message=""):
1166        pass
1167
1168
1169def process_command_ret(ret, receiver):
1170    try:
1171        if ret != "" and receiver:
1172            receiver.__read__(ret)
1173            receiver.__done__()
1174    except Exception as error:
1175        LOG.exception("Error generating log report.", exc_info=False)
1176        raise ReportException() from error
1177
1178    if ret != "" and not receiver:
1179        lines = ret.split("\n")
1180        for line in lines:
1181            line = line.strip()
1182            if line:
1183                LOG.debug(line)
1184