Allow saving configuration from the first-time wizard
This just updates the config and saves it to `flamenco-manager.yaml`. Saving the configuration doesn't restart the Manager yet, that's for another commit.
This commit is contained in:
parent
f9a3d3864a
commit
10f56148d4
@ -165,6 +165,9 @@ type ConfigService interface {
|
||||
// ForceFirstRun forces IsFirstRun() to return true. This is used to force the
|
||||
// first-time wizard on a configured system.
|
||||
ForceFirstRun()
|
||||
|
||||
// Save writes the in-memory configuration to the config file.
|
||||
Save() error
|
||||
}
|
||||
|
||||
type Shaman interface {
|
||||
|
@ -11,6 +11,8 @@ import (
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"git.blender.org/flamenco/internal/appinfo"
|
||||
"git.blender.org/flamenco/internal/find_blender"
|
||||
@ -168,6 +170,7 @@ func (f *Flamenco) FindBlenderExePath(e echo.Context) error {
|
||||
default:
|
||||
response = append(response, api.BlenderPathCheckResult{
|
||||
IsUsable: true,
|
||||
Input: result.Input,
|
||||
Path: result.FoundLocation,
|
||||
Cause: result.BlenderVersion,
|
||||
Source: result.Source,
|
||||
@ -186,6 +189,7 @@ func (f *Flamenco) FindBlenderExePath(e echo.Context) error {
|
||||
default:
|
||||
response = append(response, api.BlenderPathCheckResult{
|
||||
IsUsable: true,
|
||||
Input: result.Input,
|
||||
Path: result.FoundLocation,
|
||||
Cause: result.BlenderVersion,
|
||||
Source: result.Source,
|
||||
@ -212,6 +216,7 @@ func (f *Flamenco) CheckBlenderExePath(e echo.Context) error {
|
||||
ctx := e.Request().Context()
|
||||
checkResult, err := find_blender.CheckBlender(ctx, command)
|
||||
response := api.BlenderPathCheckResult{
|
||||
Input: command,
|
||||
Source: checkResult.Source,
|
||||
}
|
||||
|
||||
@ -229,6 +234,67 @@ func (f *Flamenco) CheckBlenderExePath(e echo.Context) error {
|
||||
return e.JSON(http.StatusOK, response)
|
||||
}
|
||||
|
||||
func (f *Flamenco) SaveWizardConfig(e echo.Context) error {
|
||||
logger := requestLogger(e)
|
||||
|
||||
var wizardCfg api.WizardConfig
|
||||
if err := e.Bind(&wizardCfg); err != nil {
|
||||
logger.Warn().Err(err).Msg("first-time wizard: bad request received")
|
||||
return sendAPIError(e, http.StatusBadRequest, "invalid format")
|
||||
}
|
||||
|
||||
logger = logger.With().Interface("config", wizardCfg).Logger()
|
||||
|
||||
if wizardCfg.StorageLocation == "" ||
|
||||
!wizardCfg.BlenderExecutable.IsUsable ||
|
||||
wizardCfg.BlenderExecutable.Path == "" {
|
||||
logger.Warn().Msg("first-time wizard: configuration is incomplete, unable to accept")
|
||||
return sendAPIError(e, http.StatusBadRequest, "configuration is incomplete")
|
||||
}
|
||||
|
||||
conf := f.config.Get()
|
||||
conf.SharedStoragePath = wizardCfg.StorageLocation
|
||||
|
||||
var executable string
|
||||
switch wizardCfg.BlenderExecutable.Source {
|
||||
case api.BlenderPathSourceFileAssociation:
|
||||
// The Worker will try to use the file association when the command is set
|
||||
// to the string "blender".
|
||||
executable = "blender"
|
||||
case api.BlenderPathSourcePathEnvvar:
|
||||
// The input command can be found on $PATH, and thus we don't need to save
|
||||
// the absolute path to Blender here.
|
||||
executable = wizardCfg.BlenderExecutable.Input
|
||||
case api.BlenderPathSourceInputPath:
|
||||
// The path should be used as-is.
|
||||
executable = wizardCfg.BlenderExecutable.Path
|
||||
}
|
||||
if commandNeedsQuoting(executable) {
|
||||
executable = strconv.Quote(executable)
|
||||
}
|
||||
blenderCommand := fmt.Sprintf("%s %s", executable, config.DefaultBlenderArguments)
|
||||
|
||||
// Use the same command for each platform for now, but put them each in their
|
||||
// own definition so that they're easier to edit later.
|
||||
conf.Variables["blender"] = config.Variable{
|
||||
IsTwoWay: false,
|
||||
Values: config.VariableValues{
|
||||
{Platform: "linux", Value: blenderCommand},
|
||||
{Platform: "windows", Value: blenderCommand},
|
||||
{Platform: "darwin", Value: blenderCommand},
|
||||
},
|
||||
}
|
||||
|
||||
// Save the final configuration to disk.
|
||||
if err := f.config.Save(); err != nil {
|
||||
logger.Error().Err(err).Msg("error saving configuration file")
|
||||
return sendAPIError(e, http.StatusInternalServerError, "first-time wizard: error saving configuration file: %v", err)
|
||||
}
|
||||
|
||||
logger.Info().Msg("first-time wizard: updating configuration")
|
||||
return e.NoContent(http.StatusNoContent)
|
||||
}
|
||||
|
||||
func flamencoManagerDir() (string, error) {
|
||||
exename, err := os.Executable()
|
||||
if err != nil {
|
||||
@ -236,3 +302,7 @@ func flamencoManagerDir() (string, error) {
|
||||
}
|
||||
return filepath.Dir(exename), nil
|
||||
}
|
||||
|
||||
func commandNeedsQuoting(cmd string) bool {
|
||||
return strings.ContainsAny(cmd, " \n\t;()")
|
||||
}
|
||||
|
@ -116,6 +116,102 @@ func TestCheckSharedStoragePath(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestSaveWizardConfig(t *testing.T) {
|
||||
mf, finish := metaTestFixtures(t)
|
||||
defer finish()
|
||||
|
||||
doTest := func(body api.WizardConfig) config.Conf {
|
||||
// Always start the test with a clean configuration.
|
||||
originalConfig := config.DefaultConfig(func(c *config.Conf) {
|
||||
c.SharedStoragePath = ""
|
||||
})
|
||||
var savedConfig config.Conf
|
||||
|
||||
// Mock the loading & saving of the config.
|
||||
mf.config.EXPECT().Get().Return(&originalConfig)
|
||||
mf.config.EXPECT().Save().Do(func() error {
|
||||
savedConfig = originalConfig
|
||||
return nil
|
||||
})
|
||||
|
||||
// Call the API.
|
||||
echoCtx := mf.prepareMockedJSONRequest(body)
|
||||
err := mf.flamenco.SaveWizardConfig(echoCtx)
|
||||
if !assert.NoError(t, err) {
|
||||
t.FailNow()
|
||||
}
|
||||
|
||||
assertResponseNoContent(t, echoCtx)
|
||||
return savedConfig
|
||||
}
|
||||
|
||||
// Test situation where file association with .blend files resulted in a blender executable.
|
||||
{
|
||||
savedConfig := doTest(api.WizardConfig{
|
||||
StorageLocation: mf.tempdir,
|
||||
BlenderExecutable: api.BlenderPathCheckResult{
|
||||
IsUsable: true,
|
||||
Input: "",
|
||||
Path: "/path/to/blender",
|
||||
Source: api.BlenderPathSourceFileAssociation,
|
||||
},
|
||||
})
|
||||
assert.Equal(t, mf.tempdir, savedConfig.SharedStoragePath)
|
||||
expectBlenderVar := config.Variable{
|
||||
Values: config.VariableValues{
|
||||
{Platform: "linux", Value: "blender " + config.DefaultBlenderArguments},
|
||||
{Platform: "windows", Value: "blender " + config.DefaultBlenderArguments},
|
||||
{Platform: "darwin", Value: "blender " + config.DefaultBlenderArguments},
|
||||
},
|
||||
}
|
||||
assert.Equal(t, expectBlenderVar, savedConfig.Variables["blender"])
|
||||
}
|
||||
|
||||
// Test situation where the given command could be found on $PATH.
|
||||
{
|
||||
savedConfig := doTest(api.WizardConfig{
|
||||
StorageLocation: mf.tempdir,
|
||||
BlenderExecutable: api.BlenderPathCheckResult{
|
||||
IsUsable: true,
|
||||
Input: "kitty",
|
||||
Path: "/path/to/kitty",
|
||||
Source: api.BlenderPathSourcePathEnvvar,
|
||||
},
|
||||
})
|
||||
assert.Equal(t, mf.tempdir, savedConfig.SharedStoragePath)
|
||||
expectBlenderVar := config.Variable{
|
||||
Values: config.VariableValues{
|
||||
{Platform: "linux", Value: "kitty " + config.DefaultBlenderArguments},
|
||||
{Platform: "windows", Value: "kitty " + config.DefaultBlenderArguments},
|
||||
{Platform: "darwin", Value: "kitty " + config.DefaultBlenderArguments},
|
||||
},
|
||||
}
|
||||
assert.Equal(t, expectBlenderVar, savedConfig.Variables["blender"])
|
||||
}
|
||||
|
||||
// Test a custom command given with the full path.
|
||||
{
|
||||
savedConfig := doTest(api.WizardConfig{
|
||||
StorageLocation: mf.tempdir,
|
||||
BlenderExecutable: api.BlenderPathCheckResult{
|
||||
IsUsable: true,
|
||||
Input: "/bin/cat",
|
||||
Path: "/bin/cat",
|
||||
Source: api.BlenderPathSourceInputPath,
|
||||
},
|
||||
})
|
||||
assert.Equal(t, mf.tempdir, savedConfig.SharedStoragePath)
|
||||
expectBlenderVar := config.Variable{
|
||||
Values: config.VariableValues{
|
||||
{Platform: "linux", Value: "/bin/cat " + config.DefaultBlenderArguments},
|
||||
{Platform: "windows", Value: "/bin/cat " + config.DefaultBlenderArguments},
|
||||
{Platform: "darwin", Value: "/bin/cat " + config.DefaultBlenderArguments},
|
||||
},
|
||||
}
|
||||
assert.Equal(t, expectBlenderVar, savedConfig.Variables["blender"])
|
||||
}
|
||||
}
|
||||
|
||||
func metaTestFixtures(t *testing.T) (mockedFlamenco, func()) {
|
||||
mockCtrl := gomock.NewController(t)
|
||||
mf := newMockedFlamenco(mockCtrl)
|
||||
|
@ -754,6 +754,20 @@ func (mr *MockConfigServiceMockRecorder) ResolveVariables(arg0, arg1 interface{}
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ResolveVariables", reflect.TypeOf((*MockConfigService)(nil).ResolveVariables), arg0, arg1)
|
||||
}
|
||||
|
||||
// Save mocks base method.
|
||||
func (m *MockConfigService) Save() error {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "Save")
|
||||
ret0, _ := ret[0].(error)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// Save indicates an expected call of Save.
|
||||
func (mr *MockConfigServiceMockRecorder) Save() *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Save", reflect.TypeOf((*MockConfigService)(nil).Save))
|
||||
}
|
||||
|
||||
// MockTaskStateMachine is a mock of TaskStateMachine interface.
|
||||
type MockTaskStateMachine struct {
|
||||
ctrl *gomock.Controller
|
||||
|
@ -9,6 +9,8 @@ import (
|
||||
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
const DefaultBlenderArguments = "--factory-startup -b -y"
|
||||
|
||||
// The default configuration, use DefaultConfig() to obtain a copy.
|
||||
var defaultConfig = Conf{
|
||||
Base: Base{
|
||||
@ -61,9 +63,9 @@ var defaultConfig = Conf{
|
||||
// The default commands assume that the executables are available on $PATH.
|
||||
"blender": {
|
||||
Values: VariableValues{
|
||||
VariableValue{Platform: "linux", Value: "blender --factory-startup -b -y"},
|
||||
VariableValue{Platform: "windows", Value: "blender.exe --factory-startup -b -y"},
|
||||
VariableValue{Platform: "darwin", Value: "blender --factory-startup -b -y"},
|
||||
VariableValue{Platform: "linux", Value: "blender " + DefaultBlenderArguments},
|
||||
VariableValue{Platform: "windows", Value: "blender.exe " + DefaultBlenderArguments},
|
||||
VariableValue{Platform: "darwin", Value: "blender " + DefaultBlenderArguments},
|
||||
},
|
||||
},
|
||||
"ffmpeg": {
|
||||
|
@ -3,8 +3,8 @@
|
||||
<h1>Welcome to Flamenco!</h1>
|
||||
|
||||
<section>
|
||||
<p>Before Flamenco can be used, a few things need to be set up.</p>
|
||||
<p>This wizard will guide you through the configuration.</p>
|
||||
<p>Before Flamenco can be used, a few things need to be set up. This
|
||||
wizard will guide you through the configuration.</p>
|
||||
</section>
|
||||
<section>
|
||||
<h2>Shared Storage</h2>
|
||||
@ -13,10 +13,15 @@
|
||||
Manager and Workers exchange files. This could be a NAS in your network,
|
||||
or some other file sharing server.</p>
|
||||
|
||||
<p>Make sure this path is the same for all machines involved.</p>
|
||||
|
||||
<p class="hint">Using a service like Syncthing, ownCloud, or Dropbox for
|
||||
this is not recommended, as Flamenco does not know when every machine has
|
||||
received the files.</p>
|
||||
|
||||
<!-- TODO: @submit.prevent makes the button triggerable by pressing ENTER
|
||||
in the input field, but also prevents the browser from caching
|
||||
previously-used values. Would be great if we could have both. -->
|
||||
<form @submit.prevent="checkSharedStoragePath">
|
||||
<input v-model="sharedStoragePath" type="text">
|
||||
<button type="submit">Check</button>
|
||||
@ -33,7 +38,7 @@
|
||||
|
||||
<p>Choose which Blender to use below:</p>
|
||||
|
||||
<p v-if="blenderExeFinding">... finding Blenders ...</p>
|
||||
<p v-if="isBlenderExeFinding">... finding Blenders ...</p>
|
||||
<div v-for="blender in allBlenders" class="blender-selector"
|
||||
:class="{ 'selected-blender': (blender == selectedBlender) }">
|
||||
<dl>
|
||||
@ -46,7 +51,7 @@
|
||||
<dt>Source</dt>
|
||||
<dd>{{ sourceLabels[blender.source] }}</dd>
|
||||
</dl>
|
||||
<button @click="selectedBlender = blender">Use this Blender</button>
|
||||
<button @click="selectedBlender = blender" :disabled="selectedBlender == blender">Use this Blender</button>
|
||||
</div>
|
||||
|
||||
<p>Or provide an alternative command to try:</p>
|
||||
@ -55,12 +60,35 @@
|
||||
<input v-model="customBlenderExe" type="text">
|
||||
<button type="submit">Check</button>
|
||||
</form>
|
||||
<p v-if="blenderExeChecking">... checking ...</p>
|
||||
<p v-if="isBlenderExeChecking">... checking ...</p>
|
||||
<p v-if="blenderExeCheckResult != null && blenderExeCheckResult.is_usable" class="check-ok">
|
||||
Found something, it is selected above.</p>
|
||||
<p v-if="blenderExeCheckResult != null && !blenderExeCheckResult.is_usable" class="check-failed">
|
||||
{{ blenderExeCheckResult.cause }}</p>
|
||||
</section>
|
||||
|
||||
<section v-if="isConfigComplete">
|
||||
<h2>The Final Step</h2>
|
||||
<p>This is the configuration that will be used by Flamenco:</p>
|
||||
<dl>
|
||||
<dt>Storage</dt>
|
||||
<dd>{{ sharedStorageCheckResult.path }}</dd>
|
||||
<dt>Blender</dt>
|
||||
<dd v-if="selectedBlender.source == 'file_association'">
|
||||
Whatever Blender is associated with .blend files
|
||||
(currently "<code>{{ selectedBlender.path }}</code>")
|
||||
</dd>
|
||||
<dd v-if="selectedBlender.source == 'path_envvar'">
|
||||
The command "<code>{{ selectedBlender.input }}</code>" as found on <code>$PATH</code>
|
||||
(currently "<code>{{ selectedBlender.path }}</code>")
|
||||
</dd>
|
||||
<dd v-if="selectedBlender.source == 'input_path'">
|
||||
The command you provided:
|
||||
"<code>{{ selectedBlender.path }}</code>"
|
||||
</dd>
|
||||
</dl>
|
||||
<button @click="confirmWizard" :disabled="isConfirming">Confirm</button>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<footer class="app-footer">
|
||||
@ -73,7 +101,7 @@
|
||||
<script>
|
||||
import NotificationBar from '@/components/footer/NotificationBar.vue'
|
||||
import UpdateListener from '@/components/UpdateListener.vue'
|
||||
import { MetaApi, PathCheckInput } from "@/manager-api";
|
||||
import { MetaApi, PathCheckInput, WizardConfig } from "@/manager-api";
|
||||
import { apiClient } from '@/stores/api-query-count';
|
||||
|
||||
export default {
|
||||
@ -90,17 +118,18 @@ export default {
|
||||
allBlenders: [], // combination of autoFoundBlenders and blenderExeCheckResult.
|
||||
|
||||
autoFoundBlenders: [], // list of api.BlenderPathCheckResult
|
||||
blenderExeFinding: false,
|
||||
isBlenderExeFinding: false,
|
||||
selectedBlender: null, // the chosen api.BlenderPathCheckResult
|
||||
|
||||
customBlenderExe: "",
|
||||
blenderExeChecking: false,
|
||||
isBlenderExeChecking: false,
|
||||
blenderExeCheckResult: null, // api.BlenderPathCheckResult
|
||||
sourceLabels: {
|
||||
file_association: "This Blender runs when you double-click a .blend file.",
|
||||
path_envvar: "This Blender was found on the $PATH environment.",
|
||||
input_path: "You pointed Flamenco to this executable.",
|
||||
}
|
||||
},
|
||||
isConfirming: false,
|
||||
}),
|
||||
computed: {
|
||||
cleanSharedStoragePath() {
|
||||
@ -109,6 +138,10 @@ export default {
|
||||
cleanCustomBlenderExe() {
|
||||
return this.customBlenderExe.trim();
|
||||
},
|
||||
isConfigComplete() {
|
||||
return (this.sharedStorageCheckResult != null && this.sharedStorageCheckResult.is_usable) &&
|
||||
(this.selectedBlender != null && this.selectedBlender.is_usable);
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.findBlenderExePath();
|
||||
@ -135,7 +168,7 @@ export default {
|
||||
},
|
||||
|
||||
findBlenderExePath() {
|
||||
this.blenderExeFinding = true;
|
||||
this.isBlenderExeFinding = true;
|
||||
this.autoFoundBlenders = [];
|
||||
|
||||
console.log("Finding Blender");
|
||||
@ -149,15 +182,24 @@ export default {
|
||||
console.log("Error finding Blender:", error);
|
||||
})
|
||||
.finally(() => {
|
||||
this.blenderExeFinding = false;
|
||||
this.isBlenderExeFinding = false;
|
||||
})
|
||||
},
|
||||
|
||||
checkBlenderExePath() {
|
||||
this.blenderExeChecking = true;
|
||||
const exeToTry = this.cleanCustomBlenderExe;
|
||||
if (exeToTry == "") {
|
||||
// Just erase any previously-found custom Blender executable.
|
||||
this.isBlenderExeChecking = false;
|
||||
this.blenderExeCheckResult = null;
|
||||
this._refreshAllBlenders();
|
||||
return;
|
||||
}
|
||||
|
||||
this.isBlenderExeChecking = true;
|
||||
this.blenderExeCheckResult = null;
|
||||
|
||||
const pathCheck = new PathCheckInput(this.cleanCustomBlenderExe);
|
||||
const pathCheck = new PathCheckInput(exeToTry);
|
||||
console.log("requesting path check:", pathCheck);
|
||||
this.metaAPI.checkBlenderExePath({ pathCheckInput: pathCheck })
|
||||
.then((result) => {
|
||||
@ -172,7 +214,7 @@ export default {
|
||||
console.log("Error checking storage path:", error);
|
||||
})
|
||||
.finally(() => {
|
||||
this.blenderExeChecking = false;
|
||||
this.isBlenderExeChecking = false;
|
||||
})
|
||||
},
|
||||
|
||||
@ -183,6 +225,25 @@ export default {
|
||||
this.allBlenders = this.autoFoundBlenders.concat([this.blenderExeCheckResult]);
|
||||
}
|
||||
},
|
||||
|
||||
confirmWizard() {
|
||||
const wizardConfig = new WizardConfig(
|
||||
this.sharedStorageCheckResult.path,
|
||||
this.selectedBlender,
|
||||
);
|
||||
console.log("saving configuration:", wizardConfig);
|
||||
this.isConfirming = true;
|
||||
this.metaAPI.saveWizardConfig({ wizardConfig: wizardConfig })
|
||||
.then((result) => {
|
||||
console.log("Wizard config saved, reload the page");
|
||||
})
|
||||
.catch((error) => {
|
||||
console.log("Error saving wizard config:", error);
|
||||
})
|
||||
.finally(() => {
|
||||
this.isConfirming = false;
|
||||
})
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
Loading…
x
Reference in New Issue
Block a user