• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

INTI-CMNB / KiAuto / 6981974383

24 Nov 2023 02:31PM UTC coverage: 88.592%. First build
6981974383

push

github

set-soft
Forced test

3021 of 3410 relevant lines covered (88.59%)

3.77 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

82.89
/kiauto/interposer.py
1
# -*- coding: utf-8 -*-
2
# Copyright (c) 2022-2023 Salvador E. Tropea
3
# Copyright (c) 2022-2023 Instituto Nacional de Tecnologïa Industrial
4
# License: Apache 2.0
5
# Project: KiAuto (formerly kicad-automation-scripts)
6
import atexit
6✔
7
import os
6✔
8
import platform
6✔
9
import psutil
6✔
10
from queue import Queue, Empty
6✔
11
import re
6✔
12
import shutil
6✔
13
from sys import exit
6✔
14
from tempfile import mkdtemp
6✔
15
from threading import Thread
6✔
16
import time
6✔
17
from kiauto.misc import KICAD_DIED, CORRUPTED_PCB, PCBNEW_ERROR, EESCHEMA_ERROR
6✔
18
from kiauto import log
6✔
19
from kiauto.ui_automation import xdotool, wait_for_window, wait_point, text_replace
6✔
20
from kiauto.file_util import wait_for_file_created_by_process
6✔
21

22
KICAD_EXIT_MSG = '>>exit<<'
6✔
23
INTERPOSER_OPS = 'interposer_options.txt'
6✔
24
IGNORED_DIALOG_MSGS = {'The quick brown fox jumps over the lazy dog.', '0123456789'}
6✔
25
BOGUS_FILENAME = '#'
6✔
26
KIKIT_HIDE = 'Specify which components to hide'
6✔
27
# These dialogs are asynchronous, they can pop-up at anytime.
28
# One example is when the .kicad_wks is missing, KiCad starts drawing and then detects it.
29
INFO_DIALOGS = {'KiCad PCB Editor Information', 'KiCad Schematic Editor Information'}
6✔
30
WARN_DIALOGS = {'KiCad PCB Editor Warning', 'KiCad Schematic Editor Warning'}
6✔
31
ASYNC_DIALOGS = INFO_DIALOGS | WARN_DIALOGS
6✔
32

33

34
def check_interposer(args, logger, cfg):
6✔
35
    # Name of the interposer library
36
    machine = platform.machine().lower()
6✔
37
    extra_name = '' if machine == 'x86_64' else '_'+machine
6✔
38
    interposer_lib = os.path.abspath(os.path.join(os.path.dirname(__file__), 'interposer', f'libinterposer{extra_name}.so'))
6✔
39
    logger.debug(f'Looking for interposer lib: {interposer_lib}')
6✔
40
    if (not os.path.isfile(interposer_lib) or  # The lib isn't there
6✔
41
       args.disable_interposer or              # The user disabled it
42
       os.environ.get('KIAUTO_INTERPOSER_DISABLE') or  # The user disabled it using the environment
43
       platform.system() != 'Linux'):  # Not Linux
44
        interposer_lib = None
3✔
45
    else:
46
        os.environ['LD_PRELOAD'] = interposer_lib
3✔
47
        logger.debug('** Interposer lib found')
3✔
48
    cfg.use_interposer = interposer_lib
6✔
49
    cfg.enable_interposer = interposer_lib or args.interposer_sniff
6✔
50
    cfg.logger = logger
6✔
51

52

53
def dump_interposer_dialog(cfg):
6✔
54
    cfg.logger.debug('Storing interposer dialog ({})'.format(cfg.flog_int.name))
×
55
    if cfg.enable_interposer and not cfg.use_interposer:
×
56
        try:
×
57
            while True:
58
                tm, line = cfg.kicad_q.get(timeout=.1)
×
59
                tm *= 1000
×
60
                diff = 0
×
61
                if cfg.last_msg_time:
×
62
                    diff = tm-cfg.last_msg_time
×
63
                cfg.last_msg_time = tm
×
64
                cfg.interposer_dialog.append('>>Interposer<<:{} (@{} D {})'.format(line[:-1], round(tm, 3), round(diff, 3)))
×
65
        except Empty:
×
66
            pass
×
67
    if hasattr(cfg, 'interposer_dialog'):
×
68
        for ln in cfg.interposer_dialog:
×
69
            cfg.flog_int.write(ln+'\n')
×
70
    cfg.flog_int.close()
×
71

72

73
def remove_interposer_print_dir(cfg):
6✔
74
    cfg.logger.debug('Removing temporal dir '+cfg.interposer_print_dir)
×
75
    shutil.rmtree(cfg.interposer_print_dir, ignore_errors=True)
×
76

77

78
def create_interposer_print_options_file(cfg):
6✔
79
    """ Creates a temporal holder for the print options """
80
    # We need a file to save the print options, make it unique to avoid collisions
81
    cfg.interposer_print_dir = mkdtemp()
