├── .gitignore ├── LICENSE.md ├── README.md ├── config.py ├── icons ├── fail.svg ├── main.svg └── ok.svg ├── servicectl ├── servicectl.ui └── systemctl.py /.gitignore: -------------------------------------------------------------------------------- 1 | /*.pyc 2 | /*~ 3 | /install-dev.sh 4 | /README.html -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | ===================== 3 | 4 | Copyright © `2016` `Stanislav Tamat` 5 | 6 | Permission is hereby granted, free of charge, to any person 7 | obtaining a copy of this software and associated documentation 8 | files (the “Software”), to deal in the Software without 9 | restriction, including without limitation the rights to use, 10 | copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | copies of the Software, and to permit persons to whom the 12 | Software is furnished to do so, subject to the following 13 | conditions: 14 | 15 | The above copyright notice and this permission notice shall be 16 | included in all copies or substantial portions of the Software. 17 | 18 | THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, 19 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 20 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 21 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 22 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 23 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 24 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 25 | OTHER DEALINGS IN THE SOFTWARE. 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Service Control 2 | 3 | GUI tray program to control systemd (systemctl) services 4 | 5 | ![image](https://cloud.githubusercontent.com/assets/1845813/14977395/45150b08-1135-11e6-979e-e9017c518efd.png) 6 | 7 | ## Dependencies 8 | 9 | In Ubuntu, you need install following package: 10 | ``` 11 | sudo apt-get update 12 | sudo apt-get install gir1.2-appindicator3-0.1 13 | ``` 14 | 15 | If you get `ImportError: No module named pam` you need install 16 | manually from [Python.org](https://pypi.python.org/pypi/python-pam) or 17 | `pip install python-pam` 18 | 19 | ## License 20 | 21 | Licensed under the [MIT License](https://opensource.org/licenses/mit-license.php) -------------------------------------------------------------------------------- /config.py: -------------------------------------------------------------------------------- 1 | services = { 2 | 'apache2', 3 | 'mongodb', 4 | 'mysql', 5 | 'nginx', 6 | 'php5-fpm', 7 | 'ssh' 8 | } 9 | 10 | services = sorted(services) 11 | 12 | settings = { 13 | 'start_show': True 14 | } 15 | -------------------------------------------------------------------------------- /icons/fail.svg: -------------------------------------------------------------------------------- 1 | 2 | 17 | 19 | 20 | 22 | image/svg+xml 23 | 25 | 26 | 27 | 28 | 30 | 50 | 56 | 57 | -------------------------------------------------------------------------------- /icons/main.svg: -------------------------------------------------------------------------------- 1 | 2 | 17 | 19 | 20 | 22 | image/svg+xml 23 | 25 | 26 | 27 | 28 | 30 | 50 | 57 | 58 | -------------------------------------------------------------------------------- /icons/ok.svg: -------------------------------------------------------------------------------- 1 | 2 | 17 | 19 | 20 | 22 | image/svg+xml 23 | 25 | 26 | 27 | 28 | 30 | 50 | 56 | 57 | -------------------------------------------------------------------------------- /servicectl: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | 4 | import gi 5 | import systemctl 6 | import config 7 | gi.require_version('Gtk', '3.0') 8 | from gi.repository import Gtk, Gdk, GdkPixbuf 9 | 10 | APPIND_SUPPORT = True 11 | STATE_ID = 2 12 | ICON_MAIN = "icons/main.svg" 13 | ICON_OK = "icons/ok.svg" 14 | ICON_FAIL = "icons/fail.svg" 15 | 16 | try: 17 | gi.require_version('AppIndicator3', '0.1') 18 | from gi.repository import AppIndicator3 19 | except: 20 | APPIND_SUPPORT = False 21 | 22 | class ServiceControl: 23 | def __init__(self, title): 24 | self.systemctl = systemctl.Systemctl() 25 | 26 | self.icon = "%s/%s" % (self.systemctl.get_path(), ICON_MAIN) 27 | self.title = title 28 | 29 | self.builder = Gtk.Builder() 30 | self.builder.add_from_file("servicectl.ui") 31 | handlers = { 32 | "onSelectionChanged": lambda x: self.on_selection_changed(), 33 | "onButtonStartStopClicked": lambda x: self.on_button_click_multi_action("startstop"), 34 | "onButtonRestartClicked": lambda x: self.on_button_click("restart"), 35 | "onButtonReloadClicked": lambda x: self.on_button_click("reload") 36 | } 37 | self.builder.connect_signals(handlers) 38 | 39 | self.menu = self.builder.get_object("popupmenu") 40 | 41 | if APPIND_SUPPORT: 42 | self.tray = AppIndicator3.Indicator.new("servicectl", self.icon, AppIndicator3.IndicatorCategory.SYSTEM_SERVICES) 43 | self.tray.set_status(AppIndicator3.IndicatorStatus.ACTIVE) 44 | self._add_menu_item(self.on_click, "Show/Hide") 45 | self.tray.set_menu(self.menu) 46 | self.tray.set_title(title) 47 | else: 48 | self.tray = Gtk.StatusIcon() 49 | self.tray.set_from_file(self.icon) 50 | self.tray.connect('activate', self.on_click) 51 | self.tray.connect('popup-menu', self.on_r_click) 52 | self.tray.set_tooltip_text(title) 53 | 54 | self._add_menu_item(self.about, "About") 55 | self._add_menu_item(Gtk.main_quit, "Quit") 56 | 57 | self.window = self.builder.get_object("window") 58 | self.treeview = self.builder.get_object("treeview") 59 | self.model = self.treeview.get_model() 60 | 61 | col_state = self.builder.get_object("col-state") 62 | self.cell_state = self.builder.get_object("cell-state") 63 | col_state.set_cell_data_func(self.cell_state, self._render_icon) 64 | 65 | for service in config.services: 66 | description = self.systemctl.description(service) 67 | state = self.systemctl.is_active(service) 68 | self.model.append((service, description, state)) 69 | 70 | hide_btn_image = Gtk.Image.new_from_icon_name("window-close-symbolic", Gtk.IconSize.MENU) 71 | hide_btn = Gtk.Button(image=hide_btn_image) 72 | hide_btn.connect_object("clicked", lambda x: self.hide(), self.window) 73 | 74 | headerbar = Gtk.HeaderBar() 75 | headerbar.set_show_close_button(False) 76 | headerbar.set_title(title) 77 | headerbar.pack_end(hide_btn) 78 | 79 | self.window.set_titlebar(headerbar) 80 | self.window.set_icon_from_file(self.icon) 81 | self.window.show_all() 82 | 83 | ################# 84 | ## EVENTS HANDLE 85 | ################# 86 | 87 | def on_click(self, w): 88 | if self.window.get_property("visible"): 89 | self.hide() 90 | else: 91 | self.show() 92 | 93 | def on_r_click(self, icon, button, time): 94 | self._get_tray_menu() 95 | 96 | def pos(menu, aicon): 97 | return (Gtk.StatusIcon.position_menu(menu, aicon)) 98 | 99 | self.menu.popup(None, None, pos, icon, button, time) 100 | 101 | def on_button_click_multi_action(self, action): 102 | if action == "startstop": 103 | if self.current_state: 104 | action = "stop" 105 | else: 106 | action = "start" 107 | self.on_button_click(action) 108 | 109 | def on_button_click(self, action): 110 | if not hasattr(self, "pwd"): 111 | self.pwd = self.get_pwd() 112 | if self.systemctl.chk_pwd(self.pwd): 113 | while self.systemctl.run(self.pwd, action, self.current_service): 114 | pass 115 | self.update() 116 | else: 117 | delattr(self, "pwd") 118 | self.incorrect_pwd() 119 | 120 | def on_selection_changed(self): 121 | treeselection = self.builder.get_object("treeview-selection") 122 | (model, iter) = treeselection.get_selected() 123 | self.current_iter = iter 124 | self.current_service = model.get_value(iter, 0) 125 | self.current_state = model.get_value(iter, STATE_ID) 126 | self._buttons_toogle(self.current_state) 127 | 128 | def on_key_release(self, w, e): 129 | if e.keyval == Gdk.KEY_Return: 130 | w.response(Gtk.ResponseType.OK) 131 | if e.keyval == Gdk.KEY_Escape: 132 | w.destroy() 133 | 134 | ########### 135 | ## ACTIONS 136 | ########### 137 | 138 | def update(self): 139 | state = self.systemctl.is_active(self.current_service) 140 | icon = self.cell_state.get_property('pixbuf') 141 | self.model.set_value(self.current_iter, STATE_ID, state) 142 | self._render_icon(STATE_ID, self.cell_state, self.model, self.current_iter, icon) 143 | self.on_selection_changed() 144 | 145 | def show(self): 146 | self.window.show_all() 147 | 148 | def hide(self): 149 | self.window.hide() 150 | 151 | ########## 152 | ## DIALOG 153 | ########## 154 | 155 | def about(self, w): 156 | title = "About | %s" % (self.title,) 157 | dialog = self.builder.get_object("aboutdialog") 158 | 159 | headerbar = Gtk.HeaderBar() 160 | headerbar.set_show_close_button(False) 161 | headerbar.set_title(title) 162 | headerbar.show() 163 | 164 | pixbuf = GdkPixbuf.Pixbuf.new_from_file(self.icon) 165 | 166 | dialog.set_titlebar(headerbar) 167 | dialog.set_logo(pixbuf) 168 | dialog.run() 169 | dialog.hide() 170 | 171 | def get_pwd(self): 172 | title = "Password | %s" % (self.title,) 173 | 174 | dialog = self.builder.get_object("getpwddialog") 175 | dialog.connect("key-release-event", self.on_key_release) 176 | 177 | headerbar = Gtk.HeaderBar() 178 | headerbar.set_show_close_button(False) 179 | headerbar.set_title(title) 180 | headerbar.show() 181 | 182 | dialog.set_titlebar(headerbar) 183 | dialog.show_all() 184 | 185 | response = dialog.run() 186 | entry = self.builder.get_object("entry") 187 | password = entry.get_text() 188 | 189 | dialog.hide() 190 | 191 | if (response == Gtk.ResponseType.OK) and (password != ''): 192 | return password 193 | return None 194 | 195 | def incorrect_pwd(self): 196 | title = "Error | %s" % (self.title,) 197 | 198 | dialog = self.builder.get_object("incorrectpwddialog") 199 | dialog.connect("key-release-event", self.on_key_release) 200 | 201 | headerbar = Gtk.HeaderBar() 202 | headerbar.set_show_close_button(False) 203 | headerbar.set_title(title) 204 | 205 | dialog.set_titlebar(headerbar) 206 | dialog.show_all() 207 | dialog.run() 208 | dialog.hide() 209 | 210 | ######## 211 | ## MISC 212 | ######## 213 | def _render_icon(self, column, cell, model, iter, icon): 214 | data = self.model.get_value(iter, STATE_ID) 215 | if data: 216 | icon = ICON_OK 217 | else: 218 | icon = ICON_FAIL 219 | 220 | pixbuf = GdkPixbuf.Pixbuf.new_from_file_at_scale(icon, width=22, height=22, preserve_aspect_ratio=False) 221 | cell.set_property('pixbuf', pixbuf) 222 | 223 | def _buttons_toogle(self, state): 224 | button_enable_disable = self.builder.get_object('button_enable_disable') 225 | button_start_stop = self.builder.get_object('button_start_stop') 226 | button_restart = self.builder.get_object('button_restart') 227 | button_reload = self.builder.get_object('button_reload') 228 | if state: 229 | button_restart.set_sensitive(True) 230 | button_reload.set_sensitive(True) 231 | else: 232 | button_restart.set_sensitive(False) 233 | button_reload.set_sensitive(False) 234 | 235 | def _add_menu_item(self, command, title): 236 | menuitem = Gtk.MenuItem() 237 | menuitem.set_label(title) 238 | menuitem.connect("activate", command) 239 | 240 | self.menu.append(menuitem) 241 | self.menu.show_all() 242 | 243 | def _get_tray_menu(self): 244 | return self.menu 245 | 246 | def main(self): 247 | Gtk.main() 248 | 249 | if __name__ == '__main__': 250 | app = ServiceControl("Service Control") 251 | if config.settings["start_show"]: 252 | app.show() 253 | app.main() 254 | -------------------------------------------------------------------------------- /servicectl.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | False 17 | False 18 | 19 | 20 | True 21 | False 22 | baseline 23 | 24 | 25 | True 26 | False 27 | 5 28 | 5 29 | 5 30 | vertical 31 | 5 32 | 33 | 34 | Start / Stop 35 | True 36 | True 37 | True 38 | 39 | 40 | 41 | False 42 | True 43 | 1 44 | 45 | 46 | 47 | 48 | Restart 49 | True 50 | True 51 | True 52 | 53 | 54 | 55 | False 56 | True 57 | 2 58 | 59 | 60 | 61 | 62 | Reload 63 | True 64 | True 65 | True 66 | 67 | 68 | 69 | False 70 | True 71 | 3 72 | 73 | 74 | 75 | 76 | 1 77 | 0 78 | 79 | 80 | 81 | 82 | True 83 | True 84 | 5 85 | 5 86 | 5 87 | queue 88 | in 89 | 350 90 | 250 91 | 92 | 93 | True 94 | True 95 | False 96 | queue 97 | liststore 98 | False 99 | False 100 | both 101 | 0 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | Service 110 | True 111 | 112 | 113 | 114 | 0 115 | 116 | 117 | 118 | 119 | 120 | 121 | 200 122 | Description 123 | 124 | 125 | 126 | 1 127 | 128 | 129 | 130 | 131 | 132 | 133 | 60 134 | Running 135 | 1 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 0 146 | 0 147 | 148 | 149 | 150 | 151 | 152 | 153 | False 154 | False 155 | True 156 | normal 157 | west 158 | window 159 | Service Control 160 | 0.5 161 | (C) 2016 Stanislav Tamat 162 | https://github.com/YokiToki/service_ctl 163 | https://github.com/YokiToki/service_ctl 164 | Stanislav Tamat <libastral.so@yandex.ru> 165 | mit-x11 166 | 167 | 168 | False 169 | vertical 170 | 2 171 | 172 | 173 | False 174 | end 175 | 176 | 177 | False 178 | False 179 | 0 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | False 190 | False 191 | True 192 | dialog 193 | window 194 | question 195 | ok-cancel 196 | This application lets you modify essential parts of your system. 197 | 198 | 199 | False 200 | vertical 201 | 2 202 | 203 | 204 | False 205 | True 206 | expand 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | False 216 | False 217 | 0 218 | 219 | 220 | 221 | 222 | True 223 | True 224 | 10 225 | 10 226 | False 227 | * 228 | password 229 | 230 | 231 | False 232 | True 233 | 2 234 | 235 | 236 | 237 | 238 | 239 | 240 | False 241 | False 242 | True 243 | dialog 244 | window 245 | ok 246 | You have entered the wrong password. Please, try again. 247 | 248 | 249 | False 250 | vertical 251 | 2 252 | 253 | 254 | False 255 | True 256 | expand 257 | 258 | 259 | 260 | 261 | 262 | 263 | 264 | 265 | False 266 | False 267 | 0 268 | 269 | 270 | 271 | 272 | 273 | 274 | True 275 | False 276 | 277 | 278 | -------------------------------------------------------------------------------- /systemctl.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | 4 | import os 5 | 6 | PAM_LIB = True 7 | try: 8 | import PAM 9 | except ImportError: 10 | PAM_LIB = False 11 | import pam 12 | 13 | class Systemctl(): 14 | 15 | def get_path(self): 16 | return os.path.dirname(os.path.realpath(__file__)) 17 | 18 | def run(self, pwd, action, service): 19 | os.system('echo \'%s\'|sudo -S systemctl %s %s.service' % (pwd, action, service)) 20 | 21 | def is_enabled(self, service): 22 | out = os.popen("systemctl is-enabled %s.service" % (service,)).read().strip() 23 | if out == 'enabled': 24 | return True 25 | return False 26 | 27 | def is_active(self, service): 28 | out = os.popen("systemctl is-active %s.service" % (service,)).read().strip() 29 | if out == 'active': 30 | return True 31 | return False 32 | 33 | def description(self, service): 34 | return os.popen("systemctl show -p Description %s.service" % (service,)).read().replace("Description=", "").strip() 35 | 36 | def chk_pwd(self, pwd): 37 | user = os.getenv('USER') 38 | if pwd == None: 39 | return False 40 | if PAM_LIB: 41 | def pam_conv(a, q, d): 42 | return [(pwd, 0)] 43 | 44 | auth = PAM.pam() 45 | auth.start("passwd") 46 | auth.set_item(PAM.PAM_USER, user) 47 | auth.set_item(PAM.PAM_CONV, pam_conv) 48 | try: 49 | auth.authenticate() 50 | auth.acct_mgmt() 51 | except: 52 | return False 53 | else: 54 | return True 55 | 56 | else: 57 | 58 | auth = pam.pam() 59 | return auth.authenticate(user, pwd) 60 | --------------------------------------------------------------------------------