How to trigger script execute after Save&Apply from LUCI page

Hi Luci developers,

I'm currently working on developing OpenWrt Luci for an embedded device. The device's root filesystem isn't persistent, so I need to save configuration to U-Boot using fw_setenv.
Could you guide me on how to call an external script like fw_setenv for Luci's own configuration options (e.g., luci.main.lang in /etc/config/luci and system.system.timezone in the system config) by modifying just the JS or Lua, without altering the UCI source code?

Why won't you replace uci with your own version, that writes to uboot ?

I tried replacing /sbin/uci, but it didn't have any effect on the changes in Luci web.

It's your, unsupported here, binary/script ....

Need more details ... for a complete answer.

You can point uci to look in any directory for config files. If your using the c api
you would do something like...

uci_set_confdir(ctx, "/tmp/mydirectory"); //--> or what/where ever you what to look

using the cli

uci -p /tmp/ set my-config@my-unamed-sec[0].myfoo="bar"
or
uci -p /tmp/ set my-config.mynamed-sec.myfoo="bar"

Note that you must use -c to commit to said config in said directory.. like so

uci -c /tmp/ commit my-config

Calling an external script from luci can be achieved in multiple ways, I prefer to use ubus module, which i can call from scripts, or directly from the js in the webui

Oh... I just want to trigger an external command to save some of luci configuration settings to the uboot environment.
For example, when I go to [System] -> [System] -> [Language and Style], change the [Language] and [Design] options, and then click [Save & Apply] or [Save], luci uses uci to save these settings to /etc/config/luci. I'd like to call fw_setenv at the same time to save the settings to the uboot environment. Can you tell me how to do that without modifying the uci or ubus C source code?

1 Like

An alternative would be to override the handleSave (and/or handleSaveApply depending on what behavior you need) function to add your own implementation which includes the fw_setenv invocation, see How to create a LuCI form without new form.Map - #5 by dannil. Note that if you do this you also need to handle the UCI persisting yourself as well (if that's needed). You can take a look at for example how custom iptables commands in luci-app-firewall are handled.

Here's the link to the docs https://openwrt.github.io/luci/jsapi/LuCI.view.html#handleSave

1 Like

if your still using the old style cbi you can simply do something like this ..


m = Map("myApp", translate("Add Something"),translate("myApp Configuration"))

m.on_after_commit = function()
  os.execute("/init.d/myapp restart") --> or what ever you want to do. 
end

But as jow points out here -->simular topic

procd is a much safer solution, and probably would work with both old and new style cbi.

I tried modifying the handleSaveApply and handleSave functions in system.js, but I'm not sure how to wrap them so I can add my custom script on top of the original save.

I’m not entirely sure… I need to modify the save configuration function in the system.js file of the luci-mod-system module for the 19.07 version of LuCI.

Ya not sure how that'll work. You will need to modify the default save and apply routine to perform your desired actions. In your case, you still want the functionality of the default save and apply ... with the extra bit to fire off your function to write to uboot.

In my experience, its almost always easier to create you own cbi. Judging (but not judging) by your perceived experience, I would suggest you start looking at some of the luci src code. Second, I would focus on the older style cbi for this particular task. Its far more mature and easy to learn because, unlike the new luci , its stable and does not change.

Luci at the moment, is In my opining... is a absolute mess. The documentation seems to have been dismissed as considered irrelevant at this point, so to do any type of development with it, is an absolute nightmare !! What you spend time on coding one day, may be totally gone from the src the following day with no real explanation. I myself have withheld from doing any sort of front-end development with the new luci. I have been waiting patiently for it to stabilize for some time, but unfortunately it doesn't seem likely to happen anytime soon ...

Just my 2cents

there's enough to get you started here -->adding_new_elements_to_luci

you'll need some tools.. and you'll probably want to use windows to do the development, winSCP and putty are very beginner friendly.
putty
winSCP

Once you have installed putty and winscp, open putty and start an ssh session to your target device. First thing you'll need to do is installed the luci-compat pkg.

opkg update
opkg install luci-compat

EDIT:

heres a working demo, using both the new and a touch of the old style luci


install files to router

/www/luci-static/resources/view/myapp/config.js
'use strict';
'require view';
'require form';
'require tools.widgets as widgets';