3✔
82
    cfg.interposer_print_file = os.path.join(cfg.interposer_print_dir, INTERPOSER_OPS)
3✔
83
    cfg.logger.debug('Using temporal file {} for interposer print options'.format(cfg.interposer_print_file))
3✔
84
    os.environ['KIAUTO_INTERPOSER_PRINT'] = cfg.interposer_print_file
3✔
85
    atexit.register(remove_interposer_print_dir, cfg)
3✔
86

87

88
def save_interposer_print_data(cfg, tmpdir, fn, ext):
6✔
89
    """ Write the print options to the created file """
90
    with open(cfg.interposer_print_file, 'wt') as f:
3✔
91
        f.write(tmpdir+'\n')
3✔
92
        f.write(fn+'\n')
3✔
93
        f.write(ext+'\n')
3✔
94
    return os.path.join(tmpdir, fn+'.'+ext)
3✔
95

96

97
# def flush_queue():
98
#     """ Thread safe queue flush """
99
#     with cfg.kicad_q.mutex:
100
#         cfg.kicad_q.queue.clear()
101

102

103
def enqueue_output(out, queue):
6✔
104
    """ Read 1 line from the interposer and add it to the queue.
105
        Notes:
106
        * The queue is thread safe.
107
        * When we get an empty string we finish, this is the case for KiCad finished """
108
    tm_start = time.time()
3✔
109
    for line in iter(out.readline, ''):
3✔
110
        if (line.startswith('PANGO:') or line.startswith('GTK:') or line.startswith('IO:') or line.startswith('GLX:') or
3✔
111
           line.startswith('* ')):
112
            queue.put((time.time()-tm_start, line))
3✔
113
        # logger.error((time.time()-tm_start, line))
114
    out.close()
3✔
115

116

117
# https://stackoverflow.com/questions/375427/a-non-blocking-read-on-a-subprocess-pipe-in-python
118
def start_queue(cfg):
6✔
119
    """ Create a communication queue in a separated thread.
120
        It will collect all messages from the interposer. """
121
    if not cfg.enable_interposer:
6✔
122
        return
3✔
123
    cfg.logger.debug('Starting queue thread')
3✔
124
    cfg.kicad_q = Queue()
3✔
125
    # Avoid crashes when KiCad 5 sends an invalid Unicode sequence
126
    cfg.popen_obj.stdout.reconfigure(errors='ignore')
3✔
127
    cfg.kicad_t = Thread(target=enqueue_output, args=(cfg.popen_obj.stdout, cfg.kicad_q))
3✔
128
    cfg.kicad_t.daemon = True   # thread dies with the program
3✔
129
    cfg.kicad_t.start()
3✔
130
    cfg.collecting_io = False
3✔
131
    cfg.last_msg_time = 0
3✔
132
    cfg.interposer_dialog = []
3✔
133

134

135
def collect_io_from_queue(cfg):
6✔
136
    cfg.collected_io = set()
3✔
137
    cfg.collecting_io = True
3✔
138

139

140
def wait_queue(cfg, strs='', starts=False, times=1, timeout=300, do_to=True, kicad_can_exit=False, with_windows=False,
6✔
141
               dialog_interrupts=False):
142
    """ Wait for a string in the queue """
143
    if not cfg.use_interposer:
3✔
144
        return None
×
145
    if isinstance(strs, str):
3✔
146
        strs = [strs]
3✔
147
    end_time = time.time()+timeout*cfg.time_out_scale
3✔
148
    msg = 'Waiting for `{}` starts={} times={}'.format(strs, starts, times)
3✔
149
    cfg.interposer_dialog.append('KiAuto:'+msg)
3✔
150
    if cfg.verbose > 1:
3✔
151
        cfg.logger.debug(msg)
3✔
152
    while time.time() < end_time:
3✔
153
        try:
3✔
154
            tm, line = cfg.kicad_q.get(timeout=.1)
3✔
155
            line = line[:-1]
3✔
156
            if cfg.verbose > 1:
3✔
157
                tm *= 1000
3✔
158
                diff = 0
3✔
159
                if cfg.last_msg_time:
3✔
160
                    diff = tm-cfg.last_msg_time
3✔
161
                cfg.last_msg_time = tm
3✔
162
                cfg.logger.debug('>>Interposer<<:{} (@{} D {})'.format(line, round(tm, 3), round(diff, 3)))
3✔
163
            cfg.interposer_dialog.append(line)
3✔
164
            # The I/O can be in parallel to the UI
165
            if cfg.collecting_io and line.startswith('IO:'):
3✔
166
                cfg.collected_io.add(line)
3✔
167
        except Empty:
3✔
168
            line = ''
3✔
169
        if line == '' and cfg.popen_obj.poll() is not None:
3✔
170
            if kicad_can_exit:
3✔
171
                return KICAD_EXIT_MSG
3✔
172
            cfg.logger.error('KiCad unexpectedly died (error level {})'.format(cfg.popen_obj.poll()))
×
173
            exit(KICAD_DIED)
