syncloud-lib 333__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,9 @@
1
+ from syncloudlib.application.connection import api_post, api_get
2
+
3
+
4
+ def set_dkim_key(dkim_key):
5
+ return api_post('/config/set_dkim_key', data={"dkim_key": dkim_key})
6
+
7
+
8
+ def get_dkim_key():
9
+ return api_get('/config/get_dkim_key')
@@ -0,0 +1,40 @@
1
+ import requests_unixsocket
2
+ import json
3
+ import os
4
+
5
+ socket_file = '/var/snap/platform/common/api.socket'
6
+ socket = 'http+unix://{0}'.format(socket_file.replace('/', '%2F'))
7
+
8
+
9
+ def api_post(url, data):
10
+ session = requests_unixsocket.Session()
11
+ try:
12
+ response = session.post('{0}{1}'.format(socket, url), data=data)
13
+ if response.status_code == 200:
14
+ response_json = json.loads(response.text)
15
+ if 'success' in response_json and response_json['success']:
16
+ return response_json['data']
17
+ else:
18
+ raise Exception('service error: {0}'.format(response_json['message']))
19
+
20
+ else:
21
+ raise Exception('unable to connect to {0} with error code: {1}'.format(socket, response.status_code))
22
+ except Exception as e:
23
+ raise Exception('unable to connect to {0}'.format(socket), e)
24
+
25
+
26
+ def api_get(url):
27
+ session = requests_unixsocket.Session()
28
+ try:
29
+ response = session.get('{0}{1}'.format(socket, url))
30
+ if response.status_code == 200:
31
+ response_json = json.loads(response.text)
32
+ if 'success' in response_json and response_json['success']:
33
+ return response_json['data']
34
+ else:
35
+ raise Exception('service error: {0}'.format(response_json['message']))
36
+
37
+ else:
38
+ raise Exception('unable to connect to {0} with error code: {1}'.format(socket, response.status_code))
39
+ except Exception as e:
40
+ raise Exception('unable to connect to {0}'.format(socket), e)
@@ -0,0 +1,9 @@
1
+ from syncloudlib.application.connection import api_get
2
+
3
+
4
+ def get_app_dir(app):
5
+ return api_get('/app/install_path?name={0}'.format(app))
6
+
7
+
8
+ def get_data_dir(app):
9
+ return api_get('/app/data_path?name={0}'.format(app))
@@ -0,0 +1,9 @@
1
+ from syncloudlib.application.connection import api_post
2
+
3
+
4
+ def add_port(port, protocol):
5
+ return api_post('/port/add', data={"port": port, "protocol": protocol})
6
+
7
+
8
+ def remove_port(port, protocol):
9
+ return api_post('/port/remove', data={"port": port, "protocol": protocol})
@@ -0,0 +1,6 @@
1
+ from syncloudlib.application.connection import api_post
2
+
3
+
4
+ def restart(service_name):
5
+ return api_post('/service/restart', data={"name": service_name})
6
+
@@ -0,0 +1,9 @@
1
+ from syncloudlib.application.connection import api_post, api_get
2
+
3
+
4
+ def init_storage(app, user):
5
+ return api_post('/app/init_storage', data={"app_name": app, "user_name": user})
6
+
7
+
8
+ def get_storage_dir(app):
9
+ return api_get('/app/storage_dir?name={0}'.format(app))
@@ -0,0 +1,11 @@
1
+ from syncloudlib.application.connection import api_get
2
+
3
+
4
+ def get_app_url(app):
5
+ return api_get('/app/url?name={0}'.format(app))
6
+
7
+ def get_device_domain_name():
8
+ return api_get('/app/device_domain_name')
9
+
10
+ def get_app_domain_name(app):
11
+ return api_get('/app/domain_name?name={0}'.format(app))
@@ -0,0 +1,5 @@
1
+ from syncloudlib.application.connection import api_get
2
+
3
+
4
+ def get_email():
5
+ return api_get('/user/email')
syncloudlib/error.py ADDED
@@ -0,0 +1,6 @@
1
+ class PassthroughJsonError(Exception):
2
+ def __init__(self, message, json):
3
+ Exception.__init__(self)
4
+ self.message = message
5
+ self.json = json
6
+
syncloudlib/fs.py ADDED
@@ -0,0 +1,44 @@
1
+ from os import makedirs, chown, utime
2
+ from os.path import isdir
3
+ from grp import getgrnam
4
+ from pwd import getpwnam
5
+ from shutil import rmtree
6
+ from subprocess import check_output
7
+
8
+
9
+ def makepath(path):
10
+ if not isdir(path):
11
+ makedirs(path)
12
+
13
+
14
+ def removepath(path):
15
+ if isdir(path):
16
+ rmtree(path, ignore_errors=True)
17
+
18
+
19
+ def createfile(filepath):
20
+ try:
21
+ f = open(filepath, 'w+')
22
+ f.close()
23
+ except:
24
+ pass
25
+
26
+
27
+ def chownpath(path, user, recursive=False):
28
+ if recursive:
29
+ chownrecursive(path, user)
30
+ else:
31
+ chown(path, getpwnam(user).pw_uid, getgrnam(user).gr_gid)
32
+
33
+
34
+ def chownrecursive(path, user):
35
+ estimate_count = int(check_output('find {0} -maxdepth 3 | wc -l'.format(path), shell=True).decode())
36
+ if estimate_count > 1000:
37
+ return 'not changing permissions, too many files'
38
+
39
+ return check_output('chown -RLf {0}. {1}'.format(user, path), shell=True).decode()
40
+
41
+
42
+ def touchfile(file):
43
+ with open(file, 'a'):
44
+ utime(file, None)
syncloudlib/http.py ADDED
@@ -0,0 +1,28 @@
1
+ import time
2
+
3
+
4
+ def wait_for_rest(web_session, url, code, attempts=10):
5
+ def pred(resp):
6
+ return resp.status_code == code
7
+
8
+ wait_for_response(web_session, url, pred, attempts)
9
+
10
+
11
+ def wait_for_response(web_session, url, resp_predicate, attempts=10):
12
+
13
+ attempt=0
14
+ attempt_limit=attempts
15
+ response = None
16
+ while attempt < attempt_limit:
17
+ try:
18
+ response = web_session.get(url, verify=False)
19
+ print('code: {0}'.format(response.status_code))
20
+ if resp_predicate(response):
21
+ return
22
+ except Exception as e:
23
+ print(str(e))
24
+ time.sleep(10)
25
+ attempt = attempt + 1
26
+ if response and response.text:
27
+ print(response.text)
28
+ raise Exception('exhausted')
File without changes
@@ -0,0 +1,247 @@
1
+ import pytest
2
+ import os
3
+ from os.path import join, exists
4
+ from selenium import webdriver
5
+
6
+ from syncloudlib.integration.installer import get_data_dir, get_app_dir, get_service_prefix, get_ssh_env_vars, get_snap_data_dir
7
+ from syncloudlib.integration.device import Device
8
+ from syncloudlib.integration.selenium_wrapper import SeleniumWrapper
9
+
10
+
11
+ def pytest_addoption(parser):
12
+ parser.addoption("--domain", action="store", default="device.com")
13
+ parser.addoption("--device-host", action="store")
14
+ parser.addoption("--app-archive-path", action="store")
15
+ parser.addoption("--app", action="store")
16
+ parser.addoption("--ui-mode", action="store", default="desktop")
17
+ parser.addoption("--device-user", action="store", default="user")
18
+ parser.addoption("--build-number", action="store", default="local")
19
+ parser.addoption("--browser", action="store", default="firefox")
20
+ parser.addoption("--browser-height", action="store", default=1000)
21
+ parser.addoption("--redirect-user", action="store", default="redirect-user-notset")
22
+ parser.addoption("--redirect-password", action="store", default="redirect-password-notset")
23
+ parser.addoption("--distro", action="store", default="distro")
24
+ parser.addoption("--arch", action="store", default="unset-arch")
25
+
26
+
27
+ @pytest.fixture(scope='session')
28
+ def build_number(request):
29
+ return request.config.getoption("--build-number")
30
+
31
+
32
+ @pytest.fixture(scope='session')
33
+ def device_user(request):
34
+ return request.config.getoption("--device-user")
35
+
36
+
37
+ @pytest.fixture(scope='session')
38
+ def device_password():
39
+ return 'Password1'
40
+
41
+
42
+ @pytest.fixture(scope='session')
43
+ def redirect_user(request):
44
+ return request.config.getoption("--redirect-user")
45
+
46
+
47
+ @pytest.fixture(scope='session')
48
+ def redirect_password(request):
49
+ return request.config.getoption("--redirect-password")
50
+
51
+
52
+ @pytest.fixture(scope='session')
53
+ def app(request):
54
+ return request.config.getoption("--app")
55
+
56
+
57
+ @pytest.fixture(scope='session')
58
+ def ui_mode(request):
59
+ return request.config.getoption("--ui-mode")
60
+
61
+
62
+ @pytest.fixture(scope='session')
63
+ def app_archive_path(request):
64
+ return request.config.getoption("--app-archive-path")
65
+
66
+
67
+ @pytest.fixture(scope='session')
68
+ def device_host(request, app, domain):
69
+ device_host = request.config.getoption("--device-host")
70
+ if device_host:
71
+ return device_host
72
+ return '{0}.{1}'.format(app, domain)
73
+
74
+
75
+ @pytest.fixture(scope='session')
76
+ def domain(request, distro):
77
+ domain = request.config.getoption("--domain")
78
+ if domain:
79
+ return domain
80
+ return '{0}.com'.format(distro)
81
+
82
+
83
+ @pytest.fixture(scope='session')
84
+ def browser(request):
85
+ return request.config.getoption("--browser")
86
+
87
+
88
+ @pytest.fixture(scope='session')
89
+ def browser_height(request):
90
+ return int(request.config.getoption("--browser-height"))
91
+
92
+
93
+ @pytest.fixture(scope='session')
94
+ def distro(request):
95
+ return request.config.getoption("--distro")
96
+
97
+
98
+ @pytest.fixture(scope='session')
99
+ def arch(request):
100
+ return request.config.getoption("--arch")
101
+
102
+
103
+ @pytest.fixture(scope='session')
104
+ def app_domain(app, domain):
105
+ return '{0}.{1}'.format(app, domain)
106
+
107
+
108
+ @pytest.fixture(scope="session")
109
+ def platform_data_dir():
110
+ return get_data_dir('platform')
111
+
112
+
113
+ @pytest.fixture(scope="session")
114
+ def data_dir(app):
115
+ return get_data_dir(app)
116
+
117
+
118
+ @pytest.fixture(scope="session")
119
+ def snap_data_dir(app):
120
+ return get_snap_data_dir(app)
121
+
122
+
123
+ @pytest.fixture(scope="session")
124
+ def app_dir(app):
125
+ return get_app_dir(app)
126
+
127
+
128
+ @pytest.fixture(scope="session")
129
+ def service_prefix():
130
+ return get_service_prefix()
131
+
132
+
133
+ def new_firefox_driver(hub_url, ui_mode):
134
+ #desktop_agent = "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:104.0) Gecko/20100101 Firefox/100.0"
135
+ mobile_agent = "Mozilla/5.0 (iPhone; CPU iPhone OS 16_1_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.1 Mobile/15E148 Safari/604.1"
136
+
137
+ options = webdriver.FirefoxOptions()
138
+ options.set_preference('app.update.auto', False)
139
+ options.set_preference('app.update.enabled', False)
140
+ if ui_mode == "mobile":
141
+ options.set_preference("general.useragent.override", mobile_agent)
142
+ options.set_preference("devtools.console.stdout.content", True)
143
+ options.set_capability('acceptInsecureCerts', True)
144
+ options.set_capability('se:recordVideo', True)
145
+ options.set_preference("media.navigator.streams.fake", True)
146
+ options.set_preference("media.navigator.permission.disabled", True)
147
+
148
+ return webdriver.Remote(
149
+ command_executor=hub_url,
150
+ options=options
151
+ )
152
+
153
+
154
+ def new_chrome_driver(hub_url, ui_mode):
155
+ #desktop_agent = "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:104.0) Gecko/20100101 Firefox/100.0"
156
+ mobile_agent = "Mozilla/5.0 (iPhone; CPU iPhone OS 16_1_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.1 Mobile/15E148 Safari/604.1"
157
+
158
+ options = webdriver.ChromeOptions()
159
+ if ui_mode == "mobile":
160
+ options.add_argument('user-agent={}'.format(mobile_agent))
161
+ #options.add_argument('--headless')
162
+ options.add_argument('--no-sandbox')
163
+ options.add_argument('--disable-dev-shm-usage')
164
+ options.set_capability('goog:loggingPrefs', {'performance': 'ALL'})
165
+ options.set_capability('acceptInsecureCerts', True)
166
+ options.set_capability('se:recordVideo', True)
167
+ options.add_argument("--use-fake-ui-for-media-stream")
168
+ options.add_argument("--use-fake-device-for-media-stream")
169
+ return webdriver.Remote(
170
+ command_executor=hub_url,
171
+ options=options
172
+ )
173
+
174
+
175
+ @pytest.fixture(scope="session")
176
+ def driver(ui_mode, browser, browser_height, request):
177
+ hub_url = 'http://selenium:4444/wd/hub'
178
+ width = 1024
179
+ if ui_mode == "mobile":
180
+ width = 400
181
+
182
+ if browser == "firefox":
183
+ driver = new_firefox_driver(hub_url, ui_mode)
184
+ else:
185
+ driver = new_chrome_driver(hub_url, ui_mode)
186
+ driver.set_window_rect(0, 0, width, browser_height)
187
+
188
+ def driver_quit():
189
+ driver.quit()
190
+
191
+ request.addfinalizer(driver_quit)
192
+
193
+ return driver
194
+
195
+
196
+ @pytest.fixture(scope="session")
197
+ def ssh_env_vars(app):
198
+ return get_ssh_env_vars(app)
199
+
200
+
201
+ @pytest.fixture(scope='function')
202
+ def device_session(device):
203
+ return device.login()
204
+
205
+
206
+ @pytest.fixture(scope="session")
207
+ def device(domain, device_user,
208
+ device_password, redirect_user, redirect_password, ssh_env_vars):
209
+ return Device(domain, device_user,
210
+ device_password, redirect_user, redirect_password, ssh_env_vars)
211
+
212
+
213
+ @pytest.fixture(scope="session")
214
+ def log_dir(project_dir):
215
+ dir = join(project_dir, 'log')
216
+ if not exists(dir):
217
+ os.mkdir(dir)
218
+ return dir
219
+
220
+
221
+ @pytest.fixture(scope="session")
222
+ def artifact_dir(project_dir, distro):
223
+ dir = join(project_dir, 'artifact', distro)
224
+ if not exists(dir):
225
+ os.mkdir(dir)
226
+ return dir
227
+
228
+
229
+ @pytest.fixture(scope="session")
230
+ def screenshot_dir(artifact_dir, ui_mode):
231
+ ui_dir = join(artifact_dir, ui_mode)
232
+ if not exists(ui_dir):
233
+ os.mkdir(ui_dir)
234
+ dir = join(ui_dir, 'screenshot')
235
+ if not exists(dir):
236
+ os.mkdir(dir)
237
+ return dir
238
+
239
+
240
+ @pytest.fixture(scope="session")
241
+ def selenium_timeout():
242
+ return 30
243
+
244
+
245
+ @pytest.fixture(scope="session")
246
+ def selenium(driver, ui_mode, screenshot_dir, app_domain, selenium_timeout, browser):
247
+ return SeleniumWrapper(driver, ui_mode, screenshot_dir, app_domain, selenium_timeout, browser)
@@ -0,0 +1,111 @@
1
+ import requests
2
+
3
+ from syncloudlib.integration.installer import wait_for_platform_web, wait_for_installer
4
+ from syncloudlib.http import wait_for_rest
5
+ from syncloudlib.integration.ssh import run_scp, run_ssh
6
+ from requests.adapters import HTTPAdapter
7
+ import socket
8
+
9
+ class Device:
10
+
11
+ def __init__(self, domain, device_user, device_password, redirect_user, redirect_password,
12
+ ssh_env_vars):
13
+ self.domain = domain
14
+ self.device_user = device_user
15
+ self.device_password = device_password
16
+ self.redirect_user = redirect_user
17
+ self.redirect_password = redirect_password
18
+ self.ssh_env_vars = ssh_env_vars
19
+ self.ssh_password = 'syncloud'
20
+ self.session = None
21
+
22
+ def deactivate(self):
23
+ run_ssh(self.domain, 'rm /var/snap/platform/common/platform.db', password=self.ssh_password)
24
+
25
+ def activate(self, channel="stable"):
26
+ ip = socket.gethostbyname(self.domain)
27
+ run_ssh(self.domain, 'echo "{0} auth.{1}" >> /etc/hosts'.format(ip, self.domain), password=self.ssh_password, retries=10)
28
+ run_ssh(self.domain, '/snap/platform/current/bin/upgrade-snapd.sh {0}'.format(channel), password=self.ssh_password, retries=10)
29
+ run_ssh(self.domain, 'snap refresh platform --channel={0}'.format(channel), password=self.ssh_password, retries=10)
30
+
31
+ wait_for_rest(requests.session(), "https://{0}/rest/id".format(self.domain), 200, 10)
32
+
33
+ response = requests.post('https://{0}/rest/activate/managed'.format(self.domain),
34
+ json={'redirect_email': self.redirect_user,
35
+ 'redirect_password': self.redirect_password,
36
+ 'domain': self.domain,
37
+ 'device_username': self.device_user,
38
+ 'device_password': self.device_password}, verify=False)
39
+ if response.status_code == 200:
40
+ self.activated()
41
+ self.login()
42
+ return response
43
+
44
+ def activate_custom(self, channel="stable"):
45
+ ip = socket.gethostbyname(self.domain)
46
+ run_ssh(self.domain, 'echo "{0} auth.{1}" >> /etc/hosts'.format(ip, self.domain), password=self.ssh_password, retries=10)
47
+ run_ssh(self.domain, '/snap/platform/current/bin/upgrade-snapd.sh {0}'.format(channel), password=self.ssh_password, retries=10)
48
+ run_ssh(self.domain, 'snap refresh platform --channel={0}'.format(channel), password=self.ssh_password, retries=10)
49
+
50
+ wait_for_rest(requests.session(), "https://{0}/rest/id".format(self.domain), 200, 10)
51
+ response = requests.post('https://{0}/rest/activate/custom'.format(self.domain),
52
+ json={'domain': self.domain,
53
+ 'device_username': self.device_user,
54
+ 'device_password': self.device_password}, verify=False)
55
+ if response.status_code == 200:
56
+ self.activated()
57
+ self.login()
58
+ return response
59
+
60
+ def activated(self):
61
+ self.ssh_password = self.device_password
62
+
63
+ def login(self, retries=5):
64
+ session = requests.session()
65
+ session.mount('https://{0}'.format(self.domain), HTTPAdapter(max_retries=retries))
66
+ retry = 0
67
+ while True:
68
+ try:
69
+ session.post('https://{0}/rest/login'.format(self.domain), verify=False, allow_redirects=False,
70
+ json={'username': self.device_user, 'password': self.device_password})
71
+ response = session.get('https://{0}/rest/user'.format(self.domain), verify=False,
72
+ allow_redirects=False)
73
+ if response.status_code == 200:
74
+ self.session = session
75
+ return session
76
+ except Exception as e:
77
+ print(str(e))
78
+ print('retry {0} of {1}'.format(retry, retries))
79
+ retry += 1
80
+ if retry > retries:
81
+ raise Exception('cannot login')
82
+
83
+ def app_remove(self, app, attempts=200):
84
+ response = self.session.post('https://{0}/rest/app/remove'.format(self.domain), json={'app_id': app},
85
+ verify=False, allow_redirects=False)
86
+
87
+ wait_for_installer(self.session, self.domain, attempts=attempts)
88
+ return response
89
+
90
+ def app_install(self, app, attempts=200):
91
+ response = self.session.post('https://{0}/rest/app/install'.format(self.domain), json={'app_id': app},
92
+ verify=False, allow_redirects=False)
93
+
94
+ wait_for_installer(self.session, self.domain, attempts=attempts)
95
+ return response
96
+
97
+ def run_ssh(self, cmd, retries=0, throw=True, env_vars='', debug=True, sleep=1):
98
+ ssh_env_vars = self.ssh_env_vars + ' ' + env_vars
99
+ return run_ssh(self.domain, cmd, password=self.ssh_password, env_vars=ssh_env_vars, retries=retries,
100
+ throw=throw, debug=debug, sleep=sleep)
101
+
102
+ def scp_from_device(self, dir_from, dir_to, throw=False):
103
+ return run_scp('-r root@{0}:{1} {2}'.format(self.domain, dir_from, dir_to), password=self.ssh_password,
104
+ throw=throw)
105
+
106
+ def scp_to_device(self, dir_from, dir_to, throw=False):
107
+ return run_scp('-r {0} root@{1}:{2}'.format(dir_from, self.domain, dir_to), password=self.ssh_password,
108
+ throw=throw)
109
+
110
+ def http_get(self, url):
111
+ return self.session.get('https://{0}{1}'.format(self.domain, url), allow_redirects=False, verify=False)
@@ -0,0 +1,9 @@
1
+ import socket
2
+
3
+ def add_host_alias(app, host, domain, hosts_file='/etc/hosts'):
4
+ ip = socket.gethostbyname(host)
5
+ with open(hosts_file, "a") as hosts:
6
+ print("adding hosts: {0} {1}".format(ip, domain))
7
+ hosts.write("{0} {1}\n".format(ip, domain))
8
+ print("adding hosts: {0} {1}.{2}".format(ip, app, domain))
9
+ hosts.write("{0} {1}.{2}\n".format(ip, app, domain))
@@ -0,0 +1,78 @@
1
+ import time
2
+ from syncloudlib.integration.ssh import run_scp, run_ssh
3
+ from subprocess import check_output
4
+ from os.path import split
5
+ import json
6
+ import os
7
+
8
+ SNAP = 'snap'
9
+ SNAP_INSTALL = '{0} install --devmode'.format(SNAP)
10
+
11
+
12
+ def get_data_dir(app):
13
+ return '/var/snap/{0}/common'.format(app)
14
+
15
+ def get_snap_data_dir(app):
16
+ return '/var/snap/{0}/current'.format(app)
17
+
18
+ def get_app_dir(app):
19
+ return '/snap/{0}/current'.format(app)
20
+
21
+
22
+ def get_service_prefix():
23
+ return 'snap.'
24
+
25
+
26
+ def get_ssh_env_vars(app):
27
+ return 'SNAP={0} SNAP_COMMON={1}'.format(get_app_dir(app), get_data_dir(app))
28
+
29
+
30
+ def local_install(host, password, app_archive_path):
31
+ run_ssh(host, 'ls -la /', password=password)
32
+ _, app_archive = split(app_archive_path)
33
+ run_scp('{0} root@{1}:/'.format(app_archive_path, host), password=password, retries=3)
34
+ run_ssh(host, 'ls -la /{0}'.format(app_archive), password=password)
35
+ run_ssh(host, '{0} /{1}'.format(SNAP_INSTALL, app_archive), password=password)
36
+
37
+
38
+ def local_remove(host, password, app):
39
+ run_ssh(host, '{0} remove {1}'.format(SNAP, app), password=password)
40
+
41
+
42
+ def wait_for_platform_web(host):
43
+ print(check_output('while ! nc -w 1 -z {0} 81; do sleep 1; done'.format(host), shell=True).decode())
44
+ print(check_output('while ! nc -w 1 -z {0} 80; do sleep 1; done'.format(host), shell=True).decode())
45
+
46
+
47
+ def wait_for_installer(web_session, host, attempts=60, throw_on_error=False):
48
+ is_running = True
49
+ attempt = 0
50
+ while is_running and attempt < attempts:
51
+ try:
52
+ response = web_session.get('https://{0}/rest/installer/status'.format(host), verify=False)
53
+ if response.status_code == 200:
54
+ status = json.loads(response.text)
55
+ is_running = status['data']['is_running']
56
+ else:
57
+ if throw_on_error:
58
+ raise Exception("error http status code: {0}".format(response.status_code))
59
+ except Exception as e:
60
+ print(e.message)
61
+ if throw_on_error:
62
+ raise e
63
+
64
+ print("attempt: {0}/{1}".format(attempt, attempts))
65
+ attempt += 1
66
+ time.sleep(10)
67
+
68
+ if is_running:
69
+ raise Exception("time out waiting for thr installer")
70
+
71
+ def wait_for_file(file, attempts=10):
72
+ attempt=0
73
+ attempt_limit=attempts
74
+ while attempt < attempt_limit:
75
+ if os.path.isfile(file):
76
+ return
77
+ time.sleep(10)
78
+ attempt = attempt + 1