diff --git a/agent-winservice.py b/agent-winservice.py
index ec11ffc..380fd5b 100644
--- a/agent-winservice.py
+++ b/agent-winservice.py
@@ -1,11 +1,22 @@
-import win32serviceutil
-import win32service
-import win32event
+import logging
+import os
 import servicemanager
 import socket
+import win32event
+import win32service
+import win32serviceutil
 
 from agent import main as agent_main, reactor
 
+logger = logging.getLogger()
+fh = logging.FileHandler(
+    os.path.join(os.path.dirname(__file__), "agent-service.log"))
+formatter = logging.Formatter(
+    "%(asctime)s - %(name)s [%(levelname)s] %(message)s")
+fh.setFormatter(formatter)
+logger.addHandler(fh)
+logger.info("%s loaded", __file__)
+
 
 class AppServerSvc (win32serviceutil.ServiceFramework):
     _svc_name_ = "circle-agent"
@@ -20,11 +31,13 @@ class AppServerSvc (win32serviceutil.ServiceFramework):
         self.ReportServiceStatus(win32service.SERVICE_STOP_PENDING)
         win32event.SetEvent(self.hWaitStop)
         reactor.stop()
+        logger.info("%s stopped", __file__)
 
     def SvcDoRun(self):
         servicemanager.LogMsg(servicemanager.EVENTLOG_INFORMATION_TYPE,
                               servicemanager.PYS_SERVICE_STARTED,
                               (self._svc_name_, ''))
+        logger.info("%s starting", __file__)
         agent_main()
 
 
diff --git a/agent.py b/agent.py
index e865e16..b180e0b 100644
--- a/agent.py
+++ b/agent.py
@@ -67,6 +67,7 @@ def linux_set_time(time):
 
 
 class Context(object):
+
     @staticmethod
     def change_password(password):
         if system == 'Linux':
@@ -282,8 +283,14 @@ class Context(object):
         except IOError:
             return None
 
+    @staticmethod
+    def send_expiration(url):
+        import notify
+        notify.notify(url)
+
 
 class SerialLineReceiver(SerialLineReceiverBase):