×
174
        old_times = times
3✔
175
        for s in strs:
3✔
176
            if s == '':
3✔
177
                # Waiting for anything ... but not for nothing
178
                if line != '':
×
179
                    return line
×
180
                continue
×
181
            if starts:
3✔
182
                if line.startswith(s):
3✔
183
                    times -= 1
3✔
184
                    break
3✔
185
            elif line == s:
3✔
186
                times -= 1
3✔
187
                break
3✔
188
        if times == 0:
3✔
189
            cfg.interposer_dialog.append('KiAuto:match')
3✔
190
            cfg.logger.debug('Interposer match: '+line)
3✔
191
            return line
3✔
192
        if old_times != times:
3✔
193
            cfg.interposer_dialog.append('KiAuto:times '+str(times))
1✔
194
            cfg.logger.debug('Interposer match, times='+str(times))
1✔
195
        if (not with_windows and not kicad_can_exit and line.startswith('GTK:Window Title:') and
3✔
196
            # The change in the unsaved status is ignored here
197
           not (not cfg.ki5 and line.endswith(cfg.window_title_end))):
198
            # We aren't expecting a window, but something seems to be there
199
            # Note that window title change is normal when we expect KiCad exiting
200
            title = line[17:]
3✔
201
            if title in INFO_DIALOGS:
3✔
202
                # Async dialogs
203
                dismiss_pcb_info(cfg, title)
2✔
204
            elif title == 'pcbnew Warning' or title in WARN_DIALOGS:
1✔
205
                # KiCad 5 error during post-load, before releasing the CPU
206
                # KiCad 7 missing fonts
207
                dismiss_pcbnew_warning(cfg, title)
1✔
208
            elif title.startswith(KIKIT_HIDE):
×
209
                # Buggy KiKit plugin creating a dialog at start-up (many times)
210
                pass
×
211
            else:
212
                unknown_dialog(cfg, title)
×
213
            if dialog_interrupts:
3✔
214
                raise InterruptedError()
×
215
    if do_to:
3✔
216
        raise RuntimeError('Timed out waiting for `{}`'.format(strs))
217

218

219
def wait_swap(cfg, times=1, kicad_can_exit=False):
6✔
220
    """ Wait an OpenGL draw (buffer swap) """
221
    if not cfg.use_interposer or not times:
3✔
222
        return None
3✔
223
    return wait_queue(cfg, 'GLX:Swap', starts=True, times=times, kicad_can_exit=kicad_can_exit)
1✔
224

225

226
def set_kicad_process(cfg, pid):
6✔
227
    """ Translates the PID into a psutil object, stores it in cfg """
228
    for process in psutil.process_iter():
6✔
229
        if process.pid == pid:
6✔
230
            cfg.kicad_process = process
6✔
231
            break
6✔
232
    else:
233
        cfg.logger.error('Unable to map KiCad PID to a process')
×
234
        exit(1)
×
235

236

237
def wait_kicad_ready_i(cfg, swaps=0, kicad_can_exit=False):
6✔
238
    res = wait_swap(cfg, swaps, kicad_can_exit=kicad_can_exit)
3✔
239
    # KiCad 5 takes 0 to 2 extra swaps (is random) so here we ensure KiCad is sleeping
240
    status = cfg.kicad_process.status()
3✔
241
    if status != psutil.STATUS_SLEEPING:
3✔
242
        if swaps:
3✔
243
            cfg.logger.debug('= KiCad still running after {} swaps, waiting more'.format(swaps))
1✔
244
        else:
245
            cfg.logger.debug('= KiCad still running, waiting more')
3✔
246
        try:
3✔
247
            while cfg.kicad_process.status() != psutil.STATUS_SLEEPING:
3✔
248
                new_res = wait_queue(cfg, 'GLX:Swap', starts=True, timeout=0.1, do_to=False, kicad_can_exit=kicad_can_exit)
3✔
249
                if new_res is not None:
3✔
250
                    res = new_res
3✔
251
        except psutil.NoSuchProcess:
3✔
252
            cfg.logger.debug('= KiCad died')
3✔
253
            return KICAD_EXIT_MSG
3✔
254
        cfg.logger.debug('= KiCad finally sleeping')
3✔
255
    else:
256
        cfg.logger.debug('= KiCad already sleeping ({})'.format(status))
3✔
257
    return res
3✔
258

259

260
def open_dialog_i(cfg, name, keys, no_show=False, no_wait=False, no_main=False, extra_msg=None, raise_if=None):
6✔
261
    wait_point(cfg)
3✔
262
    # Wait for KiCad to be sleeping
263
    wait_kicad_ready_i(cfg)
3✔
264
    cfg.logger.info('Opening dialog `{}` {}'.format(name, '('+extra_msg+')' if extra_msg is not None else ''))
3✔
265
    if isinstance(keys, str):
3✔
266
        keys = ['key', keys]
3✔
267
    xdotool(keys)
3✔
268
    pre_gtk_title = 'GTK:Window Title:'