return view.extend({
	render: function() {
		var m, s, o;

		m = new form.Map('myapp');

		s = m.section(form.TypedSection, 'myapp', _('MyApp Demo'));
		s.anonymous   = true;
		s.addremove   = false;

		o = s.option(form.Flag, 'enable', _('Enable Me If You Please'));
		o.default = true;

		s.option(form.Flag, 'dep_flag', _('Set my flag, to see those who depend on me'));

		o = s.option(form.Value, 'port', _('Option B'));
		o.depends('dep_flag', '0');
		o.datatype    = 'port';
		o.placeholder = 12345;

		o = s.option(widgets.DeviceSelect, 'a_interface', _('Pick an Interface'));
		o.depends('dep_flag', '0');
		o.nocreate    = true;

		o = s.option(form.Value, 'def_opt', _('Option A'), _('default option here)'));
		o.depends('dep_flag', '1');
		o.ucioption = 'interface';
		o.retain = true;

		o = s.option(form.Value, 'user_creds', _('Text Box 1'), _('notice my place holder'));
		o.placeholder = 'username:password';

		o = s.option(form.Value, 'uid', _('Text Box 2'), _('plain ol textbox, but only intergers are allowed'));
		o.datatype = 'uinteger';


		o = s.option(form.DynamicList, 'dyna_list', _('Dynamic List'), _('create dynamic items'));
		o.placeholder = 'some=value';

		o = s.option(form.ListValue, 'debug', _('List Value'), _('Set log a level (default: 7)'));
		o.value('1', _('Error'));
		o.value('3', _('Warning'));
		o.value('7', _('Notice'));
		o.value('15', _('Info'));
		o.default = '7';

		return m.render();
	}
});

/usr/share/luci/menu.d/luci-app-myapp.json
{
	"admin/services/myapp": {
		"title": "New Tab",
		"action": {
			"type": "firstchild"
		},
		"depends": {
			"acl": [ "luci-app-myapp" ],
			"uci": { "myapp": true }
		}
	},

	"admin/services/myapp/myapp": {
		"title": "New Tab",
		"order": 1,
		"action": {
			"type": "template",
			"path": "myapp/view_tab"
		}
	},

	"admin/services/myapp/config": {
		"title": "Config",
		"order": 2,
		"action": {
			"type": "view",
			"path": "myapp/config"
		}
	}
}

/usr/share/rpcd/acl.d/luci-app-myapp.json
{
	"luci-app-myapp": {
		"description": "Grant UCI access for luci-app-myapp",
		"read": {
			"uci": [ "myapp" ]
		},
		"write": {
			"uci": [ "myapp" ]
		}
	}
}
/usr/lib/lua/luci/view/myapp/view_tab.htm
<%+header%>                                                                    
<h1><%:The New Myapp Demo%></h1>                                                      
<%+footer%>
/etc/config/myapp
config myapp
	option dep_flag '1'
	option dep_port '22'
	option a_debug '7'
	option a_interface '@lan'
	option user_creds 'user:password'
	option enable '0'
	option text_box1 'text me'
	option text_box2 '1'
	option dyna_list 'some=option'
	option some_int '1'
	option def_opt 'default'

Now your problem still remains, you'll still have to do as dannil suggested. But now you don't need to worry about the default functionality of the save ..or save and apply buttons. And now there is a least a working, albeit basic demo, of whats needed to create a newer style luci application.

a final note.. once the files have been uploaded, you'll need to either reboot the device or erase the luci-indexcache* in the tmp directory and restart rpcd, before the myapp menu option will appear.

cd /tmp
rm luci-indexcache*
/etc/init.d/rpcd restart

I wrote the following code and made changes acl.d. Now, when I click "Save & Apply," my script runs correctly:

handleSaveApply: async function (ev) {
    await this.__base__.handleSaveApply(ev);
    //console.log('triggered save & apply button');
    try {
        ui.addNotification(null, E('p', _('The system config has been successfully changed.')), 'info');
        await fs.exec("/sbin/testrun", ['fw_setenv test 1234']);
    } catch (error) {
        console.warn(error);
    }
}

But I've run into a new problem.
When I click "Save," then click "Unsaved Changes: 1," and finally click "Save & Apply,"
the handleSaveApply function doesn't work. How can I make sure it still works in this situation?

what in the console? ..right click-->inspect-->console

The code only takes effect if I click "Save & Apply."
If I click "Save," then go to "Unsaved Changes" at the top of the page, and finally click "Save & Apply," the code won't work.
It seems like the only way to ensure everything works as expected is to set handleSave: null to disable the Save button.

Can we not use another button ?? ... I honestly do not know.

Are you using the template i provided or still just hijacking the system pages ?

If your using the template, just remove the save button and functionality or replace it with reset or what ever you want to do there. It shouldn't effect anything else, and unless your actually using the mock cbi for configuration, you don't really care about full uci functionality(rollback unsaved changes and the rest). I am not real sure on how it's done anymore, but it runs much deeper then just updating the config. It will actually rollback the config if the router becomes unreachable or you do not reconnect within a allotted time. So messing around with .. the juice is likely not worth the squeeze.