servctl 1.0.1__tar.gz → 1.0.2__tar.gz

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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: servctl
3
- Version: 1.0.1
3
+ Version: 1.0.2
4
4
  Summary: Unprivileged background server and localhost proxy CLI manager
5
5
  Author: avevall
6
6
  Keywords: cli,devops,dns,doh,ftp,localhost,proxy,server
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "servctl"
7
- version = "1.0.1"
7
+ version = "1.0.2"
8
8
  description = "Unprivileged background server and localhost proxy CLI manager"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.8"
@@ -995,7 +995,7 @@ def run_worker(args):
995
995
  server.serve_forever()
996
996
 
997
997
  # ---------------------------------------------------------------------------
998
- # CLI Command Implementations
998
+ # CLI Commands
999
999
  # ---------------------------------------------------------------------------
1000
1000
 
1001
1001
  def cmd_start(args):
@@ -1131,7 +1131,7 @@ def cmd_list(args):
1131
1131
  print(sep)
1132
1132
  print("Tip: Run 'servctl status show <ID>' to view full connection URLs and configuration.\n")
1133
1133
 
1134
- def cmd_status(args):
1134
+ def cmd_status_show(args):
1135
1135
  reg = clean_registry()
1136
1136
  if not reg:
1137
1137
  print("No servers running. Start one using: servctl start")
@@ -1139,97 +1139,120 @@ def cmd_status(args):
1139
1139
 
1140
1140
  lan_ip = get_local_ip()
1141
1141
 
1142
- # Subcommand: status show
1143
- if args.status_cmd == "show":
1144
- if args.all or not args.target:
1145
- print("\nDetailed Server Configurations & Status:")
1146
- for sid, meta in reg.items():
1147
- print_server_status_card(meta, lan_ip)
1148
- print()
1149
- else:
1150
- matched = False
1151
- for sid, meta in reg.items():
1152
- if sid == args.target or str(meta["port"]) == args.target:
1153
- print(f"\nConfiguration & Runtime for '{sid}':")
1154
- print_server_status_card(meta, lan_ip)
1155
- print()
1156
- matched = True
1157
- break
1158
- if not matched:
1159
- print(f"Error: No running server matching '{args.target}'.")
1160
-
1161
- # Subcommand: status change
1162
- elif args.status_cmd == "change":
1163
- target_id = None
1142
+ if getattr(args, "all", False) or not getattr(args, "target", None):
1143
+ print("\nDetailed Server Configurations & Status:")
1144
+ for sid, meta in reg.items():
1145
+ print_server_status_card(meta, lan_ip)
1146
+ print()
1147
+ else:
1148
+ matched = False
1164
1149
  for sid, meta in reg.items():
1165
1150
  if sid == args.target or str(meta["port"]) == args.target:
1166
- target_id = sid
1151
+ print(f"\nConfiguration & Runtime for '{sid}':")
1152
+ print_server_status_card(meta, lan_ip)
1153
+ print()
1154
+ matched = True
1167
1155
  break
1168
-
1169
- if not target_id:
1156
+ if not matched:
1170
1157
  print(f"Error: No running server matching '{args.target}'.")
1171
- return
1172
1158
 
1173
- old_meta = reg[target_id]
1174
- new_port = args.port if args.port is not None else old_meta["port"]
1175
- new_access = args.access if args.access is not None else old_meta["access"]
1176
- new_root = str(Path(args.root).expanduser().resolve()) if args.root is not None else old_meta["root"]
1177
- new_target = args.target_url if args.target_url is not None else old_meta.get("target")
1178
-
1179
- # Check if anything changed
1180
- if (new_port == old_meta["port"] and
1181
- new_access == old_meta["access"] and
1182
- new_root == old_meta["root"] and
1183
- new_target == old_meta.get("target") and
1184
- not args.name):
1185
- print("Notice: No property changes specified. Use -p, -r, -a, or --target to modify properties.")
1186
- return
1159
+ def cmd_status_change(args):
1160
+ reg = clean_registry()
1161
+ if not reg:
1162
+ print("No servers running. Start one using: servctl start")
1163
+ return
1187
1164
 
1188
- # Check port conflicts
1189
- new_host = "0.0.0.0" if new_access == "network" else "127.0.0.1"
1190
- is_udp = (old_meta["type"] == "dns")
1191
- if new_port != old_meta["port"] and is_port_in_use(new_port, new_host, udp=is_udp):
1192
- print(f"Error: New port {new_port} is already in use on {new_host}.")
1193
- return
1165
+ target_id = None
1166
+ for sid, meta in reg.items():
1167
+ if sid == args.target or str(meta["port"]) == args.target:
1168
+ target_id = sid
1169
+ break
1194
1170
 
1195
- # Check privilege
1196
- if new_port < 1024 and os.name != "nt" and os.geteuid() != 0:
1197
- print(f"Error: Port {new_port} is a privileged port (< 1024) requiring root/sudo.")
1198
- return
1171
+ if not target_id:
1172
+ print(f"Error: No running server matching '{args.target}'.")
1173
+ return
1199
1174
 
