| package systemd |
| |
| import ( |
| "context" |
| "errors" |
| "fmt" |
| "sync" |
| |
| systemdDbus "github.com/coreos/go-systemd/v22/dbus" |
| dbus "github.com/godbus/dbus/v5" |
| ) |
| |
| var ( |
| dbusC *systemdDbus.Conn |
| dbusMu sync.RWMutex |
| dbusInited bool |
| dbusRootless bool |
| ) |
| |
| type dbusConnManager struct{} |
| |
| // newDbusConnManager initializes systemd dbus connection manager. |
| func newDbusConnManager(rootless bool) *dbusConnManager { |
| dbusMu.Lock() |
| defer dbusMu.Unlock() |
| if dbusInited && rootless != dbusRootless { |
| panic("can't have both root and rootless dbus") |
| } |
| dbusInited = true |
| dbusRootless = rootless |
| return &dbusConnManager{} |
| } |
| |
| // getConnection lazily initializes and returns systemd dbus connection. |
| func (d *dbusConnManager) getConnection() (*systemdDbus.Conn, error) { |
| // In the case where dbusC != nil |
| // Use the read lock the first time to ensure |
| // that Conn can be acquired at the same time. |
| dbusMu.RLock() |
| if conn := dbusC; conn != nil { |
| dbusMu.RUnlock() |
| return conn, nil |
| } |
| dbusMu.RUnlock() |
| |
| // In the case where dbusC == nil |
| // Use write lock to ensure that only one |
| // will be created |
| dbusMu.Lock() |
| defer dbusMu.Unlock() |
| if conn := dbusC; conn != nil { |
| return conn, nil |
| } |
| |
| conn, err := d.newConnection() |
| if err != nil { |
| // When dbus-user-session is not installed, we can't detect whether we should try to connect to user dbus or system dbus, so d.dbusRootless is set to false. |
| // This may fail with a cryptic error "read unix @->/run/systemd/private: read: connection reset by peer: unknown." |
| // https://github.com/moby/moby/issues/42793 |
| return nil, fmt.Errorf("failed to connect to dbus (hint: for rootless containers, maybe you need to install dbus-user-session package, see https://github.com/opencontainers/runc/blob/master/docs/cgroup-v2.md): %w", err) |
| } |
| dbusC = conn |
| return conn, nil |
| } |
| |
| func (d *dbusConnManager) newConnection() (*systemdDbus.Conn, error) { |
| if dbusRootless { |
| return newUserSystemdDbus() |
| } |
| return systemdDbus.NewWithContext(context.TODO()) |
| } |
| |
| // resetConnection resets the connection to its initial state |
| // (so it can be reconnected if necessary). |
| func (d *dbusConnManager) resetConnection(conn *systemdDbus.Conn) { |
| dbusMu.Lock() |
| defer dbusMu.Unlock() |
| if dbusC != nil && dbusC == conn { |
| dbusC.Close() |
| dbusC = nil |
| } |
| } |
| |
| // retryOnDisconnect calls op, and if the error it returns is about closed dbus |
| // connection, the connection is re-established and the op is retried. This helps |
| // with the situation when dbus is restarted and we have a stale connection. |
| func (d *dbusConnManager) retryOnDisconnect(op func(*systemdDbus.Conn) error) error { |
| for { |
| conn, err := d.getConnection() |
| if err != nil { |
| return err |
| } |
| err = op(conn) |
| if err == nil { |
| return nil |
| } |
| if !errors.Is(err, dbus.ErrClosed) { |
| return err |
| } |
| d.resetConnection(conn) |
| } |
| } |