blob: 507e8ec4ebf328e5cd605b595b5a28cb434bdf33 [file]
// Copyright 2021 The Chromium OS Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// Package kioskmode provides ways to set policies for local device accounts
// in a Kiosk mode.
package kioskmode
import (
"context"
"io/ioutil"
"os"
"strings"
"time"
"chromiumos/tast/common/policy"
"chromiumos/tast/common/policy/fakedms"
"chromiumos/tast/errors"
"chromiumos/tast/local/chrome"
"chromiumos/tast/local/chrome/uiauto/nodewith"
"chromiumos/tast/local/policyutil"
"chromiumos/tast/local/policyutil/fixtures"
"chromiumos/tast/local/syslog"
"chromiumos/tast/local/upstart"
"chromiumos/tast/testing"
"chromiumos/tast/timing"
)
var (
// WebKioskAccountID identifier of the web Kiosk application.
WebKioskAccountID = "arbitrary_id_web_kiosk_1"
webKioskAccountType = policy.AccountTypeKioskWebApp
webKioskIconURL = "https://www.google.com"
webKioskTitle = "TastKioskModeSetByPolicyGooglePage"
webKioskURL = "https://www.google.com"
// DeviceLocalAccountInfo uses *string instead of string for internal data
// structure. That is needed since fields in json are marked as omitempty.
webKioskPolicy = policy.DeviceLocalAccountInfo{
AccountID: &WebKioskAccountID,
AccountType: &webKioskAccountType,
WebKioskAppInfo: &policy.WebKioskAppInfo{
Url: &webKioskURL,
Title: &webKioskTitle,
IconUrl: &webKioskIconURL,
}}
// KioskAppAccountID identifier of the Kiosk application.
KioskAppAccountID = "arbitrary_id_store_app_2"
kioskAppAccountType = policy.AccountTypeKioskApp
// KioskAppID pointing to the Printtest app - not listed in the WebStore.
KioskAppID = "aajgmlihcokkalfjbangebcffdoanjfo"
// KioskAppBtnNode node representing this application on the Apps menu on
// the Sign-in screen.
KioskAppBtnNode = nodewith.Name("Simple Printest").ClassName("MenuItemView")
kioskAppPolicy = policy.DeviceLocalAccountInfo{
AccountID: &KioskAppAccountID,
AccountType: &kioskAppAccountType,
KioskAppInfo: &policy.KioskAppInfo{
AppId: &KioskAppID,
}}
// DefaultLocalAccountsConfiguration holds default Kiosks accounts
// configuration. Each, when setting public account policies can be
// referred by id: KioskAppAccountID and WebKioskAccountID
DefaultLocalAccountsConfiguration = policy.DeviceLocalAccounts{
Val: []policy.DeviceLocalAccountInfo{
kioskAppPolicy,
webKioskPolicy,
},
}
)
const (
// kioskStartingLog is reported by chrome once the kiosk mode is starting.
kioskStartingLog = "Starting kiosk mode"
// kioskLaunchSucceededLog is reported by chrome once the kiosk launch is succeeded.
kioskLaunchSucceededLog = "Kiosk launch succeeded"
// kioskClosingSplashScreenLog is reported by chtome once the splash screen is gone.
kioskClosingSplashScreenLog = "App window created, closing splash screen."
)
// Kiosk structure holds necessary references and provides a way to safely
// close Kiosk mode.
type Kiosk struct {
cr *chrome.Chrome
fdms *fakedms.FakeDMS
localAccounts *policy.DeviceLocalAccounts
autostart bool
}
// Close clears policies, but keeps serving device local accounts then closes
// Chrome. Ideally we would serve an empty policies slice however, that makes
// Chrome crashes when AutoLaunch() option was used.
func (k *Kiosk) Close(ctx context.Context) (retErr error) {
// If Chrome fails to start in RestartChromeWithOptions it has already been
// cleaned up by startChromeClearPolicies.
if k.cr == nil {
return errors.New("Skipping kiosk.Close() because Chrome is nil")
}
// Using defer to make sure Chrome is always closed.
defer func(ctx context.Context) {
if err := k.cr.Close(ctx); err != nil {
// Chrome error supersedes previous error if any.
retErr = errors.Wrap(err, "could not close Chrome while closing Kiosk session")
}
}(ctx)
var policies []policy.Policy
// When AutoLaunch() option was used, then the corresponding policy has to
// be removed before starting a new Chrome session. Otherwise Kiosk will
// start again. When applying an empty policies slice, Chrome crashes.
// Hence the safest way is to apply local accounts again. That way Chrome
// will load them but will start normally. If the next tests want to use
// policy they will override them.
if k.autostart {
policies = append(policies, k.localAccounts)
}
if err := policyutil.ServeAndRefresh(ctx, k.fdms, k.cr, policies); err != nil {
testing.ContextLog(ctx, "Could not serve and refresh policies. If kioskmode.AutoLaunch() option was used it may impact next test : ", err)
return errors.Wrap(err, "could not clear policies")
}
return nil
}
// ConfirmKioskStarted uses reader for looking for logs that confirm Kiosk
// mode starting and also successful launch of Kiosk.
// reader Reader instance should be processing logs filtered for Chrome only.
func ConfirmKioskStarted(ctx context.Context, reader *syslog.Reader) error {
// Check that Kiosk starts successfully.
testing.ContextLog(ctx, "Waiting for Kiosk mode start")
const (
// logScanTimeout is a timeout for log messages indicating Kiosk startup
// and successful launch to be present. It is set to over a minute as Kiosk
// mode launch varies depending on device. crbug.com/1222136
logScanTimeout = 90 * time.Second
)
if _, err := reader.Wait(ctx, logScanTimeout,
func(e *syslog.Entry) bool {
return strings.Contains(e.Content, kioskStartingLog)
},
); err != nil {
return errors.Wrap(err, "failed to verify starting of Kiosk mode")
}
testing.ContextLog(ctx, "Waiting for successful Kiosk mode launch")
if _, err := reader.Wait(ctx, logScanTimeout,
func(e *syslog.Entry) bool {
return strings.Contains(e.Content, kioskLaunchSucceededLog)
},
); err != nil {
return errors.Wrap(err, "failed to verify successful launch of Kiosk mode")
}
return nil
}
// IsKioskAppStarted searches for existing logs to confirm Kiosk is running.
func IsKioskAppStarted(ctx context.Context) error {
logContent, err := ioutil.ReadFile(syslog.ChromeLogFile)
if err != nil {
return errors.Wrap(err, "failed to read "+syslog.ChromeLogFile)
}
if !strings.Contains(string(logContent), kioskClosingSplashScreenLog) {
return errors.New("failed to verify successful launch of Kiosk mode")
}
return nil
}
// New starts Chrome, sets passed Kiosk related options to policies and
// restarts Chrome. When kioskmode.AutoLaunch() is used, then it auto starts
// given Kiosk application. Alternatively use kioskmode.ExtraChromeOptions()
// passing chrome.LoadSigninProfileExtension(). In that case Chrome is started
// and stays on Signin screen with Kiosk accounts loaded.
// Use defer kiosk.Close(ctx) to clean.
func New(ctx context.Context, fdms *fakedms.FakeDMS, opts ...Option) (*Kiosk, *chrome.Chrome, error) {
cfg, err := NewConfig(opts)
if err != nil {
return nil, nil, errors.Wrap(err, "failed to process options")
}
if cfg.m.DeviceLocalAccounts == nil {
return nil, nil, errors.Wrap(err, "local device accounts were not set")
}
err = func(ctx context.Context) error {
testing.ContextLog(ctx, "Kiosk mode: Starting Chrome to set Kiosk policies")
cr, err := chrome.New(
ctx,
chrome.FakeLogin(chrome.Creds{User: fixtures.Username, Pass: fixtures.Password}), // Required as refreshing policies require test API.
chrome.DMSPolicy(fdms.URL),
chrome.KeepEnrollment(),
)
if err != nil {
return errors.Wrap(err, "failed to start Chrome")
}
// Close the previous Chrome instance.
defer cr.Close(ctx)
// Set local accounts policy.
policies := []policy.Policy{
cfg.m.DeviceLocalAccounts,
}
// Handle the AutoLaunch setup.
if cfg.m.AutoLaunch == true {
policies = append(policies, &policy.DeviceLocalAccountAutoLoginId{
Val: *cfg.m.AutoLaunchKioskAppID,
})
}
// Handle setting device policies.
if cfg.m.ExtraPolicies != nil {
policies = append(policies, cfg.m.ExtraPolicies...)
}
pb := policy.NewBlob()
pb.AddPolicies(policies)
// Handle public account policies.
if cfg.m.PublicAccountPolicies != nil {
for accountID, policies := range cfg.m.PublicAccountPolicies {
pb.AddPublicAccountPolicies(accountID, policies)
}
}
// Update policies.
if err := policyutil.ServeBlobAndRefresh(ctx, fdms, cr, pb); err != nil {
// In case of AutoLaunch was used we try to override policies with
// local accounts similarly as in kioskmode.Close().
if cfg.m.AutoLaunch == true {
if err := policyutil.ServeAndRefresh(ctx, fdms, cr, []policy.Policy{cfg.m.DeviceLocalAccounts}); err != nil {
testing.ContextLog(ctx, "Could not serve and refresh policies. If kioskmode.AutoLaunch() option was used it may impact next test : ", err)
}
}
return errors.Wrap(err, "failed to serve and refresh policies")
}
return nil
}(ctx)
if err != nil {
return nil, nil, errors.Wrap(err, "failed preparing Chrome to start with given Kiosk configuration")
}
reader, err := syslog.NewReader(ctx, syslog.Program("chrome"))
if err != nil {
return nil, nil, errors.Wrap(err, "failed to start log reader")
}
defer reader.Close()
var cr *chrome.Chrome
if cfg.m.AutoLaunch {
opts := []chrome.Option{
chrome.NoLogin(),
chrome.DMSPolicy(fdms.URL),
chrome.KeepEnrollment(),
}
opts = append(opts, cfg.m.ExtraChromeOptions...)
testing.ContextLog(ctx, "Kiosk mode: Starting Chrome in Kiosk mode")
// Restart Chrome. After that Kiosk auto starts.
cr, err = chrome.New(ctx, opts...)
if err != nil {
if err := startChromeClearPolicies(ctx, fdms, fixtures.Username, fixtures.Password); err != nil {
return nil, nil, errors.Wrap(err, "could not finish cleanup")
}
return nil, nil, errors.Wrap(err, "Chrome restart failed")
}
if err := ConfirmKioskStarted(ctx, reader); err != nil {
if err := policyutil.ServeAndRefresh(ctx, fdms, cr, []policy.Policy{cfg.m.DeviceLocalAccounts}); err != nil {
testing.ContextLog(ctx, "Could not serve and refresh policies. If kioskmode.AutoLaunch() option was used it may impact next test : ", err)
}
cr.Close(ctx)
return nil, nil, errors.Wrap(err, "there was a problem while checking chrome logs for Kiosk related entries")
}
} else {
opts := []chrome.Option{
chrome.DeferLogin(),
chrome.DMSPolicy(fdms.URL),
chrome.KeepEnrollment(),
}
opts = append(opts, cfg.m.ExtraChromeOptions...)
testing.ContextLog(ctx, "Kiosk mode: Starting Chrome on Signin screen with set Kiosk apps")
// Restart Chrome. Chrome stays on Sing-in screen
cr, err = chrome.New(ctx, opts...)
if err != nil {
return nil, nil, errors.Wrap(err, "Chrome restart failed")
}
}
return &Kiosk{cr: cr, fdms: fdms, localAccounts: cfg.m.DeviceLocalAccounts, autostart: cfg.m.AutoLaunch}, cr, nil
}
// startChromeClearPolicies is called when Chrome fails to start in autostart
// mode - when kioskmode.AutoLaunch() option was used. We need to start Chrome
// and clean policies to prevent Chrome starting automatically in Kiosk mode
// for next test.
// FIXME: this cleanup doesn't work on some devices (e.g. chell). FakeLogin()
// doesn't work either. Need to figure out some way to fix this.
func startChromeClearPolicies(ctx context.Context, fdms *fakedms.FakeDMS, username, password string) error {
cr, err := chrome.New(
ctx,
chrome.NoLogin(),
chrome.DMSPolicy(fdms.URL),
chrome.KeepEnrollment(),
)
if err != nil {
return errors.Wrap(err, "failed to start Chrome for cleanup")
}
defer cr.Close(ctx)
if err := policyutil.ServeAndRefresh(ctx, fdms, cr, []policy.Policy{}); err != nil {
return errors.Wrap(err, "failed to clear policies")
}
return nil
}
// WaitForCrxInCache waits for Kiosk crx to be available in cache.
func WaitForCrxInCache(ctx context.Context, id string) error {
const crxCachePath = "/home/chronos/kiosk/crx/"
ctx, st := timing.Start(ctx, "wait_crx_cache")
defer st.End()
return testing.Poll(ctx, func(ctx context.Context) error {
files, err := ioutil.ReadDir(crxCachePath)
if err != nil {
if errors.Is(err, os.ErrNotExist) {
return errors.Wrap(err, "Kiosk crx cache does not exist yet")
}
return testing.PollBreak(errors.Wrap(err, "failed to list content of Kiosk cache"))
}
for _, file := range files {
if strings.HasPrefix(file.Name(), id) {
testing.ContextLog(ctx, "Found crx in cache: "+file.Name())
return nil
}
}
return errors.Wrap(err, "Kiosk crx cache does not have "+id)
}, nil)
}
// RestartChromeWithOptions replaces the current Chrome in kiosk instance with
// a new one using custom options. It will be closed by Kiosk.Close().
func (k *Kiosk) RestartChromeWithOptions(ctx context.Context, opts ...chrome.Option) (*chrome.Chrome, error) {
if err := k.cr.Close(ctx); err != nil {
return nil, errors.Wrap(err, "failed to close Chrome")
}
k.cr = nil
testing.ContextLog(ctx, "Restarting ui")
if err := upstart.RestartJob(ctx, "ui"); err != nil {
if err := startChromeClearPolicies(ctx, k.fdms, fixtures.Username, fixtures.Password); err != nil {
return nil, errors.Wrap(err, "could not finish cleanup")
}
return nil, errors.Wrap(err, "failed to restart ui")
}
cr, err := chrome.New(ctx, opts...)
if err != nil {
if err := startChromeClearPolicies(ctx, k.fdms, fixtures.Username, fixtures.Password); err != nil {
return nil, errors.Wrap(err, "could not finish cleanup")
}
return nil, errors.Wrap(err, "failed to start new Chrome")
}
k.cr = cr
return cr, err
}