1200
- # Determine new ID
1201
- if args.name:
1202
- new_id = args.name
1203
- elif old_meta["id"] == f"{old_meta['type']}-{old_meta['port']}":
1204
- new_id = f"{old_meta['type']}-{new_port}"
1205
- else:
1206
- new_id = old_meta["id"]
1175
+ old_meta = reg[target_id]
1176
+
1177
+ # Resolve optional positional extra (could be port or root directory)
1178
+ parsed_port = args.port
1179
+ parsed_root = args.root
1180
+ if getattr(args, "extra", None):
1181
+ if args.extra.isdigit() and not parsed_port:
1182
+ parsed_port = int(args.extra)
1183
+ elif not parsed_root:
1184
+ parsed_root = args.extra
1185
+
1186
+ # Target new properties
1187
+ new_type = args.type if getattr(args, "type", None) else old_meta["type"]
1188
+ if new_type in ("websecure", "https"):
1189
+ new_type = "websecure"
1190
+
1191
+ new_port = parsed_port if parsed_port is not None else old_meta["port"]
1192
+ new_access = args.access if args.access is not None else old_meta["access"]
1193
+ new_root = str(Path(parsed_root).expanduser().resolve()) if parsed_root is not None else old_meta["root"]
1194
+ new_target = args.target_url if getattr(args, "target_url", None) is not None else old_meta.get("target")
1195
+
1196
+ # Check if anything changed
1197
+ if (new_port == old_meta["port"] and
1198
+ new_access == old_meta["access"] and
1199
+ new_root == old_meta["root"] and
1200
+ new_type == old_meta["type"] and
1201
+ new_target == old_meta.get("target") and
1202
+ not getattr(args, "name", None)):
1203
+ print("Notice: No property changes specified. Use -p, -r, -t, -a, or --target to modify properties.")
1204
+ return
1207
1205
 
1208
- print(f"Applying property changes to '{old_meta['id']}'...")
1209
- stop_server_by_id(target_id, reg)
1206
+ # Check directory
1207
+ if new_type in ("web", "websecure", "ftp", "ftps", "webdav") and not Path(new_root).is_dir():
1208
+ print(f"Error: Directory '{new_root}' does not exist.")
1209
+ return
1210
1210
 
1211
- pid, log_path = spawn_server(new_id, old_meta["type"], new_port, new_host, new_root, new_target)
1212
- if not pid:
1213
- print(f"Error: Failed to re-launch server with new settings. Check ({log_path}).")
1214
- return
1211
+ # Check port conflicts
1212
+ new_host = "0.0.0.0" if new_access == "network" else "127.0.0.1"
1213
+ is_udp = (new_type == "dns")
1214
+ if new_port != old_meta["port"] and is_port_in_use(new_port, new_host, udp=is_udp):
1215
+ print(f"Error: Port {new_port} ({'UDP' if is_udp else 'TCP'}) is already in use on {new_host}.")
1216
+ return
1215
1217
 
1216
- reg[new_id] = {
1217
- "id": new_id,
1218
- "type": old_meta["type"],
1219
- "port": new_port,
1220
- "host": new_host,
1221
- "access": new_access,
1222
- "root": new_root,
1223
- "target": new_target,
1224
- "pid": pid,
1225
- "started_at": time.time(),
1226
- "log_file": str(log_path)
1227
- }
1228
- write_registry(reg)
1229
-
1230
- print(f"✔ Successfully updated '{old_meta['id']}' -> '{new_id}'!")
1231
- print_server_status_card(reg[new_id])
1232
- print()
1218
+ # Check privilege
1219
+ if new_port < 1024 and os.name != "nt" and os.geteuid() != 0:
1220
+ print(f"Error: Port {new_port} is a privileged port (< 1024) requiring root/sudo.")
1221
+ return
1222
+
1223
+ # Determine new ID
1224
+ if getattr(args, "name", None):
1225
+ new_id = args.name
1226
+ elif old_meta["id"] == f"{old_meta['type']}-{old_meta['port']}":
1227
+ new_id = f"{new_type}-{new_port}"
1228
+ else:
1229
+ new_id = old_meta["id"]
1230
+
1231
+ print(f"Applying property changes to '{old_meta['id']}'...")
1232
+ stop_server_by_id(target_id, reg)
1233
+
1234
+ pid, log_path = spawn_server(new_id, new_type, new_port, new_host, new_root, new_target)
1235
+ if not pid:
1236
+ print(f"Error: Failed to re-launch server with new settings. Check log ({log_path}).")
1237
+ return
1238
+
1239
+ reg[new_id] = {
1240
+ "id": new_id,
1241
+ "type": new_type,
1242
+ "port": new_port,
1243
+ "host": new_host,
1244
+ "access": new_access,
1245
+ "root": new_root,
1246
+ "target": new_target,
1247
+ "pid": pid,
1248
+ "started_at": time.time(),
1249
+ "log_file": str(log_path)
1250
+ }
1251
+ write_registry(reg)
1252
+
1253
+ print(f"\n✔ Successfully updated '{old_meta['id']}' -> '{new_id}'!")
1254
+ print_server_status_card(reg[new_id])
1255
+ print()
1233
1256
 
