diff --git a/benchmate/api/actions/drop_site.py b/benchmate/api/actions/drop_site.py
new file mode 100644
index 0000000..75bd5a5
--- /dev/null
+++ b/benchmate/api/actions/drop_site.py
@@ -0,0 +1,206 @@
+import os
+import subprocess
+import time
+
+import frappe
+
+from benchmate.api.utils import get_benchmate_settings
+
+
+def update_deletion_log_status(docname, new_text=None, status=None):
+ """
+ ? Update the BM Site Deletion Logs record for a given docname.
+ ? Appends log text and/or updates the status field, committing immediately.
+ """
+ try:
+ log_doc = frappe.get_doc("BM Site Deletion Logs", docname)
+
+ # ? Append new log text if provided
+ if new_text:
+ updated_log = (log_doc.log or "") + new_text
+ log_doc.db_set("log", updated_log, update_modified=False)
+
+ # ? Update status if provided
+ if status:
+ log_doc.db_set("status", status, update_modified=False)
+
+ frappe.db.commit()
+ log_doc.reload()
+ except Exception as e:
+ frappe.log_error(f"Error updating BM Site Deletion Logs: {e}", "BenchMate SiteDeletionLogs")
+
+
+def drop_site_background(
+ bench_name: str, bench_path: str, site_name: str, sudo_password: str, mysql_root_password: str
+):
+ """
+ Background task to drop (delete) a Frappe site inside a given bench.
+ Captures real-time logs into BM Site Deletion Logs doctype,
+ and cleans up temporary log files after completion.
+ """
+ bench_path = os.path.abspath(bench_path)
+ log_file = os.path.join(bench_path, f"bench_drop_site_{site_name}.log")
+
+ # ? Create a unique BM Site Deletion Logs record for tracking
+ log_timestamp = int(time.time())
+ log_name = f"{site_name}-{log_timestamp}"
+ if not frappe.db.exists("BM Site Deletion Logs", log_name):
+ frappe.get_doc(
+ {
+ "doctype": "BM Site Deletion Logs",
+ "site_name": site_name,
+ "log": "",
+ "log_timestamp": log_timestamp,
+ "status": "In Process",
+ }
+ ).insert(ignore_permissions=True)
+ frappe.db.commit()
+
+ # ? Command to drop the site with root DB password, no --verbose
+ cmd = [
+ "sudo",
+ "-S",
+ "bench",
+ "drop-site",
+ site_name,
+ "--db-root-password",
+ mysql_root_password,
+ "--no-backup",
+ "--force",
+ ]
+
+ # ? Launch subprocess and redirect stdout/stderr into a log file
+ try:
+ with open(log_file, "w") as f:
+ proc = subprocess.Popen(
+ cmd,
+ cwd=bench_path,
+ stdin=subprocess.PIPE,
+ stdout=f,
+ stderr=subprocess.STDOUT,
+ text=True,
+ )
+ # ? Send sudo password to subprocess
+ proc.stdin.write(sudo_password + "\n")
+ proc.stdin.flush()
+ proc.stdin.close()
+
+ # ? Wait for process completion with timeout (10 mins)
+ try:
+ proc.wait(timeout=600)
+ except subprocess.TimeoutExpired:
+ proc.kill()
+ update_deletion_log_status(
+ log_name,
+ new_text="\nTimed out while deleting site!\n",
+ status="Error",
+ )
+ frappe.msgprint(
+ msg=f"Timeout expired while deleting site {site_name}.",
+ title="Site Deletion Timeout",
+ alert=True,
+ indicator="red",
+ )
+ return
+
+ # ? Tail the log file and update BM Site Deletion Logs in real-time
+ with open(log_file) as f:
+ f.seek(0, os.SEEK_SET)
+ # ? Stream entire log file content to document
+ for line in f:
+ update_deletion_log_status(log_name, new_text=line)
+
+ # ? Update status based on exit code
+ if proc.returncode == 0:
+ update_deletion_log_status(log_name, status="Success")
+ remove_bm_site(bench_name=bench_name, bench_path=bench_path, site_name=site_name)
+ else:
+ update_deletion_log_status(log_name, status="Error")
+
+ except Exception as e:
+ frappe.msgprint(
+ msg=f"Error While Deleting Site {site_name} in bench {bench_name}",
+ title="Site Deletion Error",
+ realtime=True,
+ alert=True,
+ indicator="red",
+ )
+ frappe.log_error(f"Error running bench drop-site: {e}", "BenchMate SiteDeletionLogs")
+ update_deletion_log_status(log_name, status="Error")
+
+ else:
+ frappe.msgprint(
+ msg=f"Site {site_name} deleted successfully from bench {bench_name}",
+ title="Site Deletion Success",
+ realtime=True,
+ alert=True,
+ indicator="green",
+ )
+
+ finally:
+ # ? Always clean up the temporary log file
+ try:
+ if os.path.exists(log_file):
+ os.remove(log_file)
+ except Exception as cleanup_error:
+ frappe.log_error(
+ f"Failed to remove temp log file {log_file}: {cleanup_error}",
+ "BenchMate SiteDeletionLogs",
+ )
+
+
+def remove_bm_site(bench_name: str, bench_path: str, site_name: str):
+ """
+ Remove the BM Site record from the system.
+ Unlinks the site from its bench and deletes its record.
+ """
+ # ? Find and delete the BM Site record matching this site
+ site_doc_name = frappe.db.get_value("BM Site", {"bench_name": bench_name, "site_name": site_name})
+ if site_doc_name:
+ frappe.delete_doc("BM Site", site_doc_name, ignore_permissions=True)
+ frappe.db.commit()
+
+
+@frappe.whitelist()
+def execute(bench_name: str, bench_path: str, site_name: str):
+ """
+ ? Public API method (whitelisted) to enqueue site deletion.
+ ? Validates input and enqueues the background site deletion task.
+ """
+ if not bench_path or not site_name:
+ frappe.throw("bench_path and site_name are required", frappe.ValidationError)
+
+ # ? Fetch global BenchMate settings (sudo password, DB password)
+ settings = get_benchmate_settings()
+ sudo_password = settings.get("sudo_password")
+ mysql_root_password = settings.get("db_password")
+
+ # ? Validate required passwords are configured
+ if not sudo_password:
+ frappe.throw("Sudo password not configured", frappe.ValidationError)
+ if not mysql_root_password:
+ frappe.throw("MySQL root password not configured", frappe.ValidationError)
+
+ # ? Enqueue background task for long execution
+ try:
+ frappe.enqueue(
+ drop_site_background,
+ queue="long",
+ timeout=3600,
+ bench_name=bench_name,
+ bench_path=bench_path,
+ site_name=site_name,
+ sudo_password=sudo_password,
+ mysql_root_password=mysql_root_password,
+ )
+ except Exception as e:
+ frappe.throw(f"Failed to enqueue site deletion: {e!s}")
+
+ return {
+ "success": True,
+ "message": (
+ f"Deleting site {site_name} in the background. "
+ f"Check the BM Site Deletion Logs for more details."
+ ),
+ "data": {"bench_path": bench_path},
+ }
diff --git a/benchmate/api/utils.py b/benchmate/api/utils.py
index eb4893a..7306609 100644
--- a/benchmate/api/utils.py
+++ b/benchmate/api/utils.py
@@ -23,3 +23,35 @@ def get_benchmate_settings():
"Benchmate Settings are not enabled. Please enable them to proceed.",
frappe.ValidationError,
)
+
+
+# ! benchmate.api.utils.get_sites
+@frappe.whitelist()
+def get_sites(bench_name: str):
+ try:
+ site_list = frappe.get_all(
+ "BM Site",
+ filters={"bench_name": bench_name},
+ fields=["name", "site_name", "status"],
+ )
+
+ if not site_list or len(site_list) < 0:
+ frappe.throw(
+ f"There are no sites in bench {bench_name}",
+ frappe.ValidationError,
+ )
+
+ except Exception as e:
+ frappe.log_error("Error while benchmate.api.utils.get_sites", frappe.get_traceback())
+ return {
+ "success": True,
+ "message": str(e),
+ "data": None,
+ }
+
+ else:
+ return {
+ "success": True,
+ "message": "Successfully Get The Site List",
+ "data": site_list,
+ }
diff --git a/benchmate/benchmate/doctype/bm_bench/bm_bench.js b/benchmate/benchmate/doctype/bm_bench/bm_bench.js
index d5cf6ef..4c6eb72 100644
--- a/benchmate/benchmate/doctype/bm_bench/bm_bench.js
+++ b/benchmate/benchmate/doctype/bm_bench/bm_bench.js
@@ -19,6 +19,15 @@ function addBenchActions(frm) {
__("Actions")
);
+ // ? Add "Drop Site" button and pair it with handler
+ frm.add_custom_button(
+ __("Drop Site"),
+ function () {
+ dropSite(frm);
+ },
+ __("Actions")
+ );
+
// ? Add "Start Bench" button and pair it with handler
frm.add_custom_button(
__("Start Bench"),
@@ -39,7 +48,7 @@ function addBenchActions(frm) {
}
function createSite(frm) {
- let d = new frappe.ui.Dialog({
+ let dialog = new frappe.ui.Dialog({
title: __("Create New Site"),
fields: [
{
@@ -51,7 +60,7 @@ function createSite(frm) {
],
primary_action_label: __("Create"),
primary_action(values) {
- d.hide();
+ dialog.hide();
frappe.call({
method: "benchmate.api.actions.create_site.execute",
args: {
@@ -87,7 +96,7 @@ function createSite(frm) {
});
},
});
- d.show();
+ dialog.show();
}
// ? Function to handle Start Bench action
@@ -161,3 +170,100 @@ function stopBench(frm) {
},
});
}
+
+// ? Function to handle the Drop Site action from BM Bench form
+function dropSite(frm) {
+ // ? Create a dialog box for site selection and drop confirmation
+ let dialog = new frappe.ui.Dialog({
+ title: __("Drop A Site"),
+
+ // ? Fields inside the dialog
+ fields: [
+ {
+ label: __("Site"), // ? BM Site link field
+ fieldname: "site",
+ fieldtype: "Link",
+ options: "BM Site",
+ reqd: 1,
+ get_query: function () {
+ // ? Restrict sites only for the current bench
+ return {
+ filters: [["bench_name", "=", frm.doc.name]],
+ };
+ },
+ onchange: function () {
+ // ? On site selection, fetch the actual site_name
+ let site = dialog.get_value("site");
+ if (site) {
+ frappe.call({
+ method: "frappe.client.get_value",
+ args: {
+ doctype: "BM Site",
+ filters: { name: site },
+ fieldname: ["site_name"],
+ },
+ freeze: true,
+ freeze_message: __(`Fetching Site Name...`),
+ callback: function (response) {
+ // ? Auto-populate the readonly Site Name field
+ dialog.set_value("site_name", response.message.site_name);
+ },
+ });
+ }
+ },
+ },
+ {
+ label: __("Site Name"), // ? Read-only field to confirm exact site_name
+ fieldname: "site_name",
+ fieldtype: "Data",
+ read_only: 1,
+ reqd: 1,
+ },
+ ],
+
+ // ? Primary action button: Drop Site
+ primary_action_label: __("Drop"),
+ primary_action(values) {
+ // ? Hide dialog after confirmation
+ dialog.hide();
+
+ // ? Call server-side method to drop the site
+ frappe.call({
+ method: "benchmate.api.actions.drop_site.execute",
+ args: {
+ bench_name: frm.doc.name, // ? Current bench name
+ bench_path: frm.doc.path, // ? Bench path
+ site_name: values.site_name, // ? Site selected to drop
+ },
+ freeze: true,
+ freeze_message: __(`Dropping Site ${values.site_name}...`),
+
+ // ? Handle callback after server execution
+ callback: function (r) {
+ if (r.message.success) {
+ // ? Show success message on site drop
+ frappe.show_alert(
+ {
+ message: __(r.message.message),
+ indicator: "green",
+ },
+ 5
+ );
+ } else {
+ // ? Show error message if failed
+ frappe.show_alert(
+ {
+ message: __(r.message.message),
+ indicator: "red",
+ },
+ 5
+ );
+ }
+ },
+ });
+ },
+ });
+
+ // ? Display the dialog to the user
+ dialog.show();
+}
diff --git a/benchmate/benchmate/doctype/bm_site_deletion_logs/__init__.py b/benchmate/benchmate/doctype/bm_site_deletion_logs/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/benchmate/benchmate/doctype/bm_site_deletion_logs/bm_site_deletion_logs.js b/benchmate/benchmate/doctype/bm_site_deletion_logs/bm_site_deletion_logs.js
new file mode 100644
index 0000000..95cd396
--- /dev/null
+++ b/benchmate/benchmate/doctype/bm_site_deletion_logs/bm_site_deletion_logs.js
@@ -0,0 +1,8 @@
+// Copyright (c) 2025, Karan Mistry and contributors
+// For license information, please see license.txt
+
+// frappe.ui.form.on("BM Site Deletion Logs", {
+// refresh(frm) {
+
+// },
+// });
diff --git a/benchmate/benchmate/doctype/bm_site_deletion_logs/bm_site_deletion_logs.json b/benchmate/benchmate/doctype/bm_site_deletion_logs/bm_site_deletion_logs.json
new file mode 100644
index 0000000..5302bd1
--- /dev/null
+++ b/benchmate/benchmate/doctype/bm_site_deletion_logs/bm_site_deletion_logs.json
@@ -0,0 +1,102 @@
+{
+ "actions": [],
+ "allow_rename": 1,
+ "autoname": "format:{site_name}-{log_timestamp}",
+ "creation": "2025-09-07 23:41:44.865581",
+ "doctype": "DocType",
+ "engine": "InnoDB",
+ "field_order": [
+ "site_name",
+ "column_break_bkvd",
+ "status",
+ "log_timestamp",
+ "section_break_jlrx",
+ "log"
+ ],
+ "fields": [
+ {
+ "fieldname": "site_name",
+ "fieldtype": "Data",
+ "in_list_view": 1,
+ "in_standard_filter": 1,
+ "label": "Site Name",
+ "read_only": 1
+ },
+ {
+ "fieldname": "column_break_bkvd",
+ "fieldtype": "Column Break"
+ },
+ {
+ "default": "In Process",
+ "fieldname": "status",
+ "fieldtype": "Select",
+ "in_list_view": 1,
+ "in_standard_filter": 1,
+ "label": "Status",
+ "options": "\nIn Process\nSuccess\nError",
+ "read_only": 1
+ },
+ {
+ "fieldname": "log_timestamp",
+ "fieldtype": "Data",
+ "in_list_view": 1,
+ "in_standard_filter": 1,
+ "label": "Log Timestamp",
+ "read_only": 1,
+ "unique": 1
+ },
+ {
+ "fieldname": "section_break_jlrx",
+ "fieldtype": "Section Break"
+ },
+ {
+ "fieldname": "log",
+ "fieldtype": "Long Text",
+ "in_list_view": 1,
+ "label": "Log",
+ "read_only": 1
+ }
+ ],
+ "grid_page_length": 50,
+ "index_web_pages_for_search": 1,
+ "links": [],
+ "modified": "2025-09-07 23:41:44.865581",
+ "modified_by": "Administrator",
+ "module": "BenchMate",
+ "name": "BM Site Deletion Logs",
+ "naming_rule": "Expression",
+ "owner": "Administrator",
+ "permissions": [
+ {
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "System Manager",
+ "share": 1,
+ "write": 1
+ }
+ ],
+ "row_format": "Dynamic",
+ "sort_field": "creation",
+ "sort_order": "DESC",
+ "states": [
+ {
+ "color": "Blue",
+ "title": "In Process"
+ },
+ {
+ "color": "Green",
+ "title": "Success"
+ },
+ {
+ "color": "Red",
+ "title": "Error"
+ }
+ ],
+ "title_field": "site_name",
+ "track_seen": 1
+}
diff --git a/benchmate/benchmate/doctype/bm_site_deletion_logs/bm_site_deletion_logs.py b/benchmate/benchmate/doctype/bm_site_deletion_logs/bm_site_deletion_logs.py
new file mode 100644
index 0000000..0d96f97
--- /dev/null
+++ b/benchmate/benchmate/doctype/bm_site_deletion_logs/bm_site_deletion_logs.py
@@ -0,0 +1,9 @@
+# Copyright (c) 2025, Karan Mistry and contributors
+# For license information, please see license.txt
+
+# import frappe
+from frappe.model.document import Document
+
+
+class BMSiteDeletionLogs(Document):
+ pass
diff --git a/benchmate/benchmate/doctype/bm_site_deletion_logs/test_bm_site_deletion_logs.py b/benchmate/benchmate/doctype/bm_site_deletion_logs/test_bm_site_deletion_logs.py
new file mode 100644
index 0000000..c239de8
--- /dev/null
+++ b/benchmate/benchmate/doctype/bm_site_deletion_logs/test_bm_site_deletion_logs.py
@@ -0,0 +1,20 @@
+# Copyright (c) 2025, Karan Mistry and Contributors
+# See license.txt
+
+# import frappe
+from frappe.tests import IntegrationTestCase
+
+# On IntegrationTestCase, the doctype test records and all
+# link-field test record dependencies are recursively loaded
+# Use these module variables to add/remove to/from that list
+EXTRA_TEST_RECORD_DEPENDENCIES = [] # eg. ["User"]
+IGNORE_TEST_RECORD_DEPENDENCIES = [] # eg. ["User"]
+
+
+class IntegrationTestBMSiteDeletionLogs(IntegrationTestCase):
+ """
+ Integration tests for BMSiteDeletionLogs.
+ Use this class for testing interactions between multiple components.
+ """
+
+ pass
diff --git a/benchmate/benchmate/workspace/benchmate/benchmate.json b/benchmate/benchmate/workspace/benchmate/benchmate.json
index ea97eca..ad73c54 100644
--- a/benchmate/benchmate/workspace/benchmate/benchmate.json
+++ b/benchmate/benchmate/workspace/benchmate/benchmate.json
@@ -1,7 +1,7 @@
{
"app": "benchmate",
"charts": [],
- "content": "[{\"id\":\"E4TboPQAiW\",\"type\":\"header\",\"data\":{\"text\":\"BenchMate\",\"col\":12}},{\"id\":\"pckRgVGLCJ\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"BM Bench\",\"col\":3}},{\"id\":\"uIKLe2weO2\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"BM Site\",\"col\":3}},{\"id\":\"1zPvETmYcy\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"BM App\",\"col\":3}},{\"id\":\"OdX7vGpNVa\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"BM Sync Log\",\"col\":3}},{\"id\":\"CZYPa4MOMK\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Bm Site Creation Logs\",\"col\":3}},{\"id\":\"Zlbft996Gt\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"BM Settings\",\"col\":3}}]",
+ "content": "[{\"id\":\"E4TboPQAiW\",\"type\":\"header\",\"data\":{\"text\":\"BenchMate\",\"col\":12}},{\"id\":\"pckRgVGLCJ\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"BM Bench\",\"col\":3}},{\"id\":\"uIKLe2weO2\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"BM Site\",\"col\":3}},{\"id\":\"1zPvETmYcy\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"BM App\",\"col\":3}},{\"id\":\"OdX7vGpNVa\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"BM Sync Log\",\"col\":3}},{\"id\":\"CZYPa4MOMK\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Bm Site Creation Logs\",\"col\":3}},{\"id\":\"5GjUZsGiN_\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Bm Site Deletion Logs\",\"col\":3}},{\"id\":\"Zlbft996Gt\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"BM Settings\",\"col\":3}}]",
"creation": "2025-08-18 03:30:43.609066",
"custom_blocks": [],
"docstatus": 0,
@@ -15,7 +15,7 @@
"label": "BenchMate",
"link_type": "DocType",
"links": [],
- "modified": "2025-09-07 21:22:03.176848",
+ "modified": "2025-09-07 23:42:31.333942",
"modified_by": "Administrator",
"module": "BenchMate",
"name": "BenchMate",
@@ -44,6 +44,14 @@
"stats_filter": "[]",
"type": "DocType"
},
+ {
+ "color": "Grey",
+ "doc_view": "List",
+ "label": "Bm Site Deletion Logs",
+ "link_to": "BM Site Deletion Logs",
+ "stats_filter": "[]",
+ "type": "DocType"
+ },
{
"color": "Grey",
"doc_view": "List",