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