1234
1257
  def cmd_logs(args):
1235
1258
  reg = read_registry()
@@ -1272,7 +1295,7 @@ SYNOPSIS
1272
1295
  servctl stop [PORT | ID ...] [--all]
1273
1296
  servctl list
1274
1297
  servctl status show [PORT | ID | --all]
1275
- servctl status change <PORT | ID> [-p PORT] [-r ROOT] [-a ACCESS] [--target TARGET] [-n NAME]
1298
+ servctl status change <PORT | ID> [-p PORT] [-r ROOT] [-t TYPE] [-a ACCESS] [--target TARGET] [-n NAME]
1276
1299
  servctl logs [PORT | ID] [-f] [-n LINES]
1277
1300
  servctl man
1278
1301
 
@@ -1325,8 +1348,8 @@ STATUS & CONFIGURATION MANAGEMENT
1325
1348
  Displays detailed status cards for all active servers.
1326
1349
 
1327
1350
  servctl status change <ID/PORT> [OPTIONS...]
1328
- Dynamically changes running server properties (port, root, access,
1329
- target) and restarts it seamlessly in the background.
1351
+ Dynamically changes running server properties (port, root, type,
1352
+ access, target) and restarts it seamlessly in the background.
1330
1353
 
1331
1354
  EXAMPLES
1332
1355
  1. Start a default HTTP web server:
@@ -1341,6 +1364,7 @@ EXAMPLES
1341
1364
 
1342
1365
  4. Change a running server's properties on the fly:
1343
1366
  $ servctl status change web-8080 -p 8085 -a network
1367
+ $ servctl status change web-8080 -t websecure
1344
1368
 
1345
1369
  5. Stop multiple servers at once:
1346
1370
  $ servctl stop 8080 8443
@@ -1368,6 +1392,16 @@ def normalize_status_argv(argv):
1368
1392
  else:
1369
1393
  return argv[:idx + 1] + ["show"] + args_after
1370
1394
 
1395
+ def setup_change_parser(p):
1396
+ p.add_argument("target", help="Server ID or Port to modify")
1397
+ p.add_argument("extra", nargs="?", default=None, help="Optional new port or root directory")
1398
+ p.add_argument("-p", "--port", type=int, default=None, help="New port number")
1399
+ p.add_argument("-r", "--root", default=None, help="New root directory")
1400
+ p.add_argument("-t", "--type", choices=["web", "websecure", "https", "webdav", "ftp", "ftps", "dns", "proxy"], default=None, help="Change server type")
1401
+ p.add_argument("-a", "--access", choices=["local", "network"], default=None, help="New access scope")
1402
+ p.add_argument("--target", dest="target_url", default=None, help="New upstream target URL")
1403
+ p.add_argument("-n", "--name", default=None, help="Rename server ID")
1404
+
1371
1405
  def main():
1372
1406
  parser = argparse.ArgumentParser(prog="servctl", description="Unprivileged background server and proxy manager")
1373
1407
  subparsers = parser.add_subparsers(dest="command")
@@ -1406,12 +1440,11 @@ def main():
1406
1440
 
1407
1441
  # status change
1408
1442
  p_change = sub_status.add_parser("change", help="Change properties of a running server")
1409
- p_change.add_argument("target", help="Server ID or Port to modify")
1410
- p_change.add_argument("-p", "--port", type=int, default=None, help="New port number")
1411
- p_change.add_argument("-r", "--root", default=None, help="New root directory")
1412
- p_change.add_argument("-a", "--access", choices=["local", "network"], default=None, help="New access scope")
1413
- p_change.add_argument("--target", dest="target_url", default=None, help="New upstream target URL")
1414
- p_change.add_argument("-n", "--name", default=None, help="Rename server ID")
1443
+ setup_change_parser(p_change)
1444
+
1445
+ # Top-level direct 'servctl change'
1446
+ p_top_change = subparsers.add_parser("change", help="Change properties of a running server")
1447
+ setup_change_parser(p_top_change)
1415
1448
 
1416
1449
  # LOGS
1417
1450
  p_logs = subparsers.add_parser("logs", help="View server logs")
@@ -1444,7 +1477,12 @@ def main():
1444
1477
  elif args.command == "list":
1445
1478
  cmd_list(args)
1446
1479
  elif args.command == "status":
1447
- cmd_status(args)
1480
+ if getattr(args, "status_cmd", None) == "change":
1481
+ cmd_status_change(args)
1482
+ else:
1483
+ cmd_status_show(args)
1484
+ elif args.command == "change":
1485
+ cmd_status_change(args)
1448
1486
  elif args.command == "logs":
1449
1487
  cmd_logs(args)
1450
1488
  elif args.command == "man":
File without changes
File without changes
File without changes