bsm-cli 1.6.0b3__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.
- bsm_cli/__init__.py +0 -0
- bsm_cli/__main__.py +88 -0
- bsm_cli/account.py +75 -0
- bsm_cli/addon.py +362 -0
- bsm_cli/allowlist.py +229 -0
- bsm_cli/auth.py +106 -0
- bsm_cli/backup.py +263 -0
- bsm_cli/bans.py +190 -0
- bsm_cli/config.py +89 -0
- bsm_cli/content.py +21 -0
- bsm_cli/decorators.py +138 -0
- bsm_cli/main_menus.py +293 -0
- bsm_cli/permissions.py +191 -0
- bsm_cli/player.py +59 -0
- bsm_cli/plugins.py +296 -0
- bsm_cli/properties.py +197 -0
- bsm_cli/server.py +471 -0
- bsm_cli/system.py +142 -0
- bsm_cli/users.py +245 -0
- bsm_cli/world.py +171 -0
- bsm_cli-1.6.0b3.dist-info/METADATA +77 -0
- bsm_cli-1.6.0b3.dist-info/RECORD +26 -0
- bsm_cli-1.6.0b3.dist-info/WHEEL +5 -0
- bsm_cli-1.6.0b3.dist-info/entry_points.txt +2 -0
- bsm_cli-1.6.0b3.dist-info/licenses/LICENSE +21 -0
- bsm_cli-1.6.0b3.dist-info/top_level.txt +1 -0
bsm_cli/server.py
ADDED
|
@@ -0,0 +1,471 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import os
|
|
3
|
+
|
|
4
|
+
import click
|
|
5
|
+
import questionary
|
|
6
|
+
from bsm_cli.allowlist import interactive_allowlist_workflow
|
|
7
|
+
from bsm_cli.decorators import monitor_task, pass_async_context
|
|
8
|
+
from bsm_cli.permissions import interactive_permissions_workflow
|
|
9
|
+
from bsm_cli.properties import interactive_properties_workflow
|
|
10
|
+
|
|
11
|
+
from bsm_api_client.exceptions import AuthError
|
|
12
|
+
from bsm_api_client.models import CommandPayload, InstallServerPayload
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _print_server_table(servers):
|
|
16
|
+
"""Prints a formatted table of server information to the console."""
|
|
17
|
+
header = f"{'SERVER NAME':<25} {'STATUS':<15} {'VERSION'}"
|
|
18
|
+
click.secho(header, bold=True)
|
|
19
|
+
click.echo("-" * 65)
|
|
20
|
+
|
|
21
|
+
if not servers:
|
|
22
|
+
click.echo(" No servers found.")
|
|
23
|
+
else:
|
|
24
|
+
for server_data in servers:
|
|
25
|
+
name = getattr(server_data, "name", "N/A")
|
|
26
|
+
status = getattr(server_data, "status", "UNKNOWN").upper()
|
|
27
|
+
version = getattr(server_data, "version", "UNKNOWN")
|
|
28
|
+
|
|
29
|
+
color_map = {
|
|
30
|
+
"RUNNING": "green",
|
|
31
|
+
"STOPPED": "red",
|
|
32
|
+
"STARTING": "yellow",
|
|
33
|
+
"STOPPING": "yellow",
|
|
34
|
+
"INSTALLING": "bright_cyan",
|
|
35
|
+
"UPDATING": "bright_cyan",
|
|
36
|
+
"INSTALLED": "bright_magenta",
|
|
37
|
+
"UPDATED": "bright_magenta",
|
|
38
|
+
"UNKNOWN": "bright_black",
|
|
39
|
+
}
|
|
40
|
+
status_color = color_map.get(status, "red")
|
|
41
|
+
|
|
42
|
+
status_styled = click.style(f"{status:<10}", fg=status_color)
|
|
43
|
+
name_styled = click.style(name, fg="cyan")
|
|
44
|
+
version_styled = click.style(version, fg="bright_white")
|
|
45
|
+
|
|
46
|
+
click.echo(f" {name_styled:<38} {status_styled:<20} {version_styled}")
|
|
47
|
+
click.echo("-" * 65)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@click.group()
|
|
51
|
+
def server():
|
|
52
|
+
"""Manages servers."""
|
|
53
|
+
pass
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
@server.command("list")
|
|
57
|
+
@click.option(
|
|
58
|
+
"--loop", is_flag=True, help="Continuously refresh server statuses every 5 seconds."
|
|
59
|
+
)
|
|
60
|
+
@click.option("--server-name", help="Display status for only a specific server.")
|
|
61
|
+
@pass_async_context
|
|
62
|
+
async def list_servers(ctx, loop, server_name): # noqa: C901
|
|
63
|
+
"""Lists all configured Bedrock servers and their current operational status."""
|
|
64
|
+
client = ctx.obj.get("client")
|
|
65
|
+
if not client:
|
|
66
|
+
click.secho("You are not logged in.", fg="red")
|
|
67
|
+
return
|
|
68
|
+
|
|
69
|
+
async def _display_status():
|
|
70
|
+
response = await client.async_get_servers()
|
|
71
|
+
all_servers = response.servers or []
|
|
72
|
+
|
|
73
|
+
if server_name:
|
|
74
|
+
servers_to_show = [
|
|
75
|
+
s for s in all_servers if getattr(s, "name", "") == server_name
|
|
76
|
+
]
|
|
77
|
+
else:
|
|
78
|
+
servers_to_show = all_servers
|
|
79
|
+
|
|
80
|
+
_print_server_table(servers_to_show)
|
|
81
|
+
|
|
82
|
+
try:
|
|
83
|
+
if loop:
|
|
84
|
+
# Initial display
|
|
85
|
+
click.clear()
|
|
86
|
+
click.secho(
|
|
87
|
+
"--- Bedrock Servers Status (Press CTRL+C to exit) ---",
|
|
88
|
+
fg="magenta",
|
|
89
|
+
bold=True,
|
|
90
|
+
)
|
|
91
|
+
await _display_status()
|
|
92
|
+
|
|
93
|
+
# Try to use WebSocket for updates
|
|
94
|
+
try:
|
|
95
|
+
ws_client = await client.websocket_connect()
|
|
96
|
+
|
|
97
|
+
async with ws_client:
|
|
98
|
+
# Subscribe to multiple topics for comprehensive status updates
|
|
99
|
+
await ws_client.subscribe("event:after_server_statuses_updated")
|
|
100
|
+
await ws_client.subscribe("event:after_server_start")
|
|
101
|
+
await ws_client.subscribe("event:after_server_stop")
|
|
102
|
+
await ws_client.subscribe("event:after_server_updated")
|
|
103
|
+
await ws_client.subscribe("event:after_delete_server_data")
|
|
104
|
+
|
|
105
|
+
# Listen for updates
|
|
106
|
+
async for _ in ws_client.listen():
|
|
107
|
+
click.clear()
|
|
108
|
+
click.secho(
|
|
109
|
+
"--- Bedrock Servers Status (Press CTRL+C to exit) ---",
|
|
110
|
+
fg="magenta",
|
|
111
|
+
bold=True,
|
|
112
|
+
)
|
|
113
|
+
await _display_status()
|
|
114
|
+
|
|
115
|
+
except (KeyboardInterrupt, click.Abort):
|
|
116
|
+
raise
|
|
117
|
+
except AuthError:
|
|
118
|
+
click.secho(
|
|
119
|
+
"WebSocket authentication failed. Attempting to refresh token...",
|
|
120
|
+
fg="yellow",
|
|
121
|
+
)
|
|
122
|
+
try:
|
|
123
|
+
await client.authenticate()
|
|
124
|
+
# Retry WebSocket once
|
|
125
|
+
ws_client = await client.websocket_connect()
|
|
126
|
+
async with ws_client:
|
|
127
|
+
await ws_client.subscribe("event:after_server_statuses_updated")
|
|
128
|
+
async for _ in ws_client.listen():
|
|
129
|
+
click.clear()
|
|
130
|
+
click.secho(
|
|
131
|
+
"--- Bedrock Servers Status (Press CTRL+C to exit) ---",
|
|
132
|
+
fg="magenta",
|
|
133
|
+
bold=True,
|
|
134
|
+
)
|
|
135
|
+
await _display_status()
|
|
136
|
+
except Exception as e:
|
|
137
|
+
click.secho(
|
|
138
|
+
f"WebSocket retry failed ({e}), falling back to polling...",
|
|
139
|
+
fg="yellow",
|
|
140
|
+
)
|
|
141
|
+
await asyncio.sleep(2)
|
|
142
|
+
while True:
|
|
143
|
+
click.clear()
|
|
144
|
+
click.secho(
|
|
145
|
+
"--- Bedrock Servers Status (Press CTRL+C to exit) ---",
|
|
146
|
+
fg="magenta",
|
|
147
|
+
bold=True,
|
|
148
|
+
)
|
|
149
|
+
await _display_status()
|
|
150
|
+
await asyncio.sleep(5)
|
|
151
|
+
except Exception as e:
|
|
152
|
+
# Fallback to polling if WebSocket fails
|
|
153
|
+
click.secho(
|
|
154
|
+
f"WebSocket connection failed ({e}), falling back to polling...",
|
|
155
|
+
fg="yellow",
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
# If we are here, WebSocket failed or closed. Fallback to polling.
|
|
159
|
+
await asyncio.sleep(2)
|
|
160
|
+
while True:
|
|
161
|
+
click.clear()
|
|
162
|
+
click.secho(
|
|
163
|
+
"--- Bedrock Servers Status (Press CTRL+C to exit) ---",
|
|
164
|
+
fg="magenta",
|
|
165
|
+
bold=True,
|
|
166
|
+
)
|
|
167
|
+
try:
|
|
168
|
+
await _display_status()
|
|
169
|
+
except Exception as e:
|
|
170
|
+
click.secho(f"Error refreshing status: {e}", fg="red")
|
|
171
|
+
await asyncio.sleep(5)
|
|
172
|
+
else:
|
|
173
|
+
if not server_name:
|
|
174
|
+
click.secho("--- Bedrock Servers Status ---", fg="magenta", bold=True)
|
|
175
|
+
await _display_status()
|
|
176
|
+
|
|
177
|
+
except (KeyboardInterrupt, click.Abort):
|
|
178
|
+
click.secho("\nExiting status monitor.", fg="green")
|
|
179
|
+
except Exception as e:
|
|
180
|
+
click.secho(f"An error occurred: {e}", fg="red")
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
@server.command("start")
|
|
184
|
+
@click.option(
|
|
185
|
+
"-s", "--server", "server_name", required=True, help="Name of the server to start."
|
|
186
|
+
)
|
|
187
|
+
@pass_async_context
|
|
188
|
+
async def start_server(ctx, server_name: str):
|
|
189
|
+
"""Starts a specific Bedrock server instance."""
|
|
190
|
+
client = ctx.obj.get("client")
|
|
191
|
+
if not client:
|
|
192
|
+
click.secho("You are not logged in.", fg="red")
|
|
193
|
+
return
|
|
194
|
+
|
|
195
|
+
click.echo(f"Attempting to start server '{server_name}'...")
|
|
196
|
+
try:
|
|
197
|
+
response = await client.async_start_server(server_name)
|
|
198
|
+
if response.task_id:
|
|
199
|
+
await monitor_task(
|
|
200
|
+
client,
|
|
201
|
+
response.task_id,
|
|
202
|
+
"Server started successfully",
|
|
203
|
+
"Failed to start server",
|
|
204
|
+
)
|
|
205
|
+
elif response.status == "success":
|
|
206
|
+
click.secho(f"Server '{server_name}' started successfully.", fg="green")
|
|
207
|
+
else:
|
|
208
|
+
click.secho(f"Failed to start server: {response.message}", fg="red")
|
|
209
|
+
except Exception as e:
|
|
210
|
+
click.secho(f"Failed to start server: {e}", fg="red")
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
@server.command("stop")
|
|
214
|
+
@click.option(
|
|
215
|
+
"-s", "--server", "server_name", required=True, help="Name of the server to stop."
|
|
216
|
+
)
|
|
217
|
+
@pass_async_context
|
|
218
|
+
async def stop_server(ctx, server_name: str):
|
|
219
|
+
"""Sends a graceful stop command to a running Bedrock server."""
|
|
220
|
+
client = ctx.obj.get("client")
|
|
221
|
+
if not client:
|
|
222
|
+
click.secho("You are not logged in.", fg="red")
|
|
223
|
+
return
|
|
224
|
+
|
|
225
|
+
click.echo(f"Attempting to stop server '{server_name}'...")
|
|
226
|
+
try:
|
|
227
|
+
response = await client.async_stop_server(server_name)
|
|
228
|
+
if response.task_id:
|
|
229
|
+
await monitor_task(
|
|
230
|
+
client,
|
|
231
|
+
response.task_id,
|
|
232
|
+
"Server stopped successfully",
|
|
233
|
+
"Failed to stop server",
|
|
234
|
+
)
|
|
235
|
+
elif response.status == "success":
|
|
236
|
+
click.secho(f"Stop signal sent to server '{server_name}'.", fg="green")
|
|
237
|
+
else:
|
|
238
|
+
click.secho(f"Failed to stop server: {response.message}", fg="red")
|
|
239
|
+
except Exception as e:
|
|
240
|
+
click.secho(f"Failed to stop server: {e}", fg="red")
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
@server.command("restart")
|
|
244
|
+
@click.option(
|
|
245
|
+
"-s",
|
|
246
|
+
"--server",
|
|
247
|
+
"server_name",
|
|
248
|
+
required=True,
|
|
249
|
+
help="Name of the server to restart.",
|
|
250
|
+
)
|
|
251
|
+
@pass_async_context
|
|
252
|
+
async def restart_server(ctx, server_name: str):
|
|
253
|
+
"""Gracefully restarts a specific Bedrock server."""
|
|
254
|
+
client = ctx.obj.get("client")
|
|
255
|
+
if not client:
|
|
256
|
+
click.secho("You are not logged in.", fg="red")
|
|
257
|
+
return
|
|
258
|
+
|
|
259
|
+
click.echo(f"Attempting to restart server '{server_name}'...")
|
|
260
|
+
try:
|
|
261
|
+
response = await client.async_restart_server(server_name)
|
|
262
|
+
if response.task_id:
|
|
263
|
+
await monitor_task(
|
|
264
|
+
client,
|
|
265
|
+
response.task_id,
|
|
266
|
+
"Server restarted successfully",
|
|
267
|
+
"Failed to restart server",
|
|
268
|
+
)
|
|
269
|
+
elif response.status == "success":
|
|
270
|
+
click.secho(f"Restart signal sent to server '{server_name}'.", fg="green")
|
|
271
|
+
else:
|
|
272
|
+
click.secho(f"Failed to restart server: {response.message}", fg="red")
|
|
273
|
+
except Exception as e:
|
|
274
|
+
click.secho(f"Failed to restart server: {e}", fg="red")
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
@server.command("install")
|
|
278
|
+
@click.pass_context
|
|
279
|
+
async def install(ctx): # noqa: C901
|
|
280
|
+
"""Guides you through installing and configuring a new Bedrock server instance."""
|
|
281
|
+
client = ctx.obj.get("client")
|
|
282
|
+
if not client:
|
|
283
|
+
click.secho("You are not logged in.", fg="red")
|
|
284
|
+
return
|
|
285
|
+
|
|
286
|
+
try:
|
|
287
|
+
click.secho("--- New Bedrock Server Installation ---", bold=True)
|
|
288
|
+
server_name = await questionary.text(
|
|
289
|
+
"Enter a name for the new server:"
|
|
290
|
+
).ask_async()
|
|
291
|
+
if not server_name:
|
|
292
|
+
raise click.Abort()
|
|
293
|
+
|
|
294
|
+
target_version = await questionary.text(
|
|
295
|
+
"Enter server version (e.g., LATEST, PREVIEW, CUSTOM, 1.20.81.01):",
|
|
296
|
+
default="LATEST",
|
|
297
|
+
).ask_async()
|
|
298
|
+
if not target_version:
|
|
299
|
+
raise click.Abort()
|
|
300
|
+
|
|
301
|
+
server_zip_path = None
|
|
302
|
+
if target_version.upper() == "CUSTOM":
|
|
303
|
+
response = await client.async_get_custom_zips()
|
|
304
|
+
available_files = response.custom_zips
|
|
305
|
+
|
|
306
|
+
if not available_files:
|
|
307
|
+
click.secho(
|
|
308
|
+
"No custom server ZIP files found in the content/custom directory.",
|
|
309
|
+
fg="red",
|
|
310
|
+
)
|
|
311
|
+
return
|
|
312
|
+
|
|
313
|
+
file_map = {os.path.basename(f): f for f in available_files}
|
|
314
|
+
choices = sorted(list(file_map.keys())) + ["Cancel"]
|
|
315
|
+
selection = await questionary.select(
|
|
316
|
+
"Select a custom server ZIP to install:", choices=choices
|
|
317
|
+
).ask_async()
|
|
318
|
+
|
|
319
|
+
if not selection or selection == "Cancel":
|
|
320
|
+
raise click.Abort()
|
|
321
|
+
server_zip_path = file_map[selection]
|
|
322
|
+
|
|
323
|
+
overwrite = await questionary.confirm(
|
|
324
|
+
"Overwrite existing server if it exists?", default=False
|
|
325
|
+
).ask_async()
|
|
326
|
+
|
|
327
|
+
click.echo(f"\nInstalling server '{server_name}' version '{target_version}'...")
|
|
328
|
+
|
|
329
|
+
payload = InstallServerPayload(
|
|
330
|
+
server_name=server_name,
|
|
331
|
+
server_version=target_version,
|
|
332
|
+
overwrite=overwrite,
|
|
333
|
+
server_zip_path=server_zip_path,
|
|
334
|
+
)
|
|
335
|
+
install_result = await client.async_install_new_server(payload)
|
|
336
|
+
|
|
337
|
+
if install_result.task_id:
|
|
338
|
+
await monitor_task(
|
|
339
|
+
client,
|
|
340
|
+
install_result.task_id,
|
|
341
|
+
"Server installation completed successfully",
|
|
342
|
+
"Installation failed",
|
|
343
|
+
)
|
|
344
|
+
elif install_result.status == "success":
|
|
345
|
+
click.secho("Server files installed successfully.", fg="green")
|
|
346
|
+
else:
|
|
347
|
+
click.secho(f"Failed to install server: {install_result.message}", fg="red")
|
|
348
|
+
return
|
|
349
|
+
|
|
350
|
+
await interactive_properties_workflow(client, server_name)
|
|
351
|
+
if await questionary.confirm(
|
|
352
|
+
"\nConfigure the allowlist now?", default=False
|
|
353
|
+
).ask_async():
|
|
354
|
+
await interactive_allowlist_workflow(client, server_name)
|
|
355
|
+
if await questionary.confirm(
|
|
356
|
+
"\nConfigure player permissions now?", default=False
|
|
357
|
+
).ask_async():
|
|
358
|
+
await interactive_permissions_workflow(client, server_name)
|
|
359
|
+
|
|
360
|
+
click.secho(
|
|
361
|
+
"\nInstallation and initial configuration complete!", fg="green", bold=True
|
|
362
|
+
)
|
|
363
|
+
|
|
364
|
+
if await questionary.confirm(
|
|
365
|
+
f"Start server '{server_name}' now?", default=True
|
|
366
|
+
).ask_async():
|
|
367
|
+
await ctx.invoke(start_server, server_name=server_name)
|
|
368
|
+
|
|
369
|
+
except Exception as e:
|
|
370
|
+
click.secho(f"An application error occurred: {e}", fg="red")
|
|
371
|
+
|
|
372
|
+
|
|
373
|
+
@server.command("update")
|
|
374
|
+
@click.option(
|
|
375
|
+
"-s", "--server", "server_name", required=True, help="Name of the server to update."
|
|
376
|
+
)
|
|
377
|
+
@pass_async_context
|
|
378
|
+
async def update(ctx, server_name: str):
|
|
379
|
+
"""Checks for and applies updates to an existing Bedrock server."""
|
|
380
|
+
client = ctx.obj.get("client")
|
|
381
|
+
if not client:
|
|
382
|
+
click.secho("You are not logged in.", fg="red")
|
|
383
|
+
return
|
|
384
|
+
|
|
385
|
+
click.echo(f"Checking for updates for server '{server_name}'...")
|
|
386
|
+
try:
|
|
387
|
+
response = await client.async_update_server(server_name)
|
|
388
|
+
if response.task_id:
|
|
389
|
+
await monitor_task(
|
|
390
|
+
client,
|
|
391
|
+
response.task_id,
|
|
392
|
+
"Server update completed successfully",
|
|
393
|
+
"Failed to update server",
|
|
394
|
+
)
|
|
395
|
+
elif response.status == "success":
|
|
396
|
+
click.secho("Update check complete.", fg="green")
|
|
397
|
+
else:
|
|
398
|
+
click.secho(f"Failed to update server: {response.message}", fg="red")
|
|
399
|
+
except Exception as e:
|
|
400
|
+
click.secho(f"A server update error occurred: {e}", fg="red")
|
|
401
|
+
|
|
402
|
+
|
|
403
|
+
@server.command("delete")
|
|
404
|
+
@click.option(
|
|
405
|
+
"-s", "--server", "server_name", required=True, help="Name of the server to delete."
|
|
406
|
+
)
|
|
407
|
+
@click.option("-y", "--yes", is_flag=True, help="Bypass the confirmation prompt.")
|
|
408
|
+
@pass_async_context
|
|
409
|
+
async def delete_server(ctx, server_name: str, yes: bool):
|
|
410
|
+
"""Deletes all data for a server, including world, configs, and backups."""
|
|
411
|
+
client = ctx.obj.get("client")
|
|
412
|
+
if not client:
|
|
413
|
+
click.secho("You are not logged in.", fg="red")
|
|
414
|
+
return
|
|
415
|
+
|
|
416
|
+
if not yes:
|
|
417
|
+
click.secho(
|
|
418
|
+
f"WARNING: This will permanently delete all data for server '{server_name}',\n"
|
|
419
|
+
"including the installation, worlds, and all associated backups.",
|
|
420
|
+
fg="red",
|
|
421
|
+
bold=True,
|
|
422
|
+
)
|
|
423
|
+
click.confirm(
|
|
424
|
+
f"\nAre you absolutely sure you want to delete '{server_name}'?", abort=True
|
|
425
|
+
)
|
|
426
|
+
|
|
427
|
+
click.echo(f"Proceeding with deletion of server '{server_name}'...")
|
|
428
|
+
try:
|
|
429
|
+
response = await client.async_delete_server(server_name)
|
|
430
|
+
if response.task_id:
|
|
431
|
+
await monitor_task(
|
|
432
|
+
client,
|
|
433
|
+
response.task_id,
|
|
434
|
+
"Server deleted successfully",
|
|
435
|
+
"Failed to delete server",
|
|
436
|
+
)
|
|
437
|
+
elif response.status == "success":
|
|
438
|
+
click.secho(
|
|
439
|
+
f"Server '{server_name}' and all its data have been deleted.",
|
|
440
|
+
fg="green",
|
|
441
|
+
)
|
|
442
|
+
else:
|
|
443
|
+
click.secho(f"Failed to delete server: {response.message}", fg="red")
|
|
444
|
+
except Exception as e:
|
|
445
|
+
click.secho(f"Failed to delete server: {e}", fg="red")
|
|
446
|
+
|
|
447
|
+
|
|
448
|
+
@server.command("send-command")
|
|
449
|
+
@click.option(
|
|
450
|
+
"-s", "--server", "server_name", required=True, help="Name of the target server."
|
|
451
|
+
)
|
|
452
|
+
@click.argument("command_parts", nargs=-1, required=True)
|
|
453
|
+
@click.pass_context
|
|
454
|
+
async def send_command(ctx, server_name: str, command_parts: str):
|
|
455
|
+
"""Sends a command to a running Bedrock server's console."""
|
|
456
|
+
client = ctx.obj.get("client")
|
|
457
|
+
if not client:
|
|
458
|
+
click.secho("You are not logged in.", fg="red")
|
|
459
|
+
return
|
|
460
|
+
|
|
461
|
+
command_string = " ".join(command_parts)
|
|
462
|
+
click.echo(f"Sending command to '{server_name}': {command_string}")
|
|
463
|
+
try:
|
|
464
|
+
payload = CommandPayload(command=command_string)
|
|
465
|
+
response = await client.async_send_server_command(server_name, payload)
|
|
466
|
+
if response.status == "success":
|
|
467
|
+
click.secho("Command sent successfully.", fg="green")
|
|
468
|
+
else:
|
|
469
|
+
click.secho(f"Failed to send command: {response.message}", fg="red")
|
|
470
|
+
except Exception as e:
|
|
471
|
+
click.secho(f"Failed to send command: {e}", fg="red")
|
bsm_cli/system.py
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import time
|
|
2
|
+
|
|
3
|
+
import click
|
|
4
|
+
import questionary
|
|
5
|
+
|
|
6
|
+
from bsm_api_client.models import ServerSettingItemPayload
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@click.group()
|
|
10
|
+
def system():
|
|
11
|
+
"""Manages server OS-level resource monitoring and settings."""
|
|
12
|
+
pass
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@system.command("settings")
|
|
16
|
+
@click.option(
|
|
17
|
+
"-s",
|
|
18
|
+
"--server",
|
|
19
|
+
"server_name",
|
|
20
|
+
required=True,
|
|
21
|
+
help="Name of the server to configure.",
|
|
22
|
+
)
|
|
23
|
+
@click.pass_context
|
|
24
|
+
async def server_settings(ctx, server_name: str):
|
|
25
|
+
"""Configures autostart and autoupdate settings for a Bedrock server."""
|
|
26
|
+
client = ctx.obj.get("client")
|
|
27
|
+
if not client:
|
|
28
|
+
click.secho("You are not logged in.", fg="red")
|
|
29
|
+
return
|
|
30
|
+
|
|
31
|
+
click.secho(
|
|
32
|
+
f"Starting interactive settings configuration for '{server_name}'...",
|
|
33
|
+
fg="yellow",
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
settings_response = await client.async_get_server_settings(server_name)
|
|
37
|
+
if settings_response.status != "success" or not settings_response.settings:
|
|
38
|
+
click.secho(
|
|
39
|
+
f"Failed to fetch server settings: {settings_response.message}", fg="red"
|
|
40
|
+
)
|
|
41
|
+
return
|
|
42
|
+
|
|
43
|
+
settings = settings_response.settings
|
|
44
|
+
|
|
45
|
+
current_autoupdate = settings.get("settings", {}).get("autoupdate", False)
|
|
46
|
+
current_autostart = settings.get("settings", {}).get("autostart", False)
|
|
47
|
+
|
|
48
|
+
click.secho(
|
|
49
|
+
f"\n--- Interactive Settings Configuration for '{server_name}' ---", bold=True
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
autoupdate_choice = await questionary.confirm(
|
|
53
|
+
"Enable check for updates when the server starts?", default=current_autoupdate
|
|
54
|
+
).ask_async()
|
|
55
|
+
|
|
56
|
+
if autoupdate_choice is not None and autoupdate_choice != current_autoupdate:
|
|
57
|
+
payload = ServerSettingItemPayload(
|
|
58
|
+
key="settings.autoupdate", value=autoupdate_choice
|
|
59
|
+
)
|
|
60
|
+
response = await client.async_set_server_setting(server_name, payload)
|
|
61
|
+
if response.status == "success":
|
|
62
|
+
click.secho(
|
|
63
|
+
f"Autoupdate setting configured to '{autoupdate_choice}'.", fg="green"
|
|
64
|
+
)
|
|
65
|
+
else:
|
|
66
|
+
click.secho(f"Failed to set autoupdate: {response.message}", fg="red")
|
|
67
|
+
|
|
68
|
+
autostart_choice = await questionary.confirm(
|
|
69
|
+
"Enable the server to start automatically when the manager starts?",
|
|
70
|
+
default=current_autostart,
|
|
71
|
+
).ask_async()
|
|
72
|
+
|
|
73
|
+
if autostart_choice is not None and autostart_choice != current_autostart:
|
|
74
|
+
payload = ServerSettingItemPayload(
|
|
75
|
+
key="settings.autostart", value=autostart_choice
|
|
76
|
+
)
|
|
77
|
+
response = await client.async_set_server_setting(server_name, payload)
|
|
78
|
+
if response.status == "success":
|
|
79
|
+
click.secho(
|
|
80
|
+
f"Autostart setting configured to '{autostart_choice}'.", fg="green"
|
|
81
|
+
)
|
|
82
|
+
else:
|
|
83
|
+
click.secho(f"Failed to set autostart: {response.message}", fg="red")
|
|
84
|
+
|
|
85
|
+
click.secho("\nSettings configuration complete.", fg="green", bold=True)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
@system.command("monitor")
|
|
89
|
+
@click.option(
|
|
90
|
+
"-s",
|
|
91
|
+
"--server",
|
|
92
|
+
"server_name",
|
|
93
|
+
required=True,
|
|
94
|
+
help="Name of the server to monitor.",
|
|
95
|
+
)
|
|
96
|
+
@click.pass_context
|
|
97
|
+
async def monitor_usage(ctx, server_name: str):
|
|
98
|
+
"""Continuously monitors CPU and memory usage of a specific server process."""
|
|
99
|
+
client = ctx.obj.get("client")
|
|
100
|
+
if not client:
|
|
101
|
+
click.secho("You are not logged in.", fg="red")
|
|
102
|
+
return
|
|
103
|
+
|
|
104
|
+
click.secho(
|
|
105
|
+
f"Starting resource monitoring for server '{server_name}'. Press CTRL+C to exit.",
|
|
106
|
+
fg="cyan",
|
|
107
|
+
)
|
|
108
|
+
time.sleep(1)
|
|
109
|
+
|
|
110
|
+
try:
|
|
111
|
+
while True:
|
|
112
|
+
response = await client.async_get_server_process_info(server_name)
|
|
113
|
+
|
|
114
|
+
click.clear()
|
|
115
|
+
click.secho(
|
|
116
|
+
f"--- Monitoring Server: {server_name} ---", fg="magenta", bold=True
|
|
117
|
+
)
|
|
118
|
+
click.echo(
|
|
119
|
+
f"(Last updated: {time.strftime('%H:%M:%S')}, Press CTRL+C to exit)\n"
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
if response.status == "error":
|
|
123
|
+
click.secho(f"Error: {response.message}", fg="red")
|
|
124
|
+
elif response.process_info is None:
|
|
125
|
+
click.secho("Server process not found (is it running?).", fg="yellow")
|
|
126
|
+
else:
|
|
127
|
+
info = response.process_info
|
|
128
|
+
pid_str = info.get("pid", "N/A")
|
|
129
|
+
cpu_str = f"{info.get('cpu_percent', 0.0):.1f}%"
|
|
130
|
+
mem_str = f"{info.get('memory_mb', 0.0):.1f} MB"
|
|
131
|
+
uptime_str = info.get("uptime", "N/A")
|
|
132
|
+
|
|
133
|
+
click.echo(f" {'PID':<15}: {click.style(str(pid_str), fg='cyan')}")
|
|
134
|
+
click.echo(f" {'CPU Usage':<15}: {click.style(cpu_str, fg='green')}")
|
|
135
|
+
click.echo(
|
|
136
|
+
f" {'Memory Usage':<15}: {click.style(mem_str, fg='green')}"
|
|
137
|
+
)
|
|
138
|
+
click.echo(f" {'Uptime':<15}: {click.style(uptime_str, fg='white')}")
|
|
139
|
+
|
|
140
|
+
time.sleep(2)
|
|
141
|
+
except (KeyboardInterrupt, click.Abort):
|
|
142
|
+
click.secho("\nMonitoring stopped.", fg="green")
|