mirror of
https://github.com/AdguardTeam/AdGuardHome.git
synced 2026-08-04 15:28:58 +00:00
Pull request 2551: AGDNS-2966-add-manager-extension
Squashed commit of the following:
commit 902b8a52b9a89d5b568ab09c786179df56efdcd3
Author: Igor 🐧 Lobanov <i.lobanov@adguard.com>
Date: Mon Dec 22 14:46:35 2025 +0300
Pull request 2553: Fixed e2e tests
Fixed e2e tests
Changed nslookup to dig
Squashed commit of the following:
commit c0dcce547b34743b07e82e16184a5509bb6c98cf
Author: Igor Lobanov <i.lobanov@adguard.com>
Date: Sat Dec 20 13:34:50 2025 +0100
fix e2e tests
commit 6ae1d02aa241255838c0c2fabd0394061f7c69ba
Author: Igor Lobanov <i.lobanov@adguard.com>
Date: Sat Dec 20 13:06:51 2025 +0100
fixed e2e tests
changed nslookup to dig
commit e67ed4ccb884d025c874b8bbcb97f1c3f2ca49c9
Author: Eugene Burkov <E.Burkov@AdGuard.COM>
Date: Fri Dec 19 18:57:39 2025 +0300
ossvc: imp docs
commit 926719b6fbfb8ea3fd5daf51da444deda6f50b61
Author: Eugene Burkov <E.Burkov@AdGuard.COM>
Date: Thu Dec 18 19:53:55 2025 +0300
ossvc: imp code
commit d0d7abf92a05317aca2fa3b9ad724c0b5fbb9b03
Author: Eugene Burkov <E.Burkov@AdGuard.COM>
Date: Wed Dec 17 18:46:57 2025 +0300
ossvc: add ext iface, imp code
This commit is contained in:
parent
81213e81cf
commit
ddc5b1343a
12 changed files with 267 additions and 89 deletions
2
client/package.json
vendored
2
client/package.json
vendored
|
|
@ -12,7 +12,7 @@
|
|||
"test": "vitest --run",
|
||||
"test:watch": "vitest --watch",
|
||||
"test:e2e": "npx playwright test tests/e2e",
|
||||
"test:e2e:interactive": "npx playwright test --ui",
|
||||
"test:e2e:interactive": "npx playwright test --headed",
|
||||
"test:e2e:debug": "npx playwright test --debug",
|
||||
"test:e2e:codegen": "npx playwright codegen",
|
||||
"typecheck": "tsc --noEmit",
|
||||
|
|
|
|||
20
client/playwright.config.ts
vendored
20
client/playwright.config.ts
vendored
|
|
@ -20,7 +20,7 @@ export default defineConfig({
|
|||
/* Opt out of parallel tests on CI. */
|
||||
workers: process.env.CI ? 1 : undefined,
|
||||
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
|
||||
reporter: 'html',
|
||||
reporter: [['html', { open: 'never' }]],
|
||||
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
|
||||
use: {
|
||||
/* Base URL to use in actions like `await page.goto('/')`. */
|
||||
|
|
@ -40,13 +40,13 @@ export default defineConfig({
|
|||
use: { ...devices['Desktop Chrome'] },
|
||||
},
|
||||
],
|
||||
|
||||
webServer: {
|
||||
stdout: process.env.CI ? 'pipe' : 'ignore',
|
||||
command: `${!process.env.CI ? 'sudo ' : ''}./AdGuardHome --local-frontend -v -c ${CONFIG_FILE_PATH}`,
|
||||
url: 'http://127.0.0.1:3000',
|
||||
cwd: '..',
|
||||
reuseExistingServer: !process.env.CI,
|
||||
timeout: 10000,
|
||||
},
|
||||
webServer: process.env.CI
|
||||
? {
|
||||
stdout: 'pipe',
|
||||
command: `./AdGuardHome --local-frontend -v -c ${CONFIG_FILE_PATH}`,
|
||||
url: 'http://127.0.0.1:3000',
|
||||
cwd: '..',
|
||||
timeout: 10000,
|
||||
}
|
||||
: undefined,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -27,12 +27,12 @@ test.describe('General Settings', () => {
|
|||
await expect(browsingSecurity).toBeChecked();
|
||||
}
|
||||
|
||||
const resultEnabled = execSync('nslookup totalvirus.com 127.0.0.1').toString();
|
||||
const resultEnabled = execSync('dig @127.0.0.1 totalvirus.com').toString();
|
||||
|
||||
await browsingSecurityLabel.click();
|
||||
await expect(browsingSecurity).not.toBeChecked();
|
||||
|
||||
const resultDisabled = execSync('nslookup totalvirus.com 127.0.0.1').toString();
|
||||
const resultDisabled = execSync('dig @127.0.0.1 totalvirus.com').toString();
|
||||
|
||||
expect(resultEnabled).not.toEqual(resultDisabled);
|
||||
|
||||
|
|
@ -55,12 +55,12 @@ test.describe('General Settings', () => {
|
|||
await expect(parentalControl).toBeChecked();
|
||||
}
|
||||
|
||||
const resultEnabled = execSync('nslookup pornhub.com 127.0.0.1').toString();
|
||||
const resultEnabled = execSync('dig @127.0.0.1 pornhub.com').toString();
|
||||
|
||||
await parentalControlLabel.click();
|
||||
await expect(parentalControl).not.toBeChecked();
|
||||
|
||||
const resultDisabled = execSync('nslookup pornhub.com 127.0.0.1').toString();
|
||||
const resultDisabled = execSync('dig @127.0.0.1 pornhub.com').toString();
|
||||
|
||||
expect(resultEnabled).not.toEqual(resultDisabled);
|
||||
|
||||
|
|
|
|||
|
|
@ -1,31 +1,56 @@
|
|||
import { chromium, type FullConfig } from '@playwright/test';
|
||||
|
||||
import { ADMIN_USERNAME, ADMIN_PASSWORD, PORT } from '../constants';
|
||||
import { ADMIN_USERNAME, ADMIN_PASSWORD, PORT, CONFIG_FILE_PATH } from '../constants';
|
||||
|
||||
const BASE_URL = `http://127.0.0.1:${PORT}`;
|
||||
|
||||
async function checkServerAvailable(): Promise<boolean> {
|
||||
try {
|
||||
const response = await fetch(BASE_URL);
|
||||
return response.ok || response.status === 302;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async function globalSetup(config: FullConfig) {
|
||||
if (!process.env.CI) {
|
||||
const isServerRunning = await checkServerAvailable();
|
||||
if (!isServerRunning) {
|
||||
console.error(
|
||||
`\nAdGuard Home server is not running. Start it first:\n sudo ./AdGuardHome --local-frontend -v -c ${CONFIG_FILE_PATH}\n`,
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
const browser = await chromium.launch({
|
||||
slowMo: 100,
|
||||
});
|
||||
const page = await browser.newPage({ baseURL: config.webServer?.url });
|
||||
const page = await browser.newPage({ baseURL: config.webServer?.url || BASE_URL });
|
||||
|
||||
try {
|
||||
await page.goto('/');
|
||||
await page.goto('/');
|
||||
|
||||
// Check if we're on the install page or already installed
|
||||
const isInstallPage = page.url().includes('/install.html');
|
||||
|
||||
if (isInstallPage) {
|
||||
await page.getByTestId('install_get_started').click();
|
||||
await page.getByTestId('install_web_port').fill(PORT.toString());
|
||||
await page.getByTestId('install_next').click();
|
||||
await page.getByTestId('install_username').fill(ADMIN_USERNAME);
|
||||
await page.getByTestId('install_username').blur();
|
||||
await page.getByTestId('install_password').fill(ADMIN_PASSWORD);
|
||||
await page.getByTestId('install_confirm_password').click();
|
||||
await page.getByTestId('install_password').blur();
|
||||
await page.getByTestId('install_confirm_password').fill(ADMIN_PASSWORD);
|
||||
await page.getByTestId('install_confirm_password').blur();
|
||||
await page.getByTestId('install_next').click();
|
||||
await page.getByTestId('install_next').click();
|
||||
await page.getByTestId('install_open_dashboard').click();
|
||||
await page.waitForURL((url) => !url.href.endsWith('/install.html'));
|
||||
} catch (error) {
|
||||
console.error('Error during global setup:', error);
|
||||
} finally {
|
||||
await browser.close();
|
||||
}
|
||||
|
||||
await browser.close();
|
||||
}
|
||||
|
||||
export default globalSetup;
|
||||
|
|
|
|||
|
|
@ -24,18 +24,37 @@ test.describe('Rewrites', () => {
|
|||
await page.getByTestId('rewrites_answer').fill(EXAMPLE_ANSWER);
|
||||
await page.getByTestId('rewrites_save').click();
|
||||
|
||||
await expect(page.locator('.logs__text').filter({ hasText: EXAMPLE_DOMAIN })).toBeVisible();
|
||||
await expect(page.locator('.logs__text').filter({ hasText: EXAMPLE_ANSWER })).toBeVisible();
|
||||
await expect(page.locator('.logs__text').filter({ hasText: EXAMPLE_DOMAIN }).first()).toBeVisible();
|
||||
await expect(page.locator('.logs__text').filter({ hasText: EXAMPLE_ANSWER }).first()).toBeVisible();
|
||||
});
|
||||
|
||||
test('should edit a DNS rewrite', async ({ page }) => {
|
||||
await page.getByTestId('edit-rewrite').first().click();
|
||||
await expect(page.getByTestId('rewrites_domain')).toHaveValue(EXAMPLE_DOMAIN);
|
||||
// Use the first existing rewrite instead of creating a new one
|
||||
// Wait for the table to load
|
||||
await expect(page.getByTestId('edit-rewrite').first()).toBeVisible({ timeout: 10000 });
|
||||
|
||||
await page.getByTestId('rewrites_domain').clear();
|
||||
await page.getByTestId('rewrites_domain').fill(EXAMPLE_UPDATED_DOMAIN);
|
||||
// Get the current domain value before editing
|
||||
await page.getByTestId('edit-rewrite').first().click();
|
||||
const originalDomain = await page.getByTestId('rewrites_domain').inputValue();
|
||||
|
||||
// Edit the domain - use keyboard to ensure isDirty is triggered
|
||||
const domainInput = page.getByTestId('rewrites_domain');
|
||||
await domainInput.click();
|
||||
await domainInput.press('Control+a');
|
||||
await domainInput.pressSequentially(EXAMPLE_UPDATED_DOMAIN);
|
||||
await domainInput.blur();
|
||||
await page.getByTestId('rewrites_save').click();
|
||||
|
||||
await expect(page.locator('.logs__text').filter({ hasText: EXAMPLE_UPDATED_DOMAIN })).toBeVisible();
|
||||
// Verify the update
|
||||
await expect(page.locator('.logs__text').filter({ hasText: EXAMPLE_UPDATED_DOMAIN }).first()).toBeVisible({ timeout: 10000 });
|
||||
|
||||
// Restore original value
|
||||
await page.getByTestId('edit-rewrite').first().click();
|
||||
const restoreInput = page.getByTestId('rewrites_domain');
|
||||
await restoreInput.click();
|
||||
await restoreInput.press('Control+a');
|
||||
await restoreInput.pressSequentially(originalDomain);
|
||||
await restoreInput.blur();
|
||||
await page.getByTestId('rewrites_save').click();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ package ossvc
|
|||
// ActionName is the type for actions' names. It has the following valid
|
||||
// values:
|
||||
// - [ActionNameInstall]
|
||||
// - [ActionNameReload]
|
||||
// - [ActionNameRestart]
|
||||
// - [ActionNameStart]
|
||||
// - [ActionNameStop]
|
||||
// - [ActionNameUninstall]
|
||||
|
|
@ -11,7 +11,7 @@ type ActionName string
|
|||
|
||||
const (
|
||||
ActionNameInstall ActionName = "install"
|
||||
ActionNameReload ActionName = "reload"
|
||||
ActionNameRestart ActionName = "restart"
|
||||
ActionNameStart ActionName = "start"
|
||||
ActionNameStop ActionName = "stop"
|
||||
ActionNameUninstall ActionName = "uninstall"
|
||||
|
|
|
|||
|
|
@ -7,11 +7,11 @@ import (
|
|||
"github.com/kardianos/service"
|
||||
)
|
||||
|
||||
// configureServiceOptions defines additional settings of the service
|
||||
// ConfigureServiceOptions defines additional settings of the service
|
||||
// configuration. conf must not be nil.
|
||||
//
|
||||
//lint:ignore U1000 TODO(e.burkov): Use.
|
||||
func configureServiceOptions(conf *service.Config, versionInfo string) {
|
||||
// TODO(e.burkov): Use [timeutil.Clock].
|
||||
func ConfigureServiceOptions(conf *service.Config, versionInfo string) {
|
||||
conf.Option["SvcInfo"] = fmt.Sprintf("%s %s", versionInfo, time.Now())
|
||||
|
||||
configureOSOptions(conf)
|
||||
|
|
|
|||
|
|
@ -19,8 +19,8 @@ func (a *ActionInstall) Name() (name ActionName) { return ActionNameInstall }
|
|||
// isAction implements the [Action] interface for *ActionInstall.
|
||||
func (a *ActionInstall) isAction() {}
|
||||
|
||||
// ActionReload is the implementation of the [Action] interface.
|
||||
type ActionReload struct {
|
||||
// ActionRestart is the implementation of the [Action] interface.
|
||||
type ActionRestart struct {
|
||||
// ServiceConf is the configuration for the service to control.
|
||||
//
|
||||
// TODO(e.burkov): Get rid of github.com/kardianos/service dependency and
|
||||
|
|
@ -28,11 +28,11 @@ type ActionReload struct {
|
|||
ServiceConf *service.Config
|
||||
}
|
||||
|
||||
// Name implements the [Action] interface for *ActionReload.
|
||||
func (a *ActionReload) Name() (name ActionName) { return ActionNameReload }
|
||||
// Name implements the [Action] interface for *ActionRestart.
|
||||
func (a *ActionRestart) Name() (name ActionName) { return ActionNameRestart }
|
||||
|
||||
// isAction implements the [Action] interface for *ActionReload.
|
||||
func (a *ActionReload) isAction() {}
|
||||
// isAction implements the [Action] interface for *ActionRestart.
|
||||
func (a *ActionRestart) isAction() {}
|
||||
|
||||
// ActionStart is the implementation of the [Action] interface.
|
||||
type ActionStart struct {
|
||||
|
|
|
|||
|
|
@ -1,12 +1,15 @@
|
|||
package ossvc
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
_ "embed"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"syscall"
|
||||
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/aghos"
|
||||
"github.com/AdguardTeam/golibs/errors"
|
||||
|
|
@ -16,13 +19,15 @@ import (
|
|||
|
||||
// TODO(e.burkov): Declare managers for each OS.
|
||||
|
||||
// manager is the implementation of [Manager] that wraps [service.Service].
|
||||
// manager is the implementation of [Manager] that uses [service.Service].
|
||||
type manager struct {
|
||||
logger *slog.Logger
|
||||
cmdCons executil.CommandConstructor
|
||||
logger *slog.Logger
|
||||
cmdCons executil.CommandConstructor
|
||||
isOpenWrt bool
|
||||
isUnixSystemV bool
|
||||
}
|
||||
|
||||
// newManager creates a new [Manager] that wraps [service.Service].
|
||||
// newManager creates a new [Manager] that uses [service.Service].
|
||||
//
|
||||
// TODO(e.burkov): Return error.
|
||||
func newManager(_ context.Context, conf *ManagerConfig) (mgr *manager) {
|
||||
|
|
@ -31,8 +36,10 @@ func newManager(_ context.Context, conf *ManagerConfig) (mgr *manager) {
|
|||
chooseSystem()
|
||||
|
||||
return &manager{
|
||||
logger: conf.Logger,
|
||||
cmdCons: conf.CommandConstructor,
|
||||
logger: conf.Logger,
|
||||
cmdCons: conf.CommandConstructor,
|
||||
isOpenWrt: aghos.IsOpenWrt(),
|
||||
isUnixSystemV: service.Platform() == "unix-systemv",
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -43,20 +50,32 @@ var _ Manager = (*manager)(nil)
|
|||
func (m *manager) Perform(ctx context.Context, action Action) (err error) {
|
||||
switch action := action.(type) {
|
||||
case *ActionInstall:
|
||||
return m.install(ctx, action)
|
||||
case *ActionReload:
|
||||
return m.reload(ctx, action)
|
||||
err = m.install(ctx, action)
|
||||
case *ActionRestart:
|
||||
err = m.restart(ctx, action)
|
||||
case *ActionStart:
|
||||
return m.start(ctx, action)
|
||||
err = m.start(ctx, action)
|
||||
case *ActionStop:
|
||||
return m.stop(ctx, action)
|
||||
err = m.stop(ctx, action)
|
||||
case *ActionUninstall:
|
||||
return m.uninstall(ctx, action)
|
||||
err = m.uninstall(ctx, action)
|
||||
default:
|
||||
return fmt.Errorf("action: %w: %T(%[2]v)", errors.ErrBadEnumValue, action)
|
||||
err = fmt.Errorf("action: %w: %T(%[2]v)", errors.ErrBadEnumValue, action)
|
||||
}
|
||||
if err != nil {
|
||||
// Don't wrap the error, since it's informative enough as is.
|
||||
return err
|
||||
}
|
||||
|
||||
m.logger.DebugContext(ctx, "performed service action", "action", action.Name())
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// statusRestartOnFail is a custom status value used to indicate the service's
|
||||
// state of restarting after failed start.
|
||||
const statusRestartOnFail = service.StatusStopped + 1
|
||||
|
||||
// Status implements the [Manager] interface for *manager.
|
||||
func (m *manager) Status(ctx context.Context, name ServiceName) (status Status, err error) {
|
||||
m.logger.InfoContext(ctx, "getting service status", "name", name)
|
||||
|
|
@ -69,10 +88,15 @@ func (m *manager) Status(ctx context.Context, name ServiceName) (status Status,
|
|||
}
|
||||
|
||||
svcStatus, err := s.Status()
|
||||
if err != nil && service.Platform() == "unix-systemv" {
|
||||
if err != nil && m.isUnixSystemV {
|
||||
var code int
|
||||
code, err = m.runInitdCommand(ctx, string(name), "status")
|
||||
if err != nil || code != 0 {
|
||||
// Treat an error or non-zero exit code as stopped status on Unix
|
||||
// System V.
|
||||
//
|
||||
// TODO(e.burkov): Investigate if it's a valid assumption, and
|
||||
// properly handle errors in similar cases.
|
||||
return StatusStopped, nil
|
||||
}
|
||||
|
||||
|
|
@ -87,30 +111,75 @@ func (m *manager) Status(ctx context.Context, name ServiceName) (status Status,
|
|||
return "", fmt.Errorf("getting service status: %w", err)
|
||||
}
|
||||
|
||||
switch svcStatus {
|
||||
case service.StatusRunning:
|
||||
return StatusRunning, nil
|
||||
case service.StatusStopped:
|
||||
return StatusStopped, nil
|
||||
default:
|
||||
return "", fmt.Errorf("service status: %w: %v", errors.ErrBadEnumValue, svcStatus)
|
||||
return statusToInternal(svcStatus)
|
||||
}
|
||||
|
||||
// type check
|
||||
var _ ReloadManager = (*manager)(nil)
|
||||
|
||||
// Reload implements the [ReloadManager] interface for *manager.
|
||||
//
|
||||
// TODO(e.burkov): On Windows just don't implement this interface.
|
||||
func (m *manager) Reload(ctx context.Context, name ServiceName) (err error) {
|
||||
if runtime.GOOS == "windows" {
|
||||
return errors.ErrUnsupported
|
||||
}
|
||||
|
||||
nameStr := string(name)
|
||||
|
||||
var pid int
|
||||
pidFile := filepath.Join("/var", "run", nameStr+".pid")
|
||||
data, err := os.ReadFile(pidFile)
|
||||
if err != nil {
|
||||
if !errors.Is(err, os.ErrNotExist) {
|
||||
return fmt.Errorf("reading service pid file: %w", err)
|
||||
}
|
||||
|
||||
pid, err = aghos.PIDByCommand(ctx, m.logger, nameStr, os.Getpid())
|
||||
if err != nil {
|
||||
return fmt.Errorf("finding process: %w", err)
|
||||
}
|
||||
} else {
|
||||
parts := bytes.SplitN(data, []byte("\n"), 2)
|
||||
if len(parts) == 0 {
|
||||
return fmt.Errorf("parsing %q: %w", pidFile, errors.ErrEmptyValue)
|
||||
}
|
||||
|
||||
pidStr := string(bytes.TrimSpace(parts[0]))
|
||||
pid, err = strconv.Atoi(pidStr)
|
||||
if err != nil {
|
||||
return fmt.Errorf("parsing pid from %q: %w", pidFile, err)
|
||||
}
|
||||
}
|
||||
|
||||
proc, err := os.FindProcess(pid)
|
||||
if err != nil {
|
||||
return fmt.Errorf("finding process with pid %d: %w", pid, err)
|
||||
}
|
||||
|
||||
err = proc.Signal(syscall.SIGHUP)
|
||||
if err != nil {
|
||||
return fmt.Errorf("sending sighup to process with pid %d: %w", pid, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// install installs the service in the service manager.
|
||||
func (m *manager) install(ctx context.Context, action *ActionInstall) (err error) {
|
||||
m.logger.InfoContext(ctx, "installing service", "name", action.ServiceConf.Name)
|
||||
|
||||
s, err := service.New(nil, action.ServiceConf)
|
||||
s, err := service.New(emptyInterface{}, action.ServiceConf)
|
||||
if err != nil {
|
||||
return fmt.Errorf("creating service: %w", err)
|
||||
}
|
||||
|
||||
if err = s.Install(); err != nil {
|
||||
err = s.Install()
|
||||
if err != nil {
|
||||
return fmt.Errorf("installing service: %w", err)
|
||||
}
|
||||
|
||||
if aghos.IsOpenWrt() {
|
||||
if m.isOpenWrt {
|
||||
// On OpenWrt it is important to run enable after the service
|
||||
// installation. Otherwise, the service won't start on the system
|
||||
// startup.
|
||||
|
|
@ -123,19 +192,22 @@ func (m *manager) install(ctx context.Context, action *ActionInstall) (err error
|
|||
return nil
|
||||
}
|
||||
|
||||
// reload stops, if not yet, and starts the configured service in the service
|
||||
// restart stops, if not yet, and starts the configured service in the service
|
||||
// manager.
|
||||
func (m *manager) reload(ctx context.Context, action *ActionReload) (err error) {
|
||||
m.logger.InfoContext(ctx, "reloading service", "name", action.ServiceConf.Name)
|
||||
func (m *manager) restart(ctx context.Context, action *ActionRestart) (err error) {
|
||||
m.logger.InfoContext(ctx, "restarting service", "name", action.ServiceConf.Name)
|
||||
|
||||
s, err := service.New(nil, action.ServiceConf)
|
||||
s, err := service.New(emptyInterface{}, action.ServiceConf)
|
||||
if err != nil {
|
||||
return fmt.Errorf("creating service: %w", err)
|
||||
}
|
||||
|
||||
err = s.Restart()
|
||||
if err != nil && service.Platform() == "unix-systemv" {
|
||||
_, err = m.runInitdCommand(ctx, action.ServiceConf.Name, "restart")
|
||||
if err != nil && m.isUnixSystemV {
|
||||
_, initdErr := m.runInitdCommand(ctx, action.ServiceConf.Name, "restart")
|
||||
if initdErr != nil {
|
||||
return fmt.Errorf("%w (restarting via init.d: %w)", err, initdErr)
|
||||
}
|
||||
}
|
||||
|
||||
return err
|
||||
|
|
@ -150,14 +222,17 @@ func (m *manager) start(ctx context.Context, action *ActionStart) (err error) {
|
|||
m.logger.ErrorContext(ctx, "pre-check failed", "err", err)
|
||||
}
|
||||
|
||||
s, err := service.New(nil, action.ServiceConf)
|
||||
s, err := service.New(emptyInterface{}, action.ServiceConf)
|
||||
if err != nil {
|
||||
return fmt.Errorf("creating service: %w", err)
|
||||
}
|
||||
|
||||
err = s.Start()
|
||||
if err != nil && service.Platform() == "unix-systemv" {
|
||||
_, err = m.runInitdCommand(ctx, action.ServiceConf.Name, "start")
|
||||
if err != nil && m.isUnixSystemV {
|
||||
_, initdErr := m.runInitdCommand(ctx, action.ServiceConf.Name, "start")
|
||||
if initdErr != nil {
|
||||
return fmt.Errorf("%w (starting via init.d: %w)", err, initdErr)
|
||||
}
|
||||
}
|
||||
|
||||
return err
|
||||
|
|
@ -167,14 +242,17 @@ func (m *manager) start(ctx context.Context, action *ActionStart) (err error) {
|
|||
func (m *manager) stop(ctx context.Context, action *ActionStop) (err error) {
|
||||
m.logger.InfoContext(ctx, "stopping service", "name", action.ServiceConf.Name)
|
||||
|
||||
s, err := service.New(nil, action.ServiceConf)
|
||||
s, err := service.New(emptyInterface{}, action.ServiceConf)
|
||||
if err != nil {
|
||||
return fmt.Errorf("creating service: %w", err)
|
||||
}
|
||||
|
||||
err = s.Stop()
|
||||
if err != nil && service.Platform() == "unix-systemv" {
|
||||
_, err = m.runInitdCommand(ctx, action.ServiceConf.Name, "stop")
|
||||
if err != nil && m.isUnixSystemV {
|
||||
_, initdErr := m.runInitdCommand(ctx, action.ServiceConf.Name, "stop")
|
||||
if initdErr != nil {
|
||||
return fmt.Errorf("%w (stopping via init.d: %w)", err, initdErr)
|
||||
}
|
||||
}
|
||||
|
||||
return err
|
||||
|
|
@ -184,7 +262,7 @@ func (m *manager) stop(ctx context.Context, action *ActionStop) (err error) {
|
|||
func (m *manager) uninstall(ctx context.Context, action *ActionUninstall) (err error) {
|
||||
m.logger.InfoContext(ctx, "uninstalling service", "name", action.ServiceConf.Name)
|
||||
|
||||
if aghos.IsOpenWrt() {
|
||||
if m.isOpenWrt {
|
||||
// On OpenWrt it is important to run disable command first as it will
|
||||
// remove the symlink.
|
||||
_, err = m.runInitdCommand(ctx, action.ServiceConf.Name, "disable")
|
||||
|
|
@ -193,20 +271,24 @@ func (m *manager) uninstall(ctx context.Context, action *ActionUninstall) (err e
|
|||
}
|
||||
}
|
||||
|
||||
s, err := service.New(nil, action.ServiceConf)
|
||||
s, err := service.New(emptyInterface{}, action.ServiceConf)
|
||||
if err != nil {
|
||||
return fmt.Errorf("creating service: %w", err)
|
||||
}
|
||||
|
||||
if err = s.Stop(); err != nil {
|
||||
err = s.Stop()
|
||||
if err != nil {
|
||||
m.logger.DebugContext(ctx, "stopping service", "err", err)
|
||||
}
|
||||
|
||||
if err = s.Uninstall(); err != nil {
|
||||
err = s.Uninstall()
|
||||
if err != nil {
|
||||
return fmt.Errorf("uninstalling service: %w", err)
|
||||
}
|
||||
|
||||
removeLaunchdStdLogs(ctx, m.logger)
|
||||
if runtime.GOOS == "darwin" {
|
||||
removeLaunchdStdLogs(ctx, m.logger)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
@ -248,9 +330,24 @@ func (m *manager) runInitdCommand(
|
|||
serviceName string,
|
||||
action string,
|
||||
) (code int, err error) {
|
||||
confPath := "/etc/init.d/" + serviceName
|
||||
confPath := filepath.Join("/etc", "init.d", serviceName)
|
||||
// Pass the script and action as a single string argument.
|
||||
//
|
||||
// TODO(e.burkov): Use CommandConstructor.
|
||||
code, _, err = aghos.RunCommand(ctx, m.cmdCons, "sh", "-c", confPath, action)
|
||||
|
||||
return code, err
|
||||
}
|
||||
|
||||
// emptyInterface is an empty implementation of the [service.Interface], as the
|
||||
// actual implementation is onlyy needed for the [service.Service.Run] method.
|
||||
type emptyInterface struct{}
|
||||
|
||||
// type check
|
||||
var _ service.Interface = emptyInterface{}
|
||||
|
||||
// Start implements the [service.Interface] interface for emptyInterface.
|
||||
func (emptyInterface) Start(_ service.Service) (err error) { return nil }
|
||||
|
||||
// Stop implements the [service.Interface] interface for emptyInterface.
|
||||
func (emptyInterface) Stop(_ service.Service) (err error) { return nil }
|
||||
|
|
|
|||
|
|
@ -4,6 +4,13 @@
|
|||
// TODO(e.burkov): Add tests.
|
||||
package ossvc
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/AdguardTeam/golibs/errors"
|
||||
"github.com/kardianos/service"
|
||||
)
|
||||
|
||||
// ServiceName is the name of a service.
|
||||
//
|
||||
// TODO(e.burkov): Validate for each platform.
|
||||
|
|
@ -21,4 +28,25 @@ const (
|
|||
|
||||
// StatusRunning means that the service is running.
|
||||
StatusRunning Status = "running"
|
||||
|
||||
// StatusRestartOnFail means that the service is restarting after failed
|
||||
// start.
|
||||
StatusRestartOnFail Status = "restart on fail"
|
||||
)
|
||||
|
||||
// statusToInternal converts a service.Status to a Status.
|
||||
//
|
||||
// TODO(e.burkov): Get rid of [service] package dependency and remove this
|
||||
// function.
|
||||
func statusToInternal(status service.Status) (s Status, err error) {
|
||||
switch status {
|
||||
case service.StatusRunning:
|
||||
return StatusRunning, nil
|
||||
case service.StatusStopped:
|
||||
return StatusStopped, nil
|
||||
case statusRestartOnFail:
|
||||
return StatusRestartOnFail, nil
|
||||
default:
|
||||
return "", fmt.Errorf("service status: %w: %v", errors.ErrBadEnumValue, status)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
13
internal/ossvc/reloadmanager.go
Normal file
13
internal/ossvc/reloadmanager.go
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
package ossvc
|
||||
|
||||
import "context"
|
||||
|
||||
// ReloadManager is the extension interface for [Manager] that provides an
|
||||
// ability to reload a service.
|
||||
type ReloadManager interface {
|
||||
Manager
|
||||
|
||||
// Reload reloads the service with the given name. As opposed to
|
||||
// [ActionRestart], this method does not stop the service.
|
||||
Reload(ctx context.Context, name ServiceName) (err error)
|
||||
}
|
||||
|
|
@ -235,10 +235,6 @@ func parseSystemctlShow(output io.Reader) (status service.Status, err error) {
|
|||
return statusFromState(loadState, activeState, subState)
|
||||
}
|
||||
|
||||
// statusRestartOnFail is a custom status value used to indicate the service's
|
||||
// state of restarting after failed start.
|
||||
const statusRestartOnFail = service.StatusStopped + 1
|
||||
|
||||
// statusFromState returns the service status based on the systemctl state
|
||||
// property values.
|
||||
func statusFromState(loadState, activeState, subState string) (status service.Status, err error) {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue