summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorLuke Shumaker <lukeshu@sbcglobal.net>2017-01-01 19:52:51 -0700
committerLuke Shumaker <lukeshu@lukeshu.com>2017-05-15 19:09:20 -0400
commit16143217c55e70b592c8df72b3315c6bc3dbdfd6 (patch)
tree4416fdcfa833d52b76847340e6b5baf11ffbf8d4
parentcb4fea2e01e6923a016fdc57c48ee9d4045d8428 (diff)
sd_login: Initial WIP implementation
-rw-r--r--sd_login/.gitignore2
-rw-r--r--sd_login/Makefile28
-rw-r--r--sd_login/cgroup_generic.go163
-rw-r--r--sd_login/cgroup_skip.go98
-rwxr-xr-xsd_login/cgroup_skip_gen.go.gen41
-rw-r--r--sd_login/cgroup_systemd.go107
-rw-r--r--sd_login/doc.go38
-rw-r--r--sd_login/gen.go3
-rw-r--r--sd_login/missing.s0
-rw-r--r--sd_login/notes.org78
-rw-r--r--sd_login/type_machine.go73
-rw-r--r--sd_login/type_monitor.go106
-rwxr-xr-xsd_login/type_pid.go.gen63
-rw-r--r--sd_login/type_seat.go37
-rw-r--r--sd_login/type_session.go67
-rw-r--r--sd_login/type_user.go57
-rw-r--r--sd_login/util.go130
-rw-r--r--sd_login/util_syscall.go51
-rw-r--r--sd_login/util_valid.go141
19 files changed, 1283 insertions, 0 deletions
diff --git a/sd_login/.gitignore b/sd_login/.gitignore
new file mode 100644
index 0000000..84030d6
--- /dev/null
+++ b/sd_login/.gitignore
@@ -0,0 +1,2 @@
+/type_pid.go
+/cgroup_skip_gen.go
diff --git a/sd_login/Makefile b/sd_login/Makefile
new file mode 100644
index 0000000..055ee05
--- /dev/null
+++ b/sd_login/Makefile
@@ -0,0 +1,28 @@
+# Copyright (C) 2016-2017 Luke Shumaker <lukeshu@sbcglobal.net>
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+files.src.gen += type_pid.go
+files.src.gen += cgroup_skip_gen.go
+
+files.generate: $(files.src.gen)
+maintainer-clean:
+ rm -f -- $(files.src.gen)
+.PHONY: files.generate maintainer-clean
+
+%.go: %.go.gen
+ ./$^ > $@
+
+cgroup_skip_gen.go: cgroup_skip.go
+
+.DELETE_ON_ERROR:
diff --git a/sd_login/cgroup_generic.go b/sd_login/cgroup_generic.go
new file mode 100644
index 0000000..3842c64
--- /dev/null
+++ b/sd_login/cgroup_generic.go
@@ -0,0 +1,163 @@
+// Copyright (C) 2016-2017 Luke Shumaker <lukeshu@sbcglobal.net>
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package sd_login
+
+import (
+ "bufio"
+ "fmt"
+ "io"
+ "os"
+ "strings"
+
+ "golang.org/x/sys/unix"
+)
+
+type _Cgroup string
+
+var cgVersion_cache uint
+
+func cgVersion() uint {
+ if cgVersion_cache == 0 {
+ fs, err := statfs("/sys/fs/cgroup/")
+ if err != nil {
+ return 0
+ }
+ if fs.Type == magic_CGROUP2_SUPER {
+ cgVersion_cache = 2
+ } else if fs.Type == magic_TMPFS {
+ // XXX: systemd-specific cgroup v1 logic
+ fs, err = statfs("/sys/fs/cgroup/systemd/")
+ if err != nil {
+ return 0
+ }
+ if fs.Type == magic_CGROUP2_SUPER {
+ cgVersion_cache = 2
+ } else {
+ cgVersion_cache = 1
+ }
+ }
+ }
+ return cgVersion_cache
+}
+
+// getCgroup returns the cgroup path of the process, relative to the
+// root of the cgroup hierarchy.
+//
+// In cgroup v2, there is a only one cgroup hierarchy, so the behavior
+// is obvious.
+//
+// However, in cgroup v1, there were multiple cgroup hierarchies, so
+// this function must decide which hierarchy to use. We chooses the
+// first hierarchy with either the "name=systemd" controller or the
+// "name=elogind" controller attached to it[1]. If no such hierarchy
+// exists, then an error is returned.
+//
+// However, it is possible to generally use cgroup v1, but use a
+// single (named) v2 hierarchy alongside many v1 hierarchies. In this
+// case, we use the v2 hierarchy iff it is named "systemd", otherwise
+// we use the cgroup v1 behavior.
+//
+// [1]: The "first" in that sentence is worrying; shouldn't the choice
+// of hierarchy not depend on the undefined order that controllers are
+// listed in? Well, a controller may be attached to only one
+// hierarchy at a time. So there is only an ambiguity for "first" to
+// come in if both "name=systemd" and "name=elogind" controllers
+// exist. Systemd and elogind cannot be used together, so this isn't
+// a concern.
+//
+// BUG(lukeshu): PID.getCgroup: Has systemd-specific logic. However,
+// it is only for "legacy" cgroup v1 compatibility; the cgroup v2
+// logic is totally implementation-agnostic. Unfortunately(?), no
+// distro seems to be using cgroup v2 (introduced in Linux 4.5) yet by
+// default.
+func (pid PID) getCgroup() (_Cgroup, error) {
+ cgVer := cgVersion()
+
+ var cgroupFilename string
+ if pid == 0 {
+ cgroupFilename = "/proc/self/cgroup"
+ } else {
+ cgroupFilename = fmt.Sprintf("/proc/%d/cgroup", pid)
+ }
+
+ f, err := os.Open(cgroupFilename)
+ if err != nil {
+ return "", err
+ }
+ defer f.Close()
+
+ bf := bufio.NewReader(f)
+
+ for {
+ line, err := bf.ReadString('\n')
+ if err == io.EOF {
+ break
+ }
+ if err != nil {
+ return "", err
+ }
+ line = strings.TrimSuffix(line, "\n")
+
+ parts := strings.SplitN(line, ":", 3)
+ if len(parts) != 3 {
+ continue
+ }
+
+ hierarchy := parts[0]
+ controllers := parts[1]
+ path := _Cgroup(parts[2])
+
+ switch cgVer {
+ case 1:
+ for _, controller := range strings.Split(controllers, ",") {
+ if controller == "name=systemd" || controller == "name=elogind" {
+ return path, nil
+ }
+ }
+ case 2:
+ if hierarchy != "0" {
+ continue
+ }
+ return path, nil
+ }
+ }
+ return "", unix.ENODATA
+}
+
+// cgGetRootPath determines the cgroup that all other cgroups belong
+// to. The common case is just "/", but it could be something else if
+// we are inside of a container, but have a view of the entier cgroup
+// hierarchy.
+//
+// BUG(lukeshu): cgGetRootPath: works correctly on systemd and
+// elogind, but I'm not sure it's general.
+func cgGetRootPath() (_Cgroup, error) {
+ cgpath, err := PID(1).getCgroup()
+ if err != nil {
+ return "/", err
+ }
+
+ cgpath = _Cgroup(trimOneSuffix(string(cgpath),
+ "/init.scope", // modern systemd
+ "/system.slice", // legacy systemd
+ "/system", // even more legacy systemd
+ ))
+
+ return cgpath, nil
+}
+
+func cgUnescape(s string) string {
+ return strings.TrimPrefix(s, "_")
+}
diff --git a/sd_login/cgroup_skip.go b/sd_login/cgroup_skip.go
new file mode 100644
index 0000000..7eb2be8
--- /dev/null
+++ b/sd_login/cgroup_skip.go
@@ -0,0 +1,98 @@
+// Copyright (C) 2016-2017 Luke Shumaker <lukeshu@sbcglobal.net>
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package sd_login
+
+import (
+ "strconv"
+ "strings"
+)
+
+func (cgroup _Cgroup) SkipPath(prefix _Cgroup) (_Cgroup, bool) {
+ //skip: SkipPath(prefix _Cgroup) : SkipPath(prefix)
+ rest, ok := path_startswith(string(cgroup), string(prefix))
+ if ok {
+ return _Cgroup(rest), true
+ } else {
+ return cgroup, false
+ }
+}
+
+// Skip (*.slice){1,}
+func (cgroup _Cgroup) SkipSlices() (_Cgroup, bool) {
+ //skip: SkipSlices() : SkipSlices()
+ cg := string(cgroup)
+ skipped := false
+ for {
+ cg = strings.TrimLeft(cg, "/")
+ part, rest := split2(cg, '/')
+ if !valid_slice_name(part) {
+ return _Cgroup(cg), skipped
+ }
+ skipped = true
+ cg = rest
+ }
+}
+
+// Skip user@*.service
+func (cgroup _Cgroup) SkipUserManager() (_Cgroup, bool) {
+ //skip: SkipUserManager() : SkipUserManager()
+ part, rest := split2(strings.TrimLeft(string(cgroup), "/"), '/')
+ uid_str, ok := trimPrefixSuffix(part, "user@", ".service")
+ if !ok {
+ return cgroup, false
+ }
+ _, err := strconv.Atoi(uid_str)
+ if err != nil {
+ return cgroup, false
+ }
+ return _Cgroup(rest), true
+}
+
+// Skip session-*.scope
+func (cgroup _Cgroup) SkipSession() (_Cgroup, bool) {
+ //skip: SkipSession() : SkipSession()
+ part, rest := split2(strings.TrimLeft(string(cgroup), "/"), '/')
+ session, ok := trimPrefixSuffix(part, "session-", ".scope")
+ if !ok {
+ return cgroup, false
+ }
+ if !valid_session_name(session) {
+ return cgroup, false
+ }
+ return _Cgroup(rest), true
+}
+
+// Skip (/*.slice){0,}/(user@*.service|session-*.scope)
+func (cgroup _Cgroup) SkipUserPrefix() (_Cgroup, bool) {
+ //skip: SkipUserPrefix() : SkipUserPrefix()
+ cgroup, _ = cgroup.SkipSlices()
+ cgroup, ok := cgroup.SkipUserManager()
+ if ok {
+ return cgroup, ok
+ }
+ return cgroup.SkipSession()
+}
+
+// Skip cgGetRootPath
+func (cgroup _Cgroup) SkipSystemPrefix() (_Cgroup, bool) {
+ //skip: SkipSystemPrefix() : SkipSystemPrefix()
+
+ rootpath, err := cgGetRootPath()
+ if err != nil {
+ return cgroup, false
+ }
+
+ return cgroup.SkipPath(rootpath)
+}
diff --git a/sd_login/cgroup_skip_gen.go.gen b/sd_login/cgroup_skip_gen.go.gen
new file mode 100755
index 0000000..26c515a
--- /dev/null
+++ b/sd_login/cgroup_skip_gen.go.gen
@@ -0,0 +1,41 @@
+#!/usr/bin/env bash
+# Copyright (C) 2017 Luke Shumaker <lukeshu@sbcglobal.net>
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+{
+ printf '//'
+ printf ' %q' "$0" "$@"
+ printf '\n// MACHINE GENERATED BY THE COMMAND ABOVE; DO NOT EDIT\n\n'
+
+ echo package sd_login
+
+ grep -o '//skip:.*' "$1" | cut -d: -f2- | while read -r line; do
+ sig=$(echo $(cut -d: -f1 <<<"$line"))
+ cal=$(echo $(cut -d: -f2 <<<"$line"))
+ cat <<EOF
+
+func (cgroup _Cgroup) Maybe${sig} _Cgroup {
+ cgroup, _ = cgroup.${cal}
+ return cgroup
+}
+func (cgroup _Cgroup) Must${sig} _Cgroup {
+ cgroup, ok := cgroup.${cal}
+ if !ok {
+ return ""
+ }
+ return cgroup
+}
+EOF
+ done
+} | gofmt
diff --git a/sd_login/cgroup_systemd.go b/sd_login/cgroup_systemd.go
new file mode 100644
index 0000000..bbef9d3
--- /dev/null
+++ b/sd_login/cgroup_systemd.go
@@ -0,0 +1,107 @@
+// Copyright (C) 2016-2017 Luke Shumaker <lukeshu@sbcglobal.net>
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package sd_login
+
+import (
+ "os"
+ "strconv"
+ "strings"
+)
+
+// XXX: logind
+func (cgroup _Cgroup) GetSession() Session {
+ unit := cgroup.GetUnit()
+
+ session, ok := trimPrefixSuffix(unit, "session-", ".scope")
+ if !ok || !valid_session_name(session) {
+ return ""
+ }
+
+ return Session(session)
+}
+
+// XXX: who owns this, systemd or logind? elogind doesn't implement
+// it, but idk
+func (cgroup _Cgroup) GetOwnerUser() User {
+ slice := cgroup.GetSlice()
+
+ uid_str, ok := trimPrefixSuffix(slice, "user-", ".slice")
+ if !ok {
+ return -1
+ }
+
+ uid, err := strconv.Atoi(uid_str)
+ if err != nil {
+ return -1
+ }
+
+ return User(uid)
+}
+
+// XXX: machined
+func (cgroup _Cgroup) GetMachine() Machine {
+ unit := cgroup.GetUnit()
+ if unit == "" {
+ return ""
+ }
+
+ machine, err := os.Readlink("/run/systemd/machines/unit:" + unit)
+ if err != nil {
+ return ""
+ }
+ return Machine(machine)
+}
+
+// XXX: systemd
+func (cgroup _Cgroup) decodeUnit() string {
+ unit, _ := split2(string(cgroup), '/')
+ if len(unit) < 3 {
+ return ""
+ }
+ unit = cgUnescape(unit)
+ if valid_unit_name(unit)&(unit_name_plain|unit_name_instance) == 0 {
+ return ""
+ }
+ return unit
+}
+
+func (cgroup _Cgroup) GetUnit() string {
+ unit := cgroup.MaybeSkipSlices().decodeUnit()
+ if strings.HasSuffix(unit, ".slice") {
+ return ""
+ }
+ return unit
+}
+func (cgroup _Cgroup) GetUserUnit() string {
+ return cgroup.MustSkipUserPrefix().GetUnit()
+}
+func (cgroup _Cgroup) GetSlice() string {
+ cg := string(cgroup)
+ n := 0
+ for {
+ cg = strings.TrimLeft(cg, "/")
+ part, rest := split2(cg, '/')
+ if !valid_slice_name(part) {
+ if n == 0 {
+ return "-.slice"
+ }
+ return _Cgroup(cg).decodeUnit()
+ }
+ cg = rest
+ }
+}
+func (cgroup _Cgroup) GetUserSlice() string {
+ return cgroup.MustSkipUserPrefix().GetSlice()
+}
diff --git a/sd_login/doc.go b/sd_login/doc.go
new file mode 100644
index 0000000..ba1a0f7
--- /dev/null
+++ b/sd_login/doc.go
@@ -0,0 +1,38 @@
+// Copyright (C) 2016 Luke Shumaker <lukeshu@sbcglobal.net>
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// Package sd_login TODO
+//
+// The login manager manages 4 basic object types
+//
+// - machine: A container or virtual machine
+//
+// - seat: A set of hardware devices for a workspace; ie a screen
+// and keyboard
+//
+// - session: A login session; tied to 0 or 1 seats (0 for things
+// like SSH login)
+//
+// - user: Keeps track of logged in users; that is, users with 1 or
+// more sessions
+//
+// TODO: why are machines part of this?
+//
+// - pid: A process may belong to a session, or directly to a user if
+// it is not part of a session. This "belonging" is separate
+// accounting by the login manager; it is NOT the same as the
+// EUID/RUID.
+//
+// - peer: A peer is a process on the other end of a AF_UNIX socket.
+package sd_login
diff --git a/sd_login/gen.go b/sd_login/gen.go
new file mode 100644
index 0000000..3e5b5ac
--- /dev/null
+++ b/sd_login/gen.go
@@ -0,0 +1,3 @@
+//go:generate make
+
+package sd_login
diff --git a/sd_login/missing.s b/sd_login/missing.s
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/sd_login/missing.s
diff --git a/sd_login/notes.org b/sd_login/notes.org
new file mode 100644
index 0000000..0627142
--- /dev/null
+++ b/sd_login/notes.org
@@ -0,0 +1,78 @@
+/* Error codes:
+ *
+ * invalid input parameters → -EINVAL
+ * invalid fd → -EBADF
+ * process does not exist → -ESRCH
+ * cgroup does not exist → -ENOENT
+ * machine, session does not exist → -ENXIO
+ * requested metadata on object is missing → -ENODATA
+ */
+
+* PID
+** get_...
+*** session
+*** owner_uid
+*** unit
+*** user_unit
+*** slice
+*** user_slice
+*** machine_name
+*** cgroup
+* peer
+** get_...
+*** session
+*** owner_uid
+*** unit
+*** user_unit
+*** slice
+*** user_slice
+*** machine_name
+*** cgroup
+* UID
+** is_on_seat
+** get_...
+*** state
+*** display
+*** sessions
+*** seats
+* session
+** is_active
+** is_remote
+** get_...
+*** state
+*** uid
+*** seat
+*** service
+*** type
+*** class
+*** desktop
+*** display
+*** remote_host
+*** remote_user
+*** tty
+*** vt
+* seat
+** can_multi_session
+** can_tty
+** can_graphical
+** get_...
+*** active
+*** sessions
+* machine
+** get_...
+*** class
+*** ifindices
+* top level
+** get_...
+*** seats
+*** sessions
+*** uids
+*** machine_names
+* login monitor
+** new
+** unref
+** flush
+** get_...
+*** fd
+*** events
+*** timeout
diff --git a/sd_login/type_machine.go b/sd_login/type_machine.go
new file mode 100644
index 0000000..5892017
--- /dev/null
+++ b/sd_login/type_machine.go
@@ -0,0 +1,73 @@
+// Copyright (C) 2016 Luke Shumaker <lukeshu@sbcglobal.net>
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package sd_login
+
+import (
+ "strconv"
+ "strings"
+
+ "golang.org/x/sys/unix"
+)
+
+type Machine string
+
+// running VMs/containers
+func GetMachines() ([]Machine, error) {
+ strs, err := get_files_in_directory("/run/systemd/machines/")
+ if err != nil {
+ return nil, err
+ }
+ var machines []Machine
+ for _, str := range strs {
+ if strings.HasPrefix(str, "unit:") || !valid_machine_name(str) {
+ continue
+ }
+ machines = append(machines, Machine(str))
+ }
+ return machines, nil
+}
+
+func (m Machine) GetClass() (string, error) {
+ env, err := parse_env_file("/run/systemd/machines/" + string(m))
+ if err != nil {
+ return "", err
+ }
+ class, ok := env["CLASS"]
+ if !ok {
+ return "", unix.ENXIO // or EIO?
+ }
+ return class, nil
+}
+
+func (m Machine) GetIfIndices() ([]int, error) {
+ env, err := parse_env_file("/run/systemd/machines/" + string(m))
+ if err != nil {
+ return nil, err
+ }
+ netif, ok := env["NETIF"]
+ if !ok {
+ return nil, unix.ENXIO // or EIO?
+ }
+
+ var ifindices []int
+ for _, word := range strings.FieldsFunc(netif, func(c rune) bool { return strings.ContainsRune(whitespace, c) }) {
+ ifindex, err := strconv.Atoi(word)
+ if err != nil {
+ return nil, err
+ }
+ ifindices = append(ifindices, ifindex)
+ }
+ return ifindices, nil
+}
diff --git a/sd_login/type_monitor.go b/sd_login/type_monitor.go
new file mode 100644
index 0000000..50f601a
--- /dev/null
+++ b/sd_login/type_monitor.go
@@ -0,0 +1,106 @@
+// Copyright (C) 2016 Luke Shumaker <lukeshu@sbcglobal.net>
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package sd_login
+
+import (
+ "git.lukeshu.com/go/libgnulinux/inotify"
+ "golang.org/x/sys/unix"
+)
+
+// Monitor monitors several types of events, so you don't have to do
+// busy-polling.
+//
+// However, Monitor won't actually give you the event that happened,
+// you'll need to read it yourself.
+type Monitor struct {
+ in *inotify.Inotify
+}
+
+type MonitorCategory uint
+
+const (
+ MonitorSeats MonitorCategory = 1 << iota
+ MonitorSessions
+ MonitorUsers
+ MonitorMachines
+
+ MonitorAll = MonitorSeats | MonitorSessions | MonitorUsers | MonitorMachines
+)
+
+func NewMonitor(categories MonitorCategory) (*Monitor, error) {
+ if categories == 0 {
+ return nil, unix.EINVAL
+ }
+
+ in, err := inotify.InotifyInit1(inotify.IN_CLOEXEC)
+ if err != nil {
+ return nil, err
+ }
+
+ if categories&MonitorSeats != 0 {
+ _, err := in.AddWatch("/run/systemd/seats/", inotify.IN_MOVED_TO|inotify.IN_DELETE)
+ if err != nil {
+ in.Close()
+ return nil, err
+ }
+ }
+
+ if categories&MonitorSessions != 0 {
+ _, err := in.AddWatch("/run/systemd/sessions/", inotify.IN_MOVED_TO|inotify.IN_DELETE)
+ if err != nil {
+ in.Close()
+ return nil, err
+ }
+ }
+
+ if categories&MonitorUsers != 0 {
+ _, err := in.AddWatch("/run/systemd/users/", inotify.IN_MOVED_TO|inotify.IN_DELETE)
+ if err != nil {
+ in.Close()
+ return nil, err
+ }
+ }
+
+ if categories&MonitorMachines != 0 {
+ _, err := in.AddWatch("/run/systemd/machines/", inotify.IN_MOVED_TO|inotify.IN_DELETE)
+ if err != nil {
+ in.Close()
+ return nil, err
+ }
+ }
+
+ return &Monitor{in: in}, nil
+}
+
+func (m *Monitor) Close() error {
+ return m.in.Close()
+}
+
+func (m *Monitor) Flush() error {
+ for {
+ ev, err := m.in.ReadNonblock()
+ if err != nil {
+ return err
+ }
+ if ev.Wd < 0 {
+ return nil
+ }
+ }
+}
+
+func (m *Monitor) Wait() error {
+ _, err := m.in.ReadBlock()
+ return err
+}
diff --git a/sd_login/type_pid.go.gen b/sd_login/type_pid.go.gen
new file mode 100755
index 0000000..ec1bfe0
--- /dev/null
+++ b/sd_login/type_pid.go.gen
@@ -0,0 +1,63 @@
+#!/usr/bin/env bash
+# Copyright (C) 2016-2017 Luke Shumaker <lukeshu@sbcglobal.net>
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+{
+ printf '//'
+ printf ' %q' "$0" "$@"
+ printf '\n// MACHINE GENERATED BY THE COMMAND ABOVE; DO NOT EDIT\n\n'
+
+ cat <<EOF
+package sd_login
+
+import "net"
+
+// PID represents a process.
+//
+// As a special case, PID(0) refers to the current process.
+type PID int
+
+func GetPeer(conn *net.UnixConn) (PID, error) {
+ ucred, err := getpeercred(conn)
+ if err != nil {
+ return -1, err
+ }
+ return PID(ucred.Pid), nil
+}
+EOF
+
+ data=(
+ 'GetSession Session'
+ 'GetOwnerUser User'
+ 'GetMachine Machine'
+
+ 'GetUnit string'
+ 'GetUserUnit string'
+ 'GetSlice string'
+ 'GetUserSlice string'
+ )
+ for item in "${data[@]}"; do
+ read Func Type <<<"$item"
+ cat <<EOF
+
+func (pid PID) ${Func}() (v ${Type}, err error) {
+ cgroup, err := pid.getCgroup()
+ if err != nil {
+ return
+ }
+ return cgroup.MustSkipSystemPrefix().${Func}(), nil
+}
+EOF
+ done
+} | gofmt
diff --git a/sd_login/type_seat.go b/sd_login/type_seat.go
new file mode 100644
index 0000000..fd86663
--- /dev/null
+++ b/sd_login/type_seat.go
@@ -0,0 +1,37 @@
+// Copyright (C) 2016 Luke Shumaker <lukeshu@sbcglobal.net>
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package sd_login
+
+type Seat string
+
+// GetSeats returns a list of all currently avialable local seats.
+func GetSeats() ([]Seat, error) {
+ strs, err := get_files_in_directory("/run/systemd/seats/")
+ if err != nil {
+ return nil, err
+ }
+ seats := make([]Seat, len(strs))
+ for i := range strs {
+ seats[i] = Seat(strs[i])
+ }
+ return seats, nil
+}
+
+func (seat Seat) GetActive() (Session, User, error)
+func (seat Seat) GetSessions() ([]Session, []User, error)
+
+func (seat Seat) CanMultiSession() bool
+func (seat Seat) CanTTY() bool
+func (seat Seat) CanGraphical() bool
diff --git a/sd_login/type_session.go b/sd_login/type_session.go
new file mode 100644
index 0000000..47856de
--- /dev/null
+++ b/sd_login/type_session.go
@@ -0,0 +1,67 @@
+// Copyright (C) 2016 Luke Shumaker <lukeshu@sbcglobal.net>
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package sd_login
+
+type Session string
+
+type SessionState int
+
+const (
+ SessionOnline SessionState = iota
+ SessionActive
+ SessionClosing
+)
+
+func GetSessions() ([]Session, error) {
+ strs, err := get_files_in_directory("/run/systemd/sessions/")
+ if err != nil {
+ return nil, err
+ }
+ sessions := make([]Session, len(strs))
+ for i := range strs {
+ sessions[i] = Session(strs[i])
+ }
+ return sessions, nil
+}
+
+func (sess Session) IsActive() bool {
+ state, err := sess.GetState()
+ return err == nil && state == SessionActive
+}
+func (sess Session) IsRemote() bool
+func (sess Session) GetState() (SessionState, error)
+func (sess Session) GetUser() (User, error)
+func (sess Session) GetSeat() (Seat, error)
+
+// PAM
+func (sess Session) GetService() (string, error)
+
+// tty, x11, wayland, mir, unspecified
+func (sess Session) GetType() (string, error)
+
+// user, greeter, lock-screen
+func (sess Session) GetClass() (string, error)
+
+// GNOME, KDE, systemd-console
+func (sess Session) GetDesktop() (string, error)
+
+// X11 DISPLAY
+func (sess Session) GetDisplay() (string, error)
+func (sess Session) GetRemoteHost() (string, error)
+
+// PAM
+func (sess Session) GetRemoteUser() (string, error)
+func (sess Session) GetTTY() (string, error)
+func (sess Session) GetVT() (uint, error)
diff --git a/sd_login/type_user.go b/sd_login/type_user.go
new file mode 100644
index 0000000..81a5418
--- /dev/null
+++ b/sd_login/type_user.go
@@ -0,0 +1,57 @@
+// Copyright (C) 2016 Luke Shumaker <lukeshu@sbcglobal.net>
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package sd_login
+
+import (
+ "strconv"
+)
+
+type User int
+
+type UserState int
+
+const (
+ UserOffline UserState = iota
+ UserLingering
+ UserOnline
+ UserActive
+ UserClosing
+)
+
+// GetUsers returns a list of all users who currently have login
+// sessions.
+func GetUsers() ([]User, error) {
+ strs, err := get_files_in_directory("/run/systemd/users/")
+ if err != nil {
+ return nil, err
+ }
+ var users []User
+ for _, uid_str := range strs {
+ uid, err := strconv.Atoi(uid_str)
+ if err != nil {
+ continue
+ }
+ users = append(users, User(uid))
+ }
+ return users, nil
+}
+
+func (uid User) IsOnSeat(seat Seat, requireActive bool) bool
+func (uid User) GetState() (UserState, error)
+
+// primary session
+func (uid User) GetDisplay() (Session, error)
+func (uid User) GetSessions(requireActive bool) ([]Session, error)
+func (uid User) GetSeats(requireActive bool) ([]Seat, error)
diff --git a/sd_login/util.go b/sd_login/util.go
new file mode 100644
index 0000000..45df0e0
--- /dev/null
+++ b/sd_login/util.go
@@ -0,0 +1,130 @@
+// Copyright (C) 2016 Luke Shumaker <lukeshu@sbcglobal.net>
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package sd_login
+
+import (
+ "io/ioutil"
+ "os"
+ "path"
+ "strings"
+)
+
+const (
+ whitespace = " \t\n\r"
+ newline = "\n\r"
+ digits = "0123456789"
+ lowercase_letters = "abcdefghijklmnopqrstuvwxyz"
+ uppercase_letters = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
+ letters = lowercase_letters + uppercase_letters
+)
+
+func trimOneSuffix(s string, suffixes ...string) string {
+ for _, suffix := range suffixes {
+ if strings.HasSuffix(s, suffix) {
+ return strings.TrimSuffix(s, suffix)
+ }
+ }
+ return s
+}
+
+func trimPrefixSuffix(s, prefix, suffix string) (string, bool) {
+ if strings.HasPrefix(s, prefix) && strings.HasSuffix(s, suffix) {
+ s = strings.TrimPrefix(s, prefix)
+ s = strings.TrimSuffix(s, suffix)
+ return s, true
+ }
+ return "", false
+}
+
+func split2(s string, b byte) (string, string) {
+ n := strings.IndexByte(s, b)
+ if n < 0 {
+ n = len(s)
+ }
+ return s[:n], s[n:]
+}
+
+func path_startswith(apath string, aprefix string) (string, bool) {
+ if path.IsAbs(apath) != path.IsAbs(aprefix) {
+ return "", false
+ }
+ for {
+ apath = strings.TrimLeft(apath, "/")
+ aprefix = strings.TrimLeft(aprefix, "/")
+
+ if aprefix == "" {
+ return apath, true
+ }
+ if apath == "" {
+ return "", false
+ }
+
+ pathPart, pathRest := split2(apath, '/')
+ prefixPart, prefixRest := split2(aprefix, '/')
+
+ if pathPart != prefixPart {
+ return "", false
+ }
+
+ apath = pathRest
+ aprefix = prefixRest
+ }
+}
+
+func get_files_in_directory(apath string) ([]string, error) {
+ files, err := ioutil.ReadDir(apath)
+ if err != nil {
+ return nil, err
+ }
+ var ret []string
+ for _, file := range files {
+ if file.Mode()&os.ModeType&^os.ModeSymlink == 0 {
+ ret = append(ret, file.Name())
+ }
+ }
+ return ret, nil
+}
+
+func parse_env_file(filename string) (map[string]string, error)
+
+/*
+ enum {
+ PRE_KEY,
+ KEY,
+ PRE_VALUE,
+ VALUE,
+ VALUE_ESCAPE,
+ SINGLE_QUOTE_VALUE,
+ SINGLE_QUOTE_VALUE_ESCAPE,
+ DOUBLE_QUOTE_VALUE,
+ DOUBLE_QUOTE_VALUE_ESCAPE,
+ COMMENT,
+ COMMENT_ESCAPE
+ } state = PRE_KEY;
+
+func parse_env_file(filename string) (map[string]string, error) {
+ contents, err := ioutil.ReadFile(filename)
+
+ for contents != "" {
+ switch state {
+ case PRE_KEY:
+ if strings.ContainsRune("#;", contents[0]) {
+ sate = COMMENT
+ } else if strings.ContainsRune(whitespace, contents[0]) {
+ state = KEY
+ last_key_whitespace
+ }
+}
+*/
diff --git a/sd_login/util_syscall.go b/sd_login/util_syscall.go
new file mode 100644
index 0000000..f3d12a8
--- /dev/null
+++ b/sd_login/util_syscall.go
@@ -0,0 +1,51 @@
+// Copyright (C) 2015-2017 Luke Shumaker <lukeshu@sbcglobal.net>
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package sd_login
+
+import (
+ "net"
+
+ "golang.org/x/sys/unix"
+)
+
+const (
+ // from <linux/magic.h>
+ magic_TMPFS = 0x01021994
+ magic_CGROUP_SUPER = 0x0027e0eb
+ magic_CGROUP2_SUPER = 0x63677270 // "cgrp"
+)
+
+const (
+ // POSIX says at least 255, but Linux isn't POSIX.
+ host_name_max = 64
+)
+
+// I borrowed this from libnslcd.git/nslcd_systemd (of which I am the
+// author) -- lukeshu
+func getpeercred(conn *net.UnixConn) (cred unix.Ucred, err error) {
+ file, err := conn.File()
+ if err != nil {
+ return
+ }
+ defer file.Close()
+ _cred, err := unix.GetsockoptUcred(int(file.Fd()), unix.SOL_SOCKET, unix.SO_PEERCRED)
+ cred = *_cred
+ return
+}
+
+func statfs(fspath string) (fs unix.Statfs_t, err error) {
+ err = unix.Statfs(fspath, &fs)
+ return
+}
diff --git a/sd_login/util_valid.go b/sd_login/util_valid.go
new file mode 100644
index 0000000..e0dd906
--- /dev/null
+++ b/sd_login/util_valid.go
@@ -0,0 +1,141 @@
+// Copyright (C) 2016 Luke Shumaker <lukeshu@sbcglobal.net>
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package sd_login
+
+import (
+ "strings"
+)
+
+func valid_session_name(id string) bool {
+ if id == "" {
+ return false
+ }
+
+ return strings.TrimLeft(id, letters+digits) == ""
+}
+
+func valid_slice_name(s string) bool {
+ return strings.HasSuffix(s, ".slice") && valid_unit_name(s) == unit_name_plain
+}
+
+func valid_machine_name(s string) bool {
+ if len(s) > host_name_max {
+ // Note that Linux HOST_NAME_MAX is 64, but DNS allows
+ // 255, so names from DNS might be invalid.
+ return false
+ }
+
+ n_dots := 0
+ dot := true
+ for _, c := range s {
+ if c == '.' {
+ if dot {
+ return false
+ }
+ dot = true
+ n_dots++
+ } else {
+ if !strings.ContainsRune(letters+digits+"-_.", c) {
+ return false
+ }
+ dot = false
+ }
+ }
+
+ if dot { // trailing dot or empty
+ return false
+ }
+
+ return true
+}
+
+const (
+ unit_name_invalid = 0
+ unit_name_plain = 1 << 0 // foo.service
+ unit_name_instance = 1 << 1 // foo@bar.service
+ unit_name_template = 1 << 2 // foo@.service
+ unit_name_any = unit_name_plain | unit_name_instance | unit_name_template
+)
+
+func valid_unit_name(unit string) int {
+ const_unit_name_max := 256
+ const_unit_types := []string{
+ "service",
+ "socket",
+ "busname",
+ "target",
+ "device",
+ "mount",
+ "automount",
+ "swap",
+ "timer",
+ "path",
+ "slice",
+ "scope",
+ }
+
+ if unit == "" {
+ return unit_name_invalid
+ }
+
+ // If the unit name is too long
+ if len(unit) >= const_unit_name_max {
+ return unit_name_invalid
+ }
+
+ // If there is no dot in the unit name
+ dot := strings.LastIndexByte(unit, '.')
+ if dot < 0 || dot == 0 {
+ return unit_name_invalid
+ }
+
+ // If the .suffix isn't a real unit type
+ utype := unit[dot+1:]
+ found := false
+ for _, _utype := range const_unit_types {
+ if utype == _utype {
+ found = true
+ break
+ }
+ }
+ if !found {
+ return unit_name_invalid
+ }
+
+ at := strings.IndexByte(unit, '@')
+
+ // If the unit has more than one '@'
+ if at >= 0 && strings.IndexByte(unit[at+1:], '@') >= 0 {
+ return unit_name_invalid
+ }
+
+ // If the '@' is at the start of the unit
+ if at == 0 {
+ return unit_name_invalid
+ }
+
+ if at < 0 {
+ return unit_name_plain
+ } else {
+ if dot > at+1 {
+ return unit_name_instance
+ }
+ if dot == at+1 {
+ return unit_name_template
+ }
+ }
+
+ return unit_name_invalid
+}