Bonjour à tous et à toutes,

Je viens vous voir en ce jour pour un mystérieux problème incluant socket unix et daemon.

Avant de rentrer dans les détails, je vais juste vous expliquer dans quel cadre j'en suis venu à faire ça. J'ai créer une interface web via django, une interface permettant la configuration d'un moteur de supervision. Dans mon instance django je me suis retrouvé modifier des paramètres systèmes, modifier des fichiers dont je n'avais pas les droits à l'aide de script inclue dans le visudo, des mises à jour de son propre code source etc.

C'est alors qu'à force de discussion, une idée m'a été remonté, pourquoi ne pas séparer la partie système dans un daemon et la partie web dans mon projet django. J'ai donc créer un daemon en python, et pour l'interaction entre mon daemon et mon instance web, j'ai opté pour un socket UNIX auquel je passe des commandes simples.

J'ai implémenté deux fonctions pour le moment dans mon daemon, la première permet de lancer des actions sur les services de la machine (start|stop|restart|whatever) et l'autre permet de faire une mise à jour de plusieurs chose :
- Le code de mon instance django
- Le code de mon moteur de supervision
- Le code de mon daemon

Et c'est pour ce dernier que j'ai un soucis, si je met à jour le code de mon daemon, il faut que mon worker de mise à jour redémarre le deamon, il faut donc qu'il se redémarre lui même, ce qui bien sûr ferme le socket Unix.
Et le gros problème c'est que lors du redémarrage de mon daemon, celui-ci s'arrête juste avant d'exécuter la fonction run() permettant la réception des messages, mon socket n'est donc plus du tout accessible et je dois le redémarrer à la main (Redémarrage à la main qui fonctionne d'ailleurs).

Du coup voilà le code permettant l'implémentation du daemon que j'ai trouver sur le net et que j'ai un tout petit peu modifier (Crédit à Sander Marechal):
Code python : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
#!/usr/bin/env python
'''
***
Modified generic daemon class
***
 
Author:         http://www.jejik.com/articles/2007/02/
                        a_simple_unix_linux_daemon_in_python/www.boxedice.com
 
License:        http://creativecommons.org/licenses/by-sa/3.0/
 
Changes:        23rd Jan 2009 (David Mytton <david@boxedice.com>)
                - Replaced hard coded '/dev/null in __init__ with os.devnull
                - Added OS check to conditionally remove code that doesn't
                  work on OS X
                - Added output to console on completion
                - Tidied up formatting
                11th Mar 2009 (David Mytton <david@boxedice.com>)
                - Fixed problem with daemon exiting on Python 2.4
                  (before SystemExit was part of the Exception base)
                13th Aug 2010 (David Mytton <david@boxedice.com>
                - Fixed unhandled exception if PID file is empty
'''
 
# Core modules
import atexit
import os
import sys
import time
import signal
import pwd
 
 
class Daemon(object):
    """
    A generic daemon class.
 
    Usage: subclass the Daemon class and override the run() method
    """
 
    def __init__(self, pidfile, stdin=os.devnull,
                 stdout=os.devnull, stderr=os.devnull,
                 home_dir='.', umask=022, verbose=1, use_gevent=False):
        self.stdin = stdin
        self.stdout = stdout
        self.stderr = stderr
        self.pidfile = pidfile
        self.home_dir = home_dir
        self.verbose = verbose
        self.umask = umask
        self.daemon_alive = True
        self.use_gevent = use_gevent
 
    def daemonize(self):
        """
        Do the UNIX double-fork magic, see Stevens' "Advanced
        Programming in the UNIX Environment" for details (ISBN 0201563177)
        http://www.erlenstar.demon.co.uk/unix/faq_2.html#SEC16
        """
        try:
            pid = os.fork()
            if pid > 0:
                # Exit first parent
                sys.exit(0)
        except OSError, e:
            sys.stderr.write(
                "fork #1 failed: %d (%s)\n" % (e.errno, e.strerror))
            sys.exit(1)
 
        # Decouple from parent environment
        os.chdir(self.home_dir)
        os.setsid()
        os.umask(self.umask)
 
        # Do second fork
        try:
            pid = os.fork()
            if pid > 0:
                # Exit from second parent
                sys.exit(0)
        except OSError, e:
            sys.stderr.write(
                "fork #2 failed: %d (%s)\n" % (e.errno, e.strerror))
            sys.exit(1)
 
        if sys.platform != 'darwin':  # This block breaks on OS X
            # Redirect standard file descriptors
            sys.stdout.flush()
            sys.stderr.flush()
            si = file(self.stdin, 'r')
            so = file(self.stdout, 'a+')
            if self.stderr:
                se = file(self.stderr, 'a+', 0)
            else:
                se = so
            os.dup2(si.fileno(), sys.stdin.fileno())
            os.dup2(so.fileno(), sys.stdout.fileno())
            os.dup2(se.fileno(), sys.stderr.fileno())
 
        def sigtermhandler(signum, frame):
            self.daemon_alive = False
            sys.exit()
 
        if self.use_gevent:
            import gevent
            gevent.reinit()
            gevent.signal(signal.SIGTERM, sigtermhandler, signal.SIGTERM, None)
            gevent.signal(signal.SIGINT, sigtermhandler, signal.SIGINT, None)
        else:
            signal.signal(signal.SIGTERM, sigtermhandler)
            signal.signal(signal.SIGINT, sigtermhandler)
 
        if self.verbose >= 1:
            print "Started"
 
        # Write pidfile
        atexit.register(
            self.delpid)  # Make sure pid file is removed if we quit
        pid = str(os.getpid())
        file(self.pidfile, 'w+').write("%s\n" % pid)
 
        # If daemon user is set change current user to self.daemon_user
        if self.daemon_user:
            try:
                uid = pwd.getpwnam(self.daemon_user)[2]
                print self.daemon_user
                print uid
                os.setuid(uid)
            except NameError, e:
                print "NameError %s" % e
                return 4
            except OSError, e:
                print "OSError %s" % e
                return 4
        return 0
 
    def delpid(self):
        os.remove(self.pidfile)
 
    def start(self, *args, **kwargs):
        """
        Start the daemon
        """
 
        if self.verbose >= 1:
            print "Starting..."
 
        # Check for a pidfile to see if the daemon already runs
        try:
            pf = file(self.pidfile, 'r')
            pid = int(pf.read().strip())
            pf.close()
        except IOError:
            pid = None
        except SystemExit:
            pid = None
 
        if pid:
            message = "pidfile %s already exists. Is it already running?\n"
            sys.stderr.write(message % self.pidfile)
            sys.exit(1)
 
        # Start the daemon
        self.daemonize()
        self.run(*args, **kwargs)
 
    def stop(self):
        """
        Stop the daemon
        """
 
        if self.verbose >= 1:
            print "Stopping..."
 
        # Get the pid from the pidfile
        pid = self.get_pid()
 
        if not pid:
            message = "pidfile %s does not exist. Not running?\n"
            sys.stderr.write(message % self.pidfile)
 
            # Just to be sure. A ValueError might occur if the PID file is
            # empty but does actually exist
            if os.path.exists(self.pidfile):
                os.remove(self.pidfile)
 
            return  # Not an error in a restart
 
        # Try killing the daemon process
        try:
            i = 0
            while 1:
                os.kill(pid, signal.SIGTERM)
                time.sleep(0.1)
                i = i + 1
                if i % 10 == 0:
                    os.kill(pid, signal.SIGHUP)
        except OSError, err:
            err = str(err)
            if err.find("No such process") > 0:
                if os.path.exists(self.pidfile):
                    os.remove(self.pidfile)
            else:
                print str(err)
                sys.exit(1)
 
        if self.verbose >= 1:
            print "Stopped"
 
    def restart(self):
        """
        Restart the daemon
        """
        self.stop()
        self.start()
 
    def get_pid(self):
        try:
            pf = file(self.pidfile, 'r')
            pid = int(pf.read().strip())
            pf.close()
        except IOError:
            pid = None
        except SystemExit:
            pid = None
        return pid
 
    def is_running(self):
        pid = self.get_pid()
 
        if pid is None:
            print 'Process is stopped'
        elif os.path.exists('/proc/%d' % pid):
            print 'Process (pid %d) is running...' % pid
        else:
            print 'Process (pid %d) is killed' % pid
 
        return pid and os.path.exists('/proc/%d' % pid)
 
    def change_user(self, username):
        ''' Set user under which the daemonized process should be run '''
        if not isinstance(username, (str, unicode)):
            raise TypeError('username should be of type str or unicode')
        self.daemon_user = username
 
    def run(self):
        """
        You should override this method when you subclass Daemon.
        It will be called after the process has been
        daemonized by start() or restart().
        """
        raise NotImplementedError

Voilà mon implémentation de ce daemon :

Code python : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
#!/usr/bin/env python
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from __future__ import absolute_import
 
# Local import
from . import LOGGER, DIR_PATH, SETTINGS_DIR_PATH, DJANGO_SETTINGS_MODULE
from .daemon import Daemon
from .import_settings import import_settings
from .workers import Workers
 
# Django import
from django.utils.translation import ugettext as _
 
# OS working module import
import sys
import socket
import os
import pwd
 
 
class BackendDaemon(Daemon):
    """
    Some value are reserved for the communication signal :
    '/' = End of message
    '+' = No errors
    '-' = Error return after this signal
    """
 
    def __init__(self, *args):
        super(BackendDaemon, self).__init__(*args)
        self.SERVER_ADDRESS = os.path.join('/', 'tmp', 'test_socket')
        self.buffer_size = 100
 
        import_settings(DIR_PATH, SETTINGS_DIR_PATH, DJANGO_SETTINGS_MODULE)
 
        # Initialize LOGGER
        self.LOGGER = LOGGER
 
        # Set user to run daemon
        self.change_user('user')
        res = self.set_user()
        if res != 0:
            raise res
 
        os.environ['DEBIAN_FRONTEND'] = 'noninteractive'
 
    def start(self, *args, **kwargs):
        # Launch UNIX socket
        self._launch_socket()
        super(BackendDaemon, self).start(*args, **kwargs)
 
    def run(self):
        self.LOGGER.debug('Run')
        while True:
            self.LOGGER.debug('Run True')
            self._use_socket()
 
    def stop(self):
        self._close_socket()
        super(BackendDaemon, self).stop()
        self.LOGGER.info('Daemon stopped')
 
    def set_user(self):
        if self.daemon_user:
            try:
                uid = pwd.getpwnam(self.daemon_user)[2]
                os.setuid(uid)
            except NameError, e:
                self.LOGGER.error("NameError %s" % e)
                return e
            except OSError, e:
                self.LOGGER.error("OSError %s" % e)
                return e
        return 0
 
    def _launch_socket(self):
        # Make sure the socket does not already exist
        try:
            os.unlink(self.SERVER_ADDRESS)
        except OSError:
            if os.path.exists(self.SERVER_ADDRESS):
                raise
        self.LOGGER.info("Opening socket ")
        try:
            self.sock = socket.socket(socket.AF_UNIX, socket.SOCK_SEQPACKET)
            self.sock.bind(self.SERVER_ADDRESS)
        except Exception as err:
            self.LOGGER.error(err)
            sys.exit()
        self.LOGGER.info("Listen for incoming connection")
 
        self.sock.listen(1)
        self.LOGGER.info("Listening....")
 
    def _use_socket(self):
        self.LOGGER.debug('Use_socket')
        while True:
            self.LOGGER.info("Waiting for a connection")
            self.connection, self.client_address = self.sock.accept()
            try:
                while True:
                    data = self.connection.recv(self.buffer_size)
                    data = data.decode('utf-8')
                    if not data == '/':
                        self.LOGGER.info("Received %s" % data)
                        self._workers(data)
                    else:
                        self.LOGGER.info("Received end (%s) signal, sending back" % data)
                        self.connection.sendall('/')
                        break
            finally:
                pass
                self.connection.close()
                self.LOGGER.info("Connection closed")
 
    def _close_socket(self):
        if os.path.exists(self.SERVER_ADDRESS):
            os.unlink(self.SERVER_ADDRESS)
        self.LOGGER.info("Socket closed")
 
    def _workers(self, data):
        try:
            worker, data = self._unpack(data)
            if hasattr(Workers, worker):
                ack, ret = getattr(Workers, worker)(*data)
                ack = unicode(ack)
                ret = unicode(ret)
                self.LOGGER.debug('Ack : %s' % ack)
                self.LOGGER.debug('Ret : %s' % ret)
                self.connection.sendall(ack)
                ret = [ret[i:i + self.buffer_size] for i in range(0, len(ret), self.buffer_size)]
                for r in ret:
                    self.LOGGER.debug(r)
                    self.connection.sendall(r)
            else:
                error = _('Command sent does not exist') + ': %s' % worker
                self.LOGGER.error(error)
                self.connection.sendall(error)
        except TypeError:
            error = _('Argument number send for command') + ' \'%s\' ' % worker + _('is false')
            self.LOGGER.error(_('Worker error') + ' : %s' % error)
            self.connection.sendall('-')
            self.connection.sendall(error)
        except Exception as err:
            error = _('Worker error') + ' : %s' % err
            self.LOGGER.error(error)
            self.connection.sendall('-')
            self.connection.sendall(error)
 
    def _unpack(self, data):
        self.LOGGER.debug('Data : %s' % data)
        if ':' in data:
            data = data.split(':')
            self.LOGGER.debug('Data2 : %s' % data)
            worker = data[0]
            self.LOGGER.debug('Worker : %s' % worker)
            return worker, data[1:]
        else:
            return '-', Exception(
                'Command attribute does not exist, send command with attribute from_version and to_version')

Et enfin voilà le code de mon mini-client qui me sert à faire de mes tests :
Code python : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
import socket
import sys
import os
 
# Create env variable
SERVER_ADDRESS = os.path.join('/', 'tmp', 'test_socket')
BUFFER_SIZE = 100
 
# Create a UDS socket
server = socket.socket(socket.AF_UNIX, socket.SOCK_SEQPACKET)
 
# Connect the socket to the port where the server is listening
print >> sys.stderr, 'connecting to %s' % SERVER_ADDRESS
try:
    server.connect(SERVER_ADDRESS)
except socket.error, msg:
    print >> sys.stderr, msg
    sys.exit(1)
 
try:
 
    update_status = None
    # Send data
    # message = 'update:1.3.0:1.3.0'
    message = 'service:django_backend:restart'
    print >> sys.stderr, 'sending "%s"' % message
    server.sendall(message)
 
    message = '/'
    print 'sending "%s"' % message
    server.sendall(message)
 
    recv = ''
    while True:
        data = server.recv(BUFFER_SIZE)
        data = data.decode('utf-8')
        if data == '/':
            break
        elif data == '+':
            update_status = True
        elif data == '-':
            update_status = False
        else:
            recv += data
    if update_status:
        print 'L\'update est un succès'
    else:
        print recv
 
 
 
finally:
    print >> sys.stderr, 'closing socket'
    server.close()

Voilà, si jamais il y a besoin d'autre chose j'y répondrais avec plaisir et si quelqu’un à une solution pour gérer ça je suis tout ouïe.

Edit :
J'ai fini par trouver les lignes qui faisait planter, elles se situent dans le fichier daemon.py :

Code python : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
        if sys.platform != 'darwin':  # This block breaks on OS X
            # Redirect standard file descriptors
            sys.stdout.flush()
            sys.stderr.flush()

Donc on ne peut pas flush les sorties standards si on ne redémarre pas le daemon dans une console, chose que je ne savais pas. Ça n'avait donc rien à voir avec le socket.