@@ -11,6 +11,8 @@ import java.net.HttpURLConnection
1111import java.net.URL
1212import java.util.Locale
1313import java.util.UUID
14+ import java.util.concurrent.Executors
15+ import java.util.concurrent.TimeUnit
1416
1517class WolServer (
1618 private val context : Context ,
@@ -30,6 +32,7 @@ class WolServer(
3032 session.uri == " /api/devices" && session.method == Method .GET -> listDevices(session)
3133 session.uri == " /api/devices" && session.method == Method .POST -> upsertDevice(session)
3234 session.uri.startsWith(" /api/devices/" ) && session.method == Method .DELETE -> deleteDevice(session)
35+ session.uri == " /api/ping" && session.method == Method .POST -> handlePing(session)
3336 else -> serveStaticOrIndex(session)
3437 }
3538 } catch (e: Exception ) {
@@ -207,6 +210,7 @@ class WolServer(
207210 val mac = json.optString(" mac" )
208211 val ip = json.optString(" ip" , " 255.255.255.255" )
209212 val port = json.optInt(" port" , 9 )
213+ val pingIp = json.optString(" pingIp" , " " )
210214 if (name.isBlank() || mac.isBlank()) return badRequest(" Missing name/mac" )
211215
212216 val arr = loadDevices()
@@ -218,6 +222,7 @@ class WolServer(
218222 obj.put(" mac" , mac)
219223 obj.put(" ip" , ip)
220224 obj.put(" port" , port)
225+ obj.put(" pingIp" , pingIp)
221226 updated = true
222227 break
223228 }
@@ -229,6 +234,7 @@ class WolServer(
229234 obj.put(" mac" , mac)
230235 obj.put(" ip" , ip)
231236 obj.put(" port" , port)
237+ obj.put(" pingIp" , pingIp)
232238 arr.put(obj)
233239 }
234240 saveDevices(arr)
@@ -267,6 +273,67 @@ class WolServer(
267273 }
268274 }
269275
276+ private fun handlePing (session : IHTTPSession ): Response {
277+ if (! isAuthorized(session)) return unauthorized()
278+ val files = mutableMapOf<String , String >()
279+ return try {
280+ session.parseBody(files)
281+ val body = files[" postData" ] ? : return badRequest(" Missing body" )
282+ val json = JSONObject (body)
283+
284+ // Accept either { "ip": "..." } or { "ips": ["...", ...] }
285+ val ips = mutableListOf<String >()
286+ if (json.has(" ip" )) {
287+ json.optString(" ip" ).trim().takeIf { it.isNotEmpty() }?.let { ips.add(it) }
288+ }
289+ if (json.has(" ips" )) {
290+ val arr = json.optJSONArray(" ips" )
291+ if (arr != null ) {
292+ for (i in 0 until arr.length()) {
293+ arr.optString(i).trim().takeIf { it.isNotEmpty() }?.let { ips.add(it) }
294+ }
295+ }
296+ }
297+ if (ips.isEmpty()) return badRequest(" Missing ip or ips" )
298+
299+ // Ping all IPs concurrently
300+ val results = JSONObject ()
301+ val executor = Executors .newFixedThreadPool(ips.size.coerceAtMost(8 ))
302+ val futures = ips.map { ip ->
303+ executor.submit<Pair <String , Boolean >> {
304+ ip to pingHost(ip)
305+ }
306+ }
307+ for (future in futures) {
308+ try {
309+ val (ip, reachable) = future.get(5 , TimeUnit .SECONDS )
310+ results.put(ip, reachable)
311+ } catch (_: Exception ) {
312+ // Timeout or error — mark as unreachable
313+ }
314+ }
315+ executor.shutdown()
316+
317+ newJsonResponse(Response .Status .OK , JSONObject ().put(" results" , results))
318+ } catch (e: Exception ) {
319+ badRequest(" Invalid request: ${e.message} " )
320+ }
321+ }
322+
323+ private fun pingHost (ip : String ): Boolean {
324+ return try {
325+ // Sanitize: only allow IP-like characters to prevent command injection
326+ val sanitized = ip.replace(Regex (" [^0-9a-fA-F.:]" ), " " )
327+ if (sanitized.isEmpty()) return false
328+
329+ val process = Runtime .getRuntime().exec(arrayOf(" ping" , " -c" , " 1" , " -W" , " 1" , sanitized))
330+ val exited = process.waitFor()
331+ exited == 0
332+ } catch (_: Exception ) {
333+ false
334+ }
335+ }
336+
270337 private fun loadDevices (): JSONArray {
271338 val s = prefs.getString(" devices" , " []" ) ? : " []"
272339 return try { JSONArray (s) } catch (_: Exception ) { JSONArray () }
@@ -347,6 +414,10 @@ class WolServer(
347414 table { border-collapse: collapse; width: 100%; }
348415 th, td { text-align: left; padding: 0.4rem; border-bottom: 1px solid #eee; }
349416 .actions button { margin-right: .4rem }
417+ .dot { display: inline-block; width: 10px; height: 10px; border-radius: 50%; margin-right: 4px; vertical-align: middle; }
418+ .dot-online { background: #22c55e; }
419+ .dot-offline { background: #ef4444; }
420+ .dot-unknown { background: #ccc; }
350421 </style>
351422 </head>
352423 <body>
@@ -365,7 +436,7 @@ class WolServer(
365436 <div class="card">
366437 <h3>Devices</h3>
367438 <table id="devicetable">
368- <thead><tr><th>Name</th><th>MAC</th><th>IP</th><th>Port</th><th class="actions">Actions</th></tr></thead>
439+ <thead><tr><th>Name</th><th>MAC</th><th>IP</th><th>Port</th><th>Status</th><th class="actions">Actions</th></tr></thead>
369440 <tbody id="devices"></tbody>
370441 </table>
371442 </div>
@@ -382,6 +453,10 @@ class WolServer(
382453 <label>MAC Address</label><br />
383454 <input name="mac" placeholder="AA:BB:CC:DD:EE:FF" required />
384455 </div>
456+ <div>
457+ <label>Device IP (for status check)</label><br />
458+ <input name="pingIp" placeholder="192.168.1.100 (optional)" />
459+ </div>
385460 <div class="row">
386461 <div>
387462 <label>Broadcast IP</label><br />
@@ -425,17 +500,27 @@ class WolServer(
425500
426501 async function refreshDevices() {
427502 const tbody = document.getElementById('devices');
428- tbody.innerHTML = '<tr><td colspan="5 ">Loading…</td></tr>';
503+ tbody.innerHTML = '<tr><td colspan="6 ">Loading…</td></tr>';
429504 try {
430505 const list = await api('/api/devices');
431506 tbody.innerHTML = '';
432507 if (!Array.isArray(list) || list.length === 0) {
433- tbody.innerHTML = '<tr><td colspan="5 ">No devices yet</td></tr>';
508+ tbody.innerHTML = '<tr><td colspan="6 ">No devices yet</td></tr>';
434509 return;
435510 }
511+ // Collect pingIps for status check
512+ const pingIps = list.map(d => d.pingIp).filter(Boolean);
513+ let statuses = {};
514+ if (pingIps.length > 0) {
515+ try {
516+ const pingRes = await api('/api/ping', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ ips: pingIps }) });
517+ statuses = pingRes.results || {};
518+ } catch (_) {}
519+ }
436520 for (const d of list) {
437521 const tr = document.createElement('tr');
438- tr.innerHTML = `<td>${' $' } {d.name||''}</td><td>${' $' } {d.mac||''}</td><td>${' $' } {d.ip||''}</td><td>${' $' } {d.port||9}</td>`;
522+ const statusDot = d.pingIp ? (statuses[d.pingIp] ? '<span class="dot dot-online"></span>Online' : '<span class="dot dot-offline"></span>Offline') : '<span class="dot dot-unknown"></span>—';
523+ tr.innerHTML = `<td>${' $' } {d.name||''}</td><td>${' $' } {d.mac||''}</td><td>${' $' } {d.ip||''}</td><td>${' $' } {d.port||9}</td><td>${' $' } {statusDot}</td>`;
439524 const tdActions = document.createElement('td');
440525 tdActions.className='actions';
441526 const wakeBtn = document.createElement('button');
@@ -452,7 +537,7 @@ class WolServer(
452537 tbody.appendChild(tr);
453538 }
454539 } catch (err) {
455- tbody.innerHTML = `<tr><td colspan="5 ">Error: ${' $' } {err.message}</td></tr>`;
540+ tbody.innerHTML = `<tr><td colspan="6 ">Error: ${' $' } {err.message}</td></tr>`;
456541 }
457542 }
458543
@@ -463,19 +548,20 @@ class WolServer(
463548 f.mac.value = d.mac||'';
464549 f.ip.value = d.ip||'255.255.255.255';
465550 f.port.value = d.port||9;
551+ f.pingIp.value = d.pingIp||'';
466552 }
467553
468554 function resetForm() {
469555 const f = document.forms[0];
470556 f.id.value = '';
471- f.name.value=''; f.mac.value=''; f.ip.value='255.255.255.255'; f.port.value=9;
557+ f.name.value=''; f.mac.value=''; f.ip.value='255.255.255.255'; f.port.value=9; f.pingIp.value='';
472558 document.getElementById('formResult').textContent='';
473559 }
474560
475561 async function saveDevice(e) {
476562 e.preventDefault();
477563 const f = e.target;
478- const payload = { id: f.id.value, name: f.name.value, mac: f.mac.value, ip: f.ip.value, port: Number(f.port.value)||9 };
564+ const payload = { id: f.id.value, name: f.name.value, mac: f.mac.value, ip: f.ip.value, port: Number(f.port.value)||9, pingIp: (f.pingIp.value||'').trim() };
479565 try {
480566 await api('/api/devices', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) });
481567 document.getElementById('formResult').textContent = 'Saved';
0 commit comments