3✔
269
    pre_gtk = pre_gtk_title if no_show else 'GTK:Window Show:'
3✔
270
    if isinstance(name, str):
3✔
271
        name = [name]
3✔
272
    name_w_pre = [pre_gtk+f for f in name]
3✔
273
    if raise_if is not None:
3✔
274
        name_w_pre.extend(raise_if)
2✔
275
    # Add the async dialogs
276
    for t in ASYNC_DIALOGS:
3✔
277
        name_w_pre.append(pre_gtk_title+t)
3✔
278
    # Wait for our dialog or any async dialog
279
    # Note: wait_queue won't dismiss them because we use "with_windows=True"
280
    while True:
2✔
281
        res = wait_queue(cfg, name_w_pre, with_windows=True)
3✔
282
        if raise_if is not None and res in raise_if:
3✔
283
            raise InterruptedError()
×
284
        title = res[len(pre_gtk_title):]
3✔
285
        if title not in ASYNC_DIALOGS:
3✔
286
            break
3✔
287
        if title in INFO_DIALOGS:
×
288
            # Get rid of the info dialog
289
            dismiss_pcb_info(cfg, title)
×
290
        else:
291
            dismiss_pcbnew_warning(cfg, title)
×
292
        # Send the keys again
293
        xdotool(keys)
×
294
    name = res[len(pre_gtk):]
3✔
295
    if no_wait:
3✔
296
        return name, None
3✔
297
    if not no_main:
3✔
298
        wait_queue(cfg, 'GTK:Main:In')
3✔
299
    # Wait for KiCad to be sleeping
300
    wait_kicad_ready_i(cfg)
3✔
301
    # The dialog is there, just make sure it has the focus
302
    return name, wait_for_window(name, name, 1)[0]
3✔
303

304

305
def check_text_replace(cfg, name):
6✔
306
    """ Wait until we get the file name """
307
    wait_queue(cfg, 'PANGO:'+name, dialog_interrupts=True)
3✔
308

309

310
def paste_text_i(cfg, msg, text):
6✔
311
    """ Paste some text and check the echo from KiCad, then wait for sleep """
312
    # Paste the name
313
    cfg.logger.info('{} ({})'.format(msg, text))
3✔
314
    wait_point(cfg)
3✔
315
    retry = True
3✔
316
    while retry:
3✔
317
        retry = False
3✔
318
        text_replace(text)
3✔
319
        try:
3✔
320
            # Look for the echo
321
            check_text_replace(cfg, text)
3✔
322
        except InterruptedError:
×
323
            cfg.logger.debug('Interrupted by a dialog while waiting echo, retrying')
×
324
            retry = True
×
325
    # Wait for KiCad to be sleeping
326
    wait_kicad_ready_i(cfg)
3✔
327

328

329
def paste_output_file_i(cfg, use_dir=False):
6✔
330
    """ Paste the output file/dir and check the echo from KiCad, then wait for sleep """
331
    name = cfg.output_dir if use_dir else cfg.output_file
3✔
332
    paste_text_i(cfg, 'Pasting output file', name)
3✔
333

334

335
def paste_bogus_filename(cfg):
6✔
336
    # We paste a bogus name that will be replaced
337
    paste_text_i(cfg, 'Paste bogus short name', BOGUS_FILENAME)
3✔
338

339

340
def setup_interposer_filename(cfg, fn=None):
6✔
341
    """ Defines the file name used by the interposer to fake the file choosers """
342
    if not cfg.use_interposer:
6✔
343
        return
3✔
344
    if fn is None:
3✔
345
        fn = cfg.output_file
3✔
346
    os.environ['KIAUTO_INTERPOSER_FILENAME'] = fn
3✔
347
    if os.path.isfile(BOGUS_FILENAME):
3✔
348
        cfg.logger.warning('Removing bogus file `{}`'.format(BOGUS_FILENAME))
×
349
        os.remove(BOGUS_FILENAME)
×
350

351

352
def send_keys(cfg, msg, keys, closes=None, delay_io=False, no_destroy=False):
6✔
353
    cfg.logger.info(msg)
6✔
354
    wait_point(cfg)
6✔
355
    if isinstance(keys, str):
6✔
356
        keys = ['key', keys]
6✔
357
    if delay_io:
6✔
358
        collect_io_from_queue(cfg)
3✔
359
    xdotool(keys)
6✔
360
    if closes is not None:
6✔
361
        if no_destroy:
3✔
362
            wait_close_dialog_i(cfg)
×
363
        else:
364
            if isinstance(closes, str):
3✔
365
                closes = [closes]
3✔
366
            for w in closes:
3✔
367
                try:
3✔
368
                    wait_queue(cfg, 'GTK:Window Destroy:'+w, dialog_interrupts=True)
3✔
369
                except InterruptedError:
×
370
                    # KiCad 7.99
371
                    xdotool(keys)
×
372
        wait_kicad_ready_i(cfg)
3✔
373

374

