@Jhonmicky, I have had the exact same need for an app I am currently developping.
You can use Desktop.py below as follows:
- Run the module on the PC
- On the iPhone :
from Desktop import announce_service
# Will launch Firefox on the Desktop, and open 'your_url'
announce_service(your_url, True)
...
# (Optional) Will close the Firefox window
announce_service(your_url, False)
Lots of things can be configured, please see in the source code. This module was developped for a specific application (MyDB, you will see it mentionned several times in the source), not as a general purpose module, you may have to tweak it a bit to suit your needs.
Note on implementation : what started out as a simple UDP broadcast function ended up as a full protocol with a fallback against routers that filter out UDP broadcast packets, and fences against lost or delayed packets, WiFi networks that are slow to authenticate, etc., as I tried it out in various environments (a tame Internet Box at home, enterprise LAN, hotel LAN, etc.)
This is still work in progress.
Hope this helps.
""" Desktop companion for MyDB Web UI.
There are two components to this module:
- service_daemon() runs on the desktop. It will automatically open a browser
window when the user activates desktop mode on the iPhone, and close the
browser window when the user exits desktop mode. This is done by listening
for MyDB Web UI service announcements.
- announce_service() is used by Web_UI.py, to announce that MyDB's Web UI
service is available / unavailable.
Revision history:
22-Jul-2019 TPO - Created this module
25-Jul-2019 TPO - Initial release """
import json
import os
import socket
import subprocess
import time
import threading
# When the following variable is set to True, both service_daemon() and
# announce_service() will print debug information on stdout.
DEBUG = True
# Change the following 2 variables if using another browser than Firefox:
START_BROWSER = [(r'"C:\Program Files (x86)\Mozilla Firefox\Firefox.exe" '
r'-new-window {ip}/'),
(r'"C:\Program Files\Mozilla Firefox\Firefox.exe" '
r'-new-window {ip}/')]
STOP_BROWSER = r'TASKKILL /IM firefox.exe /FI "WINDOWTITLE eq MyDB*"'
ANNOUNCE_PORT = 50000
ACK_PORT = 50001
MAGIC = "XXMYDBXX"
ANNOUNCE_COUNTER = 0
def debug(message: str) -> None:
global DEBUG
if DEBUG:
print(message)
def announce_service(ip: str, available: bool) -> bool:
""" Announce that MyDB's Web UI service is available / unavailable.
Broadcast the status of the MyDB Web UI service, so that the desktop
daemon can open a browser window when the user activates desktop mode on
the iPhone, and close the browser window when the user exits desktop mode.
Arguments:
- ip: string containing our IP address.
- available: if True, announce that the service is now available. If False,
announce that the service is now unavailable.
Returns: True if the announcement has been received and ackowledged by the
desktop daemon, False otherwise.
Two broadcast modes are tried:
- UDP broadcast is tried first (code is courtesy of goncalopp,
https://stackoverflow.com/a/21090815)
- If UDP broadcast fails, as can happen on LANs where broadcasting packets
are filtered by the routers (think airport or hotel LAN), a brute force
method is tried, by sending the announcement packet to 254 IP adresses,
using values 1 - 255 for byte 4 of our own IP address (should actually
use subnet mask, but this is a quick and dirty kludge !)
TODO: document ACK mechanism + counter and session id """
def do_brute_force_broadcast(s: socket.socket,
ip: str,
port: int,
data: bytes) -> None:
ip_bytes_1_to_3 = ip[:ip.rfind('.')] + '.'
for i in range(1, 255):
print(i, sep=" ")
s.sendto(data, (ip_bytes_1_to_3 + str(i), port))
print(".")
global ACK_PORT, ANNOUNCE_PORT, MAGIC, ANNOUNCE_COUNTER
SOCKET_TIMEOUT = 0.3
data = json.dumps({'magic': MAGIC,
'counter': ANNOUNCE_COUNTER,
'service available': available,
'IP': ip,
'Session id': time.time()}).encode('utf-8')
snd = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
snd.bind(('', ANNOUNCE_PORT))
rcv = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
rcv.bind(('', ACK_PORT))
rcv.settimeout(SOCKET_TIMEOUT)
ack = False
debug(f"Counter = {ANNOUNCE_COUNTER}, announcing service is "
f"{'ON' if available else 'OFF'}")
for brute_force, retries in ((False, 3), (True, 8)):
debug(f" Trying {'Brute force' if brute_force else 'UDP'} broadcast")
snd.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, not brute_force)
for retry in range(retries):
debug(f" Retry # {retry}")
if brute_force:
brute_force_broadcast_thread = threading.Thread(
target=do_brute_force_broadcast,
args=(snd, ip, ANNOUNCE_PORT, data))
brute_force_broadcast_thread.start()
else:
snd.sendto(data, ('<broadcast>', ANNOUNCE_PORT))
while 1:
try:
data_bytes, addr = rcv.recvfrom(1024)
except socket.timeout:
debug(" Socket time out, going for next retry")
break
debug(f" Received {data_bytes}")
try:
data = json.loads(data_bytes.decode('utf-8'))
except json.JSONDecodeError:
debug(" Invalid JSON, ignoring")
continue
if (isinstance(data, dict)
and data.get('magic') == MAGIC
and data.get('counter') == ANNOUNCE_COUNTER
and data.get('IP') == ip):
debug(" ACK received")
ack = True
break
print(" Invalid ACK, ignoring")
if ack:
break
if brute_force:
# Need to wait for broadcast_thread to be done before we proceed
# to close the snd socket, or do_brute_force_broadcast() will fail
# with "Errno 9: Bad file descriptor".
brute_force_broadcast_thread.join()
if ack:
break
if not ack:
debug(" Both UDP and brute force broadcast methods failed, giving up")
snd.close()
rcv.close()
ANNOUNCE_COUNTER += 1
return ack
def service_daemon():
""" Automatically open / close web browser on desktop.
service_daemon() runs on the desktop. It will automatically open a browser
window when the user activates desktop mode on the iPhone, and close the
browser window when the user exits desktop mode. This is done by listening
for MyDB Web UI service announcements. """
global ACK_PORT, ANNOUNCE_PORT, MAGIC
# Keep track of the counter value for last annoucement packet processed, in
# order to ignore retry packets sent by announce_service(), which all have
# the same counter value.
last_counter = -1
# Web_UI sessions all start with a counter value of 0, so we need to keep
# track of Web_UI sessions and reset last_counter every time a new session
# is started (i.e. when the user activates desktop mode on the iPhone)
current_session_id = -1
rcv = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
rcv.bind(('', ANNOUNCE_PORT))
snd = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
snd.bind(('', ACK_PORT))
debug(f"Listening on port {ANNOUNCE_PORT}")
while 1:
data_bytes, addr = rcv.recvfrom(1024)
debug(f"Received packet from {addr}:\n '{data_bytes}'")
try:
data = json.loads(data_bytes.decode('utf-8'))
except json.JSONDecodeError:
debug(" Invalid JSON, ignoring")
continue
if (isinstance(data, dict)
and data.get('magic') == MAGIC
and 'counter' in data
and 'IP' in data
and 'service available' in data
and 'Session id' in data):
if (data['Session id'] == current_session_id
and data['counter'] <= last_counter):
debug(f" Ignoring MyDB announcement for counter = "
f"{data['counter']}, already processed")
continue
current_session_id = data['Session id']
last_counter = data['counter']
debug(f" MyDB announcement: IP = {data['IP']}, "
f"service {'ON' if data['service available'] else 'OFF'}, "
f"counter = {data['counter']}")
ack = json.dumps({'magic': MAGIC,
'counter': data['counter'],
'IP': data['IP']}).encode('utf-8')
snd.sendto(ack, (data['IP'], ACK_PORT))
debug(f" ACK sent back to {data['IP']}:{ACK_PORT}")
if data['service available']:
debug(" Launching browser")
for start_browser in START_BROWSER:
try:
subprocess.Popen(start_browser.format(ip=data['IP']))
except FileNotFoundError:
continue
break
else:
debug(" Closing browser")
os.system(STOP_BROWSER)
else:
debug(" Not a MyDB announcement, ignoring")
if __name__ == '__main__':
service_daemon()