+
     def connectionMade(self):
         self.send_command(
             command='agent_started',
@@ -351,8 +358,13 @@ def main():
     else:
         port = '/dev/ttyS0'
     SerialPort(SerialLineReceiver(), port, reactor, baudrate=115200)
-
+    try:
+        from notify import register_publisher
+        register_publisher(reactor)
+    except:
+        logger.exception("Couldnt register notify publisher")
     reactor.run()
 
+
 if __name__ == '__main__':
     main()
diff --git a/client.pyw b/client.pyw
new file mode 100644
index 0000000..5e31e60
--- /dev/null
+++ b/client.pyw
@@ -0,0 +1,17 @@
+# Open urls in default web browser provided by circle agent
+# Part of CIRCLE project http://circlecloud.org/
+# Should be in autostart and run by the user logged in
+
+import logging
+logger = logging.getLogger()
+fh = logging.FileHandler("agent-client.log")
+formatter = logging.Formatter(
+    "%(asctime)s - %(name)s [%(levelname)s] %(message)s")
+fh.setFormatter(formatter)
+logger.addHandler(fh)
+
+
+from notify import run_client
+
+if __name__ == '__main__':
+    run_client()
diff --git a/notify.py b/notify.py
new file mode 100755
index 0000000..1d207e5
--- /dev/null
+++ b/notify.py
@@ -0,0 +1,228 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+##
+# Notify user about vm expiring
+##
+
+import cookielib
+import errno
+import json
+import logging
+import multiprocessing
+import os
+import platform
+import subprocess
+import urllib2
+
+logger = logging.getLogger()
+logger.debug("notify imported")
+file_name = "vm_renewal.json"
+win = platform.system() == "Windows"
+
+
+def parse_arguments():
+    import argparse
+    parser = argparse.ArgumentParser()
+    parser.add_argument("-u", "--url", type=str, required=True)
+    args = parser.parse_args()
+    return args
+
+
+def get_temp_dir():
+    if os.getenv("TMPDIR"):
+        temp_dir = os.getenv("TMPDIR")
+    elif os.getenv("TMP"):
+        temp_dir = os.getenv("TMP")
+    elif os.path.exists("/tmp"):
+        temp_dir = "/tmp"
+    elif os.path.exists("/var/tmp"):
+        temp_dir = "/var/tmp"
+    return temp_dir
+
+
+def wall(text):
+    if win:
+        return
+    if text is None:
+        logger.error("Incorrect function call")
+    else:
+        process = subprocess.Popen("wall", stdin=subprocess.PIPE, shell=True)
+        process.communicate(input=text)[0]
+
+
+def accept():
+    file_path = os.path.join(get_temp_dir(), file_name)
+    if not os.path.isfile(file_path):
+        print "There is no recent notification to accept."
+        return False
+
+    # Load the saved url
+    url = json.load(open(file_path, "r"))
+    cj = cookielib.CookieJar()
+    opener = urllib2.build_opener(urllib2.HTTPCookieProcessor(cj))
+
+    try:
+        opener.open(url)  # GET to collect cookies
+        cookies = cj._cookies_for_request(urllib2.Request(url))
+        token = [c for c in cookies if c.name == "csrftoken"][0].value
+        req = urllib2.Request(url, "", {
+            "accept": "application/json", "referer": url,
+            "x-csrftoken": token})
+        rsp = opener.open(req)
+        data = json.load(rsp)
+        newtime = data["new_suspend_time"]
+    except:
+        print "Renewal failed. Please try it manually at %s" % url
+        logger.exception("renew failed")
+        return False
+    else:
+        print "Renew succeeded. The machine will be suspended at %s." % newtime
+        os.remove(file_path)
+        return True
+
+
+def notify(url):
+    try:
+        logger.debug("notify(%s) called", url)
+        if win:
+            logger.info("notifying %d clients", len(clients))
+            for c in clients:
+                logger.debug("sending url %s to client %s", url, unicode(c))
+                c.sendLine(url.encode())
+        else:
+            file_path = os.path.join(get_temp_dir(), file_name)
+            if file_already_exists(file_path):
+                os.remove(file_path)
+                if file_already_exists(file_path):
+                    raise Exception(
+                        "Couldn't create file %s as new" %
+                        file_path)
+            with open(file_path, "w") as f:
+                json.dump(url, f)
+            wall("This virtual machine is going to expire! Please type \n"
+                 "  vm_renewal\n"
+                 "command to keep it running.")
+            logger.debug("wall sent, trying to start browser")
+            p = multiprocessing.Process(target=open_in_browser, args=(url, ))
+            p.start()
+    except:
+        logger.exception("Couldn't notify about %s" % url)
+
+
+def open_in_browser(url):
+    if not win:
+        display = search_display()
+        if display:
+            display, uid, gid = display
+            os.setgid(gid)
+            os.setuid(uid)
+            os.environ['DISPLAY'] = display
+            logger.debug("DISPLAY=%s", display)
+    else:
+        display = True
+
+    if display:
+        import webbrowser
+        webbrowser.open(url, new=2, autoraise=True)
+
+
+def file_already_exists(name, mode=0o644):
+    """Return whether file already exists, create it if not.
+
+    Other errors are silently ignored as the file will be reopened anyways.
+    Creating it is needed to avoid race condition.
+    """
+
+    try:
+        fd = os.open(name, os.O_CREAT | os.O_EXCL, mode)
+    except OSError as e:
+        if e.errno == errno.EEXIST:
+            return True
+    else:
+        os.close(fd)
+    return False
+
+
+def search_display():
+    """Search a valid DISPLAY env var in processes
+    """
+    env = os.getenv("DISPLAY")
+    if env:
+        return env
+
+    for pid in os.listdir("/proc"):
+        if not pid.isdigit():
+            continue
+        env = os.path.join("/proc", pid, "environ")
+        try:
+            with open(env, "r") as f:
+                envs = dict(line.split("=", 1)
+                            for line in f.read().split("\0") if "=" in line)
+            if "DISPLAY" in envs and ":" in envs["DISPLAY"]:
+                p = os.stat(os.path.join("/proc", pid))
+                return envs["DISPLAY"], p.st_uid, p.st_gid
+        except:
+            continue
+    return None
+
+if win:
+    from twisted.internet import protocol
+    from twisted.protocols import basic
+
+    clients = set()
+    port = 25683
+
+    class PubProtocol(basic.LineReceiver):
+
+        def __init__(self, factory):
+            self.factory = factory
+
+        def connectionMade(self):
+            logger.info("client connected: %s", unicode(self))
+            clients.add(self)
+
+        def connectionLost(self, reason):
+            logger.info("client disconnected: %s", unicode(self))
+            clients.remove(self)
+
+    class PubFactory(protocol.Factory):
+
+        def __init__(self):
+            clients.clear()
+
+        def buildProtocol(self, addr):
+            return PubProtocol(self)
+
+    def register_publisher(reactor):
+        reactor.listenTCP(port, PubFactory(), interface='localhost')
+
+    class SubProtocol(basic.LineReceiver):
+
+        def lineReceived(self, line):
+            print "received", line
+            open_in_browser(line)
+
+    class SubFactory(protocol.ReconnectingClientFactory):
+
+        def buildProtocol(self, addr):
+            return SubProtocol()
+
+    def run_client():
+        from twisted.internet import reactor
+        print "connect to localhost:%d" % port
+        reactor.connectTCP("localhost", port, SubFactory())
+        reactor.run()
+
+else:
+
+    def register_publisher(reactor):
+        pass
+
+
+def main():
+    args = parse_arguments()
+    notify(args.url)
+
+if __name__ == '__main__':
+    main()
diff --git a/vm_renewal b/vm_renewal
new file mode 100755
index 0000000..1ebe0fc
--- /dev/null
+++ b/vm_renewal
@@ -0,0 +1,10 @@
+#!/usr/bin/env python
+
+import notify
+
+if __name__ == '__main__':
+    try:
+        notify.accept()
+    except:
+        print ("There was an unknown error while trying to "
+            "renew this vm, please do it manually!")