375
def wait_create_i(cfg, name, fn=None, forced_ext=None):
6✔
376
    """ Wait for open+close of the file.
377
        Also look for them in the collected_io messages.
378
        And if we just get close forget about the open. """
379
    cfg.logger.info('Wait for '+name+' file creation')
3✔
380
    wait_point(cfg)
3✔
381
    if fn is None:
3✔
382
        fn = cfg.output_file
3✔
383
    fn_kicad = fn+'.'+forced_ext if forced_ext and cfg.ki8 else fn
3✔
384
    # Experimental option to use the PID approach
385
    # Could help for VirtioFS where the close seems to be somehow bypassed
386
    use_pid = os.environ.get('KIAUTO_USE_PID_FOR_CREATE')
3✔
387
    if use_pid is not None and use_pid != '0':
3✔
388
        pid = cfg.pcbnew_pid if hasattr(cfg, 'pcbnew_pid') else cfg.eeschema_pid
×
389
        return wait_for_file_created_by_process(pid, fn_kicad)
×
390
    # Normal mechanism using the interposer
391
    open_msg = 'IO:open:'+fn_kicad
3✔
392
    close_msg = 'IO:close:'+fn_kicad
3✔
393
    if cfg.collecting_io:
3✔
394
        cfg.collecting_io = False
3✔
395
        got_open = open_msg in cfg.collected_io
3✔
396
        got_close = close_msg in cfg.collected_io
3✔
397
    else:
398
        got_open = False
2✔
399
        got_close = False
2✔
400
    if got_open or got_close:
3✔
401
        if got_open:
3✔
402
            cfg.logger.debug('Found IO '+open_msg)
3✔
403
    else:
404
        msg = wait_queue(cfg, [open_msg, close_msg], starts=True)
2✔
405
        got_close = msg.startswith(close_msg)
2✔
406
    if got_close:
3✔
407
        cfg.logger.debug('Found IO '+close_msg)
3✔
408
    else:
409
        wait_queue(cfg, close_msg, starts=True)
2✔
410
    wait_kicad_ready_i(cfg)
3✔
411
    if forced_ext and cfg.ki8 and os.path.isfile(fn_kicad):
3✔
412
        os.rename(fn_kicad, fn)
×
413

414

415
def collect_dialog_messages(cfg, title):
6✔
416
    cfg.logger.info(title+' dialog found ...')
3✔
417
    cfg.logger.debug('Gathering potential dialog content')
3✔
418
    msgs = set()
3✔
419
    for msg in range(12):
3✔
420
        res = wait_queue(cfg, 'PANGO:', starts=True, timeout=0.1, do_to=False)
3✔
421
        if res is None:
3✔
422
            # Some dialogs has less messages
423
            continue
3✔
424
        res = res[6:]
3✔
425
        if res not in IGNORED_DIALOG_MSGS:
3✔
426
            msgs.add(res)
3✔
427
    cfg.logger.debug('Messages: '+str(msgs))
3✔
428
    return msgs
3✔
429

430

431
def exit_pcb_ees_error(cfg):
6✔
432
    exit(PCBNEW_ERROR if cfg.is_pcbnew else EESCHEMA_ERROR)
×
433

434

435
def unknown_dialog(cfg, title, msgs=None, fatal=True):
6✔
436
    if msgs is None:
×
437
        msgs = collect_dialog_messages(cfg, title)
×
438
    msg_unk = 'Unknown KiCad dialog: '+title
×
439
    msg_msgs = 'Potential dialog messages: '+str(msgs)
×
440
    if fatal:
×
441
        cfg.logger.error(msg_unk)
×
442
        cfg.logger.error(msg_msgs)
×
443
        exit_pcb_ees_error(cfg)
×
444
    cfg.logger.warning(msg_unk)
×
445
    cfg.logger.warning(msg_msgs)
×
446

447

448
def dismiss_dialog(cfg, title, keys):
6✔
449
    cfg.logger.debug('Dismissing dialog `{}` using {}'.format(title, keys))
3✔
450
    try:
3✔
451
        wait_for_window(title, title, 2)
3✔
452
    except RuntimeError:
×
453
        # The window was already closed
454
        return
×
455
    if isinstance(keys, str):
3✔
456
        keys = [keys]
3✔
457
    xdotool(['key']+keys)
3✔
458

459

460
def dismiss_error(cfg, title):
6✔
461
    """ KiCad 6/7: Corrupted PCB/Schematic
462
        KiCad 5: Newer KiCad needed  for PCB, missing sch lib """
463
    msgs = collect_dialog_messages(cfg, title)
3✔
464
    if "Error loading PCB '"+cfg.input_file+"'." in msgs:
3✔
465
        # KiCad 6 PCB loading error
466
        cfg.logger.error('Error loading PCB file. Corrupted?')
×
467
        exit(CORRUPTED_PCB)
×
468
    if "Error loading schematic '"+cfg.input_file+"'." in msgs:
3✔
469
        # KiCad 6 schematic loading error
