fastled 1.1.7__py2.py3-none-any.whl → 1.1.16__py2.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.
- fastled/__init__.py +3 -0
- fastled/app.py +117 -53
- fastled/compile_server.py +156 -251
- fastled/docker_manager.py +389 -135
- fastled/filewatcher.py +196 -146
- fastled/keyboard.py +91 -0
- fastled/open_browser.py +5 -1
- fastled/util.py +10 -0
- fastled/web_compile.py +294 -227
- fastled-1.1.16.dist-info/METADATA +198 -0
- fastled-1.1.16.dist-info/RECORD +20 -0
- fastled/check_cpp_syntax.py +0 -34
- fastled-1.1.7.dist-info/METADATA +0 -120
- fastled-1.1.7.dist-info/RECORD +0 -19
- {fastled-1.1.7.dist-info → fastled-1.1.16.dist-info}/LICENSE +0 -0
- {fastled-1.1.7.dist-info → fastled-1.1.16.dist-info}/WHEEL +0 -0
- {fastled-1.1.7.dist-info → fastled-1.1.16.dist-info}/entry_points.txt +0 -0
- {fastled-1.1.7.dist-info → fastled-1.1.16.dist-info}/top_level.txt +0 -0
fastled/docker_manager.py
CHANGED
@@ -1,17 +1,36 @@
|
|
1
|
-
"""
|
1
|
+
"""
|
2
|
+
New abstraction for Docker management with improved Ctrl+C handling.
|
3
|
+
"""
|
2
4
|
|
5
|
+
import _thread
|
3
6
|
import subprocess
|
4
7
|
import sys
|
8
|
+
import threading
|
5
9
|
import time
|
10
|
+
import traceback
|
11
|
+
import warnings
|
12
|
+
from datetime import datetime, timezone
|
6
13
|
from pathlib import Path
|
7
14
|
|
8
|
-
import docker
|
15
|
+
import docker
|
16
|
+
from appdirs import user_data_dir
|
17
|
+
from disklru import DiskLRUCache
|
18
|
+
from docker.client import DockerClient
|
19
|
+
from docker.models.containers import Container
|
20
|
+
from docker.models.images import Image
|
9
21
|
from filelock import FileLock
|
10
22
|
|
11
|
-
|
23
|
+
CONFIG_DIR = Path(user_data_dir("fastled", "fastled"))
|
24
|
+
CONFIG_DIR.mkdir(parents=True, exist_ok=True)
|
25
|
+
DB_FILE = CONFIG_DIR / "db.db"
|
26
|
+
DISK_CACHE = DiskLRUCache(str(DB_FILE), 10)
|
12
27
|
|
13
|
-
|
14
|
-
|
28
|
+
|
29
|
+
# Docker uses datetimes in UTC but without the timezone info. If we pass in a tz
|
30
|
+
# then it will throw an exception.
|
31
|
+
def _utc_now_no_tz() -> datetime:
|
32
|
+
now = datetime.now(timezone.utc)
|
33
|
+
return now.replace(tzinfo=None)
|
15
34
|
|
16
35
|
|
17
36
|
def _win32_docker_location() -> str | None:
|
@@ -26,11 +45,54 @@ def _win32_docker_location() -> str | None:
|
|
26
45
|
return None
|
27
46
|
|
28
47
|
|
48
|
+
_HERE = Path(__file__).parent
|
49
|
+
_FILE_LOCK = FileLock(str(_HERE / "fled.lock"))
|
50
|
+
|
51
|
+
|
52
|
+
class RunningContainer:
|
53
|
+
def __init__(self, container, first_run=False):
|
54
|
+
self.container = container
|
55
|
+
self.first_run = first_run
|
56
|
+
self.running = True
|
57
|
+
self.thread = threading.Thread(target=self._log_monitor)
|
58
|
+
self.thread.daemon = True
|
59
|
+
self.thread.start()
|
60
|
+
|
61
|
+
def _log_monitor(self):
|
62
|
+
from_date = _utc_now_no_tz() if not self.first_run else None
|
63
|
+
to_date = _utc_now_no_tz()
|
64
|
+
|
65
|
+
while self.running:
|
66
|
+
try:
|
67
|
+
for log in self.container.logs(
|
68
|
+
follow=False, since=from_date, until=to_date, stream=True
|
69
|
+
):
|
70
|
+
print(log.decode("utf-8"), end="")
|
71
|
+
time.sleep(0.1)
|
72
|
+
from_date = to_date
|
73
|
+
to_date = _utc_now_no_tz()
|
74
|
+
except KeyboardInterrupt:
|
75
|
+
print("Monitoring logs interrupted by user.")
|
76
|
+
_thread.interrupt_main()
|
77
|
+
break
|
78
|
+
except Exception as e:
|
79
|
+
print(f"Error monitoring logs: {e}")
|
80
|
+
break
|
81
|
+
|
82
|
+
def stop(self) -> None:
|
83
|
+
"""Stop monitoring the container logs"""
|
84
|
+
self.running = False
|
85
|
+
self.thread.join()
|
86
|
+
|
87
|
+
|
29
88
|
class DockerManager:
|
30
|
-
|
89
|
+
def __init__(self) -> None:
|
90
|
+
self.client: DockerClient = docker.from_env()
|
91
|
+
self.first_run = False
|
31
92
|
|
32
|
-
def
|
33
|
-
|
93
|
+
def get_lock(self) -> FileLock:
|
94
|
+
"""Get the file lock for this DockerManager instance."""
|
95
|
+
return _FILE_LOCK
|
34
96
|
|
35
97
|
@staticmethod
|
36
98
|
def is_docker_installed() -> bool:
|
@@ -46,9 +108,13 @@ class DockerManager:
|
|
46
108
|
print("Docker is not installed.")
|
47
109
|
return False
|
48
110
|
|
49
|
-
|
111
|
+
@staticmethod
|
112
|
+
def is_running() -> bool:
|
50
113
|
"""Check if Docker is running by pinging the Docker daemon."""
|
114
|
+
if not DockerManager.is_docker_installed():
|
115
|
+
return False
|
51
116
|
try:
|
117
|
+
# self.client.ping()
|
52
118
|
client = docker.from_env()
|
53
119
|
client.ping()
|
54
120
|
print("Docker is running.")
|
@@ -104,156 +170,344 @@ class DockerManager:
|
|
104
170
|
print(f"Error starting Docker: {str(e)}")
|
105
171
|
return False
|
106
172
|
|
107
|
-
def
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
if "linux" in result.stdout.lower():
|
116
|
-
return True
|
173
|
+
def validate_or_download_image(
|
174
|
+
self, image_name: str, tag: str = "latest", upgrade: bool = False
|
175
|
+
) -> None:
|
176
|
+
"""
|
177
|
+
Validate if the image exists, and if not, download it.
|
178
|
+
If upgrade is True, will pull the latest version even if image exists locally.
|
179
|
+
"""
|
180
|
+
print(f"Validating image {image_name}:{tag}...")
|
117
181
|
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
capture_output=True,
|
182
|
+
try:
|
183
|
+
local_image = self.client.images.get(f"{image_name}:{tag}")
|
184
|
+
print(f"Image {image_name}:{tag} is already available.")
|
185
|
+
|
186
|
+
if upgrade:
|
187
|
+
remote_image = self.client.images.get_registry_data(
|
188
|
+
f"{image_name}:{tag}"
|
126
189
|
)
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
190
|
+
remote_image_hash = remote_image.id
|
191
|
+
try:
|
192
|
+
remote_image_hash_from_local_image = DISK_CACHE.get(local_image.id)
|
193
|
+
except KeyboardInterrupt:
|
194
|
+
raise
|
195
|
+
except Exception:
|
196
|
+
remote_image_hash_from_local_image = None
|
197
|
+
import traceback
|
198
|
+
import warnings
|
199
|
+
|
200
|
+
stack = traceback.format_exc()
|
201
|
+
warnings.warn(
|
202
|
+
f"Error getting remote image hash from local image: {stack}"
|
203
|
+
)
|
204
|
+
if remote_image_hash_from_local_image == remote_image_hash:
|
205
|
+
print(f"Local image {image_name}:{tag} is up to date.")
|
206
|
+
return
|
207
|
+
|
208
|
+
# Quick check for latest version
|
209
|
+
|
210
|
+
print(f"Pulling newer version of {image_name}:{tag}...")
|
211
|
+
_ = self.client.images.pull(image_name, tag=tag)
|
212
|
+
print(f"Updated to newer version of {image_name}:{tag}")
|
213
|
+
local_image_hash = self.client.images.get(f"{image_name}:{tag}").id
|
214
|
+
DISK_CACHE.put(local_image_hash, remote_image_hash)
|
215
|
+
|
216
|
+
except docker.errors.ImageNotFound:
|
217
|
+
print(f"Image {image_name}:{tag} not found. Downloading...")
|
218
|
+
self.client.images.pull(image_name, tag=tag)
|
219
|
+
try:
|
220
|
+
local_image = self.client.images.get(f"{image_name}:{tag}")
|
221
|
+
local_image_hash = local_image.id
|
222
|
+
DISK_CACHE.put(local_image_hash, remote_image_hash)
|
223
|
+
print(f"Image {image_name}:{tag} downloaded successfully.")
|
224
|
+
except docker.errors.ImageNotFound:
|
225
|
+
import warnings
|
134
226
|
|
135
|
-
|
136
|
-
"""Get the file lock for this DockerManager instance."""
|
137
|
-
return _FILE_LOCK
|
227
|
+
warnings.warn(f"Image {image_name}:{tag} not found after download.")
|
138
228
|
|
139
|
-
def
|
140
|
-
"""
|
229
|
+
def tag_image(self, image_name: str, old_tag: str, new_tag: str) -> None:
|
230
|
+
"""
|
231
|
+
Tag an image with a new tag.
|
232
|
+
"""
|
233
|
+
image: Image = self.client.images.get(f"{image_name}:{old_tag}")
|
234
|
+
image.tag(image_name, new_tag)
|
235
|
+
print(f"Image {image_name}:{old_tag} tagged as {new_tag}.")
|
141
236
|
|
237
|
+
def _container_configs_match(
|
238
|
+
self,
|
239
|
+
container: Container,
|
240
|
+
command: str | None,
|
241
|
+
volumes: dict | None,
|
242
|
+
ports: dict | None,
|
243
|
+
) -> bool:
|
244
|
+
"""Compare if existing container has matching configuration"""
|
142
245
|
try:
|
143
|
-
if
|
144
|
-
|
246
|
+
# Check if container is using the same image
|
247
|
+
container_image_id = container.image.id
|
248
|
+
container_image_tags = container.image.tags
|
145
249
|
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
)
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
print("Tagging image")
|
163
|
-
|
164
|
-
tag_result = subprocess.run(
|
165
|
-
[
|
166
|
-
"docker",
|
167
|
-
"tag",
|
168
|
-
remote_image,
|
169
|
-
image_name,
|
170
|
-
],
|
171
|
-
capture_output=True,
|
172
|
-
text=True,
|
173
|
-
check=False,
|
174
|
-
)
|
175
|
-
if tag_result.returncode != 0:
|
176
|
-
print(f"Failed to tag image: {tag_result.stderr}")
|
250
|
+
# Simplified image comparison - just compare the IDs directly
|
251
|
+
if not container_image_tags:
|
252
|
+
print(f"Container using untagged image with ID: {container_image_id}")
|
253
|
+
else:
|
254
|
+
current_image = self.client.images.get(container_image_tags[0])
|
255
|
+
if container_image_id != current_image.id:
|
256
|
+
print(
|
257
|
+
f"Container using different image version. Container: {container_image_id}, Current: {current_image.id}"
|
258
|
+
)
|
259
|
+
return False
|
260
|
+
|
261
|
+
# Check command if specified
|
262
|
+
if command and container.attrs["Config"]["Cmd"] != command.split():
|
263
|
+
print(
|
264
|
+
f"Command mismatch: {container.attrs['Config']['Cmd']} != {command}"
|
265
|
+
)
|
177
266
|
return False
|
178
|
-
return True
|
179
|
-
except subprocess.CalledProcessError as e:
|
180
|
-
print(f"Failed to ensure image exists: {e}")
|
181
|
-
return False
|
182
267
|
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
return False
|
268
|
+
# Check volumes if specified
|
269
|
+
if volumes:
|
270
|
+
container_mounts = (
|
271
|
+
{
|
272
|
+
m["Source"]: {"bind": m["Destination"], "mode": m["Mode"]}
|
273
|
+
for m in container.attrs["Mounts"]
|
274
|
+
}
|
275
|
+
if container.attrs.get("Mounts")
|
276
|
+
else {}
|
277
|
+
)
|
194
278
|
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
|
204
|
-
|
279
|
+
for host_dir, mount in volumes.items():
|
280
|
+
if host_dir not in container_mounts:
|
281
|
+
print(f"Volume {host_dir} not found in container mounts.")
|
282
|
+
return False
|
283
|
+
if container_mounts[host_dir] != mount:
|
284
|
+
print(
|
285
|
+
f"Volume {host_dir} has different mount options: {container_mounts[host_dir]} != {mount}"
|
286
|
+
)
|
287
|
+
return False
|
288
|
+
|
289
|
+
# Check ports if specified
|
290
|
+
if ports:
|
291
|
+
container_ports = (
|
292
|
+
container.attrs["Config"]["ExposedPorts"]
|
293
|
+
if container.attrs["Config"].get("ExposedPorts")
|
294
|
+
else {}
|
295
|
+
)
|
296
|
+
container_port_bindings = (
|
297
|
+
container.attrs["HostConfig"]["PortBindings"]
|
298
|
+
if container.attrs["HostConfig"].get("PortBindings")
|
299
|
+
else {}
|
300
|
+
)
|
205
301
|
|
206
|
-
|
207
|
-
|
208
|
-
|
302
|
+
for container_port, host_port in ports.items():
|
303
|
+
port_key = f"{container_port}/tcp"
|
304
|
+
if port_key not in container_ports:
|
305
|
+
print(f"Container port {port_key} not found.")
|
306
|
+
return False
|
307
|
+
if not container_port_bindings.get(port_key, [{"HostPort": None}])[
|
308
|
+
0
|
309
|
+
]["HostPort"] == str(host_port):
|
310
|
+
print(f"Port {host_port} is not bound to {port_key}.")
|
311
|
+
return False
|
312
|
+
except KeyboardInterrupt:
|
313
|
+
raise
|
314
|
+
except docker.errors.NotFound:
|
315
|
+
print("Container not found.")
|
316
|
+
return False
|
317
|
+
except Exception as e:
|
318
|
+
stack = traceback.format_exc()
|
319
|
+
warnings.warn(f"Error checking container config: {e}\n{stack}")
|
320
|
+
return False
|
321
|
+
return True
|
209
322
|
|
210
323
|
def run_container(
|
211
324
|
self,
|
212
|
-
|
325
|
+
image_name: str,
|
326
|
+
tag: str,
|
327
|
+
container_name: str,
|
328
|
+
command: str | None = None,
|
213
329
|
volumes: dict[str, dict[str, str]] | None = None,
|
214
330
|
ports: dict[int, int] | None = None,
|
215
|
-
) ->
|
216
|
-
"""
|
331
|
+
) -> Container:
|
332
|
+
"""
|
333
|
+
Run a container from an image. If it already exists with matching config, start it.
|
334
|
+
If it exists with different config, remove and recreate it.
|
217
335
|
|
218
336
|
Args:
|
219
|
-
cmd: Command to run in the container
|
220
337
|
volumes: Dict mapping host paths to dicts with 'bind' and 'mode' keys
|
338
|
+
Example: {'/host/path': {'bind': '/container/path', 'mode': 'rw'}}
|
221
339
|
ports: Dict mapping host ports to container ports
|
340
|
+
Example: {8080: 80} maps host port 8080 to container port 80
|
222
341
|
"""
|
223
|
-
|
224
|
-
|
225
|
-
|
226
|
-
|
227
|
-
|
228
|
-
|
229
|
-
|
230
|
-
|
231
|
-
# Attach volumes if specified
|
232
|
-
docker_command += [
|
233
|
-
"--name",
|
234
|
-
self.container_name,
|
235
|
-
]
|
236
|
-
if ports:
|
237
|
-
for host_port, container_port in ports.items():
|
238
|
-
docker_command.extend(["-p", f"{host_port}:{container_port}"])
|
239
|
-
if volumes:
|
240
|
-
for host_path, mount_spec in volumes.items():
|
241
|
-
docker_command.extend(
|
242
|
-
["-v", f"{host_path}:{mount_spec['bind']}:{mount_spec['mode']}"]
|
342
|
+
image_name = f"{image_name}:{tag}"
|
343
|
+
try:
|
344
|
+
container: Container = self.client.containers.get(container_name)
|
345
|
+
|
346
|
+
# Check if configuration matches
|
347
|
+
if not self._container_configs_match(container, command, volumes, ports):
|
348
|
+
print(
|
349
|
+
f"Container {container_name} exists but with different configuration. Removing and recreating..."
|
243
350
|
)
|
351
|
+
container.remove(force=True)
|
352
|
+
raise docker.errors.NotFound("Container removed due to config mismatch")
|
353
|
+
print(f"Container {container_name} found with matching configuration.")
|
354
|
+
|
355
|
+
# Existing container with matching config - handle various states
|
356
|
+
if container.status == "running":
|
357
|
+
print(f"Container {container_name} is already running.")
|
358
|
+
elif container.status == "exited":
|
359
|
+
print(f"Starting existing container {container_name}.")
|
360
|
+
container.start()
|
361
|
+
elif container.status == "restarting":
|
362
|
+
print(f"Waiting for container {container_name} to restart...")
|
363
|
+
timeout = 10
|
364
|
+
container.wait(timeout=10)
|
365
|
+
if container.status == "running":
|
366
|
+
print(f"Container {container_name} has restarted.")
|
367
|
+
else:
|
368
|
+
print(
|
369
|
+
f"Container {container_name} did not restart within {timeout} seconds."
|
370
|
+
)
|
371
|
+
container.stop()
|
372
|
+
print(f"Container {container_name} has been stopped.")
|
373
|
+
container.start()
|
374
|
+
elif container.status == "paused":
|
375
|
+
print(f"Resuming existing container {container_name}.")
|
376
|
+
container.unpause()
|
377
|
+
else:
|
378
|
+
print(f"Unknown container status: {container.status}")
|
379
|
+
print(f"Starting existing container {container_name}.")
|
380
|
+
self.first_run = True
|
381
|
+
container.start()
|
382
|
+
except docker.errors.NotFound:
|
383
|
+
print(f"Creating and starting {container_name}")
|
384
|
+
container = self.client.containers.run(
|
385
|
+
image_name,
|
386
|
+
command,
|
387
|
+
name=container_name,
|
388
|
+
detach=True,
|
389
|
+
tty=True,
|
390
|
+
volumes=volumes,
|
391
|
+
ports=ports,
|
392
|
+
)
|
393
|
+
return container
|
244
394
|
|
245
|
-
|
246
|
-
|
247
|
-
|
248
|
-
|
249
|
-
|
250
|
-
|
395
|
+
def attach_and_run(self, container: Container | str) -> RunningContainer:
|
396
|
+
"""
|
397
|
+
Attach to a running container and monitor its logs in a background thread.
|
398
|
+
Returns a RunningContainer object that can be used to stop monitoring.
|
399
|
+
"""
|
400
|
+
if isinstance(container, str):
|
401
|
+
container = self.get_container(container)
|
402
|
+
|
403
|
+
print(f"Attaching to container {container.name}...")
|
404
|
+
|
405
|
+
first_run = self.first_run
|
406
|
+
self.first_run = False
|
407
|
+
|
408
|
+
return RunningContainer(container, first_run)
|
409
|
+
|
410
|
+
def suspend_container(self, container: Container | str) -> None:
|
411
|
+
"""
|
412
|
+
Suspend (pause) the container.
|
413
|
+
"""
|
414
|
+
if isinstance(container, str):
|
415
|
+
container_name = container
|
416
|
+
container = self.get_container(container)
|
417
|
+
if not container:
|
418
|
+
print(f"Could not put container {container_name} to sleep.")
|
419
|
+
return
|
420
|
+
try:
|
421
|
+
container.pause()
|
422
|
+
print(f"Container {container.name} has been suspended.")
|
423
|
+
except KeyboardInterrupt:
|
424
|
+
print(f"Container {container.name} interrupted by keyboard interrupt.")
|
425
|
+
except Exception as e:
|
426
|
+
print(f"Failed to suspend container {container.name}: {e}")
|
427
|
+
|
428
|
+
def resume_container(self, container: Container | str) -> None:
|
429
|
+
"""
|
430
|
+
Resume (unpause) the container.
|
431
|
+
"""
|
432
|
+
if isinstance(container, str):
|
433
|
+
container = self.get_container(container)
|
434
|
+
try:
|
435
|
+
container.unpause()
|
436
|
+
print(f"Container {container.name} has been resumed.")
|
437
|
+
except Exception as e:
|
438
|
+
print(f"Failed to resume container {container.name}: {e}")
|
439
|
+
|
440
|
+
def get_container(self, container_name: str) -> Container:
|
441
|
+
"""
|
442
|
+
Get a container by name.
|
443
|
+
"""
|
444
|
+
try:
|
445
|
+
return self.client.containers.get(container_name)
|
446
|
+
except docker.errors.NotFound:
|
447
|
+
print(f"Container {container_name} not found.")
|
448
|
+
raise
|
449
|
+
|
450
|
+
def is_container_running(self, container_name: str) -> bool:
|
451
|
+
"""
|
452
|
+
Check if a container is running.
|
453
|
+
"""
|
454
|
+
try:
|
455
|
+
container = self.client.containers.get(container_name)
|
456
|
+
return container.status == "running"
|
457
|
+
except docker.errors.NotFound:
|
458
|
+
print(f"Container {container_name} not found.")
|
459
|
+
return False
|
460
|
+
|
461
|
+
|
462
|
+
def main():
|
463
|
+
# Register SIGINT handler
|
464
|
+
# signal.signal(signal.SIGINT, handle_sigint)
|
465
|
+
|
466
|
+
docker_manager = DockerManager()
|
251
467
|
|
252
|
-
|
468
|
+
# Parameters
|
469
|
+
image_name = "python"
|
470
|
+
tag = "3.10-slim"
|
471
|
+
# new_tag = "my-python"
|
472
|
+
container_name = "my-python-container"
|
473
|
+
command = "python -m http.server"
|
253
474
|
|
254
|
-
|
255
|
-
|
256
|
-
|
475
|
+
try:
|
476
|
+
# Step 1: Validate or download the image
|
477
|
+
docker_manager.validate_or_download_image(image_name, tag, upgrade=True)
|
478
|
+
|
479
|
+
# Step 2: Tag the image
|
480
|
+
# docker_manager.tag_image(image_name, tag, new_tag)
|
481
|
+
|
482
|
+
# Step 3: Run the container
|
483
|
+
container = docker_manager.run_container(
|
484
|
+
image_name, tag, container_name, command
|
257
485
|
)
|
258
486
|
|
259
|
-
|
487
|
+
# Step 4: Attach and monitor the container logs
|
488
|
+
running_container = docker_manager.attach_and_run(container)
|
489
|
+
|
490
|
+
# Wait for keyboard interrupt
|
491
|
+
while True:
|
492
|
+
time.sleep(0.1)
|
493
|
+
|
494
|
+
except KeyboardInterrupt:
|
495
|
+
print("\nStopping container...")
|
496
|
+
running_container.stop()
|
497
|
+
container = docker_manager.get_container(container_name)
|
498
|
+
docker_manager.suspend_container(container)
|
499
|
+
|
500
|
+
try:
|
501
|
+
# Suspend and resume the container
|
502
|
+
container = docker_manager.get_container(container_name)
|
503
|
+
docker_manager.suspend_container(container)
|
504
|
+
|
505
|
+
input("Press Enter to resume the container...")
|
506
|
+
|
507
|
+
docker_manager.resume_container(container)
|
508
|
+
except Exception as e:
|
509
|
+
print(f"An error occurred: {e}")
|
510
|
+
|
511
|
+
|
512
|
+
if __name__ == "__main__":
|
513
|
+
main()
|