470
        cfg.logger.error('Error loading schematic file. Corrupted?')
1✔
471
        exit(EESCHEMA_ERROR)
1✔
472
    if 'KiCad was unable to open this file, as it was created with' in msgs:
2✔
473
        # KiCad 5 PCBnew loading a KiCad 6 file
474
        cfg.logger.error('Error loading PCB file. Needs KiCad 6?')
×
475
        exit(CORRUPTED_PCB)
×
476
    if 'Use the Manage Symbol Libraries dialog to fix the path (or remove the library).' in msgs:
2✔
477
        # KiCad 5 Eeschema missing lib. Should be a warning, not an error dialog
478
        cfg.logger.warning('Missing libraries, please fix it')
1✔
479
        dismiss_dialog(cfg, title, 'Return')
1✔
480
        return
1✔
481
    if 'The entire schematic could not be loaded.  Errors occurred attempting to load hierarchical sheets.' in msgs:
1✔
482
        # KiCad 6 loading a sheet, but sub-sheets are missing
483
        cfg.logger.error('Error loading schematic file. Missing schematic files?')
×
484
        exit(EESCHEMA_ERROR)
×
485
    for msg in msgs:
1✔
486
        if msg.startswith("Error loading schematic '"+cfg.input_file+"'."):
1✔
487
            # KiCad 7 schematic loading error
488
            cfg.logger.error('Error loading schematic file. Corrupted?')
1✔
489
            exit(EESCHEMA_ERROR)
1✔
490
    unknown_dialog(cfg, title, msgs)
×
491

492

493
def dismiss_file_open_error(cfg, title):
6✔
494
    """ KiCad 6: File is already opened """
495
    msgs = collect_dialog_messages(cfg, title)
2✔
496
    kind = 'PCB' if cfg.is_pcbnew else 'Schematic'
2✔
497
    fname = os.path.basename(cfg.input_file)
2✔
498
    # KiCad 6.x and <7.0.7: PCB 'xxxx' is already open.
499
    # KiCad 7.0.7: PCB 'xxxx' is already open by 'user' at 'host'
500
    start = kind+" '"+fname+"' is already open"
2✔
501
    found = False
2✔
502
    for msg in msgs:
2✔
503
        if msg.startswith(start) and msg.endswith("."):
2✔
504
            found = True
2✔
505
            fname = msg
2✔
506
            break
2✔
507
    if 'Open Anyway' in msgs and found:
2✔
508
        cfg.logger.warning('This file is already opened ({})'.format(fname))
2✔
509
        dismiss_dialog(cfg, title, ['Left', 'Return'])
2✔
510
        return
2✔
511
    unknown_dialog(cfg, title, msgs)
×
512

513

514
def dismiss_already_running(cfg, title):
6✔
515
    """ KiCad 5: Program already running """
516
    msgs = collect_dialog_messages(cfg, title)
1✔
517
    kind = 'pcbnew' if cfg.is_pcbnew else 'eeschema'
1✔
518
    if kind+' is already running. Continue?' in msgs:
1✔
519
        cfg.logger.warning(kind+' is already running')
1✔
520
        dismiss_dialog(cfg, title, 'Return')
1✔
521
        return
1✔
522
    unknown_dialog(cfg, title, msgs)
×
523

524

525
def dismiss_warning(cfg, title):
6✔
526
    """ KiCad 5 when already open file (PCB/SCH)
527
        KiCad 5 with bogus SCH files """
528
    msgs = collect_dialog_messages(cfg, title)
1✔
529
    kind = 'PCB' if cfg.is_pcbnew else 'Schematic'
1✔
530
    if kind+' file "'+cfg.input_file+'" is already open.' in msgs:
1✔
531
        cfg.logger.error('File already opened by another KiCad instance')
×
532
        exit_pcb_ees_error(cfg)
×
533
    if 'Error loading schematic file "'+os.path.abspath(cfg.input_file)+'".' in msgs:
1✔
534
        cfg.logger.error('eeschema reported an error while loading the schematic')
1✔
535
        exit(EESCHEMA_ERROR)
1✔
536
    unknown_dialog(cfg, title, msgs)
×
537

538

539
def dismiss_pcbnew_warning(cfg, title):
6✔
540
    """ Pad in invalid layer
541
        Missing font """
542
    msgs = collect_dialog_messages(cfg, title)
1✔
543
    # More generic cases
544
    for msg in msgs:
1✔
545
        # Warning about pad using an invalid layer
546
        # Missing font
547
        if msg.endswith("could not find valid layer for pad") or re.search(r"Font '(.*)' not found; substituting '(.*)'", msg):
1✔
548
            cfg.logger.warning(msg)
1✔
549
            dismiss_dialog(cfg, title, 'Return')
1✔
550
            return
1✔
551
    unknown_dialog(cfg, title, msgs)
×
552

553

554
def dismiss_remap_symbols(cfg, title):
6✔
555
    """ KiCad 5 opening an old file """
556
    msgs = collect_dialog_messages(cfg, title)
1✔
557
    if "Output Messages" in msgs and "Close" in msgs:
1✔
558
        cfg.logger.warning('Schematic needs update')
1✔
559
        dismiss_dialog(cfg, title, ['Escape'])
1✔
560
        return
1✔
561
    unknown_dialog(cfg, title, msgs)
×
562

563

564
def dismiss_save_changes(cfg, title):
6✔
565
    """ KiCad 5/6 asking for save changes to disk """
566
    msgs = collect_dialog_messages(cfg, title)
3✔
567
    if ("Save changes to '"+os.path.basename(cfg.input_file)+"' before closing?" in msgs or   # KiCad 6
3✔
568
       "If you don't save, all your changes will be permanently lost." in msgs):  # KiCad 5
569
        dismiss_dialog(cfg, title, ['Left', 'Left', 'Return'])
3✔
570
        return
3✔
571
    cfg.logger.error('Save dialog without correct messages')
×
572
    exit_pcb_ees_error(cfg)
×
573

574

575
def dismiss_pcb_info(cfg, title):
6✔
576
    """ KiCad 6 information, we know about missing worksheet style """
577
    msgs = collect_dialog_messages(cfg, title)
2✔
578
    found = False
2✔
579
    for msg in msgs:
2✔
580
        if msg.startswith("Drawing sheet ") and msg.endswith(" not found."):
2✔
581
            cfg.logger.warning("Missing worksheet file (.kicad_wks)")
2✔
582
            cfg.logger.warning(msg)
2✔
583
            found = True
2✔
584
            break
2✔
585
    if not found:
2✔
586
        unknown_dialog(cfg, title, msgs, fatal=False)
×
587
    dismiss_dialog(cfg, title, 'Return')
2✔
588

589

590
def exit_kicad_i(cfg):
6✔
591
    wait_kicad_ready_i(cfg)
3✔
592
    send_keys(cfg, 'Exiting KiCad', 'ctrl+q')
3✔
593
    pre = 'GTK:Window Title:'
3✔
594
    pre_l = len(pre)
3✔
595
    retries = 3
3✔
596
    while True:
2✔
597
        # Wait for any window
598
        res = wait_queue(cfg, pre, starts=True, timeout=2, kicad_can_exit=True, do_to=False, with_windows=True)
3✔
599
        known_dialog = False
3✔
600
        if res is not None:
3✔
601
            cfg.logger.debug('exit_kicad_i got '+res)
3✔
602
            if res == KICAD_EXIT_MSG:
3✔
603
                return
3✔
604
            title = res[pre_l:]
3✔
605
            if title == 'Save Changes?' or title == '':  # KiCad 5 without title!!!!
3✔
606
                dismiss_save_changes(cfg, title)
3✔
607
                known_dialog = True
3✔
608
            elif title == 'Pcbnew —  [Unsaved]':
1✔
609
                # KiCad 5 does it
610
                known_dialog = True
1✔
611
            else:
612
                unknown_dialog(cfg, title)
×
613
        retries -= 1
3✔
614
        if not retries:
3✔
615
            cfg.logger.error("Can't exit KiCad")
1✔
616
            return
1✔
617
        if not known_dialog:
3✔
618
            cfg.logger.warning("Retrying KiCad exit")
1✔
619
        # Wait until KiCad is sleeping again
620
        wait_kicad_ready_i(cfg, kicad_can_exit=True)
3✔
621
        # Retry the exit
622
        xdotool(['key', 'ctrl+q'])
3✔
623

624

625
def wait_close_dialog_i(cfg):
6✔
626
    """ Wait for the end of the main loop for the dialog.
627
        Then the main loop for the parent exits and enters again. """
628
    wait_queue(cfg, 'GTK:Main:Out')
×
629
    wait_queue(cfg, 'GTK:Main:In')
×
630

631

632
def wait_and_show_progress(cfg, msg, regex_str, trigger, msg_reg, skip_match=None, with_windows=False):
6✔
633
    """ msg: The message we are waiting
634
        regex_str: A regex to extract the progress message (text after PANGO:)
635
        trigger: A text that must be start at the beginning to test using the regex (PANGO:trigger)
636
        msg_reg: Message to print before the info (msg_reg: MATCH)
637
        skip_match: A match that we will skip
638
        with_windows: KiCad could pop-up a window """
639
    pres = [msg, 'PANGO:'+trigger]
3✔
640
    regex = re.compile(regex_str)
3✔
641
    with_info = False
3✔
642
    padding = 80*' '
3✔
643
    while True:
2✔
644
        res = wait_queue(cfg, pres, starts=True, with_windows=with_windows)
3✔
645
        if res.startswith(msg):
3✔
646
            # End of process detected
647
            if with_info:
3✔
648
                log.flush_info()
3✔
649
            wait_kicad_ready_i(cfg)
3✔
650
            return
3✔
651
        # Check if this message contains progress information
652
        if cfg.verbose and res.startswith('PANGO:'):
3✔
653
            res = res[6:]
3✔
654
            match = regex.match(res)
3✔
655
            if match is not None:
3✔
656
                m = match.group(1)
3✔
657
                if skip_match is None or m != skip_match:
3✔
658
                    m = msg_reg+': '+m+padding
3✔
659
                    log.info_progress(m[:80])
3✔
660
                    with_info = True
3✔
661

662

663
def wait_start_by_msg(cfg):
6✔
664
    if cfg.is_pcbnew:
3✔
665
        kind = 'PCB'
3✔
666
        prg_name = 'Pcbnew'
3✔
667
        unsaved = '  [Unsaved]'
3✔
668
    else:
669
        kind = 'Schematic'
3✔
670
        prg_name = 'Eeschema'
3✔
671
        unsaved = ' noname.sch'
3✔
672
    cfg.logger.info('Waiting for {} window ...'.format(prg_name))
3✔
673
    pre = 'GTK:Window Title:'
3✔
674
    pre_l = len(pre)
3✔
675
    cfg.logger.debug('Waiting {} to start and load the {}'.format(prg_name, kind))
3✔
676
    # Inform the elapsed time for slow loads
677
    pres = [pre, 'PANGO:0:']
3✔
678
    elapsed_r = re.compile(r'PANGO:(\d:\d\d:\d\d)')
3✔
679
    loading_msg = 'Loading '+kind
3✔
680
    prg_msg = prg_name+' —'
3✔
681
    with_elapsed = False
3✔
682
    while True:
2✔
683
        # Wait for any window
684
        res = wait_queue(cfg, pres, starts=True, timeout=cfg.wait_start, with_windows=True)
3✔
685
        cfg.logger.debug('wait_start_by_msg got '+res)
3✔
686
        match = elapsed_r.match(res)
3✔
687
        title = res[pre_l:]
3✔
688
        if not match and with_elapsed:
3✔
689
            log.flush_info()
×
690
        if not cfg.ki5 and title.endswith(cfg.window_title_end):
3✔
691
            # KiCad 6
692
            if title.startswith('[no schematic loaded]'):
2✔
693
                # False alarma, nothing loaded
694
                continue
2✔
695
            # KiCad finished the load process
696
            if title[0] == '*':
2✔
697
                # This is an old format file that will be saved in the new format
698
                cfg.logger.warning('Old file format detected, please convert it to KiCad 6 if experimenting problems')
×
699
            wait_queue(cfg, 'GTK:Main:In')
2✔
700
            return
2✔
701
        elif cfg.ki5 and title.startswith(prg_msg):
3✔
702
            # KiCad 5 title
703
            if not title.endswith(unsaved):
1✔
704
                # KiCad 5 name is "Pcbnew — PCB_NAME" or "Eeschema — SCH_NAME [HIERARCHY] — SCH_FILE_NAME"
705
                # wait_pcbnew()
706
                wait_queue(cfg, ['GTK:Window Show:'+title, 'GTK:Main:In'], starts=True, timeout=cfg.wait_start, times=2,
1✔
707
                           with_windows=True)
708
                return
1✔
709
            # The "  [Unsaved]" is changed before the final load, ignore it
710
        elif title == '' or title == cfg.pn_simple_window_title or title == 'Eeschema':
3✔
711
            # This is the main window before loading anything
712
            # Note that KiCad 5 can create dialogs BEFORE this
713
            pass
3✔
714
        elif title == loading_msg:
3✔
715
            # This is the dialog for the loading progress, wait
716
            pass
2✔
717
        elif match is not None:
3✔
718
            msg = match.group(1)
×
719
            if msg != '0:00:00':
×
720
                log.info_progress('Elapsed time: '+msg)
×
721
                with_elapsed = True
×
722
        elif title == 'Error' or (cfg.ki7 and title == 'KiCad Schematic Editor Error'):
3✔
723
            dismiss_error(cfg, title)
3✔
724
        elif title == 'File Open Error':
3✔
725
            dismiss_file_open_error(cfg, title)
1✔
726
        elif cfg.ki7 and title == 'File Open Warning':
2✔
727
            dismiss_file_open_error(cfg, title)
1✔
728
        elif title == 'Confirmation':
1✔
729
            dismiss_already_running(cfg, title)
1✔
730
        elif title == 'Warning':
1✔
731
            dismiss_warning(cfg, title)
1✔
732
        elif title == 'pcbnew Warning' or title in WARN_DIALOGS:
1✔
733
            dismiss_pcbnew_warning(cfg, title)
×
734
        elif title == 'Remap Symbols':
1✔
735
            dismiss_remap_symbols(cfg, title)
1✔
736
        elif title in INFO_DIALOGS:
×
737
            dismiss_pcb_info(cfg, title)
×
738
        elif title.startswith(KIKIT_HIDE):
×
739
            # Buggy KiKit plugin creating a dialog at start-up (many times)
740
            pass
×
741
        else:
742
            unknown_dialog(cfg, title)
×
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc